# Modalities — Vision (Image to Text)

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (on-keyword)
> **Keywords:** vision, image to text, OCR, image analysis, multimodal input, image agent, PDF analysis
> **Read by Claude in:** implement (quando o agente recebe imagem ou PDF)

**Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não exercita conteúdo multimodal. Os três tipos de conteúdo citados (`TextContent`, `UriContent`, `DataContent`) foram **conferidos por reflexão sobre `Microsoft.Extensions.AI.Abstractions` 10.9.0** em 2026-09-08 e continuam existindo com esses nomes. Last-verified: 2026-09-08.

---

## Quando usar vision

Use vision quando o input real é uma imagem ou documento visual e o modelo precisa interpretar o conteúdo visual:

- **OCR não-estruturado:** formulários digitalizados, notas escritas à mão, recibos
- **Análise de imagem:** descrever screenshots, detectar objetos, moderar conteúdo visual
- **Extração de dados de documentos:** invoices, laudos, contratos em PDF/imagem — combine com `RunAsync<T>` para saída tipada (ver §Exemplo)
- **Multimodal Q&A:** usuário envia foto + pergunta

**Não se aplica** quando o input já é texto extraído. Se você já tem o texto do documento (via OCR determinístico, parser de PDF, etc.), envie o texto diretamente para um agente de chat — vision é desnecessário e mais caro.

**Escolha do modelo:** nem todo provider suporta visão. Use o alias `"vision"` no Model Registry — o projeto configura ali o modelo capaz (ex: `gpt-4o`, `gemini-2.5-flash`). Veja `ai-agents-providers-model-registry`.

---

## A API

Vision em MAF usa `ChatMessage` multimodal — a mesma abstração `IChatClient`/`AIAgent`, mas com conteúdo misto no payload da mensagem.

```csharp
using Microsoft.Extensions.AI;

// Constrói um ChatMessage com texto + imagem
ChatMessage message = new(ChatRole.User, [
    new TextContent("O que aparece nesta imagem?"),
    new UriContent(new Uri("https://example.com/foto.jpg"), "image/jpeg")
]);

AgentResponse response = await agent.RunAsync(message);
```

O `IChatClient` (e portanto `AIAgent`) aceita `IEnumerable<ChatMessage>` — você passa a lista diretamente para `RunAsync`.

**Referência de tipos (namespace `Microsoft.Extensions.AI`):**

| Tipo | Uso |
|------|-----|
| `TextContent` | Parte textual da mensagem |
| `UriContent(uri, mediaType)` | Imagem referenciada por URL pública |
| `DataContent.LoadFromAsync(path)` | Carrega imagem de arquivo/stream (helper canônico) |
| `new DataContent(binaryData, mediaType)` | Imagem embutida a partir de bytes já em memória |

> **Conferência de 2026-09-08.** Os quatro nomes acima estão intactos na 10.9.0 — nada a corrigir neste standard além da data. O que mudou ao redor: a lista de `AIContent` cresceu (`ImageGenerationToolCallContent`, `CodeInterpreterToolCallContent`, `McpServerToolCallContent`, `WebSearchToolCallContent`), sinal de que ferramentas hospedadas passaram a devolver conteúdo tipado — assunto de `ai-agents-setup` §*Por que Responses é o default*, não deste standard.
>
> **Reescrita de conteúdo de mídia está FORA do escopo desta re-verificação** (é épico próprio). O que se fez aqui foi conferir nome por nome e re-datar.

---

## Exemplo C# completo

Agente de visão que recebe URL de imagem e retorna descrição. Para extração de dados tipados de um documento, combine com `RunAsync<T>` (ver comentário inline).

```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

public sealed class VisionAgent
{
    private readonly AIAgent _agent;

    public VisionAgent(ModelRegistry modelRegistry)
    {
        _agent = modelRegistry.GetChatClient("vision").AsAIAgent(new ChatClientAgentOptions
        {
            Name = "VisionAgent",
            ChatOptions = new()
            {
                Instructions = """
                    You are an image analysis expert.
                    Describe the image content clearly and extract any visible text or structured data.
                    """
            }
        });
    }

    /// <summary>Analisa uma imagem por URL pública e retorna descrição em texto.</summary>
    public async Task<string> AnalyzeImageUrlAsync(Uri imageUri, string question, CancellationToken ct = default)
    {
        ChatMessage message = new(ChatRole.User, [
            new TextContent(question),
            new UriContent(imageUri, "image/jpeg")
        ]);

        AgentResponse response = await _agent.RunAsync(message, cancellationToken: ct);
        return response.Text;
    }

    /// <summary>
    /// Analisa imagem a partir de um arquivo local ou caminho de upload.
    /// Usa DataContent.LoadFromAsync — padrão canônico MAF para carregar imagem de arquivo.
    /// Para extrair dados tipados, use RunAsync&lt;T&gt; em vez de RunAsync:
    ///   AgentResponse&lt;InvoiceData&gt; r = await _agent.RunAsync&lt;InvoiceData&gt;(message);
    ///   return r.Result;
    /// Veja ai-agents-structured-output para o padrão completo.
    /// </summary>
    public async Task<string> AnalyzeImageFileAsync(string filePath, string question, CancellationToken ct = default)
    {
        DataContent imageContent = await DataContent.LoadFromAsync(filePath);
        ChatMessage message = new(ChatRole.User, [
            new TextContent(question),
            imageContent
        ]);

        AgentResponse response = await _agent.RunAsync(message, cancellationToken: ct);
        return response.Text;
    }

    /// <summary>
    /// Analisa imagem a partir de bytes já em memória (ex: upload de usuário já lido).
    /// Use AnalyzeImageFileAsync quando tiver o caminho do arquivo — é o padrão preferido.
    /// </summary>
    public async Task<string> AnalyzeImageBytesAsync(BinaryData imageData, string mediaType, string question, CancellationToken ct = default)
    {
        ChatMessage message = new(ChatRole.User, [
            new TextContent(question),
            new DataContent(imageData, mediaType)
        ]);

        AgentResponse response = await _agent.RunAsync(message, cancellationToken: ct);
        return response.Text;
    }
}
```

**Consumo:**

```csharp
var agent = new VisionAgent(modelRegistry);

// Por URL pública
string desc = await agent.AnalyzeImageUrlAsync(
    new Uri("https://cdn.example.com/invoice-2024.jpg"),
    "Extraia os campos: número da nota, valor total e data de emissão.");

// Por arquivo local — padrão canônico com DataContent.LoadFromAsync
string desc2 = await agent.AnalyzeImageFileAsync(
    "nota-fiscal.png",
    "Liste todos os itens e valores desta nota fiscal.");

// Por bytes em memória (ex: upload já lido de IFormFile)
BinaryData data = await BinaryData.FromStreamAsync(formFile.OpenReadStream());
string desc3 = await agent.AnalyzeImageBytesAsync(data, "image/png",
    "Descreva o conteúdo da imagem enviada.");
```

---

## DataContent vs UriContent

| Critério | `UriContent` | `DataContent` |
|----------|-------------|---------------|
| Imagem publicamente acessível | Sim — envie a URL | — |
| Imagem privada / upload de usuário | — | Sim — bytes inline (base64 internamente) |
| Payload de rede | Pequeno (só a URI) | Grande (bytes encodados) |
| Disponibilidade | O modelo faz HTTP GET na URI; precisa estar acessível | Autossuficiente, sem dependência externa |
| Formatos suportados | `image/jpeg`, `image/png`, `image/webp`, `image/gif` (por provider) | Idem |

**Trade-off prático:** para imagens geradas internamente ou uploads de usuário, prefira `DataContent` — a URL temporária pode expirar antes do modelo fazer o GET. Para imagens em CDN estável, `UriContent` economiza largura de banda.

**PDF:** o MAF Sample confirma que o OpenAI Chat Client suporta PDF via URI/Data content. Verifique se o provider configurado suporta `application/pdf` antes de usar.

---

## Anti-patterns

| Anti-pattern | Por quê é errado | Jeito certo |
|--------------|-----------------|-------------|
| Enviar imagem para modelo sem suporte a vision | A call falha em runtime com erro de provider | Use alias `"vision"` no Model Registry — aponte para modelo com suporte confirmado |
| Enviar imagem em resolução original sem resize | Tokens de imagem são cobrados por resolução; imagens 4K em contextos que não precisam de detalhe desperdiçam custo e context window | Redimensione para resolução adequada à tarefa (ex: 800px para OCR de texto) |
| Usar vision quando OCR determinístico bastaria | Vision é probabilístico e mais caro; para extração de texto de PDF com layout fixo, um parser PDF é mais confiável e barato | Só use vision quando o layout é variável ou a imagem é não-estruturada |
| Retornar `response.Text` e parsear regex para dados estruturados | Não-determinístico; quebra com mudança de modelo | Combine com `RunAsync<T>` e um `sealed record` — veja `ai-agents-structured-output` |

---

## Checklist (verifiable by morph-eval)

- [ ] Alias `"vision"` existe em `model-registry.json` e aponta para modelo com suporte a visão confirmado
- [ ] `ChatMessage` usa `UriContent` para imagens por URL ou `DataContent` para bytes — nenhum encoding manual de base64 no código do agente
- [ ] Para extração de dados tipados, usa `RunAsync<T>` com `sealed record` (não parseia `response.Text`)
- [ ] Imagens privadas / uploads de usuário usam `DataContent`, não `UriContent` com URL temporária
- [ ] Validação de regras de negócio existe após `RunAsync<T>` quando aplicável

---

## References

- `ai-agents-setup` — packages, DI, e setup mínimo de agente
- `ai-agents-providers-model-registry` — como registrar e resolver modelos por alias
- `ai-agents-structured-output` — `RunAsync<T>` para extração tipada de dados de documentos
- Microsoft Agent Framework multimodal sample: https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/02-agents/Agents/Agent_Step10_UsingImages/README.md

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/modalities-vision.md v1.1 (2026-09-08)*
