Este es el episodio 2 de una serie de 6: arrancamos el proyecto en .NET 10 que va a terminar siendo un agente de voz de Telegram para una clínica dental. Al final del video tienes el proyecto corriendo, seis paquetes NuGet instalados y las API keys guardadas donde nadie las va a filtrar por accidente.
Pero hay dos decisiones en este setup que parecen relleno y no lo son. La primera: qué carpetas creas, y cuál de ellas vas a terminar abandonando sin usar ni una vez. La segunda: en qué versión exacta fijas dos paquetes que, si no coinciden número por número, hacen que tu proyecto truene con un TypeLoadException que no te va a decir nada útil.
Vamos por partes.
Console App, no Web API
Casi todo tutorial de agentes de IA en .NET arranca con dotnet new webapi. Tiene sentido cuando el agente responde a peticiones HTTP entrantes. El nuestro no. Recibe mensajes de Telegram por long polling y le habla de vuelta con audio: no hay ningún cliente esperando una respuesta HTTP, no hay ningún endpoint que exponer.
Montar eso sobre ASP.NET Core significa arrastrar Kestrel, middleware, un pipeline de requests que nunca vas a usar, y un modelo de hosting pensado para escalar horizontalmente detrás de un load balancer. Nada de eso resuelve el problema real: un proceso que escucha un socket de Telegram y mantiene la conexión abierta.
El proyecto se crea como Console App:
File → New → Project → Console App (C#)
Project name: ClinicaDentalSonrisa.TelegramBotFramework: .NET 10.0Sin overhead, sin middleware que explicar antes de escribir la primera línea de lógica del agente.
Tres carpetas, una que nunca se usó
Antes de instalar nada, el proyecto se organiza en tres carpetas:
Add → New Folder → AgentAdd → New Folder → VoiceAdd → New Folder → TelegramVoice/ va a tener el VoiceService que convierte texto en audio con Azure. Telegram/ va a tener el BotHandler que procesa los mensajes entrantes. Agent/ se crea pensando en el Microsoft Agent Framework, que llega en el episodio 4.
Aquí va la parte honesta: Agent/ nunca se usó. Puedes revisar el repositorio final del proyecto y esa carpeta no existe. Cuando llegó el momento de construir el agente, terminó viviendo directo en Program.cs, en un bloque de quince líneas que arma el ChatClientAgent y lo pasa al BotHandler por constructor. No hizo falta una carpeta, ni una clase propia, ni una abstracción adicional.
Esto no es un error de planificación. Es la carpeta vacía haciendo su trabajo: si al final del episodio 4 hubiera aparecido lógica real de orquestación, routing entre agentes o memoria persistente, Agent/ habría tenido sentido. Como no apareció, no forzamos una carpeta para justificar el nombre que le pusimos en el episodio 2. Preferimos borrar la carpeta a rellenarla con una clase que solo existe para que la carpeta no esté vacía.
El archivo business-info.md (el conocimiento del negocio que el agente va a usar para responder) va en la raíz del proyecto, no en ninguna de las tres carpetas. No es infraestructura; es contenido, y vive donde se edita fácil.
Seis paquetes NuGet, cero de más
Con la estructura lista, toca las dependencias. Seis paquetes, cada uno con una responsabilidad que no se solapa con la de otro:
| Paquete | Versión | Para qué sirve |
|---|---|---|
Google.GenAI | 1.6.2 | SDK oficial de Google: el modelo de lenguaje que piensa las respuestas |
Microsoft.Agents.AI | 1.3.0 | Construye y orquesta el agente sobre el chat client |
Microsoft.Extensions.AI | 10.5.0 | Interfaz común (IChatClient) entre el modelo y el agente |
Microsoft.Extensions.AI.Abstractions | 10.5.0 | Los tipos base de esa interfaz |
Microsoft.CognitiveServices.Speech | 1.50.0 | Azure Speech: convierte texto en audio con voz humana |
Telegram.Bot | 22.10.0.1 | Maneja la comunicación con la API de Telegram |
El .csproj completo:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <UserSecretsId>83ba8506-2171-4f43-908d-c574755a2f41</UserSecretsId> </PropertyGroup>
<ItemGroup> <PackageReference Include="Google.GenAI" Version="1.6.2" /> <PackageReference Include="Microsoft.Agents.AI" Version="1.3.0" /> <PackageReference Include="Microsoft.CognitiveServices.Speech" Version="1.50.0" /> <!-- estos dos SIEMPRE en el mismo número exacto --> <PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" /> <PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.5.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" /> <PackageReference Include="Telegram.Bot" Version="22.10.0.1" /> </ItemGroup>
</Project>Fíjate en el comentario sobre Microsoft.Extensions.AI y Microsoft.Extensions.AI.Abstractions. No es estilo. Microsoft.Extensions.AI depende internamente de tipos que vienen de Abstractions, y NuGet no siempre resuelve la versión más nueva de ambos si uno se actualiza y el otro se queda atrás, sobre todo si otra dependencia transitiva del proyecto (como Microsoft.Agents.AI) trae su propia referencia a Abstractions en una versión distinta. Cuando eso pasa, el runtime carga dos versiones binarias incompatibles del mismo tipo, y el fallo no aparece en tiempo de compilación: aparece en tiempo de ejecución, la primera vez que el código toca IChatClient, como un TypeLoadException que menciona un assembly y una versión sin explicar por qué eso es un problema.
La regla es simple de aplicar y fácil de olvidar: cada vez que subas uno de los dos paquetes, sube el otro al mismo número exacto en el mismo commit.
Secrets fuera del código: tres claves que después serán seis
Las API keys nunca van en el código ni en appsettings.json. Visual Studio tiene soporte nativo para User Secrets: un secrets.json que vive fuera del repositorio, en la máquina local, referenciado por el UserSecretsId del .csproj de arriba.
dotnet user-secrets set "Telegram:BotToken" "<tu-token>"dotnet user-secrets set "Google:ApiKey" "<tu-api-key>"dotnet user-secrets set "Azure:SpeechKey" "<tu-speech-key>"dotnet user-secrets set "Azure:SpeechRegion" "eastus"Cuatro claves en este episodio, no seis. El proyecto final termina con Google:LanguageModel y Azure:VoiceName, que aparecen más adelante en el curso cuando el modelo de Gemini y la voz de Azure dejan de estar fijos en el código y pasan a leerse desde configuración. Configurarlas ahora sería adelantar contexto que todavía no existe. Mejor que cada clave aparezca cuando el código realmente la necesita.
Con las claves puestas, Program.cs en este punto es deliberadamente mínimo:
using Microsoft.Extensions.Configuration;
var config = new ConfigurationBuilder() .AddUserSecrets<Program>() .Build();
Console.WriteLine("✅ Proyecto base listo.");Verde en pantalla
F5, y la consola imprime ✅ Proyecto base listo. Eso es todo lo que este episodio necesita demostrar: el proyecto compila en .NET 10, las seis dependencias están instaladas con las versiones correctas, y la configuración lee las claves sin exponer una sola de ellas en el código fuente.
No hay agente todavía. No hay voz todavía. No hay bot de Telegram todavía. Eso es exactamente el punto: el episodio 3 agrega la voz humana en español con Azure Neural TTS, y ahí es donde Azure:VoiceName entra en escena.
Si estás siguiendo el curso: clona la estructura, fija las dos versiones de Microsoft.Extensions.AI en el mismo número, y antes de seguir al episodio 3 corre el proyecto una vez y confirma que ves el mensaje en verde. Es más barato descubrir un mismatch de versiones aquí, con quince líneas de código, que tres episodios después con un agente completo encima.