Ir al contenido
  1. Articulos/

Dapr Agents en un juego conversacional con .NET: llevando D&D Copilot de demo a sistema distribuido

La mayoría de demos con agentes funcionan muy bien hasta que aparece el primer problema real: hay que persistir contexto, exponer capacidades por HTTP, desacoplar eventos, aguantar reinicios y, además, mantener el sistema operable por un equipo backend normal.

Ahí es donde Dapr Agents resulta interesante. No tanto por la palabra “agent”, sino porque aterriza el problema sobre piezas de plataforma que ya conocemos: state stores, pub/sub, workflows, APIs y sidecars.

En este artículo voy a contar cómo encaja esa idea en un proyecto real: D&D Copilot, un juego conversacional con backend en ASP.NET Core, frontend en React y una capa de NPCs que ya adopta varios conceptos de Dapr Agents aunque la aplicación esté implementada en .NET.

Qué es Dapr Agents, sin vender humo #

La conversación sobre Dapr Agents suele atascarse demasiado pronto en una pregunta equivocada: “¿dónde está la librería de C#?”.

Para mí, la clave es otra: Dapr Agents es antes un patrón que una librería. La documentación oficial usa Python porque hoy es donde Dapr está mostrando primero este framework y sus quickstarts de agentes, pero lo que realmente describe es una forma de construir agentes sobre Dapr apoyándose en capacidades del runtime que son polyglot por diseño.

Según la documentación oficial, la propuesta gira alrededor de varios conceptos muy claros:

  • Agent: agente conversacional síncrono con memoria y tools.
  • DurableAgent: agente respaldado por workflows para ejecuciones largas, recuperables y tolerantes a fallos.
  • AgentRunner: pieza que lo expone como ejecución programática, suscripción pub/sub o servicio HTTP con endpoints tipo POST /agent/run y GET /agent/instances/{id}.

Si reducimos eso a arquitectura, lo que aparece no es una dependencia mágica de Python, sino un conjunto de convenciones:

  • un endpoint estándar para interactuar con el agente,
  • un ciclo de razonamiento,
  • tools expuestas como capacidades invocables,
  • memoria sobre un state store,
  • integración con pub/sub, workflows, bindings y service invocation.

Eso no depende del lenguaje. El runtime de Dapr es el mismo y el SDK de .NET ya cubre las piezas necesarias: state, pub/sub, service invocation, secrets, bindings y workflows.

Por eso, aunque la experiencia oficial de Dapr Agents sea hoy Python-first, sí tenemos en C# los building blocks necesarios para implementar patrones arquitectónicos equivalentes sobre Dapr.

De hecho, una arquitectura mínima de agente sobre Dapr en .NET puede verse así:

app.MapPost("/agent", async (AgentRequest req, DaprClient dapr) =>
{
  var memory = await dapr.GetStateAsync<string>("statestore", "agent-memory");

  var llmResponse = await agentBrain.ReasonAsync(req.Input, memory);

  if (llmResponse.ToolToCall is not null)
  {
    var result = await dapr.InvokeMethodAsync<object>(
      llmResponse.ToolToCall.Service,
      llmResponse.ToolToCall.Method,
      llmResponse.ToolToCall.Payload
    );

    await dapr.SaveStateAsync("statestore", "agent-memory", result?.ToString());
    return Results.Ok(result);
  }

  return Results.Ok(llmResponse.Output);
});

app.MapPost("/tools/getWeather", (WeatherRequest req) =>
{
  return Results.Ok(new { forecast = "Soleado", temp = 22 });
});

No es una copia literal del SDK de Python. Es la traducción del patrón a un stack .NET, que al final es lo que de verdad importa en un sistema productivo.

El caso real: D&D Copilot #

D&D Copilot combina tres piezas:

  • Una API ASP.NET Core que gestiona autenticación, personajes, sesiones de juego y lógica principal.
  • Un cliente React que expone el flujo de login, dashboard, creación de personaje y partida.
  • Un subsistema de NPCs que usa Dapr para conversación y persistencia de estado.

La aplicación se puede arrancar localmente con backend, frontend y sidecar de Dapr. En mi caso, tras levantar Docker para Redis, el proyecto ha quedado funcionando con esta combinación:

  • API en http://localhost:5101
  • Frontend en http://localhost:3000
  • Sidecar Dapr en http://localhost:3500

La experiencia de usuario ya deja claro que no estamos ante un simple chatbot incrustado en una web, sino ante un juego donde el agente participa dentro del flujo de dominio.

Pantalla de acceso de D&D Copilot

Una vez autenticado, el dashboard expone personajes, creación de nuevas fichas y utilidades como el lanzador de dados.

Dashboard de D&D Copilot

Y al entrar en partida, el jugador ya consume directamente decisiones y respuestas del sistema de juego en un contexto persistente.

Partida en curso dentro de The Rusty Dragon Tavern

La parte más interesante aparece cuando el juego hace visible el ciclo ODA dentro de la propia experiencia. En la acción Look Around, el sistema muestra explícitamente cómo observa el entorno, decide una acción y la ejecuta.

Captura del flujo Observe Decide Act en la acción Look Around

Ese mismo patrón reaparece en la Forja cuando Marcus evalúa el contexto y recomienda una compra concreta.

Captura del flujo ODA con Marcus en la Forja

Dónde aparece Dapr Agents en este proyecto #

Lo interesante de este repositorio no es que copie la API exacta del framework oficial, sino que mapea bastante bien sus conceptos base.

1. Perfil del agente #

El proyecto define un NpcAgentProfile con nombre, rol, objetivo, instrucciones y localización. Eso encaja directamente con la noción de perfil de agente en Dapr Agents: identidad + objetivo + reglas de comportamiento.

Además, los perfiles no están hardcodeados en clases gigantes, sino cargados desde documentación viva. Los NPCs se describen en docs/NPCS.md, y el backend los carga al iniciar la aplicación. Ese detalle me parece especialmente acertado porque separa mejor el comportamiento narrativo del cableado técnico.

En otras palabras: el proyecto trata al NPC como una unidad de comportamiento configurable, no como un switch enorme escondido en un controlador.

2. Memoria persistente con Dapr State Store #

Uno de los puntos más sólidos del enfoque de Dapr Agents es que la memoria no debería depender del proceso. Aquí eso se refleja en un componente Dapr de estado llamado npc-statestore, configurado sobre Redis.

La idea es simple y potente: cada sesión del jugador y cada NPC pueden persistir historial conversacional bajo una clave estable. Cuando el servicio reinicia, el contexto no desaparece con la memoria del proceso.

La implementación usa un cliente específico para la API HTTP de Dapr y guarda la conversación con una clave del estilo agent-memory-{npc}-{sessionId}. En la práctica eso aproxima bastante bien el concepto oficial de ConversationDaprStateMemory.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: npc-statestore
spec:
  type: state.redis
  version: v1
  metadata:
    - name: redisHost
      value: localhost:6379

Ese punto tiene una consecuencia operativa importante: en local, si Redis no está disponible, el sidecar no puede inicializar el componente y el arranque falla. Es exactamente el tipo de comportamiento que conviene detectar pronto en desarrollo, porque obliga a tratar la infraestructura como parte del sistema agentic y no como un detalle opcional.

3. Integración LLM a través de Dapr Conversation API #

La pieza que conecta con el modelo no llama directamente a un SDK de proveedor desde cualquier sitio del código. En su lugar, encapsula la inferencia en un cliente DaprConversationAiClient que utiliza DaprConversationClient y un componente Dapr de conversación.

Eso también está muy alineado con la documentación oficial, que presenta la Conversation API como la puerta de entrada unificada a los modelos.

En el proyecto, el componente configurado es openai, apuntando a Azure OpenAI. La configuración exacta de secretos en producción debería externalizarse a un secret store o variables de entorno, pero el patrón de integración es correcto: el backend habla con Dapr, y Dapr resuelve el proveedor.

var aiResponse = await _foundryAiClient.GenerateCompletionAsync(conversationContext, new FoundryAiParameters
{
    Temperature = 0.7,
    MaxTokens = 400,
    SystemPrompt = systemPrompt
});

Esta abstracción es útil por tres razones:

  • evita acoplar el dominio del juego a un proveedor concreto,
  • centraliza logging y fallback,
  • y deja abierta la puerta a cambiar la implementación sin rehacer la lógica de NPCs.

4. Tools y ciclo de razonamiento #

La documentación de Dapr Agents insiste bastante en que un agente no es solo un prompt con esteroides. Tiene tools, memoria y un ciclo de decisión.

En D&D Copilot eso se ve en el servicio NpcAgentService, que organiza el flujo alrededor de cuatro ideas:

  • construir el prompt del agente a partir del perfil,
  • recuperar memoria de la conversación,
  • decidir o generar respuesta con el modelo,
  • persistir el nuevo turno y emitir eventos.

Además, el proyecto expone explícitamente un patrón observe -> decide -> act, lo cual me parece una decisión muy didáctica. No coincide exactamente con la ergonomía del framework oficial de Python, pero sí con su intención: dividir percepción, razonamiento y ejecución.

Y no es solo una abstracción interna. En la ejecución real del juego ese patrón ya aflora visualmente. La captura de Look Around es especialmente valiosa porque enseña el paso completo:

  • Observe: el personaje resume el entorno.
  • Decide: selecciona acción, objetivo y justificación.
  • Act: ejecuta el resultado y materializa cambios en el juego.

Eso convierte el sistema en algo mucho más auditable que un único prompt opaco.

Ese flujo queda visible incluso en la API:

  • POST /api/NpcAgent/observe
  • POST /api/NpcAgent/decide
  • POST /api/NpcAgent/act

Swagger lo muestra con bastante claridad:

Swagger con los endpoints del juego y del agente NPC

Para un equipo enterprise esto es valioso porque hace explícito algo que muchas demos esconden dentro de una sola llamada opaca al modelo.

5. Eventos y desacoplo #

Otro rasgo muy Dapr es el uso de eventos para desacoplar interacciones. El servicio del NPC publica eventos como npc.interaction, npc.observed o npc.decided.

Ahora bien, aquí conviene ser preciso: en el estado actual del proyecto, la publicación está registrada mediante StubDaprEventPublisher por defecto en desarrollo. Es decir, el diseño está pensado para pub/sub, pero no toda la tubería está operando todavía como un sistema event-driven completo en producción.

Y esto no es un defecto menor ni un detalle cosmético. Es exactamente el tipo de frontera que marca la diferencia entre:

  • un proyecto que ya adoptó el patrón correcto,
  • y un proyecto que ya terminó la industrialización de ese patrón.

Qué aporta Dapr aquí de verdad #

Después de revisar el código y ejecutar la aplicación, estas son las ventajas que sí veo materializadas:

Menos pegamento de infraestructura #

El backend no tiene que implementar a mano una mini-plataforma de memoria remota, llamadas a modelos, resolución de componentes y wiring distribuido. Dapr absorbe parte de ese trabajo.

Mejores límites de arquitectura #

El dominio del juego puede pensar en conceptos como sesión, NPC, contexto o acciones disponibles, mientras que la infraestructura de conversación y estado queda detrás de interfaces bastante razonables.

Evolución incremental #

Esto me parece especialmente relevante: el proyecto no necesita saltar de cero a un DurableAgent completo para empezar a ganar valor. Puede avanzar por etapas:

  1. Agente con memoria persistente.
  2. Tools y ciclo observe/decide/act.
  3. Pub/sub real para eventos de juego.
  4. Workflows para sesiones largas o multiagente.

Ese recorrido encaja muy bien con la recomendación oficial de Dapr Agents: empezar por patrones más controlados y subir autonomía solo cuando la plataforma ya soporta la complejidad.

Lo que todavía no es Dapr Agents “full framework” #

También hay que decirlo con claridad: este proyecto no está usando todavía la experiencia empaquetada del framework oficial tal y como se documenta hoy en Python.

No veo en el backend actual piezas equivalentes a:

  • DurableAgent
  • AgentRunner.serve() con POST /agent/run y GET /agent/instances/{id}
  • Workflows para checkpoint, retry y recuperación de ejecuciones largas
  • routing pub/sub automático al estilo runner.subscribe(...)

Lo que sí hay es una adopción práctica de los conceptos fundamentales desde .NET: perfil, memoria, inferencia, herramientas, exposición HTTP y publicación de eventos.

Para mí, esa distinción no resta valor. Al contrario: deja una lectura mucho más útil para equipos que están en .NET y quieren aprovechar Dapr hoy, sin esperar a una plantilla oficial que les diga cómo estructurarlo.

Cómo lo evolucionaría a partir de aquí #

Si este proyecto quisiera acercarse más al extremo “durable” del espectro de Dapr Agents, estas serían mis siguientes decisiones:

1. Sustituir el stub por pub/sub real #

El sistema ya genera eventos con sentido de dominio. El siguiente paso lógico es declararlos sobre un componente real de pub/sub y permitir que otros servicios o agentes reaccionen a ellos.

2. Pasar de interacciones síncronas a workflows durables #

Hay acciones del juego que encajan muy bien con ejecuciones largas:

  • quests multi-etapa,
  • decisiones diferidas de NPCs,
  • exploración encadenada,
  • coordinación entre varios agentes especializados.

Aquí Dapr Workflows aportaría checkpoints, reintentos y trazabilidad real. Y aunque gran parte de la documentación de agentic patterns los ilustre con Python, el salto conceptual sigue siendo totalmente válido en .NET porque el runtime y los building blocks son los mismos.

3. Introducir orquestación multiagente #

El propio dominio de D&D Copilot se presta a ello:

  • un agente tabernero para rumor y questing,
  • un agente explorador para observación del entorno,
  • un agente comerciante para economía,
  • un orquestador que enrute la intención del jugador.

Eso encaja con patrones como Routing o Orchestrator-Workers descritos en la documentación oficial.

4. Endurecer la configuración para producción #

Especialmente en estos puntos:

  • secretos del proveedor LLM fuera de componentes en claro,
  • políticas de retención para memoria conversacional,
  • observabilidad con trazas y métricas,
  • separación entre componentes de desarrollo y producción.

Por qué este caso me parece interesante #

Lo mejor de este repositorio es que baja Dapr Agents a un terreno muy tangible. En vez de un asistente genérico de viajes o soporte, lo aplica a un juego con estado, acciones, sesiones, NPCs y contexto narrativo.

Eso obliga a responder preguntas que cualquier sistema agentic serio acaba encontrando:

  • ¿dónde vive la memoria?
  • ¿cómo se separa el perfil del agente del código de infraestructura?
  • ¿qué parte decide el LLM y cuál decide el dominio?
  • ¿cómo se exponen las capacidades del agente?
  • ¿qué ocurre cuando el proceso reinicia?

Y lo hace con una conclusión bastante pragmática: un agente útil no es solo un prompt; es una pieza de software distribuido.

Cierre #

Dapr Agents me parece valioso no porque convierta cualquier cosa en “autónoma”, sino porque obliga a modelar agentes sobre cimientos operables: memoria, tools, exposición, durabilidad y mensajería.

En D&D Copilot ya se ve ese camino con bastante claridad. La implementación actual en .NET no replica todavía todo el framework oficial, pero sí adopta una parte importante de su filosofía: usar Dapr como plataforma para que los agentes dejen de ser una demo frágil y empiecen a comportarse como componentes de un sistema real.

Y esa, para mí, es la parte importante.

Enjoy coding!