quick-agent 0.1.1__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 +6 -0
- quick_agent/agent_call_tool.py +44 -0
- quick_agent/agent_registry.py +75 -0
- quick_agent/agent_tools.py +41 -0
- quick_agent/cli.py +44 -0
- quick_agent/directory_permissions.py +49 -0
- quick_agent/io_utils.py +36 -0
- quick_agent/json_utils.py +37 -0
- quick_agent/models/__init__.py +23 -0
- quick_agent/models/agent_spec.py +22 -0
- quick_agent/models/chain_step_spec.py +14 -0
- quick_agent/models/handoff_spec.py +13 -0
- quick_agent/models/loaded_agent_file.py +14 -0
- quick_agent/models/model_spec.py +14 -0
- quick_agent/models/output_spec.py +10 -0
- quick_agent/models/run_input.py +14 -0
- quick_agent/models/tool_impl_spec.py +11 -0
- quick_agent/models/tool_json.py +14 -0
- quick_agent/orchestrator.py +35 -0
- quick_agent/prompting.py +28 -0
- quick_agent/quick_agent.py +313 -0
- quick_agent/schemas/outputs.py +55 -0
- quick_agent/tools/__init__.py +0 -0
- quick_agent/tools/filesystem/__init__.py +0 -0
- quick_agent/tools/filesystem/adapter.py +26 -0
- quick_agent/tools/filesystem/read_text.py +16 -0
- quick_agent/tools/filesystem/write_text.py +19 -0
- quick_agent/tools/filesystem.read_text/tool.json +10 -0
- quick_agent/tools/filesystem.write_text/tool.json +10 -0
- quick_agent/tools_loader.py +76 -0
- quick_agent-0.1.1.data/data/quick_agent/agents/function-spec-validator.md +109 -0
- quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validate-eval-list.md +122 -0
- quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validator-contains.md +115 -0
- quick_agent-0.1.1.data/data/quick_agent/agents/template.md +88 -0
- quick_agent-0.1.1.dist-info/METADATA +918 -0
- quick_agent-0.1.1.dist-info/RECORD +45 -0
- quick_agent-0.1.1.dist-info/WHEEL +5 -0
- quick_agent-0.1.1.dist-info/entry_points.txt +2 -0
- quick_agent-0.1.1.dist-info/licenses/LICENSE +674 -0
- quick_agent-0.1.1.dist-info/top_level.txt +2 -0
- tests/test_agent.py +196 -0
- tests/test_directory_permissions.py +89 -0
- tests/test_integration.py +221 -0
- tests/test_orchestrator.py +797 -0
- tests/test_tools.py +25 -0
quick_agent/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Callable tool for inter-agent calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentCallTool:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
call_agent: Callable[[str, Path], Awaitable[BaseModel | str]],
|
|
15
|
+
run_input_source_path: str,
|
|
16
|
+
) -> None:
|
|
17
|
+
self._call_agent = call_agent
|
|
18
|
+
self._run_input_source_path = run_input_source_path
|
|
19
|
+
self.__name__ = "agent_call"
|
|
20
|
+
|
|
21
|
+
def _resolve_input_file(self, raw_input_file: str) -> Path:
|
|
22
|
+
cleaned = raw_input_file.strip()
|
|
23
|
+
if (
|
|
24
|
+
(cleaned.startswith("\"") and cleaned.endswith("\""))
|
|
25
|
+
or (cleaned.startswith("'") and cleaned.endswith("'"))
|
|
26
|
+
):
|
|
27
|
+
cleaned = cleaned[1:-1]
|
|
28
|
+
base_dir = Path(self._run_input_source_path).parent
|
|
29
|
+
cleaned = cleaned.replace("{base_directory}", str(base_dir))
|
|
30
|
+
path = Path(cleaned)
|
|
31
|
+
if not path.is_absolute():
|
|
32
|
+
path = base_dir / path
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
async def __call__(self, agent: str, input_file: str) -> dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Call another agent by ID with an input file path.
|
|
38
|
+
Returns JSON-serializable dict output if structured, else {"text": "..."}.
|
|
39
|
+
"""
|
|
40
|
+
resolved_input = self._resolve_input_file(input_file)
|
|
41
|
+
out = await self._call_agent(agent, resolved_input)
|
|
42
|
+
if isinstance(out, BaseModel):
|
|
43
|
+
return out.model_dump()
|
|
44
|
+
return {"text": out}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Agent registry and parsing utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import frontmatter
|
|
9
|
+
|
|
10
|
+
from quick_agent.models.agent_spec import AgentSpec
|
|
11
|
+
from quick_agent.models.loaded_agent_file import LoadedAgentFile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def split_step_sections(markdown_body: str) -> dict[str, str]:
|
|
15
|
+
"""
|
|
16
|
+
Extracts blocks that begin with headings "## step:<id>".
|
|
17
|
+
Returns mapping: "step:<id>" -> content for that step (excluding heading line).
|
|
18
|
+
"""
|
|
19
|
+
pattern = re.compile(r"^##\s+(step:[A-Za-z0-9_\-]+)\s*$", re.MULTILINE)
|
|
20
|
+
matches = list(pattern.finditer(markdown_body))
|
|
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)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AgentRegistry:
|
|
40
|
+
def __init__(self, agent_roots: list[Path]):
|
|
41
|
+
self.agent_roots = agent_roots
|
|
42
|
+
self._cache: dict[str, LoadedAgentFile] = {}
|
|
43
|
+
self._index: dict[str, Path] | None = None
|
|
44
|
+
|
|
45
|
+
def _build_index(self) -> dict[str, Path]:
|
|
46
|
+
index: dict[str, Path] = {}
|
|
47
|
+
for root in self.agent_roots:
|
|
48
|
+
if not root.exists():
|
|
49
|
+
continue
|
|
50
|
+
for path in root.rglob("*.md"):
|
|
51
|
+
agent_id = path.stem
|
|
52
|
+
if agent_id in index:
|
|
53
|
+
continue
|
|
54
|
+
index[agent_id] = path
|
|
55
|
+
return index
|
|
56
|
+
|
|
57
|
+
def _get_index(self) -> dict[str, Path]:
|
|
58
|
+
if self._index is None:
|
|
59
|
+
self._index = self._build_index()
|
|
60
|
+
return self._index
|
|
61
|
+
|
|
62
|
+
def list_agents(self) -> list[str]:
|
|
63
|
+
index = self._get_index()
|
|
64
|
+
return sorted(index.keys())
|
|
65
|
+
|
|
66
|
+
def get(self, agent_id: str) -> LoadedAgentFile:
|
|
67
|
+
if agent_id in self._cache:
|
|
68
|
+
return self._cache[agent_id]
|
|
69
|
+
index = self._get_index()
|
|
70
|
+
path = index.get(agent_id)
|
|
71
|
+
if path is None:
|
|
72
|
+
raise FileNotFoundError(f"Agent not found: {agent_id} (searched: {self.agent_roots})")
|
|
73
|
+
loaded = load_agent_file(path)
|
|
74
|
+
self._cache[agent_id] = loaded
|
|
75
|
+
return loaded
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Toolset builder and agent-call injection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_ai.toolsets import FunctionToolset
|
|
10
|
+
|
|
11
|
+
from quick_agent.agent_call_tool import AgentCallTool
|
|
12
|
+
from quick_agent.directory_permissions import DirectoryPermissions
|
|
13
|
+
from quick_agent.tools_loader import load_tools
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentTools:
|
|
17
|
+
def __init__(self, tool_roots: list[Path]) -> None:
|
|
18
|
+
self._tool_roots = tool_roots
|
|
19
|
+
|
|
20
|
+
def build_toolset(self, tool_ids: list[str], permissions: DirectoryPermissions) -> FunctionToolset[Any]:
|
|
21
|
+
tool_ids_for_disk = [tool_id for tool_id in tool_ids if tool_id != "agent.call"]
|
|
22
|
+
if tool_ids_for_disk:
|
|
23
|
+
return load_tools(self._tool_roots, tool_ids_for_disk, permissions)
|
|
24
|
+
return FunctionToolset()
|
|
25
|
+
|
|
26
|
+
def maybe_inject_agent_call(
|
|
27
|
+
self,
|
|
28
|
+
tool_ids: list[str],
|
|
29
|
+
toolset: FunctionToolset[Any],
|
|
30
|
+
run_input_source_path: str,
|
|
31
|
+
call_agent: Callable[[str, Path], Awaitable[BaseModel | str]],
|
|
32
|
+
) -> None:
|
|
33
|
+
if "agent.call" not in tool_ids:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
tool = AgentCallTool(call_agent, run_input_source_path)
|
|
37
|
+
toolset.add_function(
|
|
38
|
+
func=tool.__call__,
|
|
39
|
+
name="agent_call",
|
|
40
|
+
description="Call another agent and return its output.",
|
|
41
|
+
)
|
quick_agent/cli.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from quick_agent.orchestrator import Orchestrator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
parser = argparse.ArgumentParser()
|
|
15
|
+
parser.add_argument("--agents-dir", type=str, default="agents")
|
|
16
|
+
parser.add_argument("--tools-dir", type=str, default="tools")
|
|
17
|
+
parser.add_argument("--safe-dir", type=str, default="safe")
|
|
18
|
+
parser.add_argument("--agent", type=str, required=True)
|
|
19
|
+
parser.add_argument("--input", type=str, required=True)
|
|
20
|
+
parser.add_argument("--tool", action="append", default=[], help="Extra tool IDs to add at runtime")
|
|
21
|
+
args = parser.parse_args()
|
|
22
|
+
|
|
23
|
+
package_root = Path(__file__).resolve().parent
|
|
24
|
+
system_agents_dir = package_root / "agents"
|
|
25
|
+
system_tools_dir = package_root / "tools"
|
|
26
|
+
user_agents_dir = Path(args.agents_dir)
|
|
27
|
+
user_tools_dir = Path(args.tools_dir)
|
|
28
|
+
|
|
29
|
+
agent_roots = [user_agents_dir, system_agents_dir]
|
|
30
|
+
tool_roots = [user_tools_dir, system_tools_dir]
|
|
31
|
+
|
|
32
|
+
orch = Orchestrator(agent_roots, tool_roots, Path(args.safe_dir))
|
|
33
|
+
|
|
34
|
+
# Async entrypoint
|
|
35
|
+
import anyio
|
|
36
|
+
|
|
37
|
+
async def runner():
|
|
38
|
+
return await orch.run(args.agent, Path(args.input), extra_tools=args.tool)
|
|
39
|
+
|
|
40
|
+
out = anyio.run(runner)
|
|
41
|
+
if isinstance(out, BaseModel):
|
|
42
|
+
print(out.model_dump_json(indent=2))
|
|
43
|
+
else:
|
|
44
|
+
print(out)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Directory permission enforcement for file access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DirectoryPermissions:
|
|
9
|
+
def __init__(self, root: Path) -> None:
|
|
10
|
+
self._root = root.expanduser().resolve(strict=False)
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def root(self) -> Path:
|
|
14
|
+
return self._root
|
|
15
|
+
|
|
16
|
+
def scoped(self, directory: str | None) -> "DirectoryPermissions":
|
|
17
|
+
if directory:
|
|
18
|
+
candidate = (self._root / directory).expanduser().resolve(strict=False)
|
|
19
|
+
root_resolved = self._root.expanduser().resolve(strict=False)
|
|
20
|
+
if not candidate.is_relative_to(root_resolved):
|
|
21
|
+
raise ValueError(f"Scoped directory {directory!r} escapes safe root {root_resolved}.")
|
|
22
|
+
return DirectoryPermissions(candidate)
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def resolve(self, path: Path, *, for_write: bool) -> Path:
|
|
26
|
+
target = path
|
|
27
|
+
if not target.is_absolute():
|
|
28
|
+
target = self._root / target
|
|
29
|
+
resolved = target.expanduser().resolve(strict=False)
|
|
30
|
+
root_resolved = self._root.expanduser().resolve(strict=False)
|
|
31
|
+
if not resolved.is_relative_to(root_resolved):
|
|
32
|
+
raise PermissionError(f"Path {resolved} is outside safe directory {root_resolved}.")
|
|
33
|
+
if for_write:
|
|
34
|
+
root_resolved.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
return resolved
|
|
36
|
+
|
|
37
|
+
def can_read(self, path: Path) -> bool:
|
|
38
|
+
try:
|
|
39
|
+
self.resolve(path, for_write=False)
|
|
40
|
+
except PermissionError:
|
|
41
|
+
return False
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
def can_write(self, path: Path) -> bool:
|
|
45
|
+
try:
|
|
46
|
+
self.resolve(path, for_write=False)
|
|
47
|
+
except PermissionError:
|
|
48
|
+
return False
|
|
49
|
+
return True
|
quick_agent/io_utils.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Input/output helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from quick_agent.directory_permissions import DirectoryPermissions
|
|
9
|
+
from quick_agent.models.run_input import RunInput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_input(path: Path, permissions: DirectoryPermissions) -> RunInput:
|
|
13
|
+
safe_path = permissions.resolve(path, for_write=False)
|
|
14
|
+
if not safe_path.exists():
|
|
15
|
+
raise FileNotFoundError(safe_path)
|
|
16
|
+
|
|
17
|
+
if safe_path.suffix.lower() == ".json":
|
|
18
|
+
raw = json.loads(safe_path.read_text(encoding="utf-8"))
|
|
19
|
+
return RunInput(source_path=str(safe_path), kind="json", text=json.dumps(raw, indent=2), data=raw)
|
|
20
|
+
txt = safe_path.read_text(encoding="utf-8")
|
|
21
|
+
return RunInput(source_path=str(safe_path), kind="text", text=txt, data=None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ensure_parent_dir(path: Path, permissions: DirectoryPermissions) -> Path:
|
|
25
|
+
safe_path = permissions.resolve(path, for_write=True)
|
|
26
|
+
safe_path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return safe_path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_output(
|
|
31
|
+
path: Path,
|
|
32
|
+
content: str,
|
|
33
|
+
permissions: DirectoryPermissions,
|
|
34
|
+
) -> None:
|
|
35
|
+
safe_path = ensure_parent_dir(path, permissions)
|
|
36
|
+
safe_path.write_text(content, encoding="utf-8")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""JSON parsing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_first_json_object(text: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Extract the first top-level JSON object from text.
|
|
9
|
+
This is a fallback for models that wrap JSON in extra text.
|
|
10
|
+
"""
|
|
11
|
+
start = text.find("{")
|
|
12
|
+
if start == -1:
|
|
13
|
+
raise ValueError("No JSON object found in model output.")
|
|
14
|
+
|
|
15
|
+
depth = 0
|
|
16
|
+
in_string = False
|
|
17
|
+
escape = False
|
|
18
|
+
for i in range(start, len(text)):
|
|
19
|
+
ch = text[i]
|
|
20
|
+
if in_string:
|
|
21
|
+
if escape:
|
|
22
|
+
escape = False
|
|
23
|
+
elif ch == "\\":
|
|
24
|
+
escape = True
|
|
25
|
+
elif ch == "\"":
|
|
26
|
+
in_string = False
|
|
27
|
+
else:
|
|
28
|
+
if ch == "\"":
|
|
29
|
+
in_string = True
|
|
30
|
+
elif ch == "{":
|
|
31
|
+
depth += 1
|
|
32
|
+
elif ch == "}":
|
|
33
|
+
depth -= 1
|
|
34
|
+
if depth == 0:
|
|
35
|
+
return text[start : i + 1]
|
|
36
|
+
|
|
37
|
+
raise ValueError("Unbalanced JSON object in model output.")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Model types for agent configuration and runtime."""
|
|
2
|
+
|
|
3
|
+
from quick_agent.models.agent_spec import AgentSpec
|
|
4
|
+
from quick_agent.models.chain_step_spec import ChainStepSpec
|
|
5
|
+
from quick_agent.models.handoff_spec import HandoffSpec
|
|
6
|
+
from quick_agent.models.loaded_agent_file import LoadedAgentFile
|
|
7
|
+
from quick_agent.models.model_spec import ModelSpec
|
|
8
|
+
from quick_agent.models.output_spec import OutputSpec
|
|
9
|
+
from quick_agent.models.run_input import RunInput
|
|
10
|
+
from quick_agent.models.tool_impl_spec import ToolImplSpec
|
|
11
|
+
from quick_agent.models.tool_json import ToolJson
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentSpec",
|
|
15
|
+
"ChainStepSpec",
|
|
16
|
+
"HandoffSpec",
|
|
17
|
+
"LoadedAgentFile",
|
|
18
|
+
"ModelSpec",
|
|
19
|
+
"OutputSpec",
|
|
20
|
+
"RunInput",
|
|
21
|
+
"ToolImplSpec",
|
|
22
|
+
"ToolJson",
|
|
23
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Pydantic model for agent frontmatter spec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from quick_agent.models.chain_step_spec import ChainStepSpec
|
|
8
|
+
from quick_agent.models.handoff_spec import HandoffSpec
|
|
9
|
+
from quick_agent.models.model_spec import ModelSpec
|
|
10
|
+
from quick_agent.models.output_spec import OutputSpec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentSpec(BaseModel):
|
|
14
|
+
name: str
|
|
15
|
+
description: str = ""
|
|
16
|
+
model: ModelSpec = Field(default_factory=ModelSpec)
|
|
17
|
+
tools: list[str] = Field(default_factory=list)
|
|
18
|
+
schemas: dict[str, str] = Field(default_factory=dict) # alias -> "module:ClassName"
|
|
19
|
+
chain: list[ChainStepSpec]
|
|
20
|
+
output: OutputSpec = Field(default_factory=OutputSpec)
|
|
21
|
+
handoff: HandoffSpec = Field(default_factory=HandoffSpec)
|
|
22
|
+
safe_dir: str | None = None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pydantic model for a single chain step."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChainStepSpec(BaseModel):
|
|
11
|
+
id: str
|
|
12
|
+
kind: str # "text" or "structured" (you may extend: "parallel_map", "fanout", etc.)
|
|
13
|
+
prompt_section: str
|
|
14
|
+
output_schema: Optional[str] = None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Pydantic model for handoff configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HandoffSpec(BaseModel):
|
|
11
|
+
enabled: bool = False
|
|
12
|
+
agent_id: Optional[str] = None
|
|
13
|
+
input_mode: str = "final_output_json"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Loaded agent markdown plus parsed metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from quick_agent.models.agent_spec import AgentSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class LoadedAgentFile:
|
|
12
|
+
spec: AgentSpec
|
|
13
|
+
body: str
|
|
14
|
+
step_prompts: dict[str, str] # prompt_section -> markdown chunk
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pydantic model for LLM configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelSpec(BaseModel):
|
|
9
|
+
provider: str = Field(default="openai-compatible")
|
|
10
|
+
base_url: str = Field(default="https://api.openai.com/v1")
|
|
11
|
+
api_key_env: str = Field(default="OPENAI_API_KEY")
|
|
12
|
+
model_name: str = Field(default="gpt-5.2")
|
|
13
|
+
temperature: float = 0.2
|
|
14
|
+
max_tokens: int = 2048
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pydantic model for runtime input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunInput(BaseModel):
|
|
11
|
+
source_path: str
|
|
12
|
+
kind: str # "json" or "text"
|
|
13
|
+
text: str
|
|
14
|
+
data: Optional[dict[str, Any]] = None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pydantic model for tool.json files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from quick_agent.models.tool_impl_spec import ToolImplSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolJson(BaseModel):
|
|
11
|
+
id: str
|
|
12
|
+
name: str
|
|
13
|
+
description: str = ""
|
|
14
|
+
impl: ToolImplSpec
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Helper for running agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from quick_agent.agent_registry import AgentRegistry
|
|
10
|
+
from quick_agent.agent_tools import AgentTools
|
|
11
|
+
from quick_agent.directory_permissions import DirectoryPermissions
|
|
12
|
+
from quick_agent.quick_agent import QuickAgent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Orchestrator:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
agent_roots: list[Path],
|
|
19
|
+
tool_roots: list[Path],
|
|
20
|
+
safe_dir: Path,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.registry = AgentRegistry(agent_roots)
|
|
23
|
+
self.tools = AgentTools(tool_roots)
|
|
24
|
+
self.directory_permissions = DirectoryPermissions(safe_dir)
|
|
25
|
+
|
|
26
|
+
async def run(self, agent_id: str, input_path: Path, extra_tools: list[str] | None = None) -> BaseModel | str:
|
|
27
|
+
agent = QuickAgent(
|
|
28
|
+
registry=self.registry,
|
|
29
|
+
tools=self.tools,
|
|
30
|
+
directory_permissions=self.directory_permissions,
|
|
31
|
+
agent_id=agent_id,
|
|
32
|
+
input_path=input_path,
|
|
33
|
+
extra_tools=extra_tools,
|
|
34
|
+
)
|
|
35
|
+
return await agent.run()
|
quick_agent/prompting.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Prompt composition helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from quick_agent.models.run_input import RunInput
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_user_prompt(step_prompt: str, run_input: RunInput, state: dict[str, Any]) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Creates a consistent user prompt payload. Consistency helps prefix-caching backends.
|
|
14
|
+
"""
|
|
15
|
+
# 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
|
+
"""
|