quick-agent 0.1.1__py3-none-any.whl → 0.1.3__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.
Files changed (35) hide show
  1. quick_agent/__init__.py +4 -1
  2. quick_agent/agent_call_tool.py +22 -5
  3. quick_agent/agent_registry.py +7 -27
  4. quick_agent/agent_tools.py +3 -2
  5. quick_agent/cli.py +19 -5
  6. quick_agent/directory_permissions.py +7 -3
  7. quick_agent/input_adaptors.py +30 -0
  8. quick_agent/llms.txt +239 -0
  9. quick_agent/models/agent_spec.py +3 -0
  10. quick_agent/models/loaded_agent_file.py +136 -1
  11. quick_agent/models/output_spec.py +1 -1
  12. quick_agent/orchestrator.py +15 -8
  13. quick_agent/prompting.py +34 -16
  14. quick_agent/py.typed +1 -0
  15. quick_agent/quick_agent.py +171 -155
  16. quick_agent/schemas/outputs.py +6 -0
  17. quick_agent-0.1.3.data/data/quick_agent/agents/business-extract-structured.md +49 -0
  18. quick_agent-0.1.3.data/data/quick_agent/agents/business-extract.md +42 -0
  19. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/function-spec-validator.md +1 -1
  20. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validate-eval-list.md +1 -1
  21. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validator-contains.md +8 -1
  22. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/template.md +12 -1
  23. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/METADATA +21 -4
  24. quick_agent-0.1.3.dist-info/RECORD +52 -0
  25. tests/test_agent.py +273 -9
  26. tests/test_directory_permissions.py +10 -0
  27. tests/test_httpx_tools.py +295 -0
  28. tests/test_input_adaptors.py +31 -0
  29. tests/test_integration.py +134 -1
  30. tests/test_orchestrator.py +525 -111
  31. quick_agent-0.1.1.dist-info/RECORD +0 -45
  32. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/WHEEL +0 -0
  33. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/entry_points.txt +0 -0
  34. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/licenses/LICENSE +0 -0
  35. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/top_level.txt +0 -0
@@ -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(self, agent_id: str, input_path: Path, extra_tools: list[str] | None = None) -> BaseModel | str:
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
- input_path=input_path,
39
+ input_data=input_data,
33
40
  extra_tools=extra_tools,
34
41
  )
35
42
  return await agent.run()
quick_agent/prompting.py CHANGED
@@ -2,27 +2,45 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
- from typing import Any
5
+ from typing import Any, Mapping
6
+
7
+ import yaml
7
8
 
8
9
  from quick_agent.models.run_input import RunInput
9
10
 
10
11
 
11
- def make_user_prompt(step_prompt: str, run_input: RunInput, state: dict[str, Any]) -> str:
12
+ def make_user_prompt(run_input: RunInput, state: Mapping[str, Any]) -> str:
12
13
  """
13
14
  Creates a consistent user prompt payload. Consistency helps prefix-caching backends.
14
15
  """
15
16
  # Keep the preamble stable; append variable fields below.
16
- return f"""# Task Input
17
- source_path: {run_input.source_path}
18
- kind: {run_input.kind}
19
-
20
- ## Input Content
21
- {run_input.text}
22
-
23
- ## Chain State (JSON)
24
- {json.dumps(state, indent=2)}
25
-
26
- ## Step Instructions
27
- {step_prompt}
28
- """
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"
quick_agent/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561 type hints.
@@ -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.io_utils import load_input, write_output
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,68 +43,74 @@ class QuickAgent:
32
43
  tools: AgentTools,
33
44
  directory_permissions: DirectoryPermissions,
34
45
  agent_id: str,
35
- input_path: Path,
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._input_path = input_path
43
- self._extra_tools = extra_tools
44
-
45
- self.loaded = self._registry.get(self._agent_id)
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.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)
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
- self.run_input = load_input(self._input_path, self.permissions)
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 = list(dict.fromkeys((self.loaded.spec.tools or []) + (self._extra_tools or [])))
53
- self.toolset = 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()
54
71
 
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(self._agent_id)
72
+ self.model: OpenAIChatModel = build_model(self.loaded.spec.model)
73
+ self.model_settings_json: ModelSettings | None = self._build_model_settings(self.loaded.spec.model)
74
+ self.state: ChainState = self._init_state()
58
75
 
59
76
  async def run(self) -> BaseModel | str:
60
- self._tools.maybe_inject_agent_call(
61
- self.tool_ids,
62
- self.toolset,
63
- self.run_input.source_path,
64
- self._run_nested_agent,
65
- )
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
+ )
66
86
 
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
- )
87
+ final_output = await self._run_chain()
75
88
 
76
- out_path = self._write_final_output(self.loaded, final_output, self.permissions)
89
+ if self._write_output_file:
90
+ self._write_final_output(final_output)
77
91
 
78
- await self._handle_handoff(self.loaded, out_path)
92
+ await self._handle_handoff(final_output)
79
93
 
80
94
  return final_output
81
95
 
82
- async def _run_nested_agent(self, agent_id: str, input_path: Path) -> BaseModel | str:
96
+ async def _run_nested_agent(self, agent_id: str, input_data: InputAdaptor | Path) -> BaseModel | str:
97
+ nested_write_output = self.loaded.spec.nested_output == "file"
83
98
  agent = QuickAgent(
84
99
  registry=self._registry,
85
100
  tools=self._tools,
86
101
  directory_permissions=self._directory_permissions,
87
102
  agent_id=agent_id,
88
- input_path=input_path,
103
+ input_data=input_data,
89
104
  extra_tools=None,
105
+ write_output=nested_write_output,
90
106
  )
91
107
  return await agent.run()
92
108
 
93
- def _init_state(self, agent_id: str) -> dict[str, Any]:
109
+ def _init_state(self) -> ChainState:
94
110
  return {
95
- "agent_id": agent_id,
111
+ "agent_id": self._agent_id,
96
112
  "steps": {},
113
+ "final_output": None,
97
114
  }
98
115
 
99
116
  def _build_model_settings(self, model_spec: ModelSpec) -> ModelSettings | None:
@@ -103,21 +120,15 @@ class QuickAgent:
103
120
  return {"extra_body": {"format": "json"}}
104
121
  return None
105
122
 
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)
123
+ def _build_structured_model_settings(self, *, schema_cls: Type[BaseModel]) -> ModelSettings | None:
124
+ model_settings: ModelSettings | None = self.model_settings_json
125
+ provider = getattr(self.model, "provider", None)
115
126
  base_url = getattr(provider, "base_url", None)
116
127
  if base_url == "https://api.openai.com/v1":
117
- if model_settings_json is None:
128
+ if self.model_settings_json is None:
118
129
  model_settings_dict: ModelSettings = {}
119
130
  else:
120
- model_settings_dict = model_settings_json
131
+ model_settings_dict = self.model_settings_json
121
132
  extra_body_obj = model_settings_dict.get("extra_body")
122
133
  extra_body: dict[str, Any] = {}
123
134
  if isinstance(extra_body_obj, dict):
@@ -138,164 +149,169 @@ class QuickAgent:
138
149
  async def _run_step(
139
150
  self,
140
151
  *,
141
- step: Any,
142
- loaded: LoadedAgentFile,
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]:
152
+ step: ChainStepSpec,
153
+ ) -> tuple[StepOutput, BaseModel | str]:
149
154
  if step.kind == "text":
150
155
  return await self._run_text_step(
151
156
  step=step,
152
- loaded=loaded,
153
- model=model,
154
- toolset=toolset,
155
- run_input=run_input,
156
- state=state,
157
157
  )
158
158
 
159
159
  if step.kind == "structured":
160
160
  return await self._run_structured_step(
161
161
  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
162
  )
169
163
 
170
164
  raise NotImplementedError(f"Unknown step kind: {step.kind}")
171
165
 
172
- def _build_user_prompt(
173
- self,
174
- *,
175
- step: Any,
176
- loaded: LoadedAgentFile,
177
- run_input: Any,
178
- state: dict[str, Any],
179
- ) -> str:
180
- if step.prompt_section not in loaded.step_prompts:
181
- 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)
168
+
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
182
181
 
183
- step_prompt = loaded.step_prompts[step.prompt_section]
184
- return make_user_prompt(step_prompt, run_input, state)
182
+ def _normalize_system_prompt(self, text: str) -> str | list[str]:
183
+ if text:
184
+ return text
185
+ return []
185
186
 
186
187
  async def _run_text_step(
187
188
  self,
188
189
  *,
189
- step: Any,
190
- loaded: LoadedAgentFile,
191
- model: OpenAIChatModel,
192
- toolset: FunctionToolset[Any],
193
- run_input: Any,
194
- state: dict[str, Any],
195
- ) -> tuple[Any, BaseModel | str]:
196
- user_prompt = self._build_user_prompt(
197
- step=step,
198
- loaded=loaded,
199
- run_input=run_input,
200
- state=state,
201
- )
190
+ step: ChainStepSpec,
191
+ ) -> tuple[StepOutput, BaseModel | str]:
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()
202
196
  agent = Agent(
203
- model,
204
- instructions=loaded.body,
205
- toolsets=[toolset],
197
+ self.model,
198
+ instructions=step_instructions,
199
+ system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
200
+ toolsets=toolsets,
206
201
  output_type=str,
207
202
  )
208
203
  result = await agent.run(user_prompt)
209
204
  return result.output, result.output
210
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
+
211
219
  async def _run_structured_step(
212
220
  self,
213
221
  *,
214
- step: Any,
215
- loaded: LoadedAgentFile,
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]:
222
+ step: ChainStepSpec,
223
+ ) -> tuple[StepOutput, BaseModel | str]:
222
224
  if not step.output_schema:
223
225
  raise ValueError(f"Step {step.id} is structured but missing output_schema.")
224
- schema_cls = resolve_schema(loaded, step.output_schema)
226
+ schema_cls = resolve_schema(self.loaded, step.output_schema)
225
227
 
226
- model_settings = self._build_structured_model_settings(
227
- model=model,
228
- model_settings_json=model_settings_json,
229
- schema_cls=schema_cls,
230
- )
228
+ model_settings = self._build_structured_model_settings(schema_cls=schema_cls)
231
229
 
232
- user_prompt = self._build_user_prompt(
233
- step=step,
234
- loaded=loaded,
235
- run_input=run_input,
236
- state=state,
237
- )
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()
238
234
  agent = Agent(
239
- model,
240
- instructions=loaded.body,
241
- toolsets=[toolset],
242
- output_type=str,
235
+ self.model,
236
+ instructions=step_instructions,
237
+ system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
238
+ toolsets=toolsets,
239
+ output_type=schema_cls,
243
240
  model_settings=model_settings,
244
241
  )
245
242
  result = await agent.run(user_prompt)
246
243
  raw_output = result.output
247
- try:
248
- parsed = schema_cls.model_validate_json(raw_output)
249
- except ValidationError:
250
- extracted = extract_first_json_object(raw_output)
251
- parsed = schema_cls.model_validate_json(extracted)
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)
252
254
  return parsed.model_dump(), parsed
253
255
 
254
256
  async def _run_chain(
255
257
  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
258
  ) -> BaseModel | str:
259
+ if not self.loaded.spec.chain:
260
+ return await self._run_single_shot()
264
261
  final_output: BaseModel | str = ""
265
- for step in loaded.spec.chain:
262
+ for step in self.loaded.spec.chain:
266
263
  step_out, step_final = await self._run_step(
267
264
  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
265
  )
275
- state["steps"][step.id] = step_out
266
+ self.state["steps"][step.id] = step_out
267
+ self.state["final_output"] = step_out
276
268
  final_output = step_final
277
269
  return final_output
278
270
 
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)
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
+
294
+ def _write_final_output(self, final_output: BaseModel | str) -> Path:
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)
286
299
  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)
300
+ if self.loaded.spec.output.format == "json":
301
+ write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
289
302
  else:
290
- write_output(out_path, final_output.model_dump_json(indent=2), permissions)
303
+ write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
291
304
  else:
292
- write_output(out_path, str(final_output), permissions)
305
+ write_output(out_path, str(final_output), self.permissions)
293
306
  return out_path
294
307
 
295
- async def _handle_handoff(self, loaded: LoadedAgentFile, out_path: Path) -> None:
296
- if loaded.spec.handoff.enabled and loaded.spec.handoff.agent_id:
297
- # For a more robust version, generate an intermediate file for handoff input.
298
- await self._run_nested_agent(loaded.spec.handoff.agent_id, out_path)
308
+ async def _handle_handoff(self, final_output: BaseModel | str) -> None:
309
+ if self.loaded.spec.handoff.enabled and self.loaded.spec.handoff.agent_id:
310
+ if isinstance(final_output, BaseModel):
311
+ payload = final_output.model_dump_json(indent=2)
312
+ else:
313
+ payload = str(final_output)
314
+ await self._run_nested_agent(self.loaded.spec.handoff.agent_id, TextInput(payload))
299
315
 
300
316
 
301
317
  def resolve_schema(loaded: LoadedAgentFile, schema_name: str) -> Type[BaseModel]:
@@ -26,6 +26,12 @@ class ValidationResult(BaseModel):
26
26
  summary: str | None = None
27
27
 
28
28
 
29
+ class BusinessSummary(BaseModel):
30
+ company_name: str
31
+ location: str
32
+ summary: str
33
+
34
+
29
35
  class EvalRowResult(BaseModel):
30
36
  index: int = 0
31
37
  agent: str
@@ -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.