Dapr Agents

repository·main·Indexed 20 days ago

https://github.com/dapr/dapr-agents

A developer framework for building production-grade, resilient, and scalable AI agent systems. Built on top of Dapr, it leverages the Dapr workflow engine, state management, and messaging capabilities to ensure agentic workflows complete successfully. It includes tools like DaprChatClient for provider-agnostic LLM interactions and AnthropicChatClient for native Anthropic features, supporting structured outputs via Pydantic and prompt management via Prompty files.

Tokens
49.7K
Snippets
152
Records
217
Agent score
72%

What's inside dapr-agents

  1. How MCP STDIO transport works in Dapr Agents

    main

    STDIO transport allows an agent to communicate with MCP tool servers using standard input/output streams. This is ideal for local development as it requires no network ports or HTTP servers.

    Execution Flow:

    1. The agent starts the Workflow runtime and renders Dapr components.
    2. MCPClient.connect_stdio() launches the tool module (e.g., tools.py) as a subprocess.
    3. The client discovers available tools and translates them into agent-compatible tools.
    4. When an LLM decides to call a tool, the request is dispatched over STDIO.
    5. Results are returned to the conversation loop and persisted in Dapr state stores.
  2. Use middleware hooks for MCP tool calls

    main

    The MCPServer specification allows wiring workflow-based middleware hooks around tool calls. These hooks enable cross-cutting concerns like security and observability to be implemented as reusable workflows.

    Hook Types

    • beforeCallTool: Executed before the tool is invoked. If a beforeCallTool hook returns an error, the tool call is aborted.
    • afterCallTool: Executed after the tool invocation. These hooks can log errors or results without affecting the actual tool output.

    Common Middleware Implementations

    WorkflowHook TypePurpose
    rate_limit_workflowbeforeCallToolImplements per-tool rate limits (e.g., 30/min) using a state-store counter.
    input_validation_workflowbeforeCallToolRejects arguments containing malicious patterns like SQL injection or XSS.
    audit_log_workflowafterCallToolWrites every tool invocation and its result to the state store for auditing.
  3. How Agent Coordination and Discovery works

    main

    Multi-agent systems in Dapr Agents rely on a shared registry and specific messaging patterns for coordination.

    1. Shared Registry

    Agents use AgentRegistryConfig with the same team_name to discover each other. This allows orchestrators to query the registry for available members.

    registry = AgentRegistryConfig(
        store=StateStoreService(store_name="agentregistrystore"),
        team_name="fellowship",  # Must be identical for all agents in the team
    )

    2. Agent Discovery

    An orchestrator can find all members of a team using the registry:

    # Returns a list of available agents, e.g., [frodo, sam, gandalf, legolas]
    available_agents = registry.get_team_members("fellowship")

    3. Message Flow Patterns

    • Via Orchestrator: Client $\rightarrow$ Orchestrator Topic $\rightarrow$ Orchestrator Logic $\rightarrow$ Agent Topic $\rightarrow$ Specific Agent
    • Direct to Agent: Client $\rightarrow$ Agent Topic $\rightarrow$ Specific Agent
    • Broadcast: All agents subscribed to a team-wide broadcast topic (e.g., fellowship.broadcast) receive the message simultaneously.
  4. Hot-reload OpenTelemetry configuration via Dapr Configuration Store

    main

    The otel-configstore agent variant supports real-time OTel configuration updates by subscribing to the Dapr Configuration Store. This allows you to change observability settings without restarting the agent pods.

    Supported Configuration Keys

    KeyTypeDescription
    otel_sdk_disabledbooltrue disables OTel entirely
    otel_exporter_otlp_endpointstringCollector endpoint (e.g. http://collector:4317)
    otel_exporter_otlp_headersstringAuth token / headers for the exporter
    otel_service_namestringService name for traces/logs
    otel_tracing_enabledboolEnable/disable tracing
    otel_traces_exporterstringotlp_grpc, otlp_http, zipkin, console
    otel_logging_enabledboolEnable/disable log export
    otel_logs_exporterstringotlp_grpc, otlp_http, console

    Example: Runtime Updates

    To update configuration at runtime, use redis-cli to set the keys in the Redis instance used by the Dapr Configuration Store.

    NAMESPACE=dapr-agents
    REDIS_POD=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=redis -o jsonpath='{.items[0].metadata.name}')
    
    # Switch to a new collector endpoint
    kubectl exec $REDIS_POD -n $NAMESPACE -- redis-cli SET otel_exporter_otlp_endpoint "http://new-collector:4317"
    
    # Disable OTel
    kubectl exec $REDIS_POD -n $NAMESPACE -- redis-cli SET otel_sdk_disabled "true"
    
    # Re-enable with a different exporter
    kubectl exec $REDIS_POD -n $NAMESPACE -- redis-cli SET otel_sdk_disabled "false"
    kubectl exec $REDIS_POD -n $NAMESPACE -- redis-cli SET otel_traces_exporter "otlp_http"
  5. How MCPServer resources and auto-discovery work

    main

    An MCPServer is a first-class Dapr resource that declares connection details for a Model Context Protocol (MCP) server.

    Key Characteristics:

    • Sidecar Management: The Dapr sidecar handles all transport (e.g., sse), authentication, and retries. The agent does not connect to the MCP server directly.
    • Auto-discovery: A DurableAgent can automatically discover tools by querying the sidecar's metadata API. This eliminates the need for manual DaprMCPClient wiring. When a resource is loaded, the sidecar registers workflows such as dapr.internal.mcp.<resource_name>.ListTools and dapr.internal.mcp.<resource_name>.CallTool.<tool_name>.

    Example Resource Configuration (Conceptual): An MCPServer resource (defined in a YAML file) specifies the transport and the available tools (e.g., get_weather, get_forecast).

  6. Implement web-enriched context using the `before_llm_call` hook

    main

    You can create an 'Expert Agent' that automatically enriches user queries with real-time web data by using a before_llm_call hook. This pattern allows the agent to query a search engine (like Tavily) before the LLM is invoked, injecting the search results into the prompt as fresh context. This prevents the model from hedging on recent events or information postdating its training cutoff.

    Workflow:

    1. The user asks a question.
    2. The before_llm_call hook triggers a web search based on that question.
    3. The search results are injected into the prompt.
    4. The LLM generates a response grounded in the retrieved context.
  7. How the @message_router decorator works

    main

    The @message_router decorator is applied directly to a workflow function to turn it into a Pub/Sub handler. This eliminates the need for a separate handler function and manual workflow scheduling.

    When a message arrives on the specified topic:

    1. The workflow is automatically scheduled.
    2. The incoming message is automatically validated against the provided message_model (e.g., a Pydantic model).
    3. Optional hooks (payload_filter, model_filter, mapper) are executed to decide if the workflow should run and how the input should be shaped.

    Important Constraints:

    • Blocking Nature: Hooks run on the per-topic consumer thread. They must be fast, in-memory, and side-effect-free. Do not perform external I/O (like database lookups or HTTP calls) inside hooks; instead, perform those checks inside the workflow body.
    • No Async Hooks: async def hooks are rejected at decoration time because they would still block the consumer thread.
    @message_router(
        pubsub="messagepubsub",
        topic="blog.requests",
        message_model=StartBlogMessage,
    )
    def my_workflow(ctx, wf_input: dict) -> str:
        ...
  8. Use DaprChatClient to call LLMs

    main

    The DaprChatClient is the primary interface for interacting with Large Language Models (LLMs) via Dapr's Conversation API. It allows you to write provider-agnostic code that can switch between different LLM backends (like OpenAI, Anthropic, or local echo components) simply by changing Dapr component configurations, without modifying your application logic.

    Key capabilities include:

    • Provider Agnosticism: Switch LLM providers via Dapr components.
    • Prompt Caching: Reduce latency and costs.
    • PII Obfuscation: Mask sensitive information.
    • Resilience: Built-in support for retries, timeouts, and circuit breaking via Dapr resiliency policies.
    from dapr_agents.llm import DaprChatClient
    from dapr_agents.types import UserMessage
    
    # Basic usage
    llm = DaprChatClient()
    response = llm.generate("Name a famous dog!")
    
    # Using a prompty file for context
    llm = DaprChatClient.from_prompty("basic.prompty")
    response = llm.generate(input_data={"question": "What is your name?"})
    
    # Using explicit messages
    response = llm.generate(messages=[UserMessage("hello")])
  9. Understand Multi-Agent Orchestration Patterns

    main

    Dapr Agents supports different orchestration_mode values within the DurableAgent class to coordinate multiple autonomous agents:

    1. Random Orchestration (orchestration_mode="random")

      • Randomly selects an agent for each task with avoidance logic.
      • Best for load distribution and testing.
      • Uses topic: fellowship.orchestrator.random.requests.
    2. Round-Robin Orchestration (orchestration_mode="roundrobin")

      • Cycles through agents sequentially in a deterministic order.
      • Ensures fair task distribution.
      • Uses topic: fellowship.orchestrator.roundrobin.requests.
    3. Agent Orchestration (orchestration_mode="agent")

      • Formerly known as "LLM Orchestrator".
      • Uses AI-powered planning to select the best agent based on task content and execution plans.
      • Uses topic: llm.orchestrator.requests.
  10. Use Agents as Tools in Workflows

    main

    Dapr Agents allows one DurableAgent to call another agent as a synchronous child workflow tool. When an LLM selects an agent as a tool, the framework schedules the target agent's workflow as a Dapr child workflow. The result of the target agent's execution is returned to the caller as a ToolMessage, integrating seamlessly into the caller's conversation history and durable execution model.

    There are three primary ways to configure an agent to act as a tool for another agent:

    1. Explicit Cross-App Factory: Use agent_to_tool to wire agents that live in separate Dapr applications. This method does not require a shared registry.
    2. Direct Instance Passing (In-Process): Pass a DurableAgent instance directly into the tools list of another agent. This is used when both agents run in the same Dapr app/Python process; the framework auto-converts the instance into an AgentWorkflowTool.
    3. Shared Registry (Auto-discovery): If both agents are configured with a registry, the framework automatically discovers peer agents at the start of a workflow run. The load_tools mechanism scans the registry and registers all peers (excluding orchestrators and self) in the tool_executor.
    from dapr_agents.tool.workflow.agent_tool import agent_to_tool
    
    # 1. Explicit cross-app factory (no registry needed)
    sam_tool = agent_to_tool(
        "sam",
        description="Sam Gamgee. Goal: Manage provisions.",
        target_app_id="SamApp",
    )
    frodo = DurableAgent(name="frodo", tools=[sam_tool], ...)
    
    # 2. Pass DurableAgent instance directly (auto-converted, same app)
    sam = DurableAgent(name="sam", registry=registry, ...)
    frodo = DurableAgent(name="frodo", tools=[sam], ...)
    
    # 3. Shared registry (auto-discovery)
    sam = DurableAgent(name="sam", registry=registry, ...)
    frodo = DurableAgent(name="frodo", registry=registry, ...)
  11. When to use Pydantic vs dataclasses

    main

    The project follows a specific mental model of "config vs data" to decide between Pydantic and dataclasses:

    Use Pydantic for Data

    Use Pydantic for data that crosses trust boundaries or is persisted. This includes:

    • API payloads
    • Pub/sub messages
    • Persisted state (workflow state, timeline messages, trigger/broadcast schemas, tool records, etc.)
    • Schemas requiring coercion, validation, or versioned migrations.

    Use dataclasses for Config

    Use dataclasses for configuration and dependency injection. This includes:

    • Agent construction knobs passed in code (e.g., agent config classes).
    • Dependency injection of services, stores, policies, and behavior hooks.
  12. Enable Hot-Reload for DurableAgent using Configuration Stores

    main

    You can enable a DurableAgent to perform zero-downtime updates to its persona and settings by subscribing to a Dapr Configuration Store. This is achieved using RuntimeSubscriptionConfig in your agent implementation.

    When the agent starts, it first calls get_configuration() to load all existing values from the store. Once initialized, it subscribes to the store to receive real-time updates. When a key in the configuration store changes, the agent receives the update and applies the new configuration (e.g., updating its agent_role or max_iterations).

    # Conceptual implementation pattern
    # The agent implementation uses RuntimeSubscriptionConfig to subscribe to Dapr configuration changes.