requisite-ai 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- requisite/__init__.py +116 -0
- requisite/agents/__init__.py +6 -0
- requisite/agents/agent.py +318 -0
- requisite/agents/registry.py +66 -0
- requisite/ai.py +329 -0
- requisite/capabilities/__init__.py +28 -0
- requisite/capabilities/registry.py +213 -0
- requisite/capabilities/resolvers.py +166 -0
- requisite/config/__init__.py +5 -0
- requisite/config/settings.py +118 -0
- requisite/core/__init__.py +1 -0
- requisite/core/exceptions.py +116 -0
- requisite/core/interfaces.py +193 -0
- requisite/orchestrators/__init__.py +12 -0
- requisite/orchestrators/base.py +92 -0
- requisite/orchestrators/factory.py +101 -0
- requisite/orchestrators/langgraph_orchestrator.py +148 -0
- requisite/orchestrators/native.py +153 -0
- requisite/providers/__init__.py +12 -0
- requisite/providers/base.py +185 -0
- requisite/providers/factory.py +155 -0
- requisite/providers/gemini_provider.py +362 -0
- requisite/providers/openai_provider.py +316 -0
- requisite/py.typed +0 -0
- requisite/skills/__init__.py +6 -0
- requisite/skills/base.py +81 -0
- requisite/skills/registry.py +64 -0
- requisite/tools/__init__.py +7 -0
- requisite/tools/base.py +136 -0
- requisite/tools/decorator.py +77 -0
- requisite/tools/registry.py +113 -0
- requisite/tools/schema.py +107 -0
- requisite/workflows/__init__.py +6 -0
- requisite/workflows/workflow.py +169 -0
- requisite_ai-0.3.0.dist-info/METADATA +379 -0
- requisite_ai-0.3.0.dist-info/RECORD +39 -0
- requisite_ai-0.3.0.dist-info/WHEEL +5 -0
- requisite_ai-0.3.0.dist-info/licenses/LICENSE +21 -0
- requisite_ai-0.3.0.dist-info/top_level.txt +1 -0
requisite/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
requisite
|
|
3
|
+
=========
|
|
4
|
+
|
|
5
|
+
Declare what your AI application needs -- not which SDK provides it.
|
|
6
|
+
|
|
7
|
+
A provider-agnostic, plugin-based framework for building AI-powered
|
|
8
|
+
applications and agents.
|
|
9
|
+
|
|
10
|
+
This top-level package intentionally exposes a *small* public surface.
|
|
11
|
+
Everything a typical developer needs to get started lives here; deeper
|
|
12
|
+
building blocks (provider base classes, registries, exceptions, etc.)
|
|
13
|
+
live in their respective sub-packages and can be imported explicitly
|
|
14
|
+
when you need to extend the framework.
|
|
15
|
+
|
|
16
|
+
Quick start
|
|
17
|
+
-----------
|
|
18
|
+
>>> from requisite import AI
|
|
19
|
+
>>> ai = AI() # reads configuration from environment / .env
|
|
20
|
+
>>> response = ai.chat("Explain LangGraph in one sentence.")
|
|
21
|
+
>>> print(response)
|
|
22
|
+
|
|
23
|
+
Switching providers requires no code changes beyond configuration:
|
|
24
|
+
>>> ai = AI(provider="gemini", model="gemini-2.5-flash")
|
|
25
|
+
|
|
26
|
+
Tool calling:
|
|
27
|
+
>>> from requisite.tools import tool
|
|
28
|
+
>>> @tool
|
|
29
|
+
... def search_weather(city: str) -> str:
|
|
30
|
+
... '''Look up the current weather for a city.'''
|
|
31
|
+
... return f"Sunny in {city}"
|
|
32
|
+
>>> ai.chat("What's the weather in Paris?", tools=[search_weather]) # doctest: +SKIP
|
|
33
|
+
|
|
34
|
+
Agents and multi-agent workflows:
|
|
35
|
+
>>> from requisite import Agent, Workflow
|
|
36
|
+
>>> research = Agent(name="Researcher", provider="openai") # doctest: +SKIP
|
|
37
|
+
>>> writer = Agent(name="Writer", provider="openai") # doctest: +SKIP
|
|
38
|
+
>>> workflow = Workflow()
|
|
39
|
+
>>> workflow.add(research).add(writer) # doctest: +SKIP
|
|
40
|
+
>>> workflow.run("Research AI trends and write a summary.") # doctest: +SKIP
|
|
41
|
+
>>> workflow.use_langgraph() # doctest: +SKIP
|
|
42
|
+
|
|
43
|
+
Declaring capabilities instead of binding to specific tool implementations:
|
|
44
|
+
>>> assistant = Agent(name="Assistant", provider="openai") # doctest: +SKIP
|
|
45
|
+
>>> assistant.requires("weather", "internet_search", "filesystem") # doctest: +SKIP
|
|
46
|
+
>>> assistant.run("What's the weather in Tokyo?") # doctest: +SKIP
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from requisite.agents.agent import Agent, AgentResult
|
|
50
|
+
from requisite.agents.registry import AgentRegistry
|
|
51
|
+
from requisite.ai import AI
|
|
52
|
+
from requisite.capabilities.registry import CapabilityProvider, CapabilityRegistry
|
|
53
|
+
from requisite.capabilities import default_registry as default_capability_registry
|
|
54
|
+
from requisite.config.settings import Settings
|
|
55
|
+
from requisite.core.exceptions import (
|
|
56
|
+
AgentException,
|
|
57
|
+
AIException,
|
|
58
|
+
CapabilityException,
|
|
59
|
+
ConfigurationException,
|
|
60
|
+
MCPException,
|
|
61
|
+
ProviderException,
|
|
62
|
+
SkillException,
|
|
63
|
+
ToolException,
|
|
64
|
+
)
|
|
65
|
+
from requisite.core.interfaces import ChatResponse, Message, Role, ToolCall
|
|
66
|
+
from requisite.orchestrators.base import WorkflowResult
|
|
67
|
+
from requisite.providers.factory import ProviderRegistry, default_registry
|
|
68
|
+
from requisite.skills.base import BaseSkill
|
|
69
|
+
from requisite.skills.registry import SkillRegistry
|
|
70
|
+
from requisite.tools.base import Tool
|
|
71
|
+
from requisite.tools.decorator import tool
|
|
72
|
+
from requisite.tools.registry import ToolRegistry
|
|
73
|
+
from requisite.workflows.workflow import Workflow
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
# The core facade
|
|
77
|
+
"AI",
|
|
78
|
+
"Settings",
|
|
79
|
+
# Messages / responses
|
|
80
|
+
"Message",
|
|
81
|
+
"Role",
|
|
82
|
+
"ChatResponse",
|
|
83
|
+
"ToolCall",
|
|
84
|
+
# Tool calling
|
|
85
|
+
"tool",
|
|
86
|
+
"Tool",
|
|
87
|
+
"ToolRegistry",
|
|
88
|
+
# Skills
|
|
89
|
+
"BaseSkill",
|
|
90
|
+
"SkillRegistry",
|
|
91
|
+
# Agents
|
|
92
|
+
"Agent",
|
|
93
|
+
"AgentResult",
|
|
94
|
+
"AgentRegistry",
|
|
95
|
+
# Capabilities (agent.requires(...))
|
|
96
|
+
"CapabilityRegistry",
|
|
97
|
+
"CapabilityProvider",
|
|
98
|
+
"default_capability_registry",
|
|
99
|
+
# Multi-agent workflows
|
|
100
|
+
"Workflow",
|
|
101
|
+
"WorkflowResult",
|
|
102
|
+
# Providers
|
|
103
|
+
"ProviderRegistry",
|
|
104
|
+
"default_registry",
|
|
105
|
+
# Exceptions
|
|
106
|
+
"AIException",
|
|
107
|
+
"ProviderException",
|
|
108
|
+
"ConfigurationException",
|
|
109
|
+
"ToolException",
|
|
110
|
+
"SkillException",
|
|
111
|
+
"AgentException",
|
|
112
|
+
"MCPException",
|
|
113
|
+
"CapabilityException",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent: an :class:`~requisite.ai.AI` instance equipped with tools and
|
|
3
|
+
skills, capable of autonomously deciding which to invoke and looping
|
|
4
|
+
until it has a final answer.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
>>> from requisite import Agent
|
|
9
|
+
>>> from requisite.tools import tool
|
|
10
|
+
>>>
|
|
11
|
+
>>> @tool
|
|
12
|
+
... def search_weather(city: str) -> str:
|
|
13
|
+
... '''Look up the current weather for a city.'''
|
|
14
|
+
... return f"Sunny, 22C in {city}"
|
|
15
|
+
>>>
|
|
16
|
+
>>> agent = Agent(name="Weather Agent", provider="openai", tools=[search_weather]) # doctest: +SKIP
|
|
17
|
+
>>> result = agent.run("What's the weather in Paris?") # doctest: +SKIP
|
|
18
|
+
>>> print(result) # doctest: +SKIP
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from collections.abc import Callable, Sequence
|
|
25
|
+
from typing import Any, Optional, Union
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel, ConfigDict
|
|
28
|
+
|
|
29
|
+
from requisite.ai import AI
|
|
30
|
+
from requisite.capabilities.registry import CapabilityRegistry
|
|
31
|
+
from requisite.capabilities import default_registry as default_capability_registry
|
|
32
|
+
from requisite.config.settings import Settings
|
|
33
|
+
from requisite.core.exceptions import AgentException
|
|
34
|
+
from requisite.core.interfaces import ChatResponse, Message
|
|
35
|
+
from requisite.providers.base import BaseProvider
|
|
36
|
+
from requisite.providers.factory import ProviderRegistry
|
|
37
|
+
from requisite.skills.base import BaseSkill
|
|
38
|
+
from requisite.tools.base import Tool
|
|
39
|
+
from requisite.tools.registry import ToolRegistry
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger("requisite.agents")
|
|
42
|
+
|
|
43
|
+
ToolLike = Union[Tool, Callable[..., Any]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentResult(BaseModel):
|
|
47
|
+
"""The outcome of an :meth:`Agent.run` (or :meth:`Agent.arun`) call.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
content:
|
|
52
|
+
The agent's final answer.
|
|
53
|
+
agent_name:
|
|
54
|
+
Name of the agent that produced this result.
|
|
55
|
+
iterations:
|
|
56
|
+
Number of model round-trips it took to reach a final answer
|
|
57
|
+
(i.e. 1 + the number of tool-calling loops).
|
|
58
|
+
tool_calls_executed:
|
|
59
|
+
Names of the tools that were invoked along the way, in order.
|
|
60
|
+
raw_response:
|
|
61
|
+
The final :class:`~requisite.core.interfaces.ChatResponse`.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
65
|
+
|
|
66
|
+
content: str
|
|
67
|
+
agent_name: str
|
|
68
|
+
iterations: int
|
|
69
|
+
tool_calls_executed: list[str] = []
|
|
70
|
+
raw_response: Optional[ChatResponse] = None
|
|
71
|
+
|
|
72
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
73
|
+
return self.content
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Agent:
|
|
77
|
+
"""An LLM configured with a name, tools, and skills, that can run tasks autonomously.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
name:
|
|
82
|
+
Human-readable identifier for the agent (used in
|
|
83
|
+
:class:`~requisite.agents.registry.AgentRegistry` and in
|
|
84
|
+
multi-agent orchestration logs/results).
|
|
85
|
+
provider:
|
|
86
|
+
Provider name (``"openai"``, ``"gemini"``) or an already-built
|
|
87
|
+
:class:`~requisite.providers.base.BaseProvider` instance.
|
|
88
|
+
Defaults to the configured default provider.
|
|
89
|
+
model:
|
|
90
|
+
Overrides the provider's default model.
|
|
91
|
+
tools:
|
|
92
|
+
Plain functions, ``@tool``-decorated functions, or
|
|
93
|
+
:class:`~requisite.tools.base.Tool` instances this agent may call.
|
|
94
|
+
skills:
|
|
95
|
+
:class:`~requisite.skills.base.BaseSkill` instances this agent
|
|
96
|
+
may call; each is automatically exposed to the model as a tool.
|
|
97
|
+
requires:
|
|
98
|
+
Capability names (e.g. ``"weather"``, ``"internet_search"``,
|
|
99
|
+
``"filesystem"``) this agent needs. Each is resolved, at
|
|
100
|
+
construction time, to whichever registered implementation is
|
|
101
|
+
currently available -- see
|
|
102
|
+
:mod:`requisite.capabilities`. Prefer this over ``tools`` when
|
|
103
|
+
you want the agent's code to stay stable as the underlying
|
|
104
|
+
implementation (native tool, MCP server, paid API, plugin)
|
|
105
|
+
evolves.
|
|
106
|
+
capability_registry:
|
|
107
|
+
The :class:`~requisite.capabilities.registry.CapabilityRegistry`
|
|
108
|
+
used to resolve ``requires``. Defaults to the framework's
|
|
109
|
+
built-in registry, pre-populated with reference implementations
|
|
110
|
+
for ``"filesystem"``, ``"weather"``, and ``"internet_search"``.
|
|
111
|
+
system_prompt:
|
|
112
|
+
System prompt establishing the agent's role/persona.
|
|
113
|
+
settings:
|
|
114
|
+
Configuration source; defaults to a fresh
|
|
115
|
+
:class:`~requisite.config.settings.Settings`.
|
|
116
|
+
registry:
|
|
117
|
+
Provider registry used to resolve ``provider`` by name.
|
|
118
|
+
max_iterations:
|
|
119
|
+
Maximum number of tool-calling round-trips before
|
|
120
|
+
:class:`~requisite.core.exceptions.AgentException` is raised,
|
|
121
|
+
guarding against infinite tool-call loops.
|
|
122
|
+
|
|
123
|
+
Examples
|
|
124
|
+
--------
|
|
125
|
+
>>> agent = Agent(name="Research Agent", provider="openai") # doctest: +SKIP
|
|
126
|
+
>>> agent.run("Research AI trends.") # doctest: +SKIP
|
|
127
|
+
|
|
128
|
+
Declaring capabilities instead of binding to specific tools:
|
|
129
|
+
|
|
130
|
+
>>> agent = Agent(name="Assistant", provider="openai") # doctest: +SKIP
|
|
131
|
+
>>> agent.requires("weather", "internet_search", "filesystem") # doctest: +SKIP
|
|
132
|
+
>>> agent.run("What's the weather in Tokyo?") # doctest: +SKIP
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
name: str,
|
|
138
|
+
*,
|
|
139
|
+
provider: Optional[Union[str, BaseProvider]] = None,
|
|
140
|
+
model: Optional[str] = None,
|
|
141
|
+
tools: Optional[Sequence[ToolLike]] = None,
|
|
142
|
+
skills: Optional[Sequence[BaseSkill]] = None,
|
|
143
|
+
requires: Optional[Sequence[str]] = None,
|
|
144
|
+
capability_registry: Optional[CapabilityRegistry] = None,
|
|
145
|
+
system_prompt: Optional[str] = None,
|
|
146
|
+
settings: Optional[Settings] = None,
|
|
147
|
+
registry: Optional[ProviderRegistry] = None,
|
|
148
|
+
max_iterations: int = 5,
|
|
149
|
+
) -> None:
|
|
150
|
+
self.name = name
|
|
151
|
+
self.max_iterations = max_iterations
|
|
152
|
+
self._capability_registry = capability_registry or default_capability_registry
|
|
153
|
+
|
|
154
|
+
self._tool_registry = ToolRegistry()
|
|
155
|
+
for tool_like in tools or []:
|
|
156
|
+
self._tool_registry.register(tool_like)
|
|
157
|
+
for skill in skills or []:
|
|
158
|
+
self._tool_registry.register(skill.as_tool())
|
|
159
|
+
|
|
160
|
+
self._ai = AI(
|
|
161
|
+
provider=provider,
|
|
162
|
+
model=model,
|
|
163
|
+
settings=settings,
|
|
164
|
+
registry=registry,
|
|
165
|
+
system_prompt=system_prompt,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if requires:
|
|
169
|
+
self.requires(*requires)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def ai(self) -> AI:
|
|
173
|
+
"""The underlying :class:`~requisite.ai.AI` facade backing this agent."""
|
|
174
|
+
return self._ai
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def tools(self) -> ToolRegistry:
|
|
178
|
+
"""The tool registry backing this agent's tool-calling loop."""
|
|
179
|
+
return self._tool_registry
|
|
180
|
+
|
|
181
|
+
def requires(self, *capabilities: str) -> "Agent":
|
|
182
|
+
"""Declare capabilities this agent needs, resolved to whichever
|
|
183
|
+
implementation is currently available.
|
|
184
|
+
|
|
185
|
+
This is the framework-agnostic alternative to binding an agent to
|
|
186
|
+
a specific tool: ``agent.requires("weather")`` resolves against
|
|
187
|
+
:class:`~requisite.capabilities.registry.CapabilityRegistry`
|
|
188
|
+
(native tools, MCP servers, cloud APIs, or third-party plugins
|
|
189
|
+
can all register the same capability name), so application code
|
|
190
|
+
stays stable as the underlying implementation evolves.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
*capabilities:
|
|
195
|
+
One or more capability names (e.g. ``"weather"``,
|
|
196
|
+
``"internet_search"``, ``"filesystem"``, ``"github"``).
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
Agent
|
|
201
|
+
``self``, for fluent chaining:
|
|
202
|
+
``agent.requires("weather").requires("internet_search")``.
|
|
203
|
+
|
|
204
|
+
Raises
|
|
205
|
+
------
|
|
206
|
+
requisite.core.exceptions.CapabilityException
|
|
207
|
+
If any requested capability has no registered provider, or
|
|
208
|
+
every registered provider for it is currently unavailable.
|
|
209
|
+
|
|
210
|
+
Examples
|
|
211
|
+
--------
|
|
212
|
+
>>> agent = Agent(name="Assistant", provider="openai") # doctest: +SKIP
|
|
213
|
+
>>> agent.requires("internet_search", "weather", "filesystem") # doctest: +SKIP
|
|
214
|
+
"""
|
|
215
|
+
for capability in capabilities:
|
|
216
|
+
resolved_tool = self._capability_registry.resolve(capability)
|
|
217
|
+
if resolved_tool.name != capability:
|
|
218
|
+
# Expose the tool to the model under the stable capability
|
|
219
|
+
# name, not the implementation's function name -- so the
|
|
220
|
+
# model-facing tool name never changes even if the
|
|
221
|
+
# provider behind "weather" is swapped out later.
|
|
222
|
+
resolved_tool = resolved_tool.model_copy(update={"name": capability})
|
|
223
|
+
self._tool_registry.register(resolved_tool)
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def run(self, prompt: Union[str, Sequence[Message]], **kwargs: Any) -> AgentResult:
|
|
227
|
+
"""Run the agent on a task, looping through tool calls until a final answer.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
prompt:
|
|
232
|
+
The task, as a string or an existing conversation history.
|
|
233
|
+
**kwargs:
|
|
234
|
+
Passed through to each underlying ``chat_response`` call
|
|
235
|
+
(e.g. ``temperature``, ``model``).
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
AgentResult
|
|
240
|
+
|
|
241
|
+
Raises
|
|
242
|
+
------
|
|
243
|
+
requisite.core.exceptions.AgentException
|
|
244
|
+
If ``max_iterations`` tool-calling round-trips are exhausted
|
|
245
|
+
without the model returning a final answer.
|
|
246
|
+
"""
|
|
247
|
+
messages: list[Message] = (
|
|
248
|
+
[Message.user(prompt)] if isinstance(prompt, str) else list(prompt)
|
|
249
|
+
)
|
|
250
|
+
available_tools = self._tool_registry.all() or None
|
|
251
|
+
tools_executed: list[str] = []
|
|
252
|
+
|
|
253
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
254
|
+
response = self._ai.chat_response(messages, tools=available_tools, **kwargs)
|
|
255
|
+
|
|
256
|
+
if not response.has_tool_calls:
|
|
257
|
+
return AgentResult(
|
|
258
|
+
content=response.content,
|
|
259
|
+
agent_name=self.name,
|
|
260
|
+
iterations=iteration,
|
|
261
|
+
tool_calls_executed=tools_executed,
|
|
262
|
+
raw_response=response,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
messages.append(
|
|
266
|
+
Message.assistant_tool_calls(response.tool_calls, content=response.content)
|
|
267
|
+
)
|
|
268
|
+
for call in response.tool_calls:
|
|
269
|
+
tool_instance = self._tool_registry.get(call.name)
|
|
270
|
+
result = tool_instance.execute(**call.arguments)
|
|
271
|
+
tools_executed.append(call.name)
|
|
272
|
+
messages.append(
|
|
273
|
+
Message.tool_result(str(result), tool_call_id=call.id, name=call.name)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
raise AgentException(
|
|
277
|
+
f"Agent '{self.name}' exceeded max_iterations={self.max_iterations} "
|
|
278
|
+
f"without reaching a final answer.",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def arun(self, prompt: Union[str, Sequence[Message]], **kwargs: Any) -> AgentResult:
|
|
282
|
+
"""Async counterpart to :meth:`run`."""
|
|
283
|
+
messages: list[Message] = (
|
|
284
|
+
[Message.user(prompt)] if isinstance(prompt, str) else list(prompt)
|
|
285
|
+
)
|
|
286
|
+
available_tools = self._tool_registry.all() or None
|
|
287
|
+
tools_executed: list[str] = []
|
|
288
|
+
|
|
289
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
290
|
+
response = await self._ai.achat_response(messages, tools=available_tools, **kwargs)
|
|
291
|
+
|
|
292
|
+
if not response.has_tool_calls:
|
|
293
|
+
return AgentResult(
|
|
294
|
+
content=response.content,
|
|
295
|
+
agent_name=self.name,
|
|
296
|
+
iterations=iteration,
|
|
297
|
+
tool_calls_executed=tools_executed,
|
|
298
|
+
raw_response=response,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
messages.append(
|
|
302
|
+
Message.assistant_tool_calls(response.tool_calls, content=response.content)
|
|
303
|
+
)
|
|
304
|
+
for call in response.tool_calls:
|
|
305
|
+
tool_instance = self._tool_registry.get(call.name)
|
|
306
|
+
result = await tool_instance.aexecute(**call.arguments)
|
|
307
|
+
tools_executed.append(call.name)
|
|
308
|
+
messages.append(
|
|
309
|
+
Message.tool_result(str(result), tool_call_id=call.id, name=call.name)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
raise AgentException(
|
|
313
|
+
f"Agent '{self.name}' exceeded max_iterations={self.max_iterations} "
|
|
314
|
+
f"without reaching a final answer.",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def __repr__(self) -> str: # pragma: no cover - trivial
|
|
318
|
+
return f"Agent(name={self.name!r}, provider={self._ai.provider.name!r})"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Agent registry -- mirrors the shape of the other framework registries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from requisite.agents.agent import Agent
|
|
8
|
+
from requisite.core.exceptions import AgentException
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("requisite.agents.registry")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentRegistry:
|
|
14
|
+
"""A registry of agents, keyed by name.
|
|
15
|
+
|
|
16
|
+
Examples
|
|
17
|
+
--------
|
|
18
|
+
>>> from requisite.agents import Agent, AgentRegistry
|
|
19
|
+
>>> registry = AgentRegistry()
|
|
20
|
+
>>> agent = Agent(name="research") # doctest: +SKIP
|
|
21
|
+
>>> registry.register(agent) # doctest: +SKIP
|
|
22
|
+
>>> registry.get("research") # doctest: +SKIP
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._agents: dict[str, Agent] = {}
|
|
27
|
+
|
|
28
|
+
def register(self, agent: Agent) -> Agent:
|
|
29
|
+
"""Register an agent under its ``.name``. Returns the agent for chaining."""
|
|
30
|
+
if agent.name in self._agents:
|
|
31
|
+
logger.debug("Overwriting existing agent registration: '%s'", agent.name)
|
|
32
|
+
self._agents[agent.name] = agent
|
|
33
|
+
return agent
|
|
34
|
+
|
|
35
|
+
def unregister(self, name: str) -> None:
|
|
36
|
+
"""Remove an agent registration, if present. No-op if absent."""
|
|
37
|
+
self._agents.pop(name, None)
|
|
38
|
+
|
|
39
|
+
def get(self, name: str) -> Agent:
|
|
40
|
+
"""Return the registered agent named ``name``.
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
requisite.core.exceptions.AgentException
|
|
45
|
+
If no agent is registered under that name.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
return self._agents[name]
|
|
49
|
+
except KeyError as exc:
|
|
50
|
+
raise AgentException(
|
|
51
|
+
f"No agent registered under '{name}'. Available: {self.list_names()}",
|
|
52
|
+
) from exc
|
|
53
|
+
|
|
54
|
+
def list_names(self) -> list[str]:
|
|
55
|
+
"""Return the sorted list of registered agent names."""
|
|
56
|
+
return sorted(self._agents)
|
|
57
|
+
|
|
58
|
+
def all(self) -> list[Agent]:
|
|
59
|
+
"""Return all registered agents."""
|
|
60
|
+
return list(self._agents.values())
|
|
61
|
+
|
|
62
|
+
def __contains__(self, name: str) -> bool:
|
|
63
|
+
return name in self._agents
|
|
64
|
+
|
|
65
|
+
def __len__(self) -> int:
|
|
66
|
+
return len(self._agents)
|