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.
Files changed (39) hide show
  1. requisite/__init__.py +116 -0
  2. requisite/agents/__init__.py +6 -0
  3. requisite/agents/agent.py +318 -0
  4. requisite/agents/registry.py +66 -0
  5. requisite/ai.py +329 -0
  6. requisite/capabilities/__init__.py +28 -0
  7. requisite/capabilities/registry.py +213 -0
  8. requisite/capabilities/resolvers.py +166 -0
  9. requisite/config/__init__.py +5 -0
  10. requisite/config/settings.py +118 -0
  11. requisite/core/__init__.py +1 -0
  12. requisite/core/exceptions.py +116 -0
  13. requisite/core/interfaces.py +193 -0
  14. requisite/orchestrators/__init__.py +12 -0
  15. requisite/orchestrators/base.py +92 -0
  16. requisite/orchestrators/factory.py +101 -0
  17. requisite/orchestrators/langgraph_orchestrator.py +148 -0
  18. requisite/orchestrators/native.py +153 -0
  19. requisite/providers/__init__.py +12 -0
  20. requisite/providers/base.py +185 -0
  21. requisite/providers/factory.py +155 -0
  22. requisite/providers/gemini_provider.py +362 -0
  23. requisite/providers/openai_provider.py +316 -0
  24. requisite/py.typed +0 -0
  25. requisite/skills/__init__.py +6 -0
  26. requisite/skills/base.py +81 -0
  27. requisite/skills/registry.py +64 -0
  28. requisite/tools/__init__.py +7 -0
  29. requisite/tools/base.py +136 -0
  30. requisite/tools/decorator.py +77 -0
  31. requisite/tools/registry.py +113 -0
  32. requisite/tools/schema.py +107 -0
  33. requisite/workflows/__init__.py +6 -0
  34. requisite/workflows/workflow.py +169 -0
  35. requisite_ai-0.3.0.dist-info/METADATA +379 -0
  36. requisite_ai-0.3.0.dist-info/RECORD +39 -0
  37. requisite_ai-0.3.0.dist-info/WHEEL +5 -0
  38. requisite_ai-0.3.0.dist-info/licenses/LICENSE +21 -0
  39. 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,6 @@
1
+ """Agents: an AI equipped with tools and skills, plus an agent registry."""
2
+
3
+ from requisite.agents.agent import Agent, AgentResult
4
+ from requisite.agents.registry import AgentRegistry
5
+
6
+ __all__ = ["Agent", "AgentResult", "AgentRegistry"]
@@ -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)