debug-agent-py 0.2.1__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.
- debug_agent/__init__.py +23 -0
- debug_agent/chat_session.py +45 -0
- debug_agent/config.py +48 -0
- debug_agent/context_compressor.py +157 -0
- debug_agent/engine.py +180 -0
- debug_agent/inspectors/__init__.py +13 -0
- debug_agent/inspectors/async_tasks.py +108 -0
- debug_agent/inspectors/database.py +129 -0
- debug_agent/inspectors/framework.py +133 -0
- debug_agent/inspectors/http_tracker.py +93 -0
- debug_agent/inspectors/memory.py +117 -0
- debug_agent/inspectors/modules.py +129 -0
- debug_agent/inspectors/runtime.py +132 -0
- debug_agent/inspectors/system.py +79 -0
- debug_agent/inspectors/threads.py +97 -0
- debug_agent/llm_client.py +195 -0
- debug_agent/middleware.py +199 -0
- debug_agent/system_prompt_builder.py +101 -0
- debug_agent/tool_registry.py +121 -0
- debug_agent/web/__init__.py +0 -0
- debug_agent/web/chat_page.py +582 -0
- debug_agent_py-0.2.1.dist-info/METADATA +160 -0
- debug_agent_py-0.2.1.dist-info/RECORD +25 -0
- debug_agent_py-0.2.1.dist-info/WHEEL +5 -0
- debug_agent_py-0.2.1.dist-info/top_level.txt +1 -0
debug_agent/__init__.py
ADDED
|
@@ -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()
|
debug_agent/config.py
ADDED
|
@@ -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
|
debug_agent/engine.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Debug engine — the reasoning loop that connects LLM to tools.
|
|
2
|
+
|
|
3
|
+
Spring-aligned: dynamic system prompt, ChatSession, ContextCompressor, real streaming.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from debug_agent.chat_session import ChatSession
|
|
13
|
+
from debug_agent.config import AgentConfig
|
|
14
|
+
from debug_agent.context_compressor import ContextCompressor
|
|
15
|
+
from debug_agent.llm_client import LLMClient, StreamHandler
|
|
16
|
+
from debug_agent.system_prompt_builder import SystemPromptBuilder
|
|
17
|
+
from debug_agent.tool_registry import registry
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("debug_agent")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ChatCallback:
|
|
23
|
+
"""Engine to UI streaming callback."""
|
|
24
|
+
|
|
25
|
+
def on_content(self, chunk: str): ...
|
|
26
|
+
def on_tool_start(self, tool_name: str, args: str): ...
|
|
27
|
+
def on_tool_result(self, tool_name: str, result: str): ...
|
|
28
|
+
def on_complete(self): ...
|
|
29
|
+
def on_error(self, message: str): ...
|
|
30
|
+
def on_context_compressed(self, original_tokens: int, compressed_tokens: int, removed_rounds: int): ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _EngineStreamHandler(StreamHandler):
|
|
34
|
+
"""StreamHandler implementation used internally by the engine."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, cb: ChatCallback):
|
|
37
|
+
self._cb = cb
|
|
38
|
+
self.tool_calls: list[dict] = []
|
|
39
|
+
self.usage: dict | None = None
|
|
40
|
+
self.had_error = False
|
|
41
|
+
self.content = ""
|
|
42
|
+
|
|
43
|
+
def on_content(self, chunk: str):
|
|
44
|
+
self.content += chunk
|
|
45
|
+
self._cb.on_content(chunk)
|
|
46
|
+
|
|
47
|
+
def on_complete(self, tool_calls: list[dict], finish_reason: str | None, usage: dict | None):
|
|
48
|
+
self.tool_calls = tool_calls
|
|
49
|
+
self.usage = usage
|
|
50
|
+
|
|
51
|
+
def on_error(self, error: Exception):
|
|
52
|
+
self.had_error = True
|
|
53
|
+
self._cb.on_error(f"LLM API error: {error}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DebugEngine:
|
|
57
|
+
"""Orchestrates the LLM Tool reasoning loop."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, config: AgentConfig):
|
|
60
|
+
self.config = config
|
|
61
|
+
self.llm = LLMClient(config.llm)
|
|
62
|
+
self.tools = registry
|
|
63
|
+
self._sessions: dict[str, ChatSession] = {}
|
|
64
|
+
|
|
65
|
+
self.prompt_builder = SystemPromptBuilder(registry)
|
|
66
|
+
self.system_prompt = self.prompt_builder.build()
|
|
67
|
+
self.context_compressor = ContextCompressor(
|
|
68
|
+
self.llm, config.llm.model, config.llm.temperature, config.llm.context_window_tokens,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def chat(self, message: str, session_id: str = "default", callback: ChatCallback | None = None):
|
|
72
|
+
"""Process a user message with streaming via callback."""
|
|
73
|
+
if callback is None:
|
|
74
|
+
callback = ChatCallback()
|
|
75
|
+
|
|
76
|
+
session = self._get_or_create_session(session_id)
|
|
77
|
+
session.add_message({"role": "user", "content": message})
|
|
78
|
+
self._run_tool_loop(session, callback)
|
|
79
|
+
|
|
80
|
+
def clear_session(self, session_id: str = "default"):
|
|
81
|
+
session = self._sessions.get(session_id)
|
|
82
|
+
if session:
|
|
83
|
+
session.clear()
|
|
84
|
+
|
|
85
|
+
def _get_or_create_session(self, session_id: str) -> ChatSession:
|
|
86
|
+
if session_id not in self._sessions:
|
|
87
|
+
self._sessions[session_id] = ChatSession(session_id)
|
|
88
|
+
return self._sessions[session_id]
|
|
89
|
+
|
|
90
|
+
# ==================== Core Tool-Calling Loop ====================
|
|
91
|
+
|
|
92
|
+
def _run_tool_loop(self, session: ChatSession, cb: ChatCallback):
|
|
93
|
+
max_rounds = self.config.llm.max_tool_rounds
|
|
94
|
+
|
|
95
|
+
for round_num in range(max_rounds):
|
|
96
|
+
# Context compression check
|
|
97
|
+
if round_num > 0 and self.context_compressor.needs_compression(session.get_current_context_tokens()):
|
|
98
|
+
result = self.context_compressor.compress(session)
|
|
99
|
+
if result:
|
|
100
|
+
cb.on_content(
|
|
101
|
+
f"\n\n> [Context auto-compressed: {result.original_tokens}"
|
|
102
|
+
f" -> ~{result.compressed_tokens} tokens ({result.strategy})]\n\n"
|
|
103
|
+
)
|
|
104
|
+
cb.on_context_compressed(result.original_tokens, result.compressed_tokens, result.removed_rounds)
|
|
105
|
+
|
|
106
|
+
messages = [{"role": "system", "content": self.system_prompt}] + session.messages
|
|
107
|
+
tool_schemas = self.tools.all_schemas()
|
|
108
|
+
|
|
109
|
+
handler = _EngineStreamHandler(cb)
|
|
110
|
+
self.llm.chat_stream_raw(messages, tool_schemas, "auto", handler)
|
|
111
|
+
|
|
112
|
+
if handler.had_error:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if handler.usage:
|
|
116
|
+
session.record_token_usage(handler.usage)
|
|
117
|
+
|
|
118
|
+
if not handler.tool_calls:
|
|
119
|
+
# If empty content after tool calls, prompt LLM to summarize
|
|
120
|
+
if not handler.content.strip() and round_num > 0:
|
|
121
|
+
session.add_message({
|
|
122
|
+
"role": "user",
|
|
123
|
+
"content": (
|
|
124
|
+
"Based on all the diagnostic data you've gathered from the tools above, "
|
|
125
|
+
"please provide a comprehensive analysis of the findings and "
|
|
126
|
+
"actionable recommendations."
|
|
127
|
+
),
|
|
128
|
+
})
|
|
129
|
+
messages = [{"role": "system", "content": self.system_prompt}] + session.messages
|
|
130
|
+
summarize_handler = _EngineStreamHandler(cb)
|
|
131
|
+
self.llm.chat_stream_raw(messages, [], "none", summarize_handler)
|
|
132
|
+
session.add_message({"role": "assistant", "content": summarize_handler.content})
|
|
133
|
+
else:
|
|
134
|
+
session.add_message({"role": "assistant", "content": handler.content})
|
|
135
|
+
cb.on_complete()
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Execute tool calls
|
|
139
|
+
session.add_message({
|
|
140
|
+
"role": "assistant",
|
|
141
|
+
"content": handler.content,
|
|
142
|
+
"tool_calls": handler.tool_calls,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
for tc in handler.tool_calls:
|
|
146
|
+
tool_name = tc["function"]["name"]
|
|
147
|
+
try:
|
|
148
|
+
args = json.loads(tc["function"]["arguments"] or "{}")
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
args = {}
|
|
151
|
+
|
|
152
|
+
cb.on_tool_start(tool_name, tc["function"]["arguments"])
|
|
153
|
+
|
|
154
|
+
result = self.tools.execute(tool_name, args)
|
|
155
|
+
result_str = json.dumps(result, default=str, ensure_ascii=False)
|
|
156
|
+
if len(result_str) > 12000:
|
|
157
|
+
result_str = result_str[:12000]
|
|
158
|
+
|
|
159
|
+
cb.on_tool_result(tool_name, result_str)
|
|
160
|
+
session.add_message({
|
|
161
|
+
"role": "tool",
|
|
162
|
+
"tool_call_id": tc["id"],
|
|
163
|
+
"content": result_str,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
# Max rounds - force final summary
|
|
167
|
+
final_messages = [{"role": "system", "content": self.system_prompt}] + session.messages
|
|
168
|
+
final_messages.append({
|
|
169
|
+
"role": "system",
|
|
170
|
+
"content": (
|
|
171
|
+
"You have reached the maximum number of tool-calling rounds. "
|
|
172
|
+
"Based on all the diagnostic data you have gathered so far, "
|
|
173
|
+
"provide a comprehensive analysis and actionable recommendations NOW. "
|
|
174
|
+
"Do not attempt to call more tools."
|
|
175
|
+
),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
handler = _EngineStreamHandler(cb)
|
|
179
|
+
self.llm.chat_stream_raw(final_messages, [], "none", handler)
|
|
180
|
+
cb.on_complete()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Async task inspector: pending asyncio tasks and event loop details."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from debug_agent.tool_registry import debug_tool, ToolParam
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_running_loop() -> asyncio.AbstractEventLoop | None:
|
|
13
|
+
"""Try to get the currently running event loop."""
|
|
14
|
+
try:
|
|
15
|
+
return asyncio.get_running_loop()
|
|
16
|
+
except RuntimeError:
|
|
17
|
+
# No running loop in this thread
|
|
18
|
+
pass
|
|
19
|
+
try:
|
|
20
|
+
return asyncio.get_event_loop()
|
|
21
|
+
except RuntimeError:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@debug_tool(
|
|
26
|
+
"get_async_tasks",
|
|
27
|
+
"List pending asyncio tasks if running in async mode",
|
|
28
|
+
)
|
|
29
|
+
def get_async_tasks() -> dict:
|
|
30
|
+
loop = _get_running_loop()
|
|
31
|
+
if loop is None:
|
|
32
|
+
return {
|
|
33
|
+
"message": "No running asyncio event loop found. The app may be using synchronous mode.",
|
|
34
|
+
"has_running_loop": False,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
all_tasks = asyncio.all_tasks(loop=loop)
|
|
39
|
+
except RuntimeError:
|
|
40
|
+
return {"error": "Could not enumerate asyncio tasks"}
|
|
41
|
+
|
|
42
|
+
tasks_info = []
|
|
43
|
+
for task in all_tasks:
|
|
44
|
+
coro = task.get_coro() if hasattr(task, "get_coro") else None
|
|
45
|
+
tasks_info.append({
|
|
46
|
+
"name": task.get_name() if hasattr(task, "get_name") else str(task),
|
|
47
|
+
"done": task.done(),
|
|
48
|
+
"cancelled": task.cancelled(),
|
|
49
|
+
"coroutine": str(coro) if coro else None,
|
|
50
|
+
"stack_repr": repr(task),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"has_running_loop": True,
|
|
55
|
+
"total_tasks": len(all_tasks),
|
|
56
|
+
"pending_tasks": sum(1 for t in all_tasks if not t.done()),
|
|
57
|
+
"done_tasks": sum(1 for t in all_tasks if t.done()),
|
|
58
|
+
"tasks": tasks_info[:50],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@debug_tool(
|
|
63
|
+
"get_event_loop_info",
|
|
64
|
+
"Get asyncio event loop details: type, running state, debug mode",
|
|
65
|
+
)
|
|
66
|
+
def get_event_loop_info() -> dict:
|
|
67
|
+
loop = _get_running_loop()
|
|
68
|
+
if loop is None:
|
|
69
|
+
return {
|
|
70
|
+
"message": "No running asyncio event loop found",
|
|
71
|
+
"has_running_loop": False,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
info = {
|
|
75
|
+
"has_running_loop": True,
|
|
76
|
+
"loop_type": type(loop).__name__,
|
|
77
|
+
"is_running": loop.is_running(),
|
|
78
|
+
"is_closed": loop.is_closed(),
|
|
79
|
+
"debug_mode": loop.get_debug(),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Try to get additional details
|
|
83
|
+
try:
|
|
84
|
+
info["time"] = loop.time()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Async generator count
|
|
89
|
+
try:
|
|
90
|
+
info["async_generator_count"] = len(loop._asyncgens) if hasattr(loop, "_asyncgens") else 0
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# Check for uvloop
|
|
95
|
+
try:
|
|
96
|
+
import uvloop
|
|
97
|
+
info["using_uvloop"] = isinstance(loop, uvloop.Loop)
|
|
98
|
+
except ImportError:
|
|
99
|
+
info["using_uvloop"] = False
|
|
100
|
+
|
|
101
|
+
# Task count
|
|
102
|
+
try:
|
|
103
|
+
all_tasks = asyncio.all_tasks(loop=loop)
|
|
104
|
+
info["active_task_count"] = len(all_tasks)
|
|
105
|
+
except RuntimeError:
|
|
106
|
+
info["active_task_count"] = 0
|
|
107
|
+
|
|
108
|
+
return info
|