quick-agent 0.1.1__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quick_agent-0.1.1 → quick_agent-0.1.3}/PKG-INFO +21 -4
- {quick_agent-0.1.1 → quick_agent-0.1.3}/README.md +18 -3
- quick_agent-0.1.3/agents/business-extract-structured.md +49 -0
- quick_agent-0.1.3/agents/business-extract.md +42 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/function-spec-validator.md +1 -1
- {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/subagent-validate-eval-list.md +1 -1
- {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/subagent-validator-contains.md +8 -1
- {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/template.md +12 -1
- {quick_agent-0.1.1 → quick_agent-0.1.3}/pyproject.toml +4 -2
- quick_agent-0.1.3/src/quick_agent/__init__.py +9 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/agent_call_tool.py +22 -5
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/agent_registry.py +7 -27
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/agent_tools.py +3 -2
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/cli.py +19 -5
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/directory_permissions.py +7 -3
- quick_agent-0.1.3/src/quick_agent/input_adaptors.py +30 -0
- quick_agent-0.1.3/src/quick_agent/llms.txt +239 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/agent_spec.py +3 -0
- quick_agent-0.1.3/src/quick_agent/models/loaded_agent_file.py +149 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/output_spec.py +1 -1
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/orchestrator.py +15 -8
- quick_agent-0.1.3/src/quick_agent/prompting.py +46 -0
- quick_agent-0.1.3/src/quick_agent/py.typed +1 -0
- quick_agent-0.1.3/src/quick_agent/quick_agent.py +329 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/schemas/outputs.py +6 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/PKG-INFO +21 -4
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/SOURCES.txt +7 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/requires.txt +2 -0
- quick_agent-0.1.3/src/tests/test_agent.py +460 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_directory_permissions.py +10 -0
- quick_agent-0.1.3/src/tests/test_httpx_tools.py +295 -0
- quick_agent-0.1.3/src/tests/test_input_adaptors.py +31 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_integration.py +134 -1
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_orchestrator.py +525 -111
- quick_agent-0.1.1/src/quick_agent/__init__.py +0 -6
- quick_agent-0.1.1/src/quick_agent/models/loaded_agent_file.py +0 -14
- quick_agent-0.1.1/src/quick_agent/prompting.py +0 -28
- quick_agent-0.1.1/src/quick_agent/quick_agent.py +0 -313
- quick_agent-0.1.1/src/tests/test_agent.py +0 -196
- {quick_agent-0.1.1 → quick_agent-0.1.3}/LICENSE +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/setup.cfg +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/io_utils.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/json_utils.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/__init__.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/chain_step_spec.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/handoff_spec.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/model_spec.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/run_input.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/tool_impl_spec.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/tool_json.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/__init__.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/__init__.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/adapter.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/read_text.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/write_text.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem.read_text/tool.json +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem.write_text/tool.json +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools_loader.py +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/dependency_links.txt +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/entry_points.txt +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/top_level.txt +0 -0
- {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quick-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Minimal, local-first agent runner using Markdown front matter.
|
|
5
5
|
Author-email: Charles Verge <1906614+charlesverge@users.noreply.github.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -700,6 +700,8 @@ Requires-Dist: anyio
|
|
|
700
700
|
Requires-Dist: pydantic
|
|
701
701
|
Requires-Dist: pydantic-ai
|
|
702
702
|
Requires-Dist: python-frontmatter
|
|
703
|
+
Requires-Dist: types-PyYAML
|
|
704
|
+
Requires-Dist: PyYAML
|
|
703
705
|
Provides-Extra: dev
|
|
704
706
|
Requires-Dist: mypy; extra == "dev"
|
|
705
707
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -707,13 +709,14 @@ Provides-Extra: test
|
|
|
707
709
|
Requires-Dist: pytest; extra == "test"
|
|
708
710
|
Dynamic: license-file
|
|
709
711
|
|
|
710
|
-
#
|
|
712
|
+
# Quick Agent
|
|
711
713
|
|
|
712
|
-
|
|
714
|
+
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 limited context handling. It is intentionally small and explicit: you define the model, tools, and steps in a single Markdown file, and the orchestrator runs those steps in order with a bounded prompt preamble.
|
|
713
715
|
|
|
714
716
|
## Project Goal
|
|
715
717
|
|
|
716
718
|
Provide a simple, maintainable agent framework that:
|
|
719
|
+
|
|
717
720
|
- Uses Markdown front matter for agent configuration.
|
|
718
721
|
- Runs a deterministic chain of steps (text or structured output).
|
|
719
722
|
- Keeps context handling deliberately limited and predictable.
|
|
@@ -887,7 +890,7 @@ def main() -> None:
|
|
|
887
890
|
tools=tools,
|
|
888
891
|
directory_permissions=permissions,
|
|
889
892
|
agent_id="hello",
|
|
890
|
-
|
|
893
|
+
input_data=Path("safe/path/to/input.txt"),
|
|
891
894
|
extra_tools=None,
|
|
892
895
|
)
|
|
893
896
|
|
|
@@ -907,6 +910,18 @@ Agents are stored as Markdown files with YAML front matter and step sections:
|
|
|
907
910
|
- Body contains `## step:<id>` sections referenced by the chain.
|
|
908
911
|
|
|
909
912
|
The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
|
|
913
|
+
If the agent front matter omits `output.file`, the orchestrator returns the final output without writing a file.
|
|
914
|
+
|
|
915
|
+
## Nested Output
|
|
916
|
+
|
|
917
|
+
When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
|
|
918
|
+
own `output.file` or return output inline only. Configure this in the parent agent front matter:
|
|
919
|
+
|
|
920
|
+
```yaml
|
|
921
|
+
nested_output: inline # default, no output file for nested calls
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Use `nested_output: file` to allow nested agents to write their configured output files.
|
|
910
925
|
|
|
911
926
|
## Documentation
|
|
912
927
|
|
|
@@ -914,5 +929,7 @@ See the docs in `docs/`:
|
|
|
914
929
|
|
|
915
930
|
- [docs/cli.md](docs/cli.md): Command line usage and options.
|
|
916
931
|
- [docs/templates.md](docs/templates.md): Agent template format and examples.
|
|
932
|
+
- [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
|
|
917
933
|
- [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
|
|
918
934
|
- [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
|
|
935
|
+
- [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Quick Agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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 limited context handling. It is intentionally small and explicit: you define the model, tools, and steps in a single Markdown file, and the orchestrator runs those steps in order with a bounded prompt preamble.
|
|
4
4
|
|
|
5
5
|
## Project Goal
|
|
6
6
|
|
|
7
7
|
Provide a simple, maintainable agent framework that:
|
|
8
|
+
|
|
8
9
|
- Uses Markdown front matter for agent configuration.
|
|
9
10
|
- Runs a deterministic chain of steps (text or structured output).
|
|
10
11
|
- Keeps context handling deliberately limited and predictable.
|
|
@@ -178,7 +179,7 @@ def main() -> None:
|
|
|
178
179
|
tools=tools,
|
|
179
180
|
directory_permissions=permissions,
|
|
180
181
|
agent_id="hello",
|
|
181
|
-
|
|
182
|
+
input_data=Path("safe/path/to/input.txt"),
|
|
182
183
|
extra_tools=None,
|
|
183
184
|
)
|
|
184
185
|
|
|
@@ -198,6 +199,18 @@ Agents are stored as Markdown files with YAML front matter and step sections:
|
|
|
198
199
|
- Body contains `## step:<id>` sections referenced by the chain.
|
|
199
200
|
|
|
200
201
|
The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
|
|
202
|
+
If the agent front matter omits `output.file`, the orchestrator returns the final output without writing a file.
|
|
203
|
+
|
|
204
|
+
## Nested Output
|
|
205
|
+
|
|
206
|
+
When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
|
|
207
|
+
own `output.file` or return output inline only. Configure this in the parent agent front matter:
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
nested_output: inline # default, no output file for nested calls
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use `nested_output: file` to allow nested agents to write their configured output files.
|
|
201
214
|
|
|
202
215
|
## Documentation
|
|
203
216
|
|
|
@@ -205,5 +218,7 @@ See the docs in `docs/`:
|
|
|
205
218
|
|
|
206
219
|
- [docs/cli.md](docs/cli.md): Command line usage and options.
|
|
207
220
|
- [docs/templates.md](docs/templates.md): Agent template format and examples.
|
|
221
|
+
- [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
|
|
208
222
|
- [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
|
|
209
223
|
- [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
|
|
224
|
+
- [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "Business Extract Structured"
|
|
3
|
+
description: "Extract company name, location, and summary into structured JSON."
|
|
4
|
+
model:
|
|
5
|
+
provider: "openai-compatible"
|
|
6
|
+
base_url: "http://localhost:11434/v1"
|
|
7
|
+
api_key_env: "OPENAI_API_KEY"
|
|
8
|
+
model_name: "llama3"
|
|
9
|
+
schemas:
|
|
10
|
+
BusinessSummary: "quick_agent.schemas.outputs:BusinessSummary"
|
|
11
|
+
chain:
|
|
12
|
+
- id: company_name
|
|
13
|
+
kind: text
|
|
14
|
+
prompt_section: step:company_name
|
|
15
|
+
- id: location
|
|
16
|
+
kind: text
|
|
17
|
+
prompt_section: step:location
|
|
18
|
+
- id: summary
|
|
19
|
+
kind: structured
|
|
20
|
+
prompt_section: step:summary
|
|
21
|
+
output_schema: BusinessSummary
|
|
22
|
+
output:
|
|
23
|
+
format: "json"
|
|
24
|
+
file: "out/business_extract_structured.json"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Instructions
|
|
28
|
+
|
|
29
|
+
Extract structured details from the input description.
|
|
30
|
+
|
|
31
|
+
## step:company_name
|
|
32
|
+
|
|
33
|
+
Extract the company name from the input description.
|
|
34
|
+
Return only the company name.
|
|
35
|
+
|
|
36
|
+
## step:location
|
|
37
|
+
|
|
38
|
+
Extract the location from the input description.
|
|
39
|
+
If a city and region are present, include both.
|
|
40
|
+
Return only the location.
|
|
41
|
+
|
|
42
|
+
## step:summary
|
|
43
|
+
|
|
44
|
+
Return a JSON object with:
|
|
45
|
+
- `company_name`
|
|
46
|
+
- `location`
|
|
47
|
+
- `summary` (one sentence)
|
|
48
|
+
|
|
49
|
+
Use `state.steps.company_name` and `state.steps.location` for the fields if available.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "Business Extract"
|
|
3
|
+
description: "Extract company name, location, and a short summary from a business description."
|
|
4
|
+
model:
|
|
5
|
+
provider: "openai-compatible"
|
|
6
|
+
base_url: "http://localhost:11434/v1"
|
|
7
|
+
api_key_env: "OPENAI_API_KEY"
|
|
8
|
+
model_name: "llama3"
|
|
9
|
+
chain:
|
|
10
|
+
- id: company_name
|
|
11
|
+
kind: text
|
|
12
|
+
prompt_section: step:company_name
|
|
13
|
+
- id: location
|
|
14
|
+
kind: text
|
|
15
|
+
prompt_section: step:location
|
|
16
|
+
- id: summary
|
|
17
|
+
kind: text
|
|
18
|
+
prompt_section: step:summary
|
|
19
|
+
output:
|
|
20
|
+
format: "markdown"
|
|
21
|
+
file: "out/business_extract.md"
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Instructions
|
|
25
|
+
|
|
26
|
+
Extract structured details from the input description.
|
|
27
|
+
|
|
28
|
+
## step:company_name
|
|
29
|
+
|
|
30
|
+
Extract the company name from the input description.
|
|
31
|
+
Return only the company name.
|
|
32
|
+
|
|
33
|
+
## step:location
|
|
34
|
+
|
|
35
|
+
Extract the location from the input description.
|
|
36
|
+
If a city and region are present, include both.
|
|
37
|
+
Return only the location.
|
|
38
|
+
|
|
39
|
+
## step:summary
|
|
40
|
+
|
|
41
|
+
Write one sentence summarizing the business.
|
|
42
|
+
Use `state.steps.company_name` and `state.steps.location` if available.
|
|
@@ -49,7 +49,7 @@ handoff:
|
|
|
49
49
|
input_mode: "final_output_json"
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
## Instructions
|
|
53
53
|
|
|
54
54
|
You are an EVAL LIST EXECUTOR. Your sole responsibility is reading an eval list file, executing each test agent, validating responses, and producing a results summary. You do NOT perform the tasks yourself—you delegate them entirely.
|
|
55
55
|
|
|
@@ -47,7 +47,7 @@ handoff:
|
|
|
47
47
|
input_mode: "final_output_json"
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Instructions
|
|
51
51
|
|
|
52
52
|
You are a RESPONSE VALIDATOR. Your sole responsibility is comparing a response against expected text patterns defined in an eval.md file. You report PASS or FAIL with detailed results.
|
|
53
53
|
|
|
@@ -56,10 +56,12 @@ You are a RESPONSE VALIDATOR. Your sole responsibility is comparing a response a
|
|
|
56
56
|
### Step 1: Parse Input
|
|
57
57
|
|
|
58
58
|
Extract from the user's request:
|
|
59
|
+
|
|
59
60
|
- Response Text: the text to validate from a provided file path
|
|
60
61
|
- Eval File Path: path to the markdown file containing expected text patterns
|
|
61
62
|
|
|
62
63
|
Input formats accepted:
|
|
64
|
+
|
|
63
65
|
- `validate "{response-file.md}" against {path/to/eval.md}`
|
|
64
66
|
- `check response in {response-file.md} contains {eval.md}`
|
|
65
67
|
- Direct response text followed by eval file path
|
|
@@ -86,26 +88,31 @@ Output a structured validation report and a PASS/FAIL status.
|
|
|
86
88
|
## step:plan
|
|
87
89
|
|
|
88
90
|
Goal:
|
|
91
|
+
|
|
89
92
|
- Identify response path/text and eval file path from the input.
|
|
90
93
|
- Outline the minimal steps.
|
|
91
94
|
|
|
92
95
|
Constraints:
|
|
96
|
+
|
|
93
97
|
- Keep it short.
|
|
94
98
|
|
|
95
99
|
## step:execute
|
|
96
100
|
|
|
97
101
|
Goal:
|
|
102
|
+
|
|
98
103
|
- Read the response file (if a path is provided).
|
|
99
104
|
- Read the eval file.
|
|
100
105
|
- Check for each expected text line in the response.
|
|
101
106
|
- Note missing lines.
|
|
102
107
|
|
|
103
108
|
Constraints:
|
|
109
|
+
|
|
104
110
|
- Do not output JSON in this step.
|
|
105
111
|
|
|
106
112
|
## step:finalize
|
|
107
113
|
|
|
108
114
|
Goal:
|
|
115
|
+
|
|
109
116
|
- Return a `ContainsValidationResult` JSON object with:
|
|
110
117
|
- `status`: PASS if all expected lines are present, otherwise FAIL
|
|
111
118
|
- `checks`: list of per-line results
|
|
@@ -52,7 +52,13 @@ handoff:
|
|
|
52
52
|
input_mode: "final_output_json" # or "final_output_markdown"
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# System Prompt
|
|
56
|
+
|
|
57
|
+
This is a system prompt to included in every run
|
|
58
|
+
|
|
59
|
+
## Instructions
|
|
60
|
+
|
|
61
|
+
Instructions are only included in first run.
|
|
56
62
|
|
|
57
63
|
You are a reliable pipeline agent.
|
|
58
64
|
You must follow the chain steps in order.
|
|
@@ -61,26 +67,31 @@ You may call tools as needed. If you call `agent.call`, wait for the response an
|
|
|
61
67
|
## step:plan
|
|
62
68
|
|
|
63
69
|
Goal:
|
|
70
|
+
|
|
64
71
|
- Read the provided input (a JSON or Markdown/text file) embedded by the orchestrator.
|
|
65
72
|
- Produce a structured **Plan** that lists concrete actions and any tool calls required.
|
|
66
73
|
|
|
67
74
|
Constraints:
|
|
75
|
+
|
|
68
76
|
- Keep steps explicit.
|
|
69
77
|
- If you need another agent, call `agent.call` with a clear request.
|
|
70
78
|
|
|
71
79
|
## step:execute
|
|
72
80
|
|
|
73
81
|
Goal:
|
|
82
|
+
|
|
74
83
|
- Execute the plan.
|
|
75
84
|
- Use the declared tools. You may call tools multiple times.
|
|
76
85
|
|
|
77
86
|
Constraints:
|
|
87
|
+
|
|
78
88
|
- Write intermediate artifacts only if asked.
|
|
79
89
|
- Summarize what you did in plain text.
|
|
80
90
|
|
|
81
91
|
## step:finalize
|
|
82
92
|
|
|
83
93
|
Goal:
|
|
94
|
+
|
|
84
95
|
- Produce a final **FinalResult** object that is valid JSON for the schema.
|
|
85
96
|
- Include references to tools invoked and any sub-agent calls.
|
|
86
97
|
- If anything failed, reflect it in the structured fields rather than “hiding” it in prose.
|
|
@@ -6,13 +6,15 @@ license = {file = "LICENSE"}
|
|
|
6
6
|
authors = [
|
|
7
7
|
{name = "Charles Verge", email = "1906614+charlesverge@users.noreply.github.com"}
|
|
8
8
|
]
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.3"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"anyio",
|
|
13
13
|
"pydantic",
|
|
14
14
|
"pydantic-ai",
|
|
15
15
|
"python-frontmatter",
|
|
16
|
+
"types-PyYAML",
|
|
17
|
+
"PyYAML",
|
|
16
18
|
]
|
|
17
19
|
keywords = ["agents", "llm", "automation", "pydantic", "markdown", "orchestrator"]
|
|
18
20
|
classifiers = [
|
|
@@ -61,7 +63,7 @@ build-backend = "setuptools.build_meta"
|
|
|
61
63
|
where = ["src"]
|
|
62
64
|
|
|
63
65
|
[tool.setuptools.package-data]
|
|
64
|
-
quick_agent = ["tools/**/tool.json", "agents/**/*.md"]
|
|
66
|
+
quick_agent = ["llms.txt", "py.typed", "tools/**/tool.json", "agents/**/*.md"]
|
|
65
67
|
|
|
66
68
|
[tool.setuptools.data-files]
|
|
67
69
|
"quick_agent/agents" = ["agents/**/*.md"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Public package exports."""
|
|
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
|
|
6
|
+
from quick_agent.orchestrator import Orchestrator
|
|
7
|
+
from quick_agent.quick_agent import QuickAgent
|
|
8
|
+
|
|
9
|
+
__all__ = ["FileInput", "InputAdaptor", "Orchestrator", "QuickAgent", "TextInput"]
|
|
@@ -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}
|
|
@@ -2,43 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
from quick_agent.models.agent_spec import AgentSpec
|
|
11
|
-
from quick_agent.models.loaded_agent_file import LoadedAgentFile
|
|
7
|
+
from quick_agent.models.loaded_agent_file import LoadedAgentFile, parse_agent_sections
|
|
12
8
|
|
|
13
9
|
|
|
14
10
|
def split_step_sections(markdown_body: str) -> dict[str, str]:
|
|
15
11
|
"""
|
|
16
|
-
Extracts blocks that begin with headings "
|
|
12
|
+
Extracts blocks that begin with headings like "# step:<id>".
|
|
17
13
|
Returns mapping: "step:<id>" -> content for that step (excluding heading line).
|
|
18
14
|
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
out: dict[str, str] = {}
|
|
22
|
-
|
|
23
|
-
for i, m in enumerate(matches):
|
|
24
|
-
section_name = m.group(1)
|
|
25
|
-
start = m.end()
|
|
26
|
-
end = matches[i + 1].start() if (i + 1) < len(matches) else len(markdown_body)
|
|
27
|
-
out[section_name] = markdown_body[start:end].strip()
|
|
28
|
-
|
|
29
|
-
return out
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def load_agent_file(path: Path) -> LoadedAgentFile:
|
|
33
|
-
post = frontmatter.load(str(path))
|
|
34
|
-
spec = AgentSpec.model_validate(post.metadata)
|
|
35
|
-
steps = split_step_sections(post.content)
|
|
36
|
-
return LoadedAgentFile(spec=spec, body=post.content, step_prompts=steps)
|
|
15
|
+
sections = parse_agent_sections(markdown_body)
|
|
16
|
+
return sections.step_prompts
|
|
37
17
|
|
|
38
18
|
|
|
39
19
|
class AgentRegistry:
|
|
40
|
-
def __init__(self, agent_roots: list[Path]):
|
|
41
|
-
self.agent_roots = agent_roots
|
|
20
|
+
def __init__(self, agent_roots: list[Path]) -> None:
|
|
21
|
+
self.agent_roots: list[Path] = agent_roots
|
|
42
22
|
self._cache: dict[str, LoadedAgentFile] = {}
|
|
43
23
|
self._index: dict[str, Path] | None = None
|
|
44
24
|
|
|
@@ -70,6 +50,6 @@ class AgentRegistry:
|
|
|
70
50
|
path = index.get(agent_id)
|
|
71
51
|
if path is None:
|
|
72
52
|
raise FileNotFoundError(f"Agent not found: {agent_id} (searched: {self.agent_roots})")
|
|
73
|
-
loaded =
|
|
53
|
+
loaded = LoadedAgentFile(path)
|
|
74
54
|
self._cache[agent_id] = loaded
|
|
75
55
|
return loaded
|
|
@@ -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
|
|
@@ -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)
|