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.
- quick_agent/__init__.py +4 -1
- quick_agent/agent_call_tool.py +22 -5
- quick_agent/agent_registry.py +7 -27
- 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/models/loaded_agent_file.py +136 -1
- quick_agent/models/output_spec.py +1 -1
- quick_agent/orchestrator.py +15 -8
- quick_agent/prompting.py +34 -16
- quick_agent/py.typed +1 -0
- quick_agent/quick_agent.py +171 -155
- quick_agent/schemas/outputs.py +6 -0
- quick_agent-0.1.3.data/data/quick_agent/agents/business-extract-structured.md +49 -0
- quick_agent-0.1.3.data/data/quick_agent/agents/business-extract.md +42 -0
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/function-spec-validator.md +1 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validate-eval-list.md +1 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validator-contains.md +8 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/template.md +12 -1
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/METADATA +21 -4
- quick_agent-0.1.3.dist-info/RECORD +52 -0
- tests/test_agent.py +273 -9
- tests/test_directory_permissions.py +10 -0
- tests/test_httpx_tools.py +295 -0
- tests/test_input_adaptors.py +31 -0
- tests/test_integration.py +134 -1
- tests/test_orchestrator.py +525 -111
- quick_agent-0.1.1.dist-info/RECORD +0 -45
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/WHEEL +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/entry_points.txt +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/top_level.txt +0 -0
quick_agent/orchestrator.py
CHANGED
|
@@ -3,33 +3,40 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
9
10
|
from quick_agent.agent_registry import AgentRegistry
|
|
10
11
|
from quick_agent.agent_tools import AgentTools
|
|
11
12
|
from quick_agent.directory_permissions import DirectoryPermissions
|
|
13
|
+
from quick_agent.input_adaptors import InputAdaptor
|
|
12
14
|
from quick_agent.quick_agent import QuickAgent
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class Orchestrator:
|
|
16
18
|
def __init__(
|
|
17
19
|
self,
|
|
18
|
-
agent_roots: list[Path],
|
|
19
|
-
tool_roots: list[Path],
|
|
20
|
-
safe_dir: Path,
|
|
20
|
+
agent_roots: list[Path] | None = None,
|
|
21
|
+
tool_roots: list[Path] | None = None,
|
|
22
|
+
safe_dir: Optional[Path] = None,
|
|
21
23
|
) -> None:
|
|
22
|
-
self.registry = AgentRegistry(agent_roots)
|
|
23
|
-
self.tools = AgentTools(tool_roots)
|
|
24
|
-
self.directory_permissions = DirectoryPermissions(safe_dir)
|
|
24
|
+
self.registry: AgentRegistry = AgentRegistry(agent_roots or [])
|
|
25
|
+
self.tools: AgentTools = AgentTools(tool_roots or [])
|
|
26
|
+
self.directory_permissions: DirectoryPermissions = DirectoryPermissions(safe_dir)
|
|
25
27
|
|
|
26
|
-
async def run(
|
|
28
|
+
async def run(
|
|
29
|
+
self,
|
|
30
|
+
agent_id: str,
|
|
31
|
+
input_data: InputAdaptor | Path,
|
|
32
|
+
extra_tools: list[str] | None = None,
|
|
33
|
+
) -> BaseModel | str:
|
|
27
34
|
agent = QuickAgent(
|
|
28
35
|
registry=self.registry,
|
|
29
36
|
tools=self.tools,
|
|
30
37
|
directory_permissions=self.directory_permissions,
|
|
31
38
|
agent_id=agent_id,
|
|
32
|
-
|
|
39
|
+
input_data=input_data,
|
|
33
40
|
extra_tools=extra_tools,
|
|
34
41
|
)
|
|
35
42
|
return await agent.run()
|
quick_agent/prompting.py
CHANGED
|
@@ -2,27 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
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(
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
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.
|
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,68 +43,74 @@ 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.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
|
-
|
|
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
|
-
self.toolset = self.
|
|
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(
|
|
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.
|
|
61
|
-
self.
|
|
62
|
-
|
|
63
|
-
self.
|
|
64
|
-
|
|
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
|
-
|
|
89
|
+
if self._write_output_file:
|
|
90
|
+
self._write_final_output(final_output)
|
|
77
91
|
|
|
78
|
-
await self._handle_handoff(
|
|
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,
|
|
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
|
-
|
|
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
|
|
109
|
+
def _init_state(self) -> ChainState:
|
|
94
110
|
return {
|
|
95
|
-
"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:
|
|
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]:
|
|
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
|
-
|
|
176
|
-
loaded:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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=
|
|
205
|
-
|
|
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:
|
|
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]:
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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=
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
parsed =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
280
|
-
self
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
) ->
|
|
285
|
-
|
|
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,
|
|
296
|
-
if loaded.spec.handoff.enabled and loaded.spec.handoff.agent_id:
|
|
297
|
-
|
|
298
|
-
|
|
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]:
|
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
|
+
## 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.
|