diff --git a/es/labs/copilot/lab05-mcs-setup.md b/es/labs/copilot/lab06-mcs-setup.md similarity index 100% rename from es/labs/copilot/lab05-mcs-setup.md rename to es/labs/copilot/lab06-mcs-setup.md diff --git a/es/labs/copilot/lab06-charles-copilot-agent.md b/es/labs/copilot/lab07-charles-copilot-agent.md similarity index 100% rename from es/labs/copilot/lab06-charles-copilot-agent.md rename to es/labs/copilot/lab07-charles-copilot-agent.md diff --git a/es/labs/copilot/lab07-ric-child-agent.md b/es/labs/copilot/lab08-ric-child-agent.md similarity index 100% rename from es/labs/copilot/lab07-ric-child-agent.md rename to es/labs/copilot/lab08-ric-child-agent.md diff --git a/es/labs/copilot/lab08-bill-orchestrator.md b/es/labs/copilot/lab09-bill-orchestrator.md similarity index 100% rename from es/labs/copilot/lab08-bill-orchestrator.md rename to es/labs/copilot/lab09-bill-orchestrator.md diff --git a/es/labs/copilot/lab09-bill-publishing.md b/es/labs/copilot/lab10-bill-publishing.md similarity index 100% rename from es/labs/copilot/lab09-bill-publishing.md rename to es/labs/copilot/lab10-bill-publishing.md diff --git a/es/labs/foundry/code/agents/AndersAgent/ai-foundry/AndersAgent.csproj b/es/labs/foundry/code/agents/AndersAgent/ai-foundry/AndersAgent.csproj deleted file mode 100644 index 59a2503..0000000 --- a/es/labs/foundry/code/agents/AndersAgent/ai-foundry/AndersAgent.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net8.0 - enable - enable - AndersAgent.AIFoundry - - - - - - - - - - - - PreserveNewest - - - - diff --git a/es/labs/foundry/code/agents/AndersAgent/ai-foundry/Program.cs b/es/labs/foundry/code/agents/AndersAgent/ai-foundry/Program.cs deleted file mode 100644 index b5f805d..0000000 --- a/es/labs/foundry/code/agents/AndersAgent/ai-foundry/Program.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Azure.AI.Projects; -using Azure.AI.Agents.Persistent; -using Azure.Identity; -using Microsoft.Extensions.Configuration; - -#pragma warning disable CA2252 // API en preview - -// --- Cargar configuración --- -var config = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - -var foundryEndpoint = config["FoundryProjectEndpoint"] - ?? throw new InvalidOperationException("Falta FoundryProjectEndpoint en appsettings.json"); -var modelDeployment = config["ModelDeploymentName"] - ?? throw new InvalidOperationException("Falta ModelDeploymentName en appsettings.json"); -var functionAppBaseUrl = config["FunctionAppBaseUrl"] - ?? throw new InvalidOperationException("Falta FunctionAppBaseUrl en appsettings.json"); -var tenantId = config["TenantId"]; - -// ===================================================================== -// FASE 1: Obtener la especificación OpenAPI de la Function App -// ===================================================================== - -Console.WriteLine("[OpenAPI] Descargando especificación desde la Function App..."); - -var httpClient = new HttpClient(); -var openApiSpecUrl = $"{functionAppBaseUrl}/openapi/v3.json"; -var openApiSpec = await httpClient.GetStringAsync(openApiSpecUrl); - -Console.WriteLine($"[OpenAPI] Especificación descargada ({openApiSpec.Length} bytes)"); - -// ===================================================================== -// FASE 2: Crear agente con herramienta OpenAPI en Foundry -// ===================================================================== - -// Cliente del proyecto Foundry -// Si TenantId está configurado, se usa explícitamente para evitar conflictos -// en máquinas con múltiples tenants de Azure (error 400 "Token tenant does not match"). -var credentialOptions = new DefaultAzureCredentialOptions(); -if (!string.IsNullOrWhiteSpace(tenantId)) - credentialOptions.TenantId = tenantId; - -var projectClient = new AIProjectClient( - new Uri(foundryEndpoint), - new DefaultAzureCredential(credentialOptions)); - -// Obtener el cliente de agentes persistentes -var agentsClient = projectClient.GetPersistentAgentsClient(); - -// Definir la herramienta OpenAPI a partir de la especificación descargada -var openApiTool = new OpenApiToolDefinition( - new OpenApiFunctionDefinition( - name: "ContosoRetailAPI", - spec: BinaryData.FromString(openApiSpec), - openApiAuthentication: new OpenApiAnonymousAuthDetails()) - { - Description = "API de Contoso Retail para generar reportes de órdenes de compra" - }); - -// Instrucciones del agente Anders -var andersInstructions = """ - Eres Anders, el agente ejecutor de Contoso Retail. - - Tu responsabilidad es ejecutar acciones operativas concretas cuando se te soliciten. - Tu principal capacidad es generar reportes de órdenes de compra de clientes - usando la API de Contoso Retail disponible como herramienta OpenAPI. - - Cuando recibas datos de órdenes, debes construir el JSON del request body - con EXACTAMENTE este schema para invocar el endpoint ordersReporter: - - { - "customerName": "Nombre del Cliente", - "startDate": "YYYY-MM-DD", - "endDate": "YYYY-MM-DD", - "orders": [ - { - "orderNumber": "código de la orden", - "orderDate": "YYYY-MM-DD", - "orderLineNumber": 1, - "productName": "nombre del producto", - "brandName": "nombre de la marca", - "categoryName": "nombre de la categoría", - "quantity": 1.0, - "unitPrice": 0.00, - "lineTotal": 0.00 - } - ] - } - - Reglas: - - TODOS los campos son obligatorios para cada línea de orden. - - Si una orden tiene múltiples productos, cada producto es un elemento - separado en el array "orders" con el mismo "orderNumber" y "orderDate" - pero diferente "orderLineNumber" (secuencial: 1, 2, 3...). - - Las fechas deben estar en formato ISO: YYYY-MM-DD. - - "quantity", "unitPrice" y "lineTotal" son numéricos (double). - - Siempre confirma la acción realizada al usuario, incluyendo la URL del reporte. - Si los datos son insuficientes o inválidos, explica qué falta. - Responde en español. - """; - -Console.WriteLine("[Foundry] Buscando agente Anders existente..."); - -PersistentAgent? agent = null; - -// Buscar si ya existe un agente con el mismo nombre -await foreach (var existingAgent in agentsClient.Administration.GetAgentsAsync()) -{ - if (existingAgent.Name == "Anders - Agente Ejecutor") - { - agent = existingAgent; - Console.WriteLine($"[Foundry] Agente existente encontrado: {agent.Name} (ID: {agent.Id})"); - break; - } -} - -if (agent is null) -{ - Console.WriteLine("[Foundry] Creando agente Anders con herramienta OpenAPI..."); - - agent = (await agentsClient.Administration.CreateAgentAsync( - model: modelDeployment, - name: "Anders - Agente Ejecutor", - description: "Agente ejecutor de Contoso Retail con herramienta OpenAPI", - instructions: andersInstructions, - tools: new List { openApiTool })).Value; - - Console.WriteLine($"[Foundry] Agente creado: {agent.Name} (ID: {agent.Id})"); -} - -// ===================================================================== -// FASE 3: Interactuar con el agente (threads & runs) -// ===================================================================== - -PersistentAgentThread thread = (await agentsClient.Threads.CreateThreadAsync()).Value; -Console.WriteLine($"[Foundry] Thread creado: {thread.Id}"); -Console.WriteLine(); -Console.WriteLine("=== Chat con Anders (escribe 'salir' para terminar) ==="); -Console.WriteLine(); - -while (true) -{ - Console.Write("Tú: "); - var input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input) || - input.Equals("salir", StringComparison.OrdinalIgnoreCase)) - break; - - // Enviar mensaje del usuario al thread - await agentsClient.Messages.CreateMessageAsync( - threadId: thread.Id, - role: MessageRole.User, - content: input); - - // Ejecutar el agente sobre el thread - ThreadRun run = (await agentsClient.Runs.CreateRunAsync(thread, agent)).Value; - - // Esperar a que el run termine (polling) - Console.Write("Anders: "); - while (run.Status == RunStatus.Queued || run.Status == RunStatus.InProgress) - { - await Task.Delay(TimeSpan.FromSeconds(1)); - run = (await agentsClient.Runs.GetRunAsync(thread.Id, run.Id)).Value; - } - - // Procesar resultado - if (run.Status == RunStatus.Completed) - { - // Obtener mensajes del thread (los más recientes primero) - var messages = agentsClient.Messages.GetMessagesAsync(threadId: thread.Id); - - await foreach (PersistentThreadMessage message in messages) - { - // Solo mostrar la primera respuesta del agente (la más reciente) - if (message.Role == MessageRole.Agent) - { - foreach (MessageContent contentItem in message.ContentItems) - { - if (contentItem is MessageTextContent textContent) - { - Console.WriteLine(textContent.Text); - } - } - break; - } - } - } - else - { - Console.WriteLine($"\n[Error] Run terminó con estado: {run.Status}"); - if (run.LastError != null) - Console.WriteLine($"[Error] {run.LastError.Code}: {run.LastError.Message}"); - } - Console.WriteLine(); -} - -// ===================================================================== -// Limpieza del thread (el agente persiste para reutilizarse) -// ===================================================================== - -Console.WriteLine("[Foundry] Limpiando thread..."); -await agentsClient.Threads.DeleteThreadAsync(thread.Id); -Console.WriteLine($"[Foundry] Thread eliminado. El agente '{agent.Name}' (ID: {agent.Id}) permanece disponible."); diff --git a/es/labs/foundry/code/agents/AndersAgent/ai-foundry/appsettings.json b/es/labs/foundry/code/agents/AndersAgent/ai-foundry/appsettings.json deleted file mode 100644 index 3467b94..0000000 --- a/es/labs/foundry/code/agents/AndersAgent/ai-foundry/appsettings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "FoundryProjectEndpoint": "https://ais-contosoretail-.services.ai.azure.com/api/projects/aip-contosoretail-", - "ModelDeploymentName": "gpt-4.1", - "FunctionAppBaseUrl": "https://func-contosoretail-.azurewebsites.net/api", - "TenantId": "" -} diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/AndersAgent.csproj b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/AndersAgent.csproj deleted file mode 100644 index 17279f8..0000000 --- a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/AndersAgent.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - Exe - net8.0 - enable - enable - AndersAgent.MsFoundry - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/Program.cs b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/Program.cs index 201a2b9..2a8bae9 100644 --- a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/Program.cs +++ b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/Program.cs @@ -1,223 +1,115 @@ -using Azure.AI.Projects; -using Azure.AI.Projects.OpenAI; -using Azure.Identity; -using Microsoft.Extensions.Configuration; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Text.Json; -using OpenAI.Responses; - -#pragma warning disable OPENAI001 // OpenAI preview API - -// ===================================================================== -// Anders - Agente Ejecutor (Microsoft Foundry - nueva experiencia) -// -// Esta versión usa el SDK Azure.AI.Projects + Azure.AI.Projects.OpenAI -// con la API de Responses (nueva experiencia de Microsoft Foundry). -// -// La herramienta OpenAPI se configura vía protocol method (BinaryContent) -// ya que los tipos OpenApiAgentTool son internos en el SDK 1.2.x. -// ===================================================================== - -// --- Cargar configuración --- -var config = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - -var foundryEndpoint = config["FoundryProjectEndpoint"] - ?? throw new InvalidOperationException("Falta FoundryProjectEndpoint en appsettings.json"); -var modelDeployment = config["ModelDeploymentName"] - ?? throw new InvalidOperationException("Falta ModelDeploymentName en appsettings.json"); -var functionAppBaseUrl = config["FunctionAppBaseUrl"] - ?? throw new InvalidOperationException("Falta FunctionAppBaseUrl en appsettings.json"); -var tenantId = config["TenantId"]; -var agentName = "Anders"; - -// ===================================================================== -// FASE 1: Obtener la especificación OpenAPI de la Function App -// ===================================================================== - -Console.WriteLine("[OpenAPI] Descargando especificación desde la Function App..."); - -var httpClient = new HttpClient(); -var openApiSpecUrl = $"{functionAppBaseUrl}/openapi/v3.json"; -var openApiSpec = await httpClient.GetStringAsync(openApiSpecUrl); - -Console.WriteLine($"[OpenAPI] Especificación descargada ({openApiSpec.Length} bytes)"); - -// ===================================================================== -// FASE 2: Crear agente con herramienta OpenAPI (protocol method) -// ===================================================================== - -// Instrucciones del agente Anders -var andersInstructions = """ - Eres Anders, el agente ejecutor de Contoso Retail. - - Tu responsabilidad es ejecutar acciones operativas concretas cuando se te soliciten. - Tu principal capacidad es generar reportes de órdenes de compra de clientes - usando la API de Contoso Retail disponible como herramienta OpenAPI. - - Cuando recibas datos de órdenes, debes construir el JSON del request body - con EXACTAMENTE este schema para invocar el endpoint ordersReporter: - - { - "customerName": "Nombre del Cliente", - "startDate": "YYYY-MM-DD", - "endDate": "YYYY-MM-DD", - "orders": [ - { - "orderNumber": "código de la orden", - "orderDate": "YYYY-MM-DD", - "orderLineNumber": 1, - "productName": "nombre del producto", - "brandName": "nombre de la marca", - "categoryName": "nombre de la categoría", - "quantity": 1.0, - "unitPrice": 0.00, - "lineTotal": 0.00 - } - ] - } - - Reglas: - - TODOS los campos son obligatorios para cada línea de orden. - - Si una orden tiene múltiples productos, cada producto es un elemento - separado en el array "orders" con el mismo "orderNumber" y "orderDate" - pero diferente "orderLineNumber" (secuencial: 1, 2, 3...). - - Las fechas deben estar en formato ISO: YYYY-MM-DD. - - "quantity", "unitPrice" y "lineTotal" son numéricos (double). - - Siempre confirma la acción realizada al usuario, incluyendo la URL del reporte. - Si los datos son insuficientes o inválidos, explica qué falta. - Responde en español. - """; - -// Cliente del proyecto Foundry (nueva experiencia) -// Si TenantId está configurado, se usa explícitamente para evitar conflictos -// en máquinas con múltiples tenants de Azure (error 400 "Token tenant does not match"). -var credentialOptions = new DefaultAzureCredentialOptions(); -if (!string.IsNullOrWhiteSpace(tenantId)) - credentialOptions.TenantId = tenantId; - -AIProjectClient projectClient = new( - endpoint: new Uri(foundryEndpoint), - tokenProvider: new DefaultAzureCredential(credentialOptions)); - -// Verificar si el agente ya existe -bool shouldCreateAgent = true; -AgentRecord? existingAgent = null; - -Console.WriteLine($"[Foundry] Buscando agente existente '{agentName}'..."); -try -{ - existingAgent = projectClient.Agents.GetAgent(agentName); - Console.WriteLine($"[Foundry] Agente encontrado: {existingAgent.Name} (ID: {existingAgent.Id})"); - Console.Write("[Foundry] ¿Desea sobreescribirlo con una nueva versión? (s/N): "); - var answer = Console.ReadLine(); - shouldCreateAgent = answer?.Trim().Equals("s", StringComparison.OrdinalIgnoreCase) == true - || answer?.Trim().Equals("si", StringComparison.OrdinalIgnoreCase) == true - || answer?.Trim().Equals("sí", StringComparison.OrdinalIgnoreCase) == true; - - if (!shouldCreateAgent) - { - Console.WriteLine("[Foundry] Se conserva el agente existente."); - } -} -catch (ClientResultException ex) when (ex.Status == 404) -{ - Console.WriteLine($"[Foundry] No se encontró un agente existente con nombre '{agentName}'. Se creará uno nuevo."); -} - -AgentRecord agentRecord; - -if (shouldCreateAgent) -{ - // Construir el JSON con la definición del agente incluyendo herramienta OpenAPI - // (los tipos OpenApiAgentTool son internos, se usa protocol method con BinaryContent) - Console.WriteLine("[Foundry] Creando/actualizando agente Anders con herramienta OpenAPI..."); - - var openApiSpecJson = JsonSerializer.Deserialize(openApiSpec); - - var agentDefinitionJson = new - { - definition = new - { - kind = "prompt", - model = modelDeployment, - instructions = andersInstructions, - tools = new object[] - { - new - { - type = "openapi", - openapi = new - { - name = "ContosoRetailAPI", - description = "API de Contoso Retail para generar reportes de órdenes de compra", - spec = openApiSpecJson, - auth = new { type = "anonymous" } - } - } - } - } - }; - - var jsonContent = JsonSerializer.Serialize(agentDefinitionJson, new JsonSerializerOptions { WriteIndented = false }); - var result = await projectClient.Agents.CreateAgentVersionAsync( - agentName, - BinaryContent.Create(BinaryData.FromString(jsonContent)), - new RequestOptions()); - - // Parsear respuesta para obtener info del agente - var responseJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString()); - var version = responseJson.RootElement.TryGetProperty("version", out var vProp) ? vProp.GetString() : "?"; - Console.WriteLine($"[Foundry] Agente creado/actualizado: {agentName} (v{version})"); -} - -// Obtener el agente registrado -agentRecord = projectClient.Agents.GetAgent(agentName); -Console.WriteLine($"[Foundry] Agente obtenido: {agentRecord.Name} (ID: {agentRecord.Id})"); - -// ===================================================================== -// FASE 3: Interactuar con el agente (Responses API + Conversations) -// ===================================================================== - -// Crear conversación para multi-turn -ProjectConversation conversation = projectClient.OpenAI.Conversations.CreateProjectConversation(); -Console.WriteLine($"[Foundry] Conversación creada: {conversation.Id}"); - -// Obtener cliente de Responses vinculado al agente y conversación -ProjectResponsesClient responseClient = projectClient.OpenAI.GetProjectResponsesClientForAgent( - defaultAgent: agentName, - defaultConversationId: conversation.Id); - -Console.WriteLine(); -Console.WriteLine("=== Chat con Anders (escribe 'salir' para terminar) ==="); -Console.WriteLine(); - -while (true) -{ - Console.Write("Tú: "); - var input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input) || - input.Equals("salir", StringComparison.OrdinalIgnoreCase)) - break; - - // Enviar mensaje y obtener respuesta del agente - Console.Write("Anders: "); - try - { - ResponseResult response = responseClient.CreateResponse(input); - Console.WriteLine(response.GetOutputText()); - } - catch (Exception ex) - { - Console.WriteLine($"\n[Error] {ex.Message}"); - } - - Console.WriteLine(); -} - -Console.WriteLine("[Foundry] Chat finalizado."); -Console.WriteLine($"[Foundry] El agente '{agentName}' permanece disponible."); +using System.ComponentModel; +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; + +namespace MsFoundryAgent; + +public static class Program +{ + private const string DefaultAgentName = "Andres-Agent"; + private const string DefaultAgentInstructions = + "You are an analytical AI agent specialized in reading, understanding, and extracting insights from provided information."; + + // ----------------------------------------------------------------------- + // Tool: a simple weather stub to demonstrate function calling + // ----------------------------------------------------------------------- + [Description("Get the current weather for a given location.")] + public static string GetWeather( + [Description("The city or location name, e.g. 'Seattle'")] string location) + { + var rand = new Random(); + string[] conditions = ["sunny", "cloudy", "rainy", "stormy"]; + return $"The weather in {location} is {conditions[rand.Next(conditions.Length)]} " + + $"with a high of {rand.Next(10, 35)}°C."; + } + + public static async Task Main(string[] args) + { + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false) + .AddEnvironmentVariables() + .Build(); + + string projectEndpoint = config["Foundry:ProjectEndpoint"] + ?? throw new InvalidOperationException("Foundry:ProjectEndpoint is not configured."); + + string modelDeployment = config["Foundry:ModelDeployment"] + ?? throw new InvalidOperationException("Foundry:ModelDeployment is not configured."); + + string agentName = config["Foundry:AgentName"] ?? DefaultAgentName; + string agentInstructions = config["Foundry:AgentInstructions"] ?? DefaultAgentInstructions; + + var aiProjectClient = new AIProjectClient( + new Uri(projectEndpoint), + new DefaultAzureCredential()); + + Console.WriteLine($"Creating agent '{agentName}' on Azure AI Foundry..."); + + ChatClientAgent agent = await aiProjectClient.CreateAIAgentAsync( + name: agentName, + model: modelDeployment, + instructions: agentInstructions, + description: null, + tools: [AIFunctionFactory.Create(GetWeather)]); + + if (args.Length > 0 && args[0].Equals("deploy", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Agent '{agent.Name}' deployed successfully."); + Console.WriteLine("The agent was left in Azure AI Foundry and was not deleted."); + return; + } + + if (args.Length > 0 && args[0].Equals("verify", StringComparison.OrdinalIgnoreCase)) + { + ChatClientAgent found = await aiProjectClient.GetAIAgentAsync(agentName, tools: [AIFunctionFactory.Create(GetWeather)]); + Console.WriteLine("Agent verification succeeded."); + Console.WriteLine($"Name: {found.Name}"); + Console.WriteLine($"Model: {modelDeployment}"); + Console.WriteLine($"Endpoint: {projectEndpoint}"); + Console.WriteLine("If the portal does not show it, confirm you are in the same Foundry project and tenant."); + return; + } + + Console.WriteLine("Agent created. Starting multi-turn conversation (type 'quit' to exit).\n"); + + var history = new List(); + + while (true) + { + Console.Write("You: "); + string? userInput = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(userInput) || + userInput.Equals("quit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + history.Add(new ChatMessage(ChatRole.User, userInput)); + + Console.Write("Agent: "); + var assistantText = new System.Text.StringBuilder(); + await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(history)) + { + if (!string.IsNullOrEmpty(update.Text)) + { + Console.Write(update.Text); + assistantText.Append(update.Text); + } + } + Console.WriteLine("\n"); + + // Append the assistant reply so future turns have full context + if (assistantText.Length > 0) + history.Add(new ChatMessage(ChatRole.Assistant, assistantText.ToString())); + } + + Console.WriteLine("Cleaning up agent..."); + await aiProjectClient.Agents.DeleteAgentAsync(agent.Name); + Console.WriteLine("Done."); + } +} diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/appsettings.json b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/appsettings.json index 3467b94..9e792c4 100644 --- a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/appsettings.json +++ b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/appsettings.json @@ -1,6 +1,7 @@ -{ - "FoundryProjectEndpoint": "https://ais-contosoretail-.services.ai.azure.com/api/projects/aip-contosoretail-", - "ModelDeploymentName": "gpt-4.1", - "FunctionAppBaseUrl": "https://func-contosoretail-.azurewebsites.net/api", - "TenantId": "" -} +{ + "Foundry": { + "ProjectEndpoint": "https://.services.ai.azure.com/api/projects/", + "ModelDeployment": "gpt-4.1", + "AgentName": "AndersAgent" + } +} diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.ps1 b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.ps1 new file mode 100644 index 0000000..ad91bde --- /dev/null +++ b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.ps1 @@ -0,0 +1,52 @@ +param( + [string]$FoundryProjectEndpoint = $env:FOUNDRY_PROJECT_ENDPOINT, + [string]$FoundryModelDeploymentName = $env:FOUNDRY_MODEL_DEPLOYMENT_NAME, + [string]$FoundryAgentName = $(if ($env:FOUNDRY_AGENT_NAME) { $env:FOUNDRY_AGENT_NAME } else { "AndersAgent" }), + [string]$FoundryAgentInstructions = $(if ($env:FOUNDRY_AGENT_INSTRUCTIONS) { $env:FOUNDRY_AGENT_INSTRUCTIONS } else { "You are an analytical AI agent specialized in reading, understanding, and extracting insights from provided information." }), + [string]$ProjectFile = "./es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent.csproj" +) + +$ErrorActionPreference = "Stop" + +function Require-Command { + param([Parameter(Mandatory = $true)][string]$Name) + + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + throw "Missing required command: $Name" + } +} + +function Require-Value { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "Missing required value: $Name" + } +} + +Require-Command -Name "az" +Require-Command -Name "dotnet" + +Require-Value -Name "FoundryProjectEndpoint (or FOUNDRY_PROJECT_ENDPOINT)" -Value $FoundryProjectEndpoint +Require-Value -Name "FoundryModelDeploymentName (or FOUNDRY_MODEL_DEPLOYMENT_NAME)" -Value $FoundryModelDeploymentName + +$resolvedProjectFile = Resolve-Path -Path $ProjectFile -ErrorAction Stop + +try { + az account show | Out-Null +} +catch { + Write-Host "Azure CLI is not logged in. Running az login..." + az login | Out-Null +} + +$env:Foundry__ProjectEndpoint = $FoundryProjectEndpoint +$env:Foundry__ModelDeployment = $FoundryModelDeploymentName +$env:Foundry__AgentName = $FoundryAgentName +$env:Foundry__AgentInstructions = $FoundryAgentInstructions + +Write-Host "Deploying agent '$FoundryAgentName' to Azure AI Foundry..." +dotnet run --project $resolvedProjectFile -- deploy diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.sh b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.sh new file mode 100755 index 0000000..a8a4321 --- /dev/null +++ b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_FILE="$SCRIPT_DIR/ms_foundry_agent.csproj" + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +require_value() { + local name="$1" + if [[ -z "${!name:-}" ]]; then + echo "Missing required environment variable: $name" >&2 + exit 1 + fi +} + +require_command az +require_command dotnet + +require_value FOUNDRY_PROJECT_ENDPOINT +require_value FOUNDRY_MODEL_DEPLOYMENT_NAME + +if ! az account show >/dev/null 2>&1; then + echo "Azure CLI is not logged in. Running az login..." + az login >/dev/null +fi + +export Foundry__ProjectEndpoint="$FOUNDRY_PROJECT_ENDPOINT" +export Foundry__ModelDeployment="$FOUNDRY_MODEL_DEPLOYMENT_NAME" +export Foundry__AgentName="${FOUNDRY_AGENT_NAME:-AndersAgent}" +export Foundry__AgentInstructions="${FOUNDRY_AGENT_INSTRUCTIONS:-You are an analytical AI agent specialized in reading, understanding, and extracting insights from provided information.}" + +echo "Deploying agent '$Foundry__AgentName' to Azure AI Foundry..." +dotnet run --project "$PROJECT_FILE" -- deploy diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent.csproj b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent.csproj new file mode 100644 index 0000000..822ba5d --- /dev/null +++ b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent.csproj @@ -0,0 +1,35 @@ + + + + Exe + net8.0 + enable + enable + MsFoundryAgent + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent_v2.sln b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent_v2.sln new file mode 100644 index 0000000..659b903 --- /dev/null +++ b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/ms_foundry_agent_v2.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ms_foundry_agent", "ms_foundry_agent.csproj", "{AB0D0B82-6185-D0D9-6EBF-DBBC85DEBF29}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB0D0B82-6185-D0D9-6EBF-DBBC85DEBF29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB0D0B82-6185-D0D9-6EBF-DBBC85DEBF29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB0D0B82-6185-D0D9-6EBF-DBBC85DEBF29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB0D0B82-6185-D0D9-6EBF-DBBC85DEBF29}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {07312557-C0C5-4DBD-8CE0-783FDC2D9B9B} + EndGlobalSection +EndGlobal diff --git a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/nuget.config b/es/labs/foundry/code/agents/AndersAgent/ms-foundry/nuget.config deleted file mode 100644 index 2735cce..0000000 --- a/es/labs/foundry/code/agents/AndersAgent/ms-foundry/nuget.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/es/labs/foundry/codespaces-setup.md b/es/labs/foundry/codespaces-setup.md index 42fbda5..1f5b456 100644 --- a/es/labs/foundry/codespaces-setup.md +++ b/es/labs/foundry/codespaces-setup.md @@ -397,8 +397,8 @@ labs/foundry/ | Lab | Archivo | Descripción | | ----- | --------------------------------------------------------- | ------------------------------------------------------------ | -| Lab 3 | [Anders — Executor Agent](lab03-anders-executor-agent.md) | Crear el agente ejecutor que genera reportes e interactúa con servicios de Contoso Retail. | -| Lab 4 | [Julie — Planner Agent](lab04-julie-planner-agent.md) | Crear el agente orquestador de campañas de marketing usando el patrón workflow con sub-agentes (SqlAgent, MarketingAgent) y herramienta OpenAPI. | +| Lab 4 | [Anders — Executor Agent](lab04-anders-executor-agent.md) | Crear el agente ejecutor que genera reportes e interactúa con servicios de Contoso Retail. | +| Lab 5 | [Julie — Planner Agent (Opcional)](lab05-julie-planner-agent.md) | Crear el agente orquestador de campañas de marketing usando el patrón workflow con sub-agentes (SqlAgent, MarketingAgent) y herramienta OpenAPI. | --- diff --git a/es/labs/foundry/lab03-anders-executor-agent.md b/es/labs/foundry/lab03-anders-executor-agent.md deleted file mode 100644 index 04236f4..0000000 --- a/es/labs/foundry/lab03-anders-executor-agent.md +++ /dev/null @@ -1,426 +0,0 @@ -# Lab 3: Anders — Executor Agent - -## Tabla de contenido - -- [Lab 3: Anders — Executor Agent](#lab-3-anders--executor-agent) - - [Tabla de contenido](#tabla-de-contenido) - - [Introducción](#introducción) - - [¿Qué vamos a hacer en este lab?](#qué-vamos-a-hacer-en-este-lab) - - [3.1 — Verificar soporte OpenAPI (ya viene preconfigurado)](#31--verificar-soporte-openapi-ya-viene-preconfigurado) - - [Checklist rápido de validación](#checklist-rápido-de-validación) - - [Paso 1: Verificar paquetes NuGet](#paso-1-verificar-paquetes-nuget) - - [Paso 2: Verificar endpoints expuestos](#paso-2-verificar-endpoints-expuestos) - - [Endpoints OpenAPI generados](#endpoints-openapi-generados) - - [3.2 — Verificar la especificación OpenAPI](#32--verificar-la-especificación-openapi) - - [Obtener la especificación JSON](#obtener-la-especificación-json) - - [Explorar el Swagger UI](#explorar-el-swagger-ui) - - [3.3 — El agente Anders: Dos versiones de SDK](#33--el-agente-anders-dos-versiones-de-sdk) - - [¿Por qué dos versiones?](#por-qué-dos-versiones) - - [¿Cuál versión debo usar?](#cuál-versión-debo-usar) - - [Entendiendo el código (versión `ms-foundry/` — recomendada)](#entendiendo-el-código-versión-ms-foundry--recomendada) - - [Fase 1 — Descargar la especificación OpenAPI](#fase-1--descargar-la-especificación-openapi) - - [Fase 2 — Verificar agente existente o crear uno nuevo](#fase-2--verificar-agente-existente-o-crear-uno-nuevo) - - [Fase 3 — Chat interactivo con Responses API](#fase-3--chat-interactivo-con-responses-api) - - [Paso 1: Configurar `appsettings.json`](#paso-1-configurar-appsettingsjson) - - [Paso 2: Compilar y ejecutar](#paso-2-compilar-y-ejecutar) - - [Paso 3: Inspeccionar el agente en Azure AI Foundry](#paso-3-inspeccionar-el-agente-en-azure-ai-foundry) - - [Paso 4: Probar el agente](#paso-4-probar-el-agente) - - [Solución de problemas](#solución-de-problemas) - - [Storage Account bloqueado por política (error 503)](#storage-account-bloqueado-por-política-error-503) - - [Challenge: Respuestas en streaming](#challenge-respuestas-en-streaming) - - [Pista](#pista) - - [Criterio de éxito](#criterio-de-éxito) - - [Siguiente paso](#siguiente-paso) - ---- - -## Introducción - -Anders es el **agente ejecutor** de la arquitectura multi-agéntica de Contoso Retail. Su rol es recibir solicitudes de acciones operativas — como la generación y publicación de reportes de órdenes — y ejecutarlas interactuando con servicios externos como la Azure Function `FxContosoRetail`. - -Para que Anders pueda interactuar con la API de Contoso Retail, definiremos una **OpenAPI Tool** que permite al agente descubrir e invocar automáticamente los endpoints de la Function App a partir de su especificación OpenAPI. Adicionalmente, agregaremos soporte **OpenAPI** a la Function App para documentar la API y facilitar la exploración de sus endpoints. - -### ¿Qué vamos a hacer en este lab? - -| Paso | Descripción | -|------|-------------| -| **3.1** | Verificar el soporte OpenAPI de la Azure Function `FxContosoRetail` | -| **3.2** | Verificar la especificación OpenAPI | -| **3.3** | Entender, configurar, ejecutar y probar el agente Anders | - -## 3.1 — Verificar soporte OpenAPI (ya viene preconfigurado) - -En la versión actual del taller, la Function App `FxContosoRetail` **ya incluye** OpenAPI y endpoints decorados en el código base. En este paso no vas a implementar OpenAPI desde cero: solo validar que todo está correcto antes del despliegue. - -### Checklist rápido de validación - -### Paso 1: Verificar paquetes NuGet - -Abre [`es/labs/foundry/code/api/FxContosoRetail/FxContosoRetail.csproj`](../code/api/FxContosoRetail/FxContosoRetail.csproj) y confirma que existen estas referencias: - -- `Microsoft.Azure.Functions.Worker.Extensions.OpenApi` -- `Microsoft.Data.SqlClient` - -### Paso 2: Verificar endpoints expuestos - -Abre [`es/labs/foundry/code/api/FxContosoRetail/FxContosoRetail.cs`](../code/api/FxContosoRetail/FxContosoRetail.cs) y confirma que existen estos endpoints: - -- `HolaMundo` -- `OrdersReporter` -- `SqlExecutor` - -Además, valida que `OrdersReporter` y `SqlExecutor` tengan atributos OpenAPI (`OpenApiOperation`, `OpenApiRequestBody`, `OpenApiResponseWithBody`). Justamente estos cambios son los que necesitarás hacer cuando desees exponer tus Azure Functions existentes como herramientas OpenAPI para los agentes. - -> [!IMPORTANT] -> **Sobre la autenticación de los endpoints** -> -> En este taller usamos `AuthorizationLevel.Anonymous` para simplificar la configuración y permitir que Azure AI Foundry pueda invocar la Function App directamente como OpenAPI Tool sin necesidad de gestionar secrets ni configurar autenticación adicional. -> -> **En un entorno de producción, esto no es recomendable.** La práctica correcta es proteger la Function App con **Azure Entra ID (Easy Auth)** y hacer que Foundry se autentique usando **Managed Identity**. El flujo sería: -> -> 1. **Registrar una aplicación en Entra ID** que represente la Function App, obteniendo un Application (client) ID y un Application ID URI (por ejemplo, `api://`). -> 2. **Habilitar Easy Auth** en la Function App con `az webapp auth update`, configurándola para validar tokens emitidos por Entra ID contra la app registration. Esto protege todos los endpoints a nivel de plataforma — las peticiones sin un bearer token válido se rechazan con 401 antes de llegar al código. -> 3. **Asignar permisos a la Managed Identity** del recurso de AI Services (`ais-contosoretail-{suffix}`) como principal autorizado en la app registration, ya sea agregándola como miembro de un app role o como identidad permitida en la configuración de Easy Auth. -> 4. **Usar `OpenApiManagedAuthDetails`** en el código del agente en lugar de `OpenApiAnonymousAuthDetails`, especificando el audience de la app registration: -> ```csharp -> openApiAuthentication: new OpenApiManagedAuthDetails( -> audience: "api://") -> ``` -> -> Con esta configuración, cuando Foundry necesita llamar a la Function App, obtiene un token de Entra ID usando la managed identity del recurso de AI Services, lo envía como `Authorization: Bearer `, y Easy Auth lo valida automáticamente. Los endpoints de la Function pueden mantener `AuthorizationLevel.Anonymous` en el código C# porque la autenticación ocurre en la capa de plataforma. - -### Endpoints OpenAPI generados - -Una vez desplegada, la Function App expondrá estos endpoints adicionales: - -| Endpoint | Descripción | -|----------|-------------| -| `/api/openapi/v3.json` | Especificación OpenAPI 3.0 en formato JSON | -| `/api/swagger/ui` | Interfaz Swagger UI interactiva | - ---- - -## 3.2 — Verificar la especificación OpenAPI - -Una vez desplegada, verifica que los endpoints OpenAPI están disponibles. - -### Obtener la especificación JSON - -Abre en el navegador o con `curl`: - -``` -https://func-contosoretail-.azurewebsites.net/api/openapi/v3.json -``` - -Deberías ver un JSON con la estructura OpenAPI que describe los endpoints `HolaMundo`, `OrdersReporter` y `SqlExecutor`, incluyendo los esquemas de request/response. - -### Explorar el Swagger UI - -Navega a: - -``` -https://func-contosoretail-.azurewebsites.net/api/swagger/ui -``` - -Desde la interfaz de Swagger UI puedes explorar los endpoints y probarlos interactivamente. - -> **Importante:** La especificación OpenAPI documenta la API y sirve como referencia para entender qué parámetros enviar y qué respuesta esperar. El agente Anders usará esta información indirectamente a través de la Function Tool que definiremos en el siguiente paso. - ---- - -## 3.3 — El agente Anders: Dos versiones de SDK - -La implementación del agente Anders se proporciona en **dos versiones separadas**, cada una ubicada bajo `es/labs/foundry/code/agents/AndersAgent/`: - -| Carpeta | SDK | Paradigma de API | Estado | -|---------|-----|------------------|--------| -| `ai-foundry/` | `Azure.AI.Projects` + `Azure.AI.Agents.Persistent` | Persistent Agents (threads, runs, polling) | GA — se conserva por retrocompatibilidad | -| `ms-foundry/` | `Azure.AI.Projects` + `Azure.AI.Projects.OpenAI` | Responses API (conversaciones, respuestas de proyecto) | **Preview** (a febrero 2026) — **recomendada** | - -### ¿Por qué dos versiones? - -A finales de 2025, Microsoft introdujo una **nueva experiencia para Microsoft Foundry** basada en la **Responses API** y una superficie de gestión de agentes rediseñada. Esta nueva experiencia — expuesta a través del paquete `Azure.AI.Projects.OpenAI` — reemplaza el modelo anterior de Persistent Agents (`Azure.AI.Agents.Persistent`) con un enfoque más ágil que utiliza **agentes con nombre y versionado**, **conversaciones** y la **Responses API** en lugar de threads y runs con polling. - -Las diferencias clave entre ambos enfoques son: - -| Aspecto | `ai-foundry/` (Persistent Agents) | `ms-foundry/` (Responses API) | -|---------|-----------------------------------|-------------------------------| -| **Ciclo de vida del agente** | Se crea con un ID generado; se busca por nombre iterando la lista | Se crea/actualiza por nombre con versionado explícito (`CreateAgentVersionAsync`) | -| **Modelo de conversación** | `PersistentAgentThread` + `ThreadRun` con polling | `ProjectConversation` + `ProjectResponsesClient` — respuesta síncrona | -| **Definición de herramientas** | `OpenApiToolDefinition` con clases tipadas | Protocol method vía `BinaryContent` (los tipos son internos en SDK 1.2.x) | -| **Patrón de chat** | Crear run → hacer polling hasta completar → leer mensajes | Una sola llamada a `CreateResponse()` retorna la salida directamente | - -### ¿Cuál versión debo usar? - -**Se recomienda la versión `ms-foundry/`** para desarrollo nuevo. Está alineada con la dirección de la plataforma Microsoft Foundry y ofrece un modelo de programación más simple — particularmente la eliminación del loop de polling en favor de una sola llamada síncrona de respuesta. - -La versión `ai-foundry/` se conserva en este taller por **retrocompatibilidad**. - -> [!IMPORTANT] -> A febrero de 2026, el paquete `Azure.AI.Projects.OpenAI` y la Responses API están en **preview pública**. Las formas de la API, schemas de payload y tipos del SDK pueden cambiar antes de alcanzar disponibilidad general (GA). Si encuentras problemas como propiedades faltantes o renombradas (por ejemplo, el campo `kind` requerido en el payload de definición del agente), consulta las últimas [notas de versión de Azure.AI.Projects.OpenAI](https://www.nuget.org/packages/Azure.AI.Projects.OpenAI) para conocer los cambios que rompen compatibilidad. - ---- - -### Entendiendo el código (versión `ms-foundry/` — recomendada) - -Abre el archivo `es/labs/foundry/code/agents/AndersAgent/ms-foundry/Program.cs` y observa que está organizado en **3 fases**: - -#### Fase 1 — Descargar la especificación OpenAPI - -```csharp -var openApiSpecUrl = $"{functionAppBaseUrl}/openapi/v3.json"; -var openApiSpec = await httpClient.GetStringAsync(openApiSpecUrl); -``` - -El programa descarga la especificación OpenAPI de la Function App **en tiempo de ejecución**. Esto significa que si la API cambia (nuevos endpoints, nuevos parámetros), el agente lo detecta automáticamente al reiniciarse. - -#### Fase 2 — Verificar agente existente o crear uno nuevo - -Esta fase tiene dos partes clave: - -**Detección de agente existente:** - -Antes de crear una nueva versión, el programa verifica si el agente ya existe llamando a `GetAgent`. Si lo encuentra, le pregunta al usuario si desea conservar el agente existente o sobreescribirlo con una nueva versión. Esto evita la proliferación innecesaria de versiones del agente durante el desarrollo iterativo. - -**Definición del agente con herramienta OpenAPI (protocol method):** - -```csharp -var agentDefinitionJson = new -{ - definition = new - { - kind = "prompt", - model = modelDeployment, - instructions = andersInstructions, - tools = new object[] - { - new - { - type = "openapi", - openapi = new - { - name = "ContosoRetailAPI", - description = "API de Contoso Retail...", - spec = openApiSpecJson, - auth = new { type = "anonymous" } - } - } - } - } -}; -``` - -Dado que los tipos `OpenApiAgentTool` son internos en el SDK 1.2.x, la definición de la herramienta se construye como un objeto anónimo y se serializa vía `BinaryContent`. El campo `kind = "prompt"` es requerido por la API para indicar un agente basado en prompt. - -**System prompt (instrucciones):** - -El system prompt incluye el schema JSON exacto que Anders debe construir al invocar la API: - -```json -{ - "customerName": "Nombre del Cliente", - "startDate": "YYYY-MM-DD", - "endDate": "YYYY-MM-DD", - "orders": [ - { - "orderNumber": "código de la orden", - "orderDate": "YYYY-MM-DD", - "orderLineNumber": 1, - "productName": "nombre del producto", - "brandName": "nombre de la marca", - "categoryName": "nombre de la categoría", - "quantity": 1.0, - "unitPrice": 0.00, - "lineTotal": 0.00 - } - ] -} -``` - -> [!TIP] -> Incluir el schema en las instrucciones es una buena práctica cuando el agente debe construir payloads complejos. Aunque la especificación OpenAPI ya describe el schema, reforzarlo en el system prompt reduce significativamente los errores de formato. - -**Reutilización del agente:** - -```csharp -try -{ - existingAgent = projectClient.Agents.GetAgent(agentName); - // Pregunta al usuario si desea sobreescribir o conservar -} -catch (ClientResultException ex) when (ex.Status == 404) -{ - // Agente no encontrado — crear uno nuevo -} -``` - -Antes de crear una nueva versión del agente, el programa intenta recuperar el agente existente por nombre. Si lo encuentra, le pide al usuario que confirme si desea sobreescribirlo. Esto evita crear versiones innecesarias en Foundry al reiniciar la aplicación. - -#### Fase 3 — Chat interactivo con Responses API - -```csharp -ProjectConversation conversation = projectClient.OpenAI.Conversations.CreateProjectConversation(); -ProjectResponsesClient responseClient = projectClient.OpenAI.GetProjectResponsesClientForAgent( - defaultAgent: agentName, - defaultConversationId: conversation.Id); - -ResponseResult response = responseClient.CreateResponse(input); -Console.WriteLine(response.GetOutputText()); -``` - -El patrón de interacción en la versión `ms-foundry/` es más simple que el enfoque de Persistent Agents: -1. Se crea una `ProjectConversation` (el contexto de conversación) -2. Se obtiene un `ProjectResponsesClient`, vinculado al agente y la conversación -3. Cada mensaje del usuario se envía vía `CreateResponse()` que retorna la salida **síncronamente** — sin necesidad de loop de polling -4. El texto de respuesta se extrae con `GetOutputText()` - -> **¿Qué pasa durante una llamada de respuesta?** Cuando el modelo decide que necesita llamar a la API, Foundry ejecuta la llamada HTTP automáticamente usando la especificación OpenAPI. El resultado se envía de vuelta al modelo, que formula la respuesta final al usuario. Todo esto ocurre dentro de la única llamada a `CreateResponse()` — el código simplemente recibe la respuesta terminada. - -**Limpieza al salir:** - -Cuando el usuario escribe `salir`, el loop de chat termina. El agente **persiste** en Foundry y se reutiliza automáticamente en la siguiente ejecución. - -### Paso 1: Configurar `appsettings.json` - -Abre el archivo `es/labs/foundry/code/agents/AndersAgent/ms-foundry/appsettings.json` y reemplaza los valores con los de tu entorno: - -```json -{ - "FoundryProjectEndpoint": "", - "ModelDeploymentName": "gpt-4.1", - "FunctionAppBaseUrl": "https://func-contosoretail-.azurewebsites.net/api" -} -``` - -> **¿Dónde encuentro estos valores?** -> - **FoundryProjectEndpoint**: El `AI Foundry Endpoint` de la salida del despliegue. -> - **ModelDeploymentName**: `gpt-4.1` (nombre del deployment creado por el Bicep). -> - **FunctionAppBaseUrl**: La URL de tu Function App + `/api`. - -### Paso 2: Compilar y ejecutar - -```bash -cd es/labs/foundry/code/agents/AndersAgent/ms-foundry -dotnet build -``` - -Asegúrate de que no haya errores de compilación. Luego ejecuta: - -```bash -dotnet run -``` - -Verás en consola que el agente verifica si ya existe una versión en Foundry. Si la encuentra, te preguntará si deseas conservarla o sobreescribirla. Si no existe, se crea un agente nuevo automáticamente. - -### Paso 3: Inspeccionar el agente en Azure AI Foundry - -**Antes de interactuar con Anders**, ve al portal para inspeccionar lo que se creó: - -1. Abre [Azure AI Foundry](https://ai.azure.com) y navega a tu proyecto -2. En el menú lateral, selecciona **Agents** -3. Busca el agente **"Anders"** y haz clic en él - -Observa dos cosas clave: - -- **System prompt (instrucciones):** Verás las instrucciones completas que le dimos al agente, incluyendo el schema JSON. Esto es lo que guía su comportamiento al decidir cuándo y cómo invocar la API. -- **Tools (herramientas):** Verás **ContosoRetailAPI** listada como herramienta OpenAPI. Puedes expandirla para ver la especificación completa con el endpoint `ordersReporter`, los schemas de request/response, y la configuración de autenticación anónima. - -> [!TIP] -> El system prompt y las tools son los dos pilares que determinan qué puede hacer un agente y cómo lo hace. Entender esta relación es clave para diseñar agentes efectivos. - -### Paso 4: Probar el agente - -De vuelta en la consola, pruébalo primero con un saludo: - -``` -Tú: Hola Anders, ¿qué puedes hacer? -``` - -Anders debería responder explicando que puede generar reportes de órdenes. Luego, prueba con datos reales (pega todo en una sola línea): - -``` -Genera un reporte para Izabella Celma (periodo: 1-31 enero 2026). Orden ORD-CID-069-001 (2026-01-04): Sport-100 Helmet Black, Contoso Outdoor, Helmets, 6x$34.99=$209.94 | HL Road Frame Red 62, Contoso Outdoor, Road Frames, 10x$1431.50=$14315.00 | Long-Sleeve Logo Jersey S, Contoso Outdoor, Jerseys, 8x$49.99=$399.92. Orden ORD-CID-069-003 (2026-01-08): HL Road Frame Black 58, Contoso Outdoor, Road Frames, 3x$1431.50=$4294.50 | HL Road Frame Red 44, Contoso Outdoor, Road Frames, 7x$1431.50=$10020.50. Orden ORD-CID-069-002 (2026-01-17): HL Road Frame Red 62, Contoso Outdoor, Road Frames, 2x$1431.50=$2863.00 | LL Road Frame Black 60, Contoso Outdoor, Road Frames, 4x$337.22=$1348.88. -``` - -Lo que ocurre internamente: -1. Anders analiza el mensaje y decide que necesita llamar al endpoint `ordersReporter` -2. **Foundry ejecuta la llamada HTTP** automáticamente a la Function App con los datos estructurados según el schema -3. La Function App genera el reporte HTML, lo sube a Blob Storage y retorna la URL -4. Foundry envía el resultado de vuelta al modelo -5. Anders formula su respuesta y presenta la URL al usuario - -Abre la URL del reporte en el navegador para verificar que se generó correctamente. - -Ahora prueba con un caso más sencillo — un solo pedido con dos productos: - -``` -Tú: Genera un reporte para Marco Rivera (periodo: 5-10 febrero 2026). Orden ORD-CID-112-001 (2026-02-07): Mountain Bike Socks M, Contoso Outdoor, Socks, 3x$9.50=$28.50 | Water Bottle 30oz, Contoso Outdoor, Bottles and Cages, 1x$6.99=$6.99. -``` - -> **Nota:** Al escribir `salir`, solo se termina la conversación. El agente **persiste** en Foundry y se reutiliza automáticamente en la siguiente ejecución. - ---- - -## Solución de problemas - -### Storage Account bloqueado por política (error 503) - -En suscripciones con políticas estrictas de Azure, el Storage Account que respalda la Function App puede tener su **acceso público de red deshabilitado** automáticamente después del aprovisionamiento. Esto impide que el host de Functions alcance su propio almacenamiento, causando un error persistente **503 (Site Unavailable)** — aunque la app reporte como `Running` y `Enabled`. - -**Síntomas:** -- La Function App aparece como `Running` en el Portal de Azure y CLI -- Todas las restricciones de acceso de red muestran "Allow all" -- Cada solicitud HTTP a cualquier endpoint retorna 503 después de un timeout de ~60 segundos - -**Diagnóstico:** -```bash -az storage account show --name stcontosoretail --resource-group rg-contoso-retail --query "publicNetworkAccess" -o tsv -``` - -Si retorna `Disabled`, esa es la causa raíz. - -**Solución:** - -Se incluye un script de conveniencia en el repositorio: - -```bash -cd es/labs/foundry/setup -pwsh ./unlock-storage.ps1 -``` - -El script detecta automáticamente el sufijo desde la Function App. Si necesitas forzarlo, también acepta `-Suffix` o `-FunctionAppName`. - -Este script habilita el acceso público de red en el Storage Account y reinicia la Function App. Ver [unlock-storage.ps1](setup/unlock-storage.ps1) para detalles. - ---- - -## Challenge: Respuestas en streaming - -Actualmente el chat de Anders espera a que el agente complete toda su respuesta antes de mostrarla. Esto puede generar una pausa perceptible cuando el modelo razona y ejecuta la herramienta OpenAPI. - -**Tu reto:** modifica el loop de chat en `ms-foundry/Program.cs` para que la respuesta de Anders se imprima token a token a medida que llega, usando la API de streaming. - -### Pista - -El SDK expone `CreateResponseStreamingAsync()` como alternativa a `CreateResponse()`. Devuelve un `IAsyncEnumerable` de eventos que puedes iterar para imprimir cada fragmento de texto conforme llega: - -```csharp -await foreach (var update in responseClient.CreateResponseStreamingAsync(input)) -{ - // Filtra los eventos de tipo delta de texto e imprime el fragmento -} -``` - -Los eventos que contienen texto son de tipo `StreamingResponseOutputTextDeltaUpdate` (namespace `OpenAI.Responses`, ya importado), y su propiedad con el fragmento se llama `Delta`. - -### Criterio de éxito - -- La respuesta de Anders aparece progresivamente en la consola, letra a letra (o fragmento a fragmento), sin esperar a que complete toda la respuesta. -- El prompt `Tú:` solo aparece una vez que Anders termina de responder. -- El comportamiento de `salir` y el manejo de errores se mantienen igual que antes. - ---- - -## Siguiente paso - -Continúa con el [Lab 4 — Julie (Planner Agent)](lab04-julie-planner-agent.md). diff --git a/es/labs/foundry/lab04-anders-executor-agent.md b/es/labs/foundry/lab04-anders-executor-agent.md new file mode 100644 index 0000000..5d8a444 --- /dev/null +++ b/es/labs/foundry/lab04-anders-executor-agent.md @@ -0,0 +1,232 @@ +# Lab 4: Anders — Executor Agent + +## Tabla de contenido + +- [Lab 4: Anders — Executor Agent](#lab-4-anders--executor-agent) + - [Tabla de contenido](#tabla-de-contenido) + - [Introducción](#introducción) + - [¿Qué vamos a hacer en este lab?](#qué-vamos-a-hacer-en-este-lab) + - [Verificar endpoints expuestos](#paso-2-verificar-endpoints-expuestos) + - [Endpoints OpenAPI generados](#endpoints-openapi-generados) + - [3.2 — Verificar la especificación OpenAPI](#32--verificar-la-especificación-openapi) + - [Obtener la especificación JSON](#obtener-la-especificación-json) + - [Explorar el Swagger UI](#explorar-el-swagger-ui) + - [3.3 — El agente Anders](#33--el-agente-anders-dos-versiones-de-sdk) + - [Paso 1: Entendiendo el código (`ms-foundry/`)](#entendiendo-el-código-versión-ms-foundry--recomendada) + - [Paso 2: Inspeccionar el agente en Azure AI Foundry](#paso-3-inspeccionar-el-agente-en-azure-ai-foundry) + - [Paso 3: Probar el agente](#paso-4-probar-el-agente) + - [Notas Importantes](#notas-impotantes) + +--- + +## Introducción + +Anders es el **agente ejecutor** de la arquitectura multi-agéntica de Contoso Retail. Su rol es recibir solicitudes de acciones operativas — como la generación y publicación de reportes de órdenes — y ejecutarlas interactuando con servicios externos como la Azure Function `FxContosoRetail`. + +Para que Anders pueda interactuar con la API de Contoso Retail, usaremos una **Microsoft Foundry Tool** que permite al agente descubrir e invocar automáticamente los endpoints de la Function App a partir de su especificación OpenAPI. Adicionalmente, agregaremos soporte **OpenAPI** a la Function App para documentar la API y facilitar la exploración de sus endpoints. + +### ¿Qué vamos a hacer en este lab? + +### Paso 1: Verificar endpoints expuestos + +Abre [`es/labs/foundry/code/api/FxContosoRetail/FxContosoRetail.cs`](../code/api/FxContosoRetail/FxContosoRetail.cs) y confirma que existen estos endpoints: + +- `HolaMundo` +- `OrdersReporter` +- `SqlExecutor` + +Además, valida que `OrdersReporter` y `SqlExecutor` tengan atributos OpenAPI (`OpenApiOperation`, `OpenApiRequestBody`, `OpenApiResponseWithBody`). Justamente estos cambios son los que necesitarás hacer cuando desees exponer tus Azure Functions existentes como herramientas OpenAPI para los agentes. + +> [!IMPORTANT] +> **Sobre la autenticación de los endpoints** +> +> En este taller usamos `AuthorizationLevel.Anonymous` para simplificar la configuración y permitir que Azure AI Foundry pueda invocar la Function App directamente como OpenAPI Tool sin necesidad de gestionar secrets ni configurar autenticación adicional. +> +> **En un entorno de producción, esto no es recomendable.** La práctica correcta es proteger la Function App con **Azure Entra ID (Easy Auth)** y hacer que Foundry se autentique usando **Managed Identity**. El flujo sería: +> +> 1. **Registrar una aplicación en Entra ID** que represente la Function App, obteniendo un Application (client) ID y un Application ID URI (por ejemplo, `api://`). +> 2. **Habilitar Easy Auth** en la Function App con `az webapp auth update`, configurándola para validar tokens emitidos por Entra ID contra la app registration. Esto protege todos los endpoints a nivel de plataforma — las peticiones sin un bearer token válido se rechazan con 401 antes de llegar al código. +> 3. **Asignar permisos a la Managed Identity** del recurso de AI Services (`ais-contosoretail-{suffix}`) como principal autorizado en la app registration, ya sea agregándola como miembro de un app role o como identidad permitida en la configuración de Easy Auth. +> 4. **Usar `OpenApiManagedAuthDetails`** en el código del agente en lugar de `OpenApiAnonymousAuthDetails`, especificando el audience de la app registration: +> ```csharp +> openApiAuthentication: new OpenApiManagedAuthDetails( +> audience: "api://") +> ``` +> +> Con esta configuración, cuando Foundry necesita llamar a la Function App, obtiene un token de Entra ID usando la managed identity del recurso de AI Services, lo envía como `Authorization: Bearer `, y Easy Auth lo valida automáticamente. Los endpoints de la Function pueden mantener `AuthorizationLevel.Anonymous` en el código C# porque la autenticación ocurre en la capa de plataforma. + +### Endpoints OpenAPI generados + +Una vez desplegada, la Function App expondrá estos endpoints adicionales: + +| Endpoint | Descripción | +|----------|-------------| +| `/api/openapi/v3.json` | Especificación OpenAPI 3.0 en formato JSON | +| `/api/swagger/ui` | Interfaz Swagger UI interactiva | + +--- + +## 3.2 — Verificar la especificación OpenAPI + +Una vez desplegada, verifica que los endpoints OpenAPI están disponibles. + +### Obtener la especificación JSON + +Abre en el navegador o con `curl`: + +``` +https://func-contosoretail-.azurewebsites.net/api/openapi/v3.json +``` + +Deberías ver un JSON con la estructura OpenAPI que describe los endpoints `HolaMundo`, `OrdersReporter` y `SqlExecutor`, incluyendo los esquemas de request/response. + +### Explorar el Swagger UI + +Navega a: + +``` +https://func-contosoretail-.azurewebsites.net/api/swagger/ui +``` + +Desde la interfaz de Swagger UI puedes explorar los endpoints y probarlos interactivamente. + +> **Importante:** La especificación OpenAPI documenta la API y sirve como referencia para entender qué parámetros enviar y qué respuesta esperar. El agente Anders usará esta información indirectamente a través de la Function Tool que definiremos en el siguiente paso. + +--- + +## 3.3 — El agente Anders + +La implementación del agente Anders se proporciona bajo `es/labs/foundry/code/agents/AndersAgent/ms-foundry`: + +### Entendiendo el código (versión `ms-foundry/` — recomendada) + +Abre el archivo `es/labs/foundry/code/agents/AndersAgent/ms-foundry/Program.cs` y observa como está organizado + +### Paso 1: Configuración del Entorno en GitHub Codespaces + +1. Abre el repositorio en **GitHub Codespaces**. +2. Autentícate en tu cuenta de Azure desde la terminal: + ```bash + az login --use-device-code + ``` + +### Paso 2: Variables de Entorno de Azure AI Foundry + +Desde el portal de Azure AI Foundry, navega a la sección Overview de tu proyecto y obtén los siguientes valores para configurarlos en tu terminal de Codespaces: + +# Reemplaza los valores entre comillas con tu información real + +``` +export FOUNDRY_PROJECT_ENDPOINT="https://ais-contosoretail-XXXX.services.ai.azure.com/..." +export FOUNDRY_MODEL_DEPLOYMENT_NAME="gpt-4.1" +``` + +Las siguientres variables de entorno son opcionales, no es necesario crearlas. Pero si lo consideras necesario lo puede hacer para cambiar el nombre del agente y su compotanmiento. + +``` +export FOUNDRY_AGENT_NAME="AndersAgent" +export FOUNDRY_AGENT_INSTRUCTIONS="Eres un agente analítico especializado en lectura, comprensión y extracción de insights a partir de información proporcionada." +``` + +### Paso 3: Despliegue del Agente + +Ejecuta el script de despliegue para registrar el agente en el servicio: + +``` +./es/labs/foundry/code/agents/AndersAgent/ms-foundry/deploy-foundry-agent.sh +``` + +### Paso 4: Configuración de Herramientas (Portal) + +Una vez desplegado, sigue estos pasos en el portal web: + +Ve a Build > Agents y selecciona AndersAgent. + +Añadir Herramienta OpenAPI: + +Haz clic en Add Tool > OpenAPI tool. + +Ingresa el nombre de la herramienta (ej. myapitool). + +Pega la definición JSON de la API (Swagger/OpenAPI) de tu servicio (ej. una Azure Function que consulta pedidos). + +``` +https://func-contosoretail-.azurewebsites.net/api/openapi/v3.json +``` + +Haz clic en Create tool y no olvider precionar "Save" o "Guardar los cambios del agente, si no lo hacer tendras un error al probarlo. + + +### Paso 5: Inspeccionar el agente en Azure AI Foundry + +**Antes de interactuar con Anders**, ve al portal para inspeccionar lo que se creó: + +1. Abre [Azure AI Foundry](https://ai.azure.com) y navega a tu proyecto +2. En el menú lateral, selecciona **Agents** +3. Busca el agente **"Anders"** y haz clic en él + +Observa dos cosas clave: + +- **System prompt (instrucciones):** Verás las instrucciones completas que le dimos al agente, incluyendo el schema JSON. Esto es lo que guía su comportamiento al decidir cuándo y cómo invocar la API. +- **Tools (herramientas):** Verás **myapitool** listada como herramienta OpenAPI. Puedes expandirla para ver la especificación completa con el endpoint `ordersReporter`, los schemas de request/response, y la configuración de autenticación anónima. + +> [!TIP] +> El system prompt y las tools son los dos pilares que determinan qué puede hacer un agente y cómo lo hace. Entender esta relación es clave para diseñar agentes efectivos. + +### Paso 6: Validación y Pruebas + +Inicia una sesión de chat en el Playground del agente y prueba con prompts de negocio como: + +Para iniciar pregunata a Ander que pude hacer + +``` +Hola Anders, ¿qué puedes hacer? +``` + +Anders debería responder explicando que puede generar reportes de órdenes, aAhora pidele un reporte + +``` +Genera un reporte para el cliente Marco Rivera del periodo de enero a febrero de 2026. Muestra los pedidos en una tabla y calcula el total gastado. +``` + +Resultados Esperados: +El agente debe identificar qué herramienta llamar, pero necesita de algunos datos adiconales como: + +- Número de orden +- Fecha de la orden +- Producto, marca, categoría +- Cantidad, precio unitario, total por línea + + +Luego, prueba con datos reales (pega todo en una sola línea): + +``` +Genera un reporte para Izabella Celma (periodo: 1-31 enero 2026). Orden ORD-CID-069-001 (2026-01-04): Sport-100 Helmet Black, Contoso Outdoor, Helmets, 6x$34.99=$209.94 | HL Road Frame Red 62, Contoso Outdoor, Road Frames, 10x$1431.50=$14315.00 | Long-Sleeve Logo Jersey S, Contoso Outdoor, Jerseys, 8x$49.99=$399.92. Orden ORD-CID-069-003 (2026-01-08): HL Road Frame Black 58, Contoso Outdoor, Road Frames, 3x$1431.50=$4294.50 | HL Road Frame Red 44, Contoso Outdoor, Road Frames, 7x$1431.50=$10020.50. Orden ORD-CID-069-002 (2026-01-17): HL Road Frame Red 62, Contoso Outdoor, Road Frames, 2x$1431.50=$2863.00 | LL Road Frame Black 60, Contoso Outdoor, Road Frames, 4x$337.22=$1348.88. +``` + +Lo que ocurre internamente: +1. Anders analiza el mensaje y decide que necesita llamar al endpoint `ordersReporter` por medio del tool configurado. +2. **Foundry Tool ejecuta la llamada HTTP** automáticamente a la Function App con los datos estructurados según el schema +3. La Function App genera el reporte HTML, lo sube a Blob Storage y retorna la URL +4. Foundry envía el resultado de vuelta al modelo +5. Anders formula su respuesta y presenta la URL al usuario + +Abre la URL del reporte en el navegador para verificar que se generó correctamente. + +Ahora prueba con un caso más sencillo — un solo pedido con dos productos: + +``` +Genera un reporte para Marco Rivera (periodo: 5-10 febrero 2026). Orden ORD-CID-112-001 (2026-02-07): Mountain Bike Socks M, Contoso Outdoor, Socks, 3x$9.50=$28.50 | Water Bottle 30oz, Contoso Outdoor, Bottles and Cages, 1x$6.99=$6.99. +``` + +--- + +## Notas Importantes + +Cierre de sesión: Al finalizar, recuerda detener tu Codespace para evitar consumos innecesarios. + +Regiones: Verifica que tu proyecto de Foundry esté en una región que soporte agentes (ej. East US, East US 2). + +## Siguiente paso + +Continúa con el [Lab 5 — Laboratrios de Copilot Studio](lab05-mcs-setup.md). diff --git a/es/labs/foundry/lab04-julie-planner-agent.md b/es/labs/foundry/lab05-julie-planner-agent.md similarity index 99% rename from es/labs/foundry/lab04-julie-planner-agent.md rename to es/labs/foundry/lab05-julie-planner-agent.md index 9619e51..2521c29 100644 --- a/es/labs/foundry/lab04-julie-planner-agent.md +++ b/es/labs/foundry/lab05-julie-planner-agent.md @@ -1,8 +1,8 @@ -# Lab 4: Julie Planner Agent +# Lab 5: Julie Planner Agent ## Tabla de contenido -- [Lab 4: Julie Planner Agent](#lab-4-julie-planner-agent) +- [Lab 5: Julie Planner Agent](#lab-5-julie-planner-agent) - [Tabla de contenido](#tabla-de-contenido) - [Introducción](#introducción) - [Continuidad del setup](#continuidad-del-setup) diff --git a/es/labs/foundry/setup.md b/es/labs/foundry/setup.md index 269a964..239595b 100644 --- a/es/labs/foundry/setup.md +++ b/es/labs/foundry/setup.md @@ -237,8 +237,8 @@ labs/foundry/ | Lab | Archivo | Descripción | | ----- | --------------------------------------------------------- | ------------------------------------------------------------ | -| Lab 3 | [Anders — Executor Agent](lab03-anders-executor-agent.md) | Crear el agente ejecutor que genera reportes e interactúa con servicios de Contoso Retail. | -| Lab 4 | [Julie — Planner Agent](lab04-julie-planner-agent.md) | Crear el agente orquestador de campañas de marketing usando el patrón workflow con sub-agentes (SqlAgent, MarketingAgent) y herramienta OpenAPI. | +| Lab 4 | [Anders — Executor Agent](lab04-anders-executor-agent.md) | Crear el agente ejecutor que genera reportes e interactúa con servicios de Contoso Retail. | +| Lab 5 | [Julie — Planner Agent](lab05-julie-planner-agent.md) | Crear el agente orquestador de campañas de marketing usando el patrón workflow con sub-agentes (SqlAgent, MarketingAgent) y herramienta OpenAPI. | --- diff --git a/es/readme.md b/es/readme.md index 4e4dd43..64b644e 100644 --- a/es/readme.md +++ b/es/readme.md @@ -115,17 +115,17 @@ El workshop está dividido en laboratorios independientes pero conectados, organ ### 2. Laboratorios de Azure AI Foundry -- [Setup de infraestructura de Foundry](./labs/foundry/codespaces-setup.md) -- [Lab 3 – Agente Anders: soporte OpenAPI, despliegue de Function App y ejecución del agente executor](./labs/foundry/lab03-anders-executor-agent.md) -- [Lab 4 – Agente Julie: workflow agent con sub-agentes SqlAgent y MarketingAgent](./labs/foundry/lab04-julie-planner-agent.md) +- [Lab 3 – Setup de infraestructura de Foundry](./labs/foundry/codespaces-setup.md) +- [Lab 4 – Agente Anders: soporte OpenAPI, despliegue de Function App y ejecución del agente executor](./labs/foundry/lab04-anders-executor-agent.md) +- [Lab 5 – Agente Julie: workflow agent con sub-agentes SqlAgent y MarketingAgent (Opcional)](./labs/foundry/lab05-julie-planner-agent.md) ### 3. Laboratorios de Copilot Studio -- [Lab 5 – Setup de Copilot Studio: entorno, solución y publisher](./labs/copilot/lab05-mcs-setup.md) -- [Lab 6 – Agente Charles: Q&A de producto con SharePoint y análisis de mercado](./labs/copilot/lab06-charles-copilot-agent.md) -- [Lab 7 – Agente Ric: agente hijo para envío de correos + configuración inicial de Bill](./labs/copilot/lab07-ric-child-agent.md) -- [Lab 8 – Orquestador Bill: conexión de agentes externos (Mark, Anders) e internos (Charles) y reglas de orquestación](./labs/copilot/lab08-bill-orchestrator.md) -- [Lab 9 – Publicación de Bill en Microsoft 365 / Teams y pruebas end-to-end](./labs/copilot/lab09-bill-publishing.md) +- [Lab 6 – Setup de Copilot Studio: entorno, solución y publisher](./labs/copilot/lab06-mcs-setup.md) +- [Lab 7 – Agente Charles: Q&A de producto con SharePoint y análisis de mercado](./labs/copilot/lab07-charles-copilot-agent.md) +- [Lab 8 – Agente Ric: agente hijo para envío de correos + configuración inicial de Bill](./labs/copilot/lab08-ric-child-agent.md) +- [Lab 9 – Orquestador Bill: conexión de agentes externos (Mark, Anders) e internos (Charles) y reglas de orquestación](./labs/copilot/lab09-bill-orchestrator.md) +- [Lab 10 – Publicación de Bill en Microsoft 365 / Teams y pruebas end-to-end](./labs/copilot/lab10-bill-publishing.md) ---