agentic-systems 1.0.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.
- agentic_systems/__init__.py +92 -0
- agentic_systems/agents.py +668 -0
- agentic_systems/api.py +219 -0
- agentic_systems/bedrock_runtime_client.py +299 -0
- agentic_systems/chain.py +108 -0
- agentic_systems/cli.py +266 -0
- agentic_systems/contracts.py +545 -0
- agentic_systems/core/__init__.py +41 -0
- agentic_systems/core/agents.py +5 -0
- agentic_systems/core/contracts.py +5 -0
- agentic_systems/core/human_output.py +5 -0
- agentic_systems/core/lineage.py +5 -0
- agentic_systems/core/results.py +6 -0
- agentic_systems/core/runtime.py +209 -0
- agentic_systems/core/scheduler.py +219 -0
- agentic_systems/core/tools.py +5 -0
- agentic_systems/engines/__init__.py +15 -0
- agentic_systems/engines/bedrock.py +69 -0
- agentic_systems/engines/names.py +68 -0
- agentic_systems/engines/python_direct.py +7 -0
- agentic_systems/environments.py +802 -0
- agentic_systems/errors.py +19 -0
- agentic_systems/evals.py +384 -0
- agentic_systems/expectations.py +68 -0
- agentic_systems/factories.py +377 -0
- agentic_systems/final_answer.py +191 -0
- agentic_systems/human_output.py +925 -0
- agentic_systems/integrations/__init__.py +10 -0
- agentic_systems/integrations/langgraph.py +799 -0
- agentic_systems/lineage.py +687 -0
- agentic_systems/output_contracts.py +136 -0
- agentic_systems/providers/__init__.py +43 -0
- agentic_systems/providers/base.py +265 -0
- agentic_systems/providers/bedrock_runtime.py +2959 -0
- agentic_systems/providers/openai_runtime.py +349 -0
- agentic_systems/providers/python_direct.py +327 -0
- agentic_systems/providers/vllm_runtime.py +147 -0
- agentic_systems/results.py +404 -0
- agentic_systems/skills/__init__.py +14 -0
- agentic_systems/skills/loader.py +134 -0
- agentic_systems/skills/skill.py +230 -0
- agentic_systems/system.py +604 -0
- agentic_systems/tools/__init__.py +15 -0
- agentic_systems/tools/compat.py +159 -0
- agentic_systems/tools/decorators.py +105 -0
- agentic_systems/tools/tool.py +319 -0
- agentic_systems/utils.py +1511 -0
- agentic_systems-1.0.0.dist-info/METADATA +335 -0
- agentic_systems-1.0.0.dist-info/RECORD +52 -0
- agentic_systems-1.0.0.dist-info/WHEEL +5 -0
- agentic_systems-1.0.0.dist-info/entry_points.txt +2 -0
- agentic_systems-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
"""Agent object for Agentic Systems 1.0."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from collections.abc import Callable, Iterable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .contracts import AgentContract, RunPolicy, ValidationResult, resolve_policy, validate_contract_policy
|
|
12
|
+
from .core.runtime import RuntimeConfig
|
|
13
|
+
from .core.scheduler import SchedulerConfig, SchedulerTimeoutError, execute_async, execute_sync, merge_policy_with_scheduler
|
|
14
|
+
from .engines.names import (
|
|
15
|
+
BEDROCK_RUNTIME_ENGINE,
|
|
16
|
+
LANGGRAPH_ORCHESTRATOR,
|
|
17
|
+
OPENAI_AGENTS_FRAMEWORK,
|
|
18
|
+
OPENAI_RUNTIME_ENGINE,
|
|
19
|
+
PYTHON_DIRECT_ENGINE,
|
|
20
|
+
STRANDS_FRAMEWORK,
|
|
21
|
+
canonical_engine_name,
|
|
22
|
+
normalize_engine_text,
|
|
23
|
+
)
|
|
24
|
+
from .errors import GraphContractError
|
|
25
|
+
from .results import RunResult
|
|
26
|
+
from .final_answer import OutputSchema, final_answer
|
|
27
|
+
from .skills import LoadedSkill, Skill
|
|
28
|
+
from .tools import Tool
|
|
29
|
+
from .tools.compat import Toolkit, expand_tool_inputs
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _coerce_input(value: Any, input_contract: Any | None) -> Any:
|
|
33
|
+
if input_contract is None:
|
|
34
|
+
return value
|
|
35
|
+
if isinstance(value, input_contract):
|
|
36
|
+
return value
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
return input_contract.model_validate(value)
|
|
39
|
+
return input_contract.model_validate({"input": value})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _coerce_output_data(result: RunResult, output_contract: Any | None) -> RunResult:
|
|
43
|
+
if output_contract is None:
|
|
44
|
+
if not result.final:
|
|
45
|
+
result.final = final_answer(result.data, text=result.text)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
if isinstance(output_contract, OutputSchema):
|
|
49
|
+
result.final = final_answer(result.data or result.final, schema=output_contract, text=result.text)
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
# Backward-compatible Pydantic output contracts keep validating ``data``.
|
|
53
|
+
# The validated payload also becomes ``final`` because it represents the
|
|
54
|
+
# user-facing structured answer requested by the output contract.
|
|
55
|
+
source = result.data or result.final or _try_parse_json_object(result.text)
|
|
56
|
+
if isinstance(source, dict) and set(source) == {"text"} and source.get("text") == result.text:
|
|
57
|
+
source = _try_parse_json_object(result.text)
|
|
58
|
+
validated = output_contract.model_validate(source)
|
|
59
|
+
payload = validated.model_dump(mode="json")
|
|
60
|
+
result.data = payload
|
|
61
|
+
result.final = payload
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _try_parse_json_object(text: str) -> dict[str, Any]:
|
|
66
|
+
try:
|
|
67
|
+
parsed = json.loads(text)
|
|
68
|
+
except Exception:
|
|
69
|
+
return {"text": text}
|
|
70
|
+
return parsed if isinstance(parsed, dict) else {"value": parsed}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_framework_and_engine(engine: str | None, framework: str | None) -> tuple[str, str | None]:
|
|
76
|
+
"""Return (execution_engine, user_framework).
|
|
77
|
+
|
|
78
|
+
``engine`` describes the model/runtime provider, while ``framework``
|
|
79
|
+
describes the orchestration layer.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
resolved_engine = canonical_engine_name(engine or BEDROCK_RUNTIME_ENGINE)
|
|
83
|
+
if framework is None or str(framework).strip() == "":
|
|
84
|
+
if resolved_engine == LANGGRAPH_ORCHESTRATOR:
|
|
85
|
+
raise ValueError("langgraph_is_not_engine: LangGraph is an orchestrator, not an engine. Use framework='langgraph' with engine='bedrock-runtime'.")
|
|
86
|
+
return resolved_engine, None
|
|
87
|
+
|
|
88
|
+
fw = normalize_engine_text(framework)
|
|
89
|
+
if fw in {LANGGRAPH_ORCHESTRATOR, OPENAI_AGENTS_FRAMEWORK, STRANDS_FRAMEWORK}:
|
|
90
|
+
return resolved_engine, fw
|
|
91
|
+
raise ValueError("framework must be one of: langgraph, openai-agents, strands.")
|
|
92
|
+
|
|
93
|
+
def _framework_label(framework: str | None) -> str:
|
|
94
|
+
return str(framework).strip() if framework not in (None, "", "n/a") else "agentic-systems"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Agent:
|
|
98
|
+
"""Portable agent configuration and execution facade.
|
|
99
|
+
|
|
100
|
+
``Agent`` can now be created directly as a lightweight public API object:
|
|
101
|
+
|
|
102
|
+
.. code-block:: python
|
|
103
|
+
|
|
104
|
+
agent = Agent(name="calculator", tools=[sumar], skills=[math_skill])
|
|
105
|
+
|
|
106
|
+
A direct agent is a portable configuration. By default it targets the
|
|
107
|
+
canonical cloud engine, ``bedrock-runtime``. Use ``agent.bind(system)`` or
|
|
108
|
+
create the agent through ``system.agent(...)`` to run with AWS credentials.
|
|
109
|
+
For smoke tests without AWS, opt in explicitly with ``engine="python-runtime"``
|
|
110
|
+
and pass a structured tool plan.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
name: str,
|
|
117
|
+
instructions: str = "",
|
|
118
|
+
tools: Any = None,
|
|
119
|
+
skills: Any = None,
|
|
120
|
+
system: Any | None = None,
|
|
121
|
+
engine: str | None = None,
|
|
122
|
+
framework: str | None = None,
|
|
123
|
+
model: str | None = None,
|
|
124
|
+
contract: AgentContract | dict[str, Any] | None = None,
|
|
125
|
+
policy: RunPolicy | dict[str, Any] | None = None,
|
|
126
|
+
input_contract: Any | None = None,
|
|
127
|
+
output_contract: Any | None = None,
|
|
128
|
+
input: Any | None = None,
|
|
129
|
+
output: Any | None = None,
|
|
130
|
+
defaults: dict[str, Any] | None = None,
|
|
131
|
+
metadata: dict[str, Any] | None = None,
|
|
132
|
+
runtime: RuntimeConfig | dict[str, Any] | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
self.system = system
|
|
135
|
+
self.name = str(name or "").strip()
|
|
136
|
+
if not self.name:
|
|
137
|
+
raise ValueError("Agent name must be non-empty.")
|
|
138
|
+
self.instructions = str(instructions or "")
|
|
139
|
+
self.runtime_config = RuntimeConfig.coerce(runtime) if runtime is not None else getattr(system, "runtime_config", None)
|
|
140
|
+
if self.runtime_config is not None and engine is None:
|
|
141
|
+
engine = self.runtime_config.provider
|
|
142
|
+
self._engine_was_default = engine is None and framework is None
|
|
143
|
+
self.engine, self.framework = _resolve_framework_and_engine(engine, framework)
|
|
144
|
+
self.model = model if model is not None else (self.runtime_config.model_id if self.runtime_config else getattr(system, "model", None))
|
|
145
|
+
self.contract = AgentContract.coerce(contract)
|
|
146
|
+
self.policy = policy
|
|
147
|
+
self.input_contract = input_contract if input_contract is not None else input
|
|
148
|
+
self.output_contract = output_contract if output_contract is not None else output
|
|
149
|
+
self.defaults = defaults
|
|
150
|
+
self.metadata = dict(metadata or {})
|
|
151
|
+
|
|
152
|
+
explicit_tool_names, explicit_tools = _normalize_agent_tool_inputs(tools)
|
|
153
|
+
skill_tool_names, skill_names, skill_tools, skill_objects = _normalize_agent_skill_inputs(skills)
|
|
154
|
+
self.tools = _dedupe_preserve_order([*skill_tool_names, *explicit_tool_names])
|
|
155
|
+
self.skills = _dedupe_preserve_order(skill_names)
|
|
156
|
+
self._direct_tools = _dedupe_tools([*skill_tools, *explicit_tools])
|
|
157
|
+
self._direct_skills = _dedupe_skills(skill_objects)
|
|
158
|
+
|
|
159
|
+
self.validate().raise_if_failed()
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def tool_names(self) -> tuple[str, ...]:
|
|
163
|
+
"""Return this agent's resolved tool names."""
|
|
164
|
+
|
|
165
|
+
return self.tools
|
|
166
|
+
|
|
167
|
+
def available_tools(self) -> list[Tool]:
|
|
168
|
+
"""Return concrete Tool objects known by this agent.
|
|
169
|
+
|
|
170
|
+
Direct agents can hold concrete tools from ``tools=[...]`` and
|
|
171
|
+
``skills=[Skill(...)]``. System-backed agents may only know tool names,
|
|
172
|
+
so this method falls back to the system public registry when available.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
tools = list(self._direct_tools)
|
|
176
|
+
known = {tool.name for tool in tools}
|
|
177
|
+
public_tools = getattr(getattr(self, "system", None), "public_tools", None)
|
|
178
|
+
if public_tools is not None:
|
|
179
|
+
for name in self.tools:
|
|
180
|
+
tool = public_tools.get(name)
|
|
181
|
+
if isinstance(tool, Tool) and tool.name not in known:
|
|
182
|
+
tools.append(tool)
|
|
183
|
+
known.add(tool.name)
|
|
184
|
+
return tools
|
|
185
|
+
|
|
186
|
+
def info(self) -> dict[str, Any]:
|
|
187
|
+
"""Return a JSON-like summary of the agent configuration."""
|
|
188
|
+
|
|
189
|
+
runtime_engine = self._runtime_engine_name()
|
|
190
|
+
return {
|
|
191
|
+
"name": self.name,
|
|
192
|
+
"instructions": self.instructions,
|
|
193
|
+
"tools": list(self.tools),
|
|
194
|
+
"skills": list(self.skills),
|
|
195
|
+
"engine": runtime_engine,
|
|
196
|
+
"framework": _framework_label(self.framework),
|
|
197
|
+
"runtime_engine": runtime_engine,
|
|
198
|
+
"execution_engine": self.engine,
|
|
199
|
+
"model": self.model,
|
|
200
|
+
"contract": self.contract.model_dump(mode="json"),
|
|
201
|
+
"policy": _json_like(self.policy),
|
|
202
|
+
"input_contract": _contract_name(self.input_contract),
|
|
203
|
+
"output_contract": _contract_name(self.output_contract),
|
|
204
|
+
"has_system": self.system is not None,
|
|
205
|
+
"direct_tool_count": len(self._direct_tools),
|
|
206
|
+
"direct_tools": [tool.info() for tool in self._direct_tools],
|
|
207
|
+
"metadata": _json_like(self.metadata),
|
|
208
|
+
"runtime": self.runtime_config.to_dict() if self.runtime_config is not None else None,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
def describe(self) -> str:
|
|
212
|
+
"""Return a short human-readable description."""
|
|
213
|
+
|
|
214
|
+
tools = ", ".join(self.tools) if self.tools else "no tools"
|
|
215
|
+
skills = f"; skills: {', '.join(self.skills)}" if self.skills else ""
|
|
216
|
+
return f"Agent `{self.name}` ({tools}{skills})"
|
|
217
|
+
|
|
218
|
+
def check(self) -> ValidationResult:
|
|
219
|
+
"""Alias for ``validate`` for symmetry with Tool and Skill."""
|
|
220
|
+
|
|
221
|
+
return self.validate()
|
|
222
|
+
|
|
223
|
+
def bind(self, system: Any) -> "Agent":
|
|
224
|
+
"""Attach this direct agent to an ``AgenticSystem``.
|
|
225
|
+
|
|
226
|
+
The method returns the system-backed agent created by ``system.agent``.
|
|
227
|
+
Concrete direct tools are registered first through runtime ``Skill``
|
|
228
|
+
expansion or explicit tool registration paths.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
if system is None:
|
|
232
|
+
raise TypeError("agent.bind(system) expects an AgenticSystem-like object.")
|
|
233
|
+
if self.system is system:
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
skills_payload: list[Any] = [*self._direct_skills]
|
|
237
|
+
known_from_skills = {tool.name for skill in self._direct_skills for tool in skill.available_tools()}
|
|
238
|
+
tools_payload: list[Any] = [tool for tool in self._direct_tools if tool.name not in known_from_skills]
|
|
239
|
+
tools_payload.extend(name for name in self.tools if name not in {tool.name for tool in self._direct_tools})
|
|
240
|
+
|
|
241
|
+
return system.agent(
|
|
242
|
+
name=self.name,
|
|
243
|
+
instructions=self.instructions,
|
|
244
|
+
tools=tools_payload,
|
|
245
|
+
skills=skills_payload or list(self.skills),
|
|
246
|
+
engine=self.engine,
|
|
247
|
+
framework=self.framework,
|
|
248
|
+
input=self.input_contract,
|
|
249
|
+
output=self.output_contract,
|
|
250
|
+
contract=self.contract,
|
|
251
|
+
policy=self.policy,
|
|
252
|
+
model=self.model,
|
|
253
|
+
defaults=self.defaults,
|
|
254
|
+
runtime=self.runtime_config,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def run(self, input: Any = None, *, mode: str = "default", config: RunPolicy | dict[str, Any] | None = None) -> RunResult:
|
|
258
|
+
"""Run the agent from synchronous user code.
|
|
259
|
+
|
|
260
|
+
This is the primary execution method for notebooks, scripts and tests.
|
|
261
|
+
The selected engine is responsible for using its native synchronous
|
|
262
|
+
path, so sync execution does not create a second event loop behind the
|
|
263
|
+
user's back. Use ``agent.arun(...)`` for async applications and async
|
|
264
|
+
LangGraph nodes.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
clean_input = _coerce_input(input, self.input_contract)
|
|
268
|
+
policy = self._policy_for_runtime(resolve_policy(mode=mode, agent_policy=self.policy, run_config=config))
|
|
269
|
+
scheduler = self._scheduler()
|
|
270
|
+
if self.system is None:
|
|
271
|
+
if self.engine != PYTHON_DIRECT_ENGINE:
|
|
272
|
+
raise RuntimeError(
|
|
273
|
+
f"Direct Agent.run(...) without AgenticSystem cannot use engine={self.engine!r}. "
|
|
274
|
+
"Use `agent.bind(system)` or create the agent through `system.agent(...)` for runtime-backed execution."
|
|
275
|
+
)
|
|
276
|
+
from .providers.python_direct import PythonDirectEngine
|
|
277
|
+
|
|
278
|
+
engine = PythonDirectEngine()
|
|
279
|
+
else:
|
|
280
|
+
engine = self.system._engine(self.engine)
|
|
281
|
+
|
|
282
|
+
def _run_engine() -> RunResult:
|
|
283
|
+
return engine.run(self, clean_input, policy, mode=mode)
|
|
284
|
+
|
|
285
|
+
if scheduler is None:
|
|
286
|
+
result = _run_engine()
|
|
287
|
+
return self._finalize_result(result, clean_input)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
result, scheduler_meta = execute_sync(_run_engine, scheduler, is_success=lambda item: bool(getattr(item, "ok", True)))
|
|
291
|
+
except SchedulerTimeoutError as exc:
|
|
292
|
+
result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code="scheduler_timeout")
|
|
293
|
+
scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": True}
|
|
294
|
+
except Exception as exc: # noqa: BLE001 - return scheduler/runtime failures as RunResult.
|
|
295
|
+
result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code=type(exc).__name__)
|
|
296
|
+
scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": False}
|
|
297
|
+
return self._finalize_result(self._attach_scheduler_meta(result, scheduler_meta), clean_input)
|
|
298
|
+
|
|
299
|
+
async def arun(self, input: Any = None, *, mode: str = "default", config: RunPolicy | dict[str, Any] | None = None) -> RunResult:
|
|
300
|
+
"""Run the agent from async application code.
|
|
301
|
+
|
|
302
|
+
Async execution uses the engine's native async path when available. If a
|
|
303
|
+
custom sync-only engine is plugged in, Agentic Systems isolates that work
|
|
304
|
+
in a worker thread instead of blocking the caller's event loop.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
clean_input = _coerce_input(input, self.input_contract)
|
|
308
|
+
policy = self._policy_for_runtime(resolve_policy(mode=mode, agent_policy=self.policy, run_config=config))
|
|
309
|
+
scheduler = self._scheduler()
|
|
310
|
+
if self.system is None:
|
|
311
|
+
if self.engine != PYTHON_DIRECT_ENGINE:
|
|
312
|
+
raise RuntimeError(
|
|
313
|
+
f"Direct Agent.arun(...) without AgenticSystem cannot use engine={self.engine!r}. "
|
|
314
|
+
"Use `agent.bind(system)` or create the agent through `system.agent(...)` for runtime-backed execution."
|
|
315
|
+
)
|
|
316
|
+
from .providers.python_direct import PythonDirectEngine
|
|
317
|
+
|
|
318
|
+
engine = PythonDirectEngine()
|
|
319
|
+
else:
|
|
320
|
+
engine = self.system._engine(self.engine)
|
|
321
|
+
|
|
322
|
+
async def _run_engine() -> RunResult:
|
|
323
|
+
if hasattr(engine, "arun"):
|
|
324
|
+
return await engine.arun(self, clean_input, policy, mode=mode)
|
|
325
|
+
return await asyncio.to_thread(engine.run, self, clean_input, policy, mode=mode)
|
|
326
|
+
|
|
327
|
+
if scheduler is None:
|
|
328
|
+
result = await _run_engine()
|
|
329
|
+
return self._finalize_result(result, clean_input)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
result, scheduler_meta = await execute_async(_run_engine, scheduler, is_success=lambda item: bool(getattr(item, "ok", True)))
|
|
333
|
+
except SchedulerTimeoutError as exc:
|
|
334
|
+
result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code="scheduler_timeout")
|
|
335
|
+
scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": True}
|
|
336
|
+
except Exception as exc: # noqa: BLE001 - return scheduler/runtime failures as RunResult.
|
|
337
|
+
result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code=type(exc).__name__)
|
|
338
|
+
scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": False}
|
|
339
|
+
return self._finalize_result(self._attach_scheduler_meta(result, scheduler_meta), clean_input)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _scheduler(self) -> SchedulerConfig | None:
|
|
343
|
+
if self.runtime_config is None:
|
|
344
|
+
return None
|
|
345
|
+
return self.runtime_config.scheduler
|
|
346
|
+
|
|
347
|
+
def _policy_for_runtime(self, policy: RunPolicy) -> RunPolicy:
|
|
348
|
+
return merge_policy_with_scheduler(policy, self._scheduler())
|
|
349
|
+
|
|
350
|
+
def _attach_scheduler_meta(self, result: RunResult, scheduler_meta: dict[str, Any]) -> RunResult:
|
|
351
|
+
if self.runtime_config is not None:
|
|
352
|
+
result.meta.setdefault("runtime", self.runtime_config.to_dict())
|
|
353
|
+
result.meta.setdefault("scheduler", self.runtime_config.scheduler.to_dict())
|
|
354
|
+
result.meta["scheduler_execution"] = scheduler_meta
|
|
355
|
+
result.usage.setdefault("scheduler", {})
|
|
356
|
+
result.usage["scheduler"].update(
|
|
357
|
+
{
|
|
358
|
+
"attempts": scheduler_meta.get("attempts"),
|
|
359
|
+
"retries": scheduler_meta.get("retries"),
|
|
360
|
+
"timed_out": scheduler_meta.get("timed_out"),
|
|
361
|
+
"latency_ms": scheduler_meta.get("latency_ms"),
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
return result
|
|
365
|
+
|
|
366
|
+
def _scheduler_failure_result(self, message: str, clean_input: Any, *, mode: str, code: str) -> RunResult:
|
|
367
|
+
runtime_engine = self._runtime_engine_name()
|
|
368
|
+
return RunResult(
|
|
369
|
+
text=message,
|
|
370
|
+
data={"ok": False, "error": {"code": code, "message": message}},
|
|
371
|
+
ok=False,
|
|
372
|
+
engine=runtime_engine,
|
|
373
|
+
model=self.model or "",
|
|
374
|
+
mode=mode,
|
|
375
|
+
meta={
|
|
376
|
+
"input": _json_like(clean_input),
|
|
377
|
+
"source_result_type": "SchedulerFailure",
|
|
378
|
+
"framework": _framework_label(self.framework),
|
|
379
|
+
"runtime_engine": runtime_engine,
|
|
380
|
+
"execution_engine": self.engine,
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def _runtime_engine_name(self) -> str:
|
|
385
|
+
return self.engine
|
|
386
|
+
|
|
387
|
+
def _finalize_result(self, result: RunResult, clean_input: Any | None = None) -> RunResult:
|
|
388
|
+
result.meta["framework"] = _framework_label(self.framework)
|
|
389
|
+
if clean_input is not None:
|
|
390
|
+
result.meta.setdefault("input", _json_like(clean_input))
|
|
391
|
+
runtime_engine = self._runtime_engine_name()
|
|
392
|
+
result.meta.setdefault("runtime_engine", runtime_engine)
|
|
393
|
+
result.meta.setdefault("execution_engine", self.engine)
|
|
394
|
+
if result.engine == self.engine:
|
|
395
|
+
result.engine = runtime_engine
|
|
396
|
+
result = _coerce_output_data(result, self.output_contract)
|
|
397
|
+
validation = result.validate(self.contract)
|
|
398
|
+
result.validation = validation.to_dict()
|
|
399
|
+
result.ok = result.ok and validation.ok
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
def run_sync(self, input: Any = None, *, mode: str = "default", config: RunPolicy | dict[str, Any] | None = None) -> RunResult:
|
|
403
|
+
"""Alias for ``run`` for call sites that want explicit sync naming."""
|
|
404
|
+
|
|
405
|
+
return self.run(input, mode=mode, config=config)
|
|
406
|
+
|
|
407
|
+
def as_node(
|
|
408
|
+
self,
|
|
409
|
+
*,
|
|
410
|
+
input: str | Callable[[dict[str, Any]], Any] = "prompt",
|
|
411
|
+
output: str | Callable[[RunResult, dict[str, Any]], Any] | None = "answer",
|
|
412
|
+
trace: str | None = "ada_trace",
|
|
413
|
+
mode: str = "default",
|
|
414
|
+
config: RunPolicy | dict[str, Any] | None = None,
|
|
415
|
+
):
|
|
416
|
+
"""Return a sync LangGraph-compatible node with partial state updates."""
|
|
417
|
+
|
|
418
|
+
def _node(state: dict[str, Any]) -> Any:
|
|
419
|
+
prompt = _read_node_input(state, input)
|
|
420
|
+
result = self.run(prompt, mode=mode, config=config)
|
|
421
|
+
return _map_node_output(result, state, output, trace)
|
|
422
|
+
|
|
423
|
+
return _node
|
|
424
|
+
|
|
425
|
+
def as_async_node(
|
|
426
|
+
self,
|
|
427
|
+
*,
|
|
428
|
+
input: str | Callable[[dict[str, Any]], Any] = "prompt",
|
|
429
|
+
output: str | Callable[[RunResult, dict[str, Any]], Any] | None = "answer",
|
|
430
|
+
trace: str | None = "ada_trace",
|
|
431
|
+
mode: str = "default",
|
|
432
|
+
config: RunPolicy | dict[str, Any] | None = None,
|
|
433
|
+
):
|
|
434
|
+
"""Return an async LangGraph-compatible node.
|
|
435
|
+
|
|
436
|
+
Use this node with LangGraph ``ainvoke``/async graphs when the engine or
|
|
437
|
+
surrounding application is async-first.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
async def _node(state: dict[str, Any]) -> Any:
|
|
441
|
+
prompt = _read_node_input(state, input)
|
|
442
|
+
result = await self.arun(prompt, mode=mode, config=config)
|
|
443
|
+
return _map_node_output(result, state, output, trace)
|
|
444
|
+
|
|
445
|
+
return _node
|
|
446
|
+
|
|
447
|
+
def as_tool(self, *, name: str | None = None, description: str | None = None):
|
|
448
|
+
tool_name = name or self.name
|
|
449
|
+
|
|
450
|
+
def _agent_tool(prompt: str) -> dict[str, Any]:
|
|
451
|
+
"""Run this agent as a dict-returning tool."""
|
|
452
|
+
|
|
453
|
+
result = self.run(prompt)
|
|
454
|
+
return {"text": result.text, "data": result.data, "ok": result.ok, "trace": result.trace("compact")}
|
|
455
|
+
|
|
456
|
+
_agent_tool.__name__ = tool_name.replace(".", "_")
|
|
457
|
+
_agent_tool.__doc__ = description or f"Run agent {self.name}."
|
|
458
|
+
return _agent_tool
|
|
459
|
+
|
|
460
|
+
def validate(self) -> ValidationResult:
|
|
461
|
+
result = ValidationResult(ok=True)
|
|
462
|
+
if self.engine == LANGGRAPH_ORCHESTRATOR:
|
|
463
|
+
result.add(
|
|
464
|
+
"langgraph_is_not_engine",
|
|
465
|
+
"LangGraph is an orchestrator, not an AgenticSystem engine. Use agent.as_node(...) or agent.as_async_node(...).",
|
|
466
|
+
path="agent.engine",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
counts = Counter(self.tools)
|
|
470
|
+
for tool_name in sorted(name for name, count in counts.items() if count > 1):
|
|
471
|
+
result.add(
|
|
472
|
+
"duplicate_agent_tool",
|
|
473
|
+
f"Agent '{self.name}' references duplicate tool '{tool_name}'.",
|
|
474
|
+
path="agent.tools",
|
|
475
|
+
meta={"tool_name": tool_name, "count": counts[tool_name]},
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
for index, tool in enumerate(self._direct_tools):
|
|
479
|
+
tool_validation = tool.check()
|
|
480
|
+
for issue in tool_validation.issues:
|
|
481
|
+
result.add(
|
|
482
|
+
issue.code,
|
|
483
|
+
issue.message,
|
|
484
|
+
severity=issue.severity,
|
|
485
|
+
path=f"agent.tools[{index}].{issue.path}" if issue.path else f"agent.tools[{index}]",
|
|
486
|
+
meta={"tool_name": tool.name, **issue.meta},
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
available = set(self.tools)
|
|
490
|
+
if self.system is not None:
|
|
491
|
+
system_tools = set(self.system.tool_names)
|
|
492
|
+
available |= system_tools
|
|
493
|
+
for name in self.tools:
|
|
494
|
+
if name not in system_tools and name not in {tool.name for tool in self._direct_tools}:
|
|
495
|
+
result.add(
|
|
496
|
+
"unknown_agent_tool",
|
|
497
|
+
f"Agent '{self.name}' references unknown tool '{name}'.",
|
|
498
|
+
path="agent.tools",
|
|
499
|
+
meta={"available_tools": sorted(system_tools)},
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
for name in self.contract.must_call + self.contract.must_not_call:
|
|
503
|
+
if name not in available:
|
|
504
|
+
result.add(
|
|
505
|
+
"contract_references_unknown_tool",
|
|
506
|
+
f"Contract for agent '{self.name}' references unknown tool '{name}'.",
|
|
507
|
+
path="agent.contract",
|
|
508
|
+
meta={"available_tools": sorted(available)},
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
static_validation = validate_contract_policy(
|
|
512
|
+
self.contract,
|
|
513
|
+
self.policy,
|
|
514
|
+
available_tools=available,
|
|
515
|
+
)
|
|
516
|
+
for issue in static_validation.issues:
|
|
517
|
+
if issue.code == "contract_references_unknown_tool":
|
|
518
|
+
# Keep the historical, agent-specific error above for backwards compatibility.
|
|
519
|
+
continue
|
|
520
|
+
result.add(
|
|
521
|
+
issue.code,
|
|
522
|
+
issue.message,
|
|
523
|
+
severity=issue.severity,
|
|
524
|
+
path=f"agent.{issue.path}" if issue.path else "agent.contract_policy",
|
|
525
|
+
meta=issue.meta,
|
|
526
|
+
)
|
|
527
|
+
return result
|
|
528
|
+
|
|
529
|
+
def eval(self, cases: list[dict[str, Any]], **kwargs: Any):
|
|
530
|
+
if self.system is None:
|
|
531
|
+
raise RuntimeError("Direct Agent.eval(...) needs an attached AgenticSystem. Use `agent.bind(system)` first.")
|
|
532
|
+
return self.system.eval(self, cases, **kwargs)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _normalize_agent_tool_inputs(items: Any) -> tuple[tuple[str, ...], tuple[Tool, ...]]:
|
|
536
|
+
"""Normalize direct agent tool inputs into names and concrete Tool objects."""
|
|
537
|
+
|
|
538
|
+
if items is None:
|
|
539
|
+
return (), ()
|
|
540
|
+
if isinstance(items, Tool):
|
|
541
|
+
return (items.name,), (items,)
|
|
542
|
+
if isinstance(items, Toolkit):
|
|
543
|
+
return items.tool_names, ()
|
|
544
|
+
if isinstance(items, str):
|
|
545
|
+
return (items,), ()
|
|
546
|
+
if callable(items):
|
|
547
|
+
tool = Tool(name=getattr(items, "__name__", ""), function=items)
|
|
548
|
+
return (tool.name,), (tool,)
|
|
549
|
+
if isinstance(items, Iterable) and not isinstance(items, (str, bytes, dict)):
|
|
550
|
+
names: list[str] = []
|
|
551
|
+
tools: list[Tool] = []
|
|
552
|
+
for item in items:
|
|
553
|
+
item_names, item_tools = _normalize_agent_tool_inputs(item)
|
|
554
|
+
names.extend(item_names)
|
|
555
|
+
tools.extend(item_tools)
|
|
556
|
+
return tuple(names), tuple(tools)
|
|
557
|
+
try:
|
|
558
|
+
return expand_tool_inputs(items), ()
|
|
559
|
+
except TypeError as exc:
|
|
560
|
+
raise TypeError(f"Unsupported agent tools value: {items!r}") from exc
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _normalize_agent_skill_inputs(items: Any) -> tuple[tuple[str, ...], tuple[str, ...], tuple[Tool, ...], tuple[Skill, ...]]:
|
|
564
|
+
"""Normalize runtime/loaded skills for direct Agent construction."""
|
|
565
|
+
|
|
566
|
+
if items is None:
|
|
567
|
+
return (), (), (), ()
|
|
568
|
+
if isinstance(items, Skill):
|
|
569
|
+
return items.tool_names, (items.name,), tuple(items.available_tools()), (items,)
|
|
570
|
+
if isinstance(items, LoadedSkill):
|
|
571
|
+
runtime_skill = getattr(items, "runtime_skill", None)
|
|
572
|
+
if isinstance(runtime_skill, Skill):
|
|
573
|
+
return runtime_skill.tool_names, (runtime_skill.name,), tuple(runtime_skill.available_tools()), (runtime_skill,)
|
|
574
|
+
return tuple(items.manifest.tools), (items.manifest.name,), (), ()
|
|
575
|
+
if isinstance(items, str):
|
|
576
|
+
return (), (items,), (), ()
|
|
577
|
+
if isinstance(items, Iterable) and not isinstance(items, (str, bytes, dict)):
|
|
578
|
+
tool_names: list[str] = []
|
|
579
|
+
skill_names: list[str] = []
|
|
580
|
+
tools: list[Tool] = []
|
|
581
|
+
skills: list[Skill] = []
|
|
582
|
+
for item in items:
|
|
583
|
+
item_tool_names, item_skill_names, item_tools, item_skills = _normalize_agent_skill_inputs(item)
|
|
584
|
+
tool_names.extend(item_tool_names)
|
|
585
|
+
skill_names.extend(item_skill_names)
|
|
586
|
+
tools.extend(item_tools)
|
|
587
|
+
skills.extend(item_skills)
|
|
588
|
+
return tuple(tool_names), tuple(skill_names), tuple(tools), tuple(skills)
|
|
589
|
+
raise TypeError(f"Unsupported agent skills value: {items!r}")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _dedupe_preserve_order(values: Any) -> tuple[str, ...]:
|
|
593
|
+
out: list[str] = []
|
|
594
|
+
seen: set[str] = set()
|
|
595
|
+
for value in values or []:
|
|
596
|
+
text = str(value)
|
|
597
|
+
if text not in seen:
|
|
598
|
+
out.append(text)
|
|
599
|
+
seen.add(text)
|
|
600
|
+
return tuple(out)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _dedupe_tools(tools: Iterable[Tool]) -> tuple[Tool, ...]:
|
|
604
|
+
out: list[Tool] = []
|
|
605
|
+
seen: set[str] = set()
|
|
606
|
+
for tool in tools:
|
|
607
|
+
if tool.name not in seen:
|
|
608
|
+
out.append(tool)
|
|
609
|
+
seen.add(tool.name)
|
|
610
|
+
return tuple(out)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _dedupe_skills(skills: Iterable[Skill]) -> tuple[Skill, ...]:
|
|
614
|
+
out: list[Skill] = []
|
|
615
|
+
seen: set[str] = set()
|
|
616
|
+
for skill in skills:
|
|
617
|
+
if skill.name not in seen:
|
|
618
|
+
out.append(skill)
|
|
619
|
+
seen.add(skill.name)
|
|
620
|
+
return tuple(out)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _json_like(value: Any) -> Any:
|
|
624
|
+
if value is None:
|
|
625
|
+
return None
|
|
626
|
+
if hasattr(value, "model_dump"):
|
|
627
|
+
return value.model_dump(mode="json")
|
|
628
|
+
if isinstance(value, dict):
|
|
629
|
+
return {str(key): _json_like(item) for key, item in value.items()}
|
|
630
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
631
|
+
return [_json_like(item) for item in value]
|
|
632
|
+
if isinstance(value, type):
|
|
633
|
+
return value.__name__
|
|
634
|
+
return value
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _contract_name(value: Any) -> str | None:
|
|
638
|
+
if value is None:
|
|
639
|
+
return None
|
|
640
|
+
return getattr(value, "__name__", type(value).__name__)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _read_node_input(state: dict[str, Any], input: str | Callable[[dict[str, Any]], Any]) -> Any:
|
|
644
|
+
if callable(input):
|
|
645
|
+
return input(state)
|
|
646
|
+
if input not in state:
|
|
647
|
+
raise GraphContractError(
|
|
648
|
+
f"input key '{input}' not found in state. Available keys: {sorted(state.keys())}. "
|
|
649
|
+
"Fix: pass input='<existing_key>' or input=lambda state: ..."
|
|
650
|
+
)
|
|
651
|
+
return state[input]
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _map_node_output(
|
|
655
|
+
result: RunResult,
|
|
656
|
+
state: dict[str, Any],
|
|
657
|
+
output: str | Callable[[RunResult, dict[str, Any]], Any] | None,
|
|
658
|
+
trace: str | None,
|
|
659
|
+
) -> Any:
|
|
660
|
+
if callable(output):
|
|
661
|
+
return output(result, state)
|
|
662
|
+
|
|
663
|
+
update: dict[str, Any] = {}
|
|
664
|
+
if output is not None:
|
|
665
|
+
update[output] = result.text
|
|
666
|
+
if trace is not None:
|
|
667
|
+
update[trace] = result.trace("compact")
|
|
668
|
+
return update
|