debug-agent-py 0.2.2__tar.gz → 0.3.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.
Files changed (37) hide show
  1. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/PKG-INFO +96 -5
  2. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/README.md +81 -4
  3. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/pyproject.toml +5 -1
  4. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/__init__.py +6 -0
  5. debug_agent_py-0.3.0/src/debug_agent/inspectors/celery.py +231 -0
  6. debug_agent_py-0.3.0/src/debug_agent/inspectors/django.py +278 -0
  7. debug_agent_py-0.3.0/src/debug_agent/inspectors/flask_ext.py +171 -0
  8. debug_agent_py-0.3.0/src/debug_agent/inspectors/jinja2.py +214 -0
  9. debug_agent_py-0.3.0/src/debug_agent/inspectors/redis.py +285 -0
  10. debug_agent_py-0.3.0/src/debug_agent/inspectors/signals.py +115 -0
  11. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/PKG-INFO +96 -5
  12. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/SOURCES.txt +6 -0
  13. debug_agent_py-0.3.0/src/debug_agent_py.egg-info/requires.txt +36 -0
  14. debug_agent_py-0.2.2/src/debug_agent_py.egg-info/requires.txt +0 -18
  15. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/setup.cfg +0 -0
  16. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/__init__.py +0 -0
  17. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/chat_session.py +0 -0
  18. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/config.py +0 -0
  19. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/context_compressor.py +0 -0
  20. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/engine.py +0 -0
  21. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/async_tasks.py +0 -0
  22. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/database.py +0 -0
  23. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/framework.py +0 -0
  24. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/http_tracker.py +0 -0
  25. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/memory.py +0 -0
  26. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/modules.py +0 -0
  27. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/runtime.py +0 -0
  28. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/system.py +0 -0
  29. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/threads.py +0 -0
  30. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/llm_client.py +0 -0
  31. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/middleware.py +0 -0
  32. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/system_prompt_builder.py +0 -0
  33. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/tool_registry.py +0 -0
  34. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/web/__init__.py +0 -0
  35. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/web/chat_page.py +0 -0
  36. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/dependency_links.txt +0 -0
  37. {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debug-agent-py
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: AI-powered runtime debugging agent for Python web applications
5
5
  Author-email: ggcode <noreply@ggcode.dev>
6
6
  License: MIT
@@ -29,6 +29,20 @@ Provides-Extra: flask
29
29
  Requires-Dist: flask>=2.3; extra == "flask"
30
30
  Provides-Extra: django
31
31
  Requires-Dist: django>=4.2; extra == "django"
32
+ Provides-Extra: sqlalchemy
33
+ Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
34
+ Requires-Dist: flask-sqlalchemy>=3.0; extra == "sqlalchemy"
35
+ Provides-Extra: redis
36
+ Requires-Dist: redis>=5.0; extra == "redis"
37
+ Provides-Extra: celery
38
+ Requires-Dist: celery>=5.3; extra == "celery"
39
+ Provides-Extra: demo
40
+ Requires-Dist: flask>=2.3; extra == "demo"
41
+ Requires-Dist: sqlalchemy>=2.0; extra == "demo"
42
+ Requires-Dist: flask-sqlalchemy>=3.0; extra == "demo"
43
+ Requires-Dist: redis>=5.0; extra == "demo"
44
+ Requires-Dist: celery>=5.3; extra == "demo"
45
+ Requires-Dist: gunicorn>=21.2; extra == "demo"
32
46
  Provides-Extra: dev
33
47
  Requires-Dist: fastapi; extra == "dev"
34
48
  Requires-Dist: uvicorn; extra == "dev"
@@ -37,7 +51,11 @@ Requires-Dist: pytest; extra == "dev"
37
51
 
38
52
  # Python Debug Agent
39
53
 
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, GC, modules, database connections, routes, HTTP requests, and more.
54
+ [![debug-agent-py](https://img.shields.io/pypi/v/debug-agent-py.svg)](https://pypi.org/project/debug-agent-py/)
55
+ ![Tools](https://img.shields.io/badge/tools-51-blue)
56
+ ![Inspectors](https://img.shields.io/badge/inspectors-16-green)
57
+
58
+ 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, GC, modules, database connections, Redis, Django models/URLs, Celery tasks, Flask extensions, Jinja2 templates, signals, routes, HTTP requests, and more — **51 diagnostic tools across 16 inspectors**.
41
59
 
42
60
  ## Quick Start
43
61
 
@@ -81,10 +99,11 @@ http://localhost:8000/agent
81
99
  - **Context compression** — automatically summarizes old conversation when token limit is approached
82
100
  - **Dark-themed chat UI** with full markdown rendering (tables, code blocks, lists)
83
101
  - **Max tool rounds** (25) with forced final summary when limit is reached
84
- - **34 diagnostic tools** across 10 inspectors
102
+ - **51 diagnostic tools** across **16 inspectors**
85
103
  - Works with Flask, FastAPI, and Django
104
+ - Zero external dependencies (no Datadog, no Grafana, no APM)
86
105
 
87
- ## Inspectors & Tools (34)
106
+ ## Inspectors & Tools (51)
88
107
 
89
108
  ### Memory Inspector
90
109
  | Tool | Description |
@@ -101,12 +120,14 @@ http://localhost:8000/agent
101
120
  | `get_thread_info` | List all threads with name, daemon, alive status |
102
121
  | `get_thread_count` | Active thread count |
103
122
  | `get_thread_summary` | Thread state distribution |
123
+ | `get_thread_stacks` | Current frame/stack for all threads |
104
124
 
105
125
  ### Database Inspector
106
126
  | Tool | Description |
107
127
  |------|-------------|
108
128
  | `get_sqlalchemy_engines` | Find SQLAlchemy engines and pool status |
109
129
  | `get_db_connections` | Inspect database connection pools |
130
+ | `get_db_pool_config` | Pool configuration: size, timeout, recycle settings |
110
131
 
111
132
  ### Modules Inspector
112
133
  | Tool | Description |
@@ -120,6 +141,7 @@ http://localhost:8000/agent
120
141
  |------|-------------|
121
142
  | `get_async_tasks` | List pending asyncio tasks |
122
143
  | `get_event_loop_info` | Event loop details: type, running state |
144
+ | `get_pending_callbacks` | List scheduled callbacks on the event loop |
123
145
 
124
146
  ### Runtime Inspector
125
147
  | Tool | Description |
@@ -127,13 +149,14 @@ http://localhost:8000/agent
127
149
  | `get_memory_info` | Process memory info (RSS, VMS, shared) |
128
150
  | `get_cpu_usage` | CPU usage percentage |
129
151
  | `get_python_info` | Python version, implementation, executable path |
152
+ | `get_open_fds` | Open file descriptor count and limits |
130
153
 
131
154
  ### System Inspector
132
155
  | Tool | Description |
133
156
  |------|-------------|
134
157
  | `get_system_info` | Hostname, platform, CPU cores, disk |
135
158
  | `get_environment_variables` | Environment variables (masked secrets) |
136
- | `get_disk_usage` | Disk usage for working directory |
159
+ | `get_disk_usage` | Disk usage for the working directory |
137
160
 
138
161
  ### Framework Inspector
139
162
  | Tool | Description |
@@ -149,6 +172,54 @@ http://localhost:8000/agent
149
172
  | `get_error_requests` | Error requests (4xx/5xx) |
150
173
  | `get_request_stats` | P50/P95/P99 latency, error rate |
151
174
 
175
+ ### Redis Inspector
176
+ | Tool | Description |
177
+ |------|-------------|
178
+ | `get_redis_info` | Redis server info: memory, clients, persistence |
179
+ | `get_redis_keys` | Scan Redis keyspace with pattern matching |
180
+ | `get_redis_config` | Redis runtime configuration (CONFIG GET) |
181
+ | `get_redis_slowlog` | Redis slow query log entries |
182
+
183
+ ### Django Inspector
184
+ | Tool | Description |
185
+ |------|-------------|
186
+ | `get_django_models` | List Django models with app label, table name, field count |
187
+ | `get_django_urls` | List all URL patterns with view names and namespaces |
188
+ | `get_django_settings` | Key Django settings (DBs, INSTALLED_APPS, MIDDLEWARE) |
189
+ | `get_django_migrations` | Migration status per app: applied vs pending |
190
+
191
+ ### Celery Inspector
192
+ | Tool | Description |
193
+ |------|-------------|
194
+ | `get_celery_tasks` | List registered Celery tasks with routing info |
195
+ | `get_celery_workers` | Active Celery workers with pool and concurrency |
196
+ | `get_celery_queues` | Queue depth and message stats per queue |
197
+
198
+ ### Flask Extensions Inspector
199
+ | Tool | Description |
200
+ |------|-------------|
201
+ | `get_flask_extensions` | List registered Flask extensions and their bindings |
202
+ | `get_flask_blueprints` | List Flask blueprints with URL prefixes and routes |
203
+ | `get_flask_config` | Flask configuration object values (secrets masked) |
204
+
205
+ ### Jinja2 Inspector
206
+ | Tool | Description |
207
+ |------|-------------|
208
+ | `get_jinja_templates` | List loaded Jinja2 templates with loader paths |
209
+ | `get_jinja_filters` | List registered Jinja2 filters, tests, and globals |
210
+
211
+ ### Signals Inspector
212
+ | Tool | Description |
213
+ |------|-------------|
214
+ | `get_signal_handlers` | List Python signal handlers registered via signal module |
215
+ | `get_django_signals` | List Django signal receivers connected to senders |
216
+
217
+ ### WSGI/ASGI Inspector
218
+ | Tool | Description |
219
+ |------|-------------|
220
+ | `get_wsgi_info` | WSGI server details (Gunicorn/uWSGI workers, config) |
221
+ | `get_asgi_apps` | List ASGI application scope and middleware chain |
222
+
152
223
  ## Custom Tools
153
224
 
154
225
  ```python
@@ -171,6 +242,26 @@ def check_redis():
171
242
 
172
243
  ## Run the Demo
173
244
 
245
+ The demo uses **Flask** + **redis-py** + **SQLAlchemy** + **Celery**. Start Redis with Docker Compose first:
246
+
247
+ ### Docker Compose
248
+
249
+ ```yaml
250
+ # docker-compose.yml
251
+ services:
252
+ redis:
253
+ image: redis:7-alpine
254
+ ports:
255
+ - "6379:6379"
256
+ command: redis-server --save 60 1 --loglevel warning
257
+ ```
258
+
259
+ ```bash
260
+ docker compose up -d
261
+ ```
262
+
263
+ ### Start the app
264
+
174
265
  ```bash
175
266
  export LLM_API_KEY=your-key
176
267
  cd demo && python app.py
@@ -1,6 +1,10 @@
1
1
  # Python Debug Agent
2
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, GC, modules, database connections, routes, HTTP requests, and more.
3
+ [![debug-agent-py](https://img.shields.io/pypi/v/debug-agent-py.svg)](https://pypi.org/project/debug-agent-py/)
4
+ ![Tools](https://img.shields.io/badge/tools-51-blue)
5
+ ![Inspectors](https://img.shields.io/badge/inspectors-16-green)
6
+
7
+ 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, GC, modules, database connections, Redis, Django models/URLs, Celery tasks, Flask extensions, Jinja2 templates, signals, routes, HTTP requests, and more — **51 diagnostic tools across 16 inspectors**.
4
8
 
5
9
  ## Quick Start
6
10
 
@@ -44,10 +48,11 @@ http://localhost:8000/agent
44
48
  - **Context compression** — automatically summarizes old conversation when token limit is approached
45
49
  - **Dark-themed chat UI** with full markdown rendering (tables, code blocks, lists)
46
50
  - **Max tool rounds** (25) with forced final summary when limit is reached
47
- - **34 diagnostic tools** across 10 inspectors
51
+ - **51 diagnostic tools** across **16 inspectors**
48
52
  - Works with Flask, FastAPI, and Django
53
+ - Zero external dependencies (no Datadog, no Grafana, no APM)
49
54
 
50
- ## Inspectors & Tools (34)
55
+ ## Inspectors & Tools (51)
51
56
 
52
57
  ### Memory Inspector
53
58
  | Tool | Description |
@@ -64,12 +69,14 @@ http://localhost:8000/agent
64
69
  | `get_thread_info` | List all threads with name, daemon, alive status |
65
70
  | `get_thread_count` | Active thread count |
66
71
  | `get_thread_summary` | Thread state distribution |
72
+ | `get_thread_stacks` | Current frame/stack for all threads |
67
73
 
68
74
  ### Database Inspector
69
75
  | Tool | Description |
70
76
  |------|-------------|
71
77
  | `get_sqlalchemy_engines` | Find SQLAlchemy engines and pool status |
72
78
  | `get_db_connections` | Inspect database connection pools |
79
+ | `get_db_pool_config` | Pool configuration: size, timeout, recycle settings |
73
80
 
74
81
  ### Modules Inspector
75
82
  | Tool | Description |
@@ -83,6 +90,7 @@ http://localhost:8000/agent
83
90
  |------|-------------|
84
91
  | `get_async_tasks` | List pending asyncio tasks |
85
92
  | `get_event_loop_info` | Event loop details: type, running state |
93
+ | `get_pending_callbacks` | List scheduled callbacks on the event loop |
86
94
 
87
95
  ### Runtime Inspector
88
96
  | Tool | Description |
@@ -90,13 +98,14 @@ http://localhost:8000/agent
90
98
  | `get_memory_info` | Process memory info (RSS, VMS, shared) |
91
99
  | `get_cpu_usage` | CPU usage percentage |
92
100
  | `get_python_info` | Python version, implementation, executable path |
101
+ | `get_open_fds` | Open file descriptor count and limits |
93
102
 
94
103
  ### System Inspector
95
104
  | Tool | Description |
96
105
  |------|-------------|
97
106
  | `get_system_info` | Hostname, platform, CPU cores, disk |
98
107
  | `get_environment_variables` | Environment variables (masked secrets) |
99
- | `get_disk_usage` | Disk usage for working directory |
108
+ | `get_disk_usage` | Disk usage for the working directory |
100
109
 
101
110
  ### Framework Inspector
102
111
  | Tool | Description |
@@ -112,6 +121,54 @@ http://localhost:8000/agent
112
121
  | `get_error_requests` | Error requests (4xx/5xx) |
113
122
  | `get_request_stats` | P50/P95/P99 latency, error rate |
114
123
 
124
+ ### Redis Inspector
125
+ | Tool | Description |
126
+ |------|-------------|
127
+ | `get_redis_info` | Redis server info: memory, clients, persistence |
128
+ | `get_redis_keys` | Scan Redis keyspace with pattern matching |
129
+ | `get_redis_config` | Redis runtime configuration (CONFIG GET) |
130
+ | `get_redis_slowlog` | Redis slow query log entries |
131
+
132
+ ### Django Inspector
133
+ | Tool | Description |
134
+ |------|-------------|
135
+ | `get_django_models` | List Django models with app label, table name, field count |
136
+ | `get_django_urls` | List all URL patterns with view names and namespaces |
137
+ | `get_django_settings` | Key Django settings (DBs, INSTALLED_APPS, MIDDLEWARE) |
138
+ | `get_django_migrations` | Migration status per app: applied vs pending |
139
+
140
+ ### Celery Inspector
141
+ | Tool | Description |
142
+ |------|-------------|
143
+ | `get_celery_tasks` | List registered Celery tasks with routing info |
144
+ | `get_celery_workers` | Active Celery workers with pool and concurrency |
145
+ | `get_celery_queues` | Queue depth and message stats per queue |
146
+
147
+ ### Flask Extensions Inspector
148
+ | Tool | Description |
149
+ |------|-------------|
150
+ | `get_flask_extensions` | List registered Flask extensions and their bindings |
151
+ | `get_flask_blueprints` | List Flask blueprints with URL prefixes and routes |
152
+ | `get_flask_config` | Flask configuration object values (secrets masked) |
153
+
154
+ ### Jinja2 Inspector
155
+ | Tool | Description |
156
+ |------|-------------|
157
+ | `get_jinja_templates` | List loaded Jinja2 templates with loader paths |
158
+ | `get_jinja_filters` | List registered Jinja2 filters, tests, and globals |
159
+
160
+ ### Signals Inspector
161
+ | Tool | Description |
162
+ |------|-------------|
163
+ | `get_signal_handlers` | List Python signal handlers registered via signal module |
164
+ | `get_django_signals` | List Django signal receivers connected to senders |
165
+
166
+ ### WSGI/ASGI Inspector
167
+ | Tool | Description |
168
+ |------|-------------|
169
+ | `get_wsgi_info` | WSGI server details (Gunicorn/uWSGI workers, config) |
170
+ | `get_asgi_apps` | List ASGI application scope and middleware chain |
171
+
115
172
  ## Custom Tools
116
173
 
117
174
  ```python
@@ -134,6 +191,26 @@ def check_redis():
134
191
 
135
192
  ## Run the Demo
136
193
 
194
+ The demo uses **Flask** + **redis-py** + **SQLAlchemy** + **Celery**. Start Redis with Docker Compose first:
195
+
196
+ ### Docker Compose
197
+
198
+ ```yaml
199
+ # docker-compose.yml
200
+ services:
201
+ redis:
202
+ image: redis:7-alpine
203
+ ports:
204
+ - "6379:6379"
205
+ command: redis-server --save 60 1 --loglevel warning
206
+ ```
207
+
208
+ ```bash
209
+ docker compose up -d
210
+ ```
211
+
212
+ ### Start the app
213
+
137
214
  ```bash
138
215
  export LLM_API_KEY=your-key
139
216
  cd demo && python app.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "debug-agent-py"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "AI-powered runtime debugging agent for Python web applications"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -33,6 +33,10 @@ dependencies = [
33
33
  fastapi = ["fastapi>=0.100", "starlette>=0.27", "uvicorn>=0.23"]
34
34
  flask = ["flask>=2.3"]
35
35
  django = ["django>=4.2"]
36
+ sqlalchemy = ["sqlalchemy>=2.0", "flask-sqlalchemy>=3.0"]
37
+ redis = ["redis>=5.0"]
38
+ celery = ["celery>=5.3"]
39
+ demo = ["flask>=2.3", "sqlalchemy>=2.0", "flask-sqlalchemy>=3.0", "redis>=5.0", "celery>=5.3", "gunicorn>=21.2"]
36
40
  dev = ["fastapi", "uvicorn", "flask", "pytest"]
37
41
 
38
42
  [project.urls]
@@ -2,12 +2,18 @@
2
2
 
3
3
  from debug_agent.inspectors import ( # noqa: F401
4
4
  async_tasks,
5
+ celery,
5
6
  database,
7
+ django,
8
+ flask_ext,
6
9
  framework,
7
10
  http_tracker,
11
+ jinja2,
8
12
  memory,
9
13
  modules,
14
+ redis,
10
15
  runtime,
16
+ signals,
11
17
  system,
12
18
  threads,
13
19
  )
@@ -0,0 +1,231 @@
1
+ """Celery inspector: registered tasks, queue stats, and worker stats.
2
+
3
+ Register a Celery app at runtime so the inspectors can reach it:
4
+
5
+ from debug_agent.inspectors.celery import register_celery_app
6
+ register_celery_app("default", celery_app)
7
+
8
+ All tools degrade gracefully when Celery is not installed or no app is
9
+ registered. Inspector calls (queue/worker stats) require a running broker.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from debug_agent.tool_registry import debug_tool, ToolParam
17
+
18
+
19
+ # ─── Registration mechanism ──────────────────────────────────────────────────
20
+
21
+ _registered_celery_apps: dict[str, Any] = {}
22
+
23
+
24
+ def register_celery_app(name: str, app: Any) -> None:
25
+ """Register a Celery app under a name so the inspectors can find it."""
26
+ _registered_celery_apps[name] = app
27
+
28
+
29
+ def _celery_available() -> bool:
30
+ try:
31
+ import celery # noqa: F401
32
+ return True
33
+ except ImportError:
34
+ return False
35
+
36
+
37
+ def _ensure_apps() -> dict[str, Any] | None:
38
+ if not _celery_available():
39
+ return None
40
+ return _registered_celery_apps
41
+
42
+
43
+ # ─── Tools ───────────────────────────────────────────────────────────────────
44
+
45
+
46
+ @debug_tool(
47
+ "get_celery_tasks",
48
+ "List registered Celery tasks (name, module, bound) for each registered Celery app",
49
+ )
50
+ def get_celery_tasks() -> dict:
51
+ apps = _ensure_apps()
52
+ if apps is None:
53
+ return {"error": "Celery is not installed"}
54
+
55
+ if not apps:
56
+ return {
57
+ "message": "No Celery apps registered. Call register_celery_app(name, app) to register one.",
58
+ "registered_count": 0,
59
+ }
60
+
61
+ result = []
62
+ for name, app in apps.items():
63
+ tasks: list[dict[str, Any]] = []
64
+ try:
65
+ task_iter = app.tasks.items()
66
+ except Exception as exc:
67
+ result.append({"name": name, "error": f"Could not enumerate tasks: {exc}"})
68
+ continue
69
+
70
+ for task_name, task in task_iter:
71
+ # Skip Celery's own internal tasks (celery.*) unless the user cares.
72
+ if task_name.startswith("celery."):
73
+ continue
74
+ tasks.append({
75
+ "name": task_name,
76
+ "module": getattr(getattr(task, "run", None), "__module__", None)
77
+ or getattr(task, "__module__", None),
78
+ "bound": bool(getattr(task, "bound", False)),
79
+ })
80
+
81
+ result.append({
82
+ "name": name,
83
+ "task_count": len(tasks),
84
+ "tasks": tasks,
85
+ })
86
+
87
+ return {"registered_count": len(apps), "apps": result}
88
+
89
+
90
+ @debug_tool(
91
+ "get_celery_queues",
92
+ "Get Celery queue stats (active, reserved, scheduled counts per queue) via the inspector",
93
+ )
94
+ def get_celery_queues() -> dict:
95
+ apps = _ensure_apps()
96
+ if apps is None:
97
+ return {"error": "Celery is not installed"}
98
+
99
+ if not apps:
100
+ return {
101
+ "message": "No Celery apps registered. Call register_celery_app(name, app) to register one.",
102
+ "registered_count": 0,
103
+ }
104
+
105
+ result = []
106
+ for name, app in apps.items():
107
+ try:
108
+ inspect = app.control.inspect()
109
+ except Exception as exc:
110
+ result.append({"name": name, "error": f"Could not create inspector: {exc}"})
111
+ continue
112
+
113
+ active = _safe_inspect(inspect.active)
114
+ reserved = _safe_inspect(inspect.reserved)
115
+ scheduled = _safe_inspect(inspect.scheduled)
116
+
117
+ per_queue: dict[str, dict[str, int]] = {}
118
+ for payload_name, payload in (
119
+ ("active", active),
120
+ ("reserved", reserved),
121
+ ("scheduled", scheduled),
122
+ ):
123
+ if not isinstance(payload, dict):
124
+ continue
125
+ for _worker, entries in payload.items():
126
+ for entry in entries or []:
127
+ delivery = entry.get("delivery") if isinstance(entry, dict) else None
128
+ queue = None
129
+ if isinstance(delivery, dict):
130
+ queue = delivery.get("routing_key") or delivery.get("queue")
131
+ queue = queue or "default"
132
+ bucket = per_queue.setdefault(queue, {"active": 0, "reserved": 0, "scheduled": 0})
133
+ bucket[payload_name] += 1
134
+
135
+ totals = {
136
+ "active": sum(v["active"] for v in per_queue.values()),
137
+ "reserved": sum(v["reserved"] for v in per_queue.values()),
138
+ "scheduled": sum(v["scheduled"] for v in per_queue.values()),
139
+ }
140
+
141
+ result.append({
142
+ "name": name,
143
+ "queue_count": len(per_queue),
144
+ "totals": totals,
145
+ "queues": per_queue,
146
+ })
147
+
148
+ return {"registered_count": len(apps), "apps": result}
149
+
150
+
151
+ def _safe_inspect(func) -> Any:
152
+ """Call a celery inspect method, returning an error dict on failure."""
153
+ try:
154
+ return func()
155
+ except Exception as exc:
156
+ return {"_error": str(exc)}
157
+
158
+
159
+ @debug_tool(
160
+ "get_celery_workers",
161
+ "Get Celery worker stats (active, processed, pool info) via the inspector",
162
+ )
163
+ def get_celery_workers() -> dict:
164
+ apps = _ensure_apps()
165
+ if apps is None:
166
+ return {"error": "Celery is not installed"}
167
+
168
+ if not apps:
169
+ return {
170
+ "message": "No Celery apps registered. Call register_celery_app(name, app) to register one.",
171
+ "registered_count": 0,
172
+ }
173
+
174
+ result = []
175
+ for name, app in apps.items():
176
+ try:
177
+ inspect = app.control.inspect()
178
+ except Exception as exc:
179
+ result.append({"name": name, "error": f"Could not create inspector: {exc}"})
180
+ continue
181
+
182
+ stats = _safe_inspect(inspect.stats)
183
+ active = _safe_inspect(inspect.active)
184
+ ping = _safe_inspect(inspect.ping)
185
+
186
+ workers: list[dict[str, Any]] = []
187
+ if isinstance(stats, dict):
188
+ for worker_name, worker_stats in stats.items():
189
+ pool = worker_stats.get("pool", {}) if isinstance(worker_stats, dict) else {}
190
+ broker = worker_stats.get("broker", {}) if isinstance(worker_stats, dict) else {}
191
+ rusage = worker_stats.get("rusage", {}) if isinstance(worker_stats, dict) else {}
192
+ active_list = active.get(worker_name, []) if isinstance(active, dict) else []
193
+ total_processed = 0
194
+ if isinstance(worker_stats, dict):
195
+ total_processed = sum(
196
+ int(v) for v in (worker_stats.get("total", {}) or {}).values()
197
+ )
198
+ workers.append({
199
+ "worker": worker_name,
200
+ "active_count": len(active_list) if isinstance(active_list, list) else 0,
201
+ "processed_total": total_processed,
202
+ "pool": {
203
+ "implementation": pool.get("implementation") if isinstance(pool, dict) else None,
204
+ "max_concurrency": pool.get("max-concurrency") if isinstance(pool, dict) else None,
205
+ "processes": pool.get("processes") if isinstance(pool, dict) else None,
206
+ },
207
+ "broker": {
208
+ "transport": broker.get("transport") if isinstance(broker, dict) else None,
209
+ "name": broker.get("name") if isinstance(broker, dict) else None,
210
+ },
211
+ "uptime": worker_stats.get("uptime") if isinstance(worker_stats, dict) else None,
212
+ "rusage": _summarise_rusage(rusage),
213
+ })
214
+
215
+ reachable = list(ping.keys()) if isinstance(ping, dict) else []
216
+
217
+ result.append({
218
+ "name": name,
219
+ "worker_count": len(workers),
220
+ "reachable_workers": reachable,
221
+ "workers": workers,
222
+ })
223
+
224
+ return {"registered_count": len(apps), "apps": result}
225
+
226
+
227
+ def _summarise_rusage(rusage: Any) -> dict[str, Any]:
228
+ if not isinstance(rusage, dict):
229
+ return {}
230
+ keys = ("utime", "stime", "maxrss", "idrss", "isrss", "ixrss", "minflt", "majflt", "nvcsw", "nivcsw")
231
+ return {k: rusage.get(k) for k in keys if k in rusage}