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.
Files changed (52) hide show
  1. deer/__init__.py +36 -0
  2. deer/builtins/__init__.py +7 -0
  3. deer/builtins/python_manager/agent.py +29 -0
  4. deer/builtins/python_manager/tools.py +54 -0
  5. deer/core/__init__.py +1 -0
  6. deer/core/agent.py +463 -0
  7. deer/core/ui.py +31 -0
  8. deer/drivers/__init__.py +66 -0
  9. deer/drivers/base_driver.py +56 -0
  10. deer/drivers/gemini_driver.py +62 -0
  11. deer/drivers/ollama_driver.py +69 -0
  12. deer/executor/__init__.py +1 -0
  13. deer/executor/executor.py +168 -0
  14. deer/executor/logic.py +75 -0
  15. deer/executor/logic_secure.py +102 -0
  16. deer/main.py +71 -0
  17. deer/planner/__init__.py +1 -0
  18. deer/planner/planner.py +71 -0
  19. deer/prompts/__init__.py +6 -0
  20. deer/prompts/error_explain.py +30 -0
  21. deer/prompts/goal_improvement.py +65 -0
  22. deer/prompts/goal_validation.py +31 -0
  23. deer/prompts/humanizer.py +20 -0
  24. deer/prompts/planner.py +92 -0
  25. deer/prompts/response_improvement.py +23 -0
  26. deer/schema/__init__.py +2 -0
  27. deer/schema/io.py +40 -0
  28. deer/schema/plan.py +75 -0
  29. deer/tools/__init__.py +4 -0
  30. deer/tools/base.py +141 -0
  31. deer/tools/builtin/__init__.py +3 -0
  32. deer/tools/builtin/file_manager.py +136 -0
  33. deer/tools/builtin/git_manager.py +67 -0
  34. deer/tools/builtin/search_manager.py +123 -0
  35. deer/tools/decorators.py +127 -0
  36. deer/tools/registry.py +114 -0
  37. deer/tracing/__init__.py +2 -0
  38. deer/tracing/logging_config.py +20 -0
  39. deer/tracing/store.py +21 -0
  40. deer/utils/__init__.py +0 -0
  41. deer/utils/console.py +11 -0
  42. deer/utils/plots/__init__.py +7 -0
  43. deer/utils/plots/plot_traces.py +1034 -0
  44. deer/validator/__init__.py +1 -0
  45. deer/validator/plan_validator.py +23 -0
  46. deer/validator/rules.py +98 -0
  47. deer_agent_framework-0.0.dist-info/METADATA +163 -0
  48. deer_agent_framework-0.0.dist-info/RECORD +52 -0
  49. deer_agent_framework-0.0.dist-info/WHEEL +5 -0
  50. deer_agent_framework-0.0.dist-info/entry_points.txt +2 -0
  51. deer_agent_framework-0.0.dist-info/licenses/LICENSE +24 -0
  52. deer_agent_framework-0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,67 @@
1
+ from deer.tools import ToolProvider, tool, Return
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import shlex
6
+
7
+
8
+ class GitManagerError(ValueError):
9
+ pass
10
+
11
+
12
+ CommandOut = Return(stdout=str, stderr=str, returncode=int)
13
+
14
+
15
+ @dataclass
16
+ class GitManager(ToolProvider):
17
+
18
+ def git(self, path: str, *args: str | int) -> CommandOut:
19
+ quoted_args = " ".join(shlex.quote(str(arg)) for arg in args)
20
+ return self.run_command(f"git {quoted_args}", cwd=path)
21
+
22
+ @tool()
23
+ def git_status(self, path: str) -> CommandOut:
24
+ """Returns the short git status for a repository path inside the jail."""
25
+ return self.git(path, "status", "--short")
26
+
27
+ @tool()
28
+ def git_current_branch(self, path: str) -> CommandOut:
29
+ """Returns the current branch name for a repository path inside the jail."""
30
+ return self.git(path, "branch", "--show-current")
31
+
32
+ @tool()
33
+ def git_log(self, path: str, max_count: int) -> CommandOut:
34
+ """Returns the recent commit log for a repository path inside the jail."""
35
+ return self.git(
36
+ path, "log", "--oneline", "--decorate", f"--max-count={max_count}"
37
+ )
38
+
39
+ @tool()
40
+ def git_diff(self, path: str, target: str) -> CommandOut:
41
+ """Returns the unstaged diff for a target path inside a repository."""
42
+ return self.git(path, "diff", "--", target)
43
+
44
+ @tool()
45
+ def git_staged_diff(self, path: str, target: str) -> CommandOut:
46
+ """Returns the staged diff for a target path inside a repository."""
47
+ return self.git(path, "diff", "--cached", "--", target)
48
+
49
+ @tool()
50
+ def git_show(self, path: str, revision: str) -> CommandOut:
51
+ """Returns details for a git revision inside a repository path."""
52
+ return self.git(path, "show", "--stat", "--patch", revision)
53
+
54
+ @tool(modifies_state=True)
55
+ def git_add(self, path: str, target: str) -> CommandOut:
56
+ """Stages a target path inside a repository."""
57
+ return self.git(path, "add", "--", target)
58
+
59
+ @tool(modifies_state=True)
60
+ def git_commit(self, path: str, message: str) -> CommandOut:
61
+ """Creates a commit in a repository path with the provided message."""
62
+ return self.git(path, "commit", "-m", message)
63
+
64
+ @tool(modifies_state=True)
65
+ def git_restore(self, path: str, target: str) -> CommandOut:
66
+ """Restores unstaged changes for a target path inside a repository."""
67
+ return self.git(path, "restore", "--", target)
@@ -0,0 +1,123 @@
1
+ from deer.tools import ToolProvider, tool, Return
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import shlex
6
+
7
+
8
+ class SearchManagerError(ValueError):
9
+ pass
10
+
11
+
12
+ CommandOut = Return(stdout=str, stderr=str, returncode=int)
13
+
14
+
15
+ @dataclass
16
+ class SearchManager(ToolProvider):
17
+
18
+ def rg(self, path: str, *args: str | int) -> CommandOut:
19
+ quoted_args = " ".join(shlex.quote(str(arg)) for arg in args)
20
+ return self.run_command(f"rg {quoted_args}", cwd=path)
21
+
22
+ @tool()
23
+ def search_text(self, path: str, query: str) -> CommandOut:
24
+ """Searches for literal text under a path inside the jail."""
25
+ return self.rg(
26
+ path,
27
+ "--fixed-strings",
28
+ "--line-number",
29
+ "--column",
30
+ "--no-heading",
31
+ query,
32
+ ".",
33
+ )
34
+
35
+ @tool()
36
+ def search_regex(self, path: str, pattern: str) -> CommandOut:
37
+ """Searches for a regex pattern under a path inside the jail."""
38
+ return self.rg(
39
+ path,
40
+ "--line-number",
41
+ "--column",
42
+ "--no-heading",
43
+ pattern,
44
+ ".",
45
+ )
46
+
47
+ @tool()
48
+ def search_text_ignore_case(self, path: str, query: str) -> CommandOut:
49
+ """Searches for literal text case-insensitively under a path inside the jail."""
50
+ return self.rg(
51
+ path,
52
+ "--fixed-strings",
53
+ "--ignore-case",
54
+ "--line-number",
55
+ "--column",
56
+ "--no-heading",
57
+ query,
58
+ ".",
59
+ )
60
+
61
+ @tool()
62
+ def search_regex_ignore_case(self, path: str, pattern: str) -> CommandOut:
63
+ """Searches for a regex pattern case-insensitively under a path inside the jail."""
64
+ return self.rg(
65
+ path,
66
+ "--ignore-case",
67
+ "--line-number",
68
+ "--column",
69
+ "--no-heading",
70
+ pattern,
71
+ ".",
72
+ )
73
+
74
+ @tool()
75
+ def find_files(self, path: str, pattern: str) -> CommandOut:
76
+ """Finds files matching a glob pattern under a path inside the jail."""
77
+ return self.rg(path, "--files", "-g", pattern, ".")
78
+
79
+ @tool()
80
+ def search_file_names(self, path: str, pattern: str) -> CommandOut:
81
+ """Finds files whose names contain text under a path inside the jail."""
82
+ return self.rg(path, "--files", "-g", f"*{pattern}*", ".")
83
+
84
+ @tool()
85
+ def list_files(self, path: str) -> CommandOut:
86
+ """Lists files under a path inside the jail."""
87
+ return self.rg(path, "--files", ".")
88
+
89
+ @tool()
90
+ def search_by_extension(self, path: str, extension: str) -> CommandOut:
91
+ """Lists files with an extension under a path inside the jail."""
92
+ normalized_extension = extension.lstrip(".")
93
+ return self.rg(path, "--files", "-g", f"*.{normalized_extension}", ".")
94
+
95
+ @tool()
96
+ def search_text_in_files(self, path: str, query: str, glob: str) -> CommandOut:
97
+ """Searches for literal text under a path, restricted to files matching a glob."""
98
+ return self.rg(
99
+ path,
100
+ "--fixed-strings",
101
+ "--line-number",
102
+ "--column",
103
+ "--no-heading",
104
+ "-g",
105
+ glob,
106
+ query,
107
+ ".",
108
+ )
109
+
110
+ @tool()
111
+ def files_with_matches(self, path: str, query: str) -> CommandOut:
112
+ """Lists files containing literal text under a path inside the jail."""
113
+ return self.rg(path, "--fixed-strings", "--files-with-matches", query, ".")
114
+
115
+ @tool()
116
+ def files_without_matches(self, path: str, query: str) -> CommandOut:
117
+ """Lists files that do not contain literal text under a path inside the jail."""
118
+ return self.rg(path, "--fixed-strings", "--files-without-match", query, ".")
119
+
120
+ @tool()
121
+ def count_matches(self, path: str, query: str) -> CommandOut:
122
+ """Counts literal text matches per file under a path inside the jail."""
123
+ return self.rg(path, "--fixed-strings", "--count-matches", query, ".")
@@ -0,0 +1,127 @@
1
+ import inspect
2
+ from typing import get_type_hints, Any, Callable
3
+ from dataclasses import dataclass
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from .base import Tool
8
+
9
+ _TOOL_METADATA_ATTR = "__deer_tool_metadata__"
10
+ _MISSING = object()
11
+
12
+
13
+ def tool(
14
+ *,
15
+ modifies_state: bool = False,
16
+ ):
17
+ """Mark an instance method as a deterministic tool."""
18
+
19
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
20
+ assert func.__doc__, f"Tool '{func.__name__}' must have a docstring annotation."
21
+
22
+ metadata = _build_tool_metadata(
23
+ func=func,
24
+ name=func.__name__,
25
+ description=func.__doc__,
26
+ modifies_state=modifies_state,
27
+ )
28
+ setattr(
29
+ func,
30
+ _TOOL_METADATA_ATTR,
31
+ metadata,
32
+ )
33
+ return func
34
+ # return staticmethod(func)
35
+
36
+ return decorator
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class MethodTool(Tool):
41
+ """Adapter that exposes a decorated method as a Tool instance."""
42
+
43
+ name: str
44
+ description: str
45
+ full_description: str
46
+ params_type: Any
47
+ return_type: Any
48
+ method: Callable[..., Any]
49
+ modifies_state: bool
50
+
51
+ def run(
52
+ self,
53
+ params: dict[str, Any] | None = None,
54
+ ) -> Any:
55
+ return self.method(**(params or {}))
56
+
57
+
58
+ def is_tool_method(obj: Any) -> bool:
59
+ return hasattr(obj, _TOOL_METADATA_ATTR) and (
60
+ not obj.__self__.tools or obj.__name__ in obj.__self__.tools
61
+ )
62
+
63
+
64
+ def get_tool_metadata(obj: Any) -> dict[str, Any]:
65
+ return getattr(obj, _TOOL_METADATA_ATTR)
66
+
67
+
68
+ def _build_tool_metadata(
69
+ *,
70
+ func: Callable[..., Any],
71
+ name: str,
72
+ description: str,
73
+ modifies_state: bool,
74
+ ) -> dict[str, Any]:
75
+ if not name or not name.strip():
76
+ raise ValueError("Tool name cannot be empty.")
77
+
78
+ if not description or not description.strip():
79
+ raise ValueError("Tool description cannot be empty.")
80
+
81
+ signature = inspect.signature(func)
82
+ type_hints = get_type_hints(func)
83
+
84
+ return_type = type_hints.pop("return")
85
+ params_type = type_hints
86
+
87
+ if issubclass(return_type, BaseModel):
88
+ return_type = {
89
+ name: field.annotation for name, field in return_type.model_fields.items()
90
+ }
91
+
92
+ if return_type is _MISSING:
93
+ raise TypeError(
94
+ f"Tool '{name}' must declare a return type annotation on "
95
+ f"{func.__qualname__}."
96
+ )
97
+
98
+ full_description = _build_description(
99
+ base_description=description.strip(),
100
+ params_type=params_type,
101
+ return_type=return_type,
102
+ )
103
+
104
+ return {
105
+ "name": name,
106
+ "description": description.strip(),
107
+ "full_description": full_description,
108
+ "modifies_state": modifies_state,
109
+ "params_type": params_type,
110
+ "return_type": return_type,
111
+ }
112
+
113
+
114
+ def _build_description(
115
+ *,
116
+ base_description: str,
117
+ params_type: Any,
118
+ return_type: Any,
119
+ ) -> str:
120
+
121
+ return "\n".join(
122
+ [
123
+ base_description,
124
+ f"Parameters: {params_type}.",
125
+ f"Return: {return_type}.",
126
+ ]
127
+ )
deer/tools/registry.py ADDED
@@ -0,0 +1,114 @@
1
+ from typing import Any, Dict, Iterable
2
+
3
+ from .base import Tool
4
+ from .decorators import MethodTool, get_tool_metadata, is_tool_method
5
+ from deer.schema.io import Return
6
+
7
+
8
+ class ToolRegistry:
9
+ def __init__(self) -> None:
10
+ self._tools: Dict[str, Tool] = {}
11
+ self._providers: list = []
12
+
13
+ def register(self, *provider_tools: list):
14
+ for tool in provider_tools:
15
+ if isinstance(tool, Tool):
16
+ self._register_tool(tool)
17
+ else:
18
+ self._register_collection(tool)
19
+
20
+ def _register_tool(self, tool: Tool) -> None:
21
+ if not tool.name or not tool.name.strip():
22
+ raise ValueError("Tool name cannot be empty.")
23
+
24
+ self._tools[tool.name] = tool
25
+
26
+ def _register_collection(self, provider: Any) -> None:
27
+ self._providers.append(provider)
28
+ for attr_name in dir(provider):
29
+
30
+ if attr_name == "jail":
31
+ continue
32
+
33
+ attr = getattr(provider, attr_name)
34
+
35
+ if not is_tool_method(attr):
36
+ continue
37
+
38
+ metadata = get_tool_metadata(attr)
39
+
40
+ self.register(
41
+ MethodTool(
42
+ name=metadata["name"],
43
+ description=metadata["description"],
44
+ full_description=metadata["full_description"],
45
+ modifies_state=metadata["modifies_state"],
46
+ params_type=metadata["params_type"],
47
+ return_type=metadata["return_type"],
48
+ method=attr,
49
+ )
50
+ )
51
+
52
+ def get(self, name: str) -> Tool:
53
+ if not name or not name.strip():
54
+ raise KeyError("Tool name cannot be empty.")
55
+
56
+ if name not in self._tools:
57
+ raise KeyError(f"Tool not found: {name}")
58
+
59
+ return self._tools[name]
60
+
61
+ def has(self, name: str | None) -> bool:
62
+ if not name:
63
+ return False
64
+
65
+ return name in self._tools
66
+
67
+ def list_tools(self) -> Iterable[str]:
68
+ return self._tools.keys()
69
+
70
+ def providers(self):
71
+ return self._providers
72
+
73
+ def set_jail(self, jail_path):
74
+ self.jail_path = jail_path
75
+ for provider in self._providers:
76
+ provider.jail = jail_path
77
+
78
+ def describe(self, include_state_modifying=True, markdown=False) -> str:
79
+ if not self._tools:
80
+ return "- No tools are available."
81
+
82
+ lines = []
83
+
84
+ for tool in self._tools.values():
85
+
86
+ if not include_state_modifying and tool.modifies_state:
87
+ continue
88
+
89
+ if markdown:
90
+ cont = tool.description.split('\n')[0]
91
+ lines.append(f" - `{tool.name}`: *{cont}*")
92
+ else:
93
+ cont = tool.full_description.replace("\n", "\n ")
94
+ lines.append(
95
+ f"- {tool.name}: {cont}"
96
+ )
97
+
98
+ return "\n".join(lines)
99
+
100
+
101
+ class EchoTool(Tool):
102
+ name = "echo"
103
+ description = "Returns params['echo'] when provided; otherwise returns the input value unchanged."
104
+ modifies_state = False
105
+
106
+ def run(self, params: dict[str, Any] | None = None) -> Return(echo=dict):
107
+ params = params or {}
108
+ return params
109
+
110
+
111
+ def default_registry() -> ToolRegistry:
112
+ reg = ToolRegistry()
113
+ reg.register(EchoTool())
114
+ return reg
@@ -0,0 +1,2 @@
1
+ from .store import TraceStore
2
+ from .logging_config import logger, logger_llm
@@ -0,0 +1,20 @@
1
+ import logging
2
+
3
+ formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
4
+
5
+ logger = logging.getLogger("DEER")
6
+ if not logger.handlers:
7
+ handler = logging.StreamHandler()
8
+ handler.setFormatter(formatter)
9
+ logger.addHandler(handler)
10
+ logger.setLevel(logging.WARNING)
11
+ logger.propagate = False
12
+
13
+
14
+ logger_llm = logging.getLogger("DEER-LLM")
15
+ if not logger_llm.handlers:
16
+ handler = logging.StreamHandler()
17
+ handler.setFormatter(formatter)
18
+ logger_llm.addHandler(handler)
19
+ logger_llm.setLevel(logging.WARNING)
20
+ logger_llm.propagate = False
deer/tracing/store.py ADDED
@@ -0,0 +1,21 @@
1
+ from typing import List
2
+ from deer.schema.io import StepTrace
3
+
4
+
5
+ class TraceStore:
6
+ def __init__(self) -> None:
7
+ self._buffer: List[StepTrace] = []
8
+
9
+ def reset(self) -> None:
10
+ """Limpia la traza actual (para una nueva ejecución)."""
11
+ self._buffer.clear()
12
+
13
+ def append(self, trace: StepTrace) -> None:
14
+ self._buffer.append(trace)
15
+
16
+ def extend(self, traces: list[StepTrace]):
17
+ self._buffer.extend(traces)
18
+
19
+ def get_trace(self) -> List[StepTrace]:
20
+ """Devuelve una copia de la traza acumulada."""
21
+ return list(self._buffer)
deer/utils/__init__.py ADDED
File without changes
deer/utils/console.py ADDED
@@ -0,0 +1,11 @@
1
+ from rich.console import Console
2
+
3
+ console = Console()
4
+
5
+
6
+ def info(message: str):
7
+ console.print(f"[bold cyan]INFO[/bold cyan] {message}")
8
+
9
+
10
+ def error(message: str):
11
+ console.print(f"[bold red]ERROR[/bold red] {message}")
@@ -0,0 +1,7 @@
1
+ from .plot_traces import (
2
+ get_plot_data,
3
+ plot_steps,
4
+ draw_trace_frequency_bars,
5
+ get_execution_metrics,
6
+ plot_execution_profile,
7
+ )