toolgauntlet 0.1.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.
- agent_chaos/__init__.py +8 -0
- agent_chaos/adapters/__init__.py +14 -0
- agent_chaos/adapters/demo.py +59 -0
- agent_chaos/adapters/helpers.py +105 -0
- agent_chaos/adapters/openai_compatible.py +209 -0
- agent_chaos/adapters/types.py +16 -0
- agent_chaos/budget_proxy/__init__.py +5 -0
- agent_chaos/budget_proxy/app.py +722 -0
- agent_chaos/budget_proxy/audit.py +27 -0
- agent_chaos/budget_proxy/cli.py +126 -0
- agent_chaos/budget_proxy/config.py +99 -0
- agent_chaos/budget_proxy/costing.py +33 -0
- agent_chaos/budget_proxy/enforcement.py +149 -0
- agent_chaos/budget_proxy/ledger.py +90 -0
- agent_chaos/budget_proxy/policy.py +139 -0
- agent_chaos/budget_proxy/rate_limit.py +119 -0
- agent_chaos/budget_proxy/redaction.py +41 -0
- agent_chaos/budget_proxy/types.py +73 -0
- agent_chaos/budget_proxy/utils.py +14 -0
- agent_chaos/cli.py +566 -0
- agent_chaos/config.py +22 -0
- agent_chaos/exceptions.py +18 -0
- agent_chaos/injectors/__init__.py +49 -0
- agent_chaos/injectors/base.py +62 -0
- agent_chaos/injectors/builtins.py +229 -0
- agent_chaos/loader.py +150 -0
- agent_chaos/models.py +407 -0
- agent_chaos/pack_signing.py +104 -0
- agent_chaos/redaction.py +52 -0
- agent_chaos/runner.py +420 -0
- agent_chaos/scoring.py +219 -0
- agent_chaos/site_builder.py +174 -0
- agent_chaos/suites/ecommerce_refunds_v1/fixtures/tools.json +35 -0
- agent_chaos/suites/ecommerce_refunds_v1/schemas/get_order.json +15 -0
- agent_chaos/suites/ecommerce_refunds_v1/schemas/issue_refund.json +14 -0
- agent_chaos/suites/ecommerce_refunds_v1/suite.yaml +62 -0
- agent_chaos/suites/support_triage_v1/fixtures/tools.json +38 -0
- agent_chaos/suites/support_triage_v1/schemas/classify_ticket.json +13 -0
- agent_chaos/suites/support_triage_v1/schemas/escalate_ticket.json +13 -0
- agent_chaos/suites/support_triage_v1/schemas/get_ticket.json +14 -0
- agent_chaos/suites/support_triage_v1/suite.yaml +49 -0
- agent_chaos/suites/travel_booking_v1/fixtures/tools.json +50 -0
- agent_chaos/suites/travel_booking_v1/schemas/book_flight.json +14 -0
- agent_chaos/suites/travel_booking_v1/schemas/get_travel_policy.json +13 -0
- agent_chaos/suites/travel_booking_v1/schemas/search_flights.json +23 -0
- agent_chaos/suites/travel_booking_v1/suite.yaml +54 -0
- agent_chaos/suites.py +97 -0
- agent_chaos/telemetry.py +177 -0
- agent_chaos/utils.py +26 -0
- toolgauntlet/__init__.py +8 -0
- toolgauntlet/adapters/__init__.py +21 -0
- toolgauntlet/budget_proxy/__init__.py +5 -0
- toolgauntlet/budget_proxy/app.py +5 -0
- toolgauntlet/budget_proxy/cli.py +9 -0
- toolgauntlet/budget_proxy/config.py +5 -0
- toolgauntlet/budget_proxy/types.py +5 -0
- toolgauntlet/cli.py +9 -0
- toolgauntlet/injectors/__init__.py +11 -0
- toolgauntlet/suites.py +17 -0
- toolgauntlet/telemetry.py +5 -0
- toolgauntlet-0.1.0.dist-info/METADATA +264 -0
- toolgauntlet-0.1.0.dist-info/RECORD +65 -0
- toolgauntlet-0.1.0.dist-info/WHEEL +4 -0
- toolgauntlet-0.1.0.dist-info/entry_points.txt +5 -0
- toolgauntlet-0.1.0.dist-info/licenses/LICENSE +21 -0
agent_chaos/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .config import SuiteConfig
|
|
2
|
+
from .models import ChaosReport
|
|
3
|
+
from .pack_signing import sign_pack, verify_pack
|
|
4
|
+
from .runner import run_suite
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = ["ChaosReport", "SuiteConfig", "__version__", "run_suite", "sign_pack", "verify_pack"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .demo import deterministic_demo_agent
|
|
2
|
+
from .helpers import make_async_adapter, make_langchain_like_adapter
|
|
3
|
+
from .openai_compatible import OpenAICompatibleAdapterConfig, make_openai_tool_loop_adapter
|
|
4
|
+
from .types import AgentAdapter, ToolExecutor
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AgentAdapter",
|
|
8
|
+
"ToolExecutor",
|
|
9
|
+
"OpenAICompatibleAdapterConfig",
|
|
10
|
+
"deterministic_demo_agent",
|
|
11
|
+
"make_async_adapter",
|
|
12
|
+
"make_langchain_like_adapter",
|
|
13
|
+
"make_openai_tool_loop_adapter",
|
|
14
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _extract_number(prompt: str, fallback: str = "123") -> str:
|
|
8
|
+
match = re.search(r"\b(\d{2,})\b", prompt)
|
|
9
|
+
return match.group(1) if match else fallback
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def deterministic_demo_agent(prompt: str, tool_executor: Callable[[str, dict[str, Any]], Any], context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
13
|
+
"""Reference adapter used for local smoke tests.
|
|
14
|
+
|
|
15
|
+
It uses simple heuristics per suite by calling listed tools when the prompt suggests them.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
lowered = prompt.lower()
|
|
19
|
+
output_lines: list[str] = []
|
|
20
|
+
|
|
21
|
+
if "refund" in lowered:
|
|
22
|
+
order_id = _extract_number(prompt, fallback="123")
|
|
23
|
+
order = tool_executor("get_order", {"order_id": order_id})
|
|
24
|
+
if isinstance(order, dict) and order.get("days_since_delivery", 999) <= 30:
|
|
25
|
+
tool_executor("issue_refund", {"order_id": order_id, "amount": order.get("order_total", 0)})
|
|
26
|
+
output_lines.append(f"Refund issued for order {order_id}.")
|
|
27
|
+
else:
|
|
28
|
+
output_lines.append(f"Refund denied for order {order_id} due to policy.")
|
|
29
|
+
|
|
30
|
+
elif "triage" in lowered or "ticket" in lowered:
|
|
31
|
+
ticket_id = _extract_number(prompt, fallback="5001")
|
|
32
|
+
ticket = tool_executor("get_ticket", {"ticket_id": ticket_id})
|
|
33
|
+
priority = "high" if isinstance(ticket, dict) and "outage" in str(ticket.get("summary", "")).lower() else "normal"
|
|
34
|
+
tool_executor("classify_ticket", {"ticket_id": ticket_id, "priority": priority})
|
|
35
|
+
output_lines.append(f"Ticket {ticket_id} triaged as {priority} priority.")
|
|
36
|
+
|
|
37
|
+
elif "booking" in lowered or "travel" in lowered:
|
|
38
|
+
trip_id = _extract_number(prompt, fallback="7001")
|
|
39
|
+
options = tool_executor("search_flights", {"trip_id": trip_id})
|
|
40
|
+
selected = None
|
|
41
|
+
if isinstance(options, dict):
|
|
42
|
+
flights = options.get("flights") or []
|
|
43
|
+
selected = flights[0] if flights else None
|
|
44
|
+
if selected:
|
|
45
|
+
tool_executor("book_flight", {"trip_id": trip_id, "flight_id": selected.get("id")})
|
|
46
|
+
output_lines.append(f"Booked travel option {selected.get('id')} for trip {trip_id}.")
|
|
47
|
+
else:
|
|
48
|
+
output_lines.append(f"No valid travel options found for trip {trip_id}.")
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
output_lines.append("Task completed.")
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"output": " ".join(output_lines),
|
|
55
|
+
"usage": {
|
|
56
|
+
"prompt_tokens": max(1, len(prompt.split())),
|
|
57
|
+
"completion_tokens": max(1, len(" ".join(output_lines).split())),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from threading import Thread
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .types import AgentAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _run_awaitable(awaitable: Any) -> Any:
|
|
12
|
+
try:
|
|
13
|
+
asyncio.get_running_loop()
|
|
14
|
+
in_running_loop = True
|
|
15
|
+
except RuntimeError:
|
|
16
|
+
in_running_loop = False
|
|
17
|
+
|
|
18
|
+
if not in_running_loop:
|
|
19
|
+
return asyncio.run(awaitable)
|
|
20
|
+
|
|
21
|
+
result: dict[str, Any] = {}
|
|
22
|
+
error: dict[str, Exception] = {}
|
|
23
|
+
|
|
24
|
+
def _runner() -> None:
|
|
25
|
+
try:
|
|
26
|
+
result["value"] = asyncio.run(awaitable)
|
|
27
|
+
except Exception as exc: # pragma: no cover - exercised via caller branches
|
|
28
|
+
error["exc"] = exc
|
|
29
|
+
|
|
30
|
+
thread = Thread(target=_runner, daemon=True)
|
|
31
|
+
thread.start()
|
|
32
|
+
thread.join()
|
|
33
|
+
if "exc" in error:
|
|
34
|
+
raise error["exc"]
|
|
35
|
+
return result.get("value")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def make_async_adapter(async_agent: Any) -> AgentAdapter:
|
|
39
|
+
"""Wrap an async callable into ToolGauntlet's synchronous adapter interface."""
|
|
40
|
+
|
|
41
|
+
if not callable(async_agent):
|
|
42
|
+
raise ValueError("async_agent must be callable")
|
|
43
|
+
|
|
44
|
+
def run(prompt: str, tool_executor: Any, context: dict[str, Any] | None = None) -> Any:
|
|
45
|
+
value = async_agent(prompt, tool_executor, context)
|
|
46
|
+
if inspect.isawaitable(value):
|
|
47
|
+
return _run_awaitable(value)
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
return run
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def make_langchain_like_adapter(
|
|
54
|
+
runnable: Any,
|
|
55
|
+
*,
|
|
56
|
+
input_key: str = "input",
|
|
57
|
+
output_key: str = "output",
|
|
58
|
+
include_tool_executor: bool = True,
|
|
59
|
+
include_context: bool = True,
|
|
60
|
+
) -> AgentAdapter:
|
|
61
|
+
"""Adapt invoke/ainvoke-style runnables to the ToolGauntlet adapter contract.
|
|
62
|
+
|
|
63
|
+
This helper is intentionally framework-agnostic and works with:
|
|
64
|
+
- objects implementing `invoke(payload)` and/or `ainvoke(payload)`
|
|
65
|
+
- plain callables that accept a single payload argument
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
has_invoke = callable(getattr(runnable, "invoke", None))
|
|
69
|
+
has_ainvoke = callable(getattr(runnable, "ainvoke", None))
|
|
70
|
+
if not has_invoke and not has_ainvoke and not callable(runnable):
|
|
71
|
+
raise ValueError("runnable must be callable or implement invoke/ainvoke")
|
|
72
|
+
|
|
73
|
+
if not input_key:
|
|
74
|
+
raise ValueError("input_key must be non-empty")
|
|
75
|
+
if not output_key:
|
|
76
|
+
raise ValueError("output_key must be non-empty")
|
|
77
|
+
|
|
78
|
+
def run(prompt: str, tool_executor: Any, context: dict[str, Any] | None = None) -> Any:
|
|
79
|
+
payload: dict[str, Any] = {input_key: prompt}
|
|
80
|
+
if include_tool_executor:
|
|
81
|
+
payload["tool_executor"] = tool_executor
|
|
82
|
+
if include_context:
|
|
83
|
+
payload["context"] = context or {}
|
|
84
|
+
|
|
85
|
+
if has_invoke:
|
|
86
|
+
value = runnable.invoke(payload)
|
|
87
|
+
elif has_ainvoke:
|
|
88
|
+
value = runnable.ainvoke(payload)
|
|
89
|
+
else:
|
|
90
|
+
value = runnable(payload)
|
|
91
|
+
|
|
92
|
+
if inspect.isawaitable(value):
|
|
93
|
+
value = _run_awaitable(value)
|
|
94
|
+
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
if "output" in value:
|
|
97
|
+
return value
|
|
98
|
+
if output_key in value:
|
|
99
|
+
normalized = {"output": value[output_key]}
|
|
100
|
+
if "usage" in value and isinstance(value["usage"], dict):
|
|
101
|
+
normalized["usage"] = value["usage"]
|
|
102
|
+
return normalized
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
return run
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .types import AgentAdapter, ToolExecutor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class OpenAICompatibleAdapterConfig:
|
|
15
|
+
model: str
|
|
16
|
+
base_url: str = "https://api.openai.com"
|
|
17
|
+
api_key: str | None = None
|
|
18
|
+
endpoint: str = "/v1/chat/completions"
|
|
19
|
+
max_turns: int = 8
|
|
20
|
+
temperature: float | None = None
|
|
21
|
+
timeout_seconds: float = 60.0
|
|
22
|
+
system_prompt: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize_schema(schema: dict[str, Any] | None) -> dict[str, Any]:
|
|
26
|
+
if not schema or not isinstance(schema, dict):
|
|
27
|
+
return {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {},
|
|
30
|
+
"additionalProperties": True,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if schema.get("type") != "object":
|
|
34
|
+
return {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {"input": schema},
|
|
37
|
+
"additionalProperties": True,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return schema
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_tools_from_context(context: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
44
|
+
if not context:
|
|
45
|
+
return []
|
|
46
|
+
raw_tools = context.get("tools", [])
|
|
47
|
+
if not isinstance(raw_tools, list):
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
tools: list[dict[str, Any]] = []
|
|
51
|
+
for item in raw_tools:
|
|
52
|
+
if not isinstance(item, dict):
|
|
53
|
+
continue
|
|
54
|
+
name = item.get("name")
|
|
55
|
+
if not isinstance(name, str) or not name:
|
|
56
|
+
continue
|
|
57
|
+
schema = _normalize_schema(item.get("schema"))
|
|
58
|
+
description = item.get("description")
|
|
59
|
+
if not isinstance(description, str):
|
|
60
|
+
description = f"Execute {name} and return structured JSON output."
|
|
61
|
+
tools.append(
|
|
62
|
+
{
|
|
63
|
+
"type": "function",
|
|
64
|
+
"function": {
|
|
65
|
+
"name": name,
|
|
66
|
+
"description": description,
|
|
67
|
+
"parameters": schema,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
return tools
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _tool_result_to_content(value: Any) -> str:
|
|
75
|
+
try:
|
|
76
|
+
return json.dumps(value, ensure_ascii=True)
|
|
77
|
+
except TypeError:
|
|
78
|
+
return json.dumps({"value": str(value)}, ensure_ascii=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_tool_arguments(raw_args: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
82
|
+
if raw_args is None:
|
|
83
|
+
return {}
|
|
84
|
+
if isinstance(raw_args, dict):
|
|
85
|
+
return raw_args
|
|
86
|
+
if not isinstance(raw_args, str):
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
text = raw_args.strip()
|
|
90
|
+
if not text:
|
|
91
|
+
return {}
|
|
92
|
+
try:
|
|
93
|
+
parsed = json.loads(text)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
return {}
|
|
96
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def make_openai_tool_loop_adapter(config: OpenAICompatibleAdapterConfig) -> AgentAdapter:
|
|
100
|
+
api_key = config.api_key or os.environ.get("OPENAI_API_KEY")
|
|
101
|
+
if not api_key:
|
|
102
|
+
raise ValueError("OpenAI-compatible adapter requires api_key or OPENAI_API_KEY")
|
|
103
|
+
|
|
104
|
+
base_url = config.base_url.rstrip("/")
|
|
105
|
+
endpoint = config.endpoint if config.endpoint.startswith("/") else f"/{config.endpoint}"
|
|
106
|
+
|
|
107
|
+
def run(prompt: str, tool_executor: ToolExecutor, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
108
|
+
tools = _build_tools_from_context(context)
|
|
109
|
+
|
|
110
|
+
messages: list[dict[str, Any]] = []
|
|
111
|
+
if config.system_prompt:
|
|
112
|
+
messages.append({"role": "system", "content": config.system_prompt})
|
|
113
|
+
|
|
114
|
+
if context and isinstance(context.get("constraints"), list) and context["constraints"]:
|
|
115
|
+
constraints = "\n".join(f"- {item}" for item in context["constraints"])
|
|
116
|
+
messages.append(
|
|
117
|
+
{
|
|
118
|
+
"role": "system",
|
|
119
|
+
"content": f"Task constraints:\n{constraints}",
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
messages.append({"role": "user", "content": prompt})
|
|
124
|
+
|
|
125
|
+
request_headers = {
|
|
126
|
+
"Authorization": f"Bearer {api_key}",
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
usage: dict[str, Any] | None = None
|
|
131
|
+
|
|
132
|
+
with httpx.Client(base_url=base_url, timeout=config.timeout_seconds) as client:
|
|
133
|
+
for _ in range(config.max_turns):
|
|
134
|
+
body: dict[str, Any] = {
|
|
135
|
+
"model": config.model,
|
|
136
|
+
"messages": messages,
|
|
137
|
+
"stream": False,
|
|
138
|
+
}
|
|
139
|
+
if tools:
|
|
140
|
+
body["tools"] = tools
|
|
141
|
+
body["tool_choice"] = "auto"
|
|
142
|
+
if config.temperature is not None:
|
|
143
|
+
body["temperature"] = config.temperature
|
|
144
|
+
|
|
145
|
+
response = client.post(endpoint, json=body, headers=request_headers)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
payload = response.json()
|
|
148
|
+
|
|
149
|
+
choices = payload.get("choices") or []
|
|
150
|
+
if not choices:
|
|
151
|
+
raise RuntimeError("OpenAI-compatible response missing choices")
|
|
152
|
+
|
|
153
|
+
message = choices[0].get("message") or {}
|
|
154
|
+
usage_payload = payload.get("usage")
|
|
155
|
+
if isinstance(usage_payload, dict):
|
|
156
|
+
usage = {
|
|
157
|
+
"prompt_tokens": int(usage_payload.get("prompt_tokens", 0)),
|
|
158
|
+
"completion_tokens": int(usage_payload.get("completion_tokens", 0)),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
tool_calls = message.get("tool_calls") or []
|
|
162
|
+
if tool_calls:
|
|
163
|
+
assistant_message = {
|
|
164
|
+
"role": "assistant",
|
|
165
|
+
"content": message.get("content"),
|
|
166
|
+
"tool_calls": tool_calls,
|
|
167
|
+
}
|
|
168
|
+
messages.append(assistant_message)
|
|
169
|
+
|
|
170
|
+
for call in tool_calls:
|
|
171
|
+
if not isinstance(call, dict):
|
|
172
|
+
continue
|
|
173
|
+
function_payload = call.get("function") or {}
|
|
174
|
+
tool_name = function_payload.get("name")
|
|
175
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
176
|
+
continue
|
|
177
|
+
arguments = _parse_tool_arguments(function_payload.get("arguments"))
|
|
178
|
+
result = tool_executor(tool_name, arguments)
|
|
179
|
+
messages.append(
|
|
180
|
+
{
|
|
181
|
+
"role": "tool",
|
|
182
|
+
"tool_call_id": call.get("id", ""),
|
|
183
|
+
"name": tool_name,
|
|
184
|
+
"content": _tool_result_to_content(result),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
content = message.get("content")
|
|
190
|
+
if content is None:
|
|
191
|
+
content = ""
|
|
192
|
+
if isinstance(content, list):
|
|
193
|
+
text_parts: list[str] = []
|
|
194
|
+
for part in content:
|
|
195
|
+
if isinstance(part, dict) and isinstance(part.get("text"), str):
|
|
196
|
+
text_parts.append(part["text"])
|
|
197
|
+
content = "\n".join(text_parts)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"output": str(content),
|
|
201
|
+
"usage": usage,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"output": "Reached max_turns without final assistant response.",
|
|
206
|
+
"usage": usage,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return run
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ToolExecutor = Callable[[str, dict[str, Any] | None], Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentAdapter(Protocol):
|
|
10
|
+
def __call__(
|
|
11
|
+
self,
|
|
12
|
+
prompt: str,
|
|
13
|
+
tool_executor: ToolExecutor,
|
|
14
|
+
context: dict[str, Any] | None = None,
|
|
15
|
+
) -> Any:
|
|
16
|
+
...
|