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.
Files changed (71) hide show
  1. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/PKG-INFO +29 -6
  2. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/README.md +15 -5
  3. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/builtins/python_manager/agent.py +1 -1
  4. deer_agent_framework-0.1a2/deer/builtins/python_manager/tools.py +112 -0
  5. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/core/agent.py +12 -13
  6. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/drivers/__init__.py +7 -2
  7. deer_agent_framework-0.1a2/deer/drivers/azure_driver.py +41 -0
  8. deer_agent_framework-0.1a2/deer/drivers/base_driver.py +123 -0
  9. deer_agent_framework-0.1a2/deer/drivers/openai_driver.py +26 -0
  10. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/schema/io.py +3 -0
  11. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/__init__.py +1 -1
  12. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/base.py +58 -3
  13. deer_agent_framework-0.1a2/deer/tools/builtin/__init__.py +8 -0
  14. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/builtin/file_manager.py +38 -0
  15. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/builtin/git_manager.py +5 -5
  16. deer_agent_framework-0.1a2/deer/tools/builtin/memory_manager.py +72 -0
  17. deer_agent_framework-0.1a2/deer/tools/builtin/network_manager.py +91 -0
  18. deer_agent_framework-0.1a2/deer/tools/builtin/runtime_manager.py +180 -0
  19. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/builtin/search_manager.py +5 -5
  20. deer_agent_framework-0.1a2/deer/tools/builtin/structured_data_inspector.py +112 -0
  21. deer_agent_framework-0.1a2/deer/tools/builtin/system_inspector.py +55 -0
  22. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/registry.py +6 -6
  23. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/PKG-INFO +29 -6
  24. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/SOURCES.txt +9 -0
  25. deer_agent_framework-0.1a2/pyproject.toml +45 -0
  26. deer_agent_framework-0.1a2/tests/test_azure_driver.py +56 -0
  27. deer_agent_framework-0.1a2/tests/test_openai_driver.py +69 -0
  28. deer_agent_framework-0.0/deer/builtins/python_manager/tools.py +0 -54
  29. deer_agent_framework-0.0/deer/drivers/base_driver.py +0 -56
  30. deer_agent_framework-0.0/deer/tools/builtin/__init__.py +0 -3
  31. deer_agent_framework-0.0/pyproject.toml +0 -30
  32. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/LICENSE +0 -0
  33. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/__init__.py +0 -0
  34. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/builtins/__init__.py +0 -0
  35. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/core/__init__.py +0 -0
  36. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/core/ui.py +0 -0
  37. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/drivers/gemini_driver.py +0 -0
  38. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/drivers/ollama_driver.py +0 -0
  39. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/__init__.py +0 -0
  40. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/executor.py +0 -0
  41. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/logic.py +0 -0
  42. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/executor/logic_secure.py +0 -0
  43. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/main.py +0 -0
  44. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/planner/__init__.py +0 -0
  45. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/planner/planner.py +0 -0
  46. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/__init__.py +0 -0
  47. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/error_explain.py +0 -0
  48. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/goal_improvement.py +0 -0
  49. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/goal_validation.py +0 -0
  50. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/humanizer.py +0 -0
  51. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/planner.py +0 -0
  52. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/prompts/response_improvement.py +0 -0
  53. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/schema/__init__.py +0 -0
  54. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/schema/plan.py +0 -0
  55. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tools/decorators.py +0 -0
  56. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tracing/__init__.py +0 -0
  57. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tracing/logging_config.py +0 -0
  58. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/tracing/store.py +0 -0
  59. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/__init__.py +0 -0
  60. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/console.py +0 -0
  61. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/plots/__init__.py +0 -0
  62. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/utils/plots/plot_traces.py +0 -0
  63. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/validator/__init__.py +0 -0
  64. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/validator/plan_validator.py +0 -0
  65. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer/validator/rules.py +0 -0
  66. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/dependency_links.txt +0 -0
  67. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/entry_points.txt +0 -0
  68. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/requires.txt +0 -0
  69. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/deer_agent_framework.egg-info/top_level.txt +0 -0
  70. {deer_agent_framework-0.0 → deer_agent_framework-0.1a2}/setup.cfg +0 -0
  71. {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.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
- [![License](https://img.shields.io/badge/license-BSD--2--Clause-blue.svg)](LICENSE)
25
- [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
37
+ ![License](https://img.shields.io/badge/license-BSD--2--Clause-blue.svg)
38
+ ![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)
39
+ ![GitHub top language](https://img.shields.io/github/languages/top/dunderlab/deer-agent-framework)
40
+ ![PyPI - License](https://img.shields.io/pypi/l/deer-agent-framework)
41
+ ![PyPI](https://img.shields.io/pypi/v/deer-agent-framework)
42
+ ![PyPI - Status](https://img.shields.io/pypi/status/deer-agent-framework)
43
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/deer-agent-framework)
44
+ ![GitHub last commit](https://img.shields.io/github/last-commit/dunderlab/deer-agent-framework)
45
+ ![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/dunderlab/deer-agent-framework)
46
+ ![Upload Python Package](https://github.com/dunderlab/deer-agent-framework/actions/workflows/python-publish.yml/badge.svg)
47
+
48
+ [//]: # ([![Documentation Status](https://readthedocs.org/projects/chaski-confluent/badge/?version=latest)](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", # Strict security boundary
82
+ jail_path=Path.cwd() / "sandbox", # Strict security boundary
60
83
  format_response="markdown",
61
- max_tries_for_plan=5
84
+ max_retries=5
62
85
  )
63
86
 
64
87
  if __name__ == "__main__":
65
- agent.repl() # Instant interactive shell
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
- [![License](https://img.shields.io/badge/license-BSD--2--Clause-blue.svg)](LICENSE)
8
- [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
7
+ ![License](https://img.shields.io/badge/license-BSD--2--Clause-blue.svg)
8
+ ![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)
9
+ ![GitHub top language](https://img.shields.io/github/languages/top/dunderlab/deer-agent-framework)
10
+ ![PyPI - License](https://img.shields.io/pypi/l/deer-agent-framework)
11
+ ![PyPI](https://img.shields.io/pypi/v/deer-agent-framework)
12
+ ![PyPI - Status](https://img.shields.io/pypi/status/deer-agent-framework)
13
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/deer-agent-framework)
14
+ ![GitHub last commit](https://img.shields.io/github/last-commit/dunderlab/deer-agent-framework)
15
+ ![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/dunderlab/deer-agent-framework)
16
+ ![Upload Python Package](https://github.com/dunderlab/deer-agent-framework/actions/workflows/python-publish.yml/badge.svg)
17
+
18
+ [//]: # ([![Documentation Status](https://readthedocs.org/projects/chaski-confluent/badge/?version=latest)](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", # Strict security boundary
52
+ jail_path=Path.cwd() / "sandbox", # Strict security boundary
43
53
  format_response="markdown",
44
- max_tries_for_plan=5
54
+ max_retries=5
45
55
  )
46
56
 
47
57
  if __name__ == "__main__":
48
- agent.repl() # Instant interactive shell
58
+ agent.repl() # Instant interactive shell
49
59
  ```
50
60
 
51
61
  ### 2. Assemble your Tool Registry
@@ -21,7 +21,7 @@ agent = DeterministicAgent(
21
21
  registry=registry,
22
22
  jail_path=Path.cwd(),
23
23
  format_response="markdown",
24
- max_tries_for_plan=5,
24
+ max_retries=5,
25
25
  rollback=None,
26
26
  )
27
27
 
@@ -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
- max_tries_for_plan: int = 3,
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
- max_tries_for_plan > 0
72
- ), f"max_tries_for_plan must be positive, got {max_tries_for_plan}"
73
- self.max_tries_for_plan = max_tries_for_plan
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.max_tries_for_plan):
156
- logger.debug(f"Attempt {attempt_idx + 1} of {self.max_tries_for_plan}")
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.max_tries_for_plan):
159
+ for solution_idx in range(self.max_retries):
161
160
  logger.debug(
162
- f"Solution planning {solution_idx + 1} of {self.max_tries_for_plan}"
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.max_tries_for_plan} attempts"
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.max_tries_for_plan):
201
+ for verification_idx in range(self.max_retries):
203
202
  logger.debug(
204
- f"Verification planning {verification_idx + 1} of {self.max_tries_for_plan}"
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.max_tries_for_plan} attempts"
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
- pass
66
- # return OpenAIDriver(model_name=args.model)
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"
@@ -38,3 +38,6 @@ def Return(**fields: Any) -> type[BaseModel]:
38
38
  f"InlineModel_{next(_counter)}",
39
39
  **{name: (field_type, ...) for name, field_type in fields.items()},
40
40
  )
41
+
42
+
43
+ CommandOut = Return(stdout=str, stderr=str, returncode=int, message=str)
@@ -1,4 +1,4 @@
1
1
  from .base import Tool, ToolProvider
2
2
  from .registry import ToolRegistry
3
3
  from .decorators import tool
4
- from deer.schema.io import Return
4
+ from deer.schema.io import Return, CommandOut
@@ -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
- args = shlex.split(command)
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