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
quick_agent/__init__.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Public package exports."""
|
|
2
2
|
|
|
3
|
+
from quick_agent.input_adaptors import FileInput
|
|
4
|
+
from quick_agent.input_adaptors import InputAdaptor
|
|
5
|
+
from quick_agent.input_adaptors import TextInput
|
|
3
6
|
from quick_agent.orchestrator import Orchestrator
|
|
4
7
|
from quick_agent.quick_agent import QuickAgent
|
|
5
8
|
|
|
6
|
-
__all__ = ["Orchestrator", "QuickAgent"]
|
|
9
|
+
__all__ = ["FileInput", "InputAdaptor", "Orchestrator", "QuickAgent", "TextInput"]
|
quick_agent/agent_call_tool.py
CHANGED
|
@@ -7,11 +7,13 @@ from typing import Any, Awaitable, Callable
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from quick_agent.input_adaptors import InputAdaptor, TextInput
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class AgentCallTool:
|
|
12
14
|
def __init__(
|
|
13
15
|
self,
|
|
14
|
-
call_agent: Callable[[str, Path], Awaitable[BaseModel | str]],
|
|
16
|
+
call_agent: Callable[[str, InputAdaptor | Path], Awaitable[BaseModel | str]],
|
|
15
17
|
run_input_source_path: str,
|
|
16
18
|
) -> None:
|
|
17
19
|
self._call_agent = call_agent
|
|
@@ -32,13 +34,28 @@ class AgentCallTool:
|
|
|
32
34
|
path = base_dir / path
|
|
33
35
|
return path
|
|
34
36
|
|
|
35
|
-
async def __call__(
|
|
37
|
+
async def __call__(
|
|
38
|
+
self,
|
|
39
|
+
agent: str,
|
|
40
|
+
input_file: str | None = None,
|
|
41
|
+
input_text: str | None = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
36
43
|
"""
|
|
37
|
-
Call another agent by ID with an input file path.
|
|
44
|
+
Call another agent by ID with an input file path or inline text.
|
|
38
45
|
Returns JSON-serializable dict output if structured, else {"text": "..."}.
|
|
39
46
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
if input_file and input_text:
|
|
48
|
+
raise ValueError("Provide only one of input_file or input_text.")
|
|
49
|
+
if not input_file and input_text is None:
|
|
50
|
+
raise ValueError("Provide either input_file or input_text.")
|
|
51
|
+
if input_text is not None:
|
|
52
|
+
input_data: InputAdaptor | Path = TextInput(input_text)
|
|
53
|
+
else:
|
|
54
|
+
if input_file is None:
|
|
55
|
+
raise ValueError("Provide either input_file or input_text.")
|
|
56
|
+
resolved_input = self._resolve_input_file(input_file)
|
|
57
|
+
input_data = resolved_input
|
|
58
|
+
out = await self._call_agent(agent, input_data)
|
|
42
59
|
if isinstance(out, BaseModel):
|
|
43
60
|
return out.model_dump()
|
|
44
61
|
return {"text": out}
|
quick_agent/agent_registry.py
CHANGED
|
@@ -37,8 +37,8 @@ def load_agent_file(path: Path) -> LoadedAgentFile:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class AgentRegistry:
|
|
40
|
-
def __init__(self, agent_roots: list[Path]):
|
|
41
|
-
self.agent_roots = agent_roots
|
|
40
|
+
def __init__(self, agent_roots: list[Path]) -> None:
|
|
41
|
+
self.agent_roots: list[Path] = agent_roots
|
|
42
42
|
self._cache: dict[str, LoadedAgentFile] = {}
|
|
43
43
|
self._index: dict[str, Path] | None = None
|
|
44
44
|
|
quick_agent/agent_tools.py
CHANGED
|
@@ -10,12 +10,13 @@ from pydantic_ai.toolsets import FunctionToolset
|
|
|
10
10
|
|
|
11
11
|
from quick_agent.agent_call_tool import AgentCallTool
|
|
12
12
|
from quick_agent.directory_permissions import DirectoryPermissions
|
|
13
|
+
from quick_agent.input_adaptors import InputAdaptor
|
|
13
14
|
from quick_agent.tools_loader import load_tools
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class AgentTools:
|
|
17
18
|
def __init__(self, tool_roots: list[Path]) -> None:
|
|
18
|
-
self._tool_roots = tool_roots
|
|
19
|
+
self._tool_roots: list[Path] = tool_roots
|
|
19
20
|
|
|
20
21
|
def build_toolset(self, tool_ids: list[str], permissions: DirectoryPermissions) -> FunctionToolset[Any]:
|
|
21
22
|
tool_ids_for_disk = [tool_id for tool_id in tool_ids if tool_id != "agent.call"]
|
|
@@ -28,7 +29,7 @@ class AgentTools:
|
|
|
28
29
|
tool_ids: list[str],
|
|
29
30
|
toolset: FunctionToolset[Any],
|
|
30
31
|
run_input_source_path: str,
|
|
31
|
-
call_agent: Callable[[str, Path], Awaitable[BaseModel | str]],
|
|
32
|
+
call_agent: Callable[[str, InputAdaptor | Path], Awaitable[BaseModel | str]],
|
|
32
33
|
) -> None:
|
|
33
34
|
if "agent.call" not in tool_ids:
|
|
34
35
|
return
|
quick_agent/cli.py
CHANGED
|
@@ -7,16 +7,28 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from quick_agent.input_adaptors import InputAdaptor, TextInput
|
|
10
11
|
from quick_agent.orchestrator import Orchestrator
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
async def run_agent(
|
|
15
|
+
orch: Orchestrator,
|
|
16
|
+
agent_id: str,
|
|
17
|
+
input_adaptor: InputAdaptor | Path,
|
|
18
|
+
extra_tools: list[str],
|
|
19
|
+
) -> BaseModel | str:
|
|
20
|
+
return await orch.run(agent_id, input_adaptor, extra_tools=extra_tools)
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
def main() -> None:
|
|
14
24
|
parser = argparse.ArgumentParser()
|
|
15
25
|
parser.add_argument("--agents-dir", type=str, default="agents")
|
|
16
26
|
parser.add_argument("--tools-dir", type=str, default="tools")
|
|
17
27
|
parser.add_argument("--safe-dir", type=str, default="safe")
|
|
18
28
|
parser.add_argument("--agent", type=str, required=True)
|
|
19
|
-
parser.
|
|
29
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
30
|
+
input_group.add_argument("--input", type=str, help="Path to an input file")
|
|
31
|
+
input_group.add_argument("--input-text", type=str, help="Raw input text")
|
|
20
32
|
parser.add_argument("--tool", action="append", default=[], help="Extra tool IDs to add at runtime")
|
|
21
33
|
args = parser.parse_args()
|
|
22
34
|
|
|
@@ -30,14 +42,16 @@ def main() -> None:
|
|
|
30
42
|
tool_roots = [user_tools_dir, system_tools_dir]
|
|
31
43
|
|
|
32
44
|
orch = Orchestrator(agent_roots, tool_roots, Path(args.safe_dir))
|
|
45
|
+
input_adaptor: InputAdaptor | Path
|
|
46
|
+
if args.input_text is not None:
|
|
47
|
+
input_adaptor = TextInput(args.input_text)
|
|
48
|
+
else:
|
|
49
|
+
input_adaptor = Path(args.input)
|
|
33
50
|
|
|
34
51
|
# Async entrypoint
|
|
35
52
|
import anyio
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
return await orch.run(args.agent, Path(args.input), extra_tools=args.tool)
|
|
39
|
-
|
|
40
|
-
out = anyio.run(runner)
|
|
54
|
+
out = anyio.run(run_agent, orch, args.agent, input_adaptor, args.tool)
|
|
41
55
|
if isinstance(out, BaseModel):
|
|
42
56
|
print(out.model_dump_json(indent=2))
|
|
43
57
|
else:
|
|
@@ -6,14 +6,16 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class DirectoryPermissions:
|
|
9
|
-
def __init__(self, root: Path) -> None:
|
|
10
|
-
self._root = root.expanduser().resolve(strict=False)
|
|
9
|
+
def __init__(self, root: Path | None) -> None:
|
|
10
|
+
self._root = root.expanduser().resolve(strict=False) if root is not None else None
|
|
11
11
|
|
|
12
12
|
@property
|
|
13
|
-
def root(self) -> Path:
|
|
13
|
+
def root(self) -> Path | None:
|
|
14
14
|
return self._root
|
|
15
15
|
|
|
16
16
|
def scoped(self, directory: str | None) -> "DirectoryPermissions":
|
|
17
|
+
if self._root is None:
|
|
18
|
+
return self
|
|
17
19
|
if directory:
|
|
18
20
|
candidate = (self._root / directory).expanduser().resolve(strict=False)
|
|
19
21
|
root_resolved = self._root.expanduser().resolve(strict=False)
|
|
@@ -23,6 +25,8 @@ class DirectoryPermissions:
|
|
|
23
25
|
return self
|
|
24
26
|
|
|
25
27
|
def resolve(self, path: Path, *, for_write: bool) -> Path:
|
|
28
|
+
if self._root is None:
|
|
29
|
+
raise PermissionError("No safe directory configured; reads and writes are denied.")
|
|
26
30
|
target = path
|
|
27
31
|
if not target.is_absolute():
|
|
28
32
|
target = self._root / target
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Input adaptors for agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from quick_agent.directory_permissions import DirectoryPermissions
|
|
8
|
+
from quick_agent.io_utils import load_input
|
|
9
|
+
from quick_agent.models.run_input import RunInput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InputAdaptor:
|
|
13
|
+
def load(self) -> RunInput:
|
|
14
|
+
raise NotImplementedError("InputAdaptor.load must be implemented by subclasses.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileInput(InputAdaptor):
|
|
18
|
+
def __init__(self, path: Path, permissions: DirectoryPermissions) -> None:
|
|
19
|
+
self._run_input = load_input(path, permissions)
|
|
20
|
+
|
|
21
|
+
def load(self) -> RunInput:
|
|
22
|
+
return self._run_input
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TextInput(InputAdaptor):
|
|
26
|
+
def __init__(self, text: str) -> None:
|
|
27
|
+
self._text = text
|
|
28
|
+
|
|
29
|
+
def load(self) -> RunInput:
|
|
30
|
+
return RunInput(source_path="inline_input.txt", kind="text", text=self._text, data=None)
|
quick_agent/llms.txt
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Quick Agent (llms.txt)
|
|
2
|
+
|
|
3
|
+
**Project Summary**
|
|
4
|
+
- Quick Agent is a minimal, local-first agent runner that loads agent definitions from Markdown front matter and executes a small chain of steps with bounded context.
|
|
5
|
+
- Agents are Markdown files with YAML front matter for model/tools/chain and `## step:<id>` sections for prompts.
|
|
6
|
+
- Execution is deterministic: steps run in order, state stores prior step outputs, final output is written to disk, and optional handoff can invoke another agent.
|
|
7
|
+
|
|
8
|
+
**Primary Entry Points**
|
|
9
|
+
- CLI: `quick-agent --agent <id> --input <path>` (module: `quick_agent.cli:main`).
|
|
10
|
+
- Python API: `quick_agent.orchestrator.Orchestrator` and `quick_agent.quick_agent.QuickAgent`.
|
|
11
|
+
|
|
12
|
+
**How It Works (High Level)**
|
|
13
|
+
- Agent files are loaded by `AgentRegistry` from user and packaged directories.
|
|
14
|
+
- `QuickAgent` loads the agent spec, scopes file permissions, builds the toolset, then runs each chain step (text or structured).
|
|
15
|
+
- Structured steps validate JSON output against a Pydantic schema and attempt extraction if the raw output contains extra text.
|
|
16
|
+
- Outputs are written via `io_utils.write_output` and constrained by `DirectoryPermissions`.
|
|
17
|
+
- Input handling uses adaptors: file inputs are permission-checked at creation, and text inputs bypass filesystem access.
|
|
18
|
+
|
|
19
|
+
**Key Files**
|
|
20
|
+
- `src/quick_agent/quick_agent.py`: core execution engine and step handling.
|
|
21
|
+
- `src/quick_agent/orchestrator.py`: convenience wrapper for running agents.
|
|
22
|
+
- `src/quick_agent/agent_registry.py`: loads agent Markdown files.
|
|
23
|
+
- `src/quick_agent/agent_tools.py`: loads tools and builds the toolset.
|
|
24
|
+
- `src/quick_agent/directory_permissions.py`: enforces safe directory access.
|
|
25
|
+
- `src/quick_agent/tools/**/tool.json`: tool definitions (id, module, function).
|
|
26
|
+
- `agents/`: example agents shipped with the repo.
|
|
27
|
+
- `docs/`: CLI, template, and Python usage documentation.
|
|
28
|
+
|
|
29
|
+
**Agent File Format (Essentials)**
|
|
30
|
+
- Required fields: `name`, `model.base_url`, `model.model_name`, `chain`.
|
|
31
|
+
- Each `chain` step must reference a matching `## step:<id>` section in the body.
|
|
32
|
+
- Tools are referenced by id and resolved from `tool.json` definitions.
|
|
33
|
+
- `safe_dir` must be relative and further scopes file access inside the CLI safe root.
|
|
34
|
+
|
|
35
|
+
**Safety and Permissions**
|
|
36
|
+
- File reads and writes are restricted to a safe directory configured by `--safe-dir` (default `safe`).
|
|
37
|
+
- Agent-level `safe_dir` can further restrict access to a subdirectory.
|
|
38
|
+
- If no safe directory is configured, all reads and writes are denied.
|
|
39
|
+
|
|
40
|
+
**Tests**
|
|
41
|
+
- Tests live in `src/tests`.
|
|
42
|
+
- Run with `pytest` (requires optional dependency group `test`).
|
|
43
|
+
|
|
44
|
+
**Useful Docs**
|
|
45
|
+
- `docs/cli.md`: CLI usage and options.
|
|
46
|
+
- `docs/templates.md`: agent template format and examples.
|
|
47
|
+
- `docs/python.md`: embedding the orchestrator and inter-agent calls.
|
|
48
|
+
- `docs/state.md`: how chain state is stored and used.
|
|
49
|
+
|
|
50
|
+
**Mini Example Agent**
|
|
51
|
+
```markdown
|
|
52
|
+
---
|
|
53
|
+
name: "Hello Agent"
|
|
54
|
+
description: "Minimal example"
|
|
55
|
+
model:
|
|
56
|
+
provider: "openai-compatible"
|
|
57
|
+
base_url: "http://localhost:11434/v1"
|
|
58
|
+
api_key_env: "OPENAI_API_KEY"
|
|
59
|
+
model_name: "llama3"
|
|
60
|
+
chain:
|
|
61
|
+
- id: hello
|
|
62
|
+
kind: text
|
|
63
|
+
prompt_section: step:hello
|
|
64
|
+
output:
|
|
65
|
+
format: json
|
|
66
|
+
file: out/hello.json
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## step:hello
|
|
70
|
+
|
|
71
|
+
Say hello to the input.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Mini Example (Structured Output)**
|
|
75
|
+
```markdown
|
|
76
|
+
---
|
|
77
|
+
name: "Structured Agent"
|
|
78
|
+
model:
|
|
79
|
+
provider: "openai-compatible"
|
|
80
|
+
base_url: "http://localhost:11434/v1"
|
|
81
|
+
api_key_env: "OPENAI_API_KEY"
|
|
82
|
+
model_name: "llama3"
|
|
83
|
+
schemas:
|
|
84
|
+
Summary: "quick_agent.schemas.outputs:SummaryOutput"
|
|
85
|
+
chain:
|
|
86
|
+
- id: summarize
|
|
87
|
+
kind: structured
|
|
88
|
+
prompt_section: step:summarize
|
|
89
|
+
output_schema: Summary
|
|
90
|
+
output:
|
|
91
|
+
format: json
|
|
92
|
+
file: out/summary.json
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## step:summarize
|
|
96
|
+
|
|
97
|
+
Summarize the input into a short title and 2 bullet points.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Schema Snippet (for Structured Output)**
|
|
101
|
+
```python
|
|
102
|
+
from pydantic import BaseModel
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SummaryOutput(BaseModel):
|
|
106
|
+
title: str
|
|
107
|
+
bullets: list[str]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Input Adaptors (Python)**
|
|
111
|
+
```python
|
|
112
|
+
from pathlib import Path
|
|
113
|
+
|
|
114
|
+
from quick_agent import FileInput
|
|
115
|
+
from quick_agent import Orchestrator
|
|
116
|
+
from quick_agent import TextInput
|
|
117
|
+
|
|
118
|
+
orchestrator = Orchestrator([Path("agents")], [Path("tools")], safe_dir=Path("safe"))
|
|
119
|
+
|
|
120
|
+
file_input = FileInput(Path("safe/input.txt"), orchestrator.directory_permissions)
|
|
121
|
+
text_input = TextInput("hello from memory")
|
|
122
|
+
|
|
123
|
+
result_from_file = await orchestrator.run("example", file_input)
|
|
124
|
+
result_from_text = await orchestrator.run("example", text_input)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Multi-Step Example (State + Structured Output)**
|
|
128
|
+
```markdown
|
|
129
|
+
---
|
|
130
|
+
name: "Draft And Summarize"
|
|
131
|
+
model:
|
|
132
|
+
provider: "openai-compatible"
|
|
133
|
+
base_url: "http://localhost:11434/v1"
|
|
134
|
+
api_key_env: "OPENAI_API_KEY"
|
|
135
|
+
model_name: "llama3"
|
|
136
|
+
schemas:
|
|
137
|
+
Summary: "quick_agent.schemas.outputs:SummaryOutput"
|
|
138
|
+
chain:
|
|
139
|
+
- id: draft
|
|
140
|
+
kind: text
|
|
141
|
+
prompt_section: step:draft
|
|
142
|
+
- id: summarize
|
|
143
|
+
kind: structured
|
|
144
|
+
prompt_section: step:summarize
|
|
145
|
+
output_schema: Summary
|
|
146
|
+
output:
|
|
147
|
+
format: json
|
|
148
|
+
file: out/draft_summary.json
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## step:draft
|
|
152
|
+
|
|
153
|
+
Write a short draft based on the input.
|
|
154
|
+
|
|
155
|
+
## step:summarize
|
|
156
|
+
|
|
157
|
+
Summarize the draft from state as a title and 2 bullets.
|
|
158
|
+
Use the value at state.steps.draft.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Inter-Agent Call Example (agent.call)**
|
|
162
|
+
```markdown
|
|
163
|
+
---
|
|
164
|
+
name: "Parent Agent"
|
|
165
|
+
tools:
|
|
166
|
+
- "agent.call"
|
|
167
|
+
nested_output: inline
|
|
168
|
+
chain:
|
|
169
|
+
- id: invoke_child
|
|
170
|
+
kind: text
|
|
171
|
+
prompt_section: step:invoke_child
|
|
172
|
+
output:
|
|
173
|
+
format: json
|
|
174
|
+
file: out/parent.json
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## step:invoke_child
|
|
178
|
+
|
|
179
|
+
Call agent_call with agent "child" and input_file "{base_directory}/child_input.txt".
|
|
180
|
+
Then respond with only the returned text value.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```markdown
|
|
184
|
+
---
|
|
185
|
+
name: "Child Agent"
|
|
186
|
+
chain:
|
|
187
|
+
- id: respond
|
|
188
|
+
kind: text
|
|
189
|
+
prompt_section: step:respond
|
|
190
|
+
output:
|
|
191
|
+
format: json
|
|
192
|
+
file: out/child.json
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## step:respond
|
|
196
|
+
|
|
197
|
+
Reply with exactly: pong
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Nested calls default to `nested_output: inline`, so the child agent above will not write
|
|
201
|
+
`out/child.json` unless the parent sets `nested_output: file`.
|
|
202
|
+
|
|
203
|
+
**Handoff Example (Run Another Agent After Output)**
|
|
204
|
+
```markdown
|
|
205
|
+
---
|
|
206
|
+
name: "Writer With Handoff"
|
|
207
|
+
chain:
|
|
208
|
+
- id: write
|
|
209
|
+
kind: text
|
|
210
|
+
prompt_section: step:write
|
|
211
|
+
output:
|
|
212
|
+
format: json
|
|
213
|
+
file: out/writer.json
|
|
214
|
+
handoff:
|
|
215
|
+
enabled: true
|
|
216
|
+
agent_id: "reviewer"
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## step:write
|
|
220
|
+
|
|
221
|
+
Write a short answer to the input.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```markdown
|
|
225
|
+
---
|
|
226
|
+
name: "Reviewer"
|
|
227
|
+
chain:
|
|
228
|
+
- id: review
|
|
229
|
+
kind: text
|
|
230
|
+
prompt_section: step:review
|
|
231
|
+
output:
|
|
232
|
+
format: json
|
|
233
|
+
file: out/reviewer.json
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## step:review
|
|
237
|
+
|
|
238
|
+
Review the inline output produced by the writer and provide a brief critique.
|
|
239
|
+
```
|
quick_agent/models/agent_spec.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
5
7
|
from pydantic import BaseModel, Field
|
|
6
8
|
|
|
7
9
|
from quick_agent.models.chain_step_spec import ChainStepSpec
|
|
@@ -19,4 +21,5 @@ class AgentSpec(BaseModel):
|
|
|
19
21
|
chain: list[ChainStepSpec]
|
|
20
22
|
output: OutputSpec = Field(default_factory=OutputSpec)
|
|
21
23
|
handoff: HandoffSpec = Field(default_factory=HandoffSpec)
|
|
24
|
+
nested_output: Literal["inline", "file"] = "inline"
|
|
22
25
|
safe_dir: str | None = None
|
quick_agent/orchestrator.py
CHANGED
|
@@ -3,33 +3,40 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
9
10
|
from quick_agent.agent_registry import AgentRegistry
|
|
10
11
|
from quick_agent.agent_tools import AgentTools
|
|
11
12
|
from quick_agent.directory_permissions import DirectoryPermissions
|
|
13
|
+
from quick_agent.input_adaptors import InputAdaptor
|
|
12
14
|
from quick_agent.quick_agent import QuickAgent
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class Orchestrator:
|
|
16
18
|
def __init__(
|
|
17
19
|
self,
|
|
18
|
-
agent_roots: list[Path],
|
|
19
|
-
tool_roots: list[Path],
|
|
20
|
-
safe_dir: Path,
|
|
20
|
+
agent_roots: list[Path] | None = None,
|
|
21
|
+
tool_roots: list[Path] | None = None,
|
|
22
|
+
safe_dir: Optional[Path] = None,
|
|
21
23
|
) -> None:
|
|
22
|
-
self.registry = AgentRegistry(agent_roots)
|
|
23
|
-
self.tools = AgentTools(tool_roots)
|
|
24
|
-
self.directory_permissions = DirectoryPermissions(safe_dir)
|
|
24
|
+
self.registry: AgentRegistry = AgentRegistry(agent_roots or [])
|
|
25
|
+
self.tools: AgentTools = AgentTools(tool_roots or [])
|
|
26
|
+
self.directory_permissions: DirectoryPermissions = DirectoryPermissions(safe_dir)
|
|
25
27
|
|
|
26
|
-
async def run(
|
|
28
|
+
async def run(
|
|
29
|
+
self,
|
|
30
|
+
agent_id: str,
|
|
31
|
+
input_data: InputAdaptor | Path,
|
|
32
|
+
extra_tools: list[str] | None = None,
|
|
33
|
+
) -> BaseModel | str:
|
|
27
34
|
agent = QuickAgent(
|
|
28
35
|
registry=self.registry,
|
|
29
36
|
tools=self.tools,
|
|
30
37
|
directory_permissions=self.directory_permissions,
|
|
31
38
|
agent_id=agent_id,
|
|
32
|
-
|
|
39
|
+
input_data=input_data,
|
|
33
40
|
extra_tools=extra_tools,
|
|
34
41
|
)
|
|
35
42
|
return await agent.run()
|
quick_agent/prompting.py
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, Mapping
|
|
7
7
|
|
|
8
8
|
from quick_agent.models.run_input import RunInput
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def make_user_prompt(step_prompt: str, run_input: RunInput, state:
|
|
11
|
+
def make_user_prompt(step_prompt: str, run_input: RunInput, state: Mapping[str, Any]) -> str:
|
|
12
12
|
"""
|
|
13
13
|
Creates a consistent user prompt payload. Consistency helps prefix-caching backends.
|
|
14
14
|
"""
|
quick_agent/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561 type hints.
|