quick-agent 0.1.1__py3-none-any.whl → 0.1.3__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.
- quick_agent/__init__.py +4 -1
- quick_agent/agent_call_tool.py +22 -5
- quick_agent/agent_registry.py +7 -27
- quick_agent/agent_tools.py +3 -2
- quick_agent/cli.py +19 -5
- quick_agent/directory_permissions.py +7 -3
- quick_agent/input_adaptors.py +30 -0
- quick_agent/llms.txt +239 -0
- quick_agent/models/agent_spec.py +3 -0
- quick_agent/models/loaded_agent_file.py +136 -1
- quick_agent/models/output_spec.py +1 -1
- quick_agent/orchestrator.py +15 -8
- quick_agent/prompting.py +34 -16
- quick_agent/py.typed +1 -0
- quick_agent/quick_agent.py +171 -155
- quick_agent/schemas/outputs.py +6 -0
- quick_agent-0.1.3.data/data/quick_agent/agents/business-extract-structured.md +49 -0
- quick_agent-0.1.3.data/data/quick_agent/agents/business-extract.md +42 -0
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/function-spec-validator.md +1 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validate-eval-list.md +1 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validator-contains.md +8 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/template.md +12 -1
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/METADATA +21 -4
- quick_agent-0.1.3.dist-info/RECORD +52 -0
- tests/test_agent.py +273 -9
- tests/test_directory_permissions.py +10 -0
- tests/test_httpx_tools.py +295 -0
- tests/test_input_adaptors.py +31 -0
- tests/test_integration.py +134 -1
- tests/test_orchestrator.py +525 -111
- quick_agent-0.1.1.dist-info/RECORD +0 -45
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/WHEEL +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/entry_points.txt +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/top_level.txt +0 -0
tests/test_orchestrator.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import types
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, cast
|
|
4
|
+
from typing import Any, Literal, cast
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from pydantic import BaseModel
|
|
@@ -11,6 +11,7 @@ from pydantic_ai.toolsets import FunctionToolset
|
|
|
11
11
|
|
|
12
12
|
from quick_agent import quick_agent as qa_module
|
|
13
13
|
from quick_agent import agent_tools as tools_module
|
|
14
|
+
from quick_agent import input_adaptors as input_adaptors_module
|
|
14
15
|
from quick_agent.agent_call_tool import AgentCallTool
|
|
15
16
|
from quick_agent.agent_registry import AgentRegistry
|
|
16
17
|
from quick_agent.agent_tools import AgentTools
|
|
@@ -26,6 +27,7 @@ from quick_agent.orchestrator import Orchestrator
|
|
|
26
27
|
from quick_agent.quick_agent import QuickAgent
|
|
27
28
|
from quick_agent.quick_agent import build_model
|
|
28
29
|
from quick_agent.quick_agent import resolve_schema
|
|
30
|
+
from quick_agent.prompting import make_user_prompt
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class DummyProvider:
|
|
@@ -76,7 +78,8 @@ class FakeAgent:
|
|
|
76
78
|
def __init__(
|
|
77
79
|
self,
|
|
78
80
|
model: Any,
|
|
79
|
-
instructions: str,
|
|
81
|
+
instructions: str | None,
|
|
82
|
+
system_prompt: str | list[str],
|
|
80
83
|
toolsets: list[Any],
|
|
81
84
|
output_type: Any,
|
|
82
85
|
model_settings: Any | None = None,
|
|
@@ -84,6 +87,7 @@ class FakeAgent:
|
|
|
84
87
|
FakeAgent.last_init = {
|
|
85
88
|
"model": model,
|
|
86
89
|
"instructions": instructions,
|
|
90
|
+
"system_prompt": system_prompt,
|
|
87
91
|
"toolsets": toolsets,
|
|
88
92
|
"output_type": output_type,
|
|
89
93
|
"model_settings": model_settings,
|
|
@@ -157,10 +161,10 @@ class RecordingQuickAgent(QuickAgent):
|
|
|
157
161
|
|
|
158
162
|
class HandoffQuickAgent(QuickAgent):
|
|
159
163
|
def __init__(self) -> None:
|
|
160
|
-
self.calls: list[tuple[str, Path]] = []
|
|
164
|
+
self.calls: list[tuple[str, input_adaptors_module.InputAdaptor | Path]] = []
|
|
161
165
|
|
|
162
|
-
async def _run_nested_agent(self, agent_id: str,
|
|
163
|
-
self.calls.append((agent_id,
|
|
166
|
+
async def _run_nested_agent(self, agent_id: str, input_data: input_adaptors_module.InputAdaptor | Path) -> str:
|
|
167
|
+
self.calls.append((agent_id, input_data))
|
|
164
168
|
return "ok"
|
|
165
169
|
|
|
166
170
|
|
|
@@ -187,7 +191,12 @@ def _make_loaded_with_chain(
|
|
|
187
191
|
output=output or OutputSpec(file="out/result.json"),
|
|
188
192
|
handoff=handoff or HandoffSpec(),
|
|
189
193
|
)
|
|
190
|
-
return LoadedAgentFile(
|
|
194
|
+
return LoadedAgentFile.from_parts(
|
|
195
|
+
spec=spec,
|
|
196
|
+
instructions="system",
|
|
197
|
+
system_prompt="",
|
|
198
|
+
step_prompts={"step:one": "do thing"},
|
|
199
|
+
)
|
|
191
200
|
|
|
192
201
|
|
|
193
202
|
def _permissions(tmp_path: Path | None = None) -> DirectoryPermissions:
|
|
@@ -236,7 +245,7 @@ def test_resolve_schema_valid_missing_and_invalid() -> None:
|
|
|
236
245
|
chain=[ChainStepSpec(id="s1", kind="text", prompt_section="step:one")],
|
|
237
246
|
schemas={"Good": "schemas.orch:GoodSchema", "Bad": "schemas.orch:NotSchema"},
|
|
238
247
|
)
|
|
239
|
-
loaded = LoadedAgentFile(spec=spec,
|
|
248
|
+
loaded = LoadedAgentFile.from_parts(spec=spec, instructions="", system_prompt="", step_prompts={})
|
|
240
249
|
|
|
241
250
|
try:
|
|
242
251
|
assert resolve_schema(loaded, "Good") is GoodSchema
|
|
@@ -306,12 +315,41 @@ def test_maybe_inject_agent_call_tool_skips_when_missing() -> None:
|
|
|
306
315
|
assert toolset.add_calls == []
|
|
307
316
|
|
|
308
317
|
|
|
318
|
+
@pytest.mark.anyio
|
|
319
|
+
async def test_agent_call_tool_accepts_input_text() -> None:
|
|
320
|
+
recorder = AsyncCallRecorder(return_value="ok")
|
|
321
|
+
tool = AgentCallTool(recorder, "run/input.json")
|
|
322
|
+
|
|
323
|
+
result = await tool(agent="child", input_text="hello")
|
|
324
|
+
|
|
325
|
+
assert result == {"text": "ok"}
|
|
326
|
+
assert len(recorder.calls) == 1
|
|
327
|
+
args = recorder.calls[0]["args"]
|
|
328
|
+
assert args[0] == "child"
|
|
329
|
+
assert isinstance(args[1], input_adaptors_module.TextInput)
|
|
330
|
+
run_input = args[1].load()
|
|
331
|
+
assert run_input.kind == "text"
|
|
332
|
+
assert run_input.text == "hello"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@pytest.mark.anyio
|
|
336
|
+
async def test_agent_call_tool_rejects_missing_or_duplicate_input() -> None:
|
|
337
|
+
recorder = AsyncCallRecorder(return_value="ok")
|
|
338
|
+
tool = AgentCallTool(recorder, "run/input.json")
|
|
339
|
+
|
|
340
|
+
with pytest.raises(ValueError):
|
|
341
|
+
await tool(agent="child")
|
|
342
|
+
with pytest.raises(ValueError):
|
|
343
|
+
await tool(agent="child", input_file="a.txt", input_text="hi")
|
|
344
|
+
|
|
345
|
+
|
|
309
346
|
def test_init_state_contains_agent_id_and_steps() -> None:
|
|
310
347
|
qa = object.__new__(QuickAgent)
|
|
311
348
|
|
|
312
|
-
|
|
349
|
+
qa._agent_id = "agent-1"
|
|
350
|
+
state = qa._init_state()
|
|
313
351
|
|
|
314
|
-
assert state == {"agent_id": "agent-1", "steps": {}}
|
|
352
|
+
assert state == {"agent_id": "agent-1", "steps": {}, "final_output": None}
|
|
315
353
|
|
|
316
354
|
|
|
317
355
|
def test_build_model_settings_openai_compatible() -> None:
|
|
@@ -348,14 +386,11 @@ def test_build_model_settings_other_provider() -> None:
|
|
|
348
386
|
def test_build_structured_model_settings_non_openai_passthrough() -> None:
|
|
349
387
|
qa = object.__new__(QuickAgent)
|
|
350
388
|
schema = ExampleSchema
|
|
351
|
-
model = cast(OpenAIChatModel, DummyOpenAIModel("http://localhost"))
|
|
352
389
|
settings: ModelSettings = {"extra_body": {"format": "json"}}
|
|
390
|
+
qa.model = cast(OpenAIChatModel, DummyOpenAIModel("http://localhost"))
|
|
391
|
+
qa.model_settings_json = settings
|
|
353
392
|
|
|
354
|
-
result = qa._build_structured_model_settings(
|
|
355
|
-
model=model,
|
|
356
|
-
model_settings_json=settings,
|
|
357
|
-
schema_cls=schema,
|
|
358
|
-
)
|
|
393
|
+
result = qa._build_structured_model_settings(schema_cls=schema)
|
|
359
394
|
|
|
360
395
|
assert result == settings
|
|
361
396
|
|
|
@@ -363,13 +398,10 @@ def test_build_structured_model_settings_non_openai_passthrough() -> None:
|
|
|
363
398
|
def test_build_structured_model_settings_openai_injects_schema() -> None:
|
|
364
399
|
qa = object.__new__(QuickAgent)
|
|
365
400
|
schema = ExampleSchema
|
|
366
|
-
model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
|
|
401
|
+
qa.model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
|
|
402
|
+
qa.model_settings_json = None
|
|
367
403
|
|
|
368
|
-
result = qa._build_structured_model_settings(
|
|
369
|
-
model=model,
|
|
370
|
-
model_settings_json=None,
|
|
371
|
-
schema_cls=schema,
|
|
372
|
-
)
|
|
404
|
+
result = qa._build_structured_model_settings(schema_cls=schema)
|
|
373
405
|
|
|
374
406
|
assert result is not None
|
|
375
407
|
extra_body_obj = result.get("extra_body")
|
|
@@ -384,16 +416,6 @@ def test_build_structured_model_settings_openai_injects_schema() -> None:
|
|
|
384
416
|
assert json_schema_obj["strict"] is True
|
|
385
417
|
|
|
386
418
|
|
|
387
|
-
def test_build_user_prompt_raises_for_missing_section() -> None:
|
|
388
|
-
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:missing")
|
|
389
|
-
loaded = LoadedAgentFile(spec=_make_loaded_with_chain([step]).spec, body="body", step_prompts={})
|
|
390
|
-
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
391
|
-
|
|
392
|
-
qa = object.__new__(QuickAgent)
|
|
393
|
-
with pytest.raises(KeyError):
|
|
394
|
-
qa._build_user_prompt(step=step, loaded=loaded, run_input=run_input, state={"steps": {}})
|
|
395
|
-
|
|
396
|
-
|
|
397
419
|
def test_build_user_prompt_uses_prompting(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
398
420
|
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
399
421
|
loaded = _make_loaded_with_chain([step])
|
|
@@ -402,10 +424,41 @@ def test_build_user_prompt_uses_prompting(monkeypatch: pytest.MonkeyPatch) -> No
|
|
|
402
424
|
monkeypatch.setattr(qa_module, "make_user_prompt", recorder)
|
|
403
425
|
|
|
404
426
|
qa = object.__new__(QuickAgent)
|
|
405
|
-
|
|
427
|
+
qa.loaded = loaded
|
|
428
|
+
qa.run_input = run_input
|
|
429
|
+
qa.state = {"agent_id": "agent-1", "steps": {}, "final_output": None}
|
|
430
|
+
result = qa._build_user_prompt()
|
|
406
431
|
|
|
407
432
|
assert result == "prompt"
|
|
408
|
-
assert recorder.calls == [
|
|
433
|
+
assert recorder.calls == [
|
|
434
|
+
(
|
|
435
|
+
(run_input, {"agent_id": "agent-1", "steps": {}, "final_output": None}),
|
|
436
|
+
{},
|
|
437
|
+
)
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@pytest.mark.anyio
|
|
442
|
+
async def test_run_text_step_raises_for_missing_section(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
443
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
444
|
+
|
|
445
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:missing")
|
|
446
|
+
loaded = _make_loaded_with_chain([step])
|
|
447
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
448
|
+
|
|
449
|
+
qa = object.__new__(QuickAgent)
|
|
450
|
+
qa.loaded = loaded
|
|
451
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
452
|
+
qa.model_settings_json = None
|
|
453
|
+
qa.toolset = RecordingToolset()
|
|
454
|
+
qa.tool_ids = []
|
|
455
|
+
qa.run_input = run_input
|
|
456
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
457
|
+
|
|
458
|
+
with pytest.raises(KeyError):
|
|
459
|
+
await qa._run_text_step(
|
|
460
|
+
step=step,
|
|
461
|
+
)
|
|
409
462
|
|
|
410
463
|
|
|
411
464
|
@pytest.mark.anyio
|
|
@@ -418,23 +471,51 @@ async def test_run_step_text_returns_output(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
418
471
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
419
472
|
|
|
420
473
|
qa = object.__new__(QuickAgent)
|
|
474
|
+
qa.loaded = loaded
|
|
475
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
476
|
+
qa.model_settings_json = None
|
|
477
|
+
qa.toolset = RecordingToolset()
|
|
478
|
+
qa.tool_ids = []
|
|
479
|
+
qa.run_input = run_input
|
|
480
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
421
481
|
output, final = await qa._run_step(
|
|
422
482
|
step=step,
|
|
423
|
-
loaded=loaded,
|
|
424
|
-
model=cast(OpenAIChatModel, object()),
|
|
425
|
-
model_settings_json=None,
|
|
426
|
-
toolset=RecordingToolset(),
|
|
427
|
-
run_input=run_input,
|
|
428
|
-
state={"agent_id": "a", "steps": {}},
|
|
429
483
|
)
|
|
430
484
|
|
|
431
485
|
assert output == "hello"
|
|
432
486
|
assert final == "hello"
|
|
433
487
|
assert FakeAgent.last_init is not None
|
|
434
|
-
assert FakeAgent.last_init["instructions"] == "
|
|
488
|
+
assert FakeAgent.last_init["instructions"] == "systemdo thing"
|
|
489
|
+
assert FakeAgent.last_init["system_prompt"] == []
|
|
435
490
|
assert FakeAgent.last_init["output_type"] is str
|
|
436
491
|
|
|
437
492
|
|
|
493
|
+
@pytest.mark.anyio
|
|
494
|
+
async def test_run_text_step_omits_tools_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
495
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
496
|
+
FakeAgent.next_output = "hello"
|
|
497
|
+
|
|
498
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
499
|
+
loaded = _make_loaded_with_chain([step])
|
|
500
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
501
|
+
|
|
502
|
+
qa = object.__new__(QuickAgent)
|
|
503
|
+
qa.loaded = loaded
|
|
504
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
505
|
+
qa.model_settings_json = None
|
|
506
|
+
qa.toolset = RecordingToolset()
|
|
507
|
+
qa.run_input = run_input
|
|
508
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
509
|
+
qa.tool_ids = []
|
|
510
|
+
|
|
511
|
+
await qa._run_text_step(
|
|
512
|
+
step=step,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
assert FakeAgent.last_init is not None
|
|
516
|
+
assert FakeAgent.last_init["toolsets"] == []
|
|
517
|
+
|
|
518
|
+
|
|
438
519
|
@pytest.mark.anyio
|
|
439
520
|
async def test_run_step_structured_parses_json_with_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
440
521
|
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
@@ -451,19 +532,25 @@ async def test_run_step_structured_parses_json_with_fallback(monkeypatch: pytest
|
|
|
451
532
|
chain=[step],
|
|
452
533
|
schemas={"Example": "schemas.struct:ExampleSchema"},
|
|
453
534
|
)
|
|
454
|
-
loaded = LoadedAgentFile(
|
|
535
|
+
loaded = LoadedAgentFile.from_parts(
|
|
536
|
+
spec=spec,
|
|
537
|
+
instructions="system",
|
|
538
|
+
system_prompt="",
|
|
539
|
+
step_prompts={"step:one": "do thing"},
|
|
540
|
+
)
|
|
455
541
|
run_input = RunInput(source_path="in.json", kind="json", text="{}", data={})
|
|
456
542
|
|
|
457
543
|
try:
|
|
458
544
|
qa = object.__new__(QuickAgent)
|
|
545
|
+
qa.loaded = loaded
|
|
546
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
547
|
+
qa.model_settings_json = {"extra_body": {"format": "json"}}
|
|
548
|
+
qa.toolset = RecordingToolset()
|
|
549
|
+
qa.tool_ids = []
|
|
550
|
+
qa.run_input = run_input
|
|
551
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
459
552
|
output, final = await qa._run_step(
|
|
460
553
|
step=step,
|
|
461
|
-
loaded=loaded,
|
|
462
|
-
model=cast(OpenAIChatModel, object()),
|
|
463
|
-
model_settings_json={"extra_body": {"format": "json"}},
|
|
464
|
-
toolset=RecordingToolset(),
|
|
465
|
-
run_input=run_input,
|
|
466
|
-
state={"agent_id": "a", "steps": {}},
|
|
467
554
|
)
|
|
468
555
|
finally:
|
|
469
556
|
sys.modules.pop("schemas.struct", None)
|
|
@@ -471,6 +558,8 @@ async def test_run_step_structured_parses_json_with_fallback(monkeypatch: pytest
|
|
|
471
558
|
assert output == {"x": 7}
|
|
472
559
|
assert isinstance(final, ExampleSchema)
|
|
473
560
|
assert final.x == 7
|
|
561
|
+
assert FakeAgent.last_init is not None
|
|
562
|
+
assert FakeAgent.last_init["output_type"] is ExampleSchema
|
|
474
563
|
|
|
475
564
|
|
|
476
565
|
@pytest.mark.anyio
|
|
@@ -481,15 +570,15 @@ async def test_run_step_unknown_kind_raises(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
481
570
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
482
571
|
|
|
483
572
|
qa = object.__new__(QuickAgent)
|
|
573
|
+
qa.loaded = loaded
|
|
574
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
575
|
+
qa.model_settings_json = None
|
|
576
|
+
qa.toolset = RecordingToolset()
|
|
577
|
+
qa.run_input = run_input
|
|
578
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
484
579
|
with pytest.raises(NotImplementedError):
|
|
485
580
|
await qa._run_step(
|
|
486
581
|
step=step,
|
|
487
|
-
loaded=loaded,
|
|
488
|
-
model=cast(OpenAIChatModel, object()),
|
|
489
|
-
model_settings_json=None,
|
|
490
|
-
toolset=RecordingToolset(),
|
|
491
|
-
run_input=run_input,
|
|
492
|
-
state={"agent_id": "a", "steps": {}},
|
|
493
582
|
)
|
|
494
583
|
|
|
495
584
|
|
|
@@ -502,15 +591,15 @@ async def test_run_text_step_uses_build_user_prompt(monkeypatch: pytest.MonkeyPa
|
|
|
502
591
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
503
592
|
|
|
504
593
|
qa = object.__new__(QuickAgent)
|
|
594
|
+
qa.loaded = loaded
|
|
595
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
596
|
+
qa.toolset = RecordingToolset()
|
|
597
|
+
qa.tool_ids = []
|
|
598
|
+
qa.run_input = run_input
|
|
505
599
|
monkeypatch.setattr(qa, "_build_user_prompt", SyncCallRecorder(return_value="prompt"))
|
|
506
600
|
|
|
507
601
|
output, final = await qa._run_text_step(
|
|
508
602
|
step=step,
|
|
509
|
-
loaded=loaded,
|
|
510
|
-
model=cast(OpenAIChatModel, object()),
|
|
511
|
-
toolset=RecordingToolset(),
|
|
512
|
-
run_input=run_input,
|
|
513
|
-
state={"agent_id": "a", "steps": {}},
|
|
514
603
|
)
|
|
515
604
|
|
|
516
605
|
assert output == "ok"
|
|
@@ -518,6 +607,129 @@ async def test_run_text_step_uses_build_user_prompt(monkeypatch: pytest.MonkeyPa
|
|
|
518
607
|
assert FakeAgent.last_prompt == "prompt"
|
|
519
608
|
|
|
520
609
|
|
|
610
|
+
@pytest.mark.anyio
|
|
611
|
+
async def test_run_text_step_no_instructions_or_system_prompt(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
612
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
613
|
+
FakeAgent.next_output = "ok"
|
|
614
|
+
|
|
615
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
616
|
+
spec = AgentSpec(
|
|
617
|
+
name="test",
|
|
618
|
+
model=ModelSpec(base_url="http://x", model_name="m"),
|
|
619
|
+
chain=[step],
|
|
620
|
+
output=OutputSpec(file=None),
|
|
621
|
+
)
|
|
622
|
+
loaded = LoadedAgentFile.from_parts(
|
|
623
|
+
spec=spec,
|
|
624
|
+
instructions="",
|
|
625
|
+
system_prompt="",
|
|
626
|
+
step_prompts={"step:one": "do thing"},
|
|
627
|
+
)
|
|
628
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
629
|
+
|
|
630
|
+
qa = object.__new__(QuickAgent)
|
|
631
|
+
qa.loaded = loaded
|
|
632
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
633
|
+
qa.model_settings_json = None
|
|
634
|
+
qa.toolset = RecordingToolset()
|
|
635
|
+
qa.tool_ids = []
|
|
636
|
+
qa.run_input = run_input
|
|
637
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
638
|
+
|
|
639
|
+
output, final = await qa._run_text_step(
|
|
640
|
+
step=step,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
assert output == "ok"
|
|
644
|
+
assert final == "ok"
|
|
645
|
+
assert FakeAgent.last_init is not None
|
|
646
|
+
assert FakeAgent.last_init["instructions"] == "do thing"
|
|
647
|
+
assert FakeAgent.last_init["system_prompt"] == []
|
|
648
|
+
assert FakeAgent.last_prompt == make_user_prompt(run_input, qa.state)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@pytest.mark.anyio
|
|
652
|
+
async def test_run_text_step_system_prompt_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
653
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
654
|
+
FakeAgent.next_output = "ok"
|
|
655
|
+
|
|
656
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
657
|
+
spec = AgentSpec(
|
|
658
|
+
name="test",
|
|
659
|
+
model=ModelSpec(base_url="http://x", model_name="m"),
|
|
660
|
+
chain=[step],
|
|
661
|
+
output=OutputSpec(file=None),
|
|
662
|
+
)
|
|
663
|
+
loaded = LoadedAgentFile.from_parts(
|
|
664
|
+
spec=spec,
|
|
665
|
+
instructions="",
|
|
666
|
+
system_prompt="You are concise.",
|
|
667
|
+
step_prompts={"step:one": "do thing"},
|
|
668
|
+
)
|
|
669
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
670
|
+
|
|
671
|
+
qa = object.__new__(QuickAgent)
|
|
672
|
+
qa.loaded = loaded
|
|
673
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
674
|
+
qa.model_settings_json = None
|
|
675
|
+
qa.toolset = RecordingToolset()
|
|
676
|
+
qa.tool_ids = []
|
|
677
|
+
qa.run_input = run_input
|
|
678
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
679
|
+
|
|
680
|
+
output, final = await qa._run_text_step(
|
|
681
|
+
step=step,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
assert output == "ok"
|
|
685
|
+
assert final == "ok"
|
|
686
|
+
assert FakeAgent.last_init is not None
|
|
687
|
+
assert FakeAgent.last_init["instructions"] == "do thing"
|
|
688
|
+
assert FakeAgent.last_init["system_prompt"] == "You are concise."
|
|
689
|
+
assert FakeAgent.last_prompt == make_user_prompt(run_input, qa.state)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@pytest.mark.anyio
|
|
693
|
+
async def test_run_text_step_instructions_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
694
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
695
|
+
FakeAgent.next_output = "ok"
|
|
696
|
+
|
|
697
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
698
|
+
spec = AgentSpec(
|
|
699
|
+
name="test",
|
|
700
|
+
model=ModelSpec(base_url="http://x", model_name="m"),
|
|
701
|
+
chain=[step],
|
|
702
|
+
output=OutputSpec(file=None),
|
|
703
|
+
)
|
|
704
|
+
loaded = LoadedAgentFile.from_parts(
|
|
705
|
+
spec=spec,
|
|
706
|
+
instructions="Use the tool.",
|
|
707
|
+
system_prompt="",
|
|
708
|
+
step_prompts={"step:one": "do thing"},
|
|
709
|
+
)
|
|
710
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
711
|
+
|
|
712
|
+
qa = object.__new__(QuickAgent)
|
|
713
|
+
qa.loaded = loaded
|
|
714
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
715
|
+
qa.model_settings_json = None
|
|
716
|
+
qa.toolset = RecordingToolset()
|
|
717
|
+
qa.tool_ids = []
|
|
718
|
+
qa.run_input = run_input
|
|
719
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
720
|
+
|
|
721
|
+
output, final = await qa._run_text_step(
|
|
722
|
+
step=step,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
assert output == "ok"
|
|
726
|
+
assert final == "ok"
|
|
727
|
+
assert FakeAgent.last_init is not None
|
|
728
|
+
assert FakeAgent.last_init["instructions"] == "Use the tool.do thing"
|
|
729
|
+
assert FakeAgent.last_init["system_prompt"] == []
|
|
730
|
+
assert FakeAgent.last_prompt == make_user_prompt(run_input, qa.state)
|
|
731
|
+
|
|
732
|
+
|
|
521
733
|
@pytest.mark.anyio
|
|
522
734
|
async def test_run_structured_step_missing_schema_raises() -> None:
|
|
523
735
|
step = ChainStepSpec(id="s1", kind="structured", prompt_section="step:one", output_schema=None)
|
|
@@ -525,15 +737,14 @@ async def test_run_structured_step_missing_schema_raises() -> None:
|
|
|
525
737
|
run_input = RunInput(source_path="in.json", kind="json", text="{}", data={})
|
|
526
738
|
|
|
527
739
|
qa = object.__new__(QuickAgent)
|
|
740
|
+
qa.loaded = loaded
|
|
741
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
742
|
+
qa.model_settings_json = None
|
|
743
|
+
qa.toolset = RecordingToolset()
|
|
744
|
+
qa.run_input = run_input
|
|
528
745
|
with pytest.raises(ValueError):
|
|
529
746
|
await qa._run_structured_step(
|
|
530
747
|
step=step,
|
|
531
|
-
loaded=loaded,
|
|
532
|
-
model=cast(OpenAIChatModel, object()),
|
|
533
|
-
model_settings_json=None,
|
|
534
|
-
toolset=RecordingToolset(),
|
|
535
|
-
run_input=run_input,
|
|
536
|
-
state={"agent_id": "a", "steps": {}},
|
|
537
748
|
)
|
|
538
749
|
|
|
539
750
|
|
|
@@ -553,19 +764,25 @@ async def test_run_structured_step_parses_json(monkeypatch: pytest.MonkeyPatch)
|
|
|
553
764
|
chain=[step],
|
|
554
765
|
schemas={"Example": "schemas.struct2:ExampleSchema"},
|
|
555
766
|
)
|
|
556
|
-
loaded = LoadedAgentFile(
|
|
767
|
+
loaded = LoadedAgentFile.from_parts(
|
|
768
|
+
spec=spec,
|
|
769
|
+
instructions="system",
|
|
770
|
+
system_prompt="",
|
|
771
|
+
step_prompts={"step:one": "do thing"},
|
|
772
|
+
)
|
|
557
773
|
run_input = RunInput(source_path="in.json", kind="json", text="{}", data={})
|
|
558
774
|
|
|
559
775
|
try:
|
|
560
776
|
qa = object.__new__(QuickAgent)
|
|
777
|
+
qa.loaded = loaded
|
|
778
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
779
|
+
qa.model_settings_json = None
|
|
780
|
+
qa.toolset = RecordingToolset()
|
|
781
|
+
qa.tool_ids = []
|
|
782
|
+
qa.run_input = run_input
|
|
783
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
561
784
|
output, final = await qa._run_structured_step(
|
|
562
785
|
step=step,
|
|
563
|
-
loaded=loaded,
|
|
564
|
-
model=cast(OpenAIChatModel, object()),
|
|
565
|
-
model_settings_json=None,
|
|
566
|
-
toolset=RecordingToolset(),
|
|
567
|
-
run_input=run_input,
|
|
568
|
-
state={"agent_id": "a", "steps": {}},
|
|
569
786
|
)
|
|
570
787
|
finally:
|
|
571
788
|
sys.modules.pop("schemas.struct2", None)
|
|
@@ -590,19 +807,25 @@ async def test_run_structured_step_adds_json_schema_for_openai(monkeypatch: pyte
|
|
|
590
807
|
chain=[step],
|
|
591
808
|
schemas={"Example": "schemas.struct3:ExampleSchema"},
|
|
592
809
|
)
|
|
593
|
-
loaded = LoadedAgentFile(
|
|
810
|
+
loaded = LoadedAgentFile.from_parts(
|
|
811
|
+
spec=spec,
|
|
812
|
+
instructions="system",
|
|
813
|
+
system_prompt="",
|
|
814
|
+
step_prompts={"step:one": "do thing"},
|
|
815
|
+
)
|
|
594
816
|
run_input = RunInput(source_path="in.json", kind="json", text="{}", data={})
|
|
595
817
|
|
|
596
818
|
try:
|
|
597
819
|
qa = object.__new__(QuickAgent)
|
|
820
|
+
qa.loaded = loaded
|
|
821
|
+
qa.model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
|
|
822
|
+
qa.model_settings_json = None
|
|
823
|
+
qa.toolset = RecordingToolset()
|
|
824
|
+
qa.tool_ids = []
|
|
825
|
+
qa.run_input = run_input
|
|
826
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
598
827
|
await qa._run_structured_step(
|
|
599
828
|
step=step,
|
|
600
|
-
loaded=loaded,
|
|
601
|
-
model=cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1")),
|
|
602
|
-
model_settings_json=None,
|
|
603
|
-
toolset=RecordingToolset(),
|
|
604
|
-
run_input=run_input,
|
|
605
|
-
state={"agent_id": "a", "steps": {}},
|
|
606
829
|
)
|
|
607
830
|
finally:
|
|
608
831
|
sys.modules.pop("schemas.struct3", None)
|
|
@@ -623,22 +846,95 @@ async def test_run_chain_updates_state_and_returns_last() -> None:
|
|
|
623
846
|
loaded = _make_loaded_with_chain([step1, step2])
|
|
624
847
|
|
|
625
848
|
qa = RecordingQuickAgent(outputs=[({"a": 1}, "first"), ("b", "second")])
|
|
626
|
-
|
|
849
|
+
qa.loaded = loaded
|
|
850
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
851
|
+
qa.model_settings_json = None
|
|
852
|
+
qa.toolset = RecordingToolset()
|
|
853
|
+
qa.run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
854
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
627
855
|
|
|
628
|
-
final = await qa._run_chain(
|
|
629
|
-
loaded=loaded,
|
|
630
|
-
model=cast(OpenAIChatModel, object()),
|
|
631
|
-
model_settings_json=None,
|
|
632
|
-
toolset=RecordingToolset(),
|
|
633
|
-
run_input=RunInput(source_path="in.txt", kind="text", text="hi", data=None),
|
|
634
|
-
state=state,
|
|
635
|
-
)
|
|
856
|
+
final = await qa._run_chain()
|
|
636
857
|
|
|
637
858
|
assert final == "second"
|
|
638
|
-
assert state["steps"] == {"s1": {"a": 1}, "s2": "b"}
|
|
859
|
+
assert qa.state["steps"] == {"s1": {"a": 1}, "s2": "b"}
|
|
860
|
+
assert qa.state["final_output"] == "b"
|
|
639
861
|
assert qa.calls == ["s1", "s2"]
|
|
640
862
|
|
|
641
863
|
|
|
864
|
+
@pytest.mark.anyio
|
|
865
|
+
async def test_run_chain_single_shot_system_prompt_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
866
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
867
|
+
FakeAgent.next_output = "hello"
|
|
868
|
+
|
|
869
|
+
spec = AgentSpec(
|
|
870
|
+
name="test",
|
|
871
|
+
model=ModelSpec(base_url="http://x", model_name="m"),
|
|
872
|
+
chain=[],
|
|
873
|
+
output=OutputSpec(file=None),
|
|
874
|
+
)
|
|
875
|
+
loaded = LoadedAgentFile.from_parts(
|
|
876
|
+
spec=spec,
|
|
877
|
+
instructions="",
|
|
878
|
+
system_prompt="You are concise.",
|
|
879
|
+
step_prompts={},
|
|
880
|
+
)
|
|
881
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
882
|
+
|
|
883
|
+
qa = object.__new__(QuickAgent)
|
|
884
|
+
qa.loaded = loaded
|
|
885
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
886
|
+
qa.model_settings_json = None
|
|
887
|
+
qa.toolset = RecordingToolset()
|
|
888
|
+
qa.tool_ids = []
|
|
889
|
+
qa.run_input = run_input
|
|
890
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
891
|
+
|
|
892
|
+
output = await qa._run_chain()
|
|
893
|
+
|
|
894
|
+
assert output == "hello"
|
|
895
|
+
assert FakeAgent.last_init is not None
|
|
896
|
+
assert FakeAgent.last_init["instructions"] is None
|
|
897
|
+
assert FakeAgent.last_init["system_prompt"] == "You are concise."
|
|
898
|
+
assert FakeAgent.last_prompt == make_user_prompt(run_input, qa.state)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
@pytest.mark.anyio
|
|
902
|
+
async def test_run_chain_single_shot_instructions_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
903
|
+
monkeypatch.setattr(qa_module, "Agent", FakeAgent)
|
|
904
|
+
FakeAgent.next_output = "hello"
|
|
905
|
+
|
|
906
|
+
spec = AgentSpec(
|
|
907
|
+
name="test",
|
|
908
|
+
model=ModelSpec(base_url="http://x", model_name="m"),
|
|
909
|
+
chain=[],
|
|
910
|
+
output=OutputSpec(file=None),
|
|
911
|
+
)
|
|
912
|
+
loaded = LoadedAgentFile.from_parts(
|
|
913
|
+
spec=spec,
|
|
914
|
+
instructions="Use the tool.",
|
|
915
|
+
system_prompt="",
|
|
916
|
+
step_prompts={},
|
|
917
|
+
)
|
|
918
|
+
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
919
|
+
|
|
920
|
+
qa = object.__new__(QuickAgent)
|
|
921
|
+
qa.loaded = loaded
|
|
922
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
923
|
+
qa.model_settings_json = None
|
|
924
|
+
qa.toolset = RecordingToolset()
|
|
925
|
+
qa.tool_ids = []
|
|
926
|
+
qa.run_input = run_input
|
|
927
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
928
|
+
|
|
929
|
+
output = await qa._run_chain()
|
|
930
|
+
|
|
931
|
+
assert output == "hello"
|
|
932
|
+
assert FakeAgent.last_init is not None
|
|
933
|
+
assert FakeAgent.last_init["instructions"] == "Use the tool."
|
|
934
|
+
assert FakeAgent.last_init["system_prompt"] == []
|
|
935
|
+
assert FakeAgent.last_prompt == make_user_prompt(run_input, qa.state)
|
|
936
|
+
|
|
937
|
+
|
|
642
938
|
def test_write_final_output_serializes_model(tmp_path: Path) -> None:
|
|
643
939
|
safe_root = tmp_path / "safe"
|
|
644
940
|
out_path = safe_root / "out.json"
|
|
@@ -648,7 +944,9 @@ def test_write_final_output_serializes_model(tmp_path: Path) -> None:
|
|
|
648
944
|
|
|
649
945
|
permissions = DirectoryPermissions(safe_root)
|
|
650
946
|
qa = object.__new__(QuickAgent)
|
|
651
|
-
|
|
947
|
+
qa.loaded = loaded
|
|
948
|
+
qa.permissions = permissions
|
|
949
|
+
result_path = qa._write_final_output(OutputSchema(msg="hi"))
|
|
652
950
|
|
|
653
951
|
assert result_path == out_path
|
|
654
952
|
assert "\"msg\": \"hi\"" in out_path.read_text(encoding="utf-8")
|
|
@@ -663,7 +961,9 @@ def test_write_final_output_writes_text(tmp_path: Path) -> None:
|
|
|
663
961
|
|
|
664
962
|
permissions = DirectoryPermissions(safe_root)
|
|
665
963
|
qa = object.__new__(QuickAgent)
|
|
666
|
-
|
|
964
|
+
qa.loaded = loaded
|
|
965
|
+
qa.permissions = permissions
|
|
966
|
+
result_path = qa._write_final_output("hello")
|
|
667
967
|
|
|
668
968
|
assert result_path == out_path
|
|
669
969
|
assert out_path.read_text(encoding="utf-8") == "hello"
|
|
@@ -671,26 +971,49 @@ def test_write_final_output_writes_text(tmp_path: Path) -> None:
|
|
|
671
971
|
|
|
672
972
|
@pytest.mark.anyio
|
|
673
973
|
async def test_handle_handoff_runs_followup() -> None:
|
|
674
|
-
out_path = Path("/tmp/out.json")
|
|
675
974
|
handoff = HandoffSpec(enabled=True, agent_id="next")
|
|
676
975
|
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
677
976
|
loaded = _make_loaded_with_chain([step], handoff=handoff)
|
|
678
977
|
|
|
679
978
|
qa = HandoffQuickAgent()
|
|
680
|
-
|
|
979
|
+
qa.loaded = loaded
|
|
980
|
+
await qa._handle_handoff("hello")
|
|
981
|
+
|
|
982
|
+
assert len(qa.calls) == 1
|
|
983
|
+
agent_id, input_data = qa.calls[0]
|
|
984
|
+
assert agent_id == "next"
|
|
985
|
+
assert isinstance(input_data, input_adaptors_module.TextInput)
|
|
986
|
+
run_input = input_data.load()
|
|
987
|
+
assert run_input.kind == "text"
|
|
988
|
+
assert run_input.text == "hello"
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@pytest.mark.anyio
|
|
992
|
+
async def test_handle_handoff_serializes_structured_output() -> None:
|
|
993
|
+
handoff = HandoffSpec(enabled=True, agent_id="next")
|
|
994
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
995
|
+
loaded = _make_loaded_with_chain([step], handoff=handoff)
|
|
996
|
+
|
|
997
|
+
qa = HandoffQuickAgent()
|
|
998
|
+
qa.loaded = loaded
|
|
999
|
+
await qa._handle_handoff(OutputSchema(msg="hi"))
|
|
681
1000
|
|
|
682
|
-
assert qa.calls ==
|
|
1001
|
+
assert len(qa.calls) == 1
|
|
1002
|
+
_, input_data = qa.calls[0]
|
|
1003
|
+
assert isinstance(input_data, input_adaptors_module.TextInput)
|
|
1004
|
+
run_input = input_data.load()
|
|
1005
|
+
assert "\"msg\": \"hi\"" in run_input.text
|
|
683
1006
|
|
|
684
1007
|
|
|
685
1008
|
@pytest.mark.anyio
|
|
686
1009
|
async def test_handle_handoff_skips_when_disabled() -> None:
|
|
687
|
-
out_path = Path("/tmp/out.json")
|
|
688
1010
|
handoff = HandoffSpec(enabled=False, agent_id="next")
|
|
689
1011
|
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
690
1012
|
loaded = _make_loaded_with_chain([step], handoff=handoff)
|
|
691
1013
|
|
|
692
1014
|
qa = HandoffQuickAgent()
|
|
693
|
-
|
|
1015
|
+
qa.loaded = loaded
|
|
1016
|
+
await qa._handle_handoff("ignored")
|
|
694
1017
|
|
|
695
1018
|
assert qa.calls == []
|
|
696
1019
|
|
|
@@ -705,7 +1028,12 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
705
1028
|
tools=["tool.a", "agent.call", "tool.a"],
|
|
706
1029
|
output=OutputSpec(file=str(tmp_path / "out.json")),
|
|
707
1030
|
)
|
|
708
|
-
loaded = LoadedAgentFile(
|
|
1031
|
+
loaded = LoadedAgentFile.from_parts(
|
|
1032
|
+
spec=spec,
|
|
1033
|
+
instructions="system",
|
|
1034
|
+
system_prompt="",
|
|
1035
|
+
step_prompts={"step:one": "do thing"},
|
|
1036
|
+
)
|
|
709
1037
|
|
|
710
1038
|
run_input = RunInput(source_path=str(tmp_path / "input.json"), kind="json", text="{}", data={})
|
|
711
1039
|
toolset = RecordingToolset()
|
|
@@ -722,7 +1050,7 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
722
1050
|
write_output_recorder = SyncCallRecorder(return_value=out_path)
|
|
723
1051
|
handoff_recorder = AsyncCallRecorder(return_value=None)
|
|
724
1052
|
|
|
725
|
-
monkeypatch.setattr(
|
|
1053
|
+
monkeypatch.setattr(input_adaptors_module, "load_input", load_input_recorder)
|
|
726
1054
|
monkeypatch.setattr(qa_module, "build_model", build_model_recorder)
|
|
727
1055
|
monkeypatch.setattr(QuickAgent, "_build_model_settings", build_settings_recorder)
|
|
728
1056
|
monkeypatch.setattr(QuickAgent, "_run_chain", run_chain_recorder)
|
|
@@ -739,7 +1067,7 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
739
1067
|
tools=tools,
|
|
740
1068
|
directory_permissions=_permissions(tmp_path),
|
|
741
1069
|
agent_id="agent-1",
|
|
742
|
-
|
|
1070
|
+
input_data=tmp_path / "input.json",
|
|
743
1071
|
extra_tools=["tool.b"],
|
|
744
1072
|
)
|
|
745
1073
|
|
|
@@ -756,6 +1084,8 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
756
1084
|
assert load_args[1].root == _permissions(tmp_path).root
|
|
757
1085
|
assert build_model_recorder.calls == [((loaded.spec.model,), {})]
|
|
758
1086
|
|
|
1087
|
+
assert build_settings_recorder.calls == [((loaded.spec.model,), {})]
|
|
1088
|
+
|
|
759
1089
|
assert build_toolset_recorder.calls
|
|
760
1090
|
args, kwargs = build_toolset_recorder.calls[0]
|
|
761
1091
|
assert kwargs == {}
|
|
@@ -766,7 +1096,6 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
766
1096
|
]
|
|
767
1097
|
assert isinstance(args[1], DirectoryPermissions)
|
|
768
1098
|
|
|
769
|
-
assert build_settings_recorder.calls == [((loaded.spec.model,), {})]
|
|
770
1099
|
maybe_args, maybe_kwargs = maybe_inject_recorder.calls[0]
|
|
771
1100
|
assert maybe_kwargs == {}
|
|
772
1101
|
assert maybe_args[0] == [
|
|
@@ -779,19 +1108,104 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
779
1108
|
assert callable(maybe_args[3])
|
|
780
1109
|
|
|
781
1110
|
assert run_chain_recorder.calls
|
|
782
|
-
|
|
783
|
-
assert run_chain_kwargs["loaded"] is loaded
|
|
784
|
-
assert run_chain_kwargs["model"] is model
|
|
785
|
-
assert run_chain_kwargs["model_settings_json"] is settings
|
|
786
|
-
assert run_chain_kwargs["toolset"] is toolset
|
|
787
|
-
assert run_chain_kwargs["run_input"] is run_input
|
|
788
|
-
assert run_chain_kwargs["state"]["agent_id"] == "agent-1"
|
|
1111
|
+
assert run_chain_recorder.calls[0]["kwargs"] == {}
|
|
789
1112
|
|
|
790
1113
|
assert write_output_recorder.calls
|
|
791
1114
|
write_args, write_kwargs = write_output_recorder.calls[0]
|
|
792
1115
|
assert write_kwargs == {}
|
|
793
|
-
assert write_args[0]
|
|
794
|
-
assert
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1116
|
+
assert write_args[0] == "final"
|
|
1117
|
+
assert handoff_recorder.calls == [{"args": ("final",), "kwargs": {}}]
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
@pytest.mark.anyio
|
|
1121
|
+
async def test_run_skips_write_when_output_file_missing(
|
|
1122
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1123
|
+
) -> None:
|
|
1124
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
1125
|
+
spec = AgentSpec(
|
|
1126
|
+
name="test",
|
|
1127
|
+
model=ModelSpec(base_url="http://x", model_name="m"),
|
|
1128
|
+
chain=[step],
|
|
1129
|
+
output=OutputSpec(file=None),
|
|
1130
|
+
)
|
|
1131
|
+
loaded = LoadedAgentFile.from_parts(
|
|
1132
|
+
spec=spec,
|
|
1133
|
+
instructions="system",
|
|
1134
|
+
system_prompt="",
|
|
1135
|
+
step_prompts={"step:one": "do thing"},
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
run_input = RunInput(source_path=str(tmp_path / "input.json"), kind="json", text="{}", data={})
|
|
1139
|
+
toolset = RecordingToolset()
|
|
1140
|
+
model = object()
|
|
1141
|
+
|
|
1142
|
+
load_input_recorder = SyncCallRecorder(return_value=run_input)
|
|
1143
|
+
build_model_recorder = SyncCallRecorder(return_value=model)
|
|
1144
|
+
build_toolset_recorder = SyncCallRecorder(return_value=toolset)
|
|
1145
|
+
build_settings_recorder = SyncCallRecorder(return_value=None)
|
|
1146
|
+
maybe_inject_recorder = SyncCallRecorder(return_value=None)
|
|
1147
|
+
run_chain_recorder = AsyncCallRecorder(return_value="final")
|
|
1148
|
+
write_output_recorder = SyncCallRecorder(return_value=tmp_path / "out.json")
|
|
1149
|
+
handoff_recorder = AsyncCallRecorder(return_value=None)
|
|
1150
|
+
|
|
1151
|
+
monkeypatch.setattr(input_adaptors_module, "load_input", load_input_recorder)
|
|
1152
|
+
monkeypatch.setattr(qa_module, "build_model", build_model_recorder)
|
|
1153
|
+
monkeypatch.setattr(QuickAgent, "_build_model_settings", build_settings_recorder)
|
|
1154
|
+
monkeypatch.setattr(QuickAgent, "_run_chain", run_chain_recorder)
|
|
1155
|
+
monkeypatch.setattr(QuickAgent, "_write_final_output", write_output_recorder)
|
|
1156
|
+
monkeypatch.setattr(QuickAgent, "_handle_handoff", handoff_recorder)
|
|
1157
|
+
|
|
1158
|
+
tools = AgentTools([tmp_path])
|
|
1159
|
+
monkeypatch.setattr(tools, "build_toolset", build_toolset_recorder)
|
|
1160
|
+
monkeypatch.setattr(tools, "maybe_inject_agent_call", maybe_inject_recorder)
|
|
1161
|
+
fake_registry = FakeRegistry(loaded)
|
|
1162
|
+
|
|
1163
|
+
agent = QuickAgent(
|
|
1164
|
+
registry=fake_registry,
|
|
1165
|
+
tools=tools,
|
|
1166
|
+
directory_permissions=_permissions(tmp_path),
|
|
1167
|
+
agent_id="agent-1",
|
|
1168
|
+
input_data=tmp_path / "input.json",
|
|
1169
|
+
extra_tools=None,
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
result = await agent.run()
|
|
1173
|
+
|
|
1174
|
+
assert result == "final"
|
|
1175
|
+
assert write_output_recorder.calls == []
|
|
1176
|
+
assert handoff_recorder.calls == [{"args": ("final",), "kwargs": {}}]
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
@pytest.mark.anyio
|
|
1180
|
+
@pytest.mark.parametrize(
|
|
1181
|
+
("nested_output", "expected_write_output"),
|
|
1182
|
+
[
|
|
1183
|
+
("inline", False),
|
|
1184
|
+
("file", True),
|
|
1185
|
+
],
|
|
1186
|
+
)
|
|
1187
|
+
async def test_run_nested_agent_respects_nested_output(
|
|
1188
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1189
|
+
nested_output: Literal["inline", "file"],
|
|
1190
|
+
expected_write_output: bool,
|
|
1191
|
+
) -> None:
|
|
1192
|
+
qa = object.__new__(QuickAgent)
|
|
1193
|
+
qa._registry = cast(AgentRegistry, object())
|
|
1194
|
+
qa._tools = cast(AgentTools, object())
|
|
1195
|
+
qa._directory_permissions = cast(DirectoryPermissions, object())
|
|
1196
|
+
|
|
1197
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
1198
|
+
loaded = _make_loaded_with_chain([step])
|
|
1199
|
+
loaded.spec.nested_output = nested_output
|
|
1200
|
+
qa.loaded = loaded
|
|
1201
|
+
|
|
1202
|
+
init_recorder = SyncCallRecorder(return_value=None)
|
|
1203
|
+
run_recorder = AsyncCallRecorder(return_value="ok")
|
|
1204
|
+
monkeypatch.setattr(QuickAgent, "__init__", init_recorder)
|
|
1205
|
+
monkeypatch.setattr(QuickAgent, "run", run_recorder)
|
|
1206
|
+
|
|
1207
|
+
await qa._run_nested_agent("child", Path("input.txt"))
|
|
1208
|
+
|
|
1209
|
+
assert len(init_recorder.calls) == 1
|
|
1210
|
+
_, kwargs = init_recorder.calls[0]
|
|
1211
|
+
assert kwargs["write_output"] is expected_write_output
|