quick-agent 0.1.1__py3-none-any.whl → 0.1.2__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 +2 -2
- 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/orchestrator.py +15 -8
- quick_agent/prompting.py +2 -2
- quick_agent/py.typed +1 -0
- quick_agent/quick_agent.py +87 -132
- quick_agent/schemas/outputs.py +6 -0
- quick_agent-0.1.2.data/data/quick_agent/agents/business-extract-structured.md +49 -0
- quick_agent-0.1.2.data/data/quick_agent/agents/business-extract.md +42 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/METADATA +17 -4
- quick_agent-0.1.2.dist-info/RECORD +51 -0
- tests/test_directory_permissions.py +10 -0
- tests/test_input_adaptors.py +31 -0
- tests/test_integration.py +134 -1
- tests/test_orchestrator.py +183 -94
- quick_agent-0.1.1.dist-info/RECORD +0 -45
- {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/function-spec-validator.md +0 -0
- {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/subagent-validate-eval-list.md +0 -0
- {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/subagent-validator-contains.md +0 -0
- {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/template.md +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/WHEEL +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/entry_points.txt +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/top_level.txt +0 -0
tests/test_integration.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
1
|
import os
|
|
2
|
+
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
from quick_agent.orchestrator import Orchestrator
|
|
@@ -154,6 +154,9 @@ Use the extracted JSON from the chain state as the ContactInfo object.
|
|
|
154
154
|
assert output.summary
|
|
155
155
|
assert "Avery" in output.summary
|
|
156
156
|
assert "Acme" in output.summary
|
|
157
|
+
assert output_path.exists()
|
|
158
|
+
file_output = ContactSummary.model_validate_json(output_path.read_text(encoding="utf-8"))
|
|
159
|
+
assert file_output.model_dump() == output.model_dump()
|
|
157
160
|
|
|
158
161
|
|
|
159
162
|
def test_orchestrator_allows_agent_call_tool(tmp_path: Path) -> None:
|
|
@@ -187,6 +190,7 @@ Reply with exactly: pong
|
|
|
187
190
|
name: Parent Agent
|
|
188
191
|
tools:
|
|
189
192
|
- "agent.call"
|
|
193
|
+
nested_output: inline
|
|
190
194
|
chain:
|
|
191
195
|
- id: invoke
|
|
192
196
|
kind: text
|
|
@@ -219,3 +223,132 @@ Then respond with only the returned text value.
|
|
|
219
223
|
|
|
220
224
|
output = anyio.run(_run_agent, orchestrator, "parent", parent_input)
|
|
221
225
|
assert output == "pong"
|
|
226
|
+
assert not child_output.exists()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_orchestrator_allows_agent_call_tool_with_inline_text(tmp_path: Path) -> None:
|
|
230
|
+
_require_env("OPENAI_API_KEY")
|
|
231
|
+
safe_root = tmp_path / "safe"
|
|
232
|
+
safe_root.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
|
|
234
|
+
agents_dir = tmp_path / "agents"
|
|
235
|
+
agents_dir.mkdir(parents=True)
|
|
236
|
+
|
|
237
|
+
child_output = safe_root / "out" / "child.json"
|
|
238
|
+
child_md = f"""---
|
|
239
|
+
name: Child Agent
|
|
240
|
+
chain:
|
|
241
|
+
- id: respond
|
|
242
|
+
kind: text
|
|
243
|
+
prompt_section: step:respond
|
|
244
|
+
output:
|
|
245
|
+
format: json
|
|
246
|
+
file: {child_output}
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## step:respond
|
|
250
|
+
|
|
251
|
+
Reply with exactly: pong
|
|
252
|
+
"""
|
|
253
|
+
(agents_dir / "child.md").write_text(child_md, encoding="utf-8")
|
|
254
|
+
|
|
255
|
+
parent_output = safe_root / "out" / "parent.json"
|
|
256
|
+
parent_md = f"""---
|
|
257
|
+
name: Parent Agent
|
|
258
|
+
tools:
|
|
259
|
+
- "agent.call"
|
|
260
|
+
nested_output: inline
|
|
261
|
+
chain:
|
|
262
|
+
- id: invoke
|
|
263
|
+
kind: text
|
|
264
|
+
prompt_section: step:invoke
|
|
265
|
+
output:
|
|
266
|
+
format: json
|
|
267
|
+
file: {parent_output}
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## step:invoke
|
|
271
|
+
|
|
272
|
+
Call agent_call with agent "child" and input_text "hello from memory".
|
|
273
|
+
Then respond with only the returned text value.
|
|
274
|
+
"""
|
|
275
|
+
(agents_dir / "parent.md").write_text(parent_md, encoding="utf-8")
|
|
276
|
+
|
|
277
|
+
parent_input = safe_root / "parent_input.txt"
|
|
278
|
+
parent_input.write_text("call child", encoding="utf-8")
|
|
279
|
+
|
|
280
|
+
orchestrator = Orchestrator(
|
|
281
|
+
[agents_dir],
|
|
282
|
+
[tmp_path / "tools"],
|
|
283
|
+
safe_dir=safe_root,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
import anyio
|
|
287
|
+
|
|
288
|
+
output = anyio.run(_run_agent, orchestrator, "parent", parent_input)
|
|
289
|
+
assert output == "pong"
|
|
290
|
+
assert not child_output.exists()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_orchestrator_allows_nested_output_file(tmp_path: Path) -> None:
|
|
294
|
+
_require_env("OPENAI_API_KEY")
|
|
295
|
+
safe_root = tmp_path / "safe"
|
|
296
|
+
safe_root.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
|
|
298
|
+
agents_dir = tmp_path / "agents"
|
|
299
|
+
agents_dir.mkdir(parents=True)
|
|
300
|
+
|
|
301
|
+
child_output = safe_root / "out" / "child.json"
|
|
302
|
+
child_md = f"""---
|
|
303
|
+
name: Child Agent
|
|
304
|
+
chain:
|
|
305
|
+
- id: respond
|
|
306
|
+
kind: text
|
|
307
|
+
prompt_section: step:respond
|
|
308
|
+
output:
|
|
309
|
+
format: json
|
|
310
|
+
file: {child_output}
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## step:respond
|
|
314
|
+
|
|
315
|
+
Reply with exactly: pong
|
|
316
|
+
"""
|
|
317
|
+
(agents_dir / "child.md").write_text(child_md, encoding="utf-8")
|
|
318
|
+
|
|
319
|
+
parent_output = safe_root / "out" / "parent.json"
|
|
320
|
+
parent_md = f"""---
|
|
321
|
+
name: Parent Agent
|
|
322
|
+
tools:
|
|
323
|
+
- "agent.call"
|
|
324
|
+
nested_output: file
|
|
325
|
+
chain:
|
|
326
|
+
- id: invoke
|
|
327
|
+
kind: text
|
|
328
|
+
prompt_section: step:invoke
|
|
329
|
+
output:
|
|
330
|
+
format: json
|
|
331
|
+
file: {parent_output}
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## step:invoke
|
|
335
|
+
|
|
336
|
+
Call agent_call with agent "child" and input_text "hello from memory".
|
|
337
|
+
Then respond with only the returned text value.
|
|
338
|
+
"""
|
|
339
|
+
(agents_dir / "parent.md").write_text(parent_md, encoding="utf-8")
|
|
340
|
+
|
|
341
|
+
parent_input = safe_root / "parent_input.txt"
|
|
342
|
+
parent_input.write_text("call child", encoding="utf-8")
|
|
343
|
+
|
|
344
|
+
orchestrator = Orchestrator(
|
|
345
|
+
[agents_dir],
|
|
346
|
+
[tmp_path / "tools"],
|
|
347
|
+
safe_dir=safe_root,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
import anyio
|
|
351
|
+
|
|
352
|
+
output = anyio.run(_run_agent, orchestrator, "parent", parent_input)
|
|
353
|
+
assert output == "pong"
|
|
354
|
+
assert child_output.exists()
|
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
|
|
@@ -157,10 +158,10 @@ class RecordingQuickAgent(QuickAgent):
|
|
|
157
158
|
|
|
158
159
|
class HandoffQuickAgent(QuickAgent):
|
|
159
160
|
def __init__(self) -> None:
|
|
160
|
-
self.calls: list[tuple[str, Path]] = []
|
|
161
|
+
self.calls: list[tuple[str, input_adaptors_module.InputAdaptor | Path]] = []
|
|
161
162
|
|
|
162
|
-
async def _run_nested_agent(self, agent_id: str,
|
|
163
|
-
self.calls.append((agent_id,
|
|
163
|
+
async def _run_nested_agent(self, agent_id: str, input_data: input_adaptors_module.InputAdaptor | Path) -> str:
|
|
164
|
+
self.calls.append((agent_id, input_data))
|
|
164
165
|
return "ok"
|
|
165
166
|
|
|
166
167
|
|
|
@@ -306,12 +307,41 @@ def test_maybe_inject_agent_call_tool_skips_when_missing() -> None:
|
|
|
306
307
|
assert toolset.add_calls == []
|
|
307
308
|
|
|
308
309
|
|
|
310
|
+
@pytest.mark.anyio
|
|
311
|
+
async def test_agent_call_tool_accepts_input_text() -> None:
|
|
312
|
+
recorder = AsyncCallRecorder(return_value="ok")
|
|
313
|
+
tool = AgentCallTool(recorder, "run/input.json")
|
|
314
|
+
|
|
315
|
+
result = await tool(agent="child", input_text="hello")
|
|
316
|
+
|
|
317
|
+
assert result == {"text": "ok"}
|
|
318
|
+
assert len(recorder.calls) == 1
|
|
319
|
+
args = recorder.calls[0]["args"]
|
|
320
|
+
assert args[0] == "child"
|
|
321
|
+
assert isinstance(args[1], input_adaptors_module.TextInput)
|
|
322
|
+
run_input = args[1].load()
|
|
323
|
+
assert run_input.kind == "text"
|
|
324
|
+
assert run_input.text == "hello"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@pytest.mark.anyio
|
|
328
|
+
async def test_agent_call_tool_rejects_missing_or_duplicate_input() -> None:
|
|
329
|
+
recorder = AsyncCallRecorder(return_value="ok")
|
|
330
|
+
tool = AgentCallTool(recorder, "run/input.json")
|
|
331
|
+
|
|
332
|
+
with pytest.raises(ValueError):
|
|
333
|
+
await tool(agent="child")
|
|
334
|
+
with pytest.raises(ValueError):
|
|
335
|
+
await tool(agent="child", input_file="a.txt", input_text="hi")
|
|
336
|
+
|
|
337
|
+
|
|
309
338
|
def test_init_state_contains_agent_id_and_steps() -> None:
|
|
310
339
|
qa = object.__new__(QuickAgent)
|
|
311
340
|
|
|
312
|
-
|
|
341
|
+
qa._agent_id = "agent-1"
|
|
342
|
+
state = qa._init_state()
|
|
313
343
|
|
|
314
|
-
assert state == {"agent_id": "agent-1", "steps": {}}
|
|
344
|
+
assert state == {"agent_id": "agent-1", "steps": {}, "final_output": None}
|
|
315
345
|
|
|
316
346
|
|
|
317
347
|
def test_build_model_settings_openai_compatible() -> None:
|
|
@@ -348,14 +378,11 @@ def test_build_model_settings_other_provider() -> None:
|
|
|
348
378
|
def test_build_structured_model_settings_non_openai_passthrough() -> None:
|
|
349
379
|
qa = object.__new__(QuickAgent)
|
|
350
380
|
schema = ExampleSchema
|
|
351
|
-
model = cast(OpenAIChatModel, DummyOpenAIModel("http://localhost"))
|
|
352
381
|
settings: ModelSettings = {"extra_body": {"format": "json"}}
|
|
382
|
+
qa.model = cast(OpenAIChatModel, DummyOpenAIModel("http://localhost"))
|
|
383
|
+
qa.model_settings_json = settings
|
|
353
384
|
|
|
354
|
-
result = qa._build_structured_model_settings(
|
|
355
|
-
model=model,
|
|
356
|
-
model_settings_json=settings,
|
|
357
|
-
schema_cls=schema,
|
|
358
|
-
)
|
|
385
|
+
result = qa._build_structured_model_settings(schema_cls=schema)
|
|
359
386
|
|
|
360
387
|
assert result == settings
|
|
361
388
|
|
|
@@ -363,13 +390,10 @@ def test_build_structured_model_settings_non_openai_passthrough() -> None:
|
|
|
363
390
|
def test_build_structured_model_settings_openai_injects_schema() -> None:
|
|
364
391
|
qa = object.__new__(QuickAgent)
|
|
365
392
|
schema = ExampleSchema
|
|
366
|
-
model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
|
|
393
|
+
qa.model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
|
|
394
|
+
qa.model_settings_json = None
|
|
367
395
|
|
|
368
|
-
result = qa._build_structured_model_settings(
|
|
369
|
-
model=model,
|
|
370
|
-
model_settings_json=None,
|
|
371
|
-
schema_cls=schema,
|
|
372
|
-
)
|
|
396
|
+
result = qa._build_structured_model_settings(schema_cls=schema)
|
|
373
397
|
|
|
374
398
|
assert result is not None
|
|
375
399
|
extra_body_obj = result.get("extra_body")
|
|
@@ -390,8 +414,13 @@ def test_build_user_prompt_raises_for_missing_section() -> None:
|
|
|
390
414
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
391
415
|
|
|
392
416
|
qa = object.__new__(QuickAgent)
|
|
417
|
+
qa.loaded = loaded
|
|
418
|
+
qa.run_input = run_input
|
|
419
|
+
qa.state = {"agent_id": "agent-1", "steps": {}, "final_output": None}
|
|
393
420
|
with pytest.raises(KeyError):
|
|
394
|
-
qa._build_user_prompt(
|
|
421
|
+
qa._build_user_prompt(
|
|
422
|
+
step=step,
|
|
423
|
+
)
|
|
395
424
|
|
|
396
425
|
|
|
397
426
|
def test_build_user_prompt_uses_prompting(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -402,10 +431,20 @@ def test_build_user_prompt_uses_prompting(monkeypatch: pytest.MonkeyPatch) -> No
|
|
|
402
431
|
monkeypatch.setattr(qa_module, "make_user_prompt", recorder)
|
|
403
432
|
|
|
404
433
|
qa = object.__new__(QuickAgent)
|
|
405
|
-
|
|
434
|
+
qa.loaded = loaded
|
|
435
|
+
qa.run_input = run_input
|
|
436
|
+
qa.state = {"agent_id": "agent-1", "steps": {}, "final_output": None}
|
|
437
|
+
result = qa._build_user_prompt(
|
|
438
|
+
step=step,
|
|
439
|
+
)
|
|
406
440
|
|
|
407
441
|
assert result == "prompt"
|
|
408
|
-
assert recorder.calls == [
|
|
442
|
+
assert recorder.calls == [
|
|
443
|
+
(
|
|
444
|
+
(loaded.step_prompts["step:one"], run_input, {"agent_id": "agent-1", "steps": {}, "final_output": None}),
|
|
445
|
+
{},
|
|
446
|
+
)
|
|
447
|
+
]
|
|
409
448
|
|
|
410
449
|
|
|
411
450
|
@pytest.mark.anyio
|
|
@@ -418,14 +457,14 @@ async def test_run_step_text_returns_output(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
418
457
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
419
458
|
|
|
420
459
|
qa = object.__new__(QuickAgent)
|
|
460
|
+
qa.loaded = loaded
|
|
461
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
462
|
+
qa.model_settings_json = None
|
|
463
|
+
qa.toolset = RecordingToolset()
|
|
464
|
+
qa.run_input = run_input
|
|
465
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
421
466
|
output, final = await qa._run_step(
|
|
422
467
|
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
468
|
)
|
|
430
469
|
|
|
431
470
|
assert output == "hello"
|
|
@@ -456,14 +495,14 @@ async def test_run_step_structured_parses_json_with_fallback(monkeypatch: pytest
|
|
|
456
495
|
|
|
457
496
|
try:
|
|
458
497
|
qa = object.__new__(QuickAgent)
|
|
498
|
+
qa.loaded = loaded
|
|
499
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
500
|
+
qa.model_settings_json = {"extra_body": {"format": "json"}}
|
|
501
|
+
qa.toolset = RecordingToolset()
|
|
502
|
+
qa.run_input = run_input
|
|
503
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
459
504
|
output, final = await qa._run_step(
|
|
460
505
|
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
506
|
)
|
|
468
507
|
finally:
|
|
469
508
|
sys.modules.pop("schemas.struct", None)
|
|
@@ -481,15 +520,15 @@ async def test_run_step_unknown_kind_raises(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
481
520
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
482
521
|
|
|
483
522
|
qa = object.__new__(QuickAgent)
|
|
523
|
+
qa.loaded = loaded
|
|
524
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
525
|
+
qa.model_settings_json = None
|
|
526
|
+
qa.toolset = RecordingToolset()
|
|
527
|
+
qa.run_input = run_input
|
|
528
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
484
529
|
with pytest.raises(NotImplementedError):
|
|
485
530
|
await qa._run_step(
|
|
486
531
|
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
532
|
)
|
|
494
533
|
|
|
495
534
|
|
|
@@ -502,15 +541,14 @@ async def test_run_text_step_uses_build_user_prompt(monkeypatch: pytest.MonkeyPa
|
|
|
502
541
|
run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
503
542
|
|
|
504
543
|
qa = object.__new__(QuickAgent)
|
|
544
|
+
qa.loaded = loaded
|
|
545
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
546
|
+
qa.toolset = RecordingToolset()
|
|
547
|
+
qa.run_input = run_input
|
|
505
548
|
monkeypatch.setattr(qa, "_build_user_prompt", SyncCallRecorder(return_value="prompt"))
|
|
506
549
|
|
|
507
550
|
output, final = await qa._run_text_step(
|
|
508
551
|
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
552
|
)
|
|
515
553
|
|
|
516
554
|
assert output == "ok"
|
|
@@ -525,15 +563,14 @@ async def test_run_structured_step_missing_schema_raises() -> None:
|
|
|
525
563
|
run_input = RunInput(source_path="in.json", kind="json", text="{}", data={})
|
|
526
564
|
|
|
527
565
|
qa = object.__new__(QuickAgent)
|
|
566
|
+
qa.loaded = loaded
|
|
567
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
568
|
+
qa.model_settings_json = None
|
|
569
|
+
qa.toolset = RecordingToolset()
|
|
570
|
+
qa.run_input = run_input
|
|
528
571
|
with pytest.raises(ValueError):
|
|
529
572
|
await qa._run_structured_step(
|
|
530
573
|
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
574
|
)
|
|
538
575
|
|
|
539
576
|
|
|
@@ -558,14 +595,14 @@ async def test_run_structured_step_parses_json(monkeypatch: pytest.MonkeyPatch)
|
|
|
558
595
|
|
|
559
596
|
try:
|
|
560
597
|
qa = object.__new__(QuickAgent)
|
|
598
|
+
qa.loaded = loaded
|
|
599
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
600
|
+
qa.model_settings_json = None
|
|
601
|
+
qa.toolset = RecordingToolset()
|
|
602
|
+
qa.run_input = run_input
|
|
603
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
561
604
|
output, final = await qa._run_structured_step(
|
|
562
605
|
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
606
|
)
|
|
570
607
|
finally:
|
|
571
608
|
sys.modules.pop("schemas.struct2", None)
|
|
@@ -595,14 +632,14 @@ async def test_run_structured_step_adds_json_schema_for_openai(monkeypatch: pyte
|
|
|
595
632
|
|
|
596
633
|
try:
|
|
597
634
|
qa = object.__new__(QuickAgent)
|
|
635
|
+
qa.loaded = loaded
|
|
636
|
+
qa.model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
|
|
637
|
+
qa.model_settings_json = None
|
|
638
|
+
qa.toolset = RecordingToolset()
|
|
639
|
+
qa.run_input = run_input
|
|
640
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
598
641
|
await qa._run_structured_step(
|
|
599
642
|
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
643
|
)
|
|
607
644
|
finally:
|
|
608
645
|
sys.modules.pop("schemas.struct3", None)
|
|
@@ -623,19 +660,18 @@ async def test_run_chain_updates_state_and_returns_last() -> None:
|
|
|
623
660
|
loaded = _make_loaded_with_chain([step1, step2])
|
|
624
661
|
|
|
625
662
|
qa = RecordingQuickAgent(outputs=[({"a": 1}, "first"), ("b", "second")])
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
state=state,
|
|
635
|
-
)
|
|
663
|
+
qa.loaded = loaded
|
|
664
|
+
qa.model = cast(OpenAIChatModel, object())
|
|
665
|
+
qa.model_settings_json = None
|
|
666
|
+
qa.toolset = RecordingToolset()
|
|
667
|
+
qa.run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
|
|
668
|
+
qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
|
|
669
|
+
|
|
670
|
+
final = await qa._run_chain()
|
|
636
671
|
|
|
637
672
|
assert final == "second"
|
|
638
|
-
assert state["steps"] == {"s1": {"a": 1}, "s2": "b"}
|
|
673
|
+
assert qa.state["steps"] == {"s1": {"a": 1}, "s2": "b"}
|
|
674
|
+
assert qa.state["final_output"] == "b"
|
|
639
675
|
assert qa.calls == ["s1", "s2"]
|
|
640
676
|
|
|
641
677
|
|
|
@@ -648,7 +684,9 @@ def test_write_final_output_serializes_model(tmp_path: Path) -> None:
|
|
|
648
684
|
|
|
649
685
|
permissions = DirectoryPermissions(safe_root)
|
|
650
686
|
qa = object.__new__(QuickAgent)
|
|
651
|
-
|
|
687
|
+
qa.loaded = loaded
|
|
688
|
+
qa.permissions = permissions
|
|
689
|
+
result_path = qa._write_final_output(OutputSchema(msg="hi"))
|
|
652
690
|
|
|
653
691
|
assert result_path == out_path
|
|
654
692
|
assert "\"msg\": \"hi\"" in out_path.read_text(encoding="utf-8")
|
|
@@ -663,7 +701,9 @@ def test_write_final_output_writes_text(tmp_path: Path) -> None:
|
|
|
663
701
|
|
|
664
702
|
permissions = DirectoryPermissions(safe_root)
|
|
665
703
|
qa = object.__new__(QuickAgent)
|
|
666
|
-
|
|
704
|
+
qa.loaded = loaded
|
|
705
|
+
qa.permissions = permissions
|
|
706
|
+
result_path = qa._write_final_output("hello")
|
|
667
707
|
|
|
668
708
|
assert result_path == out_path
|
|
669
709
|
assert out_path.read_text(encoding="utf-8") == "hello"
|
|
@@ -671,26 +711,49 @@ def test_write_final_output_writes_text(tmp_path: Path) -> None:
|
|
|
671
711
|
|
|
672
712
|
@pytest.mark.anyio
|
|
673
713
|
async def test_handle_handoff_runs_followup() -> None:
|
|
674
|
-
out_path = Path("/tmp/out.json")
|
|
675
714
|
handoff = HandoffSpec(enabled=True, agent_id="next")
|
|
676
715
|
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
677
716
|
loaded = _make_loaded_with_chain([step], handoff=handoff)
|
|
678
717
|
|
|
679
718
|
qa = HandoffQuickAgent()
|
|
680
|
-
|
|
719
|
+
qa.loaded = loaded
|
|
720
|
+
await qa._handle_handoff("hello")
|
|
721
|
+
|
|
722
|
+
assert len(qa.calls) == 1
|
|
723
|
+
agent_id, input_data = qa.calls[0]
|
|
724
|
+
assert agent_id == "next"
|
|
725
|
+
assert isinstance(input_data, input_adaptors_module.TextInput)
|
|
726
|
+
run_input = input_data.load()
|
|
727
|
+
assert run_input.kind == "text"
|
|
728
|
+
assert run_input.text == "hello"
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
@pytest.mark.anyio
|
|
732
|
+
async def test_handle_handoff_serializes_structured_output() -> None:
|
|
733
|
+
handoff = HandoffSpec(enabled=True, agent_id="next")
|
|
734
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
735
|
+
loaded = _make_loaded_with_chain([step], handoff=handoff)
|
|
736
|
+
|
|
737
|
+
qa = HandoffQuickAgent()
|
|
738
|
+
qa.loaded = loaded
|
|
739
|
+
await qa._handle_handoff(OutputSchema(msg="hi"))
|
|
681
740
|
|
|
682
|
-
assert qa.calls ==
|
|
741
|
+
assert len(qa.calls) == 1
|
|
742
|
+
_, input_data = qa.calls[0]
|
|
743
|
+
assert isinstance(input_data, input_adaptors_module.TextInput)
|
|
744
|
+
run_input = input_data.load()
|
|
745
|
+
assert "\"msg\": \"hi\"" in run_input.text
|
|
683
746
|
|
|
684
747
|
|
|
685
748
|
@pytest.mark.anyio
|
|
686
749
|
async def test_handle_handoff_skips_when_disabled() -> None:
|
|
687
|
-
out_path = Path("/tmp/out.json")
|
|
688
750
|
handoff = HandoffSpec(enabled=False, agent_id="next")
|
|
689
751
|
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
690
752
|
loaded = _make_loaded_with_chain([step], handoff=handoff)
|
|
691
753
|
|
|
692
754
|
qa = HandoffQuickAgent()
|
|
693
|
-
|
|
755
|
+
qa.loaded = loaded
|
|
756
|
+
await qa._handle_handoff("ignored")
|
|
694
757
|
|
|
695
758
|
assert qa.calls == []
|
|
696
759
|
|
|
@@ -722,7 +785,7 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
722
785
|
write_output_recorder = SyncCallRecorder(return_value=out_path)
|
|
723
786
|
handoff_recorder = AsyncCallRecorder(return_value=None)
|
|
724
787
|
|
|
725
|
-
monkeypatch.setattr(
|
|
788
|
+
monkeypatch.setattr(input_adaptors_module, "load_input", load_input_recorder)
|
|
726
789
|
monkeypatch.setattr(qa_module, "build_model", build_model_recorder)
|
|
727
790
|
monkeypatch.setattr(QuickAgent, "_build_model_settings", build_settings_recorder)
|
|
728
791
|
monkeypatch.setattr(QuickAgent, "_run_chain", run_chain_recorder)
|
|
@@ -739,7 +802,7 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
739
802
|
tools=tools,
|
|
740
803
|
directory_permissions=_permissions(tmp_path),
|
|
741
804
|
agent_id="agent-1",
|
|
742
|
-
|
|
805
|
+
input_data=tmp_path / "input.json",
|
|
743
806
|
extra_tools=["tool.b"],
|
|
744
807
|
)
|
|
745
808
|
|
|
@@ -779,19 +842,45 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
|
|
|
779
842
|
assert callable(maybe_args[3])
|
|
780
843
|
|
|
781
844
|
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"
|
|
845
|
+
assert run_chain_recorder.calls[0]["kwargs"] == {}
|
|
789
846
|
|
|
790
847
|
assert write_output_recorder.calls
|
|
791
848
|
write_args, write_kwargs = write_output_recorder.calls[0]
|
|
792
849
|
assert write_kwargs == {}
|
|
793
|
-
assert write_args[0]
|
|
794
|
-
assert
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
850
|
+
assert write_args[0] == "final"
|
|
851
|
+
assert handoff_recorder.calls == [{"args": ("final",), "kwargs": {}}]
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
@pytest.mark.anyio
|
|
855
|
+
@pytest.mark.parametrize(
|
|
856
|
+
("nested_output", "expected_write_output"),
|
|
857
|
+
[
|
|
858
|
+
("inline", False),
|
|
859
|
+
("file", True),
|
|
860
|
+
],
|
|
861
|
+
)
|
|
862
|
+
async def test_run_nested_agent_respects_nested_output(
|
|
863
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
864
|
+
nested_output: Literal["inline", "file"],
|
|
865
|
+
expected_write_output: bool,
|
|
866
|
+
) -> None:
|
|
867
|
+
qa = object.__new__(QuickAgent)
|
|
868
|
+
qa._registry = cast(AgentRegistry, object())
|
|
869
|
+
qa._tools = cast(AgentTools, object())
|
|
870
|
+
qa._directory_permissions = cast(DirectoryPermissions, object())
|
|
871
|
+
|
|
872
|
+
step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
|
|
873
|
+
loaded = _make_loaded_with_chain([step])
|
|
874
|
+
loaded.spec.nested_output = nested_output
|
|
875
|
+
qa.loaded = loaded
|
|
876
|
+
|
|
877
|
+
init_recorder = SyncCallRecorder(return_value=None)
|
|
878
|
+
run_recorder = AsyncCallRecorder(return_value="ok")
|
|
879
|
+
monkeypatch.setattr(QuickAgent, "__init__", init_recorder)
|
|
880
|
+
monkeypatch.setattr(QuickAgent, "run", run_recorder)
|
|
881
|
+
|
|
882
|
+
await qa._run_nested_agent("child", Path("input.txt"))
|
|
883
|
+
|
|
884
|
+
assert len(init_recorder.calls) == 1
|
|
885
|
+
_, kwargs = init_recorder.calls[0]
|
|
886
|
+
assert kwargs["write_output"] is expected_write_output
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
quick_agent/__init__.py,sha256=RdySRuttQey8SWqmhQD0HQ387LpGOikh-vEwvZnwSW0,170
|
|
2
|
-
quick_agent/agent_call_tool.py,sha256=EdLsNTTfgeoiLGC5Pu9lJ-KNStzrwQ9cAS8KvsQ07og,1509
|
|
3
|
-
quick_agent/agent_registry.py,sha256=SWO5cs634BuA_vxjm3_EEX_yM7wT1FyK6WRjsiNVu1w,2453
|
|
4
|
-
quick_agent/agent_tools.py,sha256=N5oit0LiHeURU_Fge0TF9wlfaP57Y_ggX_IZxWcGTxM,1396
|
|
5
|
-
quick_agent/cli.py,sha256=7e_UPrXfU9JG-khC9tC9DzDkXUJ4Fm9weCEqiIEYmY0,1395
|
|
6
|
-
quick_agent/directory_permissions.py,sha256=3dp0AoNvmkyuR6BSxiePki__2eVpXLgHGfP7stT7lo0,1729
|
|
7
|
-
quick_agent/io_utils.py,sha256=BUmUoZepL4lSYe0JbKxLm4mFFQ6Zt6MZQzprzgoBweU,1200
|
|
8
|
-
quick_agent/json_utils.py,sha256=G6uP9SrdEw5WtA9--dBqdJcTYD2JgdgotPJdleErB_c,1003
|
|
9
|
-
quick_agent/orchestrator.py,sha256=liN5SElPJ8m879tsxL2tLJKAb7wCtByXxXQ9OC6-was,1081
|
|
10
|
-
quick_agent/prompting.py,sha256=MQxrIJTMj0s_aNOJoqLPDv-_hcTlZOIzFdvXuFTrS3A,633
|
|
11
|
-
quick_agent/quick_agent.py,sha256=H2ypcWFniEHcUk2Kql7fPDC2ePavCpF78uFS7AVJwnE,11188
|
|
12
|
-
quick_agent/tools_loader.py,sha256=oveR-EGAZo3Q7NPJ2aWKU3d8xd8kIpibOrjInpUZwQg,2642
|
|
13
|
-
quick_agent/models/__init__.py,sha256=YseFAVWLarh0wZCVUL0fmkjVgA-FnYJLEiVinG1-6Tk,737
|
|
14
|
-
quick_agent/models/agent_spec.py,sha256=K02ZbqMRbkdbk2f75ETfrGI55OUFiVQH562qsg1r5AQ,798
|
|
15
|
-
quick_agent/models/chain_step_spec.py,sha256=NcdYFmgJhA5ODIwCTgTb2fmmJJesAZCVAIy3cGC5dhs,342
|
|
16
|
-
quick_agent/models/handoff_spec.py,sha256=46Pt4JGU8-6f-cdbDR0oqWuJbg5ENJcXrFN1IK3chvs,280
|
|
17
|
-
quick_agent/models/loaded_agent_file.py,sha256=rWRxrm0KjgmNIiYfooMM95eohMB1zCZC-eqrp1mf8w0,313
|
|
18
|
-
quick_agent/models/model_spec.py,sha256=spnIZ8BNtXT5t8YRL1nh7cXrgWwNSptt_BMnAB8w1Lg,425
|
|
19
|
-
quick_agent/models/output_spec.py,sha256=bvKFU1ONeza95qethNIiVgrsnT7l1TKMsdHjfXVVJnc,229
|
|
20
|
-
quick_agent/models/run_input.py,sha256=WpV-QPlOPXMmZR6eIvP7vJOuCeBt3xWe4j4ngEbmSxg,282
|
|
21
|
-
quick_agent/models/tool_impl_spec.py,sha256=7B161m8nFZCNjslb4VZdSi-mdAZM0jhp4kV4uaq8X1s,216
|
|
22
|
-
quick_agent/models/tool_json.py,sha256=eYFBPCqxmRfyo7GYHBXCfog7kUzpjginfjt9grQy4ic,274
|
|
23
|
-
quick_agent/schemas/outputs.py,sha256=4yQigYus0cqu2_AWSPi566i2AT18jtKURkaqEQc7U6E,1434
|
|
24
|
-
quick_agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
quick_agent/tools/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
-
quick_agent/tools/filesystem/adapter.py,sha256=aYcX1qjI9wRSANSNSB3KWbKi7uDIYo3EpJotpUxnQUM,1027
|
|
27
|
-
quick_agent/tools/filesystem/read_text.py,sha256=owpZxwCFoYbljABhwpNKU4g7-8xDPJDu0kZYF7bPLfs,478
|
|
28
|
-
quick_agent/tools/filesystem/write_text.py,sha256=lwsMCk_p8xurVxychM2UBieXDp0SgN3C6rufLBUzDxo,584
|
|
29
|
-
quick_agent/tools/filesystem.read_text/tool.json,sha256=UtkOxcpi6qYd611tJx-zFXVUjOwx2ZbmxEWsF4cAVpo,246
|
|
30
|
-
quick_agent/tools/filesystem.write_text/tool.json,sha256=EcGmB_M_KDTqOrnxk_NVhKRoW_KADFFksgJ7fmw472s,275
|
|
31
|
-
quick_agent-0.1.1.data/data/quick_agent/agents/function-spec-validator.md,sha256=Dlc4j1vFpDHj7w2vcWrvC83EEg_sWwwt1GOaRgcsm6Y,3289
|
|
32
|
-
quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validate-eval-list.md,sha256=K83L9BKvGcttPRINwD6VSPopEHnB_aw4tpfSI6KM2u8,4119
|
|
33
|
-
quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validator-contains.md,sha256=3VxtKpyogwcJ4Er5tQFyYNe2nJBtANErb5D-_snBATw,3115
|
|
34
|
-
quick_agent-0.1.1.data/data/quick_agent/agents/template.md,sha256=6IBuVZjK5OPIchI8ruvxao8CdLy0ZOi0meh6d1exI0E,2458
|
|
35
|
-
quick_agent-0.1.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
36
|
-
tests/test_agent.py,sha256=tcPpH9WmGPQjMo_jkFJlF8kgOZ3QktIvUHTAJaH1hWA,5533
|
|
37
|
-
tests/test_directory_permissions.py,sha256=RvehzVQeOYC2y4-aEHzdQVMz0yJfqhyUnfgAUNGu9RY,2489
|
|
38
|
-
tests/test_integration.py,sha256=MT_ANbQZ54NbcOtwXn6ZiYLiAa9byT_RDZXmHxV7fW4,5872
|
|
39
|
-
tests/test_orchestrator.py,sha256=CGbo6Q6Y4iSXzt2F9V1PYx34WzV8eI6aL2Olmr2zDOw,28118
|
|
40
|
-
tests/test_tools.py,sha256=Yc6AKCz79XJwHqiRV8dUBE6GVNdspUU0hIeGCXx-Rvc,936
|
|
41
|
-
quick_agent-0.1.1.dist-info/METADATA,sha256=Khq2FvREwc_kLjOQrKRshoUsZ545Wdr07E4t7hTuBxY,46615
|
|
42
|
-
quick_agent-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
43
|
-
quick_agent-0.1.1.dist-info/entry_points.txt,sha256=bij-xFaQMSrMj7zearzi-bMfcyweum_GvURFMVZGde8,53
|
|
44
|
-
quick_agent-0.1.1.dist-info/top_level.txt,sha256=KT1ID0FVC0OLzQnoBKRIHbXLRNugiU2gcgj_f4DtAsw,18
|
|
45
|
-
quick_agent-0.1.1.dist-info/RECORD,,
|
{quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/function-spec-validator.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|