# # Copyright 2026 The Dapr Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import functools import logging from typing import Any, Optional from pydantic import BaseModel, Field from dapr_agents.tool.workflow.tool_context import WorkflowContextInjectedTool from dapr_agents.workflow.utils.names import sanitize_agent_name logger = logging.getLogger(__name__) AGENT_WORKFLOW_SUFFIX = "_agent_workflow" # kept for backward compat def agent_workflow_id( agent_name: str, framework: Optional[str] = None, workflow_name: Optional[str] = None, ) -> str: """ Return the Dapr-registered workflow name for an agent. Priority order: 1. If workflow_name is provided, use it directly 2. If agent_name is already a full workflow name (starts with 'dapr.' and ends with '.workflow'), return it as-is 3. If framework is provided and not "Dapr Agents", use 'dapr.{framework}.{agent_name}.workflow' 4. Otherwise, use the standard format: 'dapr.agents.{agent_name}.workflow' Agent names are sanitized to comply with OpenAI's name requirements (no spaces, <, |, \\, /, >) before constructing workflow names. This supports: - Other agentic frameworks (e.g., "dapr.openai.catering-coordinator.workflow") - Dapr Agents framework (e.g., "dapr.agents.catering-coordinator.workflow") - Explicit workflow names passed directly Args: agent_name: The agent name (e.g., "catering-coordinator", "Samwise Gamgee") framework: Optional framework name (e.g., "openai", "pydantic_ai", "langgraph", "crewai"). If None or "Dapr Agents", uses the standard dapr.agents.* format. workflow_name: Optional explicit workflow name. If provided, this takes precedence over all other parameters. Returns: The Dapr-registered workflow name with sanitized agent name. """ from dapr_agents.tool.utils.function_calling import sanitize_openai_tool_name # Priority 1: Explicit workflow name if workflow_name: return workflow_name # Priority 2: Already a full workflow name if agent_name.startswith("dapr.") and agent_name.endswith(".workflow"): return agent_name # Sanitize agent name to comply with OpenAI requirements # This ensures workflow names don't contain invalid characters sanitized_agent_name = sanitize_openai_tool_name(agent_name) # Priority 3: Use framework if provided and not "Dapr Agents" if framework and framework != "Dapr Agents": # Normalize framework name (replace spaces/underscores with dots if needed) normalized_framework = framework.replace(" ", "-").replace("_", "-").lower() return f"dapr.{normalized_framework}.{sanitized_agent_name}.workflow" # Priority 4: Default to dapr.agents.* format return f"dapr.agents.{sanitized_agent_name}.workflow" class AgentTaskArgs(BaseModel): """Arguments accepted by AgentWorkflowTool.""" task: str = Field( ..., description="The instruction or task to send to the agent.", ) class AgentWorkflowTool(WorkflowContextInjectedTool): """ A WorkflowContextInjectedTool that invokes another DurableAgent as a synchronous child workflow. The parent agent's LLM calls this tool with a ``task`` string; the tool schedules a child workflow named ``{target_agent_name}_agent_workflow`` (optionally on a different Dapr app via ``target_app_id``) and waits for the child agent's final response. The workflow context (``ctx``) is injected by the dispatch loop and is never exposed in the LLM provider's function-call schema. """ target_agent_name: str target_app_id: Optional[str] = None """ Dapr app-id of the app hosting the target agent. ``None`` means the target agent is in the same Dapr app (in-process); cross-app invocation requires appropriate Dapr access-control policies. """ def _schedule_agent_workflow( ctx: Any, task: str, agent_name: str, agent_type: str = "agents", target_app_id: Optional[str] = None, _source_agent: Optional[str] = None, workflow_name: Optional[str] = None, framework: Optional[str] = None, _child_instance_id: Optional[str] = None, ) -> Any: """ Schedule a child workflow for a named agent. This is intentionally a *sync* function that returns a Dapr workflow Task. The parent workflow yields on it:: result = yield tool_obj(ctx=ctx, task="...") Args: ctx: Dapr workflow context supplied by the dispatch loop. task: The instruction to forward to the child agent. agent_name: Registered name of the target agent. agent_type: Framework/type prefix for the workflow name (e.g. "strands", "agents"). target_app_id: Dapr app-id for cross-app routing; ``None`` for in-process. _source_agent: Name of the calling agent; forwarded in ``_message_metadata`` so the child agent labels the user message as "on behalf of". workflow_name: Optional explicit workflow name. Takes precedence over framework. framework: Optional framework name for constructing workflow name. _child_instance_id: Explicit instance ID to assign to the child workflow. When provided, this exact ID is used so callers can record it in ``tool_history`` before the yield completes. """ input_payload: dict = {"task": task} if _source_agent: input_payload["_message_metadata"] = {"source": _source_agent} workflow_id = agent_workflow_id( agent_name, framework=framework, workflow_name=workflow_name ) call_kwargs: dict = { "workflow": workflow_id, "input": input_payload, } if target_app_id: call_kwargs["app_id"] = target_app_id if _child_instance_id: call_kwargs["instance_id"] = _child_instance_id logger.debug( "Scheduling child workflow '%s' app_id=%r task=%r", workflow_id, target_app_id, task, ) return ctx.call_child_workflow(**call_kwargs) def agent_to_tool( agent_name: str, description: str, *, agent_type: str = "agents", target_app_id: Optional[str] = None, workflow_name: Optional[str] = None, framework: Optional[str] = None, ) -> AgentWorkflowTool: """ Create an AgentWorkflowTool for a named agent. This is the explicit factory for cases where you know the agent name and (optionally) its Dapr app ID — no registry lookup is performed. Use it for cross-app agents where you have a known ``target_app_id``, or for advanced scenarios where you want full control. For registry-based auto-discovery, simply register both agents in the same registry; the parent agent's ``load_tools`` activity will pick up all registry peers automatically at workflow start. Args: agent_name: The name of the target agent (e.g., ``"catering-coordinator"``). This value is used to derive the tool name exposed to the LLM; the underlying :class:`AgentTool` normalizes it (title-casing, removing spaces and underscores), so e.g. ``"my agent"`` becomes ``"MyAgent"``. It should correspond to the agent's registered name under this normalization, rather than needing to match character-for-character. description: Human-readable description shown to the LLM in the tool schema (e.g. ``"Ring-bearer. Goal: carry the One Ring to Mordor."``). agent_type: Framework/type prefix for the workflow name (e.g. "strands", "agents"). Defaults to "agents" for backward compatibility. target_app_id: Dapr app-id of the app hosting the target agent. Pass ``None`` (default) for in-process invocation where both agents are registered in the same Dapr application. workflow_name: Optional explicit workflow name (e.g., ``"dapr.openai.catering-coordinator.workflow"``). If provided, this takes precedence over framework-based construction. framework: Optional framework name (e.g., ``"openai"``, ``"pydantic_ai"``). Used to construct workflow name as ``dapr.{framework}.{agent_name}.workflow``. If None or "Dapr Agents", uses ``dapr.agents.{agent_name}.workflow``. Returns: AgentWorkflowTool ready to be registered in a DurableAgent's toolset. Example — explicit cross-app:: from dapr_agents.tool.workflow import agent_to_tool sam_tool = agent_to_tool( "sam", "Logistics & Support. Goal: Manage provisions and supplies.", target_app_id="sam-app", ) frodo = DurableAgent(name="frodo", tools=[sam_tool], ...) Example — with framework:: catering_tool = agent_to_tool( "catering-coordinator", "Coordinates catering services.", framework="openai", ) # This will call workflow: dapr.openai.CateringCoordinator.workflow """ executor = functools.partial( _schedule_agent_workflow, agent_name=agent_name, agent_type=agent_type, target_app_id=target_app_id, workflow_name=workflow_name, framework=framework, ) setattr( executor, "__name__", agent_name ) # partial has no __name__; AgentTool validator reads it return AgentWorkflowTool( name=agent_name, description=description, func=executor, args_model=AgentTaskArgs, target_agent_name=agent_name, target_app_id=target_app_id, )