Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ jobs:
run: |
pip cache purge
pip install --upgrade pip setuptools wheel
pip install -e .
pip install -e .[test]
- name: Check Typing
run: |
tox -e type
- name: Run Tests
run: |
tox -e pytest
tox -e pytest
- name: Run Integration Tests
run: |
tox -e integration
200 changes: 196 additions & 4 deletions dapr_agents/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
from dapr_agents.llm.utils.defaults import get_default_llm
from dapr_agents.memory import ConversationDaprStateMemory, ConversationListMemory
from dapr_agents.prompt.base import PromptTemplateBase
from dapr_agents.registry import (
RegistryMixin,
AgentMetadata,
ComponentMappings,
StateStoreComponent,
PubSubComponent,
ToolDefinition,
TOOL_TYPE_FUNCTION,
TOOL_TYPE_MCP,
TOOL_TYPE_AGENT,
TOOL_TYPE_UNKNOWN,
)
from dapr_agents.storage.daprstores.stateservice import StateStoreError
from dapr_agents.tool.base import AgentTool
from dapr_agents.tool.executor import AgentToolExecutor
Expand All @@ -27,7 +39,7 @@
logger = logging.getLogger(__name__)


class AgentBase(AgentComponents):
class AgentBase(AgentComponents, RegistryMixin):
"""
Base class for agent behavior.

Expand All @@ -40,6 +52,9 @@ class AgentBase(AgentComponents):
Infrastructure (pub/sub, durable state, registry) is provided by `AgentComponents`.
"""

# Class attribute for agent category override (can be overridden by subclasses)
_agent_category_override: Optional[str] = None

def __init__(
self,
*,
Expand All @@ -55,7 +70,12 @@ def __init__(
# Components (infrastructure)
pubsub_config: Optional[AgentPubSubConfig] = None,
state_config: Optional[AgentStateConfig] = None,
registry_config: Optional[AgentRegistryConfig] = None,
registry_config: Optional[
AgentRegistryConfig
] = None, # TODO: Consolidate with agent_registry_config into unified RegistryConfig
agent_registry_config: Optional[
AgentRegistryConfig
] = None, # TODO: Part of unified RegistryConfig (design TBD)
base_metadata: Optional[Dict[str, Any]] = None,
max_etag_attempts: int = 10,
# Memory / runtime
Expand Down Expand Up @@ -83,7 +103,10 @@ def __init__(

pubsub_config: Pub/Sub config used by `AgentComponents`.
state_config: Durable state config used by `AgentComponents`.
registry_config: Team registry config used by `AgentComponents`.
registry_config: Team registry config used by `AgentComponents` for pub/sub addressing (backward compatible).
In future versions, will be merged into a unified registry configuration.
agent_registry_config: Agent metadata registry config for agent discovery (optional, independent).
In future versions, will be merged into a unified registry configuration.
execution_config: Execution dials for the agent run.
base_metadata: Default Dapr state metadata used by `AgentComponents`.
max_etag_attempts: Concurrency retry count for registry mutations.
Expand Down Expand Up @@ -118,6 +141,10 @@ def __init__(
max_etag_attempts=max_etag_attempts,
)

# TODO: Future improvement - merge registry_config and agent_registry_config into single
# unified configuration that supports multiple named registry stores. Keep current dual
# config for backward compatibility. See dapr_agents/registry/README.md#future-improvements

# -----------------------------
# Memory wiring
# -----------------------------
Expand Down Expand Up @@ -191,7 +218,7 @@ def __init__(
logger.warning("Agent failed to load persisted state; starting fresh.")

# -----------------------------
# Agent metadata & registry registration (from AgentComponents)
# Agent metadata & team registry registration (from AgentComponents)
# -----------------------------
base_meta: Dict[str, Any] = {
"name": self.name,
Expand All @@ -218,6 +245,16 @@ def __init__(
"Registry configuration not provided; skipping agent registration."
)

# -----------------------------
# Agent registry registration (agent metadata discovery)
# -----------------------------
self._agent_registry_config = agent_registry_config
if self._agent_registry_config is not None:
try:
self._register_agent_metadata()
except Exception as exc: # noqa: BLE001
logger.warning("Could not register agent in agent registry: %s", exc)

# ------------------------------------------------------------------
# Presentation helpers
# ------------------------------------------------------------------
Expand Down Expand Up @@ -648,6 +685,161 @@ def list_team_agents(
team=team,
)

# ------------------------------------------------------------------
# Agent Registry (RegistryMixin implementation)
# ------------------------------------------------------------------
def _build_agent_metadata(self) -> Optional[AgentMetadata]:
"""
Build agent metadata for registry registration.

Returns:
AgentMetadata instance or None if registration should be skipped.
"""
try:
# Extract component mappings
components = self._extract_component_mappings()

# Extract tool definitions
tools = self._extract_tool_definitions()

# Determine agent category (use override if set, otherwise default to "agent")
agent_category = self._agent_category_override or "agent"

# Build metadata
metadata = AgentMetadata(
name=self.name,
role=self.profile_config.role,
goal=self.profile_config.goal,
tool_choice=self.execution_config.tool_choice,
instructions=list(self.profile_config.instructions)
if self.profile_config.instructions
else None,
tools=tools,
components=components,
system_prompt=self.profile_config.system_prompt or "",
agent_id=str(id(self)),
agent_framework="dapr-agents",
agent_class=self.__class__.__name__,
agent_category=agent_category,
dapr_app_id=None, # Will be auto-detected by Registry
namespace=None,
sub_agents=[],
)
return metadata
except Exception as exc:
logger.warning("Failed to build agent metadata: %s", exc, exc_info=True)
return None

def _extract_component_mappings(self) -> ComponentMappings:
"""
Extract component mappings from agent configuration.

Returns:
ComponentMappings instance with state stores and pub/sub components.
"""
state_stores: Dict[str, StateStoreComponent] = {}
pubsub_components: Dict[str, PubSubComponent] = {}

# Extract state store from state_config
if self._state_config is not None and self._state_config.store is not None:
state_stores["workflow"] = StateStoreComponent(
name=self._state_config.store.store_name,
usage="Durable workflow state storage",
)

# Extract state store from agent_registry_config
if (
self._agent_registry_config is not None
and self._agent_registry_config.store is not None
):
state_stores["agent_registry"] = StateStoreComponent(
name=self._agent_registry_config.store.store_name,
usage="Agent metadata discovery registry",
)

# Extract state store from team registry_config
if (
self._registry_config is not None
and self._registry_config.store is not None
):
state_stores["team_registry"] = StateStoreComponent(
name=self._registry_config.store.store_name,
usage="Team pub/sub addressing registry",
)

# Extract state store from memory_config (if it's Dapr-backed)
if hasattr(self.memory, "store_name"):
state_stores["memory"] = StateStoreComponent(
name=self.memory.store_name, # type: ignore
usage="Conversation memory storage",
)

# Extract pub/sub from pubsub_config
if self._pubsub_config is not None:
pubsub_components["default"] = PubSubComponent(
name=self._pubsub_config.pubsub_name,
usage="Agent messaging and broadcasts",
topic_name=self._pubsub_config.agent_topic or self.name,
)

return ComponentMappings(
state_stores=state_stores,
pubsub_components=pubsub_components,
)

def _extract_tool_definitions(self) -> List[ToolDefinition]:
"""
Extract tool definitions from agent's tools.

Returns:
List of ToolDefinition instances.
"""
tool_defs: List[ToolDefinition] = []

for tool in self.tools:
try:
# Determine tool type and extract metadata
if isinstance(tool, AgentTool):
tool_type = tool.tool_type # Use the tool's declared type
name = tool.name
description = tool.description
elif callable(tool):
# Check if it's an MCP tool (heuristic: has mcp-related attributes)
if hasattr(tool, "__mcp__") or (
hasattr(tool, "__module__")
and "mcp" in str(tool.__module__).lower()
):
tool_type = TOOL_TYPE_MCP
else:
tool_type = TOOL_TYPE_FUNCTION
name = getattr(tool, "__name__", str(tool))
description = (
getattr(tool, "__doc__", "No description") or "No description"
)
# Clean up description
description = (
description.strip().split("\n")[0]
if description
else "No description"
)
else:
tool_type = TOOL_TYPE_UNKNOWN
name = str(tool)
description = "Unknown tool type"

tool_defs.append(
ToolDefinition(
name=name,
description=description,
tool_type=tool_type,
)
)
except Exception as exc:
logger.debug("Failed to extract tool definition: %s", exc)
continue

return tool_defs

# ------------------------------------------------------------------
# Misc helpers
# ------------------------------------------------------------------
Expand Down
14 changes: 12 additions & 2 deletions dapr_agents/agents/durable.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class DurableAgent(AgentBase):

"""

# Override agent category for workflow-based agents
_agent_category_override: Optional[str] = "durable-agent"

def __init__(
self,
*,
Expand All @@ -61,7 +64,12 @@ def __init__(
# Infrastructure
pubsub_config: AgentPubSubConfig,
state_config: Optional[AgentStateConfig] = None,
registry_config: Optional[AgentRegistryConfig] = None,
registry_config: Optional[
AgentRegistryConfig
] = None, # TODO: Part of unified RegistryConfig
agent_registry_config: Optional[
AgentRegistryConfig
] = None, # TODO: Part of unified RegistryConfig
# Memory / runtime
memory_config: Optional[AgentMemoryConfig] = None,
llm: Optional[ChatClientBase] = None,
Expand All @@ -87,7 +95,8 @@ def __init__(

pubsub_config: Dapr Pub/Sub configuration for triggers/broadcasts.
state_config: Durable state configuration and model customization.
registry_config: Team registry configuration.
registry_config: Team registry configuration for pub/sub addressing.
agent_registry_config: Agent metadata registry config for agent discovery (optional, independent).
execution_config: Execution dials for the agent run.

memory_config: Conversation memory config; defaults to in-memory, or Dapr state-backed if available.
Expand All @@ -109,6 +118,7 @@ def __init__(
state_config=state_config,
memory_config=memory_config,
registry_config=registry_config,
agent_registry_config=agent_registry_config,
execution_config=execution_config,
agent_metadata=agent_metadata,
llm=llm,
Expand Down
Loading
Loading