quick-agent 0.1.2__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.2 → quick_agent-0.1.3}/PKG-INFO +5 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/README.md +2 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/agents/business-extract-structured.md +1 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/agents/business-extract.md +1 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/agents/function-spec-validator.md +1 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/agents/subagent-validate-eval-list.md +1 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/agents/subagent-validator-contains.md +8 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/agents/template.md +12 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/pyproject.toml +3 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/agent_registry.py +5 -25
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/llms.txt +1 -1
- quick_agent-0.1.3/src/quick_agent/models/loaded_agent_file.py +149 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/output_spec.py +1 -1
- quick_agent-0.1.3/src/quick_agent/prompting.py +46 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/quick_agent.py +99 -38
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent.egg-info/PKG-INFO +5 -1
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent.egg-info/SOURCES.txt +1 -0
- {quick_agent-0.1.2 → 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.3/src/tests/test_httpx_tools.py +295 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/tests/test_orchestrator.py +353 -28
- quick_agent-0.1.2/src/quick_agent/models/loaded_agent_file.py +0 -14
- quick_agent-0.1.2/src/quick_agent/prompting.py +0 -28
- quick_agent-0.1.2/src/tests/test_agent.py +0 -196
- {quick_agent-0.1.2 → quick_agent-0.1.3}/LICENSE +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/setup.cfg +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/__init__.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/agent_call_tool.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/agent_tools.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/cli.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/directory_permissions.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/input_adaptors.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/io_utils.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/json_utils.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/__init__.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/agent_spec.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/chain_step_spec.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/handoff_spec.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/model_spec.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/run_input.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/tool_impl_spec.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/models/tool_json.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/orchestrator.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/py.typed +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/schemas/outputs.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/__init__.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/__init__.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/adapter.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/read_text.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/write_text.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem.read_text/tool.json +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem.write_text/tool.json +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent/tools_loader.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent.egg-info/dependency_links.txt +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent.egg-info/entry_points.txt +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/quick_agent.egg-info/top_level.txt +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/tests/test_directory_permissions.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/tests/test_input_adaptors.py +0 -0
- {quick_agent-0.1.2 → quick_agent-0.1.3}/src/tests/test_integration.py +0 -0
- {quick_agent-0.1.2 → 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"
|
|
@@ -908,6 +910,7 @@ Agents are stored as Markdown files with YAML front matter and step sections:
|
|
|
908
910
|
- Body contains `## step:<id>` sections referenced by the chain.
|
|
909
911
|
|
|
910
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.
|
|
911
914
|
|
|
912
915
|
## Nested Output
|
|
913
916
|
|
|
@@ -926,6 +929,7 @@ See the docs in `docs/`:
|
|
|
926
929
|
|
|
927
930
|
- [docs/cli.md](docs/cli.md): Command line usage and options.
|
|
928
931
|
- [docs/templates.md](docs/templates.md): Agent template format and examples.
|
|
932
|
+
- [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
|
|
929
933
|
- [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
|
|
930
934
|
- [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
|
|
931
935
|
- [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
|
|
@@ -199,6 +199,7 @@ Agents are stored as Markdown files with YAML front matter and step sections:
|
|
|
199
199
|
- Body contains `## step:<id>` sections referenced by the chain.
|
|
200
200
|
|
|
201
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.
|
|
202
203
|
|
|
203
204
|
## Nested Output
|
|
204
205
|
|
|
@@ -217,6 +218,7 @@ See the docs in `docs/`:
|
|
|
217
218
|
|
|
218
219
|
- [docs/cli.md](docs/cli.md): Command line usage and options.
|
|
219
220
|
- [docs/templates.md](docs/templates.md): Agent template format and examples.
|
|
221
|
+
- [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
|
|
220
222
|
- [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
|
|
221
223
|
- [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
|
|
222
224
|
- [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
|
|
@@ -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 = [
|
|
@@ -2,38 +2,18 @@
|
|
|
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:
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Project Summary**
|
|
4
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
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,
|
|
6
|
+
- Execution is deterministic: steps run in order, state stores prior step outputs, and optional handoff can invoke another agent. If `output.file` is set, the final output is written to disk.
|
|
7
7
|
|
|
8
8
|
**Primary Entry Points**
|
|
9
9
|
- CLI: `quick-agent --agent <id> --input <path>` (module: `quick_agent.cli:main`).
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Loaded agent markdown plus parsed metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import frontmatter
|
|
11
|
+
|
|
12
|
+
from quick_agent.models.agent_spec import AgentSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
SECTION_HEADER_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.MULTILINE)
|
|
18
|
+
STEP_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class LoadedAgentFile:
|
|
23
|
+
spec: AgentSpec
|
|
24
|
+
instructions: str
|
|
25
|
+
system_prompt: str
|
|
26
|
+
step_prompts: dict[str, str] # prompt_section -> markdown chunk
|
|
27
|
+
|
|
28
|
+
def __init__(self, agent: Path | str) -> None:
|
|
29
|
+
post, source_label = load_agent_frontmatter(agent)
|
|
30
|
+
spec = AgentSpec.model_validate(post.metadata)
|
|
31
|
+
sections = parse_agent_sections(post.content)
|
|
32
|
+
if sections.first_section_start is not None and (
|
|
33
|
+
sections.instructions_start is not None or sections.system_prompt_start is not None
|
|
34
|
+
):
|
|
35
|
+
preamble = post.content[: sections.first_section_start]
|
|
36
|
+
if preamble.strip():
|
|
37
|
+
logger.warning("Ignored text before instructions or system prompt in %s", source_label)
|
|
38
|
+
if not sections.step_prompts and sections.instructions_start is None and sections.system_prompt_start is None:
|
|
39
|
+
raise ValueError("Agent markdown must include instructions, system prompt, or step sections.")
|
|
40
|
+
self.spec = spec
|
|
41
|
+
self.instructions = sections.instructions
|
|
42
|
+
self.system_prompt = sections.system_prompt
|
|
43
|
+
self.step_prompts = sections.step_prompts
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_parts(
|
|
47
|
+
cls,
|
|
48
|
+
*,
|
|
49
|
+
spec: AgentSpec,
|
|
50
|
+
instructions: str,
|
|
51
|
+
system_prompt: str,
|
|
52
|
+
step_prompts: dict[str, str],
|
|
53
|
+
) -> "LoadedAgentFile":
|
|
54
|
+
obj = cls.__new__(cls)
|
|
55
|
+
obj.spec = spec
|
|
56
|
+
obj.instructions = instructions
|
|
57
|
+
obj.system_prompt = system_prompt
|
|
58
|
+
obj.step_prompts = step_prompts
|
|
59
|
+
return obj
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ParsedAgentSections:
|
|
64
|
+
instructions: str
|
|
65
|
+
system_prompt: str
|
|
66
|
+
step_prompts: dict[str, str]
|
|
67
|
+
instructions_start: int | None
|
|
68
|
+
system_prompt_start: int | None
|
|
69
|
+
first_section_start: int | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_agent_frontmatter(agent: Path | str) -> tuple[frontmatter.Post, str]:
|
|
73
|
+
if isinstance(agent, Path):
|
|
74
|
+
post = frontmatter.load(str(agent))
|
|
75
|
+
return post, str(agent)
|
|
76
|
+
agent_path = Path(agent)
|
|
77
|
+
if agent_path.exists():
|
|
78
|
+
post = frontmatter.load(str(agent_path))
|
|
79
|
+
return post, str(agent_path)
|
|
80
|
+
post = frontmatter.loads(agent)
|
|
81
|
+
return post, "<inline>"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def normalize_header_text(header_text: str) -> str:
|
|
85
|
+
normalized = header_text.strip().lower().replace("_", " ")
|
|
86
|
+
return re.sub(r"\s+", " ", normalized)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def classify_section_header(header_text: str) -> tuple[str, str] | None:
|
|
90
|
+
header = header_text.strip()
|
|
91
|
+
if ":" in header:
|
|
92
|
+
prefix, step_id = header.split(":", 1)
|
|
93
|
+
if prefix.strip().lower() == "step":
|
|
94
|
+
step_id = step_id.strip()
|
|
95
|
+
if step_id and STEP_ID_RE.match(step_id):
|
|
96
|
+
return ("step", f"step:{step_id}")
|
|
97
|
+
normalized = normalize_header_text(header_text)
|
|
98
|
+
if normalized == "instructions":
|
|
99
|
+
return ("instructions", "instructions")
|
|
100
|
+
if normalized == "system prompt":
|
|
101
|
+
return ("system_prompt", "system_prompt")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_agent_sections(markdown_body: str) -> ParsedAgentSections:
|
|
106
|
+
matches = list(SECTION_HEADER_RE.finditer(markdown_body))
|
|
107
|
+
recognized: list[tuple[str, str, int, int]] = []
|
|
108
|
+
instructions_start: int | None = None
|
|
109
|
+
system_prompt_start: int | None = None
|
|
110
|
+
|
|
111
|
+
for match in matches:
|
|
112
|
+
header_text = match.group(2).strip()
|
|
113
|
+
classified = classify_section_header(header_text)
|
|
114
|
+
if classified is None:
|
|
115
|
+
continue
|
|
116
|
+
kind, key = classified
|
|
117
|
+
recognized.append((kind, key, match.start(), match.end()))
|
|
118
|
+
if kind == "instructions" and instructions_start is None:
|
|
119
|
+
instructions_start = match.start()
|
|
120
|
+
if kind == "system_prompt" and system_prompt_start is None:
|
|
121
|
+
system_prompt_start = match.start()
|
|
122
|
+
|
|
123
|
+
instructions = ""
|
|
124
|
+
system_prompt = ""
|
|
125
|
+
step_prompts: dict[str, str] = {}
|
|
126
|
+
|
|
127
|
+
for index, (kind, key, _start, end) in enumerate(recognized):
|
|
128
|
+
section_start = end
|
|
129
|
+
next_index = index + 1
|
|
130
|
+
section_end = recognized[next_index][2] if next_index < len(recognized) else len(markdown_body)
|
|
131
|
+
content = markdown_body[section_start:section_end].strip()
|
|
132
|
+
if kind == "instructions":
|
|
133
|
+
if not instructions:
|
|
134
|
+
instructions = content
|
|
135
|
+
elif kind == "system_prompt":
|
|
136
|
+
if not system_prompt:
|
|
137
|
+
system_prompt = content
|
|
138
|
+
else:
|
|
139
|
+
step_prompts[key] = content
|
|
140
|
+
|
|
141
|
+
first_section_start = recognized[0][2] if recognized else None
|
|
142
|
+
return ParsedAgentSections(
|
|
143
|
+
instructions=instructions,
|
|
144
|
+
system_prompt=system_prompt,
|
|
145
|
+
step_prompts=step_prompts,
|
|
146
|
+
instructions_start=instructions_start,
|
|
147
|
+
system_prompt_start=system_prompt_start,
|
|
148
|
+
first_section_start=first_section_start,
|
|
149
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Prompt composition helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from quick_agent.models.run_input import RunInput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_user_prompt(run_input: RunInput, state: Mapping[str, Any]) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Creates a consistent user prompt payload. Consistency helps prefix-caching backends.
|
|
15
|
+
"""
|
|
16
|
+
# Keep the preamble stable; append variable fields below.
|
|
17
|
+
is_inline = run_input.source_path == "inline_input.txt"
|
|
18
|
+
steps_state = state.get("steps") if isinstance(state, Mapping) else None
|
|
19
|
+
has_state = bool(steps_state)
|
|
20
|
+
include_input_header = not (is_inline and not has_state)
|
|
21
|
+
|
|
22
|
+
lines: list[str] = []
|
|
23
|
+
if not is_inline:
|
|
24
|
+
lines.extend(
|
|
25
|
+
[
|
|
26
|
+
"# Task Input",
|
|
27
|
+
f"source_path: {run_input.source_path}",
|
|
28
|
+
f"kind: {run_input.kind}",
|
|
29
|
+
"",
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if include_input_header:
|
|
34
|
+
lines.append("## Input Content")
|
|
35
|
+
lines.append(run_input.text)
|
|
36
|
+
|
|
37
|
+
if has_state:
|
|
38
|
+
state_yaml = yaml.safe_dump(
|
|
39
|
+
steps_state,
|
|
40
|
+
allow_unicode=False,
|
|
41
|
+
default_flow_style=False,
|
|
42
|
+
sort_keys=True,
|
|
43
|
+
).rstrip()
|
|
44
|
+
lines.extend(["", "## Chain State (YAML)", state_yaml])
|
|
45
|
+
|
|
46
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
@@ -53,9 +53,9 @@ class QuickAgent:
|
|
|
53
53
|
self._agent_id: str = agent_id
|
|
54
54
|
self._input_data: InputAdaptor | Path = input_data
|
|
55
55
|
self._extra_tools: list[str] | None = extra_tools
|
|
56
|
-
self._write_output_file: bool = write_output
|
|
57
|
-
|
|
58
56
|
self.loaded: LoadedAgentFile = self._registry.get(self._agent_id)
|
|
57
|
+
output_file = self.loaded.spec.output.file
|
|
58
|
+
self._write_output_file: bool = write_output and bool(output_file)
|
|
59
59
|
safe_dir = self.loaded.spec.safe_dir
|
|
60
60
|
if safe_dir is not None and Path(safe_dir).is_absolute():
|
|
61
61
|
raise ValueError("safe_dir must be a relative path.")
|
|
@@ -66,22 +66,23 @@ class QuickAgent:
|
|
|
66
66
|
input_adaptor = FileInput(self._input_data, self.permissions)
|
|
67
67
|
self.run_input: RunInput = input_adaptor.load()
|
|
68
68
|
|
|
69
|
-
self.tool_ids: list[str] =
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
self.toolset: FunctionToolset[Any] = self._tools.build_toolset(self.tool_ids, self.permissions)
|
|
69
|
+
self.tool_ids: list[str] = self._build_tool_ids()
|
|
70
|
+
self.toolset: FunctionToolset[Any] | None = self._build_toolset()
|
|
73
71
|
|
|
74
72
|
self.model: OpenAIChatModel = build_model(self.loaded.spec.model)
|
|
75
73
|
self.model_settings_json: ModelSettings | None = self._build_model_settings(self.loaded.spec.model)
|
|
76
74
|
self.state: ChainState = self._init_state()
|
|
77
75
|
|
|
78
76
|
async def run(self) -> BaseModel | str:
|
|
79
|
-
self.
|
|
80
|
-
self.
|
|
81
|
-
|
|
82
|
-
self.
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
if self.has_tools():
|
|
78
|
+
if self.toolset is None:
|
|
79
|
+
raise ValueError("Toolset is missing while tools are enabled.")
|
|
80
|
+
self._tools.maybe_inject_agent_call(
|
|
81
|
+
self.tool_ids,
|
|
82
|
+
self.toolset,
|
|
83
|
+
self.run_input.source_path,
|
|
84
|
+
self._run_nested_agent,
|
|
85
|
+
)
|
|
85
86
|
|
|
86
87
|
final_output = await self._run_chain()
|
|
87
88
|
|
|
@@ -162,34 +163,59 @@ class QuickAgent:
|
|
|
162
163
|
|
|
163
164
|
raise NotImplementedError(f"Unknown step kind: {step.kind}")
|
|
164
165
|
|
|
165
|
-
def _build_user_prompt(
|
|
166
|
-
self,
|
|
167
|
-
*,
|
|
168
|
-
step: ChainStepSpec,
|
|
169
|
-
) -> str:
|
|
170
|
-
if step.prompt_section not in self.loaded.step_prompts:
|
|
171
|
-
raise KeyError(f"Missing step section {step.prompt_section!r} in agent.md body.")
|
|
166
|
+
def _build_user_prompt(self) -> str:
|
|
167
|
+
return make_user_prompt(self.run_input, self.state)
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
def _build_step_instructions(self, step_prompt: str) -> str:
|
|
170
|
+
if not self.loaded.instructions:
|
|
171
|
+
return step_prompt
|
|
172
|
+
return f"{self.loaded.instructions}{step_prompt}"
|
|
173
|
+
|
|
174
|
+
def _build_single_shot_prompt(self) -> str:
|
|
175
|
+
return make_user_prompt(self.run_input, self.state)
|
|
176
|
+
|
|
177
|
+
def _normalize_agent_text(self, text: str) -> str | None:
|
|
178
|
+
if text:
|
|
179
|
+
return text
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def _normalize_system_prompt(self, text: str) -> str | list[str]:
|
|
183
|
+
if text:
|
|
184
|
+
return text
|
|
185
|
+
return []
|
|
175
186
|
|
|
176
187
|
async def _run_text_step(
|
|
177
188
|
self,
|
|
178
189
|
*,
|
|
179
190
|
step: ChainStepSpec,
|
|
180
191
|
) -> tuple[StepOutput, BaseModel | str]:
|
|
181
|
-
user_prompt = self._build_user_prompt(
|
|
182
|
-
|
|
183
|
-
)
|
|
192
|
+
user_prompt = self._build_user_prompt()
|
|
193
|
+
step_prompt = self.loaded.step_prompts[step.prompt_section]
|
|
194
|
+
step_instructions = self._build_step_instructions(step_prompt)
|
|
195
|
+
toolsets = self._toolsets_for_run()
|
|
184
196
|
agent = Agent(
|
|
185
197
|
self.model,
|
|
186
|
-
instructions=
|
|
187
|
-
|
|
198
|
+
instructions=step_instructions,
|
|
199
|
+
system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
|
|
200
|
+
toolsets=toolsets,
|
|
188
201
|
output_type=str,
|
|
189
202
|
)
|
|
190
203
|
result = await agent.run(user_prompt)
|
|
191
204
|
return result.output, result.output
|
|
192
205
|
|
|
206
|
+
async def _run_single_shot(self) -> BaseModel | str:
|
|
207
|
+
user_prompt = self._build_single_shot_prompt()
|
|
208
|
+
toolsets = self._toolsets_for_run()
|
|
209
|
+
agent = Agent(
|
|
210
|
+
self.model,
|
|
211
|
+
instructions=self._normalize_agent_text(self.loaded.instructions),
|
|
212
|
+
system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
|
|
213
|
+
toolsets=toolsets,
|
|
214
|
+
output_type=str,
|
|
215
|
+
)
|
|
216
|
+
result = await agent.run(user_prompt)
|
|
217
|
+
return result.output
|
|
218
|
+
|
|
193
219
|
async def _run_structured_step(
|
|
194
220
|
self,
|
|
195
221
|
*,
|
|
@@ -201,28 +227,37 @@ class QuickAgent:
|
|
|
201
227
|
|
|
202
228
|
model_settings = self._build_structured_model_settings(schema_cls=schema_cls)
|
|
203
229
|
|
|
204
|
-
user_prompt = self._build_user_prompt(
|
|
205
|
-
|
|
206
|
-
)
|
|
230
|
+
user_prompt = self._build_user_prompt()
|
|
231
|
+
step_prompt = self.loaded.step_prompts[step.prompt_section]
|
|
232
|
+
step_instructions = self._build_step_instructions(step_prompt)
|
|
233
|
+
toolsets = self._toolsets_for_run()
|
|
207
234
|
agent = Agent(
|
|
208
235
|
self.model,
|
|
209
|
-
instructions=
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
instructions=step_instructions,
|
|
237
|
+
system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
|
|
238
|
+
toolsets=toolsets,
|
|
239
|
+
output_type=schema_cls,
|
|
212
240
|
model_settings=model_settings,
|
|
213
241
|
)
|
|
214
242
|
result = await agent.run(user_prompt)
|
|
215
243
|
raw_output = result.output
|
|
216
|
-
|
|
217
|
-
parsed =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
if isinstance(raw_output, BaseModel):
|
|
245
|
+
parsed = raw_output
|
|
246
|
+
elif isinstance(raw_output, dict):
|
|
247
|
+
parsed = schema_cls.model_validate(raw_output)
|
|
248
|
+
else:
|
|
249
|
+
try:
|
|
250
|
+
parsed = schema_cls.model_validate_json(raw_output)
|
|
251
|
+
except ValidationError:
|
|
252
|
+
extracted = extract_first_json_object(raw_output)
|
|
253
|
+
parsed = schema_cls.model_validate_json(extracted)
|
|
221
254
|
return parsed.model_dump(), parsed
|
|
222
255
|
|
|
223
256
|
async def _run_chain(
|
|
224
257
|
self,
|
|
225
258
|
) -> BaseModel | str:
|
|
259
|
+
if not self.loaded.spec.chain:
|
|
260
|
+
return await self._run_single_shot()
|
|
226
261
|
final_output: BaseModel | str = ""
|
|
227
262
|
for step in self.loaded.spec.chain:
|
|
228
263
|
step_out, step_final = await self._run_step(
|
|
@@ -233,8 +268,34 @@ class QuickAgent:
|
|
|
233
268
|
final_output = step_final
|
|
234
269
|
return final_output
|
|
235
270
|
|
|
271
|
+
def has_tools(self) -> bool:
|
|
272
|
+
if not self.tool_ids:
|
|
273
|
+
return False
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
def _build_tool_ids(self) -> list[str]:
|
|
277
|
+
if not self.loaded.spec.tools:
|
|
278
|
+
return []
|
|
279
|
+
return list(dict.fromkeys((self.loaded.spec.tools or []) + (self._extra_tools or [])))
|
|
280
|
+
|
|
281
|
+
def _build_toolset(self) -> FunctionToolset[Any] | None:
|
|
282
|
+
if not self.has_tools():
|
|
283
|
+
return None
|
|
284
|
+
return self._tools.build_toolset(self.tool_ids, self.permissions)
|
|
285
|
+
|
|
286
|
+
def _toolsets_for_run(self) -> list[FunctionToolset[Any]]:
|
|
287
|
+
if not self.has_tools():
|
|
288
|
+
return []
|
|
289
|
+
toolset = self.toolset
|
|
290
|
+
if toolset is None:
|
|
291
|
+
return []
|
|
292
|
+
return [toolset]
|
|
293
|
+
|
|
236
294
|
def _write_final_output(self, final_output: BaseModel | str) -> Path:
|
|
237
|
-
|
|
295
|
+
output_file = self.loaded.spec.output.file
|
|
296
|
+
if not output_file:
|
|
297
|
+
raise ValueError("Output file is not configured.")
|
|
298
|
+
out_path = Path(output_file)
|
|
238
299
|
if isinstance(final_output, BaseModel):
|
|
239
300
|
if self.loaded.spec.output.format == "json":
|
|
240
301
|
write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
|