From 47001274804a45715c658d691d9bec21a513ea0c Mon Sep 17 00:00:00 2001 From: Clayton Brown Date: Mon, 20 Apr 2026 23:41:13 +1000 Subject: [PATCH] feat(024-generic-agent-interface-contrib): add AgentRegistry for pluggable agent discovery --- .../tasks.md | 2 +- tradingagents/agents/__init__.py | 2 + tradingagents/agents/registry.py | 63 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tradingagents/agents/registry.py diff --git a/.kiro/specs/active/024-generic-agent-interface-contrib/tasks.md b/.kiro/specs/active/024-generic-agent-interface-contrib/tasks.md index daf56c3b..3c2ae693 100644 --- a/.kiro/specs/active/024-generic-agent-interface-contrib/tasks.md +++ b/.kiro/specs/active/024-generic-agent-interface-contrib/tasks.md @@ -13,6 +13,6 @@ No standardized input/output contract for agents. Hard to swap, compose, or benc - [x] 2. Define AgentOutput schema: rating (5-tier), confidence, price_targets, thesis, risk_factors - [x] 3. Create BaseAgent abstract class with analyze(input) -> output contract - [x] 4. Refactor existing agents (fundamentals, sentiment, news, technical) to implement BaseAgent -- [ ] 5. Create AgentRegistry for pluggable agent discovery +- [x] 5. Create AgentRegistry for pluggable agent discovery - [ ] 6. Add agent benchmarking: compare outputs across different LLM backends - [ ] 7. Document interface for third-party agent contributions diff --git a/tradingagents/agents/__init__.py b/tradingagents/agents/__init__.py index 0360e6c0..a81db0e4 100644 --- a/tradingagents/agents/__init__.py +++ b/tradingagents/agents/__init__.py @@ -1,4 +1,5 @@ from .base_agent import BaseAgent +from .registry import AgentRegistry from .utils.agent_utils import create_msg_delete from .utils.agent_states import AgentState, InvestDebateState, RiskDebateState from .utils.memory import FinancialSituationMemory @@ -28,6 +29,7 @@ from .managers.portfolio_manager import create_portfolio_manager from .trader.trader import create_trader __all__ = [ + "AgentRegistry", "BaseAgent", "FundamentalsAgent", "SentimentAgent", diff --git a/tradingagents/agents/registry.py b/tradingagents/agents/registry.py new file mode 100644 index 00000000..97ec2729 --- /dev/null +++ b/tradingagents/agents/registry.py @@ -0,0 +1,63 @@ +"""AgentRegistry for pluggable agent discovery.""" + +from __future__ import annotations + +from typing import Callable + +from .base_agent import BaseAgent + + +class AgentRegistry: + """Registry that maps agent names to their factory callables. + + Usage:: + + registry = AgentRegistry() + registry.register("fundamentals", FundamentalsAgent, llm=my_llm) + agent = registry.get("fundamentals") + output = agent.analyze(agent_input) + + Agents can be registered either as a pre-built instance or as a class + (with optional ``**kwargs`` forwarded to the constructor on first access). + """ + + def __init__(self) -> None: + self._factories: dict[str, Callable[[], BaseAgent]] = {} + self._instances: dict[str, BaseAgent] = {} + + def register( + self, + name: str, + agent: type[BaseAgent] | BaseAgent, + **kwargs, + ) -> None: + """Register an agent class or instance under *name*. + + If *agent* is a class, ``kwargs`` are forwarded to its constructor + when :meth:`get` is called. If it is already an instance, it is + stored directly. + """ + if isinstance(agent, BaseAgent): + self._instances[name] = agent + elif isinstance(agent, type) and issubclass(agent, BaseAgent): + self._factories[name] = lambda: agent(**kwargs) + else: + raise TypeError(f"Expected BaseAgent subclass or instance, got {type(agent)}") + + def get(self, name: str) -> BaseAgent: + """Return the agent registered under *name*, instantiating lazily if needed.""" + if name not in self._instances: + if name not in self._factories: + raise KeyError(f"No agent registered under '{name}'") + self._instances[name] = self._factories.pop(name)() + return self._instances[name] + + def list(self) -> list[str]: + """Return sorted list of all registered agent names.""" + return sorted({*self._factories, *self._instances}) + + def __contains__(self, name: str) -> bool: + return name in self._factories or name in self._instances + + def __len__(self) -> int: + return len(self.list())