deer-agent-framework 0.0__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.
- deer/__init__.py +36 -0
- deer/builtins/__init__.py +7 -0
- deer/builtins/python_manager/agent.py +29 -0
- deer/builtins/python_manager/tools.py +54 -0
- deer/core/__init__.py +1 -0
- deer/core/agent.py +463 -0
- deer/core/ui.py +31 -0
- deer/drivers/__init__.py +66 -0
- deer/drivers/base_driver.py +56 -0
- deer/drivers/gemini_driver.py +62 -0
- deer/drivers/ollama_driver.py +69 -0
- deer/executor/__init__.py +1 -0
- deer/executor/executor.py +168 -0
- deer/executor/logic.py +75 -0
- deer/executor/logic_secure.py +102 -0
- deer/main.py +71 -0
- deer/planner/__init__.py +1 -0
- deer/planner/planner.py +71 -0
- deer/prompts/__init__.py +6 -0
- deer/prompts/error_explain.py +30 -0
- deer/prompts/goal_improvement.py +65 -0
- deer/prompts/goal_validation.py +31 -0
- deer/prompts/humanizer.py +20 -0
- deer/prompts/planner.py +92 -0
- deer/prompts/response_improvement.py +23 -0
- deer/schema/__init__.py +2 -0
- deer/schema/io.py +40 -0
- deer/schema/plan.py +75 -0
- deer/tools/__init__.py +4 -0
- deer/tools/base.py +141 -0
- deer/tools/builtin/__init__.py +3 -0
- deer/tools/builtin/file_manager.py +136 -0
- deer/tools/builtin/git_manager.py +67 -0
- deer/tools/builtin/search_manager.py +123 -0
- deer/tools/decorators.py +127 -0
- deer/tools/registry.py +114 -0
- deer/tracing/__init__.py +2 -0
- deer/tracing/logging_config.py +20 -0
- deer/tracing/store.py +21 -0
- deer/utils/__init__.py +0 -0
- deer/utils/console.py +11 -0
- deer/utils/plots/__init__.py +7 -0
- deer/utils/plots/plot_traces.py +1034 -0
- deer/validator/__init__.py +1 -0
- deer/validator/plan_validator.py +23 -0
- deer/validator/rules.py +98 -0
- deer_agent_framework-0.0.dist-info/METADATA +163 -0
- deer_agent_framework-0.0.dist-info/RECORD +52 -0
- deer_agent_framework-0.0.dist-info/WHEEL +5 -0
- deer_agent_framework-0.0.dist-info/entry_points.txt +2 -0
- deer_agent_framework-0.0.dist-info/licenses/LICENSE +24 -0
- deer_agent_framework-0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
ERROR_EXPLAIN_PROMPT = """
|
|
2
|
+
Execution error:
|
|
3
|
+
{error}
|
|
4
|
+
|
|
5
|
+
Available tools:
|
|
6
|
+
{tools}
|
|
7
|
+
|
|
8
|
+
Analysis task:
|
|
9
|
+
Determine whether the failure was caused by one of the following:
|
|
10
|
+
|
|
11
|
+
1. Missing capability
|
|
12
|
+
- The required operation cannot be performed because no suitable tool exists.
|
|
13
|
+
|
|
14
|
+
2. Incorrect tool selection
|
|
15
|
+
- A valid tool exists, but the wrong tool was selected.
|
|
16
|
+
|
|
17
|
+
3. Invalid tool usage
|
|
18
|
+
- A valid tool exists, but it was invoked with invalid arguments, invalid sequencing, or incompatible data.
|
|
19
|
+
|
|
20
|
+
4. Logic or execution failure
|
|
21
|
+
- The failure originated from executable logic, runtime behavior, or tool-side execution.
|
|
22
|
+
|
|
23
|
+
Requirements:
|
|
24
|
+
- Analyze the error using ONLY the available tools listed above.
|
|
25
|
+
- Do NOT invent capabilities that are not explicitly available.
|
|
26
|
+
- Explicitly state whether the system lacks the required tooling to complete the task.
|
|
27
|
+
- If a missing capability is detected, identify the exact missing operation.
|
|
28
|
+
- If a valid tool exists, identify which tool should have been used and why.
|
|
29
|
+
- Keep the explanation concise, deterministic, and technical.
|
|
30
|
+
"""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
GOAL_IMPROVEMENT_PROMPT = """
|
|
2
|
+
System role:
|
|
3
|
+
You are a deterministic prompt optimizer.
|
|
4
|
+
|
|
5
|
+
Your task is to improve the user's goal prompt while preserving:
|
|
6
|
+
- original intent,
|
|
7
|
+
- semantic meaning,
|
|
8
|
+
- requested outcome,
|
|
9
|
+
- operational constraints,
|
|
10
|
+
- and domain context.
|
|
11
|
+
|
|
12
|
+
Do not change the task itself.
|
|
13
|
+
|
|
14
|
+
The optimization must:
|
|
15
|
+
- increase clarity,
|
|
16
|
+
- reduce ambiguity,
|
|
17
|
+
- improve structural consistency,
|
|
18
|
+
- improve determinism,
|
|
19
|
+
- improve executability,
|
|
20
|
+
- and remove unnecessary wording.
|
|
21
|
+
|
|
22
|
+
Do NOT:
|
|
23
|
+
- introduce new requirements,
|
|
24
|
+
- invent assumptions,
|
|
25
|
+
- expand scope,
|
|
26
|
+
- add explanations,
|
|
27
|
+
- add conversational filler,
|
|
28
|
+
- or transform concise prompts into verbose prompts.
|
|
29
|
+
|
|
30
|
+
Prefer:
|
|
31
|
+
- explicit instructions,
|
|
32
|
+
- deterministic wording,
|
|
33
|
+
- structured constraints,
|
|
34
|
+
- normalized terminology,
|
|
35
|
+
- and unambiguous execution semantics.
|
|
36
|
+
|
|
37
|
+
Agent identity:
|
|
38
|
+
{identity}
|
|
39
|
+
|
|
40
|
+
The optimized prompt must reflect the agent identity when selecting:
|
|
41
|
+
- terminology,
|
|
42
|
+
- tools,
|
|
43
|
+
- transformations,
|
|
44
|
+
- formatting,
|
|
45
|
+
- assumptions,
|
|
46
|
+
- domain-specific reasoning,
|
|
47
|
+
- and response structure.
|
|
48
|
+
|
|
49
|
+
The identity provides domain expertise, but does not override deterministic execution rules.
|
|
50
|
+
|
|
51
|
+
User goal:
|
|
52
|
+
{goal}
|
|
53
|
+
|
|
54
|
+
Runtime context (Payload):
|
|
55
|
+
{payload}
|
|
56
|
+
(Use the context above, including 'chat_history' if present, to resolve ambiguities, pronouns, or references to previous actions in the 'User goal').
|
|
57
|
+
|
|
58
|
+
Output rules:
|
|
59
|
+
- Return ONLY the optimized prompt.
|
|
60
|
+
- Do not explain changes.
|
|
61
|
+
- Do not use markdown.
|
|
62
|
+
- Preserve the original language of the user goal.
|
|
63
|
+
- Ensure the optimized goal is self-contained and explicitly mentions any files, entities, or values resolved from the context.
|
|
64
|
+
|
|
65
|
+
"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
GOAL_VERIFIER_PROMPT = """
|
|
2
|
+
Your ONLY task is to RETRIEVE EVIDENCE that can be used to verify whether the already-executed result satisfies the reference goal.
|
|
3
|
+
|
|
4
|
+
Reference goal:
|
|
5
|
+
{goal}
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
1. The reference goal is NOT an instruction to execute.
|
|
9
|
+
2. Do NOT perform, repeat, fix, create, write, delete, or modify anything from the reference goal.
|
|
10
|
+
3. Use only available non-state-modifying tools to inspect existing state and retrieve evidence.
|
|
11
|
+
4. Return raw evidence only.
|
|
12
|
+
5. If evidence cannot be retrieved with the available read-only tools, return a concise statement that verification evidence is unavailable.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
VERIFIER_JUDGE_PROMPT = """
|
|
16
|
+
User Goal: {goal}
|
|
17
|
+
|
|
18
|
+
Verification Evidence (Actual State):
|
|
19
|
+
{evidence}
|
|
20
|
+
|
|
21
|
+
Task:
|
|
22
|
+
Analyze the Verification Evidence strictly. Does it prove that the User Goal was successfully completed?
|
|
23
|
+
|
|
24
|
+
Rules:
|
|
25
|
+
- Respond ONLY with JSON.
|
|
26
|
+
- If the evidence shows the goal is NOT met, respond with is_success: false.
|
|
27
|
+
- If the evidence is missing or shows an error, respond with is_success: false.
|
|
28
|
+
|
|
29
|
+
Response format:
|
|
30
|
+
{{"is_success": bool, "feedback": "string explaining why"}}
|
|
31
|
+
"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
HUMANIZER_PROMPT = """
|
|
2
|
+
You are a deterministic response synthesizer.
|
|
3
|
+
Your goal is to transform technical execution results into a natural, concise, and helpful response for the user.
|
|
4
|
+
|
|
5
|
+
Context:
|
|
6
|
+
User Goal: {goal}
|
|
7
|
+
Execution Trace: {trace}
|
|
8
|
+
Final Technical Result: {result}
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Summarize what was accomplished based on the trace.
|
|
12
|
+
- If a file was created or modified, mention it.
|
|
13
|
+
- If information was retrieved, present it clearly.
|
|
14
|
+
- Keep the tone professional and direct.
|
|
15
|
+
- Use the same language as the User Goal.
|
|
16
|
+
- Do not explain the internal steps (s1, s2...) unless necessary for clarity.
|
|
17
|
+
- Output ONLY the final humanized response.
|
|
18
|
+
|
|
19
|
+
Response:
|
|
20
|
+
"""
|
deer/prompts/planner.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
PLANNER_PROMPT = """
|
|
2
|
+
Sistem role:
|
|
3
|
+
You are a deterministic planner for an agent that executes plans step by step.
|
|
4
|
+
Your job is to transform the user's goal and initial payload into a structurally valid, deterministic, executable plan.
|
|
5
|
+
|
|
6
|
+
Agent identity:
|
|
7
|
+
{identity}
|
|
8
|
+
(The identity provides domain expertise but does not override deterministic execution rules).
|
|
9
|
+
|
|
10
|
+
Runtime executor:
|
|
11
|
+
- The Executor does NOT use an LLM and executes the generated plan exactly as written.
|
|
12
|
+
- Every step must be concrete, deterministic, and directly executable.
|
|
13
|
+
- A plan is an ordered list of steps. Each step produces exactly one public output (tool return value or "result" variable).
|
|
14
|
+
- A later step can consume the output of a previous step via "input_from".
|
|
15
|
+
- Inside tool parameters ("params") or logic, you can use "input" to refer to the direct dependency output (from "input_from"), or use a previous step ID (e.g., "s1") to refer to its output.
|
|
16
|
+
- In logic, "context" contains all previous outputs, and "params" are local constants.
|
|
17
|
+
|
|
18
|
+
Structural & Action Rules:
|
|
19
|
+
- Respond ONLY with valid JSON. No Markdown, no explanations, no text before/after.
|
|
20
|
+
- Root object must contain only a "steps" list.
|
|
21
|
+
- Each step must have: "id" (s1, s2...), "tool", "logic", "input_from", and "params" (a JSON object).
|
|
22
|
+
- Each step must define EXACTLY one action: either "tool" (name from Available tools) or "logic" (Python script).
|
|
23
|
+
- "input_from" must be null or a previous step ID. The first step MUST have "input_from": null.
|
|
24
|
+
|
|
25
|
+
Logic Script Rules:
|
|
26
|
+
- Use "logic" for deterministic transformations, calculations, or to provide a direct conversational response when no tools are required.
|
|
27
|
+
- Do NOT use "logic" to build large Markdown documents, source-code listings, or reports containing many files.
|
|
28
|
+
- If the task requires presenting large text, file contents, Markdown, or code blocks, keep those values as tool outputs or structured data and let the framework render them after execution.
|
|
29
|
+
- "logic" MUST use Python syntax (True, False, None), NOT JSON (true, false, null).
|
|
30
|
+
- It must assign the final step output to a variable named "result".
|
|
31
|
+
- Prohibited: imports, print, open, eval, exec, functions, classes, loops, try/except, with, global/nonlocal.
|
|
32
|
+
- Available names: input, params, context, pi, abs, min, max, round, str, int, float, len.
|
|
33
|
+
- Prefer simple expressions and short string literals.
|
|
34
|
+
- Avoid f-strings when interpolating dictionaries, tool outputs, JSON-like data, file contents, or any value that may contain braces, quotes, backticks, or newlines.
|
|
35
|
+
- Do not embed Markdown code fences, triple backticks, triple quotes, or complete source files inside "logic".
|
|
36
|
+
- Any multiline Python string MUST use triple quotes, but multiline strings should be avoided except for short messages.
|
|
37
|
+
- A string literal assigned in "logic" should be concise; large content must remain outside "logic".
|
|
38
|
+
- Never place literal newlines inside single-quoted or double-quoted strings.
|
|
39
|
+
- Generated Python code MUST always be syntactically valid.
|
|
40
|
+
|
|
41
|
+
Failure Handling & Task Limitations:
|
|
42
|
+
- Conversational Goals: If the user goal is purely conversational (greetings, identity questions, or general knowledge) and requires no tools, solve it with a single "logic" step assigning the response to "result".
|
|
43
|
+
- Missing Tools: If the user goal requires a specific technical action (e.g., file manipulation, git operations) for which no tool is available, you MUST return a valid Plan JSON with a single logic step.
|
|
44
|
+
- In the case of missing technical tools, "result" must contain:
|
|
45
|
+
1. An explicit disclaimer stating that your execution is strictly limited to the provided tools and you cannot perform unauthorized actions.
|
|
46
|
+
2. A technical explanation of why the specific action cannot be completed.
|
|
47
|
+
- NEVER attempt to bypass missing tools by using prohibited Python features (like 'open', 'os', or 'imports') inside a logic step.
|
|
48
|
+
|
|
49
|
+
Available tools:
|
|
50
|
+
{tools}
|
|
51
|
+
|
|
52
|
+
Example of Success (Transforming output from a tool):
|
|
53
|
+
{{
|
|
54
|
+
"steps": [
|
|
55
|
+
{{
|
|
56
|
+
"id": "s1",
|
|
57
|
+
"tool": "read_file",
|
|
58
|
+
"params": {{ "path": "hello.txt" }},
|
|
59
|
+
"input_from": null
|
|
60
|
+
}},
|
|
61
|
+
{{
|
|
62
|
+
"id": "s2",
|
|
63
|
+
"tool": null,
|
|
64
|
+
"logic": "result = content.upper()",
|
|
65
|
+
"input_from": "s1",
|
|
66
|
+
"params": {{}}
|
|
67
|
+
}}
|
|
68
|
+
]
|
|
69
|
+
}}
|
|
70
|
+
|
|
71
|
+
Example of Failure (User wants to perform an action, but the specific domain tool is missing entirely from the 'Available tools' list):
|
|
72
|
+
{{
|
|
73
|
+
"steps": [
|
|
74
|
+
{{
|
|
75
|
+
"id": "s1",
|
|
76
|
+
"tool": null,
|
|
77
|
+
"logic": "result = 'I cannot perform this task directly because my execution is limited to the provided tools, and no tool matching the required capability is currently available in the system configuration.'",
|
|
78
|
+
"input_from": null,
|
|
79
|
+
"params": {{}}
|
|
80
|
+
}}
|
|
81
|
+
]
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
User goal:
|
|
86
|
+
{goal}
|
|
87
|
+
|
|
88
|
+
Initial payload:
|
|
89
|
+
{payload}
|
|
90
|
+
|
|
91
|
+
Generate the final Plan JSON now.
|
|
92
|
+
""".strip()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
RESPONSE_IMPROVEMENT_PROMPT = """
|
|
2
|
+
You are a deterministic response formatter.
|
|
3
|
+
|
|
4
|
+
Your only job is formatting.
|
|
5
|
+
|
|
6
|
+
Rules:
|
|
7
|
+
- NEVER change the semantic meaning.
|
|
8
|
+
- NEVER rewrite sentences.
|
|
9
|
+
- NEVER summarize.
|
|
10
|
+
- NEVER explain.
|
|
11
|
+
- NEVER remove content.
|
|
12
|
+
- ONLY improve formatting.
|
|
13
|
+
- Add Markdown code fences when code is detected.
|
|
14
|
+
- Detect the correct language for code fences when possible.
|
|
15
|
+
- Preserve all code exactly.
|
|
16
|
+
- Preserve indentation exactly.
|
|
17
|
+
- If formatting is already correct, return the original response unchanged.
|
|
18
|
+
- If the format is "markdown" and the text contains Python or Bash code, YOU MUST wrap those code blocks using "~~~" fences (e.g., ~~~python or ~~~bash).
|
|
19
|
+
- Output only the final formatted response.
|
|
20
|
+
|
|
21
|
+
Original response:
|
|
22
|
+
{response}
|
|
23
|
+
"""
|
deer/schema/__init__.py
ADDED
deer/schema/io.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from typing import Any, Dict, Optional, List
|
|
3
|
+
from pydantic import BaseModel, Field, create_model
|
|
4
|
+
|
|
5
|
+
_counter = itertools.count()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentInput(BaseModel):
|
|
9
|
+
goal: str = Field(..., description="Goal or task to fulfill")
|
|
10
|
+
payload: Dict[str, Any] = Field(default_factory=dict, description="Initial data")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StepTrace(BaseModel):
|
|
14
|
+
step_id: str
|
|
15
|
+
tool: str
|
|
16
|
+
input: Any
|
|
17
|
+
output: Any
|
|
18
|
+
error: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Trace(BaseModel):
|
|
22
|
+
steps: List[StepTrace] = Field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentOutput(BaseModel):
|
|
26
|
+
result: Any = None
|
|
27
|
+
trace: List[StepTrace] = Field(default_factory=list)
|
|
28
|
+
validated: bool = False
|
|
29
|
+
# image: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def text(self):
|
|
33
|
+
return f"{str(self.result)}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def Return(**fields: Any) -> type[BaseModel]:
|
|
37
|
+
return create_model(
|
|
38
|
+
f"InlineModel_{next(_counter)}",
|
|
39
|
+
**{name: (field_type, ...) for name, field_type in fields.items()},
|
|
40
|
+
)
|
deer/schema/plan.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any, NamedTuple
|
|
2
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Step(BaseModel):
|
|
6
|
+
|
|
7
|
+
id: str = Field(..., description="Unique step identifier (e.g., s1, s2)")
|
|
8
|
+
|
|
9
|
+
tool: Optional[str] = Field(
|
|
10
|
+
None, description="Name of the tool to invoke. Optional if 'logic' is provided."
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logic: Optional[str] = Field(
|
|
14
|
+
None,
|
|
15
|
+
description="Restricted Python expression executed by the deterministic runtime.",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
input_from: Optional[str] = Field(
|
|
19
|
+
None,
|
|
20
|
+
description="ID of the previous step whose output will be passed as input.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
params: Dict[str, Any] = Field(
|
|
24
|
+
default_factory=dict,
|
|
25
|
+
description="Literal parameters or configuration for the tool/logic.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@model_validator(mode="before")
|
|
29
|
+
@classmethod
|
|
30
|
+
def _normalize_string_nulls(cls, data: Any) -> Any:
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
for field_name in ("tool", "logic", "input_from"):
|
|
35
|
+
if data.get(field_name) == "null":
|
|
36
|
+
data[field_name] = None
|
|
37
|
+
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
@field_validator("id")
|
|
41
|
+
@classmethod
|
|
42
|
+
def _non_empty_id(cls, v: str) -> str:
|
|
43
|
+
if not v or not v.strip():
|
|
44
|
+
raise ValueError("Step.id cannot be empty")
|
|
45
|
+
return v
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def _validate_action(self) -> "Step":
|
|
49
|
+
has_tool = bool(self.tool and self.tool.strip())
|
|
50
|
+
has_logic = bool(self.logic and self.logic.strip())
|
|
51
|
+
|
|
52
|
+
if has_tool == has_logic:
|
|
53
|
+
raise ValueError("A step must provide exactly one of 'tool' or 'logic'.")
|
|
54
|
+
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Plan(BaseModel):
|
|
59
|
+
|
|
60
|
+
steps: List[Step] = Field(
|
|
61
|
+
default_factory=list,
|
|
62
|
+
description="Ordered sequence of steps to be executed by the orchestrator.",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def step_by_id(self, step_id: str) -> Step:
|
|
66
|
+
for s in self.steps:
|
|
67
|
+
if s.id == step_id:
|
|
68
|
+
return s
|
|
69
|
+
raise KeyError(f"Step not found: {step_id}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class VerificationResult(NamedTuple):
|
|
73
|
+
is_success: bool
|
|
74
|
+
feedback: str
|
|
75
|
+
verification_plan: Optional[Plan] = None
|
deer/tools/__init__.py
ADDED
deer/tools/base.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Type
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import shlex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolProviderError(ValueError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandRunnerError(ValueError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ToolProvider:
|
|
21
|
+
# jail: Path | str | None = field(default=None, kw_only=True)
|
|
22
|
+
tools: list[str] | None = field(default=None, kw_only=True)
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
# pass
|
|
26
|
+
self.jail_ = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def jail(self):
|
|
30
|
+
assert self.jail_ is not None, (
|
|
31
|
+
"Filesystem jail is not configured. "
|
|
32
|
+
"The runtime cannot access the sandbox root path."
|
|
33
|
+
)
|
|
34
|
+
return self.jail_
|
|
35
|
+
|
|
36
|
+
@jail.setter
|
|
37
|
+
def jail(self, jail):
|
|
38
|
+
self.jail_ = Path(jail).resolve(strict=True)
|
|
39
|
+
|
|
40
|
+
def jailed_path(self, path: str | Path) -> Path:
|
|
41
|
+
"""
|
|
42
|
+
Validates and returns a safe path within the jail.
|
|
43
|
+
|
|
44
|
+
1. If 'path' is relative, it's joined to the jail.
|
|
45
|
+
2. If 'path' is absolute, it's checked for containment.
|
|
46
|
+
3. All '..' and symlinks are resolved before validation.
|
|
47
|
+
"""
|
|
48
|
+
path = Path(path)
|
|
49
|
+
|
|
50
|
+
# Handle point 2: If relative, interpret it as inside the jail.
|
|
51
|
+
# If absolute, it remains as is to be validated against the jail.
|
|
52
|
+
if not path.is_absolute():
|
|
53
|
+
path = self.jail / path
|
|
54
|
+
|
|
55
|
+
# Handle point 1: Normalize "..", symlinks, etc.
|
|
56
|
+
# strict=False allows the path to not exist yet (e.g., for creating files).
|
|
57
|
+
resolved = path.resolve(strict=False)
|
|
58
|
+
|
|
59
|
+
# Real containment verification
|
|
60
|
+
try:
|
|
61
|
+
# relative_to raises ValueError if 'resolved' is not a child of 'self.jail'
|
|
62
|
+
resolved.relative_to(self.jail)
|
|
63
|
+
except ValueError:
|
|
64
|
+
raise ToolProviderError(
|
|
65
|
+
f"Security breach: Path escapes jail: {resolved}"
|
|
66
|
+
) from None
|
|
67
|
+
|
|
68
|
+
return resolved
|
|
69
|
+
|
|
70
|
+
def run_command(
|
|
71
|
+
self,
|
|
72
|
+
command: str,
|
|
73
|
+
*,
|
|
74
|
+
cwd: str | Path,
|
|
75
|
+
timeout_seconds: int = 30,
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""Execute a command string without invoking a shell."""
|
|
78
|
+
args = shlex.split(command)
|
|
79
|
+
|
|
80
|
+
if not args:
|
|
81
|
+
raise CommandRunnerError("Command cannot be empty.")
|
|
82
|
+
|
|
83
|
+
safe_cwd = self.jail if cwd is None else self.jailed_path(cwd)
|
|
84
|
+
|
|
85
|
+
completed = subprocess.run(
|
|
86
|
+
args,
|
|
87
|
+
cwd=safe_cwd,
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=timeout_seconds,
|
|
91
|
+
check=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"stdout": completed.stdout,
|
|
96
|
+
"stderr": completed.stderr,
|
|
97
|
+
"returncode": completed.returncode,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Tool(ABC):
|
|
102
|
+
"""Base contract for deterministic tools."""
|
|
103
|
+
|
|
104
|
+
# input_schema: Type[Any] | None = None
|
|
105
|
+
# output_schema: Type[Any] | None = None
|
|
106
|
+
#
|
|
107
|
+
# name: str = ""
|
|
108
|
+
# description: str = ""
|
|
109
|
+
# modifies_state: bool = False
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
if not self.name:
|
|
113
|
+
self.name = self.__class__.__name__.lower()
|
|
114
|
+
|
|
115
|
+
def validate_input(self, value: Any) -> Any:
|
|
116
|
+
return self._validate_with_schema(self.params_type, value)
|
|
117
|
+
|
|
118
|
+
def validate_output(self, value: Any) -> Any:
|
|
119
|
+
return self._validate_with_schema(self.return_type, value)
|
|
120
|
+
|
|
121
|
+
def _validate_with_schema(self, schema: Type[Any] | None, value: Any) -> Any:
|
|
122
|
+
if schema is None:
|
|
123
|
+
return value
|
|
124
|
+
|
|
125
|
+
if isinstance(schema, type) and issubclass(schema, BaseModel):
|
|
126
|
+
return schema.model_validate(value)
|
|
127
|
+
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def run(self, params: dict[str, Any] | None = None) -> Any:
|
|
132
|
+
"""Execute the tool deterministically.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value: Main input value.
|
|
136
|
+
params: Optional literal parameters.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The deterministic output produced by the tool.
|
|
140
|
+
"""
|
|
141
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from deer.tools import ToolProvider, tool, Return
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileManagerError(ValueError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class FileManager(ToolProvider):
|
|
14
|
+
|
|
15
|
+
@tool(modifies_state=True)
|
|
16
|
+
def new_file(self, path: str, content: str) -> Return(exists=bool):
|
|
17
|
+
"""Creates a new file with the given content."""
|
|
18
|
+
safe_path = self.jailed_path(path)
|
|
19
|
+
|
|
20
|
+
safe_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
with open(safe_path, "w") as f:
|
|
23
|
+
f.write(content)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"exists": safe_path.exists(),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@tool()
|
|
30
|
+
def read_file(self, path: str) -> Return(content=str):
|
|
31
|
+
"""Reads the content of a file."""
|
|
32
|
+
safe_path = self.jailed_path(path)
|
|
33
|
+
|
|
34
|
+
with open(safe_path, "r") as f:
|
|
35
|
+
content = f.read()
|
|
36
|
+
return {
|
|
37
|
+
"content": content,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@tool(modifies_state=True)
|
|
41
|
+
def delete_file(self, path: str) -> Return(exists=bool):
|
|
42
|
+
"""Deletes a file."""
|
|
43
|
+
safe_path = self.jailed_path(path)
|
|
44
|
+
|
|
45
|
+
if safe_path.is_file():
|
|
46
|
+
safe_path.unlink()
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError(f"'{path}' is not a file or does not exist.")
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"exists": not safe_path.exists(),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@tool(modifies_state=True)
|
|
55
|
+
def create_directory(self, path: str) -> Return(status=str):
|
|
56
|
+
"""Creates a directory."""
|
|
57
|
+
safe_path = self.jailed_path(path)
|
|
58
|
+
safe_path.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
return {"status": "success"}
|
|
60
|
+
|
|
61
|
+
@tool(modifies_state=True)
|
|
62
|
+
def delete_directory(self, path: str) -> Return(exists=bool):
|
|
63
|
+
"""Removes a directory and all its contents recursively."""
|
|
64
|
+
safe_path = self.jailed_path(path)
|
|
65
|
+
|
|
66
|
+
if safe_path.is_dir():
|
|
67
|
+
shutil.rmtree(safe_path)
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError(f"'{path}' is not a directory or does not exist.")
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"exists": not safe_path.exists(),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@tool()
|
|
76
|
+
def get_file_info(self, path: str) -> Return(
|
|
77
|
+
exists=bool,
|
|
78
|
+
size_bytes=int,
|
|
79
|
+
is_dir=bool,
|
|
80
|
+
is_file=bool,
|
|
81
|
+
last_modified=float,
|
|
82
|
+
):
|
|
83
|
+
"""Retrieves metadata about a file or directory, including its existence, size, type, and modification time."""
|
|
84
|
+
safe_path = self.jailed_path(path)
|
|
85
|
+
if not safe_path.exists():
|
|
86
|
+
return {"exists": False}
|
|
87
|
+
|
|
88
|
+
stats = safe_path.stat()
|
|
89
|
+
return {
|
|
90
|
+
"exists": True,
|
|
91
|
+
"size_bytes": stats.st_size,
|
|
92
|
+
"is_dir": safe_path.is_dir(),
|
|
93
|
+
"is_file": safe_path.is_file(),
|
|
94
|
+
"last_modified": stats.st_mtime,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@tool()
|
|
98
|
+
def directory_tree(self, path: str, max_depth: int) -> Return(tree=dict):
|
|
99
|
+
"""Returns the directory structure as a nested dictionary."""
|
|
100
|
+
safe_path = self.jailed_path(path)
|
|
101
|
+
|
|
102
|
+
if not safe_path.exists():
|
|
103
|
+
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
104
|
+
|
|
105
|
+
if not safe_path.is_dir():
|
|
106
|
+
raise ValueError(f"Path is not a directory: {path}")
|
|
107
|
+
|
|
108
|
+
def build_node(current_path: Path, depth: int = 0) -> dict:
|
|
109
|
+
relative_path = current_path.relative_to(self.jail)
|
|
110
|
+
|
|
111
|
+
node = {
|
|
112
|
+
"name": current_path.name,
|
|
113
|
+
"path": str(relative_path),
|
|
114
|
+
"type": "directory" if current_path.is_dir() else "file",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if current_path.is_file():
|
|
118
|
+
node["size_bytes"] = current_path.stat().st_size
|
|
119
|
+
return node
|
|
120
|
+
|
|
121
|
+
if depth >= max_depth:
|
|
122
|
+
node["children"] = []
|
|
123
|
+
node["truncated"] = True
|
|
124
|
+
return node
|
|
125
|
+
|
|
126
|
+
children = sorted(
|
|
127
|
+
current_path.iterdir(),
|
|
128
|
+
key=lambda item: (not item.is_dir(), item.name.lower()),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
node["children"] = [build_node(child, depth + 1) for child in children]
|
|
132
|
+
return node
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"tree": build_node(safe_path),
|
|
136
|
+
}
|