コンテンツにスキップ

ラボ BAF3 - Mistral AI を使用したビジョン分析の追加

このラボでは、保険請求の損傷写真を分析するために AI ビジョン機能を追加し、 mistral-medium-2505 モデルを使用します。エージェントは車両損傷を自動で評価し、修理費用を見積もり、安全上の懸念事項を写真から特定できるようになります。

学習内容
  • Mistral AI ビジョンモデルを統合してマルチモーダル分析を行う方法
  • Azure Blob Storage から画像データを処理する方法
  • AI を活用した損傷評価の実装
  • AI 生成の分析結果に対する承認ワークフローの作成

概要

ラボ BAF2 では、Azure AI Search Knowledgebases を使用して請求検索を追加しました。今回はマルチモーダルな AI ビジョン機能を強化し、損傷写真を分析して詳細な評価レポートを提供します。

Vision Service は Azure AI Services にデプロイされた mistral-medium-2505 モデルを使用し、画像を分析して損傷評価を含む構造化された JSON 応答を生成します。

演習 1: 前提条件の更新

ビジョン分析を追加する前に、Mistral ビジョンモデルをデプロイする必要があります。

Step 1: Microsoft Foundry で Mistral ビジョンモデルをデプロイする

1️⃣ Microsoft Foundry にサインインします。

2️⃣ プロジェクトを開くか、新規作成します。

3️⃣ Models + endpointsDeploy modelDeploy base model に移動します。

4️⃣ mistral-medium-2505 を検索してデプロイします:

  • Model: mistral-medium-2505
  • Deployment name: mistral-medium-2505(この名前を正確に使用)
  • Version: 最新

5️⃣ デプロイ完了まで待機します(約 2~3 分)。

6️⃣ デプロイ情報をメモします:

  • gpt-4.1 デプロイと 同じエンドポイント を使用
  • 他のモデルと 同じ API キー を使用
  • モデル名 のみ mistral-medium-2505 に変更

Step 2: Azure Storage アカウントとコンテナーを作成する

損傷写真用のストレージアカウントとコンテナーを作成します。

1️⃣ Azure Portal にサインインします。

2️⃣ 新しい Storage Account を作成します:

  • 上部検索バーで「Storage accounts」を検索
  • + Create をクリック
  • 詳細を入力:

    • Subscription: 対象のサブスクリプション
    • Resource group: 既存リソースグループを使用
    • Storage account name: 一意の名前(例: zavadamagestorage + イニシャル)
    • Region: AI サービスと同じリージョン
    • Performance: Standard
    • Redundancy: Locally-redundant storage (LRS)
  • Review + CreateCreate をクリック

  • デプロイ完了まで待機(約 1~2 分)

3️⃣ 新しいストレージアカウントに移動し、匿名アクセスを有効化:

  • 左メニューの SettingsConfiguration を選択
  • Allow Blob anonymous accessEnabled に設定
  • 上部の Save をクリック
パブリックアクセスに必須

ストレージアカウントレベルで匿名 Blob アクセスを有効化しないと、個別コンテナーでのパブリックアクセス設定が機能しません。

4️⃣ 左メニューの Data storage 配下から Containers を選択。

5️⃣ 上部の + Container をクリック。

5️⃣ 新しいコンテナーを設定:

  • Name: claim-photos
  • Public access level: Blob (anonymous read access for blobs only)
  • Create をクリック

6️⃣ 構成用にストレージアカウントの詳細をコピー:

  • 左メニューの Access keys
  • key1 または key2 の Connection string をコピー
  • Storage account name をコピー
なぜ Blob パブリックアクセス?

コンテナーを「Blob」パブリックアクセスレベルに設定することで:

  • AI ビジョンモデルが個別画像 URL に直接アクセス可能
  • チャットインターフェイスで画像を表示
  • 読み取り専用で認証不要

個別 Blob URL のみが公開され、コンテナー一覧は非公開のためセキュリティを保持します。

Step 3: 構成を更新する

ビジョンモデルと Blob Storage の構成を環境変数に追加します。

1️⃣ .env.local ファイルを開きます。

2️⃣ ビジョンモデルと Blob Storage の構成を追加します:

# Vision & Fraud analysis model (mistral-medium-2505)
VISION_MODEL_NAME=mistral-medium-2505

# Storage
AZURE_STORAGE_ACCOUNT_NAME=YOUR-STORAGE-ACCOUNT

# Blob Storage for Damage Photos
BLOB_STORAGE_CONTAINER_NAME=claim-photos
BLOB_STORAGE_BASE_URL=https://YOUR-STORAGE-ACCOUNT.blob.core.windows.net

3️⃣ .env.local.user ファイルを開きます。

4️⃣ Blob Storage 接続文字列を追加します:

# Storage
SECRET_AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=YOUR-STORAGE-ACCOUNT;AccountKey=YOUR-STORAGE-KEY;EndpointSuffix=core.windows.net
構成メモ
  • SECRET_AZURE_STORAGE_CONNECTION_STRING: Azure Portal からコピーした接続文字列
  • AZURE_STORAGE_ACCOUNT_NAME: ストレージアカウント名
  • BLOB_STORAGE_CONTAINER_NAME: claim-photos 固定
  • BLOB_STORAGE_BASE_URL: YOUR-STORAGE-ACCOUNT を実際のストレージアカウント名に置換

演習 2: Vision と Storage サービスを作成する

AI ビジョン分析と Blob Storage を扱うサービスを作成します。

Step 1: VisionService と BlobStorageService を作成する

コードの概要

VisionService: Mistral AI ビジョンモデルで損傷写真を分析

  • mistral-medium-2505 デプロイに接続
  • 画像バイトを受け取り、構造化された損傷分析 JSON を生成
  • 損傷タイプ、深刻度、費用見積もり、安全上の懸念、修理推奨を提供
  • 失敗時のフォールバックロジック
  • 一貫性のある事実ベース回答のため温度 0.3 を使用

BlobStorageService: Azure Blob Storage で損傷写真を管理

  • 請求番号ごとにタイムスタンプ付きで写真をアップロード
  • AI 分析用に写真をダウンロード
  • 不要になった写真を削除
  • MIME タイプ (JPEG, PNG, GIF, BMP, WebP) を自動検出
  • 直接 URL アクセス用にパブリック Blob アクセスを設定

これは完全な実装で、後から追加するメソッドはありません。

1️⃣ src/Services/VisionService.cs を新規作成し、完全な実装を追加:

using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.ClientModel;
using System.Text.Json;
using OpenAI.Chat;

namespace InsuranceAgent.Services;

/// <summary>
/// Service for analyzing images using Mistral AI model capabilities
/// </summary>
public class VisionService
{
    private readonly ChatClient _chatClient;
    private readonly IConfiguration _configuration;
    private readonly ILogger<VisionService> _logger;

    public VisionService(
        IConfiguration configuration,
        ILogger<VisionService> logger)
    {
        _configuration = configuration;
        _logger = logger;

        // Use shared endpoint and API key, but different model for vision analysis
        var endpoint = configuration["AIModels:Endpoint"]
            ?? throw new InvalidOperationException("AIModels:Endpoint not configured");
        var apiKey = configuration["AIModels:ApiKey"]
            ?? throw new InvalidOperationException("AIModels:ApiKey not configured");
        var deployment = configuration["AIModels:VisionModel:Name"] 
            ?? throw new InvalidOperationException("AIModels:VisionModel:Name not configured");

        _logger.LogInformation("🔍 VisionService Configuration:");
        _logger.LogInformation("   Endpoint: {Endpoint}", endpoint);
        _logger.LogInformation("   Deployment: {DeploymentName}", deployment);

        var azureClient = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey));
        _chatClient = azureClient.GetChatClient(deployment);
    }

    /// <summary>
    /// Analyzes an insurance claim damage photo using Mistral AI model
    /// </summary>
    /// <param name="imageBytes">The image file bytes</param>
    /// <param name="fileName">The image file name</param>
    /// <returns>Detailed damage analysis</returns>
    public async Task<DamageAnalysisResult> AnalyzeDamagePhotoAsync(byte[] imageBytes, string fileName)
    {
        try
        {
            _logger.LogInformation("Starting vision analysis for {FileName} ({Size} bytes)", fileName, imageBytes.Length);

            // Create the vision analysis prompt
            var prompt = @"You are an expert insurance claims adjuster analyzing damage photos. 

Analyze this image and provide a detailed assessment in the following JSON format:

{
  ""damageType"": ""<type of damage: water, fire, storm, hail, flood, etc.>"",
  ""severity"": ""<Low, Medium, High, or Critical>"",
  ""detailedDescription"": ""<detailed description of what you see in the image>"",
  ""affectedAreas"": [""<list of affected areas/structures as array>""],
  ""estimatedRepairCost"": <numeric estimate in dollars>,
  ""safetyConcerns"": ""<any immediate safety concerns>"",
  ""repairRecommendations"": ""<recommended repair actions>"",
  ""urgency"": ""<Immediate, Within 1 week, Within 1 month, Non-urgent>"",
  ""requiresSpecialist"": <true/false>,
  ""specialistType"": ""<type of specialist needed, if any>""
}

Be specific, professional, and focus on details that would help with claims processing.";

            // Create chat completion request with image
            var messages = new List<ChatMessage>
            {
                new UserChatMessage(
                    ChatMessageContentPart.CreateTextPart(prompt),
                    ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(imageBytes), GetMimeType(fileName))
                )
            };

            var chatOptions = new ChatCompletionOptions
            {
                Temperature = 0.3f, // Lower temperature for more consistent analysis
                ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat()
            };

            // Call Mistral AI model for vision analysis
            var response = await _chatClient.CompleteChatAsync(messages, chatOptions);

            var analysisJson = response.Value.Content[0].Text ?? "{}";
            _logger.LogInformation("Vision analysis completed for {FileName}", fileName);
            _logger.LogDebug("Analysis result: {Analysis}", analysisJson);

            // Parse the JSON response
            var result = JsonSerializer.Deserialize<DamageAnalysisResult>(analysisJson, new JsonSerializerOptions 
            { 
                PropertyNameCaseInsensitive = true 
            });

            if (result == null)
            {
                _logger.LogWarning("Failed to parse vision analysis result for {FileName}", fileName);
                return CreateFallbackResult(fileName);
            }

            result.AnalyzedAt = DateTime.UtcNow;
            result.FileName = fileName;

            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error analyzing image {FileName}", fileName);
            return CreateFallbackResult(fileName);
        }
    }

    private string GetMimeType(string fileName)
    {
        var extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".gif" => "image/gif",
            ".webp" => "image/webp",
            _ => "image/jpeg"
        };
    }

    private DamageAnalysisResult CreateFallbackResult(string fileName)
    {
        return new DamageAnalysisResult
        {
            FileName = fileName,
            DamageType = "Unknown",
            Severity = "Medium",
            DetailedDescription = "Unable to analyze image automatically. Manual review required.",
            AffectedAreas = new[] { "Unknown" },
            EstimatedRepairCost = 0,
            SafetyConcerns = "Please review manually",
            RepairRecommendations = "Manual assessment required",
            Urgency = "Within 1 week",
            RequiresSpecialist = true,
            SpecialistType = "Insurance Adjuster",
            AnalyzedAt = DateTime.UtcNow
        };
    }
}

/// <summary>
/// Result of damage analysis using Mistral AI model
/// </summary>
public class DamageAnalysisResult
{
    public string FileName { get; set; } = "";
    public string DamageType { get; set; } = "";
    public string Severity { get; set; } = "";
    public string DetailedDescription { get; set; } = "";
    public string[] AffectedAreas { get; set; } = Array.Empty<string>();
    public double EstimatedRepairCost { get; set; }
    public string SafetyConcerns { get; set; } = "";
    public string RepairRecommendations { get; set; } = "";
    public string Urgency { get; set; } = "";
    public bool RequiresSpecialist { get; set; }
    public string SpecialistType { get; set; } = "";
    public DateTime AnalyzedAt { get; set; }
}

2️⃣ src/Services/BlobStorageService.cs を新規作成し、完全な実装を追加:

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Configuration;

namespace InsuranceAgent.Services;

/// <summary>
/// Service for uploading and managing damage photos in Azure Blob Storage
/// </summary>
public class BlobStorageService
{
    private readonly BlobServiceClient _blobServiceClient;
    private readonly string _containerName;
    private readonly string _baseUrl;

    public BlobStorageService(IConfiguration configuration)
    {
        var connectionString = configuration["SECRET_AZURE_STORAGE_CONNECTION_STRING"]
            ?? throw new InvalidOperationException("SECRET_AZURE_STORAGE_CONNECTION_STRING not configured");

        _containerName = configuration["BLOB_STORAGE_CONTAINER_NAME"] ?? "claim-photos";
        _baseUrl = configuration["BLOB_STORAGE_BASE_URL"] ?? "";

        _blobServiceClient = new BlobServiceClient(connectionString);
    }

    /// <summary>
    /// Uploads a damage photo and returns the public URL
    /// </summary>
    /// <param name="claimNumber">The claim number for organizing photos</param>
    /// <param name="imageBytes">The image file bytes</param>
    /// <param name="fileName">Original filename</param>
    /// <returns>Public URL to the uploaded blob</returns>
    public async Task<string> UploadDamagePhotoAsync(string claimNumber, byte[] imageBytes, string fileName)
    {
        // Ensure container exists
        var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
        await containerClient.CreateIfNotExistsAsync(PublicAccessType.Blob);

        // Generate unique blob name: {claimNumber}/{timestamp}_{filename}
        var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
        var blobName = $"{claimNumber}/{timestamp}_{fileName}";

        var blobClient = containerClient.GetBlobClient(blobName);

        // Check if blob already exists
        if (await blobClient.ExistsAsync())
        {
            Console.WriteLine($"⏭️  Blob already exists: {blobName}");
            return blobClient.Uri.ToString();
        }

        // Set content type based on file extension
        var contentType = GetContentType(fileName);
        var blobHttpHeaders = new BlobHttpHeaders { ContentType = contentType };

        // Upload the image
        using var stream = new MemoryStream(imageBytes);
        await blobClient.UploadAsync(stream, new BlobUploadOptions
        {
            HttpHeaders = blobHttpHeaders
        });

        // Return public URL
        return blobClient.Uri.ToString();
    }

    /// <summary>
    /// Downloads a damage photo by URL
    /// </summary>
    public async Task<byte[]> DownloadPhotoAsync(string blobUrl)
    {
        var blobClient = new BlobClient(new Uri(blobUrl));
        var response = await blobClient.DownloadContentAsync();
        return response.Value.Content.ToArray();
    }

    /// <summary>
    /// Deletes a damage photo by URL
    /// </summary>
    public async Task<bool> DeletePhotoAsync(string blobUrl)
    {
        try
        {
            var blobClient = new BlobClient(new Uri(blobUrl));
            await blobClient.DeleteIfExistsAsync();
            return true;
        }
        catch
        {
            return false;
        }
    }

    private string GetContentType(string fileName)
    {
        var extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".gif" => "image/gif",
            ".bmp" => "image/bmp",
            ".webp" => "image/webp",
            _ => "application/octet-stream"
        };
    }
}

演習 3: KnowledgeBaseService をビジョン機能で拡張する

VisionPlugin を作成する前に、KnowledgeBaseService にプラグインが使用するメソッドを追加します。これによりプラグイン作成時の依存関係が満たされます。

Step 1: KnowledgeBaseService コンストラクターを更新する

変更点

KnowledgeBaseService コンストラクターに、損傷写真をアップロードするためのオプションの BlobStorageService パラメーターを追加します。コンストラクターはラボ BAF2 ですでに IConfiguration のみを受け取るよう簡略化されており、今回は BlobStorageService を追加するだけです。

1️⃣ src/Services/KnowledgeBaseService.cs を開きます。

2️⃣ KnowledgeBaseService コンストラクターを見つけ、以下のコードブロックに置き換えます:

private readonly BlobStorageService? _blobStorageService;

public KnowledgeBaseService(IConfiguration configuration, BlobStorageService? blobStorageService = null)
{
    _configuration = configuration;

    _searchEndpoint = configuration["AZURE_AI_SEARCH_ENDPOINT"]
        ?? throw new InvalidOperationException("AZURE_AI_SEARCH_ENDPOINT not configured");
    _searchApiKey = configuration["SECRET_AZURE_AI_SEARCH_API_KEY"]
        ?? throw new InvalidOperationException("SECRET_AZURE_AI_SEARCH_API_KEY not configured");

    _aiEndpoint = configuration["MODELS_ENDPOINT"]
        ?? throw new InvalidOperationException("MODELS_ENDPOINT not configured");
    _aiApiKey = configuration["AIModels:ApiKey"]
        ?? throw new InvalidOperationException("AIModels:ApiKey not configured");
    _embeddingModel = configuration["EMBEDDING_MODEL_NAME"]
        ?? "text-embedding-ada-002";

    var credential = new AzureKeyCredential(_searchApiKey);
    _indexClient = new SearchIndexClient(new Uri(_searchEndpoint), credential);
    _retrievalClient = new KnowledgeBaseRetrievalClient(new Uri(_searchEndpoint), KnowledgeBaseName, credential);

    _openAIClient = new AzureOpenAIClient(new Uri(_aiEndpoint), new AzureKeyCredential(_aiApiKey));

    _blobStorageService = blobStorageService;
}
コンストラクターの変更

コンストラクターはオプションの BlobStorageService パラメーターを受け取り、データインデックス時に損傷写真を Azure Blob Storage へアップロードするために使用します。

Step 2: ビジョン関連メソッドを追加する

コードの概要

GetClaimImageUrlAsync: imageUrl フィールドを直接検索インデックスから取得。単純なフィールド取得には RetrieveAsync より効率的。nullable string を返す。

UploadSampleDamagePhotosAsync: 写真アップロードの完全ワークフロー

  • JSON ファイルから請求を読み込み
  • policyholder 名のパターン (firstname-lastname-*.jpg) で画像をマッチ
  • Blob Storage に請求番号ごとにアップロード
  • claim-documents-index に検索可能ドキュメントを作成
  • claims インデックスを imageUrl フィールドで更新
  • 初回起動時に自動実行

これにより 35 枚のサンプル損傷写真がすぐにアップロード・インデックス化されます。

KnowledgeBaseService クラスの終了直前に、以下の 2 つの新メソッドを追加します:

GetClaimImageUrlAsync メソッド - 請求インデックスから画像 URL を直接取得:

/// <summary>
/// Gets the damage photo URL for a specific claim
/// Checks both claims index and claim-documents index
/// </summary>
/// <param name="claimNumber">The claim number to retrieve the image for</param>
/// <returns>The image URL or null if not found</returns>
public async Task<string?> GetClaimImageUrlAsync(string claimNumber)
{
    // Check claims index for imageUrl (still stored there for direct access)
    var claimsClient = _indexClient.GetSearchClient(ClaimsIndex);

    var searchOptions = new SearchOptions
    {
        Filter = $"claimNumber eq '{claimNumber}'",
        Size = 1,
        Select = { "imageUrl" }
    };

    var searchResults = await claimsClient.SearchAsync<SearchDocument>("*", searchOptions);

    await foreach (var searchResult in searchResults.Value.GetResultsAsync())
    {
        var doc = searchResult.Document;
        if (doc.ContainsKey("imageUrl") && doc["imageUrl"] != null)
        {
            return doc["imageUrl"].ToString();
        }
    }

    return null;
}
RetrieveAsync を使わない理由

シンプルなフィールド取得のみの場合、Knowledge Base の RetrieveAsync API を使うよりも検索インデックスを直接クエリする方が効率的です。

UploadSampleDamagePhotosAsync メソッド - infra/img/sample-images から損傷写真をアップロードする完全実装:

/// <summary>
/// Uploads sample damage photos to blob storage and indexes them in Azure AI Search
/// Reads claims from claims.json, matches images from infra/img/sample-images by policyholder name,
/// uploads to blob storage, creates searchable documents, and updates claims with imageUrl
/// </summary>
private async Task UploadSampleDamagePhotosAsync()
{
    if (_blobStorageService == null) return;

    Console.WriteLine("📸 Uploading sample damage photos to blob storage and indexing...");

    var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
    var dataPath = Path.Combine(baseDirectory, "infra", "data", "sample-data");
    var filePath = Path.Combine(dataPath, "claims.json");
    var imagesPath = Path.Combine(baseDirectory, "infra", "img", "sample-images");

    if (!File.Exists(filePath))
    {
        Console.WriteLine($"⚠️  Claims data file not found: {filePath}");
        return;
    }

    if (!Directory.Exists(imagesPath))
    {
        Console.WriteLine($"⚠️  Sample images directory not found: {imagesPath}");
        return;
    }

    var json = await File.ReadAllTextAsync(filePath);
    var claimsData = System.Text.Json.JsonSerializer.Deserialize<List<System.Text.Json.JsonElement>>(json);

    if (claimsData == null || !claimsData.Any())
    {
        Console.WriteLine("⚠️  No claims data to process");
        return;
    }

    var uploadCount = 0;
    var claimsClient = _indexClient.GetSearchClient(ClaimsIndex);
    var claimsToUpdate = new List<SearchDocument>();

    Console.WriteLine($"📋 Processing {claimsData.Count} total claims for damage photos...");
    Console.WriteLine($"📸 Uploading to blob storage...");

    foreach (var claimData in claimsData)
    {
        var claimNumber = claimData.GetProperty("claimNumber").GetString() ?? "";
        var policyholderName = claimData.GetProperty("policyholderName").GetString() ?? "";

        // Build the expected image filename based on policyholder name
        // Format: firstname-lastname-description.jpg (e.g., "ajlal-nueimat-deer-collision.jpg")
        var nameKey = policyholderName.ToLower().Replace(" ", "-");

        // Find matching image file in sample-images directory
        var imageFiles = Directory.GetFiles(imagesPath, $"{nameKey}*.jpg");

        if (imageFiles.Length == 0)
        {
            Console.WriteLine($"⏭️  No image found for {claimNumber} ({policyholderName})");
            continue;
        }

        var imageFile = imageFiles[0];
        var fileName = Path.GetFileName(imageFile);

        Console.WriteLine($"📸 Processing damage photo for claim {claimNumber}: {fileName}");

        try
        {
            // Read image from local file
            var imageBytes = await File.ReadAllBytesAsync(imageFile);

            // Upload to blob storage - blob URL will be directly accessible for viewing and AI analysis
            var blobUrl = await _blobStorageService.UploadDamagePhotoAsync(claimNumber, imageBytes, fileName);

            // Update the claim record with the image URL for direct access
            claimsToUpdate.Add(new SearchDocument
            {
                ["id"] = claimNumber,
                ["imageUrl"] = blobUrl
            });

            uploadCount++;
            Console.WriteLine($"✅ Uploaded photo for {claimNumber}: {blobUrl}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"⚠️  Failed to upload photo for {claimNumber}: {ex.Message}");
        }
    }

    // Update claims with image URLs
    if (claimsToUpdate.Any())
    {
        Console.WriteLine($"📝 Updating {claimsToUpdate.Count} claims with image URLs...");
        var claimsBatch = IndexDocumentsBatch.MergeOrUpload(claimsToUpdate);
        await claimsClient.IndexDocumentsAsync(claimsBatch);
        Console.WriteLine($"✅ Updated {claimsToUpdate.Count} claims with image URLs");
    }

    if (uploadCount > 0)
    {
        Console.WriteLine($"📸 Total: Uploaded {uploadCount} damage photos to blob storage");
    }
    else
    {
        Console.WriteLine("⚠️  No damage photos found to upload");
    }
}
コードの詳細

UploadSampleDamagePhotosAsync は以下を行います:

  • 請求データの読み込み: infra/data/sample-data/claims.json から全請求を読み込み
  • 画像マッチング: infra/img/sample-images で policyholder 名パターンに一致する画像を検索
  • Blob Storage へアップロード: BlobStorageService を使用し請求番号階層で各画像をアップロード
  • 請求更新: 既存の請求レコードに imageUrl フィールドをマージし claims-index を更新
  • 自動実行: IndexSampleDataAsync 呼び出し時に実行

これにより: - ✅ 画像を永続的に保存 - ✅ claims インデックスに imageUrl を追加 - ✅ 画像が即座にビジョン分析可能

Step 3: IndexSampleDataAsync メソッドを更新する

IndexSampleDataAsync メソッドを見つけ、写真アップロード呼び出しを追加する以下のコードに更新します:

public async Task IndexSampleDataAsync()
{
    await IndexClaimsDataAsync();

    // Upload damage photos to blob storage if BlobStorageService is available
    if (_blobStorageService != null)
    {
        await UploadSampleDamagePhotosAsync();
    }

    Console.WriteLine("✅ Sample data indexed successfully");
}

演習 4: Vision プラグインを作成する

KnowledgeBaseService に必要なメソッドが追加されたので、これらを使用する VisionPlugin を作成します。

Step 1: 完全な VisionPlugin を作成する

コードの概要

VisionPlugin は AI ビジョン分析機能を提供します:

  • ShowDamagePhoto: 請求から損傷写真を取得し、チャット内で表示
  • AnalyzeAndShowDamagePhoto: 写真をダウンロードし、Mistral AI で分析。構造化結果を抽出し、フォーマットして提示
  • ApproveAnalysis / RejectAnalysis: AI 分析に対するユーザー承認フローを処理(本番では DB 更新やワークフローをトリガー)
  • NotifyUserAsync: 長時間処理中のリアルタイムストリーミング更新用ヘルパー

各メソッドは [Description] 属性を持ち、AI エージェントがユーザーの意図に応じて呼び出せるようにします。

1️⃣ src/Plugins/VisionPlugin.cs を新規作成し、完全な実装を追加:

using Microsoft.Agents.Builder;
using Microsoft.Agents.Core;
using Microsoft.Agents.Core.Models;
using System.ComponentModel;
using InsuranceAgent.Services;
using Microsoft.Extensions.Configuration;

namespace ZavaInsurance.Plugins
{
    /// <summary>
    /// Vision Plugin for Zava Insurance
    /// Uses AI vision models to analyze damage photos from insurance claims
    /// Provides damage assessment, severity analysis, and repair cost estimates
    /// </summary>
    public class VisionPlugin
    {
        private readonly ITurnContext _turnContext;
        private readonly KnowledgeBaseService _knowledgeBaseService;
        private readonly VisionService _visionService;
        private readonly BlobStorageService _blobStorageService;
        private readonly IConfiguration _configuration;

        public VisionPlugin(
            ITurnContext turnContext, 
            KnowledgeBaseService knowledgeBaseService, 
            VisionService visionService, 
            BlobStorageService blobStorageService, 
            IConfiguration configuration)
        {
            _turnContext = turnContext ?? throw new ArgumentNullException(nameof(turnContext));
            _knowledgeBaseService = knowledgeBaseService ?? throw new ArgumentNullException(nameof(knowledgeBaseService));
            _visionService = visionService ?? throw new ArgumentNullException(nameof(visionService));
            _blobStorageService = blobStorageService ?? throw new ArgumentNullException(nameof(blobStorageService));
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        }

        /// <summary>
        /// Finds and shows the first damage photo for a claim (without analyzing)
        /// Proxies image through devtunnel for inline display in chat
        /// </summary>
        [Description("Finds and shows the first damage photo for a claim. Use this when user wants to see/view the damage photo. Does not perform AI analysis.")]
        public async Task<string> ShowDamagePhoto(string claimNumber)
        {
            await NotifyUserAsync($"🔍 Searching for damage photos in claim {claimNumber}...");

            try
            {
                // Search for the claim with image URL
                var imageUrl = await _knowledgeBaseService.GetClaimImageUrlAsync(claimNumber);

                if (string.IsNullOrEmpty(imageUrl))
                {
                    return $"❌ No damage photo found for claim {claimNumber}.\n\n" +
                           $"_The claim may not have an uploaded damage photo yet._";
                }

                // Get bot endpoint for devtunnel proxy (required for image display in M365 Copilot)
                var botEndpoint = _configuration["BOT_ENDPOINT"];
                if (string.IsNullOrEmpty(botEndpoint))
                {
                    var botDomain = _configuration["BOT_DOMAIN"];
                    botEndpoint = !string.IsNullOrEmpty(botDomain) ? $"https://{botDomain}" : "http://localhost:3978";
                }
                botEndpoint = botEndpoint.TrimEnd('/');

                // Proxy the blob storage URL through devtunnel for inline display
                var proxyUrl = $"{botEndpoint}/api/image?url={Uri.EscapeDataString(imageUrl)}";

                // Return the image with Markdown syntax for inline display
                return $"📸 **Damage Photo for Claim {claimNumber}**\n\n" +
                       $"![Damage Photo]({proxyUrl})\n\n" +
                       $"_Image stored in Azure Blob Storage_";
            }
            catch (Exception ex)
            {
                return $"❌ Error retrieving damage photo: {ex.Message}";
            }
        }

        /// <summary>
        /// Analyzes a damage photo using Mistral AI vision model and presents results
        /// Downloads image, calls VisionService, formats structured analysis
        /// </summary>
        [Description("Analyzes a damage photo using Mistral AI model and requests user approval before updating the system.")]
        public async Task<string> AnalyzeAndShowDamagePhoto(string claimNumber, string documentId)
        {
            await NotifyUserAsync($"🤖 Starting AI Vision Analysis for claim {claimNumber}...");

            try
            {
                // Get the image URL for this claim from knowledge base
                var imageUrl = await _knowledgeBaseService.GetClaimImageUrlAsync(claimNumber);

                if (string.IsNullOrEmpty(imageUrl))
                {
                    return $"❌ No damage photo found for claim {claimNumber}.\n\n" +
                           $"Please ensure a damage photo has been uploaded first.";
                }

                await NotifyUserAsync($"📸 Downloading image from blob storage...");

                // Download the image bytes from blob storage
                using var httpClient = new HttpClient();
                var imageBytes = await httpClient.GetByteArrayAsync(imageUrl);
                var fileName = Path.GetFileName(new Uri(imageUrl).LocalPath);

                await NotifyUserAsync($"🤖 Analyzing damage with Mistral AI Vision...");

                // Analyze the image using Vision AI (Mistral model)
                var analysisResult = await _visionService.AnalyzeDamagePhotoAsync(imageBytes, fileName);

                await NotifyUserAsync($"✅ Analysis complete!");

                // Format the structured analysis results for user
                var response = $"🔍 **AI Vision Analysis Results**\n\n";
                response += $"**Claim:** {claimNumber}\n";
                response += $"**Image:** {imageUrl}\n\n";
                response += $"**Analysis:**\n";
                response += $"- **Damage Type:** {analysisResult.DamageType}\n";
                response += $"- **Severity:** {analysisResult.Severity}\n";
                response += $"- **Estimated Cost:** ${analysisResult.EstimatedRepairCost:N2}\n";
                response += $"- **Urgency:** {analysisResult.Urgency}\n";
                response += $"- **Description:** {analysisResult.DetailedDescription}\n";
                if (analysisResult.AffectedAreas.Length > 0)
                    response += $"- **Affected Areas:** {string.Join(", ", analysisResult.AffectedAreas)}\n";
                if (!string.IsNullOrEmpty(analysisResult.SafetyConcerns))
                    response += $"- **Safety Concerns:** {analysisResult.SafetyConcerns}\n";
                if (!string.IsNullOrEmpty(analysisResult.RepairRecommendations))
                    response += $"- **Recommendations:** {analysisResult.RepairRecommendations}\n";
                if (analysisResult.RequiresSpecialist)
                    response += $"- **Specialist Required:** {analysisResult.SpecialistType}\n";
                response += $"\n---\n\n";
                response += $"Would you like to:\n";
                response += $"- Approve this analysis and update the claim\n";
                response += $"- Reject the analysis\n";
                response += $"- Check for fraud indicators\n";

                return response;
            }
            catch (Exception ex)
            {
                return $"❌ Error analyzing damage photo: {ex.Message}\n\n" +
                       $"Please try again or contact support if the issue persists.";
            }
        }

        /// <summary>
        /// Approves a damage photo analysis via text command
        /// In production: would update database, trigger workflows, assign adjusters
        /// </summary>
        [Description("Approves a damage photo analysis by claim number and document ID. Use this when user says 'approve' or 'approve analysis'.")]
        public async Task<string> ApproveAnalysis(string claimNumber, string documentId, string userFeedback = "")
        {
            return await HandleAnalysisApproval(claimNumber, documentId, true, userFeedback);
        }

        /// <summary>
        /// Rejects a damage photo analysis via text command
        /// In production: would flag for manual review, assign human adjuster
        /// </summary>
        [Description("Rejects a damage photo analysis by claim number and document ID. Use this when user says 'reject' or 'reject analysis'.")]
        public async Task<string> RejectAnalysis(string claimNumber, string documentId, string userFeedback = "")
        {
            return await HandleAnalysisApproval(claimNumber, documentId, false, userFeedback);
        }

        /// <summary>
        /// Common logic for handling analysis approval or rejection
        /// Provides structured feedback and next steps
        /// </summary>
        private async Task<string> HandleAnalysisApproval(string claimNumber, string documentId, bool approved, string userFeedback = "")
        {
            await NotifyUserAsync($"Processing {(approved ? "approval" : "rejection")}...");

            try
            {
                var action = approved ? "approved" : "rejected";
                var emoji = approved ? "✅" : "❌";

                // In a real system, you would:
                // 1. Update the claim status in the database
                // 2. Store the analysis results
                // 3. Update estimated costs
                // 4. Trigger workflow actions (assign adjuster, schedule inspection, etc.)

                var response = $"{emoji} **Analysis {action.ToUpper()}**\n\n";
                response += $"**Claim:** {claimNumber}\n";

                if (approved)
                {
                    response += $"**Status:** The AI analysis has been accepted and the claim has been updated.\n\n";
                    response += $"**Next Steps:**\n";
                    response += $"- The estimated repair costs have been added to the claim\n";
                    response += $"- An adjuster will be notified for final review\n";
                    response += $"- The claim is ready for processing\n";
                }
                else
                {
                    response += $"**Status:** The AI analysis has been rejected.\n\n";
                    response += $"**Next Steps:**\n";
                    response += $"- The claim will be flagged for manual review\n";
                    response += $"- An adjuster will be assigned to inspect the damage\n";
                    response += $"- Additional documentation may be requested\n";
                }

                if (!string.IsNullOrEmpty(userFeedback))
                {
                    response += $"\n**Your Feedback:** {userFeedback}\n";
                }

                response += $"\n_Note: In a production system, this would update the claim database and trigger automated workflows._";

                return response;
            }
            catch (Exception ex)
            {
                return $"❌ Error processing {(approved ? "approval" : "rejection")}: {ex.Message}";
            }
        }

        /// <summary>
        /// Helper to send real-time streaming updates during long operations
        /// Shows as typing indicators with messages in chat
        /// </summary>
        private async Task NotifyUserAsync(string message)
        {
            // Use StreamingResponse for real-time feedback
            if (!_turnContext.Activity.ChannelId.Channel!.Contains(Channels.Webchat))
            {
                await _turnContext.StreamingResponse.QueueInformativeUpdateAsync(message);
            }
            else
            {
                await _turnContext.StreamingResponse.QueueInformativeUpdateAsync(message).ConfigureAwait(false);
            }
        }
    }
}

演習 5: ClaimsPlugin で損傷写真を表示する

Step 1: ClaimsPlugin を更新して損傷写真を表示する

コードの概要

GetClaimImageUrlAsync が利用可能になったため、ClaimsPlugin を更新して請求詳細に損傷写真を表示できるようにします。ラボ BAF2 で削除していた画像表示機能を復活させます。

1️⃣ src/Plugins/ClaimsPlugin.cs を開きます。

2️⃣ GetClaimDetails メソッドを見つけ、終盤付近の以下の部分を探します:

            result.AppendLine("**Documentation Status:**");
            var isComplete = GetFieldValue(claimDoc, "isDocumentationComplete");
            result.AppendLine($"- Documentation Complete: {(isComplete == "True" || isComplete == "true" ? "Yes" : "No")}");
            var missingDocs = GetFieldValue(claimDoc, "missingDocumentation");
            result.AppendLine($"- Missing Documentation: {(string.IsNullOrWhiteSpace(missingDocs) ? "None" : missingDocs)}");

            await NotifyUserAsync($"Retrieved details for claim {claimId}");

            return result.ToString();

3️⃣ これを画像表示を含む更新版に置き換えます:

            result.AppendLine("**Documentation Status:**");
            var isComplete = GetFieldValue(claimDoc, "isDocumentationComplete");
            result.AppendLine($"- Documentation Complete: {(isComplete == "True" || isComplete == "true" ? "Yes" : "No")}");
            var missingDocs = GetFieldValue(claimDoc, "missingDocumentation");
            result.AppendLine($"- Missing Documentation: {(string.IsNullOrWhiteSpace(missingDocs) ? "None" : missingDocs)}");

            // Get damage photo URL if available
            var imageUrl = await _knowledgeBaseService.GetClaimImageUrlAsync(claimId);

            if (!string.IsNullOrEmpty(imageUrl))
            {
                // Get bot endpoint for devtunnel proxy
                var botEndpoint = _configuration["BOT_ENDPOINT"];
                if (string.IsNullOrEmpty(botEndpoint))
                {
                    var botDomain = _configuration["BOT_DOMAIN"];
                    botEndpoint = !string.IsNullOrEmpty(botDomain) ? $"https://{botDomain}" : "http://localhost:3978";
                }
                botEndpoint = botEndpoint.TrimEnd('/');

                // Proxy the blob storage URL through devtunnel
                var proxyUrl = $"{botEndpoint}/api/image?url={Uri.EscapeDataString(imageUrl)}";

                result.AppendLine();
                result.AppendLine("**Damage Photo:**");
                result.AppendLine($"![Damage Photo]({proxyUrl})");
            }

            await NotifyUserAsync($"Retrieved details for claim {claimId}");

            return result.ToString();
更新理由

ラボ BAF2 では GetClaimImageUrlAsync が存在しなかったため画像表示を省略していました。Exercise 3 で追加したので、ここで機能を復元します。

演習 6: サービス登録とエージェント構成を更新する

すべてを結び付けるために、Program.cs とエージェント構成を更新します。

Step 1: サービス登録と KnowledgeBaseService ファクトリを更新する

コードの概要
  • サービス登録: BlobStorageService(シングルトン)と VisionService(スコープ)を DI に登録
  • KnowledgeBaseService ファクトリ: BlobStorageService を簡略化されたコンストラクターへ渡すよう更新

ファクトリパターンで適切なサービス解決と初期化順序を保証します。

1️⃣ src/Program.cs を開きます。

2️⃣ builder.Services.AddSingleton<KnowledgeBaseService>(); を見つけ、以下の登録ブロックに置き換えます:

// Register Blob Storage Service for damage photo uploads
builder.Services.AddSingleton<BlobStorageService>();

// Register VisionService for Mistral AI vision analysis
builder.Services.AddScoped<VisionService>();

// Register Knowledge Base Service with BlobStorageService dependency
builder.Services.AddSingleton<KnowledgeBaseService>(serviceProvider =>
{
    var configuration = serviceProvider.GetRequiredService<IConfiguration>();
    var blobStorageService = serviceProvider.GetRequiredService<BlobStorageService>();

    return new KnowledgeBaseService(configuration, blobStorageService);
});
簡略化されたコンストラクター

更新後の KnowledgeBaseService コンストラクターは以下を受け取ります:

  • IConfiguration: 接続文字列やエンドポイント
  • BlobStorageService: 損傷写真アップロード用(オプション)

サービス内部で SearchIndexClient、KnowledgeBaseRetrievalClient、AzureOpenAIClient を生成し、依存を簡素化します。

Step 2: エージェントに VisionPlugin を追加する

コードの概要

Agent Instructions: システムプロンプトを更新し VisionPlugin のツール (ShowDamagePhoto, AnalyzeAndShowDamagePhoto, ApproveAnalysis) を追加
Plugin 作成: 必要な依存関係 (context, knowledge base, vision service, blob storage, configuration) を用いて VisionPlugin をインスタンス化
Tool 登録: 4 つのビジョンツールをエージェントの機能セットに追加

1️⃣ src/Agent/ZavaInsuranceAgent.cs を開きます。

2️⃣ AgentInstructions プロパティを見つけ、以下で置き換えます:

private readonly string AgentInstructions = """
You are a professional insurance claims assistant for Zava Insurance.

Whenever the user starts a new conversation or provides a prompt to start a new conversation like "start over", "restart", 
"new conversation", "what can you do?", "how can you help me?", etc. use {{StartConversationPlugin.StartConversation}} and 
provide to the user exactly the message you get back from the plugin.

**Available Tools:**
Use {{DateTimeFunctionTool.getDate}} to get the current date and time.
For claims search, use {{ClaimsPlugin.SearchClaims}} and {{ClaimsPlugin.GetClaimDetails}}.
For damage photo viewing, use {{VisionPlugin.ShowDamagePhoto}}.
For AI vision damage analysis, use {{VisionPlugin.AnalyzeAndShowDamagePhoto}} and require approval via {{VisionPlugin.ApproveAnalysis}}.

Stick to the scenario above and use only the information from the tools when answering questions.
Be concise and professional in your responses.
""";

3️⃣ GetClientAgent メソッド内で、var knowledgeBaseService = ... の後に以下を追加:

// Resolve vision and storage services
var visionService = scope.ServiceProvider.GetRequiredService<VisionService>();
var blobStorageService = scope.ServiceProvider.GetRequiredService<BlobStorageService>();

4️⃣ ClaimsPlugin claimsPlugin = new(...) の直後に以下を追加:

// Create VisionPlugin with all dependencies
VisionPlugin visionPlugin = new(context, knowledgeBaseService, visionService, blobStorageService, configuration);

5️⃣ toolOptions.Tools にツールを追加している箇所を見つけ、ビジョンツールを追加:

// Register Vision tools for AI damage photo analysis
toolOptions.Tools.Add(AIFunctionFactory.Create(visionPlugin.AnalyzeAndShowDamagePhoto));
toolOptions.Tools.Add(AIFunctionFactory.Create(visionPlugin.ShowDamagePhoto));
toolOptions.Tools.Add(AIFunctionFactory.Create(visionPlugin.ApproveAnalysis));
toolOptions.Tools.Add(AIFunctionFactory.Create(visionPlugin.RejectAnalysis));

Step 3: 画像プロキシエンドポイントを追加する

必要な理由

Microsoft 365 Copilot はネットワーク制限により Azure Blob Storage の URL へ直接アクセスできません。画像をチャット内で表示するため、ボットの devtunnel 経由で /api/image エンドポイントからプロキシする必要があります。

1️⃣ src/Program.cs を開きます。

2️⃣ app.MapControllers() がある箇所を探します(ファイル終盤、app.Run() の前)。

3️⃣ app.MapGet("/api/citation"後ろ に画像プロキシエンドポイントを追加します:

app.MapGet("/api/image", async (string url) =>
{
    try
    {
        using var httpClient = new HttpClient();
        var imageBytes = await httpClient.GetByteArrayAsync(url);
        var contentType = url.EndsWith(".png") ? "image/png" : "image/jpeg";
        return Results.File(imageBytes, contentType);
    }
    catch (Exception ex)
    {
        return Results.Problem($"Error retrieving image: {ex.Message}");
    }
});
プロキシの仕組み
  1. VisionPlugin が ShowDamagePhoto を呼び出し請求番号を指定
  2. knowledge base から Blob Storage URL を取得 (例: https://storage.blob.core.windows.net/claim-photos/image.jpg)
  3. プロキシ URL を構築: https://your-devtunnel.devtunnels.ms/api/image?url=<escaped-blob-url>
  4. Copilot がボットエンドポイントへリクエスト
  5. ボットが Blob Storage から画像を取得し返却
  6. チャット内で画像が表示

Step 4: StartConversationPlugin のウェルカムメッセージを更新する

ビジョン分析機能を追加したため、ウェルカムメッセージも更新します。

1️⃣ src/Plugins/StartConversationPlugin.cs を開きます。

2️⃣ StartConversation メソッド内の welcomeMessage 変数を以下に置き換えます:

var welcomeMessage = "👋 Welcome to Zava Insurance Claims Assistant!\n\n" +
                    "I'm your AI-powered insurance claims specialist. I help adjusters and investigators streamline the claims process.\n\n" +
                    "**What I can do:**\n\n" +
                    "- Search and retrieve detailed claim information\n" +
                    "- Use Mistral AI to analyze damage photos instantly\n" +
                    "- Provide damage assessments with cost estimates\n" +
                    "- Identify safety concerns from photos\n" +
                    "- Provide current date and time\n\n" +
                    "🎯 Try this workflow:\n" +
                    "1. \"Get details for claim CLM-2025-001007\"\n" +
                    "2. \"Show damage photo for this claim\"\n" +
                    "3. \"Analyze this damage photo\"\n" +
                    "4. \"Approve the analysis\" or \"Reject the analysis\"\n\n" +
                    "Ready to help with your claims investigation. What would you like to start with?";
段階的な機能アップデート

ウェルカムメッセージにビジョン分析機能 (損傷写真表示と Mistral による AI 分析) を追加しました。各ラボの進行に合わせてメッセージを更新します。

演習 7: ビジョン分析をテストする

ビジョン分析機能をテストしましょう。

Step 1: エージェントを実行する

1️⃣ VS Code で F5 を押してデバッグを開始します。

2️⃣ プロンプトが出たら (Preview) Debug in Copilot (Edge) を選択します。

3️⃣ ターミナル出力を確認し、次のような表示を確認します:

🔍 Initializing Azure AI Search Knowledge Base...
📝 Creating claims index 'claims-index'...
✅ Claims index 'claims-index' created successfully
✅ Knowledge source 'claims-knowledge-source' created
✅ Knowledge base 'zava-insurance-kb' created with model 'gpt-4.1'
📝 Indexing sample claims...
✅ Indexed 35 claims
📸 Uploading sample damage photos to blob storage and indexing...
📋 Processing 35 total claims for damage photos...
📸 Uploading to blob storage...
📸 Processing damage photo for claim CLM-2025-001001: ajlal-nueimat-deer-collision.jpg
✅ Uploaded photo for CLM-2025-001001: https://your-storage.blob.core.windows.net/claim-photos/...
...
📸 Total: Uploaded 35 damage photos to blob storage
✅ Sample data indexed successfully

4️⃣ Blob Storage を確認(任意だが推奨):

  • Azure Portal → ストレージアカウントへ
  • Containersclaim-photos を選択
  • 35 枚の画像が請求番号ごとにアップロードされていることを確認
  • 各画像は直接アクセス可能なパブリック Blob URL を持つ

5️⃣ Azure AI Search Knowledge Sources を確認(任意だが推奨):

  • Azure Portal → Azure AI Search サービスを検索
  • Agentic retrieval → 左メニューの Knowledge Sources
  • claims-knowledge-source が表示されていることを確認
  • これが請求インデックスを Knowledge Base に接続し AI 検索を可能にします

6️⃣ ブラウザーが開き、Microsoft 365 Copilot が起動します。前のラボでエージェントはすでにインストール済みです。

Step 2: 損傷写真の表示をテストする

1️⃣ Microsoft 365 Copilot で次を試します:

Show me the damage photo for claim CLM-2025-001007

エージェントは ShowDamagePhoto ツールを使用し、損傷写真を表示するはずです。

画像読み込み時間

画像は Azure Blob Storage からボットエンドポイント経由でプロキシされるため、チャット内で読み込まれるまで数秒かかる場合があります。正常な動作です。

2️⃣ 別の請求を試します:

View the damage photo for claim CLM-2025-001003

Step 2: AI ビジョン分析をテストする

1️⃣ 次を試します:

Analyze the damage photo for claim CLM-2025-001007

エージェントは以下を行います: - AnalyzeAndShowDamagePhoto ツールを使用 - 画像をダウンロードして Mistral AI で分析 - 損傷タイプ、深刻度、費用見積もり、推奨事項を含む詳細分析を提示 - 承認または拒否を求める

2️⃣ 分析を確認後、次を試します:

Approve the analysis

エージェントは ApproveAnalysis を使用し、承認と次のステップを確認するはずです。

🎉 おめでとうございます!

AI ビジョン分析を保険エージェントに追加できました!

達成したこと:

✅ Mistral medium-2505 ビジョンモデルをデプロイ
✅ 画像分析用の VisionService を作成
✅ 複数のビジョン機能を持つ VisionPlugin を構築
✅ 構造化出力による AI 損傷評価を実装
✅ AI 生成分析の承認ワークフローを追加

エージェントができること:

  • 請求の損傷写真を表示
  • マルチモーダル AI で写真を分析
  • 損傷タイプ、深刻度、費用見積もりを抽出
  • 安全上の懸念を特定し修理を推奨
  • AI 分析の承認ワークフローを提供

次のラボでは、認証とメール機能を追加し、エージェントのセキュリティとコミュニケーション機能を強化します。