debug-agent-py 0.2.1__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.
- debug_agent_py-0.2.1/PKG-INFO +160 -0
- debug_agent_py-0.2.1/README.md +123 -0
- debug_agent_py-0.2.1/pyproject.toml +44 -0
- debug_agent_py-0.2.1/setup.cfg +4 -0
- debug_agent_py-0.2.1/src/debug_agent/__init__.py +23 -0
- debug_agent_py-0.2.1/src/debug_agent/chat_session.py +45 -0
- debug_agent_py-0.2.1/src/debug_agent/config.py +48 -0
- debug_agent_py-0.2.1/src/debug_agent/context_compressor.py +157 -0
- debug_agent_py-0.2.1/src/debug_agent/engine.py +180 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/__init__.py +13 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/async_tasks.py +108 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/database.py +129 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/framework.py +133 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/http_tracker.py +93 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/memory.py +117 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/modules.py +129 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/runtime.py +132 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/system.py +79 -0
- debug_agent_py-0.2.1/src/debug_agent/inspectors/threads.py +97 -0
- debug_agent_py-0.2.1/src/debug_agent/llm_client.py +195 -0
- debug_agent_py-0.2.1/src/debug_agent/middleware.py +199 -0
- debug_agent_py-0.2.1/src/debug_agent/system_prompt_builder.py +101 -0
- debug_agent_py-0.2.1/src/debug_agent/tool_registry.py +121 -0
- debug_agent_py-0.2.1/src/debug_agent/web/__init__.py +0 -0
- debug_agent_py-0.2.1/src/debug_agent/web/chat_page.py +582 -0
- debug_agent_py-0.2.1/src/debug_agent_py.egg-info/PKG-INFO +160 -0
- debug_agent_py-0.2.1/src/debug_agent_py.egg-info/SOURCES.txt +28 -0
- debug_agent_py-0.2.1/src/debug_agent_py.egg-info/dependency_links.txt +1 -0
- debug_agent_py-0.2.1/src/debug_agent_py.egg-info/requires.txt +18 -0
- debug_agent_py-0.2.1/src/debug_agent_py.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debug-agent-py
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: AI-powered runtime debugging agent for Python web applications
|
|
5
|
+
Author-email: ggcode <noreply@ggcode.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/topcheer/python-debug-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/topcheer/python-debug-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/topcheer/python-debug-agent/issues
|
|
10
|
+
Keywords: debug,debugging,ai,llm,runtime,diagnostics,flask,fastapi,django,agent,observability
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Provides-Extra: fastapi
|
|
25
|
+
Requires-Dist: fastapi>=0.100; extra == "fastapi"
|
|
26
|
+
Requires-Dist: starlette>=0.27; extra == "fastapi"
|
|
27
|
+
Requires-Dist: uvicorn>=0.23; extra == "fastapi"
|
|
28
|
+
Provides-Extra: flask
|
|
29
|
+
Requires-Dist: flask>=2.3; extra == "flask"
|
|
30
|
+
Provides-Extra: django
|
|
31
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: fastapi; extra == "dev"
|
|
34
|
+
Requires-Dist: uvicorn; extra == "dev"
|
|
35
|
+
Requires-Dist: flask; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest; extra == "dev"
|
|
37
|
+
|
|
38
|
+
# Python Debug Agent
|
|
39
|
+
|
|
40
|
+
An AI-powered runtime debugging agent that embeds directly into your Python web application. Add one dependency, configure an LLM key, and chat with your live app at `/agent` to inspect memory, threads, routes, HTTP requests, GC stats, and more.
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install debug-agent[fastapi]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Integrate (FastAPI)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fastapi import FastAPI
|
|
54
|
+
from debug_agent.middleware import create_fastapi_router
|
|
55
|
+
|
|
56
|
+
app = FastAPI()
|
|
57
|
+
|
|
58
|
+
# One line to integrate
|
|
59
|
+
app.include_router(create_fastapi_router())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Configure LLM
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export LLM_API_KEY=your-key
|
|
66
|
+
export LLM_BASE_URL=https://api.openai.com/v1 # optional
|
|
67
|
+
export LLM_MODEL=gpt-4o # optional
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Run and open
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
http://localhost:8000/agent
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Framework Integrations
|
|
77
|
+
|
|
78
|
+
### FastAPI / Starlette
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from debug_agent.middleware import create_fastapi_router
|
|
82
|
+
app.include_router(create_fastapi_router())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Flask
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from flask import Flask
|
|
89
|
+
from debug_agent.middleware import create_flask_blueprint
|
|
90
|
+
app = Flask(__name__)
|
|
91
|
+
app.register_blueprint(create_flask_blueprint())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Any ASGI App (Starlette Mount)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from starlette.routing import Mount
|
|
98
|
+
from debug_agent.middleware import create_starlette_app
|
|
99
|
+
routes = [Mount("/agent", app=create_starlette_app())]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Built-in Tools (18+)
|
|
103
|
+
|
|
104
|
+
| Tool | Description |
|
|
105
|
+
|------|-------------|
|
|
106
|
+
| `get_gc_stats` | GC collection counts per generation |
|
|
107
|
+
| `get_memory_summary` | RSS, object counts, top types |
|
|
108
|
+
| `trigger_gc` | Force GC and show before/after |
|
|
109
|
+
| `get_thread_summary` | Thread count, names, daemon status |
|
|
110
|
+
| `get_thread_dump` | Stack traces for all threads |
|
|
111
|
+
| `get_runtime_info` | Python version, platform, PID |
|
|
112
|
+
| `get_memory_allocations` | tracemalloc top allocations |
|
|
113
|
+
| `get_routes` | List all web routes/endpoints |
|
|
114
|
+
| `get_middleware` | List registered middleware |
|
|
115
|
+
| `get_installed_packages` | Installed pip packages |
|
|
116
|
+
| `get_environment_variables` | Environment variables (masked secrets) |
|
|
117
|
+
| `get_recent_requests` | HTTP request ring buffer |
|
|
118
|
+
| `get_slow_requests` | Slowest requests sorted by duration |
|
|
119
|
+
| `get_error_requests` | Error requests (4xx/5xx) |
|
|
120
|
+
| `get_request_stats` | P50/P95/P99 latency, error rate |
|
|
121
|
+
| `get_process_info` | PID, CPU time, container detection |
|
|
122
|
+
| `get_system_info` | OS, CPU cores, load average |
|
|
123
|
+
| `get_disk_usage` | Disk usage for working directory |
|
|
124
|
+
|
|
125
|
+
## Custom Tools
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from debug_agent import debug_tool, ToolParam
|
|
129
|
+
|
|
130
|
+
@debug_tool("check_db_pool", "Check database connection pool stats")
|
|
131
|
+
def check_db_pool() -> dict:
|
|
132
|
+
return {"active": 5, "idle": 10, "max": 20}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That's it. The tool is auto-discovered and made available to the LLM.
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
| Env Var | Default | Description |
|
|
140
|
+
|---------|---------|-------------|
|
|
141
|
+
| `DEBUG_AGENT_ENABLED` | `true` | Enable/disable |
|
|
142
|
+
| `DEBUG_AGENT_BASE_PATH` | `/agent` | URL path |
|
|
143
|
+
| `LLM_BASE_URL` | `https://api.openai.com/v1` | LLM endpoint |
|
|
144
|
+
| `LLM_API_KEY` | (required) | API key |
|
|
145
|
+
| `LLM_MODEL` | `gpt-4o` | Model name |
|
|
146
|
+
| `LLM_TEMPERATURE` | `0.3` | Sampling temp |
|
|
147
|
+
| `LLM_MAX_TOOL_ROUNDS` | `10` | Max tool rounds |
|
|
148
|
+
|
|
149
|
+
## Run the Demo
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
export LLM_API_KEY=your-key
|
|
154
|
+
python demo/app.py
|
|
155
|
+
# Open http://localhost:8000/agent
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Python Debug Agent
|
|
2
|
+
|
|
3
|
+
An AI-powered runtime debugging agent that embeds directly into your Python web application. Add one dependency, configure an LLM key, and chat with your live app at `/agent` to inspect memory, threads, routes, HTTP requests, GC stats, and more.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install debug-agent[fastapi]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Integrate (FastAPI)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fastapi import FastAPI
|
|
17
|
+
from debug_agent.middleware import create_fastapi_router
|
|
18
|
+
|
|
19
|
+
app = FastAPI()
|
|
20
|
+
|
|
21
|
+
# One line to integrate
|
|
22
|
+
app.include_router(create_fastapi_router())
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 3. Configure LLM
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export LLM_API_KEY=your-key
|
|
29
|
+
export LLM_BASE_URL=https://api.openai.com/v1 # optional
|
|
30
|
+
export LLM_MODEL=gpt-4o # optional
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 4. Run and open
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
http://localhost:8000/agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Framework Integrations
|
|
40
|
+
|
|
41
|
+
### FastAPI / Starlette
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from debug_agent.middleware import create_fastapi_router
|
|
45
|
+
app.include_router(create_fastapi_router())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Flask
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from flask import Flask
|
|
52
|
+
from debug_agent.middleware import create_flask_blueprint
|
|
53
|
+
app = Flask(__name__)
|
|
54
|
+
app.register_blueprint(create_flask_blueprint())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Any ASGI App (Starlette Mount)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from starlette.routing import Mount
|
|
61
|
+
from debug_agent.middleware import create_starlette_app
|
|
62
|
+
routes = [Mount("/agent", app=create_starlette_app())]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Built-in Tools (18+)
|
|
66
|
+
|
|
67
|
+
| Tool | Description |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `get_gc_stats` | GC collection counts per generation |
|
|
70
|
+
| `get_memory_summary` | RSS, object counts, top types |
|
|
71
|
+
| `trigger_gc` | Force GC and show before/after |
|
|
72
|
+
| `get_thread_summary` | Thread count, names, daemon status |
|
|
73
|
+
| `get_thread_dump` | Stack traces for all threads |
|
|
74
|
+
| `get_runtime_info` | Python version, platform, PID |
|
|
75
|
+
| `get_memory_allocations` | tracemalloc top allocations |
|
|
76
|
+
| `get_routes` | List all web routes/endpoints |
|
|
77
|
+
| `get_middleware` | List registered middleware |
|
|
78
|
+
| `get_installed_packages` | Installed pip packages |
|
|
79
|
+
| `get_environment_variables` | Environment variables (masked secrets) |
|
|
80
|
+
| `get_recent_requests` | HTTP request ring buffer |
|
|
81
|
+
| `get_slow_requests` | Slowest requests sorted by duration |
|
|
82
|
+
| `get_error_requests` | Error requests (4xx/5xx) |
|
|
83
|
+
| `get_request_stats` | P50/P95/P99 latency, error rate |
|
|
84
|
+
| `get_process_info` | PID, CPU time, container detection |
|
|
85
|
+
| `get_system_info` | OS, CPU cores, load average |
|
|
86
|
+
| `get_disk_usage` | Disk usage for working directory |
|
|
87
|
+
|
|
88
|
+
## Custom Tools
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from debug_agent import debug_tool, ToolParam
|
|
92
|
+
|
|
93
|
+
@debug_tool("check_db_pool", "Check database connection pool stats")
|
|
94
|
+
def check_db_pool() -> dict:
|
|
95
|
+
return {"active": 5, "idle": 10, "max": 20}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
That's it. The tool is auto-discovered and made available to the LLM.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Env Var | Default | Description |
|
|
103
|
+
|---------|---------|-------------|
|
|
104
|
+
| `DEBUG_AGENT_ENABLED` | `true` | Enable/disable |
|
|
105
|
+
| `DEBUG_AGENT_BASE_PATH` | `/agent` | URL path |
|
|
106
|
+
| `LLM_BASE_URL` | `https://api.openai.com/v1` | LLM endpoint |
|
|
107
|
+
| `LLM_API_KEY` | (required) | API key |
|
|
108
|
+
| `LLM_MODEL` | `gpt-4o` | Model name |
|
|
109
|
+
| `LLM_TEMPERATURE` | `0.3` | Sampling temp |
|
|
110
|
+
| `LLM_MAX_TOOL_ROUNDS` | `10` | Max tool rounds |
|
|
111
|
+
|
|
112
|
+
## Run the Demo
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install -e ".[dev]"
|
|
116
|
+
export LLM_API_KEY=your-key
|
|
117
|
+
python demo/app.py
|
|
118
|
+
# Open http://localhost:8000/agent
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "debug-agent-py"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "AI-powered runtime debugging agent for Python web applications"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "ggcode", email = "noreply@ggcode.dev"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["debug", "debugging", "ai", "llm", "runtime", "diagnostics", "flask", "fastapi", "django", "agent", "observability"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Debuggers",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
fastapi = ["fastapi>=0.100", "starlette>=0.27", "uvicorn>=0.23"]
|
|
34
|
+
flask = ["flask>=2.3"]
|
|
35
|
+
django = ["django>=4.2"]
|
|
36
|
+
dev = ["fastapi", "uvicorn", "flask", "pytest"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/topcheer/python-debug-agent"
|
|
40
|
+
Repository = "https://github.com/topcheer/python-debug-agent"
|
|
41
|
+
Issues = "https://github.com/topcheer/python-debug-agent/issues"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Debug Agent — AI-powered runtime debugging for Python applications."""
|
|
2
|
+
|
|
3
|
+
from debug_agent.config import AgentConfig
|
|
4
|
+
from debug_agent.tool_registry import debug_tool, ToolParam, registry
|
|
5
|
+
from debug_agent.engine import DebugEngine, ChatCallback
|
|
6
|
+
from debug_agent.chat_session import ChatSession
|
|
7
|
+
from debug_agent.system_prompt_builder import SystemPromptBuilder
|
|
8
|
+
from debug_agent.context_compressor import ContextCompressor, CompressionResult
|
|
9
|
+
from debug_agent.llm_client import LLMClient, StreamHandler
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AgentConfig", "debug_tool", "ToolParam", "registry", "DebugEngine", "ChatCallback",
|
|
14
|
+
"ChatSession", "SystemPromptBuilder", "ContextCompressor", "CompressionResult",
|
|
15
|
+
"LLMClient", "StreamHandler", "setup",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup(config: AgentConfig | None = None) -> DebugEngine:
|
|
20
|
+
"""Initialize the debug agent and return the engine instance."""
|
|
21
|
+
from debug_agent import inspectors # noqa: F401 — triggers registration
|
|
22
|
+
cfg = config or AgentConfig.from_env()
|
|
23
|
+
return DebugEngine(cfg)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Chat session management with token tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChatSession:
|
|
10
|
+
"""Manages conversation history and cumulative token usage."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, session_id: str):
|
|
13
|
+
self.session_id = session_id
|
|
14
|
+
self.created_at = time.time()
|
|
15
|
+
self.messages: list[dict[str, Any]] = []
|
|
16
|
+
self.last_active_at = self.created_at
|
|
17
|
+
|
|
18
|
+
self.last_token_usage: dict | None = None
|
|
19
|
+
self.cumulative_prompt_tokens: int = 0
|
|
20
|
+
self.cumulative_completion_tokens: int = 0
|
|
21
|
+
|
|
22
|
+
def add_message(self, message: dict[str, Any]):
|
|
23
|
+
self.messages.append(message)
|
|
24
|
+
self.last_active_at = time.time()
|
|
25
|
+
|
|
26
|
+
def replace_messages(self, new_messages: list[dict[str, Any]]):
|
|
27
|
+
self.messages = new_messages
|
|
28
|
+
self.last_active_at = time.time()
|
|
29
|
+
|
|
30
|
+
def record_token_usage(self, usage: dict | None):
|
|
31
|
+
if not usage:
|
|
32
|
+
return
|
|
33
|
+
self.last_token_usage = usage
|
|
34
|
+
self.cumulative_prompt_tokens = usage.get("prompt_tokens", 0)
|
|
35
|
+
self.cumulative_completion_tokens += usage.get("completion_tokens", 0)
|
|
36
|
+
|
|
37
|
+
def get_current_context_tokens(self) -> int:
|
|
38
|
+
return self.cumulative_prompt_tokens
|
|
39
|
+
|
|
40
|
+
def clear(self):
|
|
41
|
+
self.messages = []
|
|
42
|
+
self.last_token_usage = None
|
|
43
|
+
self.cumulative_prompt_tokens = 0
|
|
44
|
+
self.cumulative_completion_tokens = 0
|
|
45
|
+
self.last_active_at = time.time()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Configuration for Debug Agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class LLMConfig:
|
|
11
|
+
base_url: str = "https://open.bigmodel.cn/api/coding/paas/v4"
|
|
12
|
+
api_key: str = ""
|
|
13
|
+
model: str = "glm-5.2"
|
|
14
|
+
temperature: float = 0.3
|
|
15
|
+
max_tokens: int = 4096
|
|
16
|
+
max_tool_rounds: int = 25
|
|
17
|
+
timeout_seconds: int = 120
|
|
18
|
+
max_retries: int = 3
|
|
19
|
+
retry_base_delay_ms: int = 1000
|
|
20
|
+
retry_max_delay_ms: int = 30000
|
|
21
|
+
context_window_tokens: int = 100000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AgentConfig:
|
|
26
|
+
enabled: bool = True
|
|
27
|
+
base_path: str = "/agent"
|
|
28
|
+
llm: LLMConfig = field(default_factory=LLMConfig)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_env(cls) -> AgentConfig:
|
|
32
|
+
return cls(
|
|
33
|
+
enabled=os.getenv("DEBUG_AGENT_ENABLED", "true").lower() == "true",
|
|
34
|
+
base_path=os.getenv("DEBUG_AGENT_BASE_PATH", "/agent"),
|
|
35
|
+
llm=LLMConfig(
|
|
36
|
+
base_url=os.getenv("LLM_BASE_URL", "https://open.bigmodel.cn/api/coding/paas/v4"),
|
|
37
|
+
api_key=os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY", ""),
|
|
38
|
+
model=os.getenv("LLM_MODEL", "glm-5.2"),
|
|
39
|
+
temperature=float(os.getenv("LLM_TEMPERATURE", "0.3")),
|
|
40
|
+
max_tokens=int(os.getenv("LLM_MAX_TOKENS", "4096")),
|
|
41
|
+
max_tool_rounds=int(os.getenv("LLM_MAX_TOOL_ROUNDS", "25")),
|
|
42
|
+
timeout_seconds=int(os.getenv("LLM_TIMEOUT_SECONDS", "120")),
|
|
43
|
+
max_retries=int(os.getenv("LLM_MAX_RETRIES", "3")),
|
|
44
|
+
retry_base_delay_ms=int(os.getenv("LLM_RETRY_BASE_DELAY_MS", "1000")),
|
|
45
|
+
retry_max_delay_ms=int(os.getenv("LLM_RETRY_MAX_DELAY_MS", "30000")),
|
|
46
|
+
context_window_tokens=int(os.getenv("LLM_CONTEXT_WINDOW_TOKENS", "100000")),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Context compressor — summarizes older conversation rounds via LLM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from debug_agent.chat_session import ChatSession
|
|
10
|
+
from debug_agent.llm_client import LLMClient
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("debug_agent")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CompressionResult:
|
|
16
|
+
def __init__(self, original_tokens: int, compressed_tokens: int, removed_rounds: int, strategy: str):
|
|
17
|
+
self.original_tokens = original_tokens
|
|
18
|
+
self.compressed_tokens = compressed_tokens
|
|
19
|
+
self.removed_rounds = removed_rounds
|
|
20
|
+
self.strategy = strategy
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ContextCompressor:
|
|
24
|
+
def __init__(self, llm: LLMClient, model: str, temperature: float, max_context_tokens: int, recent_rounds_to_keep: int = 3):
|
|
25
|
+
self.llm = llm
|
|
26
|
+
self.model = model
|
|
27
|
+
self.temperature = temperature
|
|
28
|
+
self.max_context_tokens = max_context_tokens
|
|
29
|
+
self.recent_rounds_to_keep = recent_rounds_to_keep
|
|
30
|
+
|
|
31
|
+
def needs_compression(self, current_tokens: int) -> bool:
|
|
32
|
+
return current_tokens > self.max_context_tokens * 0.75
|
|
33
|
+
|
|
34
|
+
def compress(self, session: ChatSession) -> CompressionResult | None:
|
|
35
|
+
original_tokens = session.get_current_context_tokens()
|
|
36
|
+
if not self.needs_compression(original_tokens):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
rounds = self._identify_rounds(session.messages)
|
|
40
|
+
|
|
41
|
+
keep_count = min(self.recent_rounds_to_keep, len(rounds) - 1)
|
|
42
|
+
if keep_count < 1:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
summarize_count = len(rounds) - keep_count
|
|
46
|
+
|
|
47
|
+
to_summarize = []
|
|
48
|
+
for i in range(summarize_count):
|
|
49
|
+
to_summarize.extend(rounds[i])
|
|
50
|
+
|
|
51
|
+
to_keep = []
|
|
52
|
+
for i in range(summarize_count, len(rounds)):
|
|
53
|
+
to_keep.extend(rounds[i])
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
summary = self._summarize_with_llm(to_summarize)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.warning(f"LLM summarization failed: {e}")
|
|
59
|
+
summary = self._fallback_truncate(to_summarize)
|
|
60
|
+
|
|
61
|
+
compressed = [
|
|
62
|
+
{"role": "system", "content": f"[Previous conversation summary — {summarize_count} rounds compressed]\n\n{summary}"},
|
|
63
|
+
*to_keep,
|
|
64
|
+
]
|
|
65
|
+
compressed_tokens = self._estimate_tokens(compressed)
|
|
66
|
+
session.replace_messages(compressed)
|
|
67
|
+
|
|
68
|
+
return CompressionResult(original_tokens, compressed_tokens, summarize_count, f"LLM summarized {summarize_count} rounds")
|
|
69
|
+
|
|
70
|
+
def _summarize_with_llm(self, old_messages: list[dict]) -> str:
|
|
71
|
+
conversation_text = ""
|
|
72
|
+
for msg in old_messages:
|
|
73
|
+
role = msg.get("role", "")
|
|
74
|
+
content = msg.get("content", "")
|
|
75
|
+
if role == "user":
|
|
76
|
+
conversation_text += f"[User] {content}\n\n"
|
|
77
|
+
elif role == "assistant":
|
|
78
|
+
if content:
|
|
79
|
+
conversation_text += f"[Assistant] {content}\n\n"
|
|
80
|
+
for tc in msg.get("tool_calls", []):
|
|
81
|
+
fn = tc.get("function", {})
|
|
82
|
+
conversation_text += f"[Tool Call] {fn.get('name', '')}({fn.get('arguments', '')})\n\n"
|
|
83
|
+
elif role == "tool":
|
|
84
|
+
if len(content) > 2000:
|
|
85
|
+
content = content[:2000] + "...[truncated]"
|
|
86
|
+
conversation_text += f"[Tool Result] {content}\n\n"
|
|
87
|
+
|
|
88
|
+
prompt = """You are a conversation summarizer for a Python debugging assistant.
|
|
89
|
+
Summarize the KEY diagnostic findings from the conversation below concisely.
|
|
90
|
+
|
|
91
|
+
Focus on preserving:
|
|
92
|
+
- Problems investigated and their root causes (if found)
|
|
93
|
+
- Key tool results: actual numbers, statuses, error messages, configuration values
|
|
94
|
+
- Recommendations or fixes already suggested
|
|
95
|
+
- Any unresolved issues or follow-up actions pending
|
|
96
|
+
|
|
97
|
+
Rules:
|
|
98
|
+
- Be concise but preserve ALL important data points
|
|
99
|
+
- Use bullet points
|
|
100
|
+
- Do NOT include full JSON dumps
|
|
101
|
+
- Keep it under 600 words"""
|
|
102
|
+
|
|
103
|
+
response = self.llm.chat(
|
|
104
|
+
[
|
|
105
|
+
{"role": "system", "content": prompt},
|
|
106
|
+
{"role": "user", "content": f"Conversation to summarize:\n\n{conversation_text}"},
|
|
107
|
+
],
|
|
108
|
+
tools=None,
|
|
109
|
+
)
|
|
110
|
+
return response["choices"][0]["message"]["content"]
|
|
111
|
+
|
|
112
|
+
def _fallback_truncate(self, messages: list[dict]) -> str:
|
|
113
|
+
sb = "Previous conversation summary (fallback):\n\n"
|
|
114
|
+
for msg in messages:
|
|
115
|
+
if msg.get("role") == "user" and msg.get("content"):
|
|
116
|
+
q = msg["content"][:100] + "..." if len(msg["content"]) > 100 else msg["content"]
|
|
117
|
+
sb += f"- User asked: {q}\n"
|
|
118
|
+
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
119
|
+
for tc in msg["tool_calls"]:
|
|
120
|
+
sb += f"- Called tool: {tc.get('function', {}).get('name', '')}\n"
|
|
121
|
+
return sb
|
|
122
|
+
|
|
123
|
+
def _identify_rounds(self, messages: list[dict]) -> list[list[dict]]:
|
|
124
|
+
rounds = []
|
|
125
|
+
current = []
|
|
126
|
+
has_assistant = False
|
|
127
|
+
|
|
128
|
+
for msg in messages:
|
|
129
|
+
role = msg.get("role", "")
|
|
130
|
+
if role == "user":
|
|
131
|
+
if current:
|
|
132
|
+
rounds.append(current)
|
|
133
|
+
current = []
|
|
134
|
+
has_assistant = False
|
|
135
|
+
current.append(msg)
|
|
136
|
+
elif role == "assistant":
|
|
137
|
+
if has_assistant:
|
|
138
|
+
rounds.append(current)
|
|
139
|
+
current = []
|
|
140
|
+
has_assistant = False
|
|
141
|
+
current.append(msg)
|
|
142
|
+
has_assistant = True
|
|
143
|
+
else:
|
|
144
|
+
current.append(msg)
|
|
145
|
+
|
|
146
|
+
if current:
|
|
147
|
+
rounds.append(current)
|
|
148
|
+
return rounds
|
|
149
|
+
|
|
150
|
+
def _estimate_tokens(self, messages: list[dict]) -> int:
|
|
151
|
+
chars = 0
|
|
152
|
+
for msg in messages:
|
|
153
|
+
chars += len(msg.get("content", "") or "")
|
|
154
|
+
for tc in msg.get("tool_calls", []):
|
|
155
|
+
fn = tc.get("function", {})
|
|
156
|
+
chars += len(fn.get("name", "")) + len(fn.get("arguments", ""))
|
|
157
|
+
return chars // 4
|