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,56 @@
|
|
|
1
|
+
from typing import Protocol, Type, TypeVar
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
from typing import TypeVar, Type, Optional, Any
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
# T represents any class inheriting from BaseModel
|
|
12
|
+
T = TypeVar("T", bound=BaseModel)
|
|
13
|
+
logger = logging.getLogger("DEER-LLM")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LLMDriver(Protocol):
|
|
17
|
+
"""Contract for drivers supporting Architectural Brilliance."""
|
|
18
|
+
temperature_json = 0.0
|
|
19
|
+
top_p = 1.0
|
|
20
|
+
|
|
21
|
+
def __init__(self, model_name: str):
|
|
22
|
+
self.model_name = model_name
|
|
23
|
+
|
|
24
|
+
def _send_post_request(self, url: str, payload: dict, headers: Optional[dict] = None) -> dict:
|
|
25
|
+
"""Centralized helper to handle HTTP POST requests for all drivers."""
|
|
26
|
+
if headers is None:
|
|
27
|
+
headers = {"Content-Type": "application/json"}
|
|
28
|
+
else:
|
|
29
|
+
headers.setdefault("Content-Type", "application/json")
|
|
30
|
+
|
|
31
|
+
data = json.dumps(payload).encode("utf-8")
|
|
32
|
+
req = urllib.request.Request(url, data=data, headers=headers)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
with urllib.request.urlopen(req) as response:
|
|
36
|
+
return json.loads(response.read().decode("utf-8"))
|
|
37
|
+
except urllib.error.HTTPError as e:
|
|
38
|
+
error_body = e.read().decode("utf-8")
|
|
39
|
+
logger.error(f"HTTP Error {e.code}: {error_body}")
|
|
40
|
+
raise RuntimeError(f"API request failed with status {e.code}: {error_body}")
|
|
41
|
+
except urllib.error.URLError as e:
|
|
42
|
+
logger.error(f"URL Error: {e.reason}")
|
|
43
|
+
raise RuntimeError(f"Failed to reach server: {e.reason}")
|
|
44
|
+
|
|
45
|
+
def generate_text(self, prompt: str) -> str:
|
|
46
|
+
raise NotImplementedError("Subclasses must implement generate_text")
|
|
47
|
+
|
|
48
|
+
def generate_json(self, prompt: str, response_model: Optional[Type[T]] = None) -> T:
|
|
49
|
+
raise NotImplementedError("Subclasses must implement generate_json")
|
|
50
|
+
|
|
51
|
+
def extract_json(self, text: str) -> dict:
|
|
52
|
+
text = text.strip()
|
|
53
|
+
text = re.sub(r"^```(?:json)?\s*", "", text)
|
|
54
|
+
text = re.sub(r"^json\s*", "", text)
|
|
55
|
+
text = re.sub(r"\s*```$", "", text)
|
|
56
|
+
return json.loads(text)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import TypeVar, Type, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from .base_driver import LLMDriver, logger
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=BaseModel)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GeminiDriver(LLMDriver):
|
|
13
|
+
def __init__(self, model_name: str, api_version: str = "v1beta"):
|
|
14
|
+
super().__init__(model_name)
|
|
15
|
+
self.api_key = os.getenv("GEMINI_API_KEY")
|
|
16
|
+
if not self.api_key:
|
|
17
|
+
raise ValueError("GEMINI_API_KEY environment variable is not set.")
|
|
18
|
+
|
|
19
|
+
# Setup the definitive REST endpoint for Gemini
|
|
20
|
+
self.url = f"https://generativelanguage.googleapis.com/{api_version}/models/{self.model_name}:generateContent?key={self.api_key}"
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return "Gemini"
|
|
24
|
+
|
|
25
|
+
def generate_text(self, prompt: str) -> str:
|
|
26
|
+
logger.debug(f"Generating text with model {self.model_name} and prompt: {prompt}")
|
|
27
|
+
|
|
28
|
+
payload = {
|
|
29
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
30
|
+
"generationConfig": {"responseMimeType": "text/plain"}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
response_json = self._send_post_request(self.url, payload)
|
|
35
|
+
return response_json["candidates"][0]["content"]["parts"][0]["text"]
|
|
36
|
+
except (KeyError, IndexError) as e:
|
|
37
|
+
logger.error(f"Unexpected response structure from Gemini API: {e}")
|
|
38
|
+
raise RuntimeError("Failed to parse text from Gemini API response.")
|
|
39
|
+
|
|
40
|
+
def generate_json(self, prompt: str, response_model: Optional[Type[T]] = None) -> T:
|
|
41
|
+
logger.debug(f"Generating JSON with model {self.model_name} and prompt: {prompt}")
|
|
42
|
+
|
|
43
|
+
payload = {
|
|
44
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
45
|
+
"generationConfig": {"responseMimeType": "application/json"}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if response_model:
|
|
49
|
+
payload["generationConfig"]["responseSchema"] = response_model.model_json_schema()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response_json = self._send_post_request(self.url, payload)
|
|
53
|
+
response_text = response_json["candidates"][0]["content"]["parts"][0]["text"]
|
|
54
|
+
data = json.loads(response_text)
|
|
55
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
|
56
|
+
logger.error(f"Error parsing JSON response from Gemini: {e}")
|
|
57
|
+
raise RuntimeError(f"Error processing JSON output from Gemini: {e}")
|
|
58
|
+
|
|
59
|
+
if response_model:
|
|
60
|
+
data = response_model.model_validate(data)
|
|
61
|
+
|
|
62
|
+
return data
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import TypeVar, Type, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from .base_driver import LLMDriver, logger
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound=BaseModel)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OllamaDriver(LLMDriver):
|
|
12
|
+
def __init__(self, model_name: str, host: str = "http://localhost:11434"):
|
|
13
|
+
super().__init__(model_name)
|
|
14
|
+
self.base_url = host.rstrip("/")
|
|
15
|
+
self.url = f"{self.base_url}/api/generate"
|
|
16
|
+
|
|
17
|
+
# Default fallback values for generation options if not defined externally
|
|
18
|
+
self.temperature_json = getattr(self, "temperature_json", 0.2)
|
|
19
|
+
self.top_p = getattr(self, "top_p", 0.9)
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
return "Ollama"
|
|
23
|
+
|
|
24
|
+
def generate_text(self, prompt: str) -> str:
|
|
25
|
+
logger.debug(f"Generating text with model {self.model_name} and prompt: {prompt}")
|
|
26
|
+
|
|
27
|
+
payload = {
|
|
28
|
+
"model": self.model_name,
|
|
29
|
+
"prompt": prompt,
|
|
30
|
+
"stream": False
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
response_json = self._send_post_request(self.url, payload)
|
|
34
|
+
return response_json.get("response", "")
|
|
35
|
+
|
|
36
|
+
def generate_json(self, prompt: str, response_model: Optional[Type[T]] = None) -> T:
|
|
37
|
+
logger.debug(f"Generating JSON with model {self.model_name} and prompt: {prompt}")
|
|
38
|
+
|
|
39
|
+
payload = {
|
|
40
|
+
"model": self.model_name,
|
|
41
|
+
"prompt": prompt,
|
|
42
|
+
"stream": False,
|
|
43
|
+
"options": {
|
|
44
|
+
"temperature": self.temperature_json,
|
|
45
|
+
"top_p": self.top_p,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if response_model:
|
|
50
|
+
payload["format"] = response_model.model_json_schema()
|
|
51
|
+
else:
|
|
52
|
+
payload["format"] = "json"
|
|
53
|
+
|
|
54
|
+
response_json = self._send_post_request(self.url, payload)
|
|
55
|
+
response_text = response_json.get("response", "")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(response_text)
|
|
59
|
+
except json.JSONDecodeError as e:
|
|
60
|
+
if hasattr(self, "extract_json"):
|
|
61
|
+
data = self.extract_json(response_text)
|
|
62
|
+
else:
|
|
63
|
+
logger.error(f"Failed to decode JSON from response: {e}")
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
if response_model:
|
|
67
|
+
data = response_model.model_validate(data)
|
|
68
|
+
|
|
69
|
+
return data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .executor import Executor
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from deer.schema.io import AgentOutput, StepTrace
|
|
6
|
+
from deer.schema.plan import Plan
|
|
7
|
+
from deer.tracing.store import TraceStore
|
|
8
|
+
from deer.tools.registry import ToolRegistry
|
|
9
|
+
from .logic import evaluate_logic
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("DEER")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Executor:
|
|
15
|
+
def __init__(self, registry: ToolRegistry) -> None:
|
|
16
|
+
self.registry = registry
|
|
17
|
+
self.trace_store = TraceStore()
|
|
18
|
+
|
|
19
|
+
def execute(self, plan: Plan, payload: {}) -> AgentOutput:
|
|
20
|
+
self.trace_store.reset()
|
|
21
|
+
context: Dict[str, Any] = {}
|
|
22
|
+
last_output: Any = None
|
|
23
|
+
|
|
24
|
+
for step in plan.steps:
|
|
25
|
+
value = self.resolve_input(step.input_from, step.id, payload, context)
|
|
26
|
+
logger.debug(f"Executing step {step.id} with input '{step.input_from}'")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
if step.logic is not None:
|
|
30
|
+
output = self.execute_logic(
|
|
31
|
+
logic=step.logic,
|
|
32
|
+
input_value=value,
|
|
33
|
+
params=step.params or {},
|
|
34
|
+
context=context,
|
|
35
|
+
)
|
|
36
|
+
trace_tool = "logic"
|
|
37
|
+
|
|
38
|
+
if step.tool is not None:
|
|
39
|
+
params = self.resolve_params(step.params or {}, value, context)
|
|
40
|
+
output = self.execute_tool(
|
|
41
|
+
tool_name=step.tool,
|
|
42
|
+
params=params,
|
|
43
|
+
)
|
|
44
|
+
trace_tool = step.tool
|
|
45
|
+
|
|
46
|
+
context[step.id] = output
|
|
47
|
+
last_output = output
|
|
48
|
+
|
|
49
|
+
self.trace_store.append(
|
|
50
|
+
StepTrace(
|
|
51
|
+
step_id=step.id,
|
|
52
|
+
tool=trace_tool,
|
|
53
|
+
input=value,
|
|
54
|
+
output=output,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
except Exception as ex:
|
|
58
|
+
self.trace_store.append(
|
|
59
|
+
StepTrace(
|
|
60
|
+
step_id=step.id,
|
|
61
|
+
tool=step.tool or "logic",
|
|
62
|
+
input=value,
|
|
63
|
+
output=None,
|
|
64
|
+
error=str(ex),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
raise
|
|
68
|
+
|
|
69
|
+
return AgentOutput(result=last_output, trace=self.trace_store.get_trace())
|
|
70
|
+
|
|
71
|
+
def resolve_input(
|
|
72
|
+
self,
|
|
73
|
+
input_from: str | None,
|
|
74
|
+
step_id: str,
|
|
75
|
+
payload: {},
|
|
76
|
+
context: Dict[str, Any],
|
|
77
|
+
) -> Any:
|
|
78
|
+
if input_from is None:
|
|
79
|
+
return payload
|
|
80
|
+
|
|
81
|
+
if input_from not in context:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Unresolved dependency: '{input_from}' required by step '{step_id}'."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
output = context[input_from]
|
|
87
|
+
|
|
88
|
+
# # Automatically unwrap single-key dictionaries to simplify logic and tool usage.
|
|
89
|
+
# # This ensures 'input' refers directly to the value (e.g., file content)
|
|
90
|
+
# # instead of the wrapper dictionary.
|
|
91
|
+
# if isinstance(output, dict) and len(output) == 1:
|
|
92
|
+
# return next(iter(output.values()))
|
|
93
|
+
|
|
94
|
+
return output
|
|
95
|
+
|
|
96
|
+
def resolve_params(
|
|
97
|
+
self,
|
|
98
|
+
params: Dict[str, Any],
|
|
99
|
+
input_value: Any,
|
|
100
|
+
context: Dict[str, Any],
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""Resolves placeholders like 'input' or step IDs in parameters."""
|
|
103
|
+
resolved = {}
|
|
104
|
+
for k, v in params.items():
|
|
105
|
+
if v == "input":
|
|
106
|
+
resolved[k] = input_value
|
|
107
|
+
elif isinstance(v, str) and v in context:
|
|
108
|
+
resolved[k] = context[v]
|
|
109
|
+
else:
|
|
110
|
+
resolved[k] = v
|
|
111
|
+
return resolved
|
|
112
|
+
|
|
113
|
+
def execute_tool(
|
|
114
|
+
self,
|
|
115
|
+
tool_name: str,
|
|
116
|
+
params: Dict[str, Any],
|
|
117
|
+
) -> Any:
|
|
118
|
+
tool = self.registry.get(tool_name)
|
|
119
|
+
input_params = tool.validate_input(params)
|
|
120
|
+
output = tool.run(params=input_params)
|
|
121
|
+
logger.info(f"Executed tool: {tool_name}")
|
|
122
|
+
logger.debug(f"Tool {tool_name} output: {output}")
|
|
123
|
+
return tool.validate_output(output)
|
|
124
|
+
|
|
125
|
+
def execute_logic(
|
|
126
|
+
self,
|
|
127
|
+
logic: str,
|
|
128
|
+
input_value: Any,
|
|
129
|
+
params: Dict[str, Any],
|
|
130
|
+
context: Dict[str, Any],
|
|
131
|
+
) -> Any:
|
|
132
|
+
# logic = self.fix_multiline_strings(logic)
|
|
133
|
+
logger.debug(f"Executing logic: {logic}")
|
|
134
|
+
return evaluate_logic(
|
|
135
|
+
logic,
|
|
136
|
+
input_value=input_value,
|
|
137
|
+
params=params,
|
|
138
|
+
context=context,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
#
|
|
142
|
+
# def fix_multiline_strings(self, code: str) -> str:
|
|
143
|
+
# pattern = r'=\s*"([^"\n]*\n(?:.*\n)*?.*?)"'
|
|
144
|
+
#
|
|
145
|
+
# def replacer(match):
|
|
146
|
+
# content = match.group(1)
|
|
147
|
+
# return '= """' + content + '"""'
|
|
148
|
+
#
|
|
149
|
+
# return re.sub(pattern, replacer, code, flags=re.MULTILINE)
|
|
150
|
+
|
|
151
|
+
# def normalize_logic(self, logic: str) -> str:
|
|
152
|
+
# logic = logic.strip()
|
|
153
|
+
#
|
|
154
|
+
# # multiline JSON escapes
|
|
155
|
+
# logic = logic.replace("\\n", "\n")
|
|
156
|
+
#
|
|
157
|
+
# # Caso: string completo serializado
|
|
158
|
+
# try:
|
|
159
|
+
# parsed = json.loads(logic)
|
|
160
|
+
# if isinstance(parsed, str):
|
|
161
|
+
# logic = parsed
|
|
162
|
+
# except Exception:
|
|
163
|
+
# pass
|
|
164
|
+
#
|
|
165
|
+
# # Caso: código Python contaminado con escaping JSON
|
|
166
|
+
# logic = logic.replace('\\"', '"')
|
|
167
|
+
#
|
|
168
|
+
# return logic
|
deer/executor/logic.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Any, Dict, Mapping
|
|
3
|
+
from .logic_secure import ALLOWED_NODE_TYPES, SAFE_GLOBALS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UnsafeLogicError(ValueError):
|
|
7
|
+
"""Raised when the code contains disallowed or dangerous syntax."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_safe_variable_name(name: str) -> bool:
|
|
11
|
+
return name.isidentifier() and not name.startswith("_")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def evaluate_logic(
|
|
15
|
+
logic_code: str,
|
|
16
|
+
*,
|
|
17
|
+
input_value: Any,
|
|
18
|
+
params: Mapping[str, Any],
|
|
19
|
+
context: Mapping[str, Any],
|
|
20
|
+
) -> Any:
|
|
21
|
+
"""Safely executes Python code and returns the value of 'result'."""
|
|
22
|
+
|
|
23
|
+
# 1. Parse and Validate AST
|
|
24
|
+
try:
|
|
25
|
+
tree = ast.parse(logic_code, mode="exec")
|
|
26
|
+
except SyntaxError as e:
|
|
27
|
+
raise UnsafeLogicError(f"Syntax error in logic: {e}")
|
|
28
|
+
|
|
29
|
+
_validate_ast(tree)
|
|
30
|
+
|
|
31
|
+
# 2. Prepare Scope
|
|
32
|
+
local_scope = {
|
|
33
|
+
"input": input_value,
|
|
34
|
+
"params": dict(params),
|
|
35
|
+
"context": dict(context),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Flatten dictionaries for direct access
|
|
39
|
+
if isinstance(input_value, dict):
|
|
40
|
+
for key, value in input_value.items():
|
|
41
|
+
if _is_safe_variable_name(key):
|
|
42
|
+
local_scope[key] = value
|
|
43
|
+
|
|
44
|
+
for key, value in params.items():
|
|
45
|
+
if _is_safe_variable_name(key):
|
|
46
|
+
local_scope[key] = value
|
|
47
|
+
|
|
48
|
+
# 3. Execute
|
|
49
|
+
try:
|
|
50
|
+
compiled = compile(tree, filename="<step.logic>", mode="exec")
|
|
51
|
+
exec(compiled, SAFE_GLOBALS, local_scope)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise RuntimeError(f"Runtime error in logic execution: {e}")
|
|
54
|
+
|
|
55
|
+
if "result" not in local_scope:
|
|
56
|
+
raise UnsafeLogicError("Logic must assign a value to the 'result' variable.")
|
|
57
|
+
|
|
58
|
+
return local_scope["result"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate_ast(tree: ast.AST) -> None:
|
|
62
|
+
for node in ast.walk(tree):
|
|
63
|
+
if not isinstance(node, ALLOWED_NODE_TYPES):
|
|
64
|
+
raise UnsafeLogicError(f"Disallowed syntax: {type(node).__name__}")
|
|
65
|
+
|
|
66
|
+
# Do not allow access to private attributes (._secret)
|
|
67
|
+
if isinstance(node, ast.Attribute) and node.attr.startswith("_"):
|
|
68
|
+
raise UnsafeLogicError("Access to private attributes is not allowed.")
|
|
69
|
+
|
|
70
|
+
# Do not allow assignments to protected variables
|
|
71
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
72
|
+
if node.id.startswith("_") or node.id in SAFE_GLOBALS:
|
|
73
|
+
raise UnsafeLogicError(
|
|
74
|
+
f"Cannot assign a value to protected variable: {node.id}"
|
|
75
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
# Allowed nodes to support mathematical logic, strings, and basic assignments
|
|
5
|
+
ALLOWED_NODE_TYPES = (
|
|
6
|
+
# --- Basic Structure ---
|
|
7
|
+
ast.Module, # Root container
|
|
8
|
+
ast.Expr, # Standalone expressions
|
|
9
|
+
ast.Assign, # Variable assignment (e.g., x = 10)
|
|
10
|
+
ast.Load, # Load variable value
|
|
11
|
+
ast.Store, # Store variable value
|
|
12
|
+
ast.Name, # Variable names
|
|
13
|
+
ast.Constant, # Literal values (numbers, strings, None, True, False)
|
|
14
|
+
# --- Mathematical Operations ---
|
|
15
|
+
ast.BinOp, # Binary operations (+, -, *, /)
|
|
16
|
+
ast.UnaryOp, # Unary operations (not, -x)
|
|
17
|
+
ast.Add,
|
|
18
|
+
ast.Sub,
|
|
19
|
+
ast.Mult,
|
|
20
|
+
ast.Div,
|
|
21
|
+
ast.FloorDiv,
|
|
22
|
+
ast.Mod,
|
|
23
|
+
ast.Pow,
|
|
24
|
+
ast.USub,
|
|
25
|
+
ast.UAdd,
|
|
26
|
+
# --- Logic and Comparison ---
|
|
27
|
+
ast.Compare, # Comparisons (==, !=, <, etc.)
|
|
28
|
+
ast.Eq,
|
|
29
|
+
ast.NotEq,
|
|
30
|
+
ast.Lt,
|
|
31
|
+
ast.LtE,
|
|
32
|
+
ast.Gt,
|
|
33
|
+
ast.GtE,
|
|
34
|
+
ast.In,
|
|
35
|
+
ast.NotIn,
|
|
36
|
+
ast.BoolOp, # Logical operators (and, or)
|
|
37
|
+
ast.And,
|
|
38
|
+
ast.Or,
|
|
39
|
+
ast.IfExp, # Ternary expressions (x if condition else y)
|
|
40
|
+
# --- Data Structures and Access ---
|
|
41
|
+
ast.List, # [1, 2, 3]
|
|
42
|
+
ast.Dict, # {'a': 1}
|
|
43
|
+
ast.Tuple, # (1, 2)
|
|
44
|
+
ast.Set, # {1, 2}
|
|
45
|
+
ast.Subscript, # Indexed/key access: list[0] or dict['k']
|
|
46
|
+
ast.Slice, # Slicing: list[1:5]
|
|
47
|
+
ast.Attribute, # Attribute access: object.property (required for Pydantic)
|
|
48
|
+
# --- Strings and Formatting ---
|
|
49
|
+
ast.JoinedStr, # f-strings
|
|
50
|
+
ast.FormattedValue, # Values inside f-strings
|
|
51
|
+
# --- Calls and Functions ---
|
|
52
|
+
ast.Call, # Allow calling functions from _SAFE_GLOBALS
|
|
53
|
+
ast.keyword, # Keyword arguments: func(x=1)
|
|
54
|
+
# --- Advanced Safe Flexibility ---
|
|
55
|
+
ast.ListComp, # [x for x in items] (useful for data transformation)
|
|
56
|
+
ast.DictComp, # {k: v for k, v in items}
|
|
57
|
+
ast.comprehension, # Internal comprehension logic
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
SAFE_GLOBALS = {
|
|
61
|
+
# --- Sandbox Privacy ---
|
|
62
|
+
"__builtins__": {}, # Blocks access to dangerous functions like eval, exec, open
|
|
63
|
+
# --- Mathematical Constants ---
|
|
64
|
+
"pi": math.pi,
|
|
65
|
+
"e": math.e,
|
|
66
|
+
# --- Arithmetic and Numbers ---
|
|
67
|
+
"abs": abs,
|
|
68
|
+
"min": min,
|
|
69
|
+
"max": max,
|
|
70
|
+
"round": round,
|
|
71
|
+
"pow": pow,
|
|
72
|
+
"sum": sum,
|
|
73
|
+
"sqrt": math.sqrt,
|
|
74
|
+
"log": math.log,
|
|
75
|
+
"exp": math.exp,
|
|
76
|
+
"ceil": math.ceil,
|
|
77
|
+
"floor": math.floor,
|
|
78
|
+
"sin": math.sin,
|
|
79
|
+
"cos": math.cos,
|
|
80
|
+
"tan": math.tan,
|
|
81
|
+
# --- Type Conversion (Casting) ---
|
|
82
|
+
"str": str,
|
|
83
|
+
"int": int,
|
|
84
|
+
"float": float,
|
|
85
|
+
"bool": bool,
|
|
86
|
+
"list": list,
|
|
87
|
+
"dict": dict,
|
|
88
|
+
"set": set,
|
|
89
|
+
"tuple": tuple,
|
|
90
|
+
# --- Collection Processing ---
|
|
91
|
+
"len": len,
|
|
92
|
+
"range": range,
|
|
93
|
+
"enumerate": enumerate,
|
|
94
|
+
"zip": zip,
|
|
95
|
+
"sorted": sorted, # Allows safe list sorting
|
|
96
|
+
"reversed": reversed,
|
|
97
|
+
# --- Advanced Logic ---
|
|
98
|
+
"any": any, # Is any value True?
|
|
99
|
+
"all": all, # Are all values True?
|
|
100
|
+
"filter": filter,
|
|
101
|
+
"map": map,
|
|
102
|
+
}
|
deer/main.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from deer.builtins import agents
|
|
7
|
+
from deer.utils.console import console, error, info
|
|
8
|
+
from deer.drivers import drivers_parser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def title():
|
|
12
|
+
console.print()
|
|
13
|
+
console.print(
|
|
14
|
+
Text(
|
|
15
|
+
"DEER",
|
|
16
|
+
style="bold cyan",
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
console.print(
|
|
20
|
+
"Deterministic Executable Engine for Runtime Agents",
|
|
21
|
+
style="dim",
|
|
22
|
+
)
|
|
23
|
+
console.print()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def agents_list():
|
|
27
|
+
console.print()
|
|
28
|
+
console.print("[bold]Available agents[/bold]")
|
|
29
|
+
for name in agents:
|
|
30
|
+
console.print(f" • [green]{name}[/green]")
|
|
31
|
+
console.print()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def example():
|
|
35
|
+
console.print("[dim]Example:[/dim] deer python_manager")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main():
|
|
39
|
+
args = drivers_parser.parse_args()
|
|
40
|
+
|
|
41
|
+
title()
|
|
42
|
+
|
|
43
|
+
if selected_agent := args.agent:
|
|
44
|
+
if selected_agent in agents:
|
|
45
|
+
info(f"Launching agent '{selected_agent}'")
|
|
46
|
+
|
|
47
|
+
command = [
|
|
48
|
+
sys.executable,
|
|
49
|
+
agents[selected_agent],
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
if args.backend:
|
|
53
|
+
command.extend(["--backend", args.backend])
|
|
54
|
+
|
|
55
|
+
if args.model:
|
|
56
|
+
command.extend(["--model", args.model])
|
|
57
|
+
|
|
58
|
+
result = subprocess.run(command)
|
|
59
|
+
sys.exit(result.returncode)
|
|
60
|
+
|
|
61
|
+
error(f"Unknown agent '{selected_agent}'")
|
|
62
|
+
agents_list()
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
error("No agent selected")
|
|
66
|
+
example()
|
|
67
|
+
agents_list()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
deer/planner/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .planner import Planner
|
deer/planner/planner.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from deer.prompts import PLANNER_PROMPT, GOAL_IMPROVEMENT_PROMPT
|
|
4
|
+
from deer.schema.io import AgentInput
|
|
5
|
+
from deer.tools.registry import ToolRegistry
|
|
6
|
+
from deer.drivers.base_driver import LLMDriver
|
|
7
|
+
from deer.schema.plan import Plan
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Planner:
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
identity: str,
|
|
15
|
+
driver: LLMDriver | None = None,
|
|
16
|
+
registry: ToolRegistry | None = None,
|
|
17
|
+
format_response: str = "plain",
|
|
18
|
+
improve_goal: bool = True,
|
|
19
|
+
):
|
|
20
|
+
self.llm_driver = driver
|
|
21
|
+
self.registry = registry
|
|
22
|
+
self.identity = identity
|
|
23
|
+
self.format_response = format_response
|
|
24
|
+
self.improve_goal = improve_goal
|
|
25
|
+
|
|
26
|
+
self.goal = ""
|
|
27
|
+
|
|
28
|
+
def plan(
|
|
29
|
+
self,
|
|
30
|
+
agent_input: AgentInput,
|
|
31
|
+
feedback={},
|
|
32
|
+
include_state_modifying: bool = True,
|
|
33
|
+
) -> Plan:
|
|
34
|
+
if self.improve_goal:
|
|
35
|
+
self.run_improve_goal(agent_input=agent_input)
|
|
36
|
+
plan_prompt = self.build_prompt(
|
|
37
|
+
agent_input,
|
|
38
|
+
feedback,
|
|
39
|
+
include_state_modifying=include_state_modifying,
|
|
40
|
+
)
|
|
41
|
+
plan = self.llm_driver.generate_json(plan_prompt, response_model=Plan)
|
|
42
|
+
return plan
|
|
43
|
+
|
|
44
|
+
def run_improve_goal(self, agent_input: AgentInput):
|
|
45
|
+
goal_prompt = GOAL_IMPROVEMENT_PROMPT.format(
|
|
46
|
+
identity=self.identity,
|
|
47
|
+
goal=agent_input.goal,
|
|
48
|
+
payload=json.dumps(agent_input.payload, ensure_ascii=False),
|
|
49
|
+
)
|
|
50
|
+
improved_goal = self.llm_driver.generate_text(goal_prompt)
|
|
51
|
+
agent_input.goal = improved_goal
|
|
52
|
+
self.goal = improved_goal
|
|
53
|
+
|
|
54
|
+
def build_prompt(
|
|
55
|
+
self,
|
|
56
|
+
agent_input: AgentInput,
|
|
57
|
+
feedback={},
|
|
58
|
+
include_state_modifying=True,
|
|
59
|
+
) -> str:
|
|
60
|
+
# As usable data must be JSON consistent.
|
|
61
|
+
agent_input.payload.update(feedback)
|
|
62
|
+
payload = json.dumps(agent_input.payload, ensure_ascii=False)
|
|
63
|
+
|
|
64
|
+
planner_prompt = PLANNER_PROMPT.format(
|
|
65
|
+
identity=self.identity,
|
|
66
|
+
goal=agent_input.goal,
|
|
67
|
+
payload=payload,
|
|
68
|
+
tools=self.registry.describe(include_state_modifying),
|
|
69
|
+
format_response=self.format_response,
|
|
70
|
+
)
|
|
71
|
+
return planner_prompt
|
deer/prompts/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .error_explain import ERROR_EXPLAIN_PROMPT
|
|
2
|
+
from .goal_improvement import GOAL_IMPROVEMENT_PROMPT
|
|
3
|
+
from .humanizer import HUMANIZER_PROMPT
|
|
4
|
+
from .planner import PLANNER_PROMPT
|
|
5
|
+
from .response_improvement import RESPONSE_IMPROVEMENT_PROMPT
|
|
6
|
+
from .goal_validation import VERIFIER_JUDGE_PROMPT, GOAL_VERIFIER_PROMPT
|