Foundry IQ: a camada de conhecimento dos agentes no Microsoft Foundry - Parte 4
No post de hoje, vamos utilizar o agente criado na Parte 3 dentro de uma aplicação console, permitindo interagir com ele de forma direta via terminal, com visual bonito e respostas em tempo real.
Antes de seguir na parte 4, recomendo a leitura das partes anteriores!
Leia a parte 1, parte 2 e parte 3!
Para começar, vamos criar uma nova aplicação console com o seguinte comando:
dotnet new console -n FoundryIqOverview.Console
Com o projeto criado, iremos adicionar os pacotes necessários via NuGet:
dotnet add package Azure.AI.Projects --version 1.2.0-beta.5
dotnet add package Azure.AI.Projects.OpenAI --version 1.0.0-beta.5
dotnet add package Azure.Identity --version 1.17.1
dotnet add package Spectre.Console --version 0.54.0Esses pacotes permitem comunicação com o Microsoft Foundry e deixam o terminal mais elegante com o uso do Spectre.Console.
Agora, vamos criar uma classe de serviço que irá abstrair a implementação do SDK:
using Azure.AI.Projects;
using Azure.Identity;
namespace FoundryIqOverview.Console.Services;
public partial class MicrosoftFoundryService(string projectEndpoint)
{
private AIProjectClient CreateAiProjectClient()
{
return new(new Uri(projectEndpoint), new AzureCliCredential());
}
}Aqui, usamos new AzureCliCredential() para autenticar a aplicação via CLI do Azure. Isso significa que o usuário que estiver autenticado localmente com az login será utilizado para acessar os recursos no Azure, simplificando bastante o desenvolvimento local sem a necessidade de configurar segredos ou credenciais adicionais.
Estamos estruturando essa classe como partial para que cada funcionalidade do serviço (criação de agente, recuperação de agente, criação de conversa etc.) fique isolada em um arquivo diferente. Essa abordagem melhora a organização, facilita a manutenção e permite que cada método tenha foco em uma única responsabilidade.
A seguir, criaremos os métodos individuais:
Criar agente
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
namespace FoundryIqOverview.Console.Services;
public partial class MicrosoftFoundryService
{
public async Task<AgentRecord> CreateAgentAsync(string agentName, string instructions, string model)
{
var projectClient = CreateAiProjectClient();
AgentDefinition agentDefinition = new PromptAgentDefinition(model)
{
Instructions = instructions,
};
await projectClient.Agents.CreateAgentVersionAsync(
agentName,
options: new AgentVersionCreationOptions(agentDefinition));
return await GetAgentAsync(agentName);
}
}Buscar agente por nome
using Azure.AI.Projects.OpenAI;
namespace FoundryIqOverview.Console.Services;
public partial class MicrosoftFoundryService
{
public async Task<AgentRecord> GetAgentAsync(string agentName)
{
var projectClient = CreateAiProjectClient();
var agent = await projectClient.Agents.GetAgentAsync(agentName);
return agent == null ? throw new Exception("Not possible to retrieve the agent") : agent.Value;
}
};Criar conversa
using Azure.AI.Projects.OpenAI;
namespace FoundryIqOverview.Console.Services;
public partial class MicrosoftFoundryService
{
public async Task<ProjectConversation> CreateConversationAsync()
{
var projectClient = CreateAiProjectClient();
ProjectConversation conversation = await projectClient.OpenAI.Conversations.CreateProjectConversationAsync();
return conversation;
}
};Agora, vamos definir alguns records para representar os eventos que queremos rastrear durante a execução do agente (resposta, conclusão, metadados etc.):
namespace FoundryIqOverview.Console.Services.Models;
public abstract record ResponseEvent;
public record ResponseContentEvent(string Id, string Message) : ResponseEvent;
public record ResponseCompletedEvent(string ResponseId) : ResponseEvent;
public record ResponseNonMappedEvent(string EventType) : ResponseEvent;
public record ResponseMetadataEvent(
int InputTokenCount,
int OutputTokenCount,
IEnumerable<string> Tools) : ResponseEvent;Com isso, iremos criar o método RunAsync do nosso serviço:
using System.Diagnostics.CodeAnalysis;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.Identity;
using FoundryIqOverview.Console.Services.Models;
using OpenAI.Responses;
namespace FoundryIqOverview.Console.Services;
public partial class MicrosoftFoundryService
{
[Experimental("OPENAI001")]
public async Task RunAsync(
string agentName,
string agentVersion,
string conversationId,
string input,
Func<ResponseEvent, Task> onUpdate)
{
var projectClient = CreateAiProjectClient();
AgentReference agentReference = new(name: agentName, version: agentVersion);
ProjectResponsesClient responseClient = projectClient.OpenAI.GetProjectResponsesClientForAgent(
agentReference,
defaultConversationId: conversationId);
var updates = responseClient.CreateResponseStreamingAsync(input);
var responseId = string.Empty;
await foreach (var update in updates)
{
switch (update)
{
case StreamingResponseOutputTextDeltaUpdate deltaUpdate:
await onUpdate(new ResponseContentEvent(deltaUpdate.ItemId, deltaUpdate.Delta));
break;
case StreamingResponseCompletedUpdate completedUpdate:
responseId = completedUpdate.Response.Id;
await onUpdate(new ResponseCompletedEvent());
break;
default:
await onUpdate(new ResponseNonMappedEvent(update.GetType().ToString()));
break;
}
}
var result = await responseClient.GetResponseAsync(
new GetResponseOptions(responseId));
await onUpdate(new ResponseMetadataEvent(
result.Value.Usage.InputTokenCount,
result.Value.Usage.OutputTokenCount,
result.Value.Tools.Select(x => x.GetType().ToString())));
}
}
O parâmetro Func<ResponseEvent, Task> onUpdate permite que o método RunAsync comunique os diferentes eventos da execução (texto parcial, finalização, metadados etc.) de forma assíncrona com a camada de apresentação. Ao chamar await onUpdate(...), passamos eventos para serem processados em tempo real, por exemplo, atualizando o conteúdo mostrado no terminal.
Agora com o serviço pronto, vamos para a classe principal do console app, onde tudo se conecta:
using System.Text;
using Spectre.Console;
using FoundryIqOverview.Console.Services;
using FoundryIqOverview.Console.Services.Models;
using Spectre.Console.Rendering;
#pragma warning disable OPENAI001
var agentName = "contoso-software-agent";
var projectUrl = "<your-project-url>";
AnsiConsole.Clear();
var prompt = """
Explain (1) the Azure Key Vault authentication scenario called “Application-plus-user” (what it means, how the auth flow works, and what permissions the application identity and the user each need), and (2) Contoso’s SLA policy, including availability commitments, incident severity levels, and its operational details.
""";
AnsiConsole.Write(
new FigletText("Contoso Agent")
.Centered()
.Color(Color.CornflowerBlue));
AnsiConsole.MarkupLine(
$"[yellow]Prompt:[/] [cyan]{prompt} [/]\n");
if (string.IsNullOrWhiteSpace(projectUrl))
{
AnsiConsole.MarkupLine("[red]PROJECT_ENDPOINT environment variable not set.[/]");
return;
}
var service = new MicrosoftFoundryService(projectUrl);
var agent = await service.GetAgentAsync(agentName);
var conversation = await service.CreateConversationAsync();
var buffer = new StringBuilder();
// Initial renderable (recommended by the official tutorial)
IRenderable BuildPanel(string content) =>
new Panel(new Markup(content))
{
Header = new PanelHeader("[bold green]Agent Response[/]"),
Border = BoxBorder.Rounded,
Padding = new Padding(1, 1)
};
await AnsiConsole.Live(BuildPanel("[grey]Waiting for agent response...[/]"))
.AutoClear(true)
.Overflow(VerticalOverflow.Visible)
.StartAsync(async ctx =>
{
await service.RunAsync(
agent.Name,
agent.Versions.Latest.Version,
conversation,
prompt,
async ev =>
{
switch (ev)
{
case ResponseContentEvent update:
buffer.Append(Markup.Escape(update.Message));
ctx.UpdateTarget(BuildPanel(buffer.ToString()));
break;
case ResponseCompletedEvent:
buffer.Append("\n\n[bold green]✔ Response completed[/]");
ctx.UpdateTarget(BuildPanel(buffer.ToString()));
break;
case ResponseMetadataEvent usage:
buffer.Append($"""
[grey]────────────────────────────[/]
[bold yellow]Usage[/]
• Input Tokens: [cyan]{usage.InputTokenCount}[/]
• Output Tokens: [cyan]{usage.OutputTokenCount}[/]
""");
foreach (var tool in usage.Tools)
{
buffer.Append($"• Tool used: [magenta]{tool}[/]\n");
}
ctx.UpdateTarget(BuildPanel(buffer.ToString()));
break;
}
await Task.CompletedTask;
});
});
AnsiConsole.Write(BuildPanel(buffer.ToString()));
O que esse código faz:
- Configura o agente
- Define o nome do agente (
contoso-software-agent) - Define a URL do projeto no Microsoft Foundry
- Define o nome do agente (
- Limpa e prepara o console
- Limpa a tela (
AnsiConsole.Clear()) - Mostra um título grande estilizado com FigletText
- Exibe o prompt que será enviado ao agente
- Limpa a tela (
- Valida configuração básica
- Verifica se a URL do projeto existe
- Encerra o programa se estiver inválida
- Inicializa os serviços do Foundry
- Cria o
MicrosoftFoundryService - Obtém o agente pelo nome
- Cria uma nova conversa (conversation)
- Cria o
- Prepara o buffer de saída
- Usa um
StringBuilderpara juntas os textos da resposta do agente
- Usa um
- Cria um painel visual reutilizável
- Envolve o texto da resposta em um
Panel - Esse painel é atualizado em tempo real
- Envolve o texto da resposta em um
- Exibe a resposta em tempo real (streaming)
- Usa
AnsiConsole.Live(...) - Enquanto o agente responde:
- Atualiza o painel conforme novos eventos são recebidos
- Mostra a resposta sendo incrementada no console
- Usa
- Processa os eventos retornados pelo agente
- ResponseContentEvent
- Recebe texto parcial da resposta
- Atualiza o painel imediatamente
- ResponseCompletedEvent
- Indica que o agente terminou de responder
- Adiciona uma mensagem de conclusão
- ResponseMetadataEvent
- Mostra métricas de uso:
- Tokens de entrada
- Tokens de saída
- Lista ferramentas utilizadas pelo agente
- Mostra métricas de uso:
- ResponseContentEvent
- Renderiza o resultado final
- Após o streaming terminar, exibe o painel final com todo o conteúdo acumulado
Estamos utilizando a biblioteca Spectre.Console para deixar o aplicativo de terminal mais bonito, interativo e fácil de acompanhar. Com ela, conseguimos criar painéis, animações e estilos visuais que enriquecem a experiência de uso, especialmente ao lidar com respostas de agentes em tempo real.
O valor de projectUrl, necessário para conectar ao projeto do Microsoft Foundry, pode ser encontrado na página principal do portal do Foundry:

O prompt utilizado neste exemplo é exatamente o mesmo da Parte 3, garantindo consistência nos testes e permitindo comparar os resultados obtidos por diferentes interfaces:
Explain (1) the Azure Key Vault authentication scenario called
“Application-plus-user” (what it means, how the auth flow works, and what permissions the
application identity and the user each need), and (2) Contoso’s SLA policy, including
availability commitments, incident severity levels, and its operational details. Antes de executar a aplicação, devemos nos autenticar na tenant do Azure diretamente pelo terminal, garantindo que as credenciais locais estejam disponíveis para o SDK:
az loginCom a autenticação feita, vamos executar o console app:
dotnet runTemos então o resultado da execução, com o agente respondendo diretamente no terminal em tempo real:

No final da execução, o app também exibe os metadados da run, como quantidade de tokens usados e ferramentas utilizadas durante a resposta:

Além disso, podemos acessar novamente o portal do Foundry para verificar os dados da execução diretamente na interface web, incluindo histórico da conversa, uso de tokens e muito mais:


Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!
Até a próxima, abraços!