dea-agents 0.1.0__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.
- dea_agents-0.1.0/.gitignore +15 -0
- dea_agents-0.1.0/LICENSE +21 -0
- dea_agents-0.1.0/PKG-INFO +107 -0
- dea_agents-0.1.0/README.md +79 -0
- dea_agents-0.1.0/dea_agents/__init__.py +152 -0
- dea_agents-0.1.0/dea_agents/_specs.py +41 -0
- dea_agents-0.1.0/dea_agents/crewai.py +51 -0
- dea_agents-0.1.0/dea_agents/langchain.py +48 -0
- dea_agents-0.1.0/dea_agents/llamaindex.py +33 -0
- dea_agents-0.1.0/pyproject.toml +34 -0
- dea_agents-0.1.0/tests/test_client.py +49 -0
dea_agents-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shreyas Sawant and DEA contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dea-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Debug Anything from Python agents: observe running processes (strace, py-spy, perf, lsof, tcpdump, gdb/lldb, heap snapshots) and get evidence-backed findings. Wraps the DEA CLI/MCP server; tool adapters for LangChain, LlamaIndex and CrewAI.
|
|
5
|
+
Project-URL: Homepage, https://github.com/shreyassaw5394/dea
|
|
6
|
+
Project-URL: Documentation, https://github.com/shreyassaw5394/dea/tree/main/docs
|
|
7
|
+
Project-URL: Issues, https://github.com/shreyassaw5394/dea/issues
|
|
8
|
+
Author: Shreyas Sawant
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,crewai,debugging,langchain,llamaindex,mcp,observability,perf,profiling,py-spy,strace
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Provides-Extra: crewai
|
|
18
|
+
Requires-Dist: crewai>=0.30; extra == 'crewai'
|
|
19
|
+
Requires-Dist: pydantic>=2; extra == 'crewai'
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Provides-Extra: langchain
|
|
23
|
+
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
|
|
24
|
+
Requires-Dist: pydantic>=2; extra == 'langchain'
|
|
25
|
+
Provides-Extra: llamaindex
|
|
26
|
+
Requires-Dist: llama-index-core>=0.10; extra == 'llamaindex'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# dea-agents (Python)
|
|
30
|
+
|
|
31
|
+
**Debug Anything from Python.** Observe a running process, container or pod —
|
|
32
|
+
syscalls, CPU profiles, thread stacks, sockets, packets, heap retainers — and
|
|
33
|
+
get findings that cite evidence. This package wraps the
|
|
34
|
+
[DEA](https://github.com/shreyassaw5394/dea) CLI (Node ≥ 22 required:
|
|
35
|
+
`npx -y dea-agents` is used automatically) and ships tool adapters for
|
|
36
|
+
LangChain, LlamaIndex and CrewAI.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install dea-agents # core (no deps)
|
|
40
|
+
pip install 'dea-agents[langchain]' # + LangChain StructuredTools
|
|
41
|
+
pip install 'dea-agents[llamaindex]' # + LlamaIndex FunctionTools
|
|
42
|
+
pip install 'dea-agents[crewai]' # + CrewAI BaseTools
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Plain Python
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from dea_agents import DEA
|
|
49
|
+
|
|
50
|
+
dea = DEA() # uses `dea` if installed, else `npx -y dea-agents@latest`
|
|
51
|
+
print(dea.readiness()["findings"])
|
|
52
|
+
s = dea.attach(12345) # pid, "my-container", "ns/pod", or a command
|
|
53
|
+
before = dea.snapshot_memory(s)
|
|
54
|
+
# ... exercise the target ...
|
|
55
|
+
after = dea.snapshot_memory(s)
|
|
56
|
+
diff = dea.diff_runs(s, before["run_id"], after["run_id"])
|
|
57
|
+
for line in DEA.findings(diff):
|
|
58
|
+
print(line) # [high] heap_retainer: name=Array grew 2.0 KB → 50.1 MB (observed; e4,e1)
|
|
59
|
+
dea.report(s, out="report.md")
|
|
60
|
+
dea.detach(s)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every call returns the DEA `ToolResult` dict (`ok`, `findings`, `evidence`,
|
|
64
|
+
`limitations`, `suggested_next`, `artifacts`). `capture_traffic` needs
|
|
65
|
+
`consent=True` — ask your user first.
|
|
66
|
+
|
|
67
|
+
## LangChain
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from dea_agents.langchain import dea_tools
|
|
71
|
+
from langgraph.prebuilt import create_react_agent
|
|
72
|
+
|
|
73
|
+
agent = create_react_agent(llm, dea_tools())
|
|
74
|
+
agent.invoke({"messages": [("user", "pid 12345 is slow — attach and tell me why, with evidence")]})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## LlamaIndex
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from dea_agents.llamaindex import dea_tools
|
|
81
|
+
from llama_index.core.agent import ReActAgent
|
|
82
|
+
|
|
83
|
+
agent = ReActAgent.from_tools(dea_tools(), llm=llm, verbose=True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## CrewAI
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dea_agents.crewai import dea_tools
|
|
90
|
+
from crewai import Agent
|
|
91
|
+
|
|
92
|
+
sre = Agent(role="SRE", goal="Diagnose the hung worker with evidence, never guess", tools=dea_tools(), llm=llm)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Or just use MCP
|
|
96
|
+
|
|
97
|
+
Any framework with MCP support (LangChain `langchain-mcp-adapters`, LlamaIndex
|
|
98
|
+
`McpToolSpec`, CrewAI `MCPServerAdapter`, Claude Code, Codex, Cursor…) can run
|
|
99
|
+
DEA directly: `{"command": "npx", "args": ["-y", "dea-agents@latest", "serve"]}`.
|
|
100
|
+
Same 14 tools, same ToolResult, plus the guided prompts.
|
|
101
|
+
|
|
102
|
+
## Safety
|
|
103
|
+
|
|
104
|
+
DEA is read-only, never runs sudo, caps every tool at 60 s, redacts secrets in
|
|
105
|
+
what it returns, and keeps raw artifacts under `~/.dea/sessions/`. See
|
|
106
|
+
[docs/safety.md](https://github.com/shreyassaw5394/dea/blob/main/docs/safety.md).
|
|
107
|
+
MIT.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# dea-agents (Python)
|
|
2
|
+
|
|
3
|
+
**Debug Anything from Python.** Observe a running process, container or pod —
|
|
4
|
+
syscalls, CPU profiles, thread stacks, sockets, packets, heap retainers — and
|
|
5
|
+
get findings that cite evidence. This package wraps the
|
|
6
|
+
[DEA](https://github.com/shreyassaw5394/dea) CLI (Node ≥ 22 required:
|
|
7
|
+
`npx -y dea-agents` is used automatically) and ships tool adapters for
|
|
8
|
+
LangChain, LlamaIndex and CrewAI.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install dea-agents # core (no deps)
|
|
12
|
+
pip install 'dea-agents[langchain]' # + LangChain StructuredTools
|
|
13
|
+
pip install 'dea-agents[llamaindex]' # + LlamaIndex FunctionTools
|
|
14
|
+
pip install 'dea-agents[crewai]' # + CrewAI BaseTools
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Plain Python
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from dea_agents import DEA
|
|
21
|
+
|
|
22
|
+
dea = DEA() # uses `dea` if installed, else `npx -y dea-agents@latest`
|
|
23
|
+
print(dea.readiness()["findings"])
|
|
24
|
+
s = dea.attach(12345) # pid, "my-container", "ns/pod", or a command
|
|
25
|
+
before = dea.snapshot_memory(s)
|
|
26
|
+
# ... exercise the target ...
|
|
27
|
+
after = dea.snapshot_memory(s)
|
|
28
|
+
diff = dea.diff_runs(s, before["run_id"], after["run_id"])
|
|
29
|
+
for line in DEA.findings(diff):
|
|
30
|
+
print(line) # [high] heap_retainer: name=Array grew 2.0 KB → 50.1 MB (observed; e4,e1)
|
|
31
|
+
dea.report(s, out="report.md")
|
|
32
|
+
dea.detach(s)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every call returns the DEA `ToolResult` dict (`ok`, `findings`, `evidence`,
|
|
36
|
+
`limitations`, `suggested_next`, `artifacts`). `capture_traffic` needs
|
|
37
|
+
`consent=True` — ask your user first.
|
|
38
|
+
|
|
39
|
+
## LangChain
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dea_agents.langchain import dea_tools
|
|
43
|
+
from langgraph.prebuilt import create_react_agent
|
|
44
|
+
|
|
45
|
+
agent = create_react_agent(llm, dea_tools())
|
|
46
|
+
agent.invoke({"messages": [("user", "pid 12345 is slow — attach and tell me why, with evidence")]})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## LlamaIndex
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from dea_agents.llamaindex import dea_tools
|
|
53
|
+
from llama_index.core.agent import ReActAgent
|
|
54
|
+
|
|
55
|
+
agent = ReActAgent.from_tools(dea_tools(), llm=llm, verbose=True)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## CrewAI
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from dea_agents.crewai import dea_tools
|
|
62
|
+
from crewai import Agent
|
|
63
|
+
|
|
64
|
+
sre = Agent(role="SRE", goal="Diagnose the hung worker with evidence, never guess", tools=dea_tools(), llm=llm)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Or just use MCP
|
|
68
|
+
|
|
69
|
+
Any framework with MCP support (LangChain `langchain-mcp-adapters`, LlamaIndex
|
|
70
|
+
`McpToolSpec`, CrewAI `MCPServerAdapter`, Claude Code, Codex, Cursor…) can run
|
|
71
|
+
DEA directly: `{"command": "npx", "args": ["-y", "dea-agents@latest", "serve"]}`.
|
|
72
|
+
Same 14 tools, same ToolResult, plus the guided prompts.
|
|
73
|
+
|
|
74
|
+
## Safety
|
|
75
|
+
|
|
76
|
+
DEA is read-only, never runs sudo, caps every tool at 60 s, redacts secrets in
|
|
77
|
+
what it returns, and keeps raw artifacts under `~/.dea/sessions/`. See
|
|
78
|
+
[docs/safety.md](https://github.com/shreyassaw5394/dea/blob/main/docs/safety.md).
|
|
79
|
+
MIT.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""dea-agents — Debug Anything from Python.
|
|
2
|
+
|
|
3
|
+
Thin, dependency-free wrapper around the DEA CLI (``npx -y dea-agents``). Every
|
|
4
|
+
method returns the DEA ``ToolResult`` dict: ``findings`` cite ``evidence`` ids,
|
|
5
|
+
``limitations`` say what could not be observed, ``artifacts`` point at raw
|
|
6
|
+
files under ``~/.dea/sessions/<id>/``.
|
|
7
|
+
|
|
8
|
+
from dea_agents import DEA
|
|
9
|
+
dea = DEA()
|
|
10
|
+
s = dea.attach(12345)
|
|
11
|
+
print(dea.profile_cpu(s, duration_s=10)["findings"])
|
|
12
|
+
dea.detach(s)
|
|
13
|
+
|
|
14
|
+
Framework adapters: ``dea_agents.langchain``, ``dea_agents.llamaindex``,
|
|
15
|
+
``dea_agents.crewai``. Agents that speak MCP can skip this package and run
|
|
16
|
+
``npx -y dea-agents serve`` directly.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
__all__ = ["DEA", "DEAError", "TOOLS", "ToolResult"]
|
|
28
|
+
|
|
29
|
+
ToolResult = Dict[str, Any]
|
|
30
|
+
|
|
31
|
+
TOOLS: Dict[str, str] = {
|
|
32
|
+
"readiness": "OS, arch, which providers are usable, what is missing and how to install it, permission status. Call first.",
|
|
33
|
+
"attach": "Resolve a target (pid, container, pod, or a command to spawn) and open a session. Returns session_id.",
|
|
34
|
+
"detach": "Stop captures, write the report, keep artifacts.",
|
|
35
|
+
"list_processes": "ps-style list (pid, comm, cpu, mem, user, container) with optional substring filter.",
|
|
36
|
+
"trace_syscalls": "Linux strace for N seconds: top syscalls by time/count, slowest calls, error counts, blocked waits.",
|
|
37
|
+
"profile_cpu": "CPU profile (py-spy / perf / sample): top frames, hottest stacks, flamegraph.svg artifact.",
|
|
38
|
+
"dump_stacks": "Every thread's stack; flags lock waits and likely deadlocks.",
|
|
39
|
+
"inspect_sockets": "Listening ports, connections by remote, CLOSE_WAIT/TIME_WAIT pileups, fd count vs ulimit.",
|
|
40
|
+
"inspect_files": "Open fds by type, fd growth over a window, largest and deleted-but-open files.",
|
|
41
|
+
"capture_traffic": "tcpdump scoped to the target's ports (needs consent=True): per-remote bytes, RSTs, retransmits, DNS.",
|
|
42
|
+
"snapshot_memory": "RSS series; Node heap snapshot with top retainers; Python stacks with locals.",
|
|
43
|
+
"diff_runs": "Diff two prior runs (e.g. snapshot_memory#1 vs #2): what grew, appeared, disappeared.",
|
|
44
|
+
"container_info": "docker inspect/stats/top/logs, cgroup limits, OOM kills, restarts.",
|
|
45
|
+
"report": "Markdown report of the whole session.",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DEAError(RuntimeError):
|
|
50
|
+
"""Raised when the DEA CLI cannot be executed at all (Node/npx missing, bad JSON)."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DEA:
|
|
54
|
+
"""Client for the DEA CLI.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
command: how to invoke DEA. Defaults to ``["npx", "-y", "dea-agents@latest"]``;
|
|
58
|
+
pass ``["dea"]`` if installed globally. ``DEA_COMMAND`` env var (shell-split) overrides.
|
|
59
|
+
timeout: subprocess timeout in seconds (DEA itself caps tools at 60 s).
|
|
60
|
+
raise_on_error: raise ``DEAError`` when ``ok`` is false instead of returning the result.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, command: Optional[Sequence[str]] = None, timeout: float = 120.0, raise_on_error: bool = False) -> None:
|
|
64
|
+
env_cmd = os.environ.get("DEA_COMMAND")
|
|
65
|
+
if command is None and env_cmd:
|
|
66
|
+
command = env_cmd.split()
|
|
67
|
+
if command is None:
|
|
68
|
+
command = ["dea"] if shutil.which("dea") else ["npx", "-y", "dea-agents@latest"]
|
|
69
|
+
self.command: List[str] = list(command)
|
|
70
|
+
self.timeout = timeout
|
|
71
|
+
self.raise_on_error = raise_on_error
|
|
72
|
+
|
|
73
|
+
# -- core ---------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def call(self, tool: str, **args: Any) -> ToolResult:
|
|
76
|
+
"""Call any DEA tool by name. Returns the ToolResult dict."""
|
|
77
|
+
if tool not in TOOLS:
|
|
78
|
+
raise DEAError(f"unknown tool {tool!r}; known: {', '.join(TOOLS)}")
|
|
79
|
+
argv = [*self.command, "call", tool, json.dumps({k: v for k, v in args.items() if v is not None})]
|
|
80
|
+
try:
|
|
81
|
+
proc = subprocess.run(argv, capture_output=True, text=True, timeout=self.timeout, check=False)
|
|
82
|
+
except FileNotFoundError as e:
|
|
83
|
+
raise DEAError(f"cannot run {argv[0]!r}: install Node.js >= 22 (https://nodejs.org) or set DEA_COMMAND") from e
|
|
84
|
+
except subprocess.TimeoutExpired as e:
|
|
85
|
+
raise DEAError(f"{tool} timed out after {self.timeout}s") from e
|
|
86
|
+
out = proc.stdout.strip()
|
|
87
|
+
start = out.find("{")
|
|
88
|
+
if start < 0:
|
|
89
|
+
raise DEAError(f"DEA produced no JSON for {tool}: {proc.stderr.strip()[:500]}")
|
|
90
|
+
try:
|
|
91
|
+
result: ToolResult = json.loads(out[start:])
|
|
92
|
+
except json.JSONDecodeError as e:
|
|
93
|
+
raise DEAError(f"bad JSON from DEA for {tool}: {e}") from e
|
|
94
|
+
if self.raise_on_error and not result.get("ok", False):
|
|
95
|
+
raise DEAError(result.get("error") or f"{tool} failed")
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
# -- convenience --------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def readiness(self) -> ToolResult:
|
|
101
|
+
return self.call("readiness")
|
|
102
|
+
|
|
103
|
+
def list_processes(self, filter: Optional[str] = None, limit: int = 100) -> ToolResult:
|
|
104
|
+
return self.call("list_processes", filter=filter, limit=limit)
|
|
105
|
+
|
|
106
|
+
def attach(self, target: Any, kind: Optional[str] = None) -> str:
|
|
107
|
+
"""Attach and return the session_id. Raises DEAError if attach failed."""
|
|
108
|
+
r = self.call("attach", target=str(target), kind=kind)
|
|
109
|
+
if not r.get("ok"):
|
|
110
|
+
raise DEAError(r.get("error") or "attach failed")
|
|
111
|
+
return str(r["session_id"])
|
|
112
|
+
|
|
113
|
+
def detach(self, session_id: str) -> ToolResult:
|
|
114
|
+
return self.call("detach", session_id=session_id)
|
|
115
|
+
|
|
116
|
+
def trace_syscalls(self, session_id: str, duration_s: int = 10, categories: Optional[Sequence[str]] = None) -> ToolResult:
|
|
117
|
+
return self.call("trace_syscalls", session_id=session_id, duration_s=duration_s, categories=list(categories) if categories else None)
|
|
118
|
+
|
|
119
|
+
def profile_cpu(self, session_id: str, duration_s: int = 15) -> ToolResult:
|
|
120
|
+
return self.call("profile_cpu", session_id=session_id, duration_s=duration_s)
|
|
121
|
+
|
|
122
|
+
def dump_stacks(self, session_id: str) -> ToolResult:
|
|
123
|
+
return self.call("dump_stacks", session_id=session_id)
|
|
124
|
+
|
|
125
|
+
def inspect_sockets(self, session_id: str) -> ToolResult:
|
|
126
|
+
return self.call("inspect_sockets", session_id=session_id)
|
|
127
|
+
|
|
128
|
+
def inspect_files(self, session_id: str, window_s: int = 3) -> ToolResult:
|
|
129
|
+
return self.call("inspect_files", session_id=session_id, window_s=window_s)
|
|
130
|
+
|
|
131
|
+
def capture_traffic(self, session_id: str, duration_s: int = 10, filter: Optional[str] = None, consent: bool = False) -> ToolResult:
|
|
132
|
+
"""Requires consent=True — ask the user first; captures can contain secrets."""
|
|
133
|
+
return self.call("capture_traffic", session_id=session_id, duration_s=duration_s, filter=filter, consent=consent)
|
|
134
|
+
|
|
135
|
+
def snapshot_memory(self, session_id: str, heap: bool = True, window_s: int = 3) -> ToolResult:
|
|
136
|
+
return self.call("snapshot_memory", session_id=session_id, heap=heap, window_s=window_s)
|
|
137
|
+
|
|
138
|
+
def diff_runs(self, session_id: str, a: str, b: str) -> ToolResult:
|
|
139
|
+
return self.call("diff_runs", session_id=session_id, a=a, b=b)
|
|
140
|
+
|
|
141
|
+
def container_info(self, session_id: str) -> ToolResult:
|
|
142
|
+
return self.call("container_info", session_id=session_id)
|
|
143
|
+
|
|
144
|
+
def report(self, session_id: str, out: Optional[str] = None) -> ToolResult:
|
|
145
|
+
return self.call("report", session_id=session_id, out=out)
|
|
146
|
+
|
|
147
|
+
# -- helpers ------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def findings(result: ToolResult) -> List[str]:
|
|
151
|
+
"""Human-readable finding lines with their evidence ids."""
|
|
152
|
+
return [f"[{f['severity']}] {f['summary']} ({f['confidence']}; {','.join(f['evidence_ids'])})" for f in result.get("findings", [])]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Shared argument schemas for framework adapters (mirrors src/contracts.ts ToolArgs)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
SESSION = {"session_id": {"type": "string", "description": "session id from attach"}}
|
|
7
|
+
|
|
8
|
+
SPECS: Dict[str, Dict[str, Any]] = {
|
|
9
|
+
"readiness": {},
|
|
10
|
+
"attach": {
|
|
11
|
+
"target": {"type": "string", "description": "pid, container name, k8s pod (ns/name) or a command line"},
|
|
12
|
+
"kind": {"type": "string", "enum": ["pid", "container", "pod", "command"], "description": "default: auto-detect"},
|
|
13
|
+
},
|
|
14
|
+
"detach": {**SESSION},
|
|
15
|
+
"list_processes": {"filter": {"type": "string"}, "limit": {"type": "integer", "default": 100}},
|
|
16
|
+
"trace_syscalls": {**SESSION, "duration_s": {"type": "integer", "default": 10, "maximum": 60}, "categories": {"type": "array", "items": {"type": "string", "enum": ["network", "file", "process", "signal", "memory", "all"]}}},
|
|
17
|
+
"profile_cpu": {**SESSION, "duration_s": {"type": "integer", "default": 15, "maximum": 60}},
|
|
18
|
+
"dump_stacks": {**SESSION},
|
|
19
|
+
"inspect_sockets": {**SESSION},
|
|
20
|
+
"inspect_files": {**SESSION, "window_s": {"type": "integer", "default": 3}},
|
|
21
|
+
"capture_traffic": {**SESSION, "duration_s": {"type": "integer", "default": 10, "maximum": 30}, "filter": {"type": "string"}, "consent": {"type": "boolean", "description": "REQUIRED true; ask the user first"}},
|
|
22
|
+
"snapshot_memory": {**SESSION, "heap": {"type": "boolean", "default": True}, "window_s": {"type": "integer", "default": 3}},
|
|
23
|
+
"diff_runs": {**SESSION, "a": {"type": "string", "description": "earlier run id, e.g. snapshot_memory#2"}, "b": {"type": "string", "description": "later run id"}},
|
|
24
|
+
"container_info": {**SESSION},
|
|
25
|
+
"report": {**SESSION, "out": {"type": "string", "description": "optional path to also write the markdown to"}},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
REQUIRED: Dict[str, list] = {
|
|
29
|
+
"attach": ["target"],
|
|
30
|
+
"diff_runs": ["session_id", "a", "b"],
|
|
31
|
+
"capture_traffic": ["session_id", "consent"],
|
|
32
|
+
}
|
|
33
|
+
for _name, _spec in SPECS.items():
|
|
34
|
+
if "session_id" in _spec and _name not in REQUIRED:
|
|
35
|
+
REQUIRED[_name] = ["session_id"]
|
|
36
|
+
REQUIRED.setdefault("readiness", [])
|
|
37
|
+
REQUIRED.setdefault("list_processes", [])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def json_schema(tool: str) -> Dict[str, Any]:
|
|
41
|
+
return {"type": "object", "properties": SPECS[tool], "required": REQUIRED.get(tool, [])}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CrewAI tools for DEA.
|
|
2
|
+
|
|
3
|
+
from dea_agents.crewai import dea_tools
|
|
4
|
+
agent = Agent(role="SRE", goal="diagnose the slow API with evidence", tools=dea_tools(), ...)
|
|
5
|
+
|
|
6
|
+
Requires ``pip install dea-agents[crewai]``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, List, Optional, Type
|
|
12
|
+
|
|
13
|
+
from . import DEA, TOOLS
|
|
14
|
+
from ._specs import SPECS, REQUIRED
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from crewai.tools import BaseTool
|
|
18
|
+
from pydantic import BaseModel, Field, create_model
|
|
19
|
+
except ImportError as e: # pragma: no cover
|
|
20
|
+
raise ImportError("pip install 'dea-agents[crewai]'") from e
|
|
21
|
+
|
|
22
|
+
_PY = {"string": str, "integer": int, "boolean": bool, "array": List[str]}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _model(tool: str) -> Type[BaseModel]:
|
|
26
|
+
fields: dict = {}
|
|
27
|
+
for name, spec in SPECS[tool].items():
|
|
28
|
+
typ = _PY.get(spec.get("type", "string"), str)
|
|
29
|
+
required = name in REQUIRED.get(tool, [])
|
|
30
|
+
fields[name] = (typ if required else Optional[typ], Field(... if required else spec.get("default", None), description=spec.get("description", "")))
|
|
31
|
+
return create_model(f"Dea{tool.title().replace('_', '')}Args", **fields) # type: ignore[call-overload]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def dea_tools(client: Optional[DEA] = None, include: Optional[List[str]] = None) -> List["BaseTool"]:
|
|
35
|
+
dea = client or DEA()
|
|
36
|
+
out: List[BaseTool] = []
|
|
37
|
+
for tool_name, desc in TOOLS.items():
|
|
38
|
+
if include and tool_name not in include:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
class _Tool(BaseTool):
|
|
42
|
+
name: str = f"dea_{tool_name}"
|
|
43
|
+
description: str = desc + " Findings cite evidence ids; never state a cause without one."
|
|
44
|
+
args_schema: Type[BaseModel] = _model(tool_name)
|
|
45
|
+
_dea_tool: str = tool_name
|
|
46
|
+
|
|
47
|
+
def _run(self, **kwargs: Any) -> str:
|
|
48
|
+
return json.dumps(dea.call(self._dea_tool, **kwargs))
|
|
49
|
+
|
|
50
|
+
out.append(_Tool())
|
|
51
|
+
return out
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""LangChain tools for DEA.
|
|
2
|
+
|
|
3
|
+
from dea_agents.langchain import dea_tools
|
|
4
|
+
tools = dea_tools() # 14 StructuredTools, one per DEA tool
|
|
5
|
+
agent = create_react_agent(llm, tools)
|
|
6
|
+
|
|
7
|
+
Each tool returns the ToolResult as a JSON string. Requires ``pip install dea-agents[langchain]``.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, List, Optional, Type
|
|
13
|
+
|
|
14
|
+
from . import DEA, TOOLS
|
|
15
|
+
from ._specs import SPECS, REQUIRED
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from langchain_core.tools import StructuredTool
|
|
19
|
+
from pydantic import BaseModel, Field, create_model
|
|
20
|
+
except ImportError as e: # pragma: no cover
|
|
21
|
+
raise ImportError("pip install 'dea-agents[langchain]'") from e
|
|
22
|
+
|
|
23
|
+
_PY = {"string": str, "integer": int, "boolean": bool, "array": List[str]}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _model(tool: str) -> Type[BaseModel]:
|
|
27
|
+
fields: dict = {}
|
|
28
|
+
for name, spec in SPECS[tool].items():
|
|
29
|
+
typ = _PY.get(spec.get("type", "string"), str)
|
|
30
|
+
required = name in REQUIRED.get(tool, [])
|
|
31
|
+
default = ... if required else spec.get("default", None)
|
|
32
|
+
fields[name] = (typ if required else Optional[typ], Field(default, description=spec.get("description", "")))
|
|
33
|
+
return create_model(f"Dea{tool.title().replace('_', '')}Args", **fields) # type: ignore[call-overload]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def dea_tools(client: Optional[DEA] = None, include: Optional[List[str]] = None) -> List["StructuredTool"]:
|
|
37
|
+
"""Build one LangChain StructuredTool per DEA tool."""
|
|
38
|
+
dea = client or DEA()
|
|
39
|
+
out: List[StructuredTool] = []
|
|
40
|
+
for name, desc in TOOLS.items():
|
|
41
|
+
if include and name not in include:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
def _run(_name: str = name, **kwargs: Any) -> str:
|
|
45
|
+
return json.dumps(dea.call(_name, **kwargs))
|
|
46
|
+
|
|
47
|
+
out.append(StructuredTool.from_function(func=_run, name=f"dea_{name}", description=desc + " Findings cite evidence ids; never state a cause without one.", args_schema=_model(name)))
|
|
48
|
+
return out
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""LlamaIndex FunctionTools for DEA.
|
|
2
|
+
|
|
3
|
+
from dea_agents.llamaindex import dea_tools
|
|
4
|
+
agent = ReActAgent.from_tools(dea_tools(), llm=llm)
|
|
5
|
+
|
|
6
|
+
Requires ``pip install dea-agents[llamaindex]``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
from . import DEA, TOOLS
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from llama_index.core.tools import FunctionTool
|
|
17
|
+
except ImportError as e: # pragma: no cover
|
|
18
|
+
raise ImportError("pip install 'dea-agents[llamaindex]'") from e
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dea_tools(client: Optional[DEA] = None, include: Optional[List[str]] = None) -> List["FunctionTool"]:
|
|
22
|
+
dea = client or DEA()
|
|
23
|
+
out: List[FunctionTool] = []
|
|
24
|
+
for name, desc in TOOLS.items():
|
|
25
|
+
if include and name not in include:
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
def _run(_name: str = name, **kwargs: Any) -> str:
|
|
29
|
+
"""Run a DEA tool. Pass arguments as keyword arguments (session_id, duration_s, ...)."""
|
|
30
|
+
return json.dumps(dea.call(_name, **kwargs))
|
|
31
|
+
|
|
32
|
+
out.append(FunctionTool.from_defaults(fn=_run, name=f"dea_{name}", description=desc + " Findings cite evidence ids."))
|
|
33
|
+
return out
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dea-agents"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Debug Anything from Python agents: observe running processes (strace, py-spy, perf, lsof, tcpdump, gdb/lldb, heap snapshots) and get evidence-backed findings. Wraps the DEA CLI/MCP server; tool adapters for LangChain, LlamaIndex and CrewAI."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Shreyas Sawant" }]
|
|
13
|
+
keywords = ["debugging", "observability", "profiling", "agents", "mcp", "langchain", "llamaindex", "crewai", "strace", "py-spy", "perf"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Topic :: Software Development :: Debuggers",
|
|
18
|
+
"Topic :: System :: Monitoring",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
langchain = ["langchain-core>=0.2", "pydantic>=2"]
|
|
24
|
+
llamaindex = ["llama-index-core>=0.10"]
|
|
25
|
+
crewai = ["crewai>=0.30", "pydantic>=2"]
|
|
26
|
+
dev = ["pytest"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/shreyassaw5394/dea"
|
|
30
|
+
Documentation = "https://github.com/shreyassaw5394/dea/tree/main/docs"
|
|
31
|
+
Issues = "https://github.com/shreyassaw5394/dea/issues"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["dea_agents"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from dea_agents import DEA, DEAError, TOOLS
|
|
9
|
+
from dea_agents._specs import SPECS, REQUIRED, json_schema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_specs_cover_every_tool():
|
|
13
|
+
assert set(SPECS) == set(TOOLS)
|
|
14
|
+
for t in TOOLS:
|
|
15
|
+
assert json_schema(t)["type"] == "object"
|
|
16
|
+
assert REQUIRED["capture_traffic"] == ["session_id", "consent"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_call_builds_json_args_and_parses_result():
|
|
20
|
+
fake = subprocess.CompletedProcess(args=[], returncode=0, stdout=json.dumps({"ok": True, "tool": "readiness", "findings": [], "evidence": []}), stderr="")
|
|
21
|
+
with mock.patch("subprocess.run", return_value=fake) as run:
|
|
22
|
+
r = DEA(command=["dea"]).call("trace_syscalls", session_id="s", duration_s=5, categories=None)
|
|
23
|
+
assert r["ok"] is True
|
|
24
|
+
argv = run.call_args[0][0]
|
|
25
|
+
assert argv[:3] == ["dea", "call", "trace_syscalls"]
|
|
26
|
+
assert json.loads(argv[3]) == {"session_id": "s", "duration_s": 5}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_unknown_tool_rejected():
|
|
30
|
+
with pytest.raises(DEAError):
|
|
31
|
+
DEA(command=["dea"]).call("rm_rf")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_attach_returns_session_id():
|
|
35
|
+
fake = subprocess.CompletedProcess(args=[], returncode=0, stdout=json.dumps({"ok": True, "session_id": "abc"}), stderr="")
|
|
36
|
+
with mock.patch("subprocess.run", return_value=fake):
|
|
37
|
+
assert DEA(command=["dea"]).attach(123) == "abc"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_missing_node_gives_helpful_error():
|
|
41
|
+
with mock.patch("subprocess.run", side_effect=FileNotFoundError()):
|
|
42
|
+
with pytest.raises(DEAError, match="Node.js"):
|
|
43
|
+
DEA(command=["definitely-not-here"]).readiness()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="not supported")
|
|
47
|
+
def test_findings_formatter():
|
|
48
|
+
r = {"findings": [{"severity": "high", "summary": "x", "confidence": "observed", "evidence_ids": ["e1"]}]}
|
|
49
|
+
assert DEA.findings(r) == ["[high] x (observed; e1)"]
|