deer-agent-framework 0.0__tar.gz → 0.1a2__tar.gz
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_agent_framework-0.0 → deer_agent_framework-0.1a2}/PKG-INFO +29 -6
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/README.md +15 -5
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/builtins/python_manager/agent.py +1 -1
- deer_agent_framework-0.1a2/deer/builtins/python_manager/tools.py +112 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/core/agent.py +12 -13
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/drivers/__init__.py +7 -2
- deer_agent_framework-0.1a2/deer/drivers/azure_driver.py +41 -0
- deer_agent_framework-0.1a2/deer/drivers/base_driver.py +123 -0
- deer_agent_framework-0.1a2/deer/drivers/openai_driver.py +26 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/schema/io.py +3 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/__init__.py +1 -1
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/base.py +58 -3
- deer_agent_framework-0.1a2/deer/tools/builtin/__init__.py +8 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/builtin/file_manager.py +38 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/builtin/git_manager.py +5 -5
- deer_agent_framework-0.1a2/deer/tools/builtin/memory_manager.py +72 -0
- deer_agent_framework-0.1a2/deer/tools/builtin/network_manager.py +91 -0
- deer_agent_framework-0.1a2/deer/tools/builtin/runtime_manager.py +180 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/builtin/search_manager.py +5 -5
- deer_agent_framework-0.1a2/deer/tools/builtin/structured_data_inspector.py +112 -0
- deer_agent_framework-0.1a2/deer/tools/builtin/system_inspector.py +55 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/registry.py +6 -6
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/PKG-INFO +29 -6
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/SOURCES.txt +9 -0
- deer_agent_framework-0.1a2/pyproject.toml +45 -0
- deer_agent_framework-0.1a2/tests/test_azure_driver.py +56 -0
- deer_agent_framework-0.1a2/tests/test_openai_driver.py +69 -0
- deer_agent_framework-0.0/deer/builtins/python_manager/tools.py +0 -54
- deer_agent_framework-0.0/deer/drivers/base_driver.py +0 -56
- deer_agent_framework-0.0/deer/tools/builtin/__init__.py +0 -3
- deer_agent_framework-0.0/pyproject.toml +0 -30
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/LICENSE +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/builtins/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/core/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/core/ui.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/drivers/gemini_driver.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/drivers/ollama_driver.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/executor.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/logic.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/logic_secure.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/main.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/planner/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/planner/planner.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/error_explain.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/goal_improvement.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/goal_validation.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/humanizer.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/planner.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/response_improvement.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/schema/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/schema/plan.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/decorators.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tracing/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tracing/logging_config.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tracing/store.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/console.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/plots/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/plots/plot_traces.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/validator/__init__.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/validator/plan_validator.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/validator/rules.py +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/dependency_links.txt +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/entry_points.txt +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/requires.txt +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/top_level.txt +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/setup.cfg +0 -0
- {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/tests/test_plan_validator.py +0 -0
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deer-agent-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a2
|
|
4
4
|
Summary: DEER - Deterministic Executable Engine for Runtime-agents
|
|
5
5
|
Author: Yeison
|
|
6
|
+
License: BSD-2-Clause
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
6
19
|
Requires-Python: >=3.12
|
|
7
20
|
Description-Content-Type: text/markdown
|
|
8
21
|
License-File: LICENSE
|
|
@@ -21,8 +34,18 @@ Dynamic: license-file
|
|
|
21
34
|
|
|
22
35
|
**DEER** is the first framework designed for building **Deterministic Agents** in production environments where "it usually works" isn't good enough. While other frameworks rely on massive system prompts and probabilistic loops, DEER subordinates LLMs to rigid, code-defined software structures.
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
[//]: # ([](https://chaski-confluent.readthedocs.io/en/latest/?badge=latest))
|
|
26
49
|
|
|
27
50
|
---
|
|
28
51
|
|
|
@@ -56,13 +79,13 @@ agent = DeterministicAgent(
|
|
|
56
79
|
),
|
|
57
80
|
driver=get_driver_from_parser(),
|
|
58
81
|
registry=registry,
|
|
59
|
-
jail_path=Path.cwd() / "sandbox",
|
|
82
|
+
jail_path=Path.cwd() / "sandbox", # Strict security boundary
|
|
60
83
|
format_response="markdown",
|
|
61
|
-
|
|
84
|
+
max_retries=5
|
|
62
85
|
)
|
|
63
86
|
|
|
64
87
|
if __name__ == "__main__":
|
|
65
|
-
agent.repl()
|
|
88
|
+
agent.repl() # Instant interactive shell
|
|
66
89
|
```
|
|
67
90
|
|
|
68
91
|
### 2. Assemble your Tool Registry
|
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
**DEER** is the first framework designed for building **Deterministic Agents** in production environments where "it usually works" isn't good enough. While other frameworks rely on massive system prompts and probabilistic loops, DEER subordinates LLMs to rigid, code-defined software structures.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
[//]: # ([](https://chaski-confluent.readthedocs.io/en/latest/?badge=latest))
|
|
9
19
|
|
|
10
20
|
---
|
|
11
21
|
|
|
@@ -39,13 +49,13 @@ agent = DeterministicAgent(
|
|
|
39
49
|
),
|
|
40
50
|
driver=get_driver_from_parser(),
|
|
41
51
|
registry=registry,
|
|
42
|
-
jail_path=Path.cwd() / "sandbox",
|
|
52
|
+
jail_path=Path.cwd() / "sandbox", # Strict security boundary
|
|
43
53
|
format_response="markdown",
|
|
44
|
-
|
|
54
|
+
max_retries=5
|
|
45
55
|
)
|
|
46
56
|
|
|
47
57
|
if __name__ == "__main__":
|
|
48
|
-
agent.repl()
|
|
58
|
+
agent.repl() # Instant interactive shell
|
|
49
59
|
```
|
|
50
60
|
|
|
51
61
|
### 2. Assemble your Tool Registry
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from deer.tools.registry import ToolRegistry
|
|
2
|
+
from deer.tools.builtin import (
|
|
3
|
+
FileManager,
|
|
4
|
+
GitManager,
|
|
5
|
+
SearchManager,
|
|
6
|
+
MemoryManager,
|
|
7
|
+
NetworkManager,
|
|
8
|
+
RuntimeManager,
|
|
9
|
+
StructuredDataInspector,
|
|
10
|
+
SystemInspector,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
jail_path = "/Users/yeison/Development/deer-agent-framework/sandbox/root"
|
|
14
|
+
|
|
15
|
+
registry = ToolRegistry()
|
|
16
|
+
|
|
17
|
+
fileManager = FileManager(
|
|
18
|
+
# tools=[
|
|
19
|
+
# "new_file",
|
|
20
|
+
# "read_file",
|
|
21
|
+
# "delete_file",
|
|
22
|
+
# "create_directory",
|
|
23
|
+
# "get_file_info",
|
|
24
|
+
# "directory_tree",
|
|
25
|
+
# "patch_file",
|
|
26
|
+
# ],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
gitManager = GitManager(
|
|
30
|
+
# tools=[
|
|
31
|
+
# "git_status",
|
|
32
|
+
# "git_current_branch",
|
|
33
|
+
# "git_log",
|
|
34
|
+
# "git_diff",
|
|
35
|
+
# "git_staged_diff",
|
|
36
|
+
# "git_show",
|
|
37
|
+
# "git_add",
|
|
38
|
+
# "git_commit",
|
|
39
|
+
# "git_restore",
|
|
40
|
+
# ],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
searchManager = SearchManager(
|
|
44
|
+
# tools=[
|
|
45
|
+
# "search_text",
|
|
46
|
+
# "search_regex",
|
|
47
|
+
# "search_text_ignore_case",
|
|
48
|
+
# "search_regex_ignore_case",
|
|
49
|
+
# "find_files",
|
|
50
|
+
# "search_file_names",
|
|
51
|
+
# "list_files",
|
|
52
|
+
# "search_by_extension",
|
|
53
|
+
# "search_text_in_files",
|
|
54
|
+
# "files_with_matches",
|
|
55
|
+
# "files_without_matches",
|
|
56
|
+
# "count_matches",
|
|
57
|
+
# ],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
memoryManager = MemoryManager(
|
|
61
|
+
# tools=[
|
|
62
|
+
# "store_key_insight",
|
|
63
|
+
# "retrieve_key_insight",
|
|
64
|
+
# "list_memory_keys",
|
|
65
|
+
# "clear_context_memory",
|
|
66
|
+
# ]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
networkManager = NetworkManager(
|
|
70
|
+
# tools=[
|
|
71
|
+
# "fetch_endpoint",
|
|
72
|
+
# "download_asset",
|
|
73
|
+
# "check_url_availability",
|
|
74
|
+
# ]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
runtimeManager = RuntimeManager(
|
|
78
|
+
# tools=[
|
|
79
|
+
# "execute_test_suite",
|
|
80
|
+
# "compile_source_code",
|
|
81
|
+
# "check_process_status",
|
|
82
|
+
# "terminate_process",
|
|
83
|
+
# ]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
structuredDataInspector = StructuredDataInspector(
|
|
87
|
+
# tools=[
|
|
88
|
+
# "inspect_json_keys",
|
|
89
|
+
# "preview_csv_columns",
|
|
90
|
+
# "query_sqlite_metadata",
|
|
91
|
+
# "execute_sqlite_statement",
|
|
92
|
+
# ]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
systemInspector = SystemInspector(
|
|
96
|
+
# tools=[
|
|
97
|
+
# "get_environment_variable",
|
|
98
|
+
# "list_active_processes",
|
|
99
|
+
# "check_network_sockets",
|
|
100
|
+
# ]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
registry.register(
|
|
104
|
+
fileManager,
|
|
105
|
+
gitManager,
|
|
106
|
+
searchManager,
|
|
107
|
+
memoryManager,
|
|
108
|
+
networkManager,
|
|
109
|
+
runtimeManager,
|
|
110
|
+
structuredDataInspector,
|
|
111
|
+
systemInspector,
|
|
112
|
+
)
|
|
@@ -24,7 +24,6 @@ from deer.executor.executor import Executor
|
|
|
24
24
|
from deer.core.ui import WELCOME_MESSAGE
|
|
25
25
|
from deer.tools.registry import ToolRegistry, default_registry
|
|
26
26
|
from deer.schema.io import AgentInput, AgentOutput
|
|
27
|
-
|
|
28
27
|
from rich.console import Console
|
|
29
28
|
from rich.markdown import Markdown
|
|
30
29
|
|
|
@@ -37,7 +36,7 @@ class DeterministicAgent:
|
|
|
37
36
|
self,
|
|
38
37
|
description: str = "",
|
|
39
38
|
identity: str = "DeterministicAgent",
|
|
40
|
-
|
|
39
|
+
max_retries: int = 3,
|
|
41
40
|
driver: LLMDriver | None = None,
|
|
42
41
|
registry: ToolRegistry | None = None,
|
|
43
42
|
format_response: str = "plaintext",
|
|
@@ -68,9 +67,9 @@ class DeterministicAgent:
|
|
|
68
67
|
)
|
|
69
68
|
|
|
70
69
|
assert (
|
|
71
|
-
|
|
72
|
-
), f"max_tries_for_plan must be positive, got {
|
|
73
|
-
self.
|
|
70
|
+
max_retries > 0
|
|
71
|
+
), f"max_tries_for_plan must be positive, got {max_retries}"
|
|
72
|
+
self.max_retries = max_retries
|
|
74
73
|
|
|
75
74
|
self.history = []
|
|
76
75
|
self.trace = []
|
|
@@ -152,14 +151,14 @@ class DeterministicAgent:
|
|
|
152
151
|
|
|
153
152
|
response_validation = None
|
|
154
153
|
|
|
155
|
-
for attempt_idx in range(self.
|
|
156
|
-
logger.debug(f"Attempt {attempt_idx + 1} of {self.
|
|
154
|
+
for attempt_idx in range(self.max_retries):
|
|
155
|
+
logger.debug(f"Attempt {attempt_idx + 1} of {self.max_retries}")
|
|
157
156
|
|
|
158
157
|
run_trace[f"Attempt-{attempt_idx + 1}"] = {}
|
|
159
158
|
|
|
160
|
-
for solution_idx in range(self.
|
|
159
|
+
for solution_idx in range(self.max_retries):
|
|
161
160
|
logger.debug(
|
|
162
|
-
f"Solution planning {solution_idx + 1} of {self.
|
|
161
|
+
f"Solution planning {solution_idx + 1} of {self.max_retries}"
|
|
163
162
|
)
|
|
164
163
|
run_trace[f"Attempt-{attempt_idx + 1}"][
|
|
165
164
|
f"Solution-{solution_idx + 1}"
|
|
@@ -174,7 +173,7 @@ class DeterministicAgent:
|
|
|
174
173
|
|
|
175
174
|
if response is None:
|
|
176
175
|
logger.warning(
|
|
177
|
-
f"Agent failed to produce a response after {self.
|
|
176
|
+
f"Agent failed to produce a response after {self.max_retries} attempts"
|
|
178
177
|
)
|
|
179
178
|
if last_error_message:
|
|
180
179
|
last_error_message_explained = self.explain_error(
|
|
@@ -199,9 +198,9 @@ class DeterministicAgent:
|
|
|
199
198
|
for s in response.trace
|
|
200
199
|
],
|
|
201
200
|
}
|
|
202
|
-
for verification_idx in range(self.
|
|
201
|
+
for verification_idx in range(self.max_retries):
|
|
203
202
|
logger.debug(
|
|
204
|
-
f"Verification planning {verification_idx + 1} of {self.
|
|
203
|
+
f"Verification planning {verification_idx + 1} of {self.max_retries}"
|
|
205
204
|
)
|
|
206
205
|
run_trace[f"Attempt-{attempt_idx + 1}"][
|
|
207
206
|
f"Verification-{verification_idx + 1}"
|
|
@@ -229,7 +228,7 @@ class DeterministicAgent:
|
|
|
229
228
|
|
|
230
229
|
if response_validation is None:
|
|
231
230
|
logger.warning(
|
|
232
|
-
f"Agent failed to verificate after {self.
|
|
231
|
+
f"Agent failed to verificate after {self.max_retries} attempts"
|
|
233
232
|
)
|
|
234
233
|
continue
|
|
235
234
|
|
|
@@ -5,6 +5,8 @@ import os
|
|
|
5
5
|
from .base_driver import LLMDriver
|
|
6
6
|
from .gemini_driver import GeminiDriver
|
|
7
7
|
from .ollama_driver import OllamaDriver
|
|
8
|
+
from .openai_driver import OpenAIDriver
|
|
9
|
+
from .azure_driver import AzureOpenAIDriver
|
|
8
10
|
|
|
9
11
|
from deer.utils.console import error
|
|
10
12
|
|
|
@@ -12,6 +14,7 @@ backends = {
|
|
|
12
14
|
"gemini",
|
|
13
15
|
"openai",
|
|
14
16
|
"ollama",
|
|
17
|
+
"azure",
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
drivers_parser = argparse.ArgumentParser(description="DEER Agent Framework CLI")
|
|
@@ -62,5 +65,7 @@ def get_driver_from_parser():
|
|
|
62
65
|
return OllamaDriver(model_name=args.model)
|
|
63
66
|
|
|
64
67
|
case "openai":
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
return OpenAIDriver(model_name=args.model)
|
|
69
|
+
|
|
70
|
+
case "azure":
|
|
71
|
+
return AzureOpenAIDriver(model_name=args.model)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from .base_driver import OpenAIStandardDriver
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AzureOpenAIDriver(OpenAIStandardDriver):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
model_name: str,
|
|
11
|
+
api_version: str = "2024-02-15-preview"
|
|
12
|
+
):
|
|
13
|
+
super().__init__(model_name)
|
|
14
|
+
self.api_key = os.getenv("AZURE_OPENAI_API_KEY")
|
|
15
|
+
self.endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
|
|
16
|
+
self.api_version = api_version
|
|
17
|
+
|
|
18
|
+
if not self.api_key:
|
|
19
|
+
raise ValueError("AZURE_OPENAI_API_KEY environment variable is not set.")
|
|
20
|
+
if not self.endpoint:
|
|
21
|
+
raise ValueError("AZURE_OPENAI_ENDPOINT environment variable is not set.")
|
|
22
|
+
|
|
23
|
+
# In Azure, the "model" in the payload is often ignored in favor of the deployment in the URL,
|
|
24
|
+
# but we keep it for compatibility with the base class logic.
|
|
25
|
+
self.base_url = self.endpoint.rstrip("/")
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def url(self) -> str:
|
|
29
|
+
# Azure URL format: {endpoint}/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}
|
|
30
|
+
# We assume model_name is the deployment name.
|
|
31
|
+
return f"{self.base_url}/openai/deployments/{self.model_name}/chat/completions?api-version={self.api_version}"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def headers(self) -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"api-key": self.api_key,
|
|
37
|
+
"Content-Type": "application/json"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return "AzureOpenAI"
|
|
@@ -0,0 +1,123 @@
|
|
|
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)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OpenAIStandardDriver(LLMDriver):
|
|
60
|
+
"""Base driver for OpenAI-compatible APIs."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, model_name: str):
|
|
63
|
+
super().__init__(model_name)
|
|
64
|
+
# Use defaults from LLMDriver protocol if not set
|
|
65
|
+
self.temperature_json = getattr(self, "temperature_json", 0.0)
|
|
66
|
+
self.top_p = getattr(self, "top_p", 1.0)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def url(self) -> str:
|
|
70
|
+
raise NotImplementedError("Subclasses must implement url property")
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def headers(self) -> dict:
|
|
74
|
+
raise NotImplementedError("Subclasses must implement headers property")
|
|
75
|
+
|
|
76
|
+
def generate_text(self, prompt: str) -> str:
|
|
77
|
+
logger.debug(f"Generating text with model {self.model_name} and prompt: {prompt}")
|
|
78
|
+
|
|
79
|
+
payload = {
|
|
80
|
+
"model": self.model_name,
|
|
81
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
82
|
+
"temperature": self.temperature_json,
|
|
83
|
+
"top_p": self.top_p,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
response_json = self._send_post_request(self.url, payload, headers=self.headers)
|
|
88
|
+
return response_json["choices"][0]["message"]["content"]
|
|
89
|
+
except (KeyError, IndexError) as e:
|
|
90
|
+
logger.error(f"Unexpected response structure from API: {e}")
|
|
91
|
+
raise RuntimeError(f"Failed to parse text from API response: {e}")
|
|
92
|
+
|
|
93
|
+
def generate_json(self, prompt: str, response_model: Optional[Type[T]] = None) -> T:
|
|
94
|
+
logger.debug(f"Generating JSON with model {self.model_name} and prompt: {prompt}")
|
|
95
|
+
|
|
96
|
+
payload = {
|
|
97
|
+
"model": self.model_name,
|
|
98
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
99
|
+
"response_format": {"type": "json_object"},
|
|
100
|
+
"temperature": self.temperature_json,
|
|
101
|
+
"top_p": self.top_p,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
response_json = self._send_post_request(self.url, payload, headers=self.headers)
|
|
106
|
+
response_text = response_json["choices"][0]["message"]["content"]
|
|
107
|
+
data = json.loads(response_text)
|
|
108
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
|
109
|
+
logger.error(f"Error parsing JSON response from API: {e}")
|
|
110
|
+
# Try fallback extraction
|
|
111
|
+
try:
|
|
112
|
+
# We need to reach the response_text if it was already assigned before error
|
|
113
|
+
if 'response_text' in locals():
|
|
114
|
+
data = self.extract_json(response_text)
|
|
115
|
+
else:
|
|
116
|
+
raise
|
|
117
|
+
except Exception:
|
|
118
|
+
raise RuntimeError(f"Error processing JSON output from API: {e}")
|
|
119
|
+
|
|
120
|
+
if response_model:
|
|
121
|
+
data = response_model.model_validate(data)
|
|
122
|
+
|
|
123
|
+
return data
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from .base_driver import OpenAIStandardDriver
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OpenAIDriver(OpenAIStandardDriver):
|
|
8
|
+
def __init__(self, model_name: str):
|
|
9
|
+
super().__init__(model_name)
|
|
10
|
+
self.api_key = os.getenv("OPENAI_API_KEY")
|
|
11
|
+
if not self.api_key:
|
|
12
|
+
raise ValueError("OPENAI_API_KEY environment variable is not set.")
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def url(self) -> str:
|
|
16
|
+
return "https://api.openai.com/v1/chat/completions"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def headers(self) -> dict:
|
|
20
|
+
return {
|
|
21
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
22
|
+
"Content-Type": "application/json"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return "OpenAI"
|
|
@@ -6,7 +6,7 @@ from pydantic import BaseModel
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import subprocess
|
|
8
8
|
import shlex
|
|
9
|
-
|
|
9
|
+
import shutil
|
|
10
10
|
|
|
11
11
|
class ToolProviderError(ValueError):
|
|
12
12
|
pass
|
|
@@ -18,13 +18,29 @@ class CommandRunnerError(ValueError):
|
|
|
18
18
|
|
|
19
19
|
@dataclass
|
|
20
20
|
class ToolProvider:
|
|
21
|
-
# jail: Path | str | None = field(default=None, kw_only=True)
|
|
22
21
|
tools: list[str] | None = field(default=None, kw_only=True)
|
|
23
22
|
|
|
24
23
|
def __post_init__(self):
|
|
25
24
|
# pass
|
|
26
25
|
self.jail_ = None
|
|
27
26
|
|
|
27
|
+
for command in self.commands or []:
|
|
28
|
+
self.check_command(command)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def commands(self):
|
|
32
|
+
"""Return the list of commands to be verified at agent startup."""
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def allowed_commands(self):
|
|
37
|
+
"""Return the list of allowed execution commands.
|
|
38
|
+
|
|
39
|
+
If a command is not present in this list, its execution will fail
|
|
40
|
+
at runtime.
|
|
41
|
+
"""
|
|
42
|
+
return []
|
|
43
|
+
|
|
28
44
|
@property
|
|
29
45
|
def jail(self):
|
|
30
46
|
assert self.jail_ is not None, (
|
|
@@ -75,11 +91,36 @@ class ToolProvider:
|
|
|
75
91
|
timeout_seconds: int = 30,
|
|
76
92
|
) -> dict:
|
|
77
93
|
"""Execute a command string without invoking a shell."""
|
|
78
|
-
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
args = shlex.split(command)
|
|
97
|
+
except ValueError as e:
|
|
98
|
+
return {
|
|
99
|
+
"stdout": "",
|
|
100
|
+
"stderr": str(e),
|
|
101
|
+
"returncode": -1,
|
|
102
|
+
"message": f"Shell expansion error: {e}",
|
|
103
|
+
}
|
|
79
104
|
|
|
80
105
|
if not args:
|
|
81
106
|
raise CommandRunnerError("Command cannot be empty.")
|
|
82
107
|
|
|
108
|
+
if self.allowed_commands:
|
|
109
|
+
is_allowed = False
|
|
110
|
+
for allowed_cmd in self.allowed_commands:
|
|
111
|
+
allowed_args = shlex.split(allowed_cmd)
|
|
112
|
+
if len(args) >= len(allowed_args) and args[: len(allowed_args)] == allowed_args:
|
|
113
|
+
is_allowed = True
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
if not is_allowed:
|
|
117
|
+
return {
|
|
118
|
+
"stdout": "",
|
|
119
|
+
"stderr": f"Command '{command}' is not allowed.",
|
|
120
|
+
"returncode": -1,
|
|
121
|
+
"message": f"Security restriction: '{command}' is not in the allowed list or doesn't match an allowed prefix.",
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
safe_cwd = self.jail if cwd is None else self.jailed_path(cwd)
|
|
84
125
|
|
|
85
126
|
completed = subprocess.run(
|
|
@@ -95,8 +136,22 @@ class ToolProvider:
|
|
|
95
136
|
"stdout": completed.stdout,
|
|
96
137
|
"stderr": completed.stderr,
|
|
97
138
|
"returncode": completed.returncode,
|
|
139
|
+
"message": "",
|
|
98
140
|
}
|
|
99
141
|
|
|
142
|
+
def check_command(self, command: str | Path) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Validates if a command exists in the operating system.
|
|
145
|
+
Raises CommandRunnerError if the executable is not found in the PATH.
|
|
146
|
+
"""
|
|
147
|
+
# Convert to string for compatibility with Windows and Python < 3.12
|
|
148
|
+
cmd_str = str(command)
|
|
149
|
+
if shutil.which(cmd_str) is None:
|
|
150
|
+
raise CommandRunnerError(
|
|
151
|
+
f"The command '{cmd_str}' is not available on the system. "
|
|
152
|
+
f"Please ensure it is installed and configured in your PATH."
|
|
153
|
+
)
|
|
154
|
+
|
|
100
155
|
|
|
101
156
|
class Tool(ABC):
|
|
102
157
|
"""Base contract for deterministic tools."""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .file_manager import FileManager
|
|
2
|
+
from .git_manager import GitManager
|
|
3
|
+
from .search_manager import SearchManager
|
|
4
|
+
from .memory_manager import MemoryManager
|
|
5
|
+
from .network_manager import NetworkManager
|
|
6
|
+
from .runtime_manager import RuntimeManager
|
|
7
|
+
from .structured_data_inspector import StructuredDataInspector
|
|
8
|
+
from .system_inspector import SystemInspector
|