日本語版
最新ニュース
企業

製品検索とコンテンツ作成のためのシンプルな Opal ツールの構築 – PÄR WISSMARK – 最適化ソリューション アーキテクトおよび開発者

Optimizely Opal ツールを使用すると、AI エージェントが API を簡単に呼び出すことができます。この投稿では、そのうちの 2 つを公開する小さな ASP.NET ホストを構築します。1 つは製品検索用、もう 1 つは CMS コンテンツ作成用です。 具体的な例を見ていきます。Opal を介して 2 つのツール (商品データをクエリする商品検索ツールと、外部 CMS でコンテンツを作成する CMS ページ作成ツール) を公開する小さな ASP.NET ユーティリティ API です。その過程で、アプリケーションがどのように Opal ツールを登録し、それらを下に表示するかを見ていきます。 /opal/*、ワイヤータイプ HttpClient 外部システムのインスタンスを管理し、単純なベアラー トークン バリデーターでツール呼び出しを保護します。目標は、大規模ですべてを歌うバックエンドを構築することではなく、同じプロセス内で 2 つのツールをホストする、クリーンで推論が簡単なユーティリティ…

製品検索とコンテンツ作成のためのシンプルな Opal ツールの構築 – PÄR WISSMARK – 最適化ソリューション アーキテクトおよび開発者

1765721118
2025-12-13 23:27:00

Optimizely Opal ツールを使用すると、AI エージェントが API を簡単に呼び出すことができます。この投稿では、そのうちの 2 つを公開する小さな ASP.NET ホストを構築します。1 つは製品検索用、もう 1 つは CMS コンテンツ作成用です。

具体的な例を見ていきます。Opal を介して 2 つのツール (商品データをクエリする商品検索ツールと、外部 CMS でコンテンツを作成する CMS ページ作成ツール) を公開する小さな ASP.NET ユーティリティ API です。その過程で、アプリケーションがどのように Opal ツールを登録し、それらを下に表示するかを見ていきます。 /opal/*、ワイヤータイプ HttpClient 外部システムのインスタンスを管理し、単純なベアラー トークン バリデーターでツール呼び出しを保護します。目標は、大規模ですべてを歌うバックエンドを構築することではなく、同じプロセス内で 2 つのツールをホストする、クリーンで推論が簡単なユーティリティ API を構築することです。

初期設定

Program.cs Opal ツール ホストをブートストラップします。Opal ランタイムを登録し、製品システムと CMS に型指定された HTTP クライアントを接続し、ベアラー トークンを使用してツール呼び出しを保護するための小さなミドルウェアを構成します。この設定により、プレーンなアプリが Opal 対応のユーティリティ API に変わります。以下のツールを公開します /opal/tools* 認証された呼び出し元に対しては、別個の /opal/discovery エンドポイントが開いているため、エージェントは保護された操作に直接アクセスしなくても、利用可能なツールを検出できます。

ツール統合は、Optimizely.Opal.Tools 0.4.0 NuGet パッケージ(https://www.nuget.org/packages/Optimizely.Opal.Tools/)、ソリューションは .NET 10 プロジェクトに基づいています。

プロジェクトの構造はこんな感じです。

プログラム.cs

using OpalToolsService.Auth;
using OpalToolsService.Services.Cms;
using OpalToolsService.Services.ProductData;
using OpalToolsService.Tools;
using Optimizely.Opal.Tools;

var builder = WebApplication.CreateBuilder(args);

// Authentication
builder.Services.AddSingleton();

// Product data HTTP client
builder.Services.AddHttpClient((service, client) =>
{
    var configuration = service.GetRequiredService();
    ConfigureBaseAddress(client, configuration, "ExternalApi:BaseUrl", "external system");
});

// CMS HTTP client
builder.Services.AddHttpClient((service, client) =>
{
    var configuration = service.GetRequiredService();
    ConfigureBaseAddress(client, configuration, "CmsApi:BaseUrl", "CMS");
});

// Opal tools
builder.Services.AddOpalToolService();
builder.Services.AddOpalTool();
builder.Services.AddOpalTool();

var app = builder.Build();

// Bearer-token protection for tools
var tokenValidator = app.Services.GetRequiredService();

app.Use(async (context, next) =>
{
    var path = context.Request.Path.Value ?? string.Empty;
    var isToolCall = path.StartsWith("/opal/tools", StringComparison.OrdinalIgnoreCase);
    var isDiscovery = path.Equals("/opal/discovery", StringComparison.OrdinalIgnoreCase);

    if (isToolCall && !isDiscovery)
    {
        if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader) || !tokenValidator.IsValid(authHeader.ToString()))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Unauthorized");
            return;
        }
    }

    await next(context);
});

// Map Opal endpoints at /opal/*
app.MapOpalTools("/opal/");

app.Run();

static void ConfigureBaseAddress(HttpClient client, IConfiguration configuration, string configurationKey, string clientLabel)
{
    var baseUrl = configuration[configurationKey];
    if (string.IsNullOrWhiteSpace(baseUrl))
    {
        throw new InvalidOperationException($"Configuration '{configurationKey}' is required for the {clientLabel} client.");
    }

    if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri))
    {
        throw new InvalidOperationException($"Configuration '{configurationKey}' must be an absolute URI for the {clientLabel} client.");
    }

    client.BaseAddress = uri;
}

このクラスは、Opal ツール呼び出しのベアラー トークンの検証を一元管理します。構成から予期されるトークンを読み取ります Opal:BearerToken アプリケーションが起動し、 IsValid 受信した ` をチェックするメソッドAuthorization` 適切にフォーマットされたヘッダー Bearer 設定されたトークンと一致する値。ヘッダーが欠落しているか、形式が正しくない場合、または間違ったトークンが含まれている場合、メソッドは単に戻り値を返します。 false

BearerTokenValidator.cs

namespace OpalToolsService.Auth;

public class BearerTokenValidator(IConfiguration configuration)
{
    private readonly string _expectedToken = configuration["Opal:BearerToken"] ?? throw new InvalidOperationException("Opal:BearerToken must be configured");

    public bool IsValid(string authorizationHeader)
    {
        if (string.IsNullOrWhiteSpace(authorizationHeader))
        {
            return false;
        }

        const string prefix = "Bearer ";

        if (!authorizationHeader.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        var token = authorizationHeader[prefix.Length..].Trim();
        return token == _expectedToken;
    }
}

アプリ設定.json 次のキーが必要になります。 (Opal ベアラー トークンは、Opal 内にツールを追加するときに使用するトークンです。それについては、この記事の後半で説明します。)

"Opal": {
  "BearerToken": "[TOKEN FOR OPAL]"
},
"CmsApi": {
  "BaseUrl": "https://localhost:6001"
},
"ExternalApi": {
  "BaseUrl": "https://localhost:7001"
}

製品データ検索ツール

製品検索フローは、小規模で焦点を絞った入力モデルから始まります。 Models/ProductDataParameters.cs。それは、私たちが関心を持っている 1 つのことを捉えているだけです。 Product 発信者が詳細を必要とする名前。

そのモデルは次に使用されます Tools/ProductDataTool.cs、Opal ツールとして登録されています。ツールは ProductDataParameters インスタンスは、 IProductDataClient、リクエストをそのクライアントに転送します。ツール クラスは意図的にスリムなままです。呼び出しを調整し、Opal の入力/出力を形成しますが、実際のビジネス ロジックは所有しません。

実際の HTTP 対話は次の場所にあります。 Services/ProductData/ProductDataClient.cs。ここは、通常、設定されたメソッドを使用して実際の外部 API を呼び出す場所です。 HttpClient。サンプルでは、クライアントは交換可能なプレースホルダー実装を実行します。次のような既知の製品用です。 sleeping-bag そして tent 代表的な JSON を返し、それ以外の場合は単純なデフォルト項目に戻ります。結果は次のようにマッピングされます Models/ProductDataResult.cs そのため、ツールは常に予測可能な構造化された応答を返します。

スタブ データから実際の製品 API に移行する準備ができたら、クライアント実装を変更するだけで済みます。ツールとその契約はまったく同じままにすることができます。

ProductDataTool.cs

using System.ComponentModel;
using OpalToolsService.Models;
using OpalToolsService.Services.ProductData;
using Optimizely.Opal.Tools;

namespace OpalToolsService.Tools;

public class ProductDataTool(IProductDataClient client)
{
    [OpalTool(Name = "get-product-data")]
    [Description("Get product data from an external system using a product name and return the result.")]
    public async Task

ProductDataParameters.cs

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace OpalToolsService.Models;

public class ProductDataParameters
{
    [Required]
    [Description("Product to look up data in the external system.")]
    public string Product { get; set; } = string.Empty;
}

ProductDataResult.cs

namespace OpalToolsService.Models;

public class ProductDataResult
{
    public required string Product { get; init; }

    public string? Raw { get; init; }

    public DateTimeOffset RetrievedAt { get; init; }
}

IProductDataClient.cs

using OpalToolsService.Models;

namespace OpalToolsService.Services.ProductData;

public interface IProductDataClient
{
    Task GetProductDataAsync(string product, CancellationToken cancellationToken);
}

ProductDataClient.cs

using OpalToolsService.Models;

namespace OpalToolsService.Services.ProductData;

public class ProductDataClient(HttpClient httpClient) : IProductDataClient
{
    public async Task GetProductDataAsync(string product, CancellationToken cancellationToken)
    {
        // Example placeholder call. Replace with your real API endpoint and model.
        // using var response = await httpClient.GetAsync($"/api/items/{Uri.EscapeDataString(key)}", cancellationToken);
        // response.EnsureSuccessStatusCode();
        // var payload = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
        // return payload ?? throw new InvalidOperationException("External system returned an empty payload.");
        
        await Task.CompletedTask; // remove when implementing real call

        // Dummy data for demonstration purposes
        string productData = product switch
        {
            "sleeping-bag" => """
                  {
                        "id": "SB-ARCTIC-LOFTE-300",
                        "sku": "SB-ARCTIC-LOFTE-300",
                        "name": "Nordvind Löfte 300 Sovsäck",
                        "shortDescription": "Varm tresäsongssovsäck för övernattning i svensk natur, komfort ned till -2 °C.",
                        "brand": "Nordvind",
                        "comfortTempC": -2,
                        "limitTempC": -8,
                        "season": "3-season (Nordic)",
                        "weightGrams": 1300,
                        "packedVolumeLiters": 13,
                        "priceSek": 1699,
                        "imageUrl": "https://cdn.example.com/images/sleepingbags/lof­te-300/main_front.jpg"
                          }
                  """,
            "tent" => """
                  {
                    "id": "TENT-NORDLYS-3P",
                    "sku": "TENT-NORDLYS-3P",
                    "name": "Nordvind Nordlys 3P Tält",
                    "shortDescription": "Stabilt 3-säsongstält för upp till tre personer, optimerat för blåsiga nätter i svensk natur.",
                    "brand": "Nordvind",
                    "capacityPersons": 3,
                    "season": "3-season (Nordic)",
                    "weightGrams": 3200,
                    "waterproofRatingMm": 3000,
                    "vestibuleAreaSqM": 1.8,
                    "priceSek": 4299,
                    "imageUrl": "https://cdn.example.com/images/tents/nordlys-3p/main_front.jpg"
                  }
                  """,
            _ => """
                 {
                   "id": "PAD-FJALL-LUFT-5",
                   "sku": "PAD-FJALL-LUFT-5",
                   "name": "Nordvind FjällLuft 5 Liggunderlag",
                   "shortDescription": "Lätt och varmt uppblåsbart liggunderlag med R-värde 4,5 för övernattningar i svensk vår, sommar och höst.",
                   "brand": "Nordvind",
                   "rValue": 4.5,
                   "lengthCm": 183,
                   "widthCm": 52,
                   "thicknessCm": 5,
                   "weightGrams": 520,
                   "priceSek": 1199,
                   "imageUrl": "https://cdn.example.com/images/pads/fjallluft-5/main_front.jpg"
                 }
                 """
        };

        return new ProductDataResult
        {
            Product = product,
            Raw = productData,
            RetrievedAt = DateTimeOffset.UtcNow
        };
    }
}

CMSページ作成ツール

CMS ツールは商品検索と同じパターンに従いますが、コンテンツの作成が対象です。の入力モデル Models/CreateCmsPageParameters.cs Opal エージェントが送信するリクエストの形式を定義します。 Heading そして Body、さらにオプションで Preamble。でのツールの実装 Tools/CmsPageTool.cs これらのパラメータを単に受け入れて、 ICmsClient、実際の作業をそのクライアントに委任します。

力仕事が起こるのは、 Services/Cms/CmsClient.cs、からペイロードを構築します CreateCmsPageParameters、必要な CMS API トークン ヘッダーを追加し、CMS エンドポイントにポストし、応答をデシリアライズします。 CreateCmsPageResult。その結果モデル (Models/CreateCmsPageResult.cs) 呼び出し元に、作成されたページ URL とタイムスタンプまたは同様のメタデータが与えられます。 Opal エージェントの観点から見ると、コントラクトは単純です。「指定された見出し、プリアンブル、本文、ページを作成して URL を返す」というものですが、ルーティング、認証、シリアル化の詳細はすべてクライアント内にきちんと保持されています。

この場合、ツールが呼び出すとページを作成する単純な API エンドポイントを Optimizely 12 PaaS Web サイトに作成しました。そのコードも紹介します。

CmsPageTool.cs

using System.ComponentModel;
using OpalToolsService.Models;
using OpalToolsService.Services.Cms;
using Optimizely.Opal.Tools;

namespace OpalToolsService.Tools;

public class CmsPageTool(ICmsClient cmsClient)
{
    [OpalTool(Name = "create-cms-page")]
    [Description("Creates a page in the CMS with a heading, preamble and body of text.")]
    public async Task
CreateCmsPageParameters.cs
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace OpalToolsService.Models;

public class CreateCmsPageParameters
{
    [Required]
    [Description("Page heading")]
    public string Heading { get; set; } = string.Empty;

    [Description("Short preamble for the page")]
    public string? Preamble { get; set; }

    [Required]
    [Description("Main body content of the page, e.g. HTML or markdown")]
    public string Body { get; set; } = string.Empty;
}
CreateCmsPageResult.cs
namespace OpalToolsService.Models;

public class CreateCmsPageResult
{
    public required string Url { get; init; }

    public DateTimeOffset CreatedAt { get; init; }
}
ICmsClient.cs
using OpalToolsService.Models;

namespace OpalToolsService.Services.Cms;

public interface ICmsClient
{
    Task CreatePageAsync(
        string heading,
        string? preamble,
        string body,
        CancellationToken cancellationToken);
}
CmsClient.cs
using System.Net.Http.Json;
using OpalToolsService.Models;

namespace OpalToolsService.Services.Cms;

public class CmsClient(HttpClient httpClient) : ICmsClient
{
    public async Task CreatePageAsync(string heading, string? preamble, string body, CancellationToken cancellationToken)
    {
        var payload = new { heading, preamble, body };

        using var request = new HttpRequestMessage(HttpMethod.Post, "[CMS API ENDPOINT HERE]");
        request.Content = JsonContent.Create(payload);
        request.Headers.Add("X-Api-Token", "[TOKEN FOR COMMUNICATING WITH CMS]");

        using var response = await httpClient.SendAsync(request, cancellationToken);
        response.EnsureSuccessStatusCode();

        var created = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);

        return created ?? throw new InvalidOperationException("CMS did not return a page result.");
    }
}

OpalToolsService.http を使用したツール エンドポイントのテスト

Opal を経由せずにツールの健全性を迅速にチェックするには、次のコマンドを使用してエンドポイントを直接呼び出すことができます。 .http ファイル。の OpalToolsService.http 以下のスクリプトを使用すると、検出を実行し、ベアラー トークンを使用して各ツールを呼び出し、不正な呼び出しが正しく拒否されることを確認できます。 401

@host=http://localhost:5044
@token=074e44df-ec3f-4a72-bf52-b169de59ad19

### 1) Discovery (no bearer token)
GET {{host}}/opal/discovery
Accept: application/json

### 2) get product data tool (with bearer token)
POST {{host}}/opal/tools/get-product-data
Authorization: Bearer {{token}}
Content-Type: application/json
Accept: application/json

{
  "parameters": {
    "product": "tent"
  }
}

### 3) create-cms-page tool (with bearer token)
POST {{host}}/opal/tools/create-cms-page
Authorization: Bearer {{token}}
Content-Type: application/json
Accept: application/json

{
  "parameters": {
    "heading": "Test page created from Opal tool",
    "preamble": "This is a short intro/preamble created during local testing.",
    "body": "Here is the full body of the CMS page. You can put markdown, HTML, or plain text here."
  }
}

### 5) get product data without token (should return 401)
POST {{host}}/opal/tools/get-product-data
Content-Type: application/json
Accept: application/json

{
  "parameters": {
    "product": "should-fail-unauthorized"
  }
}

CMS エンドポイント

このサンプルの CMS 統合は意図的に汎用的です。 API エンドポイントは、あくまで「HTTP 呼び出しを受け付ける CMS」です。の CmsClient JSON ペイロード (ヘッダー、プリアンブル、本文) を設定された URL にポストし、CMS が期待する API トークンを追加します。これは、特定のベンダーや製品に縛られないことを意味します。Opal ツールやそのコントラクトを変更することなく、クライアントでベース URL、ルート、認証ヘッダー規則を調整することで、コンテンツ作成用の HTTP エンドポイントを公開する CMS やコンテンツ チャネルを接続できます。ただし、この場合は、テスト目的で Optimizely 12 ソリューション内に API コントローラーを作成しただけです。

[ApiController]
[Route("api/test")]
public class TestApiController(IContentRepository contentRepository, UrlResolver urlResolver) : ControllerBase
{
    [HttpPost]
    [Route("article")]
    public IActionResult CreateArticle([FromBody] CreatePageRequest request)
    {
        var expectedToken = "[TOKEN FOR COMMUNICATING WITH CMS]";
        var requestToken = Request.Headers["X-Api-Token"].ToString();
        if (string.IsNullOrEmpty(requestToken) || requestToken != expectedToken)
        {
            return Unauthorized("Invalid token.");
        }

        if (string.IsNullOrWhiteSpace(request.Heading))
        {
            return BadRequest("Heading is required.");
        }

        if (string.IsNullOrWhiteSpace(request.Body))
        {
            return BadRequest("Body is required.");
        }

        var parent = ContentReference.StartPage;
        var page = contentRepository.GetDefault(parent);

        page.Name = request.Heading;
        page.Heading = request.Heading;
        page.PageHeader.Preamble = request.Preamble ?? string.Empty;
        page.PlainText = new XhtmlString(request.Body);

        var savedRef = contentRepository.Save(
            page,
            SaveAction.Publish,
            AccessLevel.NoAccess);

        var savedPage = contentRepository.Get(savedRef);
        var publicUrl = urlResolver.GetUrl(savedRef);

        var response = new CreatePageResponse
        {
            Url = publicUrl,
            Created = savedPage.Created,
            Id = savedPage.ContentLink.ID
        };

        return Ok(response);
    }
}

public class CreatePageRequest
{
    public string? Heading { get; set; }
    public string? Preamble { get; set; }
    public string? Body { get; set; }
}

public class CreatePageResponse
{
    public string? Url { get; set; }
    public DateTime Created { get; set; }
    public int Id { get; set; }
}

オパールではどのように見えるか

API が実行されたら、Opal にツールとして登録できます。名前を選択し、ホストされている場所に指定します /opal エンドポイントからベアラー トークンを貼り付けます。 appsettings.json。保存後、ツールがリストに表示されるので、ツールをエージェントにアタッチして、Opal UI から試すことができます。テスト中に API を変更した場合は、「レジストリ」に移動し、「同期」をクリックしてツール定義を更新します。

ツールリスト、 ツールレジストリを追加

ツールレジストリの追加

追加すると、リストに表示されるはずです。

ツールのレジストリ。

テスト中にツールを更新する必要がある場合は、次の場所に移動してください。 レジストリ そしてクリックしてください 同期

チャットの質問。

製品ツールを使用した場合の結果。

チャットの質問

そしてページはCMS内で作成されます。

最後に

一歩戻って、このサンプルは、以下のツールを公開する Opal 対応ソリューションを提供します。 /opal/*、ロックダウンします /opal/tools* ミドルウェアを少し追加すると BearerTokenValidator、型指定された HTTP クライアントを通じて外部システムと通信します。それに加えて、次の 2 つの具体的な機能を追加しました。 ProductDataTool 製品データを取得するためと、 CmsPageTool CMSページの作成に使用します。構造は意図的にシンプルになっています。ツールは入力と出力に焦点を当て、クライアントは醜い詳細を処理し、モデルは契約を誠実に保ちます。

主なアイデアはかなり再利用可能です。エッジで認証を維持し、ツールを薄く保ち、アーキテクチャの残りの部分が安定するまで外部システムをスタブ化することを恐れないでください。ここから、さらにツール (在庫チェック、注文履歴、さまざまなコンテンツ タイプ) を追加することは、基本的には洗い流しと繰り返しです。

これは実際にはこれだけで、たまたま Opal と非常にうまく連携する、小さくて退屈な API です。

#製品検索とコンテンツ作成のためのシンプルな #Opal #ツールの構築 #PÄR #WISSMARK #最適化ソリューション #アーキテクトおよび開発者

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。