connectonion 0.5.8__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.
- connectonion/__init__.py +78 -0
- connectonion/address.py +320 -0
- connectonion/agent.py +450 -0
- connectonion/announce.py +84 -0
- connectonion/asgi.py +287 -0
- connectonion/auto_debug_exception.py +181 -0
- connectonion/cli/__init__.py +3 -0
- connectonion/cli/browser_agent/__init__.py +5 -0
- connectonion/cli/browser_agent/browser.py +243 -0
- connectonion/cli/browser_agent/prompt.md +107 -0
- connectonion/cli/commands/__init__.py +1 -0
- connectonion/cli/commands/auth_commands.py +527 -0
- connectonion/cli/commands/browser_commands.py +27 -0
- connectonion/cli/commands/create.py +511 -0
- connectonion/cli/commands/deploy_commands.py +220 -0
- connectonion/cli/commands/doctor_commands.py +173 -0
- connectonion/cli/commands/init.py +469 -0
- connectonion/cli/commands/project_cmd_lib.py +828 -0
- connectonion/cli/commands/reset_commands.py +149 -0
- connectonion/cli/commands/status_commands.py +168 -0
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
- connectonion/cli/docs/connectonion.md +1256 -0
- connectonion/cli/docs.md +123 -0
- connectonion/cli/main.py +148 -0
- connectonion/cli/templates/meta-agent/README.md +287 -0
- connectonion/cli/templates/meta-agent/agent.py +196 -0
- connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
- connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
- connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
- connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
- connectonion/cli/templates/minimal/README.md +56 -0
- connectonion/cli/templates/minimal/agent.py +40 -0
- connectonion/cli/templates/playwright/README.md +118 -0
- connectonion/cli/templates/playwright/agent.py +336 -0
- connectonion/cli/templates/playwright/prompt.md +102 -0
- connectonion/cli/templates/playwright/requirements.txt +3 -0
- connectonion/cli/templates/web-research/agent.py +122 -0
- connectonion/connect.py +128 -0
- connectonion/console.py +539 -0
- connectonion/debug_agent/__init__.py +13 -0
- connectonion/debug_agent/agent.py +45 -0
- connectonion/debug_agent/prompts/debug_assistant.md +72 -0
- connectonion/debug_agent/runtime_inspector.py +406 -0
- connectonion/debug_explainer/__init__.py +10 -0
- connectonion/debug_explainer/explain_agent.py +114 -0
- connectonion/debug_explainer/explain_context.py +263 -0
- connectonion/debug_explainer/explainer_prompt.md +29 -0
- connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
- connectonion/debugger_ui.py +1039 -0
- connectonion/decorators.py +208 -0
- connectonion/events.py +248 -0
- connectonion/execution_analyzer/__init__.py +9 -0
- connectonion/execution_analyzer/execution_analysis.py +93 -0
- connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
- connectonion/host.py +579 -0
- connectonion/interactive_debugger.py +342 -0
- connectonion/llm.py +801 -0
- connectonion/llm_do.py +307 -0
- connectonion/logger.py +300 -0
- connectonion/prompt_files/__init__.py +1 -0
- connectonion/prompt_files/analyze_contact.md +62 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/prompt_files/react_evaluate.md +11 -0
- connectonion/prompt_files/react_plan.md +16 -0
- connectonion/prompt_files/reflect.md +22 -0
- connectonion/prompts.py +144 -0
- connectonion/relay.py +200 -0
- connectonion/static/docs.html +688 -0
- connectonion/tool_executor.py +279 -0
- connectonion/tool_factory.py +186 -0
- connectonion/tool_registry.py +105 -0
- connectonion/trust.py +166 -0
- connectonion/trust_agents.py +71 -0
- connectonion/trust_functions.py +88 -0
- connectonion/tui/__init__.py +57 -0
- connectonion/tui/divider.py +39 -0
- connectonion/tui/dropdown.py +251 -0
- connectonion/tui/footer.py +31 -0
- connectonion/tui/fuzzy.py +56 -0
- connectonion/tui/input.py +278 -0
- connectonion/tui/keys.py +35 -0
- connectonion/tui/pick.py +130 -0
- connectonion/tui/providers.py +155 -0
- connectonion/tui/status_bar.py +163 -0
- connectonion/usage.py +161 -0
- connectonion/useful_events_handlers/__init__.py +16 -0
- connectonion/useful_events_handlers/reflect.py +116 -0
- connectonion/useful_plugins/__init__.py +20 -0
- connectonion/useful_plugins/calendar_plugin.py +163 -0
- connectonion/useful_plugins/eval.py +139 -0
- connectonion/useful_plugins/gmail_plugin.py +162 -0
- connectonion/useful_plugins/image_result_formatter.py +127 -0
- connectonion/useful_plugins/re_act.py +78 -0
- connectonion/useful_plugins/shell_approval.py +159 -0
- connectonion/useful_tools/__init__.py +44 -0
- connectonion/useful_tools/diff_writer.py +192 -0
- connectonion/useful_tools/get_emails.py +183 -0
- connectonion/useful_tools/gmail.py +1596 -0
- connectonion/useful_tools/google_calendar.py +613 -0
- connectonion/useful_tools/memory.py +380 -0
- connectonion/useful_tools/microsoft_calendar.py +604 -0
- connectonion/useful_tools/outlook.py +488 -0
- connectonion/useful_tools/send_email.py +205 -0
- connectonion/useful_tools/shell.py +97 -0
- connectonion/useful_tools/slash_command.py +201 -0
- connectonion/useful_tools/terminal.py +285 -0
- connectonion/useful_tools/todo_list.py +241 -0
- connectonion/useful_tools/web_fetch.py +216 -0
- connectonion/xray.py +467 -0
- connectonion-0.5.8.dist-info/METADATA +741 -0
- connectonion-0.5.8.dist-info/RECORD +113 -0
- connectonion-0.5.8.dist-info/WHEEL +4 -0
- connectonion-0.5.8.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Provide runtime inspection tools for AI-powered exception debugging with live frame access
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [pathlib, typing, re] | imported by [debug_agent/agent.py, debug_agent/__init__.py, auto_debug_exception.py] | tested by [tests/test_runtime_inspector.py]
|
|
5
|
+
Data flow: auto_debug_exception() creates RuntimeInspector(frame, traceback) → stores frame.f_globals + frame.f_locals in self.namespace → AI agent calls methods: execute_in_frame(code) uses eval/exec, inspect_object(var) shows type/attrs/methods, validate_assumption(statement) tests hypothesis, test_fix(code) validates solutions, explore_namespace() lists all variables → returns formatted strings → AI interprets for debugging
|
|
6
|
+
State/Effects: stores frozen exception frame and namespace | execute_in_frame() can modify namespace via exec | no file I/O or external side effects | evaluates arbitrary Python code (security: only in debug context)
|
|
7
|
+
Integration: exposes RuntimeInspector class with methods: execute_in_frame(code), inspect_object(variable_name), validate_assumption(statement), test_fix(fix_code), try_alternative(code), explore_namespace(), get_traceback() | used as class-based tool (Agent auto-extracts methods via tool_factory.extract_methods_from_instance)
|
|
8
|
+
Performance: eval/exec are fast | namespace is dict copy (O(n) initial cost) | inspection uses dir() and getattr() | traceback formatting uses traceback module
|
|
9
|
+
Errors: execute_in_frame() catches exceptions and returns error strings (doesn't raise) | missing variables return "not found" messages | no frame returns "No runtime context available"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional, List, Dict
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RuntimeInspector:
|
|
18
|
+
"""Inspector that operates on a frozen exception runtime state.
|
|
19
|
+
|
|
20
|
+
Pass an instance of this class directly to the Agent as a tool.
|
|
21
|
+
ConnectOnion will automatically discover all public methods!
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, frame=None, exception_traceback=None):
|
|
25
|
+
"""Initialize with optional exception frame and traceback.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
frame: The frame object where the exception occurred
|
|
29
|
+
exception_traceback: The traceback object from the exception
|
|
30
|
+
"""
|
|
31
|
+
self.frame = frame
|
|
32
|
+
self.exception_traceback = exception_traceback
|
|
33
|
+
self.namespace = {}
|
|
34
|
+
|
|
35
|
+
if frame:
|
|
36
|
+
# Combine locals and globals for execution context
|
|
37
|
+
self.namespace.update(frame.f_globals)
|
|
38
|
+
self.namespace.update(frame.f_locals)
|
|
39
|
+
|
|
40
|
+
def set_context(self, frame, exception_traceback):
|
|
41
|
+
"""Update the runtime context (called by auto_debug).
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
frame: The frame object where the exception occurred
|
|
45
|
+
exception_traceback: The traceback object from the exception
|
|
46
|
+
"""
|
|
47
|
+
self.frame = frame
|
|
48
|
+
self.exception_traceback = exception_traceback
|
|
49
|
+
self.namespace = {}
|
|
50
|
+
self.namespace.update(frame.f_globals)
|
|
51
|
+
self.namespace.update(frame.f_locals)
|
|
52
|
+
|
|
53
|
+
def execute_in_frame(self, code: str) -> str:
|
|
54
|
+
"""Execute Python code in the exception frame context.
|
|
55
|
+
|
|
56
|
+
Access all variables, call functions, and test hypotheses with real data.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
code: Python code to execute in the exception frame
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The result of execution or error message
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
execute_in_frame("type(profile)")
|
|
66
|
+
execute_in_frame("list(data.keys())")
|
|
67
|
+
execute_in_frame("profile.get('name', 'default')")
|
|
68
|
+
"""
|
|
69
|
+
if not self.frame:
|
|
70
|
+
return "No runtime context available"
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
result = eval(code, self.namespace)
|
|
74
|
+
return self._format_result(result)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
# Try exec for statements (like assignments)
|
|
77
|
+
try:
|
|
78
|
+
exec(code, self.namespace)
|
|
79
|
+
return "Executed successfully"
|
|
80
|
+
except Exception:
|
|
81
|
+
return f"Error: {e}"
|
|
82
|
+
|
|
83
|
+
def inspect_object(self, variable_name: str) -> str:
|
|
84
|
+
"""Deep inspection of an object in the runtime context.
|
|
85
|
+
|
|
86
|
+
Shows the object's type, attributes, methods, and current state.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
variable_name: Name of the variable to inspect
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Detailed information about the object
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
inspect_object("profile")
|
|
96
|
+
"""
|
|
97
|
+
if not self.frame:
|
|
98
|
+
return "No runtime context available"
|
|
99
|
+
|
|
100
|
+
if variable_name not in self.namespace:
|
|
101
|
+
return f"Variable '{variable_name}' not found in scope"
|
|
102
|
+
|
|
103
|
+
obj = self.namespace[variable_name]
|
|
104
|
+
result = [f"=== {variable_name} ==="]
|
|
105
|
+
result.append(f"Type: {type(obj).__name__}")
|
|
106
|
+
result.append(f"Value: {self._format_result(obj, max_length=200)}")
|
|
107
|
+
|
|
108
|
+
# Type-specific details
|
|
109
|
+
if isinstance(obj, dict):
|
|
110
|
+
result.append(f"Keys ({len(obj)}): {list(obj.keys())[:10]}")
|
|
111
|
+
if len(obj) > 10:
|
|
112
|
+
result.append(f" ... +{len(obj) - 10} more")
|
|
113
|
+
elif isinstance(obj, (list, tuple)):
|
|
114
|
+
result.append(f"Length: {len(obj)}")
|
|
115
|
+
if obj:
|
|
116
|
+
result.append(f"First: {self._format_result(obj[0], max_length=100)}")
|
|
117
|
+
elif hasattr(obj, '__dict__'):
|
|
118
|
+
attrs = vars(obj)
|
|
119
|
+
result.append(f"Attributes: {list(attrs.keys())[:10]}")
|
|
120
|
+
|
|
121
|
+
# Show methods (non-private)
|
|
122
|
+
methods = [m for m in dir(obj)
|
|
123
|
+
if not m.startswith('_') and callable(getattr(obj, m, None))]
|
|
124
|
+
if methods:
|
|
125
|
+
result.append(f"Methods: {methods[:10]}")
|
|
126
|
+
if len(methods) > 10:
|
|
127
|
+
result.append(f" ... +{len(methods) - 10} more")
|
|
128
|
+
|
|
129
|
+
return "\n".join(result)
|
|
130
|
+
|
|
131
|
+
def test_fix(self, original_code: str, fixed_code: str) -> str:
|
|
132
|
+
"""Test a potential fix using the actual runtime data.
|
|
133
|
+
|
|
134
|
+
Compare what the original code produced vs what the fix would produce.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
original_code: The code that caused the error
|
|
138
|
+
fixed_code: The proposed fix to test
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Comparison of results from both versions
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
test_fix("data['key']", "data.get('key', 'default')")
|
|
145
|
+
"""
|
|
146
|
+
if not self.frame:
|
|
147
|
+
return "No runtime context available"
|
|
148
|
+
|
|
149
|
+
result = ["=== Testing Fix ==="]
|
|
150
|
+
|
|
151
|
+
# Test original
|
|
152
|
+
result.append(f"\nOriginal: {original_code}")
|
|
153
|
+
try:
|
|
154
|
+
original_result = eval(original_code, self.namespace)
|
|
155
|
+
result.append(f" → {self._format_result(original_result)}")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
result.append(f" ✗ {e}")
|
|
158
|
+
|
|
159
|
+
# Test fix
|
|
160
|
+
result.append(f"\nFixed: {fixed_code}")
|
|
161
|
+
try:
|
|
162
|
+
fixed_result = eval(fixed_code, self.namespace)
|
|
163
|
+
result.append(f" → {self._format_result(fixed_result)}")
|
|
164
|
+
result.append(" ✓ Fix works!")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
result.append(f" ✗ {e}")
|
|
167
|
+
|
|
168
|
+
return "\n".join(result)
|
|
169
|
+
|
|
170
|
+
def validate_assumption(self, assumption: str) -> str:
|
|
171
|
+
"""Validate an assumption about the runtime state.
|
|
172
|
+
|
|
173
|
+
Test any assumption using the actual data.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
assumption: Python expression that should return True/False
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Whether the assumption holds and details
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
validate_assumption("isinstance(profile, dict)")
|
|
183
|
+
validate_assumption("'notifications' in profile")
|
|
184
|
+
validate_assumption("len(items) > 0")
|
|
185
|
+
"""
|
|
186
|
+
if not self.frame:
|
|
187
|
+
return "No runtime context available"
|
|
188
|
+
|
|
189
|
+
result = [f"=== Validating: {assumption} ==="]
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
validation_result = eval(assumption, self.namespace)
|
|
193
|
+
|
|
194
|
+
if validation_result is True:
|
|
195
|
+
result.append("✓ TRUE")
|
|
196
|
+
elif validation_result is False:
|
|
197
|
+
result.append("✗ FALSE")
|
|
198
|
+
else:
|
|
199
|
+
result.append(f"Result: {self._format_result(validation_result)} (not boolean)")
|
|
200
|
+
|
|
201
|
+
# Add helpful context
|
|
202
|
+
self._add_validation_context(assumption, result)
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
result.append(f"Error: {e}")
|
|
206
|
+
|
|
207
|
+
return "\n".join(result)
|
|
208
|
+
|
|
209
|
+
def trace_variable(self, variable_name: str) -> str:
|
|
210
|
+
"""Trace how a variable changed through the call stack.
|
|
211
|
+
|
|
212
|
+
Shows the variable's value in each frame of the call stack.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
variable_name: Name of the variable to trace
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The variable's values through the call stack
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
trace_variable("user_data")
|
|
222
|
+
"""
|
|
223
|
+
if not self.exception_traceback:
|
|
224
|
+
return "No traceback available"
|
|
225
|
+
|
|
226
|
+
result = [f"=== Tracing '{variable_name}' ==="]
|
|
227
|
+
|
|
228
|
+
frame_num = 0
|
|
229
|
+
current_traceback = self.exception_traceback
|
|
230
|
+
|
|
231
|
+
while current_traceback:
|
|
232
|
+
frame = current_traceback.tb_frame
|
|
233
|
+
frame_num += 1
|
|
234
|
+
|
|
235
|
+
func_name = frame.f_code.co_name
|
|
236
|
+
filename = Path(frame.f_code.co_filename).name
|
|
237
|
+
line_no = current_traceback.tb_lineno
|
|
238
|
+
|
|
239
|
+
result.append(f"\n#{frame_num} {func_name}() at {filename}:{line_no}")
|
|
240
|
+
|
|
241
|
+
if variable_name in frame.f_locals:
|
|
242
|
+
value = frame.f_locals[variable_name]
|
|
243
|
+
result.append(f" {variable_name} = {self._format_result(value)}")
|
|
244
|
+
else:
|
|
245
|
+
result.append(f" (not in scope)")
|
|
246
|
+
|
|
247
|
+
current_traceback = current_traceback.tb_next
|
|
248
|
+
|
|
249
|
+
return "\n".join(result)
|
|
250
|
+
|
|
251
|
+
def explore_namespace(self) -> str:
|
|
252
|
+
"""Explore all available variables in the exception context.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List of all variables and their types
|
|
256
|
+
"""
|
|
257
|
+
if not self.frame:
|
|
258
|
+
return "No runtime context available"
|
|
259
|
+
|
|
260
|
+
result = ["=== Available Variables ==="]
|
|
261
|
+
|
|
262
|
+
# Group by type for better organization
|
|
263
|
+
by_type: Dict[str, List[str]] = {}
|
|
264
|
+
|
|
265
|
+
for name, value in self.namespace.items():
|
|
266
|
+
if name.startswith('__'):
|
|
267
|
+
continue # Skip dunder variables
|
|
268
|
+
|
|
269
|
+
type_name = type(value).__name__
|
|
270
|
+
if type_name not in by_type:
|
|
271
|
+
by_type[type_name] = []
|
|
272
|
+
by_type[type_name].append(name)
|
|
273
|
+
|
|
274
|
+
# Show variables grouped by type
|
|
275
|
+
for type_name in sorted(by_type.keys()):
|
|
276
|
+
vars_list = by_type[type_name][:10] # Limit to 10 per type
|
|
277
|
+
if len(by_type[type_name]) > 10:
|
|
278
|
+
vars_list.append(f"... +{len(by_type[type_name]) - 10} more")
|
|
279
|
+
result.append(f"\n{type_name}: {', '.join(vars_list)}")
|
|
280
|
+
|
|
281
|
+
return "\n".join(result)
|
|
282
|
+
|
|
283
|
+
def try_alternative(self, failing_expr: str, *alternatives: str) -> str:
|
|
284
|
+
"""Try multiple alternative expressions to find what works.
|
|
285
|
+
|
|
286
|
+
Useful for exploring different ways to access data.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
failing_expr: The expression that's failing
|
|
290
|
+
*alternatives: Alternative expressions to try
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Results of all attempts
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
try_alternative(
|
|
297
|
+
"data['key']",
|
|
298
|
+
"data.get('key')",
|
|
299
|
+
"data.get('Key')", # Different case
|
|
300
|
+
"data.get('keys')" # Plural
|
|
301
|
+
)
|
|
302
|
+
"""
|
|
303
|
+
if not self.frame:
|
|
304
|
+
return "No runtime context available"
|
|
305
|
+
|
|
306
|
+
result = ["=== Trying Alternatives ==="]
|
|
307
|
+
|
|
308
|
+
# Test original
|
|
309
|
+
result.append(f"\nOriginal: {failing_expr}")
|
|
310
|
+
try:
|
|
311
|
+
orig_result = eval(failing_expr, self.namespace)
|
|
312
|
+
result.append(f" ✓ Works: {self._format_result(orig_result)}")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
result.append(f" ✗ {e}")
|
|
315
|
+
|
|
316
|
+
# Test alternatives
|
|
317
|
+
for alt in alternatives:
|
|
318
|
+
result.append(f"\nAlternative: {alt}")
|
|
319
|
+
try:
|
|
320
|
+
alt_result = eval(alt, self.namespace)
|
|
321
|
+
result.append(f" ✓ Works: {self._format_result(alt_result)}")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
result.append(f" ✗ {e}")
|
|
324
|
+
|
|
325
|
+
return "\n".join(result)
|
|
326
|
+
|
|
327
|
+
def read_source_around_error(self, context_lines: int = 5) -> str:
|
|
328
|
+
"""Read source code around the error location.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
context_lines: Number of lines before and after to show
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Source code with line numbers, highlighting the error line
|
|
335
|
+
"""
|
|
336
|
+
if not self.exception_traceback:
|
|
337
|
+
return "No traceback available"
|
|
338
|
+
|
|
339
|
+
# Get the file and line from the traceback
|
|
340
|
+
current_traceback = self.exception_traceback
|
|
341
|
+
while current_traceback.tb_next: # Go to the last frame
|
|
342
|
+
current_traceback = current_traceback.tb_next
|
|
343
|
+
|
|
344
|
+
filename = current_traceback.tb_frame.f_code.co_filename
|
|
345
|
+
line_number = current_traceback.tb_lineno
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
path = Path(filename)
|
|
349
|
+
if not path.exists():
|
|
350
|
+
return f"File not found: {filename}"
|
|
351
|
+
|
|
352
|
+
with open(path, 'r') as f:
|
|
353
|
+
lines = f.readlines()
|
|
354
|
+
|
|
355
|
+
start = max(0, line_number - context_lines - 1)
|
|
356
|
+
end = min(len(lines), line_number + context_lines)
|
|
357
|
+
|
|
358
|
+
result = [f"=== {path.name}:{line_number} ===\n"]
|
|
359
|
+
for i in range(start, end):
|
|
360
|
+
line_num = i + 1
|
|
361
|
+
prefix = ">>>" if line_num == line_number else " "
|
|
362
|
+
result.append(f"{prefix} {line_num:4}: {lines[i].rstrip()}")
|
|
363
|
+
|
|
364
|
+
return "\n".join(result)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
return f"Error reading source: {e}"
|
|
367
|
+
|
|
368
|
+
def _format_result(self, obj: Any, max_length: int = 500) -> str:
|
|
369
|
+
"""Format an object for display."""
|
|
370
|
+
if obj is None:
|
|
371
|
+
return "None"
|
|
372
|
+
elif isinstance(obj, (str, int, float, bool)):
|
|
373
|
+
result = repr(obj)
|
|
374
|
+
elif isinstance(obj, (list, dict, tuple)):
|
|
375
|
+
result = repr(obj)
|
|
376
|
+
else:
|
|
377
|
+
result = f"{type(obj).__name__}: {str(obj)}"
|
|
378
|
+
|
|
379
|
+
if len(result) > max_length:
|
|
380
|
+
result = result[:max_length] + "..."
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
def _add_validation_context(self, assumption: str, result: list):
|
|
384
|
+
"""Add helpful context for validation results."""
|
|
385
|
+
# For isinstance checks, show actual type
|
|
386
|
+
if "isinstance" in assumption:
|
|
387
|
+
match = re.match(r'isinstance\((\w+),', assumption)
|
|
388
|
+
if match:
|
|
389
|
+
var_name = match.group(1)
|
|
390
|
+
if var_name in self.namespace:
|
|
391
|
+
actual_type = type(self.namespace[var_name]).__name__
|
|
392
|
+
result.append(f" Actual type: {actual_type}")
|
|
393
|
+
|
|
394
|
+
# For membership tests, show available options
|
|
395
|
+
if " in " in assumption:
|
|
396
|
+
parts = assumption.split(" in ")
|
|
397
|
+
if len(parts) == 2:
|
|
398
|
+
container_code = parts[1].strip().rstrip(')')
|
|
399
|
+
try:
|
|
400
|
+
container = eval(container_code, self.namespace)
|
|
401
|
+
if isinstance(container, dict):
|
|
402
|
+
result.append(f" Available keys: {list(container.keys())[:10]}")
|
|
403
|
+
elif isinstance(container, (list, tuple, set)):
|
|
404
|
+
result.append(f" Contains {len(container)} items")
|
|
405
|
+
except:
|
|
406
|
+
pass
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Debug explainer - AI-powered explanation of tool choices during debugging.
|
|
2
|
+
|
|
3
|
+
Provides runtime investigation capabilities to explain why an agent
|
|
4
|
+
chose to call a specific tool with specific arguments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .explain_agent import explain_tool_choice
|
|
8
|
+
from .explain_context import RuntimeContext
|
|
9
|
+
|
|
10
|
+
__all__ = ["explain_tool_choice", "RuntimeContext"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Create AI agent to explain why tools were chosen during debugging with experimental investigation capabilities
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [pathlib, explain_context.py, ../agent.py, inspect] | imported by [interactive_debugger.py] | no dedicated tests found
|
|
5
|
+
Data flow: interactive_debugger calls explain_tool_choice(breakpoint_context, agent, model) → extracts tool info (name, args, result, source code), agent info (system_prompt, available_tools), conversation history → creates RuntimeContext with experimental tools → creates explainer Agent with RuntimeContext as tool + explainer_prompt.md → sends comprehensive context prompt → Agent investigates and returns explanation string
|
|
6
|
+
State/Effects: reads explainer_prompt.md file | creates temporary explainer Agent instance | calls RuntimeContext methods (which make LLM requests) | log=False prevents logging | no persistent state
|
|
7
|
+
Integration: exposes explain_tool_choice(breakpoint_context, agent_instance, model) function | used by interactive_debugger WHY action | explainer agent has max_iterations=5 for investigation | RuntimeContext provides experimental debugging methods
|
|
8
|
+
Performance: one explainer agent per WHY request | extracts source via inspect.getsource() | may make multiple LLM calls if explainer uses investigation tools | synchronous blocking
|
|
9
|
+
Errors: FileNotFoundError if explainer_prompt.md missing | source extraction failures caught (returns "unavailable") | Agent creation and LLM errors propagate
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from .explain_context import RuntimeContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def explain_tool_choice(
|
|
17
|
+
breakpoint_context,
|
|
18
|
+
agent_instance,
|
|
19
|
+
model: str = "co/gpt-5"
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Explain why the agent chose this specific tool.
|
|
22
|
+
|
|
23
|
+
Provides all context information upfront so the explainer doesn't need
|
|
24
|
+
to call investigation tools.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
breakpoint_context: BreakpointContext from the debugger
|
|
28
|
+
agent_instance: The Agent being debugged
|
|
29
|
+
model: AI model to use (default: co/gpt-5 for consistent debugging)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Explanation string from the AI agent
|
|
33
|
+
"""
|
|
34
|
+
from ..agent import Agent
|
|
35
|
+
import inspect
|
|
36
|
+
|
|
37
|
+
# Get all the information we need
|
|
38
|
+
tool_name = breakpoint_context.tool_name
|
|
39
|
+
tool_args = breakpoint_context.tool_args
|
|
40
|
+
user_prompt = breakpoint_context.user_prompt
|
|
41
|
+
tool_result = breakpoint_context.trace_entry.get('result')
|
|
42
|
+
tool_status = breakpoint_context.trace_entry.get('status')
|
|
43
|
+
|
|
44
|
+
# Get tool source code
|
|
45
|
+
tool = agent_instance.tools.get(tool_name)
|
|
46
|
+
tool_source = "Source unavailable"
|
|
47
|
+
if tool:
|
|
48
|
+
func = tool.run if hasattr(tool, 'run') else tool
|
|
49
|
+
while hasattr(func, '__wrapped__'):
|
|
50
|
+
func = func.__wrapped__
|
|
51
|
+
tool_source = inspect.getsource(func)
|
|
52
|
+
|
|
53
|
+
# Get agent information
|
|
54
|
+
agent_name = agent_instance.name
|
|
55
|
+
agent_system_prompt = agent_instance.system_prompt or "No system prompt"
|
|
56
|
+
available_tools = [t.name for t in agent_instance.tools] if agent_instance.tools else []
|
|
57
|
+
previous_tools = breakpoint_context.previous_tools
|
|
58
|
+
iteration = breakpoint_context.iteration
|
|
59
|
+
|
|
60
|
+
# Get conversation history
|
|
61
|
+
messages = agent_instance.current_session.get('messages', [])
|
|
62
|
+
recent_messages = messages[-3:] if len(messages) > 3 else messages
|
|
63
|
+
|
|
64
|
+
# Get next planned actions
|
|
65
|
+
next_actions = breakpoint_context.next_actions or []
|
|
66
|
+
|
|
67
|
+
# Create runtime context - its methods become investigation tools
|
|
68
|
+
runtime_ctx = RuntimeContext(breakpoint_context, agent_instance)
|
|
69
|
+
|
|
70
|
+
# Load system prompt from markdown file
|
|
71
|
+
prompt_file = Path(__file__).parent / "explainer_prompt.md"
|
|
72
|
+
|
|
73
|
+
# Create explainer agent with runtime context tools
|
|
74
|
+
explainer = Agent(
|
|
75
|
+
name="tool_choice_explainer",
|
|
76
|
+
system_prompt=prompt_file,
|
|
77
|
+
tools=[runtime_ctx], # Experimental tools for deeper investigation
|
|
78
|
+
model=model,
|
|
79
|
+
max_iterations=5, # Allow investigation steps if needed
|
|
80
|
+
log=False # Don't clutter user's logs with explainer agent activity
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Build the full context prompt
|
|
84
|
+
context_prompt = f"""The agent was asked: "{user_prompt}"
|
|
85
|
+
|
|
86
|
+
It chose to call: {tool_name}({tool_args})
|
|
87
|
+
|
|
88
|
+
## Agent Information
|
|
89
|
+
- Agent name: {agent_name}
|
|
90
|
+
- System prompt: {agent_system_prompt}
|
|
91
|
+
- Iteration: {iteration}
|
|
92
|
+
- Available tools: {available_tools}
|
|
93
|
+
- Previous tools called: {previous_tools}
|
|
94
|
+
|
|
95
|
+
## Tool Information
|
|
96
|
+
- Status: {tool_status}
|
|
97
|
+
- Arguments: {tool_args}
|
|
98
|
+
- Result: {tool_result}
|
|
99
|
+
|
|
100
|
+
## Tool Source Code
|
|
101
|
+
```python
|
|
102
|
+
{tool_source}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Recent Conversation (last 3 messages)
|
|
106
|
+
{chr(10).join([f"- {msg.get('role')}: {str(msg.get('content', ''))[:200]}" for msg in recent_messages])}
|
|
107
|
+
|
|
108
|
+
## What Agent Plans Next
|
|
109
|
+
{chr(10).join([f"- {action['name']}({action['args']})" for action in next_actions]) if next_actions else "No more tools planned"}
|
|
110
|
+
|
|
111
|
+
Please explain why this tool was called with these arguments based on the context above."""
|
|
112
|
+
|
|
113
|
+
result = explainer.input(context_prompt)
|
|
114
|
+
return result
|