deepharness 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.
- deepharness/__init__.py +101 -0
- deepharness/agent/__init__.py +33 -0
- deepharness/agent/loop.py +414 -0
- deepharness/agent/output.py +173 -0
- deepharness/agent/state.py +248 -0
- deepharness/agent/turn.py +131 -0
- deepharness/errors.py +102 -0
- deepharness/graph/__init__.py +14 -0
- deepharness/graph/builder.py +186 -0
- deepharness/graph/executor.py +128 -0
- deepharness/graph/reducers.py +43 -0
- deepharness/providers/__init__.py +51 -0
- deepharness/providers/anthropic.py +293 -0
- deepharness/providers/base.py +203 -0
- deepharness/providers/client.py +121 -0
- deepharness/providers/gateways.py +99 -0
- deepharness/providers/gemini.py +232 -0
- deepharness/providers/openai.py +281 -0
- deepharness/providers/rest.py +189 -0
- deepharness/providers/wire.py +79 -0
- deepharness/py.typed +0 -0
- deepharness/tools/__init__.py +3 -0
- deepharness/tools/toolbox.py +248 -0
- deepharness-0.1.0.dist-info/METADATA +194 -0
- deepharness-0.1.0.dist-info/RECORD +28 -0
- deepharness-0.1.0.dist-info/WHEEL +5 -0
- deepharness-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepharness-0.1.0.dist-info/top_level.txt +1 -0
deepharness/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from deepharness.agent import (
|
|
2
|
+
FINAL_TOOL,
|
|
3
|
+
Agent,
|
|
4
|
+
AgentState,
|
|
5
|
+
Budget,
|
|
6
|
+
Ctx,
|
|
7
|
+
Finished,
|
|
8
|
+
Message,
|
|
9
|
+
PendingHumanInput,
|
|
10
|
+
StopReason,
|
|
11
|
+
TokenBudgetExceeded,
|
|
12
|
+
Toolbox,
|
|
13
|
+
ToolSpec,
|
|
14
|
+
load_session,
|
|
15
|
+
save_session,
|
|
16
|
+
tool,
|
|
17
|
+
)
|
|
18
|
+
from deepharness.errors import (
|
|
19
|
+
ConfigurationError,
|
|
20
|
+
DeepHarnessError,
|
|
21
|
+
HumanInputRequired,
|
|
22
|
+
ProviderError,
|
|
23
|
+
ToolNotFoundError,
|
|
24
|
+
)
|
|
25
|
+
from deepharness.graph import (
|
|
26
|
+
ConcurrentUpdateError,
|
|
27
|
+
ExecutionError,
|
|
28
|
+
Executor,
|
|
29
|
+
Graph,
|
|
30
|
+
NodeSpec,
|
|
31
|
+
StepLimitExceeded,
|
|
32
|
+
concat,
|
|
33
|
+
merge_dicts,
|
|
34
|
+
)
|
|
35
|
+
from deepharness.providers import (
|
|
36
|
+
VLLM,
|
|
37
|
+
XAI,
|
|
38
|
+
Anthropic,
|
|
39
|
+
Cerebras,
|
|
40
|
+
Completed,
|
|
41
|
+
DeepSeek,
|
|
42
|
+
Fireworks,
|
|
43
|
+
Gemini,
|
|
44
|
+
Groq,
|
|
45
|
+
LMStudio,
|
|
46
|
+
Mistral,
|
|
47
|
+
Ollama,
|
|
48
|
+
OpenAI,
|
|
49
|
+
OpenRouter,
|
|
50
|
+
TextDelta,
|
|
51
|
+
Together,
|
|
52
|
+
TokenUsage,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"FINAL_TOOL",
|
|
57
|
+
"VLLM",
|
|
58
|
+
"XAI",
|
|
59
|
+
"Agent",
|
|
60
|
+
"AgentState",
|
|
61
|
+
"Anthropic",
|
|
62
|
+
"Budget",
|
|
63
|
+
"Cerebras",
|
|
64
|
+
"Completed",
|
|
65
|
+
"ConcurrentUpdateError",
|
|
66
|
+
"ConfigurationError",
|
|
67
|
+
"Ctx",
|
|
68
|
+
"DeepHarnessError",
|
|
69
|
+
"DeepSeek",
|
|
70
|
+
"ExecutionError",
|
|
71
|
+
"Executor",
|
|
72
|
+
"Finished",
|
|
73
|
+
"Fireworks",
|
|
74
|
+
"Gemini",
|
|
75
|
+
"Graph",
|
|
76
|
+
"Groq",
|
|
77
|
+
"HumanInputRequired",
|
|
78
|
+
"LMStudio",
|
|
79
|
+
"Message",
|
|
80
|
+
"Mistral",
|
|
81
|
+
"NodeSpec",
|
|
82
|
+
"Ollama",
|
|
83
|
+
"OpenAI",
|
|
84
|
+
"OpenRouter",
|
|
85
|
+
"PendingHumanInput",
|
|
86
|
+
"ProviderError",
|
|
87
|
+
"StepLimitExceeded",
|
|
88
|
+
"StopReason",
|
|
89
|
+
"TextDelta",
|
|
90
|
+
"Together",
|
|
91
|
+
"TokenBudgetExceeded",
|
|
92
|
+
"TokenUsage",
|
|
93
|
+
"ToolNotFoundError",
|
|
94
|
+
"ToolSpec",
|
|
95
|
+
"Toolbox",
|
|
96
|
+
"concat",
|
|
97
|
+
"load_session",
|
|
98
|
+
"merge_dicts",
|
|
99
|
+
"save_session",
|
|
100
|
+
"tool",
|
|
101
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from ..tools.toolbox import Ctx, Toolbox, ToolSpec, tool
|
|
2
|
+
from .loop import Agent, TokenBudgetExceeded
|
|
3
|
+
from .output import FINAL_TOOL
|
|
4
|
+
from .state import (
|
|
5
|
+
AgentState,
|
|
6
|
+
Budget,
|
|
7
|
+
Finished,
|
|
8
|
+
Message,
|
|
9
|
+
PendingHumanInput,
|
|
10
|
+
StopReason,
|
|
11
|
+
as_dict,
|
|
12
|
+
load_session,
|
|
13
|
+
save_session,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"FINAL_TOOL",
|
|
18
|
+
"Agent",
|
|
19
|
+
"AgentState",
|
|
20
|
+
"Budget",
|
|
21
|
+
"Ctx",
|
|
22
|
+
"Finished",
|
|
23
|
+
"Message",
|
|
24
|
+
"PendingHumanInput",
|
|
25
|
+
"StopReason",
|
|
26
|
+
"TokenBudgetExceeded",
|
|
27
|
+
"ToolSpec",
|
|
28
|
+
"Toolbox",
|
|
29
|
+
"as_dict",
|
|
30
|
+
"load_session",
|
|
31
|
+
"save_session",
|
|
32
|
+
"tool",
|
|
33
|
+
]
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterator, Callable, Generator, Iterable, Iterator
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from deepharness.providers.base import (
|
|
9
|
+
LLM,
|
|
10
|
+
TextDelta,
|
|
11
|
+
TokenUsage,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..errors import (
|
|
15
|
+
ConfigurationError,
|
|
16
|
+
OutputValidationError,
|
|
17
|
+
TokenBudgetExceeded,
|
|
18
|
+
)
|
|
19
|
+
from ..tools.toolbox import Ctx, Toolbox, ToolSpec
|
|
20
|
+
from . import turn
|
|
21
|
+
from .output import FINAL_TOOL, coerce, final_tool_schema, find_final
|
|
22
|
+
from .state import (
|
|
23
|
+
AgentState,
|
|
24
|
+
Budget,
|
|
25
|
+
Finished,
|
|
26
|
+
Message,
|
|
27
|
+
PendingHumanInput,
|
|
28
|
+
StopReason,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
AgentEvent = TextDelta | Finished
|
|
32
|
+
"""What streaming a run emits: prose as it arrives, then the final state."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class _Ask:
|
|
37
|
+
"""A model call the loop needs its driver to perform."""
|
|
38
|
+
|
|
39
|
+
messages: list[dict[str, Any]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(slots=True)
|
|
43
|
+
class _Dispatch:
|
|
44
|
+
"""One turn's tool calls, for the driver to run however it runs them."""
|
|
45
|
+
|
|
46
|
+
calls: list[Any]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Agent:
|
|
50
|
+
"""A model plus tools, run as a think/act loop over an AgentState.
|
|
51
|
+
|
|
52
|
+
Ask the model, dispatch whatever tools it requests, repeat until it answers
|
|
53
|
+
or the budget runs out. Without a model the run is a passthrough, which is
|
|
54
|
+
what makes an Agent usable as a placeholder node in a Graph.
|
|
55
|
+
|
|
56
|
+
The parts worth knowing before reading the loop:
|
|
57
|
+
|
|
58
|
+
* Only stop_reason == "answer" means the model actually replied. Every other
|
|
59
|
+
reason leaves output empty or partial, so state.answered is the check to
|
|
60
|
+
make before trusting it.
|
|
61
|
+
* run() is a real synchronous path, not arun() wrapped in an event loop, so
|
|
62
|
+
it raises for a tool that turns out to be `async def`. arun() dispatches a
|
|
63
|
+
turn's tools concurrently, sync ones included - those go to threads.
|
|
64
|
+
* A failing tool does not end the run: the error becomes that call's result
|
|
65
|
+
so the model gets a turn to correct itself.
|
|
66
|
+
* total_usage accumulates across every model call this instance makes, not
|
|
67
|
+
per run, and Budget(tokens=...) turns crossing it into TokenBudgetExceeded
|
|
68
|
+
with the partial state attached.
|
|
69
|
+
* Every public entry point - arun, run, astream, stream - is the same loop;
|
|
70
|
+
only the I/O differs. See astream_events for the one async driver.
|
|
71
|
+
|
|
72
|
+
Pausing has two flavours, and they resolve differently: a tool marked
|
|
73
|
+
requires_approval has not run yet (approve it and it runs), while a tool
|
|
74
|
+
raising HumanInputRequired is asking a question (your answer becomes its
|
|
75
|
+
result). See deepharness/agent/turn.py.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
__slots__ = (
|
|
79
|
+
"_budget",
|
|
80
|
+
"_final_schema",
|
|
81
|
+
"_model",
|
|
82
|
+
"_name",
|
|
83
|
+
"_output",
|
|
84
|
+
"_system",
|
|
85
|
+
"_tools",
|
|
86
|
+
"_total_usage",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
model: LLM | None = None,
|
|
92
|
+
*,
|
|
93
|
+
tools: Iterable[Callable[..., Any]] | Toolbox = (),
|
|
94
|
+
system: str | None = None,
|
|
95
|
+
name: str = "agent",
|
|
96
|
+
budget: Budget | None = None,
|
|
97
|
+
output: type | None = None,
|
|
98
|
+
):
|
|
99
|
+
self._model = model
|
|
100
|
+
self._tools = tools if isinstance(tools, Toolbox) else Toolbox(tools)
|
|
101
|
+
self._system = system
|
|
102
|
+
self._name = name
|
|
103
|
+
self._budget = budget or Budget()
|
|
104
|
+
self._output = output
|
|
105
|
+
self._final_schema = final_tool_schema(output) if output is not None else None
|
|
106
|
+
self._total_usage = TokenUsage(0, 0, 0)
|
|
107
|
+
|
|
108
|
+
# Read-only views: an agent's configuration is settled at construction, and
|
|
109
|
+
# total_usage is live state no caller should be able to reset or inflate.
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def name(self) -> str:
|
|
113
|
+
return self._name
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def model(self) -> LLM | None:
|
|
117
|
+
return self._model
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def tools(self) -> Toolbox:
|
|
121
|
+
return self._tools
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def system(self) -> str | None:
|
|
125
|
+
return self._system
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def budget(self) -> Budget:
|
|
129
|
+
return self._budget
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def output(self) -> type | None:
|
|
133
|
+
return self._output
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def total_usage(self) -> TokenUsage:
|
|
137
|
+
"""Cumulative usage across every model call this agent has made."""
|
|
138
|
+
return self._total_usage
|
|
139
|
+
|
|
140
|
+
def as_tool(
|
|
141
|
+
self, *, name: str | None = None, description: str | None = None
|
|
142
|
+
) -> Callable[..., Any]:
|
|
143
|
+
"""Wrap this Agent as a tool callable from another Agent's toolbox.
|
|
144
|
+
|
|
145
|
+
The wrapped tool takes a single `input` string, runs it through
|
|
146
|
+
arun() as a user message, and returns the resulting output text.
|
|
147
|
+
It's async, so register it with an Agent that calls arun() -
|
|
148
|
+
call_sync() raises ConfigurationError for async tools, same as any
|
|
149
|
+
other async tool.
|
|
150
|
+
|
|
151
|
+
The sub-agent inherits the caller's deps, so a delegated run keeps the
|
|
152
|
+
database handle or tenant the parent was given.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
async def call(input: str, ctx: Ctx) -> str:
|
|
156
|
+
result = await self.arun(input, deps=ctx.deps)
|
|
157
|
+
return result.output
|
|
158
|
+
|
|
159
|
+
call.__name__ = name or self._name
|
|
160
|
+
call._tool_spec = ToolSpec( # type: ignore[attr-defined]
|
|
161
|
+
name=name or self._name,
|
|
162
|
+
description=description
|
|
163
|
+
or self._system
|
|
164
|
+
or f"Delegate a task to the '{self._name}' agent.",
|
|
165
|
+
parameters={
|
|
166
|
+
"type": "object",
|
|
167
|
+
"properties": {"input": {"type": "string"}},
|
|
168
|
+
"required": ["input"],
|
|
169
|
+
},
|
|
170
|
+
func=call,
|
|
171
|
+
ctx_params=("ctx",),
|
|
172
|
+
)
|
|
173
|
+
return call
|
|
174
|
+
|
|
175
|
+
def _account_for_usage(
|
|
176
|
+
self,
|
|
177
|
+
response: Any,
|
|
178
|
+
state: AgentState,
|
|
179
|
+
messages: list[dict[str, Any]],
|
|
180
|
+
) -> None:
|
|
181
|
+
if response.usage is None:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
self._total_usage = self._total_usage + response.usage
|
|
185
|
+
if (
|
|
186
|
+
self._budget.tokens is not None
|
|
187
|
+
and self._total_usage.total_tokens > self._budget.tokens
|
|
188
|
+
):
|
|
189
|
+
raise TokenBudgetExceeded(
|
|
190
|
+
self._name,
|
|
191
|
+
self._total_usage,
|
|
192
|
+
self._budget.tokens,
|
|
193
|
+
state=self._result(state, messages, response.content, "token_budget"),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _result(
|
|
197
|
+
self,
|
|
198
|
+
state: AgentState,
|
|
199
|
+
messages: list[dict[str, Any]],
|
|
200
|
+
output: Any,
|
|
201
|
+
stop_reason: StopReason,
|
|
202
|
+
paused: list[PendingHumanInput] | None = None,
|
|
203
|
+
) -> AgentState:
|
|
204
|
+
return AgentState(
|
|
205
|
+
messages=messages,
|
|
206
|
+
output=output,
|
|
207
|
+
usage=self._total_usage,
|
|
208
|
+
stop_reason=stop_reason,
|
|
209
|
+
paused=paused or [],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _turns(
|
|
213
|
+
self, state: AgentState, messages: list[dict[str, Any]]
|
|
214
|
+
) -> Generator[_Ask | _Dispatch, Any, AgentState]:
|
|
215
|
+
"""The think/act loop itself, with the I/O lifted out of it.
|
|
216
|
+
|
|
217
|
+
The loop yields the work it needs done - a model call, or a round of
|
|
218
|
+
tool calls - and its driver performs that work and sends the result
|
|
219
|
+
back. run() can then stay a genuinely synchronous path and arun() a
|
|
220
|
+
concurrent one, while everything they agree on (the step budget, stop
|
|
221
|
+
reasons, pausing for a human, feeding a failed tool back to the model)
|
|
222
|
+
lives here once instead of being maintained in two copies.
|
|
223
|
+
|
|
224
|
+
A run that starts from a paused state settles the approvals first: the
|
|
225
|
+
allowed calls run now, with the arguments the model originally sent, and
|
|
226
|
+
only then does the loop go back to the model with their results.
|
|
227
|
+
"""
|
|
228
|
+
approved = turn.settle(state, messages, self._name)
|
|
229
|
+
if approved:
|
|
230
|
+
results = yield _Dispatch(approved)
|
|
231
|
+
turn.record_results(messages, approved, results)
|
|
232
|
+
|
|
233
|
+
for _ in range(self._budget.steps):
|
|
234
|
+
response = yield _Ask(messages)
|
|
235
|
+
self._account_for_usage(response, state, messages)
|
|
236
|
+
|
|
237
|
+
final = find_final(response) if self._final_schema else None
|
|
238
|
+
if final is not None:
|
|
239
|
+
turn.record_request(messages, response)
|
|
240
|
+
try:
|
|
241
|
+
answer = coerce(self._output, final.arguments)
|
|
242
|
+
except OutputValidationError as exc:
|
|
243
|
+
# Same courtesy a failing tool gets: hand the model the
|
|
244
|
+
# error so it can call FINAL_TOOL again with valid fields.
|
|
245
|
+
messages.append(
|
|
246
|
+
Message.tool(
|
|
247
|
+
f"Error: {exc}", name=FINAL_TOOL, call_id=final.id
|
|
248
|
+
).to_dict()
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
return self._result(state, messages, answer, "answer")
|
|
252
|
+
|
|
253
|
+
if not response.tool_calls:
|
|
254
|
+
messages.append(Message.ai(response.content).to_dict())
|
|
255
|
+
if self._final_schema is not None:
|
|
256
|
+
# output= was asked for, so plain prose is not an answer yet.
|
|
257
|
+
messages.append(
|
|
258
|
+
Message.human(f"Answer by calling {FINAL_TOOL}.").to_dict()
|
|
259
|
+
)
|
|
260
|
+
continue
|
|
261
|
+
return self._result(state, messages, response.content, "answer")
|
|
262
|
+
|
|
263
|
+
if not self._tools:
|
|
264
|
+
raise ConfigurationError(
|
|
265
|
+
f"{self._name} received tool calls but has no tools registered"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
turn.record_request(messages, response)
|
|
269
|
+
wanted = [call for call in response.tool_calls if call.name != FINAL_TOOL]
|
|
270
|
+
gated = turn.gated(self._tools, wanted)
|
|
271
|
+
if gated:
|
|
272
|
+
# Nothing in this turn runs until the human rules on the gated
|
|
273
|
+
# call: letting the rest run first would half-apply a turn the
|
|
274
|
+
# human may be about to refuse.
|
|
275
|
+
return self._result(state, messages, "", "paused", paused=gated)
|
|
276
|
+
|
|
277
|
+
results = yield _Dispatch(wanted)
|
|
278
|
+
dispatched = wanted
|
|
279
|
+
pending = turn.record_results(messages, dispatched, results)
|
|
280
|
+
if pending:
|
|
281
|
+
return self._result(state, messages, "", "paused", paused=pending)
|
|
282
|
+
|
|
283
|
+
return self._result(state, messages, "", "step_budget")
|
|
284
|
+
|
|
285
|
+
def _passthrough(self, state: AgentState) -> AgentState:
|
|
286
|
+
"""Without a model an Agent is inert - a placeholder node in a Graph.
|
|
287
|
+
|
|
288
|
+
Returns the state untouched rather than announcing itself: printing from
|
|
289
|
+
inside the agent would make this path untestable without capturing
|
|
290
|
+
stdout, and a library has no business writing to a caller's console.
|
|
291
|
+
"""
|
|
292
|
+
return state
|
|
293
|
+
|
|
294
|
+
def _schemas(self) -> list[dict[str, Any]] | None:
|
|
295
|
+
schemas = self._tools.schemas()
|
|
296
|
+
if self._final_schema is not None:
|
|
297
|
+
schemas.append(self._final_schema)
|
|
298
|
+
return schemas or None
|
|
299
|
+
|
|
300
|
+
async def arun(self, state: Any = None, *, deps: Any = None) -> AgentState:
|
|
301
|
+
"""Run to completion. Tool calls in the same turn dispatch concurrently."""
|
|
302
|
+
async for event in self.astream_events(state, deps=deps):
|
|
303
|
+
if isinstance(event, Finished):
|
|
304
|
+
return event.state
|
|
305
|
+
raise AssertionError("a run always ends with Finished") # pragma: no cover
|
|
306
|
+
|
|
307
|
+
def run(self, state: Any = None, *, deps: Any = None) -> AgentState:
|
|
308
|
+
"""Synchronous counterpart to arun(). Raises if a tool is `async def`."""
|
|
309
|
+
for event in self.stream_events(state, deps=deps):
|
|
310
|
+
if isinstance(event, Finished):
|
|
311
|
+
return event.state
|
|
312
|
+
raise AssertionError("a run always ends with Finished") # pragma: no cover
|
|
313
|
+
|
|
314
|
+
async def astream_events(
|
|
315
|
+
self, state: Any = None, *, deps: Any = None
|
|
316
|
+
) -> AsyncIterator[AgentEvent]:
|
|
317
|
+
"""Drive one run, emitting prose as it arrives then the final state.
|
|
318
|
+
|
|
319
|
+
The only async driver: arun() consumes this and keeps the last event, so
|
|
320
|
+
the loop's mechanics - budget, approvals, tool dispatch - exist once
|
|
321
|
+
rather than once per public method. Providers that cannot really stream
|
|
322
|
+
still work here; their turn simply arrives as a single delta.
|
|
323
|
+
"""
|
|
324
|
+
state = AgentState.of(state)
|
|
325
|
+
if self._model is None:
|
|
326
|
+
yield Finished(self._passthrough(state))
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
ctx = Ctx(state=state, deps=deps)
|
|
330
|
+
turns = self._turns(state, turn.prepare(state, self._system))
|
|
331
|
+
schemas = self._schemas()
|
|
332
|
+
outcome: Any = None
|
|
333
|
+
try:
|
|
334
|
+
while True:
|
|
335
|
+
request = turns.send(outcome)
|
|
336
|
+
if isinstance(request, _Ask):
|
|
337
|
+
async for event in self._model.astream_events(
|
|
338
|
+
request.messages, tools=schemas
|
|
339
|
+
):
|
|
340
|
+
if isinstance(event, TextDelta):
|
|
341
|
+
yield event
|
|
342
|
+
else:
|
|
343
|
+
outcome = event.response
|
|
344
|
+
else:
|
|
345
|
+
outcome = await asyncio.gather(
|
|
346
|
+
*(
|
|
347
|
+
self._call_tool(call.name, call.arguments, ctx)
|
|
348
|
+
for call in request.calls
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
except StopIteration as done:
|
|
352
|
+
yield Finished(done.value)
|
|
353
|
+
|
|
354
|
+
def stream_events(
|
|
355
|
+
self, state: Any = None, *, deps: Any = None
|
|
356
|
+
) -> Iterator[AgentEvent]:
|
|
357
|
+
"""Synchronous counterpart to astream_events()."""
|
|
358
|
+
state = AgentState.of(state)
|
|
359
|
+
if self._model is None:
|
|
360
|
+
yield Finished(self._passthrough(state))
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
ctx = Ctx(state=state, deps=deps)
|
|
364
|
+
turns = self._turns(state, turn.prepare(state, self._system))
|
|
365
|
+
schemas = self._schemas()
|
|
366
|
+
outcome: Any = None
|
|
367
|
+
try:
|
|
368
|
+
while True:
|
|
369
|
+
request = turns.send(outcome)
|
|
370
|
+
if isinstance(request, _Ask):
|
|
371
|
+
for event in self._model.stream_events(
|
|
372
|
+
request.messages, tools=schemas
|
|
373
|
+
):
|
|
374
|
+
if isinstance(event, TextDelta):
|
|
375
|
+
yield event
|
|
376
|
+
else:
|
|
377
|
+
outcome = event.response
|
|
378
|
+
else:
|
|
379
|
+
outcome = [
|
|
380
|
+
self._call_tool_sync(call.name, call.arguments, ctx)
|
|
381
|
+
for call in request.calls
|
|
382
|
+
]
|
|
383
|
+
except StopIteration as done:
|
|
384
|
+
yield Finished(done.value)
|
|
385
|
+
|
|
386
|
+
async def astream(
|
|
387
|
+
self, state: Any = None, *, deps: Any = None
|
|
388
|
+
) -> AsyncIterator[str]:
|
|
389
|
+
"""Just the text, for the common "print as it types" case."""
|
|
390
|
+
async for event in self.astream_events(state, deps=deps):
|
|
391
|
+
if isinstance(event, TextDelta):
|
|
392
|
+
yield event.text
|
|
393
|
+
|
|
394
|
+
def stream(self, state: Any = None, *, deps: Any = None) -> Iterator[str]:
|
|
395
|
+
"""Synchronous counterpart to astream()."""
|
|
396
|
+
for event in self.stream_events(state, deps=deps):
|
|
397
|
+
if isinstance(event, TextDelta):
|
|
398
|
+
yield event.text
|
|
399
|
+
|
|
400
|
+
async def _call_tool(self, name: str, arguments: dict[str, Any], ctx: Ctx) -> Any:
|
|
401
|
+
try:
|
|
402
|
+
return await self._tools.call(name, ctx=ctx, **arguments)
|
|
403
|
+
except ConfigurationError:
|
|
404
|
+
raise
|
|
405
|
+
except Exception as exc: # noqa: BLE001 - see transcript.record_results
|
|
406
|
+
return exc
|
|
407
|
+
|
|
408
|
+
def _call_tool_sync(self, name: str, arguments: dict[str, Any], ctx: Ctx) -> Any:
|
|
409
|
+
try:
|
|
410
|
+
return self._tools.call_sync(name, ctx=ctx, **arguments)
|
|
411
|
+
except ConfigurationError:
|
|
412
|
+
raise
|
|
413
|
+
except Exception as exc: # noqa: BLE001 - see transcript.record_results
|
|
414
|
+
return exc
|