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/quick_agent.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any, Type
|
|
7
|
+
from typing import Any, Type, TypeAlias, TypedDict
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ValidationError
|
|
10
10
|
from pydantic_ai import Agent
|
|
@@ -16,13 +16,24 @@ from pydantic_ai.toolsets import FunctionToolset
|
|
|
16
16
|
from quick_agent.agent_registry import AgentRegistry
|
|
17
17
|
from quick_agent.agent_tools import AgentTools
|
|
18
18
|
from quick_agent.directory_permissions import DirectoryPermissions
|
|
19
|
-
from quick_agent.
|
|
19
|
+
from quick_agent.input_adaptors import FileInput, InputAdaptor, TextInput
|
|
20
|
+
from quick_agent.io_utils import write_output
|
|
20
21
|
from quick_agent.json_utils import extract_first_json_object
|
|
21
22
|
from quick_agent.models.loaded_agent_file import LoadedAgentFile
|
|
23
|
+
from quick_agent.models.chain_step_spec import ChainStepSpec
|
|
22
24
|
from quick_agent.models.model_spec import ModelSpec
|
|
25
|
+
from quick_agent.models.run_input import RunInput
|
|
23
26
|
from quick_agent.prompting import make_user_prompt
|
|
24
27
|
from quick_agent.tools_loader import import_symbol
|
|
25
28
|
|
|
29
|
+
StepOutput: TypeAlias = str | dict[str, Any]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ChainState(TypedDict):
|
|
33
|
+
agent_id: str
|
|
34
|
+
steps: dict[str, StepOutput]
|
|
35
|
+
final_output: StepOutput | None
|
|
36
|
+
|
|
26
37
|
|
|
27
38
|
class QuickAgent:
|
|
28
39
|
def __init__(
|
|
@@ -32,29 +43,37 @@ class QuickAgent:
|
|
|
32
43
|
tools: AgentTools,
|
|
33
44
|
directory_permissions: DirectoryPermissions,
|
|
34
45
|
agent_id: str,
|
|
35
|
-
|
|
46
|
+
input_data: InputAdaptor | Path,
|
|
36
47
|
extra_tools: list[str] | None,
|
|
48
|
+
write_output: bool = True,
|
|
37
49
|
) -> None:
|
|
38
|
-
self._registry = registry
|
|
39
|
-
self._tools = tools
|
|
40
|
-
self._directory_permissions = directory_permissions
|
|
41
|
-
self._agent_id = agent_id
|
|
42
|
-
self.
|
|
43
|
-
self._extra_tools = extra_tools
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
self._registry: AgentRegistry = registry
|
|
51
|
+
self._tools: AgentTools = tools
|
|
52
|
+
self._directory_permissions: DirectoryPermissions = directory_permissions
|
|
53
|
+
self._agent_id: str = agent_id
|
|
54
|
+
self._input_data: InputAdaptor | Path = input_data
|
|
55
|
+
self._extra_tools: list[str] | None = extra_tools
|
|
56
|
+
self._write_output_file: bool = write_output
|
|
57
|
+
|
|
58
|
+
self.loaded: LoadedAgentFile = self._registry.get(self._agent_id)
|
|
46
59
|
safe_dir = self.loaded.spec.safe_dir
|
|
47
60
|
if safe_dir is not None and Path(safe_dir).is_absolute():
|
|
48
61
|
raise ValueError("safe_dir must be a relative path.")
|
|
49
|
-
self.permissions = self._directory_permissions.scoped(safe_dir)
|
|
50
|
-
|
|
62
|
+
self.permissions: DirectoryPermissions = self._directory_permissions.scoped(safe_dir)
|
|
63
|
+
if isinstance(self._input_data, InputAdaptor):
|
|
64
|
+
input_adaptor = self._input_data
|
|
65
|
+
else:
|
|
66
|
+
input_adaptor = FileInput(self._input_data, self.permissions)
|
|
67
|
+
self.run_input: RunInput = input_adaptor.load()
|
|
51
68
|
|
|
52
|
-
self.tool_ids
|
|
53
|
-
|
|
69
|
+
self.tool_ids: list[str] = list(
|
|
70
|
+
dict.fromkeys((self.loaded.spec.tools or []) + (self._extra_tools or []))
|
|
71
|
+
)
|
|
72
|
+
self.toolset: FunctionToolset[Any] = self._tools.build_toolset(self.tool_ids, self.permissions)
|
|
54
73
|
|
|
55
|
-
self.model = build_model(self.loaded.spec.model)
|
|
56
|
-
self.model_settings_json = self._build_model_settings(self.loaded.spec.model)
|
|
57
|
-
self.state = self._init_state(
|
|
74
|
+
self.model: OpenAIChatModel = build_model(self.loaded.spec.model)
|
|
75
|
+
self.model_settings_json: ModelSettings | None = self._build_model_settings(self.loaded.spec.model)
|
|
76
|
+
self.state: ChainState = self._init_state()
|
|
58
77
|
|
|
59
78
|
async def run(self) -> BaseModel | str:
|
|
60
79
|
self._tools.maybe_inject_agent_call(
|
|
@@ -64,36 +83,33 @@ class QuickAgent:
|
|
|
64
83
|
self._run_nested_agent,
|
|
65
84
|
)
|
|
66
85
|
|
|
67
|
-
final_output = await self._run_chain(
|
|
68
|
-
loaded=self.loaded,
|
|
69
|
-
model=self.model,
|
|
70
|
-
model_settings_json=self.model_settings_json,
|
|
71
|
-
toolset=self.toolset,
|
|
72
|
-
run_input=self.run_input,
|
|
73
|
-
state=self.state,
|
|
74
|
-
)
|
|
86
|
+
final_output = await self._run_chain()
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
if self._write_output_file:
|
|
89
|
+
self._write_final_output(final_output)
|
|
77
90
|
|
|
78
|
-
await self._handle_handoff(
|
|
91
|
+
await self._handle_handoff(final_output)
|
|
79
92
|
|
|
80
93
|
return final_output
|
|
81
94
|
|
|
82
|
-
async def _run_nested_agent(self, agent_id: str,
|
|
95
|
+
async def _run_nested_agent(self, agent_id: str, input_data: InputAdaptor | Path) -> BaseModel | str:
|
|
96
|
+
nested_write_output = self.loaded.spec.nested_output == "file"
|
|
83
97
|
agent = QuickAgent(
|
|
84
98
|
registry=self._registry,
|
|
85
99
|
tools=self._tools,
|
|
86
100
|
directory_permissions=self._directory_permissions,
|
|
87
101
|
agent_id=agent_id,
|
|
88
|
-
|
|
102
|
+
input_data=input_data,
|
|
89
103
|
extra_tools=None,
|
|
104
|
+
write_output=nested_write_output,
|
|
90
105
|
)
|
|
91
106
|
return await agent.run()
|
|
92
107
|
|
|
93
|
-
def _init_state(self
|
|
108
|
+
def _init_state(self) -> ChainState:
|
|
94
109
|
return {
|
|
95
|
-
"agent_id":
|
|
110
|
+
"agent_id": self._agent_id,
|
|
96
111
|
"steps": {},
|
|
112
|
+
"final_output": None,
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
def _build_model_settings(self, model_spec: ModelSpec) -> ModelSettings | None:
|
|
@@ -103,21 +119,15 @@ class QuickAgent:
|
|
|
103
119
|
return {"extra_body": {"format": "json"}}
|
|
104
120
|
return None
|
|
105
121
|
|
|
106
|
-
def _build_structured_model_settings(
|
|
107
|
-
self
|
|
108
|
-
|
|
109
|
-
model: OpenAIChatModel,
|
|
110
|
-
model_settings_json: ModelSettings | None,
|
|
111
|
-
schema_cls: Type[BaseModel],
|
|
112
|
-
) -> ModelSettings | None:
|
|
113
|
-
model_settings: ModelSettings | None = model_settings_json
|
|
114
|
-
provider = getattr(model, "provider", None)
|
|
122
|
+
def _build_structured_model_settings(self, *, schema_cls: Type[BaseModel]) -> ModelSettings | None:
|
|
123
|
+
model_settings: ModelSettings | None = self.model_settings_json
|
|
124
|
+
provider = getattr(self.model, "provider", None)
|
|
115
125
|
base_url = getattr(provider, "base_url", None)
|
|
116
126
|
if base_url == "https://api.openai.com/v1":
|
|
117
|
-
if model_settings_json is None:
|
|
127
|
+
if self.model_settings_json is None:
|
|
118
128
|
model_settings_dict: ModelSettings = {}
|
|
119
129
|
else:
|
|
120
|
-
model_settings_dict = model_settings_json
|
|
130
|
+
model_settings_dict = self.model_settings_json
|
|
121
131
|
extra_body_obj = model_settings_dict.get("extra_body")
|
|
122
132
|
extra_body: dict[str, Any] = {}
|
|
123
133
|
if isinstance(extra_body_obj, dict):
|
|
@@ -138,33 +148,16 @@ class QuickAgent:
|
|
|
138
148
|
async def _run_step(
|
|
139
149
|
self,
|
|
140
150
|
*,
|
|
141
|
-
step:
|
|
142
|
-
|
|
143
|
-
model: OpenAIChatModel,
|
|
144
|
-
model_settings_json: ModelSettings | None,
|
|
145
|
-
toolset: FunctionToolset[Any],
|
|
146
|
-
run_input: Any,
|
|
147
|
-
state: dict[str, Any],
|
|
148
|
-
) -> tuple[Any, BaseModel | str]:
|
|
151
|
+
step: ChainStepSpec,
|
|
152
|
+
) -> tuple[StepOutput, BaseModel | str]:
|
|
149
153
|
if step.kind == "text":
|
|
150
154
|
return await self._run_text_step(
|
|
151
155
|
step=step,
|
|
152
|
-
loaded=loaded,
|
|
153
|
-
model=model,
|
|
154
|
-
toolset=toolset,
|
|
155
|
-
run_input=run_input,
|
|
156
|
-
state=state,
|
|
157
156
|
)
|
|
158
157
|
|
|
159
158
|
if step.kind == "structured":
|
|
160
159
|
return await self._run_structured_step(
|
|
161
160
|
step=step,
|
|
162
|
-
loaded=loaded,
|
|
163
|
-
model=model,
|
|
164
|
-
model_settings_json=model_settings_json,
|
|
165
|
-
toolset=toolset,
|
|
166
|
-
run_input=run_input,
|
|
167
|
-
state=state,
|
|
168
161
|
)
|
|
169
162
|
|
|
170
163
|
raise NotImplementedError(f"Unknown step kind: {step.kind}")
|
|
@@ -172,37 +165,26 @@ class QuickAgent:
|
|
|
172
165
|
def _build_user_prompt(
|
|
173
166
|
self,
|
|
174
167
|
*,
|
|
175
|
-
step:
|
|
176
|
-
loaded: LoadedAgentFile,
|
|
177
|
-
run_input: Any,
|
|
178
|
-
state: dict[str, Any],
|
|
168
|
+
step: ChainStepSpec,
|
|
179
169
|
) -> str:
|
|
180
|
-
if step.prompt_section not in loaded.step_prompts:
|
|
170
|
+
if step.prompt_section not in self.loaded.step_prompts:
|
|
181
171
|
raise KeyError(f"Missing step section {step.prompt_section!r} in agent.md body.")
|
|
182
172
|
|
|
183
|
-
step_prompt = loaded.step_prompts[step.prompt_section]
|
|
184
|
-
return make_user_prompt(step_prompt, run_input, state)
|
|
173
|
+
step_prompt = self.loaded.step_prompts[step.prompt_section]
|
|
174
|
+
return make_user_prompt(step_prompt, self.run_input, self.state)
|
|
185
175
|
|
|
186
176
|
async def _run_text_step(
|
|
187
177
|
self,
|
|
188
178
|
*,
|
|
189
|
-
step:
|
|
190
|
-
|
|
191
|
-
model: OpenAIChatModel,
|
|
192
|
-
toolset: FunctionToolset[Any],
|
|
193
|
-
run_input: Any,
|
|
194
|
-
state: dict[str, Any],
|
|
195
|
-
) -> tuple[Any, BaseModel | str]:
|
|
179
|
+
step: ChainStepSpec,
|
|
180
|
+
) -> tuple[StepOutput, BaseModel | str]:
|
|
196
181
|
user_prompt = self._build_user_prompt(
|
|
197
182
|
step=step,
|
|
198
|
-
loaded=loaded,
|
|
199
|
-
run_input=run_input,
|
|
200
|
-
state=state,
|
|
201
183
|
)
|
|
202
184
|
agent = Agent(
|
|
203
|
-
model,
|
|
204
|
-
instructions=loaded.body,
|
|
205
|
-
toolsets=[toolset],
|
|
185
|
+
self.model,
|
|
186
|
+
instructions=self.loaded.body,
|
|
187
|
+
toolsets=[self.toolset],
|
|
206
188
|
output_type=str,
|
|
207
189
|
)
|
|
208
190
|
result = await agent.run(user_prompt)
|
|
@@ -211,34 +193,21 @@ class QuickAgent:
|
|
|
211
193
|
async def _run_structured_step(
|
|
212
194
|
self,
|
|
213
195
|
*,
|
|
214
|
-
step:
|
|
215
|
-
|
|
216
|
-
model: OpenAIChatModel,
|
|
217
|
-
model_settings_json: ModelSettings | None,
|
|
218
|
-
toolset: FunctionToolset[Any],
|
|
219
|
-
run_input: Any,
|
|
220
|
-
state: dict[str, Any],
|
|
221
|
-
) -> tuple[Any, BaseModel | str]:
|
|
196
|
+
step: ChainStepSpec,
|
|
197
|
+
) -> tuple[StepOutput, BaseModel | str]:
|
|
222
198
|
if not step.output_schema:
|
|
223
199
|
raise ValueError(f"Step {step.id} is structured but missing output_schema.")
|
|
224
|
-
schema_cls = resolve_schema(loaded, step.output_schema)
|
|
200
|
+
schema_cls = resolve_schema(self.loaded, step.output_schema)
|
|
225
201
|
|
|
226
|
-
model_settings = self._build_structured_model_settings(
|
|
227
|
-
model=model,
|
|
228
|
-
model_settings_json=model_settings_json,
|
|
229
|
-
schema_cls=schema_cls,
|
|
230
|
-
)
|
|
202
|
+
model_settings = self._build_structured_model_settings(schema_cls=schema_cls)
|
|
231
203
|
|
|
232
204
|
user_prompt = self._build_user_prompt(
|
|
233
205
|
step=step,
|
|
234
|
-
loaded=loaded,
|
|
235
|
-
run_input=run_input,
|
|
236
|
-
state=state,
|
|
237
206
|
)
|
|
238
207
|
agent = Agent(
|
|
239
|
-
model,
|
|
240
|
-
instructions=loaded.body,
|
|
241
|
-
toolsets=[toolset],
|
|
208
|
+
self.model,
|
|
209
|
+
instructions=self.loaded.body,
|
|
210
|
+
toolsets=[self.toolset],
|
|
242
211
|
output_type=str,
|
|
243
212
|
model_settings=model_settings,
|
|
244
213
|
)
|
|
@@ -253,49 +222,35 @@ class QuickAgent:
|
|
|
253
222
|
|
|
254
223
|
async def _run_chain(
|
|
255
224
|
self,
|
|
256
|
-
*,
|
|
257
|
-
loaded: LoadedAgentFile,
|
|
258
|
-
model: OpenAIChatModel,
|
|
259
|
-
model_settings_json: ModelSettings | None,
|
|
260
|
-
toolset: FunctionToolset[Any],
|
|
261
|
-
run_input: Any,
|
|
262
|
-
state: dict[str, Any],
|
|
263
225
|
) -> BaseModel | str:
|
|
264
226
|
final_output: BaseModel | str = ""
|
|
265
|
-
for step in loaded.spec.chain:
|
|
227
|
+
for step in self.loaded.spec.chain:
|
|
266
228
|
step_out, step_final = await self._run_step(
|
|
267
229
|
step=step,
|
|
268
|
-
loaded=loaded,
|
|
269
|
-
model=model,
|
|
270
|
-
model_settings_json=model_settings_json,
|
|
271
|
-
toolset=toolset,
|
|
272
|
-
run_input=run_input,
|
|
273
|
-
state=state,
|
|
274
230
|
)
|
|
275
|
-
state["steps"][step.id] = step_out
|
|
231
|
+
self.state["steps"][step.id] = step_out
|
|
232
|
+
self.state["final_output"] = step_out
|
|
276
233
|
final_output = step_final
|
|
277
234
|
return final_output
|
|
278
235
|
|
|
279
|
-
def _write_final_output(
|
|
280
|
-
self
|
|
281
|
-
loaded: LoadedAgentFile,
|
|
282
|
-
final_output: BaseModel | str,
|
|
283
|
-
permissions: DirectoryPermissions,
|
|
284
|
-
) -> Path:
|
|
285
|
-
out_path = Path(loaded.spec.output.file)
|
|
236
|
+
def _write_final_output(self, final_output: BaseModel | str) -> Path:
|
|
237
|
+
out_path = Path(self.loaded.spec.output.file)
|
|
286
238
|
if isinstance(final_output, BaseModel):
|
|
287
|
-
if loaded.spec.output.format == "json":
|
|
288
|
-
write_output(out_path, final_output.model_dump_json(indent=2), permissions)
|
|
239
|
+
if self.loaded.spec.output.format == "json":
|
|
240
|
+
write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
|
|
289
241
|
else:
|
|
290
|
-
write_output(out_path, final_output.model_dump_json(indent=2), permissions)
|
|
242
|
+
write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
|
|
291
243
|
else:
|
|
292
|
-
write_output(out_path, str(final_output), permissions)
|
|
244
|
+
write_output(out_path, str(final_output), self.permissions)
|
|
293
245
|
return out_path
|
|
294
246
|
|
|
295
|
-
async def _handle_handoff(self,
|
|
296
|
-
if loaded.spec.handoff.enabled and loaded.spec.handoff.agent_id:
|
|
297
|
-
|
|
298
|
-
|
|
247
|
+
async def _handle_handoff(self, final_output: BaseModel | str) -> None:
|
|
248
|
+
if self.loaded.spec.handoff.enabled and self.loaded.spec.handoff.agent_id:
|
|
249
|
+
if isinstance(final_output, BaseModel):
|
|
250
|
+
payload = final_output.model_dump_json(indent=2)
|
|
251
|
+
else:
|
|
252
|
+
payload = str(final_output)
|
|
253
|
+
await self._run_nested_agent(self.loaded.spec.handoff.agent_id, TextInput(payload))
|
|
299
254
|
|
|
300
255
|
|
|
301
256
|
def resolve_schema(loaded: LoadedAgentFile, schema_name: str) -> Type[BaseModel]:
|
quick_agent/schemas/outputs.py
CHANGED
|
@@ -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
|
+
# Business Extract Structured
|
|
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
|
+
# Business Extract
|
|
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quick-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -707,13 +707,14 @@ Provides-Extra: test
|
|
|
707
707
|
Requires-Dist: pytest; extra == "test"
|
|
708
708
|
Dynamic: license-file
|
|
709
709
|
|
|
710
|
-
#
|
|
710
|
+
# Quick Agent
|
|
711
711
|
|
|
712
|
-
|
|
712
|
+
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
713
|
|
|
714
714
|
## Project Goal
|
|
715
715
|
|
|
716
716
|
Provide a simple, maintainable agent framework that:
|
|
717
|
+
|
|
717
718
|
- Uses Markdown front matter for agent configuration.
|
|
718
719
|
- Runs a deterministic chain of steps (text or structured output).
|
|
719
720
|
- Keeps context handling deliberately limited and predictable.
|
|
@@ -887,7 +888,7 @@ def main() -> None:
|
|
|
887
888
|
tools=tools,
|
|
888
889
|
directory_permissions=permissions,
|
|
889
890
|
agent_id="hello",
|
|
890
|
-
|
|
891
|
+
input_data=Path("safe/path/to/input.txt"),
|
|
891
892
|
extra_tools=None,
|
|
892
893
|
)
|
|
893
894
|
|
|
@@ -908,6 +909,17 @@ Agents are stored as Markdown files with YAML front matter and step sections:
|
|
|
908
909
|
|
|
909
910
|
The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
|
|
910
911
|
|
|
912
|
+
## Nested Output
|
|
913
|
+
|
|
914
|
+
When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
|
|
915
|
+
own `output.file` or return output inline only. Configure this in the parent agent front matter:
|
|
916
|
+
|
|
917
|
+
```yaml
|
|
918
|
+
nested_output: inline # default, no output file for nested calls
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
Use `nested_output: file` to allow nested agents to write their configured output files.
|
|
922
|
+
|
|
911
923
|
## Documentation
|
|
912
924
|
|
|
913
925
|
See the docs in `docs/`:
|
|
@@ -916,3 +928,4 @@ See the docs in `docs/`:
|
|
|
916
928
|
- [docs/templates.md](docs/templates.md): Agent template format and examples.
|
|
917
929
|
- [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
|
|
918
930
|
- [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
|
|
931
|
+
- [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
quick_agent/__init__.py,sha256=lgTMjPhbumIZ1Pna33khCaAs4DSjErnYoCkvK_YxBCc,362
|
|
2
|
+
quick_agent/agent_call_tool.py,sha256=yyXpWWpQlhSV8qxkeA1tLdrfcHhYg2cmhf4sxx8FfvA,2195
|
|
3
|
+
quick_agent/agent_registry.py,sha256=laHxiB3ko2oaQDhN7xG7u7Kc7WTy7j-rPqvfiWdpxEY,2473
|
|
4
|
+
quick_agent/agent_tools.py,sha256=GwaPkoG_vy1f6hatvRQFoDj0IJ-6nIYV746qc8y-yMg,1475
|
|
5
|
+
quick_agent/cli.py,sha256=nzVF4paXxugaPzmHTMwCBkbQkfkkv9hqGQH4Fh0jJ_U,1973
|
|
6
|
+
quick_agent/directory_permissions.py,sha256=r6Lc56oIrGhVVUVrCoPInue_BCdGZsD5oDgoAHJWiyE,1955
|
|
7
|
+
quick_agent/input_adaptors.py,sha256=ZaK0Mm27cGMGb5y6fsgV0gXRPMW2zmHn9l9dXKzOufA,865
|
|
8
|
+
quick_agent/io_utils.py,sha256=BUmUoZepL4lSYe0JbKxLm4mFFQ6Zt6MZQzprzgoBweU,1200
|
|
9
|
+
quick_agent/json_utils.py,sha256=G6uP9SrdEw5WtA9--dBqdJcTYD2JgdgotPJdleErB_c,1003
|
|
10
|
+
quick_agent/llms.txt,sha256=SNxYm6oFbqvc4mzGsxSpWg1l0iwT-Be0saVw3rtoruQ,6459
|
|
11
|
+
quick_agent/orchestrator.py,sha256=SGa5oeRgbjkKXdFL5kszZT-R6-jIgyOkHEMmjLrDWjc,1321
|
|
12
|
+
quick_agent/prompting.py,sha256=XMlnRut3aU2FEabhXuvb4Z_DXKzi1psZfbbx4iDyg_4,645
|
|
13
|
+
quick_agent/py.typed,sha256=3Q9vdii9v-_pbj9sglgDzJAIelvARaYaDJlB2u-NDqg,38
|
|
14
|
+
quick_agent/quick_agent.py,sha256=3Cok1VPzYZo4Nityyprub2XaKVRBkaoeTm6fs67BRZw,10319
|
|
15
|
+
quick_agent/tools_loader.py,sha256=oveR-EGAZo3Q7NPJ2aWKU3d8xd8kIpibOrjInpUZwQg,2642
|
|
16
|
+
quick_agent/models/__init__.py,sha256=YseFAVWLarh0wZCVUL0fmkjVgA-FnYJLEiVinG1-6Tk,737
|
|
17
|
+
quick_agent/models/agent_spec.py,sha256=YsCrs9DSKJJJBsqnMAxb_19zG6UuNHtXwiuTZjbFg5U,882
|
|
18
|
+
quick_agent/models/chain_step_spec.py,sha256=NcdYFmgJhA5ODIwCTgTb2fmmJJesAZCVAIy3cGC5dhs,342
|
|
19
|
+
quick_agent/models/handoff_spec.py,sha256=46Pt4JGU8-6f-cdbDR0oqWuJbg5ENJcXrFN1IK3chvs,280
|
|
20
|
+
quick_agent/models/loaded_agent_file.py,sha256=rWRxrm0KjgmNIiYfooMM95eohMB1zCZC-eqrp1mf8w0,313
|
|
21
|
+
quick_agent/models/model_spec.py,sha256=spnIZ8BNtXT5t8YRL1nh7cXrgWwNSptt_BMnAB8w1Lg,425
|
|
22
|
+
quick_agent/models/output_spec.py,sha256=bvKFU1ONeza95qethNIiVgrsnT7l1TKMsdHjfXVVJnc,229
|
|
23
|
+
quick_agent/models/run_input.py,sha256=WpV-QPlOPXMmZR6eIvP7vJOuCeBt3xWe4j4ngEbmSxg,282
|
|
24
|
+
quick_agent/models/tool_impl_spec.py,sha256=7B161m8nFZCNjslb4VZdSi-mdAZM0jhp4kV4uaq8X1s,216
|
|
25
|
+
quick_agent/models/tool_json.py,sha256=eYFBPCqxmRfyo7GYHBXCfog7kUzpjginfjt9grQy4ic,274
|
|
26
|
+
quick_agent/schemas/outputs.py,sha256=8FTIczUV079imbygbFU3h9tyLWSH8aQZCPBTxM8QVtk,1527
|
|
27
|
+
quick_agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
quick_agent/tools/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
quick_agent/tools/filesystem/adapter.py,sha256=aYcX1qjI9wRSANSNSB3KWbKi7uDIYo3EpJotpUxnQUM,1027
|
|
30
|
+
quick_agent/tools/filesystem/read_text.py,sha256=owpZxwCFoYbljABhwpNKU4g7-8xDPJDu0kZYF7bPLfs,478
|
|
31
|
+
quick_agent/tools/filesystem/write_text.py,sha256=lwsMCk_p8xurVxychM2UBieXDp0SgN3C6rufLBUzDxo,584
|
|
32
|
+
quick_agent/tools/filesystem.read_text/tool.json,sha256=UtkOxcpi6qYd611tJx-zFXVUjOwx2ZbmxEWsF4cAVpo,246
|
|
33
|
+
quick_agent/tools/filesystem.write_text/tool.json,sha256=EcGmB_M_KDTqOrnxk_NVhKRoW_KADFFksgJ7fmw472s,275
|
|
34
|
+
quick_agent-0.1.2.data/data/quick_agent/agents/business-extract-structured.md,sha256=mhieyFbkJ23kjZPvpKguW4xorkTgzwAev1EZCUkcS44,1182
|
|
35
|
+
quick_agent-0.1.2.data/data/quick_agent/agents/business-extract.md,sha256=2SQdD9a4Bg_PXqL5-D5-kJMzZOYAigi0N54XsnoeNCU,996
|
|
36
|
+
quick_agent-0.1.2.data/data/quick_agent/agents/function-spec-validator.md,sha256=Dlc4j1vFpDHj7w2vcWrvC83EEg_sWwwt1GOaRgcsm6Y,3289
|
|
37
|
+
quick_agent-0.1.2.data/data/quick_agent/agents/subagent-validate-eval-list.md,sha256=K83L9BKvGcttPRINwD6VSPopEHnB_aw4tpfSI6KM2u8,4119
|
|
38
|
+
quick_agent-0.1.2.data/data/quick_agent/agents/subagent-validator-contains.md,sha256=3VxtKpyogwcJ4Er5tQFyYNe2nJBtANErb5D-_snBATw,3115
|
|
39
|
+
quick_agent-0.1.2.data/data/quick_agent/agents/template.md,sha256=6IBuVZjK5OPIchI8ruvxao8CdLy0ZOi0meh6d1exI0E,2458
|
|
40
|
+
quick_agent-0.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
41
|
+
tests/test_agent.py,sha256=tcPpH9WmGPQjMo_jkFJlF8kgOZ3QktIvUHTAJaH1hWA,5533
|
|
42
|
+
tests/test_directory_permissions.py,sha256=-9h7W1VO0LekPLCxEqIAGgebquNks2F8M02BdZDLlQY,2815
|
|
43
|
+
tests/test_input_adaptors.py,sha256=sle9zrumq3CUVGtDPZi2pf0om-Z7v4SI6khnTf7Rz4c,918
|
|
44
|
+
tests/test_integration.py,sha256=XbI6abGeoZm3r_BqpQyhxhF36N7VYtNGcJ9-xNxZysM,9044
|
|
45
|
+
tests/test_orchestrator.py,sha256=fWeZzTF_cQj6nbeEVh0cC5uKwgWP56deDQxkcCO1uNk,31001
|
|
46
|
+
tests/test_tools.py,sha256=Yc6AKCz79XJwHqiRV8dUBE6GVNdspUU0hIeGCXx-Rvc,936
|
|
47
|
+
quick_agent-0.1.2.dist-info/METADATA,sha256=1xL8o4mz4qW2dLuotIPTGJxuHRHm1Uxp42j-w6nsQ-8,47103
|
|
48
|
+
quick_agent-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
49
|
+
quick_agent-0.1.2.dist-info/entry_points.txt,sha256=bij-xFaQMSrMj7zearzi-bMfcyweum_GvURFMVZGde8,53
|
|
50
|
+
quick_agent-0.1.2.dist-info/top_level.txt,sha256=KT1ID0FVC0OLzQnoBKRIHbXLRNugiU2gcgj_f4DtAsw,18
|
|
51
|
+
quick_agent-0.1.2.dist-info/RECORD,,
|
|
@@ -45,6 +45,16 @@ def test_directory_permissions_can_read_write(tmp_path: Path) -> None:
|
|
|
45
45
|
assert perms.can_write(Path("../nope.txt")) is False
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def test_directory_permissions_without_root_denies_all() -> None:
|
|
49
|
+
perms = DirectoryPermissions(None)
|
|
50
|
+
|
|
51
|
+
with pytest.raises(PermissionError):
|
|
52
|
+
perms.resolve(Path("anything.txt"), for_write=False)
|
|
53
|
+
|
|
54
|
+
assert perms.can_read(Path("anything.txt")) is False
|
|
55
|
+
assert perms.can_write(Path("anything.txt")) is False
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
def test_agent_cannot_write_outside_scoped_directory(
|
|
49
59
|
tmp_path: Path,
|
|
50
60
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from quick_agent.directory_permissions import DirectoryPermissions
|
|
6
|
+
from quick_agent.input_adaptors import FileInput
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_file_input_checks_permissions_at_creation(tmp_path: Path) -> None:
|
|
10
|
+
safe_root = tmp_path / "safe"
|
|
11
|
+
safe_root.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
input_path = safe_root / "input.txt"
|
|
13
|
+
input_path.write_text("ok", encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
permissions = DirectoryPermissions(safe_root)
|
|
16
|
+
|
|
17
|
+
adaptor = FileInput(input_path, permissions)
|
|
18
|
+
|
|
19
|
+
run_input = adaptor.load()
|
|
20
|
+
assert run_input.text == "ok"
|
|
21
|
+
assert run_input.source_path == str(input_path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_file_input_denies_without_root(tmp_path: Path) -> None:
|
|
25
|
+
input_path = tmp_path / "input.txt"
|
|
26
|
+
input_path.write_text("nope", encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
permissions = DirectoryPermissions(None)
|
|
29
|
+
|
|
30
|
+
with pytest.raises(PermissionError):
|
|
31
|
+
FileInput(input_path, permissions)
|