agentropic 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {agentropic-0.4.0 → agentropic-0.4.2}/PKG-INFO +1 -1
- {agentropic-0.4.0 → agentropic-0.4.2}/examples/hitl_agent.py +1 -1
- {agentropic-0.4.0 → agentropic-0.4.2}/pyproject.toml +1 -1
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/agent.py +30 -8
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/hitl.py +40 -2
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/tracing.py +9 -1
- {agentropic-0.4.0 → agentropic-0.4.2}/tests/test_hitl.py +43 -0
- agentropic-0.4.0/tests/test_agent.py +0 -40
- agentropic-0.4.0/tests/test_streaming.py +0 -124
- {agentropic-0.4.0 → agentropic-0.4.2}/.gitignore +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/LICENSE +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/README.md +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/examples/basic_agent.py +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/__init__.py +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/llm.py +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/mcp/__init__.py +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/mcp/client.py +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/memory.py +0 -0
- {agentropic-0.4.0 → agentropic-0.4.2}/src/agentropic/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentropic
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A minimal, hackable agentic framework: LLM calls via litellm, @tool-decorated functions, and MCP server support.
|
|
5
5
|
Author-email: Vishal Tiwari <vishaltiwari.up2019@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentropic"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "A minimal, hackable agentic framework: LLM calls via litellm, @tool-decorated functions, and MCP server support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -38,12 +38,14 @@ from contextlib import AsyncExitStack
|
|
|
38
38
|
from dataclasses import asdict, dataclass, field
|
|
39
39
|
|
|
40
40
|
from .hitl import (
|
|
41
|
+
APPROVAL_SYSTEM_NOTE,
|
|
41
42
|
CANCELLED_MESSAGE,
|
|
42
|
-
DEFAULT_DENIAL_MESSAGE,
|
|
43
43
|
ApprovalDecision,
|
|
44
44
|
ApprovalHandler,
|
|
45
45
|
ApprovalRequest,
|
|
46
46
|
RunState,
|
|
47
|
+
denial_message,
|
|
48
|
+
denial_reminder,
|
|
47
49
|
evaluate_policy,
|
|
48
50
|
)
|
|
49
51
|
from .llm import LLMClient, LLMResponse
|
|
@@ -197,6 +199,7 @@ class _RunCtx:
|
|
|
197
199
|
decisions: dict[str, ApprovalDecision] = field(default_factory=dict)
|
|
198
200
|
sticky: dict[str, bool] = field(default_factory=dict)
|
|
199
201
|
pending: list[ApprovalRequest] = field(default_factory=list)
|
|
202
|
+
denied_notes: list[str] = field(default_factory=list) # "name(args)" of calls a human denied
|
|
200
203
|
run_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
201
204
|
|
|
202
205
|
|
|
@@ -303,8 +306,14 @@ class Agent:
|
|
|
303
306
|
|
|
304
307
|
# -- message building --------------------------------------------------
|
|
305
308
|
|
|
309
|
+
def _uses_approvals(self) -> bool:
|
|
310
|
+
return any(tl.requires_approval for tl in self.registry.list())
|
|
311
|
+
|
|
306
312
|
def _build_messages(self, user_input: str) -> list[dict]:
|
|
307
|
-
|
|
313
|
+
system = self.instructions
|
|
314
|
+
if self._uses_approvals():
|
|
315
|
+
system = f"{system}\n\n{APPROVAL_SYSTEM_NOTE}"
|
|
316
|
+
messages = [{"role": "system", "content": system}]
|
|
308
317
|
messages.extend(self.memory.get())
|
|
309
318
|
messages.append({"role": "user", "content": user_input})
|
|
310
319
|
return messages
|
|
@@ -421,10 +430,8 @@ class Agent:
|
|
|
421
430
|
edited=decision.override_args is not None, auto=auto,
|
|
422
431
|
)
|
|
423
432
|
if not decision.approved:
|
|
424
|
-
content = (
|
|
425
|
-
|
|
426
|
-
if decision.message else DEFAULT_DENIAL_MESSAGE
|
|
427
|
-
)
|
|
433
|
+
content = denial_message(name, decision.message)
|
|
434
|
+
ctx.denied_notes.append(f"{name}({json.dumps(args, default=str)})")
|
|
428
435
|
record = ToolCallRecord(
|
|
429
436
|
id=call_id, name=name, arguments=args, result=content,
|
|
430
437
|
error=True, iteration=iteration, status="denied",
|
|
@@ -443,6 +450,10 @@ class Agent:
|
|
|
443
450
|
tool_call=ToolCallRecord(id=call_id, name=name, arguments=args, iteration=iteration),
|
|
444
451
|
)
|
|
445
452
|
tool_msg, record = await self._execute_tool(call_id, name, args, iteration)
|
|
453
|
+
if decision is not None and decision.approved and not record.error:
|
|
454
|
+
# Tell the model approval was granted, so it doesn't report
|
|
455
|
+
# the action as "still needing approval".
|
|
456
|
+
tool_msg["content"] = f"[Approved by the user and executed] {tool_msg['content']}"
|
|
446
457
|
self._record_tool_result(ctx, tool_msg, record)
|
|
447
458
|
if stream:
|
|
448
459
|
yield StreamEvent(type="tool_result", iteration=iteration, tool_call=record)
|
|
@@ -560,9 +571,19 @@ class Agent:
|
|
|
560
571
|
for iteration in range(first_iteration, self.max_iterations + 1):
|
|
561
572
|
self.tracer.iteration_start(iteration, self.max_iterations)
|
|
562
573
|
llm_started = time.monotonic()
|
|
574
|
+
# If a human denied any call in this run, put an explicit
|
|
575
|
+
# reminder right next to where the model writes its answer.
|
|
576
|
+
# Small models otherwise ignore the DENIED tool result and
|
|
577
|
+
# summarise the original request as if everything ran.
|
|
578
|
+
# Transient: sent with this request only, never stored.
|
|
579
|
+
request_messages = ctx.messages
|
|
580
|
+
if ctx.denied_notes:
|
|
581
|
+
request_messages = ctx.messages + [
|
|
582
|
+
{"role": "user", "content": denial_reminder(ctx.denied_notes)}
|
|
583
|
+
]
|
|
563
584
|
if stream:
|
|
564
585
|
response = None
|
|
565
|
-
async for item in self.llm.astream_response(
|
|
586
|
+
async for item in self.llm.astream_response(request_messages, tools=tools_schema):
|
|
566
587
|
if isinstance(item, str):
|
|
567
588
|
self.tracer.stream_text(item)
|
|
568
589
|
yield StreamEvent(type="text", iteration=iteration, text=item)
|
|
@@ -570,7 +591,7 @@ class Agent:
|
|
|
570
591
|
response = item
|
|
571
592
|
assert response is not None
|
|
572
593
|
else:
|
|
573
|
-
response = await self.llm.acomplete(
|
|
594
|
+
response = await self.llm.acomplete(request_messages, tools=tools_schema)
|
|
574
595
|
ctx.raw_responses.append(response)
|
|
575
596
|
self.tracer.llm_response(
|
|
576
597
|
response.content or "",
|
|
@@ -613,6 +634,7 @@ class Agent:
|
|
|
613
634
|
self.tracer.final_answer(
|
|
614
635
|
result.output, result.iterations, len(result.tool_calls),
|
|
615
636
|
result.duration_seconds, result.usage, streamed=stream,
|
|
637
|
+
denied=[tc.name for tc in result.tool_calls if tc.status == "denied"],
|
|
616
638
|
)
|
|
617
639
|
yield StreamEvent(type="done", iteration=iteration, result=result)
|
|
618
640
|
return
|
|
@@ -39,8 +39,46 @@ STATE_VERSION = 1
|
|
|
39
39
|
# (sync or async). Used on @tool(requires_approval=...).
|
|
40
40
|
ApprovalPolicy = t.Union[bool, t.Callable[[dict], t.Union[bool, t.Awaitable[bool]]]]
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
CANCELLED_MESSAGE = (
|
|
43
|
+
"CANCELLED: this tool call was NOT executed because the user moved on "
|
|
44
|
+
"without approving it. The action did not happen."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Added to the system prompt when any tool needs approval, so the model
|
|
48
|
+
# knows how to treat denials instead of reporting success.
|
|
49
|
+
APPROVAL_SYSTEM_NOTE = (
|
|
50
|
+
"Some tools require human approval before they run. If a tool result "
|
|
51
|
+
"starts with 'DENIED' or 'CANCELLED', that action was NOT performed: never "
|
|
52
|
+
"say or imply it was done. Tell the user clearly which actions were not "
|
|
53
|
+
"carried out and why, and do not retry the same call unless the user asks."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def denial_message(tool_name: str, reason: t.Optional[str] = None) -> str:
|
|
58
|
+
"""The tool result the model sees when a human rejects a call. Worded
|
|
59
|
+
bluntly on purpose: softer phrasing gets ignored by smaller models,
|
|
60
|
+
which then claim the action succeeded."""
|
|
61
|
+
msg = (
|
|
62
|
+
f"DENIED: the user rejected this call to '{tool_name}'. "
|
|
63
|
+
f"The action was NOT performed and nothing was changed."
|
|
64
|
+
)
|
|
65
|
+
if reason:
|
|
66
|
+
msg += f" Reason given by the user: {reason}"
|
|
67
|
+
return msg
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def denial_reminder(denied_calls: list[str]) -> str:
|
|
71
|
+
"""Transient note sent just before the model writes its next reply, so
|
|
72
|
+
it can't overlook denials. Not stored in memory or RunState."""
|
|
73
|
+
items = "\n".join(f"- {c}" for c in denied_calls)
|
|
74
|
+
return (
|
|
75
|
+
"[Automated notice from the agent runtime, not written by the user]\n"
|
|
76
|
+
"The user DENIED the following tool calls. They were NOT executed and "
|
|
77
|
+
"had NO effect:\n"
|
|
78
|
+
f"{items}\n"
|
|
79
|
+
"In your reply, state plainly that these actions were not performed. "
|
|
80
|
+
"Do not say or imply they were done, and do not retry them."
|
|
81
|
+
)
|
|
44
82
|
|
|
45
83
|
|
|
46
84
|
@dataclass
|
|
@@ -323,14 +323,22 @@ class AgentTracer:
|
|
|
323
323
|
duration: float,
|
|
324
324
|
usage: dict,
|
|
325
325
|
streamed: bool = False,
|
|
326
|
+
denied: t.Optional[list] = None,
|
|
326
327
|
) -> None:
|
|
328
|
+
"""`denied` = names of tool calls a human rejected during this run."""
|
|
329
|
+
executed = tool_call_count - len(denied or [])
|
|
327
330
|
stats = (
|
|
328
331
|
f"{self._c(C.DIM)}⏱ {duration:.2f}s 🔁 {iterations} iteration(s) "
|
|
329
|
-
f"🔧 {
|
|
332
|
+
f"🔧 {executed} tool call(s) run"
|
|
330
333
|
)
|
|
331
334
|
if usage and usage.get("total_tokens"):
|
|
332
335
|
stats += f" 🎫 {usage['total_tokens']} tokens"
|
|
333
336
|
stats += self._c(C.RESET)
|
|
337
|
+
if denied:
|
|
338
|
+
stats += (
|
|
339
|
+
f"\n{self._c(C.RED)}🚫 {len(denied)} denied (not executed): "
|
|
340
|
+
f"{', '.join(denied)}{self._c(C.RESET)}"
|
|
341
|
+
)
|
|
334
342
|
self._print()
|
|
335
343
|
# When streamed, the answer was already printed live above.
|
|
336
344
|
lines = [stats] if streamed else [output, "", stats]
|
|
@@ -125,6 +125,49 @@ async def test_handler_denies_with_message(monkeypatch):
|
|
|
125
125
|
assert "too risky" in tool_msgs[0]["content"]
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_denial_is_explicit_and_system_prompt_explains_it(monkeypatch):
|
|
130
|
+
_script(monkeypatch, [DELETE, DONE])
|
|
131
|
+
result = await _agent(approval_handler=lambda req: False).arun("delete it")
|
|
132
|
+
tool_msg = next(m for m in result.messages if m["role"] == "tool")
|
|
133
|
+
assert tool_msg["content"].startswith("DENIED")
|
|
134
|
+
assert "NOT performed" in tool_msg["content"]
|
|
135
|
+
assert "DENIED" in result.messages[0]["content"] # system note added
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_denial_reminder_sent_to_model_but_not_stored(monkeypatch):
|
|
140
|
+
sent = []
|
|
141
|
+
turns = [DELETE, DONE]
|
|
142
|
+
|
|
143
|
+
async def acompletion(**kwargs):
|
|
144
|
+
sent.append(list(kwargs["messages"]))
|
|
145
|
+
kind, payload = turns[len(sent) - 1]
|
|
146
|
+
if kind == "tools":
|
|
147
|
+
return _full(tool_calls=[_tc(*x) for x in payload], finish="tool_calls")
|
|
148
|
+
return _full(content=payload)
|
|
149
|
+
|
|
150
|
+
monkeypatch.setattr(llm_mod.litellm, "acompletion", acompletion)
|
|
151
|
+
agent = _agent(approval_handler=lambda req: False)
|
|
152
|
+
result = await agent.arun("delete it")
|
|
153
|
+
|
|
154
|
+
last_request = sent[-1][-1]
|
|
155
|
+
assert last_request["role"] == "user"
|
|
156
|
+
assert "NOT executed" in last_request["content"]
|
|
157
|
+
assert "delete_file" in last_request["content"]
|
|
158
|
+
# transient: not in the run's messages or the agent's memory
|
|
159
|
+
assert all("Automated notice" not in str(m.get("content")) for m in result.messages)
|
|
160
|
+
assert all("Automated notice" not in str(m.get("content")) for m in agent.memory.get())
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_approved_result_is_tagged(monkeypatch):
|
|
165
|
+
_script(monkeypatch, [DELETE, DONE])
|
|
166
|
+
result = await _agent(approval_handler=lambda req: True).arun("delete it")
|
|
167
|
+
tool_msg = next(m for m in result.messages if m["role"] == "tool")
|
|
168
|
+
assert tool_msg["content"].startswith("[Approved by the user and executed]")
|
|
169
|
+
|
|
170
|
+
|
|
128
171
|
@pytest.mark.asyncio
|
|
129
172
|
async def test_async_handler_with_override_args(monkeypatch):
|
|
130
173
|
_script(monkeypatch, [DELETE, DONE])
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from agentropic.tools import tool, ToolRegistry, Tool
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_tool_decorator_builds_schema():
|
|
7
|
-
@tool()
|
|
8
|
-
def greet(name: str, excited: bool = False) -> str:
|
|
9
|
-
"""Say hello to someone."""
|
|
10
|
-
return f"Hello {name}!" + ("!!!" if excited else "")
|
|
11
|
-
|
|
12
|
-
built: Tool = greet._tool
|
|
13
|
-
assert built.name == "greet"
|
|
14
|
-
assert "name" in built.parameters["properties"]
|
|
15
|
-
assert "name" in built.parameters["required"]
|
|
16
|
-
assert "excited" not in built.parameters["required"]
|
|
17
|
-
assert built.call(name="Ada") == "Hello Ada!"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_registry_openai_schema():
|
|
21
|
-
@tool()
|
|
22
|
-
def noop() -> str:
|
|
23
|
-
"""Does nothing."""
|
|
24
|
-
return "ok"
|
|
25
|
-
|
|
26
|
-
registry = ToolRegistry([noop])
|
|
27
|
-
schema = registry.to_openai_tools()
|
|
28
|
-
assert schema[0]["function"]["name"] == "noop"
|
|
29
|
-
assert registry.get("noop") is not None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.mark.asyncio
|
|
33
|
-
async def test_async_tool_call():
|
|
34
|
-
@tool()
|
|
35
|
-
async def slow_add(a: int, b: int) -> int:
|
|
36
|
-
"""Add two numbers asynchronously."""
|
|
37
|
-
return a + b
|
|
38
|
-
|
|
39
|
-
result = await slow_add._tool.acall(a=2, b=3)
|
|
40
|
-
assert result == 5
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"""Streaming + tool-call loop tests using a fake LLM (no API key needed)."""
|
|
2
|
-
import types
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from agentropic import Agent, StreamEvent, tool
|
|
7
|
-
from agentropic import llm as llm_mod
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# -- fake litellm ---------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
def _ns(**kw):
|
|
13
|
-
return types.SimpleNamespace(**kw)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _tool_call(id_, name, args):
|
|
17
|
-
return _ns(id=id_, function=_ns(name=name, arguments=args))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _full_response(content=None, tool_calls=None, finish="stop"):
|
|
21
|
-
return _ns(
|
|
22
|
-
choices=[_ns(message=_ns(content=content, tool_calls=tool_calls), finish_reason=finish)],
|
|
23
|
-
usage=_ns(prompt_tokens=1, completion_tokens=1, total_tokens=2),
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class _FakeStream:
|
|
28
|
-
def __init__(self, chunks):
|
|
29
|
-
self._chunks = list(chunks)
|
|
30
|
-
|
|
31
|
-
def __aiter__(self):
|
|
32
|
-
return self
|
|
33
|
-
|
|
34
|
-
async def __anext__(self):
|
|
35
|
-
if not self._chunks:
|
|
36
|
-
raise StopAsyncIteration
|
|
37
|
-
return self._chunks.pop(0)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _install_fake_llm(monkeypatch):
|
|
41
|
-
"""Turn 1: model calls get_weather. Turn 2: model answers in 3 tokens."""
|
|
42
|
-
calls = {"n": 0}
|
|
43
|
-
|
|
44
|
-
async def acompletion(**kwargs):
|
|
45
|
-
calls["n"] += 1
|
|
46
|
-
first = calls["n"] == 1
|
|
47
|
-
if kwargs.get("stream"):
|
|
48
|
-
if first:
|
|
49
|
-
chunk = _ns(choices=[_ns(delta=_ns(content=None))], _final=_full_response(
|
|
50
|
-
tool_calls=[_tool_call("c1", "get_weather", '{"city": "Tokyo"}')], finish="tool_calls"))
|
|
51
|
-
return _FakeStream([chunk])
|
|
52
|
-
tokens = ["It's ", "sunny ", "in Tokyo."]
|
|
53
|
-
chunks = [_ns(choices=[_ns(delta=_ns(content=tok))]) for tok in tokens]
|
|
54
|
-
chunks[-1]._final = _full_response(content="".join(tokens))
|
|
55
|
-
return _FakeStream(chunks)
|
|
56
|
-
if first:
|
|
57
|
-
return _full_response(tool_calls=[_tool_call("c1", "get_weather", '{"city": "Tokyo"}')], finish="tool_calls")
|
|
58
|
-
return _full_response(content="It's sunny in Tokyo.")
|
|
59
|
-
|
|
60
|
-
def stream_chunk_builder(chunks, messages=None):
|
|
61
|
-
return chunks[-1]._final
|
|
62
|
-
|
|
63
|
-
monkeypatch.setattr(llm_mod.litellm, "acompletion", acompletion)
|
|
64
|
-
monkeypatch.setattr(llm_mod.litellm, "stream_chunk_builder", stream_chunk_builder)
|
|
65
|
-
return calls
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@tool()
|
|
69
|
-
def get_weather(city: str) -> str:
|
|
70
|
-
"""Get the weather."""
|
|
71
|
-
return f"sunny in {city}"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _agent():
|
|
75
|
-
return Agent(name="t", model="fake/model", tools=[get_weather])
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# -- tests ---------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
@pytest.mark.asyncio
|
|
81
|
-
async def test_arun_non_streaming_with_tools(monkeypatch):
|
|
82
|
-
_install_fake_llm(monkeypatch)
|
|
83
|
-
result = await _agent().arun("weather?")
|
|
84
|
-
assert result == "It's sunny in Tokyo."
|
|
85
|
-
assert result.tool_calls[0].name == "get_weather"
|
|
86
|
-
assert result.tool_calls[0].result == "sunny in Tokyo"
|
|
87
|
-
assert result.iterations == 2
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@pytest.mark.asyncio
|
|
91
|
-
async def test_arun_stream_true_yields_events_and_runs_tools(monkeypatch):
|
|
92
|
-
_install_fake_llm(monkeypatch)
|
|
93
|
-
events: list[StreamEvent] = []
|
|
94
|
-
async for ev in await _agent().arun("weather?", stream=True):
|
|
95
|
-
events.append(ev)
|
|
96
|
-
|
|
97
|
-
types_seen = [e.type for e in events]
|
|
98
|
-
assert types_seen == ["tool_call", "tool_result", "text", "text", "text", "done"]
|
|
99
|
-
assert events[0].tool_call.arguments == {"city": "Tokyo"}
|
|
100
|
-
assert events[1].tool_call.result == "sunny in Tokyo"
|
|
101
|
-
assert "".join(e.text for e in events if e.type == "text") == "It's sunny in Tokyo."
|
|
102
|
-
assert events[-1].result.output == "It's sunny in Tokyo."
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@pytest.mark.asyncio
|
|
106
|
-
async def test_astream_text_only(monkeypatch):
|
|
107
|
-
_install_fake_llm(monkeypatch)
|
|
108
|
-
text = "".join([tok async for tok in _agent().astream("weather?")])
|
|
109
|
-
assert text == "It's sunny in Tokyo."
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def test_sync_run_stream(monkeypatch):
|
|
113
|
-
_install_fake_llm(monkeypatch)
|
|
114
|
-
events = list(_agent().run("weather?", stream=True))
|
|
115
|
-
assert events[-1].type == "done"
|
|
116
|
-
assert events[-1].result.output == "It's sunny in Tokyo."
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_result_is_json_serializable(monkeypatch):
|
|
120
|
-
import json
|
|
121
|
-
|
|
122
|
-
_install_fake_llm(monkeypatch)
|
|
123
|
-
result = _agent().run("weather?")
|
|
124
|
-
json.dumps(result.to_dict()) # tool_calls are plain dicts now
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|