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,1039 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Provide Rich-formatted UI for interactive debugging with breakpoint display and user interaction
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [typing, dataclasses, enum, json, ast, inspect, pprint, questionary, rich.*] | imported by [interactive_debugger.py] | no dedicated test file found
|
|
5
|
+
Data flow: InteractiveDebugger creates DebuggerUI() → calls .show_welcome(agent_name) → .get_user_prompt() for input → .show_executing(prompt) → .show_breakpoint(context: BreakpointContext) → displays Rich panels/tables with tool execution details → returns BreakpointAction enum (CONTINUE, EDIT, WHY, QUIT) → .edit_value(context, agent) opens Python REPL for live modifications → returns modifications dict → .display_explanation(text, context) shows AI analysis
|
|
6
|
+
State/Effects: no persistent state (stateless UI) | uses Rich Console to write formatted output to terminal | uses questionary for interactive menus | REPL execution in .edit_value() can have arbitrary side effects (user code) | does not modify agent or trace data directly (returns modifications)
|
|
7
|
+
Integration: exposes DebuggerUI class, BreakpointContext dataclass, BreakpointAction enum | show_breakpoint() displays comprehensive debugging info: execution context, tool args/result, LLM next actions, source code, execution history | edit_value() provides REPL with access to trace_entry, tool_args, agent, result variables | display_explanation() formats AI-generated debugging insights
|
|
8
|
+
Performance: Rich rendering is fast | source code inspection uses inspect.getsource() | execution history can be large (shows all tools) | REPL evaluation is synchronous (blocks UI)
|
|
9
|
+
Errors: REPL errors caught and displayed with syntax highlighting | getsource() failures handled gracefully (shows "unavailable") | questionary keyboard interrupts propagate up | assumes terminal supports Rich formatting
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, Optional, Tuple, List
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
import json
|
|
16
|
+
import ast
|
|
17
|
+
import inspect
|
|
18
|
+
from pprint import pformat
|
|
19
|
+
|
|
20
|
+
import questionary
|
|
21
|
+
from questionary import Style
|
|
22
|
+
from rich.console import Console as RichConsole
|
|
23
|
+
from rich.console import Group
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.syntax import Syntax
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
from rich.tree import Tree
|
|
29
|
+
from rich import box
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BreakpointAction(Enum):
|
|
33
|
+
"""User's choice at a breakpoint"""
|
|
34
|
+
CONTINUE = "continue"
|
|
35
|
+
EDIT = "edit"
|
|
36
|
+
WHY = "why"
|
|
37
|
+
QUIT = "quit"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class BreakpointContext:
|
|
42
|
+
"""All data needed to display a breakpoint"""
|
|
43
|
+
tool_name: str
|
|
44
|
+
tool_args: Dict
|
|
45
|
+
trace_entry: Dict
|
|
46
|
+
user_prompt: str
|
|
47
|
+
iteration: int
|
|
48
|
+
max_iterations: int
|
|
49
|
+
previous_tools: List[str]
|
|
50
|
+
next_actions: Optional[List[Dict]] = None # Preview of next planned tools
|
|
51
|
+
tool_function: Optional[Any] = None # The actual tool function for source inspection
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DebuggerUI:
|
|
55
|
+
"""Handles all user interaction and display for the debugger."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
"""Initialize the UI with styling."""
|
|
59
|
+
self.console = RichConsole()
|
|
60
|
+
self.style = Style([
|
|
61
|
+
('question', 'fg:#00ffff bold'),
|
|
62
|
+
('pointer', 'fg:#00ff00 bold'),
|
|
63
|
+
('highlighted', 'fg:#00ff00 bold'),
|
|
64
|
+
('selected', 'fg:#00ffff'),
|
|
65
|
+
('instruction', 'fg:#808080'),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
def show_welcome(self, agent_name: str) -> None:
|
|
69
|
+
"""Display welcome panel for debug session.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
agent_name: Name of the agent being debugged
|
|
73
|
+
"""
|
|
74
|
+
self.console.print(Panel(
|
|
75
|
+
"[bold cyan]🔍 Interactive Debug Session Started[/bold cyan]\n\n"
|
|
76
|
+
f"Agent: [yellow]{agent_name}[/yellow]\n"
|
|
77
|
+
"Tools with @xray will pause for inspection\n"
|
|
78
|
+
"Interactive menu at breakpoints to continue or edit\n",
|
|
79
|
+
title="Auto Debug",
|
|
80
|
+
border_style="cyan"
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
def get_user_prompt(self) -> Optional[str]:
|
|
84
|
+
"""Get prompt from user or None if they want to quit.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
User's prompt string or None to quit
|
|
88
|
+
"""
|
|
89
|
+
prompt = input("\nEnter prompt for agent (or 'quit' to exit): ").strip()
|
|
90
|
+
|
|
91
|
+
if prompt.lower() in ['quit', 'exit', 'q']:
|
|
92
|
+
self.console.print("[yellow]Debug session ended.[/yellow]")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
return prompt if prompt else self.get_user_prompt() # Retry if empty
|
|
96
|
+
|
|
97
|
+
def show_executing(self, prompt: str) -> None:
|
|
98
|
+
"""Show that a prompt is being executed.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
prompt: The prompt being executed
|
|
102
|
+
"""
|
|
103
|
+
self.console.print(f"\n[cyan]→ Executing: {prompt}[/cyan]")
|
|
104
|
+
|
|
105
|
+
def show_result(self, result: str) -> None:
|
|
106
|
+
"""Display the final result of task execution.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
result: The result to display
|
|
110
|
+
"""
|
|
111
|
+
self.console.print(f"\n[green]✓ Result:[/green] {result}")
|
|
112
|
+
|
|
113
|
+
def show_interrupted(self) -> None:
|
|
114
|
+
"""Show that task was interrupted."""
|
|
115
|
+
self.console.print("\n[yellow]Task interrupted.[/yellow]")
|
|
116
|
+
|
|
117
|
+
def show_breakpoint(self, context: BreakpointContext) -> BreakpointAction:
|
|
118
|
+
"""Display breakpoint UI and get user's choice.
|
|
119
|
+
|
|
120
|
+
Shows tool information, arguments, results, and a menu
|
|
121
|
+
for the user to choose their action.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
context: All context data for the breakpoint
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
User's chosen action
|
|
128
|
+
"""
|
|
129
|
+
self._display_breakpoint_info(context)
|
|
130
|
+
return self._show_action_menu()
|
|
131
|
+
|
|
132
|
+
def edit_value(self, context: BreakpointContext, agent: Any = None) -> Dict[str, Any]:
|
|
133
|
+
"""Start Python REPL to inspect and modify execution state.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
context: Full breakpoint context with all execution data
|
|
137
|
+
agent: Optional agent instance for accessing agent context
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dict of modified values (e.g., {'result': new_value, 'tool_args': {...}})
|
|
141
|
+
"""
|
|
142
|
+
import code
|
|
143
|
+
|
|
144
|
+
# Build namespace with all debuggable variables
|
|
145
|
+
result = context.trace_entry.get('result')
|
|
146
|
+
namespace = {
|
|
147
|
+
# Primary execution
|
|
148
|
+
'result': result,
|
|
149
|
+
'tool_name': context.tool_name,
|
|
150
|
+
'tool_args': context.tool_args.copy(), # Make it mutable
|
|
151
|
+
|
|
152
|
+
# Flow control
|
|
153
|
+
'iteration': context.iteration,
|
|
154
|
+
'max_iterations': context.max_iterations,
|
|
155
|
+
|
|
156
|
+
# Context
|
|
157
|
+
'user_prompt': context.user_prompt,
|
|
158
|
+
'next_actions': context.next_actions,
|
|
159
|
+
|
|
160
|
+
# Advanced
|
|
161
|
+
'trace_entry': context.trace_entry,
|
|
162
|
+
'previous_tools': context.previous_tools,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Add agent context if available
|
|
166
|
+
if agent:
|
|
167
|
+
namespace.update({
|
|
168
|
+
'agent_name': agent.name,
|
|
169
|
+
'model': agent.llm.model if hasattr(agent.llm, 'model') else 'unknown',
|
|
170
|
+
'tools_available': [tool.name for tool in agent.tools] if agent.tools else [],
|
|
171
|
+
'turn': agent.current_session.get('turn', 0) if agent.current_session else 0,
|
|
172
|
+
'messages': agent.current_session.get('messages', []) if agent.current_session else [],
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
# Add helper function for pretty printing in REPL
|
|
176
|
+
def pp(obj):
|
|
177
|
+
"""Pretty print helper for explicit use"""
|
|
178
|
+
from rich.pretty import pprint
|
|
179
|
+
pprint(obj, expand_all=True)
|
|
180
|
+
|
|
181
|
+
namespace['pp'] = pp # Add to namespace
|
|
182
|
+
|
|
183
|
+
# Display REPL header
|
|
184
|
+
self._display_repl_header(context, namespace)
|
|
185
|
+
|
|
186
|
+
# Customize REPL display hook to auto pretty-print
|
|
187
|
+
import sys
|
|
188
|
+
from rich.pretty import pprint
|
|
189
|
+
|
|
190
|
+
original_displayhook = sys.displayhook
|
|
191
|
+
|
|
192
|
+
def rich_displayhook(value):
|
|
193
|
+
"""Custom display hook that uses Rich pretty printing"""
|
|
194
|
+
if value is not None:
|
|
195
|
+
pprint(value, expand_all=True)
|
|
196
|
+
# Also store in _ for REPL access
|
|
197
|
+
import builtins
|
|
198
|
+
builtins._ = value
|
|
199
|
+
|
|
200
|
+
sys.displayhook = rich_displayhook
|
|
201
|
+
|
|
202
|
+
# Check if stdin is available for interactive REPL
|
|
203
|
+
if not sys.stdin or not hasattr(sys.stdin, 'isatty') or not sys.stdin.isatty():
|
|
204
|
+
self.console.print("\n[yellow]⚠️ Interactive REPL not available (stdin not connected)[/yellow]")
|
|
205
|
+
self.console.print("[dim]Tip: Run directly in a terminal (not through VSCode/IDE) to use EDIT feature[/dim]")
|
|
206
|
+
return {}
|
|
207
|
+
|
|
208
|
+
# Start interactive Python REPL
|
|
209
|
+
banner = "" # Empty banner since we show our own header
|
|
210
|
+
try:
|
|
211
|
+
code.interact(banner=banner, local=namespace, exitmsg="")
|
|
212
|
+
except SystemExit:
|
|
213
|
+
pass # Normal REPL exit
|
|
214
|
+
finally:
|
|
215
|
+
# Restore original displayhook
|
|
216
|
+
sys.displayhook = original_displayhook
|
|
217
|
+
|
|
218
|
+
# Extract modifications from namespace
|
|
219
|
+
modifications = {}
|
|
220
|
+
if namespace['result'] != result:
|
|
221
|
+
modifications['result'] = namespace['result']
|
|
222
|
+
if namespace['tool_args'] != context.tool_args:
|
|
223
|
+
modifications['tool_args'] = namespace['tool_args']
|
|
224
|
+
if namespace['iteration'] != context.iteration:
|
|
225
|
+
modifications['iteration'] = namespace['iteration']
|
|
226
|
+
if namespace['max_iterations'] != context.max_iterations:
|
|
227
|
+
modifications['max_iterations'] = namespace['max_iterations']
|
|
228
|
+
|
|
229
|
+
# Show what was modified
|
|
230
|
+
if modifications:
|
|
231
|
+
self._display_modifications(modifications)
|
|
232
|
+
else:
|
|
233
|
+
self.console.print("\n[dim]No modifications made[/dim]")
|
|
234
|
+
|
|
235
|
+
return modifications
|
|
236
|
+
|
|
237
|
+
# Private helper methods for cleaner code
|
|
238
|
+
|
|
239
|
+
def _display_breakpoint_info(self, context: BreakpointContext) -> None:
|
|
240
|
+
"""Display complete debugging context from user prompt to execution result.
|
|
241
|
+
|
|
242
|
+
Shows a comprehensive panel with:
|
|
243
|
+
- User prompt and iteration context
|
|
244
|
+
- Execution flow tree (previous → current → next tools)
|
|
245
|
+
- Current execution details (function call, result, source code)
|
|
246
|
+
- Next planned action preview
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
context: All breakpoint data including tool info, execution state, and previews
|
|
250
|
+
"""
|
|
251
|
+
# Clear some space
|
|
252
|
+
self.console.print("\n")
|
|
253
|
+
|
|
254
|
+
# Build sections without individual panels
|
|
255
|
+
sections = []
|
|
256
|
+
|
|
257
|
+
# 1. Context Section
|
|
258
|
+
prompt_display = context.user_prompt if len(context.user_prompt) <= 80 else f"{context.user_prompt[:80]}..."
|
|
259
|
+
sections.append(Text("CONTEXT", style="bold dim"))
|
|
260
|
+
sections.append(Text(f'User Prompt: "{prompt_display}"', style="cyan"))
|
|
261
|
+
sections.append(Text(f"Iteration: {context.iteration}/{context.max_iterations} | Model: o4-mini", style="dim"))
|
|
262
|
+
sections.append(Text("")) # Empty line for spacing
|
|
263
|
+
|
|
264
|
+
# 2. Execution Flow Section
|
|
265
|
+
sections.append(Text("EXECUTION FLOW", style="bold dim"))
|
|
266
|
+
|
|
267
|
+
tree = Tree("User Input")
|
|
268
|
+
llm_branch = tree.add("LLM Decision")
|
|
269
|
+
|
|
270
|
+
# Add all tools in the chain
|
|
271
|
+
all_tools = context.previous_tools + [context.tool_name]
|
|
272
|
+
for i, tool in enumerate(all_tools):
|
|
273
|
+
if tool == context.tool_name:
|
|
274
|
+
# Current tool (highlighted)
|
|
275
|
+
timing = context.trace_entry.get('timing', 0)
|
|
276
|
+
llm_branch.add(f"[bold yellow]⚡ {tool}() - {timing/1000:.4f}s ← PAUSED HERE[/bold yellow]")
|
|
277
|
+
elif i < len(context.previous_tools):
|
|
278
|
+
# Completed tools
|
|
279
|
+
llm_branch.add(f"✓ {tool}() - [dim]completed[/dim]")
|
|
280
|
+
|
|
281
|
+
# Add next planned actions based on LLM preview
|
|
282
|
+
if context.next_actions is not None:
|
|
283
|
+
if context.next_actions:
|
|
284
|
+
# Show the actual planned next tools
|
|
285
|
+
for i, action in enumerate(context.next_actions):
|
|
286
|
+
tool_name = action['name']
|
|
287
|
+
tool_args = action.get('args', {})
|
|
288
|
+
|
|
289
|
+
# Format arguments for display
|
|
290
|
+
args_display = []
|
|
291
|
+
for key, value in tool_args.items():
|
|
292
|
+
if isinstance(value, str) and len(value) > 20:
|
|
293
|
+
args_display.append(f"{key}='...'")
|
|
294
|
+
elif isinstance(value, str):
|
|
295
|
+
args_display.append(f"{key}='{value}'")
|
|
296
|
+
else:
|
|
297
|
+
args_display.append(f"{key}={value}")
|
|
298
|
+
args_str = ', '.join(args_display) if args_display else ''
|
|
299
|
+
|
|
300
|
+
llm_branch.add(f"📍 {tool_name}({args_str}) - [dim]planned next[/dim]")
|
|
301
|
+
else:
|
|
302
|
+
# No more tools planned - task complete
|
|
303
|
+
llm_branch.add("✅ Task complete - [dim]no more tools needed[/dim]")
|
|
304
|
+
else:
|
|
305
|
+
# Preview unavailable (error or couldn't determine)
|
|
306
|
+
llm_branch.add("❓ Next action - [dim]preview unavailable[/dim]")
|
|
307
|
+
|
|
308
|
+
sections.append(tree)
|
|
309
|
+
sections.append(Text("")) # Empty line for spacing
|
|
310
|
+
|
|
311
|
+
# 3. Current Execution Section (the main focus)
|
|
312
|
+
sections.append(Text("─" * 60, style="dim")) # Visual separator
|
|
313
|
+
sections.append(Text("CURRENT EXECUTION", style="bold yellow"))
|
|
314
|
+
sections.append(Text(""))
|
|
315
|
+
|
|
316
|
+
# Build the function call
|
|
317
|
+
args_str_parts = []
|
|
318
|
+
if context.tool_args:
|
|
319
|
+
for key, value in context.tool_args.items():
|
|
320
|
+
if isinstance(value, str):
|
|
321
|
+
args_str_parts.append(f'{key}="{value}"')
|
|
322
|
+
else:
|
|
323
|
+
args_str_parts.append(f'{key}={value}')
|
|
324
|
+
function_call = f"{context.tool_name}({', '.join(args_str_parts)})"
|
|
325
|
+
|
|
326
|
+
# Get the result
|
|
327
|
+
result = context.trace_entry.get('result', 'No result')
|
|
328
|
+
is_error = context.trace_entry.get('status') == 'error'
|
|
329
|
+
|
|
330
|
+
# REPL section
|
|
331
|
+
sections.append(Text(f">>> {function_call}", style="bold bright_cyan"))
|
|
332
|
+
|
|
333
|
+
if is_error:
|
|
334
|
+
error = context.trace_entry.get('error', str(result))
|
|
335
|
+
sections.append(Text(f"Error: {error}", style="red"))
|
|
336
|
+
else:
|
|
337
|
+
if isinstance(result, str):
|
|
338
|
+
display_result = result[:150] + ('...' if len(result) > 150 else '')
|
|
339
|
+
sections.append(Text(f"'{display_result}'", style="green"))
|
|
340
|
+
elif isinstance(result, (dict, list)):
|
|
341
|
+
try:
|
|
342
|
+
result_json = json.dumps(result, indent=2, ensure_ascii=False)
|
|
343
|
+
display_json = result_json[:200] + ('...' if len(result_json) > 200 else '')
|
|
344
|
+
sections.append(Text(display_json, style="green"))
|
|
345
|
+
except:
|
|
346
|
+
sections.append(Text(f"{str(result)[:100]}...", style="green"))
|
|
347
|
+
else:
|
|
348
|
+
sections.append(Text(str(result), style="green"))
|
|
349
|
+
|
|
350
|
+
sections.append(Text("")) # Spacing
|
|
351
|
+
|
|
352
|
+
# Source code section
|
|
353
|
+
source_code, file_info, start_line = self._get_tool_source(context)
|
|
354
|
+
sections.append(Text(f"Source ({file_info})", style="dim italic"))
|
|
355
|
+
|
|
356
|
+
if source_code:
|
|
357
|
+
# Use start_line_number to show actual file line numbers
|
|
358
|
+
syntax = Syntax(
|
|
359
|
+
source_code,
|
|
360
|
+
"python",
|
|
361
|
+
theme="monokai",
|
|
362
|
+
line_numbers=True,
|
|
363
|
+
start_line=start_line
|
|
364
|
+
)
|
|
365
|
+
sections.append(syntax)
|
|
366
|
+
else:
|
|
367
|
+
sections.append(Text(" Source code unavailable", style="dim"))
|
|
368
|
+
sections.append(Text("─" * 60, style="dim")) # Visual separator
|
|
369
|
+
sections.append(Text("")) # Spacing
|
|
370
|
+
|
|
371
|
+
# 4. Next Planned Action Section
|
|
372
|
+
sections.append(Text("NEXT PLANNED ACTION", style="bold dim"))
|
|
373
|
+
|
|
374
|
+
if context.next_actions is not None:
|
|
375
|
+
if context.next_actions:
|
|
376
|
+
# Show what LLM plans to do next
|
|
377
|
+
sections.append(Text("The LLM will call:", style="dim"))
|
|
378
|
+
|
|
379
|
+
for action in context.next_actions[:1]: # Show just the first one in detail
|
|
380
|
+
tool_name = action['name']
|
|
381
|
+
tool_args = action.get('args', {})
|
|
382
|
+
|
|
383
|
+
# Format the planned call
|
|
384
|
+
args_parts = []
|
|
385
|
+
for key, value in tool_args.items():
|
|
386
|
+
if isinstance(value, str):
|
|
387
|
+
# Show more of the string here since it's a preview
|
|
388
|
+
if len(value) > 50:
|
|
389
|
+
args_parts.append(f'{key}="{value[:50]}..."')
|
|
390
|
+
else:
|
|
391
|
+
args_parts.append(f'{key}="{value}"')
|
|
392
|
+
else:
|
|
393
|
+
args_parts.append(f'{key}={value}')
|
|
394
|
+
|
|
395
|
+
planned_call = f"{tool_name}({', '.join(args_parts)})"
|
|
396
|
+
sections.append(Text(planned_call, style="cyan bold"))
|
|
397
|
+
|
|
398
|
+
if len(context.next_actions) > 1:
|
|
399
|
+
sections.append(Text(f"(and {len(context.next_actions) - 1} more planned actions)", style="dim"))
|
|
400
|
+
else:
|
|
401
|
+
# Task complete
|
|
402
|
+
sections.append(Text("🎯 Task Complete", style="bold green"))
|
|
403
|
+
sections.append(Text("No further tools needed", style="green"))
|
|
404
|
+
else:
|
|
405
|
+
# Preview unavailable
|
|
406
|
+
sections.append(Text("Preview temporarily unavailable", style="dim italic"))
|
|
407
|
+
|
|
408
|
+
# 5. Add metadata footer
|
|
409
|
+
sections.append(Text("")) # Spacing
|
|
410
|
+
timing = context.trace_entry.get('timing', 0)
|
|
411
|
+
metadata = Text(
|
|
412
|
+
f"Execution time: {timing/1000:.4f}s | Iteration: {context.iteration}/{context.max_iterations} | Breakpoint: @xray",
|
|
413
|
+
style="dim italic",
|
|
414
|
+
justify="center"
|
|
415
|
+
)
|
|
416
|
+
sections.append(metadata)
|
|
417
|
+
|
|
418
|
+
# 6. Combine everything into a single panel with proper spacing
|
|
419
|
+
all_content = Group(*sections)
|
|
420
|
+
|
|
421
|
+
# 7. Create single main wrapper panel
|
|
422
|
+
if is_error:
|
|
423
|
+
title = "⚠️ Execution Paused - Error"
|
|
424
|
+
border_style = "red"
|
|
425
|
+
else:
|
|
426
|
+
title = "🔍 Execution Paused - Breakpoint"
|
|
427
|
+
border_style = "yellow"
|
|
428
|
+
|
|
429
|
+
main_panel = Panel(
|
|
430
|
+
all_content,
|
|
431
|
+
title=f"[bold {border_style}]{title}[/bold {border_style}]",
|
|
432
|
+
box=box.ROUNDED,
|
|
433
|
+
border_style=border_style,
|
|
434
|
+
padding=(1, 2)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
self.console.print(main_panel)
|
|
438
|
+
|
|
439
|
+
def _get_tool_source(self, context: BreakpointContext) -> Tuple[Optional[str], str, int]:
|
|
440
|
+
"""Get the source code of the actual tool function.
|
|
441
|
+
|
|
442
|
+
Unwraps decorators to get the original function and extracts:
|
|
443
|
+
- Source code using inspect.getsource()
|
|
444
|
+
- File location and starting line number
|
|
445
|
+
- File info formatted as "filename:line"
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
context: Breakpoint context containing tool_function
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Tuple of (source_code, file_info, start_line_number)
|
|
452
|
+
Returns (None, "source unavailable", 1) if function not available
|
|
453
|
+
"""
|
|
454
|
+
if not context.tool_function:
|
|
455
|
+
return None, "source unavailable", 1
|
|
456
|
+
|
|
457
|
+
# Unwrap to get the original function (not the wrapper)
|
|
458
|
+
func = context.tool_function
|
|
459
|
+
while hasattr(func, '__wrapped__'):
|
|
460
|
+
func = func.__wrapped__
|
|
461
|
+
|
|
462
|
+
source = inspect.getsource(func)
|
|
463
|
+
file_path = inspect.getfile(func)
|
|
464
|
+
start_line = inspect.getsourcelines(func)[1]
|
|
465
|
+
|
|
466
|
+
# Show just filename:line
|
|
467
|
+
import os
|
|
468
|
+
file_name = os.path.basename(file_path)
|
|
469
|
+
file_info = f"{file_name}:{start_line}"
|
|
470
|
+
|
|
471
|
+
return source, file_info, start_line
|
|
472
|
+
|
|
473
|
+
def _show_action_menu(self) -> BreakpointAction:
|
|
474
|
+
"""Show the action menu and get user's choice.
|
|
475
|
+
|
|
476
|
+
Tries multiple UI libraries in order of preference:
|
|
477
|
+
1. simple-term-menu (best compatibility, no asyncio conflicts)
|
|
478
|
+
2. questionary (may conflict with Playwright/asyncio)
|
|
479
|
+
3. simple input fallback (when no TTY or event loop conflicts)
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
User's chosen action (CONTINUE, EDIT, or QUIT)
|
|
483
|
+
"""
|
|
484
|
+
# Try to use simple-term-menu (no asyncio conflicts, works with Playwright)
|
|
485
|
+
try:
|
|
486
|
+
from simple_term_menu import TerminalMenu
|
|
487
|
+
|
|
488
|
+
menu_entries = [
|
|
489
|
+
"[c] Continue execution 🚀",
|
|
490
|
+
"[e] Edit values 🔍",
|
|
491
|
+
"[w] Why this tool? 🤔",
|
|
492
|
+
"[q] Quit debugging 🚫"
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
terminal_menu = TerminalMenu(
|
|
496
|
+
menu_entries,
|
|
497
|
+
title="\nAction:",
|
|
498
|
+
menu_cursor="→ ",
|
|
499
|
+
menu_cursor_style=("fg_green", "bold"),
|
|
500
|
+
menu_highlight_style=("fg_green", "bold"),
|
|
501
|
+
cycle_cursor=True,
|
|
502
|
+
clear_screen=False,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
menu_index = terminal_menu.show()
|
|
506
|
+
|
|
507
|
+
# Handle Ctrl+C or None
|
|
508
|
+
if menu_index is None:
|
|
509
|
+
self.console.print("[yellow]→ Quitting debug session...[/yellow]")
|
|
510
|
+
return BreakpointAction.QUIT
|
|
511
|
+
|
|
512
|
+
# Map index to action
|
|
513
|
+
actions = [BreakpointAction.CONTINUE, BreakpointAction.EDIT, BreakpointAction.WHY, BreakpointAction.QUIT]
|
|
514
|
+
action = actions[menu_index]
|
|
515
|
+
|
|
516
|
+
if action == BreakpointAction.CONTINUE:
|
|
517
|
+
self.console.print("[green]→ Continuing execution...[/green]")
|
|
518
|
+
elif action == BreakpointAction.WHY:
|
|
519
|
+
self.console.print("[cyan]→ Analyzing why tool was chosen...[/cyan]")
|
|
520
|
+
elif action == BreakpointAction.QUIT:
|
|
521
|
+
self.console.print("[yellow]→ Quitting debug session...[/yellow]")
|
|
522
|
+
|
|
523
|
+
return action
|
|
524
|
+
|
|
525
|
+
except (ImportError, OSError):
|
|
526
|
+
# simple-term-menu not installed, not supported (Windows), or no TTY available
|
|
527
|
+
# Fall back to questionary or simple input
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
# Fallback: Use questionary (may have asyncio conflicts with Playwright)
|
|
531
|
+
choices = [
|
|
532
|
+
questionary.Choice("[c] Continue execution 🚀", value=BreakpointAction.CONTINUE, shortcut_key='c'),
|
|
533
|
+
questionary.Choice("[e] Edit values 🔍", value=BreakpointAction.EDIT, shortcut_key='e'),
|
|
534
|
+
questionary.Choice("[w] Why this tool? 🤔", value=BreakpointAction.WHY, shortcut_key='w'),
|
|
535
|
+
questionary.Choice("[q] Quit debugging 🚫", value=BreakpointAction.QUIT, shortcut_key='q'),
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
action = questionary.select(
|
|
540
|
+
"\nAction:",
|
|
541
|
+
choices=choices,
|
|
542
|
+
style=self.style,
|
|
543
|
+
instruction="(Press c/e/w/q)",
|
|
544
|
+
use_shortcuts=True,
|
|
545
|
+
use_indicator=False,
|
|
546
|
+
use_arrow_keys=True
|
|
547
|
+
).ask()
|
|
548
|
+
except RuntimeError:
|
|
549
|
+
# Event loop conflict - use simple input fallback
|
|
550
|
+
return self._simple_input_fallback()
|
|
551
|
+
|
|
552
|
+
# Handle Ctrl+C
|
|
553
|
+
if action is None:
|
|
554
|
+
self.console.print("[yellow]→ Quitting debug session...[/yellow]")
|
|
555
|
+
return BreakpointAction.QUIT
|
|
556
|
+
|
|
557
|
+
if action == BreakpointAction.CONTINUE:
|
|
558
|
+
self.console.print("[green]→ Continuing execution...[/green]")
|
|
559
|
+
elif action == BreakpointAction.WHY:
|
|
560
|
+
self.console.print("[cyan]→ Analyzing why tool was chosen...[/cyan]")
|
|
561
|
+
elif action == BreakpointAction.QUIT:
|
|
562
|
+
self.console.print("[yellow]→ Quitting debug session...[/yellow]")
|
|
563
|
+
|
|
564
|
+
return action
|
|
565
|
+
|
|
566
|
+
def _simple_input_fallback(self) -> BreakpointAction:
|
|
567
|
+
"""Simple text input fallback when event loop conflicts occur.
|
|
568
|
+
|
|
569
|
+
Used when:
|
|
570
|
+
- Asyncio event loop is already running (Playwright, Jupyter)
|
|
571
|
+
- No TTY available
|
|
572
|
+
- Menu libraries not installed or not supported
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
User's chosen action based on keyboard input (c/e/w/q)
|
|
576
|
+
"""
|
|
577
|
+
self.console.print("\n[cyan bold]Action:[/cyan bold]")
|
|
578
|
+
self.console.print(" [c] Continue execution 🚀")
|
|
579
|
+
self.console.print(" [e] Edit values 🔍")
|
|
580
|
+
self.console.print(" [w] Why this tool? 🤔")
|
|
581
|
+
self.console.print(" [q] Quit debugging 🚫")
|
|
582
|
+
|
|
583
|
+
while True:
|
|
584
|
+
try:
|
|
585
|
+
choice = input("\nYour choice (c/e/w/q): ").strip().lower()
|
|
586
|
+
if choice == 'c':
|
|
587
|
+
self.console.print("[green]→ Continuing execution...[/green]")
|
|
588
|
+
return BreakpointAction.CONTINUE
|
|
589
|
+
elif choice == 'e':
|
|
590
|
+
return BreakpointAction.EDIT
|
|
591
|
+
elif choice == 'w':
|
|
592
|
+
self.console.print("[cyan]→ Analyzing why tool was chosen...[/cyan]")
|
|
593
|
+
return BreakpointAction.WHY
|
|
594
|
+
elif choice == 'q':
|
|
595
|
+
self.console.print("[yellow]→ Quitting debug session...[/yellow]")
|
|
596
|
+
return BreakpointAction.QUIT
|
|
597
|
+
else:
|
|
598
|
+
self.console.print("[yellow]Invalid choice. Please enter c, e, w, or q.[/yellow]")
|
|
599
|
+
except (KeyboardInterrupt, EOFError):
|
|
600
|
+
self.console.print("\n[yellow]→ Quitting debug session...[/yellow]")
|
|
601
|
+
return BreakpointAction.QUIT
|
|
602
|
+
|
|
603
|
+
def _display_current_value(self, value: Any) -> None:
|
|
604
|
+
"""Display the current value nicely formatted.
|
|
605
|
+
|
|
606
|
+
Uses Rich syntax highlighting for JSON and appropriate
|
|
607
|
+
formatting for strings, dicts, lists, and other types.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
value: The value to display (any type)
|
|
611
|
+
"""
|
|
612
|
+
self.console.print("\n")
|
|
613
|
+
|
|
614
|
+
# Create a table for the value display
|
|
615
|
+
value_table = Table(show_header=False, box=None)
|
|
616
|
+
value_table.add_column()
|
|
617
|
+
|
|
618
|
+
# Format value based on type
|
|
619
|
+
if isinstance(value, (dict, list)):
|
|
620
|
+
try:
|
|
621
|
+
json_str = json.dumps(value, indent=2, ensure_ascii=False)
|
|
622
|
+
# Use syntax highlighting for JSON
|
|
623
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
|
|
624
|
+
value_table.add_row(syntax)
|
|
625
|
+
except:
|
|
626
|
+
value_table.add_row(f"[green]{value}[/green]")
|
|
627
|
+
elif isinstance(value, str):
|
|
628
|
+
# For strings, show with quotes
|
|
629
|
+
if len(value) > 500:
|
|
630
|
+
value_table.add_row(f'[green]"{value[:500]}..."[/green]')
|
|
631
|
+
else:
|
|
632
|
+
value_table.add_row(f'[green]"{value}"[/green]')
|
|
633
|
+
else:
|
|
634
|
+
value_table.add_row(f"[green]{value}[/green]")
|
|
635
|
+
|
|
636
|
+
# Display in a panel
|
|
637
|
+
panel = Panel(
|
|
638
|
+
value_table,
|
|
639
|
+
title="[bold cyan]📝 Current Result[/bold cyan]",
|
|
640
|
+
border_style="cyan",
|
|
641
|
+
padding=(1, 2)
|
|
642
|
+
)
|
|
643
|
+
self.console.print(panel)
|
|
644
|
+
|
|
645
|
+
def _get_new_value(self) -> Optional[Any]:
|
|
646
|
+
"""Get new value from user via text input.
|
|
647
|
+
|
|
648
|
+
Prompts user to enter a Python expression and attempts to
|
|
649
|
+
parse it using ast.literal_eval(). Falls back to treating
|
|
650
|
+
as string if parsing fails.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Parsed Python value (str, dict, list, etc.) or None if empty
|
|
654
|
+
"""
|
|
655
|
+
self.console.print("\n[cyan]Enter new result value:[/cyan]")
|
|
656
|
+
self.console.print("[dim]Tip: Enter valid Python expression (string, dict, list, etc.)[/dim]")
|
|
657
|
+
self.console.print("[dim]Examples: 'new text', {'key': 'value'}, [1, 2, 3][/dim]\n")
|
|
658
|
+
|
|
659
|
+
new_value_str = input("New result: ").strip()
|
|
660
|
+
|
|
661
|
+
if not new_value_str:
|
|
662
|
+
return None
|
|
663
|
+
|
|
664
|
+
try:
|
|
665
|
+
# Try to evaluate as Python expression
|
|
666
|
+
return ast.literal_eval(new_value_str)
|
|
667
|
+
except (ValueError, SyntaxError):
|
|
668
|
+
# If not valid Python literal, treat as string
|
|
669
|
+
return new_value_str
|
|
670
|
+
|
|
671
|
+
def _display_updated_value(self, value: Any) -> None:
|
|
672
|
+
"""Display the updated value after successful modification.
|
|
673
|
+
|
|
674
|
+
Shows success message and formatted value in yellow panel
|
|
675
|
+
to distinguish from the original value display.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
value: The newly updated value to display
|
|
679
|
+
"""
|
|
680
|
+
self.console.print(f"\n[green]✅ Result updated successfully![/green]\n")
|
|
681
|
+
|
|
682
|
+
# Create a table for the updated value
|
|
683
|
+
value_table = Table(show_header=False, box=None)
|
|
684
|
+
value_table.add_column()
|
|
685
|
+
|
|
686
|
+
# Format value based on type
|
|
687
|
+
if isinstance(value, (dict, list)):
|
|
688
|
+
try:
|
|
689
|
+
json_str = json.dumps(value, indent=2, ensure_ascii=False)
|
|
690
|
+
# Use syntax highlighting
|
|
691
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
|
|
692
|
+
value_table.add_row(syntax)
|
|
693
|
+
except:
|
|
694
|
+
value_table.add_row(f"[yellow]{value}[/yellow]")
|
|
695
|
+
elif isinstance(value, str):
|
|
696
|
+
if len(value) > 500:
|
|
697
|
+
value_table.add_row(f'[yellow]"{value[:500]}..."[/yellow]')
|
|
698
|
+
else:
|
|
699
|
+
value_table.add_row(f'[yellow]"{value}"[/yellow]')
|
|
700
|
+
else:
|
|
701
|
+
value_table.add_row(f"[yellow]{value}[/yellow]")
|
|
702
|
+
|
|
703
|
+
# Display in a panel with different style
|
|
704
|
+
panel = Panel(
|
|
705
|
+
value_table,
|
|
706
|
+
title="[bold yellow]✨ Updated Result[/bold yellow]",
|
|
707
|
+
border_style="yellow",
|
|
708
|
+
padding=(1, 2)
|
|
709
|
+
)
|
|
710
|
+
self.console.print(panel)
|
|
711
|
+
def _display_repl_header(self, context: BreakpointContext, namespace: Dict[str, Any]) -> None:
|
|
712
|
+
"""Display Python REPL header with available variables.
|
|
713
|
+
|
|
714
|
+
Shows a clean table of all variables available in the REPL namespace,
|
|
715
|
+
organized by priority groups:
|
|
716
|
+
1. Execution: result, tool_name, tool_args
|
|
717
|
+
2. Control: iteration, max_iterations
|
|
718
|
+
3. Context: user_prompt, next_actions
|
|
719
|
+
4. Agent: agent_name, model, turn, tools_available
|
|
720
|
+
5. Advanced: messages, trace_entry, previous_tools
|
|
721
|
+
6. Helpers: pp (pretty print function)
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
context: Breakpoint context for reference
|
|
725
|
+
namespace: Dict of all variables available in REPL
|
|
726
|
+
"""
|
|
727
|
+
self.console.print("\n")
|
|
728
|
+
self.console.print(Panel(
|
|
729
|
+
"[bold white]Python REPL - Interactive Debugging[/bold white]\n"
|
|
730
|
+
"[dim]Modify any variable and exit() to apply changes[/dim]",
|
|
731
|
+
title="🐍 Debug Console",
|
|
732
|
+
border_style="green"
|
|
733
|
+
))
|
|
734
|
+
|
|
735
|
+
# Create clean two-column table
|
|
736
|
+
table = Table(
|
|
737
|
+
show_header=True,
|
|
738
|
+
header_style="bold cyan",
|
|
739
|
+
border_style="dim",
|
|
740
|
+
box=box.SIMPLE_HEAD, # Only header has border
|
|
741
|
+
padding=(0, 2), # 0 vertical, 2 horizontal
|
|
742
|
+
show_lines=False
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Two columns: Variable (fixed) and Value (flexible)
|
|
746
|
+
table.add_column("Variable", style="yellow", width=18, no_wrap=True)
|
|
747
|
+
table.add_column("Value", style="white", overflow="fold")
|
|
748
|
+
|
|
749
|
+
# Priority ordering for smart grouping
|
|
750
|
+
priority_order = [
|
|
751
|
+
'result', 'tool_name', 'tool_args', # Group 1: Execution
|
|
752
|
+
'iteration', 'max_iterations', # Group 2: Control
|
|
753
|
+
'user_prompt', 'next_actions', # Group 3: Context
|
|
754
|
+
'agent_name', 'model', 'turn', 'tools_available', # Group 4: Agent
|
|
755
|
+
'messages', 'trace_entry', 'previous_tools', # Group 5: Advanced
|
|
756
|
+
'pp', # Group 6: Helper (show last)
|
|
757
|
+
]
|
|
758
|
+
|
|
759
|
+
# Sort variables by priority
|
|
760
|
+
sorted_items = []
|
|
761
|
+
for key in priority_order:
|
|
762
|
+
if key in namespace:
|
|
763
|
+
sorted_items.append((key, namespace[key]))
|
|
764
|
+
|
|
765
|
+
# Add any remaining variables not in priority list
|
|
766
|
+
for key, value in namespace.items():
|
|
767
|
+
if key not in priority_order:
|
|
768
|
+
sorted_items.append((key, value))
|
|
769
|
+
|
|
770
|
+
# Add rows with automatic grouping
|
|
771
|
+
group_breaks = [2, 4, 6, 10] # Add empty row after these indices (Execution, Control, Context, Agent, Advanced)
|
|
772
|
+
|
|
773
|
+
for i, (var_name, var_value) in enumerate(sorted_items):
|
|
774
|
+
# Add empty row for visual grouping
|
|
775
|
+
if i in group_breaks:
|
|
776
|
+
table.add_row("", "")
|
|
777
|
+
|
|
778
|
+
# Format value with smart formatting
|
|
779
|
+
formatted_value = self._format_value_for_repl(var_value)
|
|
780
|
+
table.add_row(var_name, formatted_value)
|
|
781
|
+
|
|
782
|
+
self.console.print(table)
|
|
783
|
+
self.console.print()
|
|
784
|
+
|
|
785
|
+
def _format_value_for_repl(self, value: Any) -> str:
|
|
786
|
+
"""Format value with smart, consistent formatting for REPL display.
|
|
787
|
+
|
|
788
|
+
Handles different types intelligently:
|
|
789
|
+
- None/bools/numbers: Compact cyan format
|
|
790
|
+
- Strings: Truncate with char count if > 80 chars
|
|
791
|
+
- Dicts: Inline if small, indented if medium, collapsed if large
|
|
792
|
+
- Lists: Inline if simple, indented if fits, collapsed if large
|
|
793
|
+
- Functions: Show as helper description
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
value: Any Python value to format
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
Rich-formatted string for display in REPL table
|
|
800
|
+
"""
|
|
801
|
+
|
|
802
|
+
# None
|
|
803
|
+
if value is None:
|
|
804
|
+
return "[dim]None[/dim]"
|
|
805
|
+
|
|
806
|
+
# Booleans
|
|
807
|
+
elif isinstance(value, bool):
|
|
808
|
+
return f"[cyan]{value}[/cyan]"
|
|
809
|
+
|
|
810
|
+
# Numbers
|
|
811
|
+
elif isinstance(value, (int, float)):
|
|
812
|
+
return f"[cyan]{value}[/cyan]"
|
|
813
|
+
|
|
814
|
+
# Strings
|
|
815
|
+
elif isinstance(value, str):
|
|
816
|
+
return self._format_string_value(value)
|
|
817
|
+
|
|
818
|
+
# Dictionaries
|
|
819
|
+
elif isinstance(value, dict):
|
|
820
|
+
return self._format_dict_value(value)
|
|
821
|
+
|
|
822
|
+
# Lists
|
|
823
|
+
elif isinstance(value, list):
|
|
824
|
+
return self._format_list_value(value)
|
|
825
|
+
|
|
826
|
+
# Functions (like pp helper)
|
|
827
|
+
elif callable(value):
|
|
828
|
+
return f"[dim]<function>[/dim] [dim italic]- helper for pretty printing[/dim italic]"
|
|
829
|
+
|
|
830
|
+
# Other types - just show string representation
|
|
831
|
+
else:
|
|
832
|
+
str_repr = str(value)
|
|
833
|
+
if len(str_repr) <= 100:
|
|
834
|
+
return f"[white]{str_repr}[/white]"
|
|
835
|
+
else:
|
|
836
|
+
return f"[white]{str_repr[:100]}...[/white] [dim]({len(str_repr)} chars)[/dim]"
|
|
837
|
+
|
|
838
|
+
def _format_string_value(self, s: str) -> str:
|
|
839
|
+
"""Format string values with truncation and char count.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
s: String to format
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
Short strings (≤80 chars): repr() with green color
|
|
846
|
+
Long strings (>80 chars): Truncated with "..." and char count
|
|
847
|
+
"""
|
|
848
|
+
# Short strings - show as-is
|
|
849
|
+
if len(s) <= 80:
|
|
850
|
+
return f"[green]{repr(s)}[/green]"
|
|
851
|
+
|
|
852
|
+
# Long strings - truncate and show char count
|
|
853
|
+
truncated = repr(s[:80])[:-1] + "...'" # Remove closing quote, add ellipsis
|
|
854
|
+
return f"[green]{truncated}[/green]\n [dim]({len(s)} chars)[/dim]"
|
|
855
|
+
|
|
856
|
+
def _format_dict_value(self, d: dict) -> str:
|
|
857
|
+
"""Format dict values using pprint for clean output.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
d: Dictionary to format
|
|
861
|
+
|
|
862
|
+
Returns:
|
|
863
|
+
Empty dict: "{{}}" in dim
|
|
864
|
+
Small dict (≤3 keys, fits inline): Compact cyan format
|
|
865
|
+
Medium dict (≤5 lines): Multi-line with indentation
|
|
866
|
+
Large dict: Collapsed summary with key count and pp() hint
|
|
867
|
+
"""
|
|
868
|
+
if not d:
|
|
869
|
+
return "[dim]{{}}[/dim]"
|
|
870
|
+
|
|
871
|
+
# Use pprint for nice formatting
|
|
872
|
+
pp = pformat(d, width=60, depth=2, compact=True)
|
|
873
|
+
lines = pp.split('\n')
|
|
874
|
+
|
|
875
|
+
# Small dict - show inline
|
|
876
|
+
if len(d) <= 3 and len(lines) == 1 and len(pp) <= 60:
|
|
877
|
+
return f"[cyan]{pp}[/cyan]"
|
|
878
|
+
|
|
879
|
+
# Medium dict - show with indentation
|
|
880
|
+
if len(lines) <= 5:
|
|
881
|
+
formatted_lines = [lines[0]]
|
|
882
|
+
for line in lines[1:]:
|
|
883
|
+
formatted_lines.append(f" {line}")
|
|
884
|
+
return f"[cyan]{chr(10).join(formatted_lines)}[/cyan]"
|
|
885
|
+
|
|
886
|
+
# Large dict - collapse with summary
|
|
887
|
+
return f"[dim cyan]{{... {len(d)} keys}}[/dim cyan] [dim]- type: pp(var_name)[/dim]"
|
|
888
|
+
|
|
889
|
+
def _format_list_value(self, lst: list) -> str:
|
|
890
|
+
"""Format list values using pprint for clean output.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
lst: List to format
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
Empty list: "[]" in dim
|
|
897
|
+
Simple string list (≤5 items, fits inline): Compact format
|
|
898
|
+
Medium list (≤5 lines): Multi-line with indentation
|
|
899
|
+
Large list: Collapsed summary with item count and pp() hint
|
|
900
|
+
"""
|
|
901
|
+
if not lst:
|
|
902
|
+
return "[dim][][/dim]"
|
|
903
|
+
|
|
904
|
+
# Simple list of strings - show inline
|
|
905
|
+
if all(isinstance(item, str) for item in lst) and len(lst) <= 5:
|
|
906
|
+
compact = "[" + ", ".join(f'"{s}"' for s in lst) + "]"
|
|
907
|
+
if len(compact) <= 60:
|
|
908
|
+
return f"[cyan]{compact}[/cyan]"
|
|
909
|
+
|
|
910
|
+
# Use pprint for nice formatting
|
|
911
|
+
pp = pformat(lst, width=60, depth=2, compact=True)
|
|
912
|
+
lines = pp.split('\n')
|
|
913
|
+
|
|
914
|
+
# If fits in a few lines, show it
|
|
915
|
+
if len(lines) <= 5:
|
|
916
|
+
formatted_lines = [lines[0]]
|
|
917
|
+
for line in lines[1:]:
|
|
918
|
+
formatted_lines.append(f" {line}")
|
|
919
|
+
result = chr(10).join(formatted_lines)
|
|
920
|
+
return f"[cyan]{result}[/cyan]"
|
|
921
|
+
|
|
922
|
+
# Large - show summary with hint
|
|
923
|
+
return f"[dim cyan][... {len(lst)} items][/dim cyan] [dim]- type: pp(var_name)[/dim]"
|
|
924
|
+
|
|
925
|
+
def _format_value_preview(self, value: Any) -> str:
|
|
926
|
+
"""Format a value for compact preview display.
|
|
927
|
+
|
|
928
|
+
Used for showing values in constrained spaces like next action previews.
|
|
929
|
+
|
|
930
|
+
Args:
|
|
931
|
+
value: Any value to format
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
Truncated string representation (max 30 chars)
|
|
935
|
+
"""
|
|
936
|
+
if isinstance(value, str):
|
|
937
|
+
return f"'{value[:30]}...'" if len(value) > 30 else f"'{value}'"
|
|
938
|
+
elif isinstance(value, (dict, list)):
|
|
939
|
+
val_str = str(value)
|
|
940
|
+
return f"{val_str[:30]}..." if len(val_str) > 30 else val_str
|
|
941
|
+
else:
|
|
942
|
+
return str(value)
|
|
943
|
+
|
|
944
|
+
def _display_modifications(self, modifications: Dict[str, Any]) -> None:
|
|
945
|
+
"""Display what was modified during REPL session.
|
|
946
|
+
|
|
947
|
+
Shows each modified variable with its new value,
|
|
948
|
+
formatted appropriately for display.
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
modifications: Dict of variable_name -> new_value pairs
|
|
952
|
+
"""
|
|
953
|
+
self.console.print("\n[bold green]✅ Modifications Applied:[/bold green]\n")
|
|
954
|
+
|
|
955
|
+
for key, value in modifications.items():
|
|
956
|
+
# Format the value for display
|
|
957
|
+
if isinstance(value, str):
|
|
958
|
+
formatted = f"'{value}'" if len(value) <= 50 else f"'{value[:50]}...'"
|
|
959
|
+
elif isinstance(value, dict):
|
|
960
|
+
formatted = json.dumps(value, indent=2)[:100]
|
|
961
|
+
else:
|
|
962
|
+
formatted = str(value)
|
|
963
|
+
|
|
964
|
+
self.console.print(f" [yellow]{key}[/yellow] = [cyan]{formatted}[/cyan]")
|
|
965
|
+
|
|
966
|
+
self.console.print()
|
|
967
|
+
|
|
968
|
+
def display_explanation(self, explanation: str, context: BreakpointContext) -> None:
|
|
969
|
+
"""Display AI explanation of why a tool was chosen.
|
|
970
|
+
|
|
971
|
+
Args:
|
|
972
|
+
explanation: The explanation text from the AI
|
|
973
|
+
context: Breakpoint context for displaying relevant info
|
|
974
|
+
"""
|
|
975
|
+
self.console.print("\n")
|
|
976
|
+
|
|
977
|
+
panel = Panel(
|
|
978
|
+
explanation,
|
|
979
|
+
title=f"[bold cyan]🤔 Why {context.tool_name}?[/bold cyan]",
|
|
980
|
+
border_style="cyan",
|
|
981
|
+
padding=(1, 2)
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
self.console.print(panel)
|
|
985
|
+
self.console.print()
|
|
986
|
+
|
|
987
|
+
input("[dim]Press Enter to return to menu...[/dim]")
|
|
988
|
+
|
|
989
|
+
def display_execution_analysis(self, analysis) -> None:
|
|
990
|
+
"""Display post-execution analysis with improvement suggestions.
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
analysis: ExecutionAnalysis object with structured results
|
|
994
|
+
"""
|
|
995
|
+
self.console.print("\n")
|
|
996
|
+
self.console.print("[bold cyan]═══ 📊 Execution Analysis ═══[/bold cyan]\n")
|
|
997
|
+
|
|
998
|
+
# Task completion status
|
|
999
|
+
status_emoji = "✅" if analysis.task_completed else "❌"
|
|
1000
|
+
self.console.print(f"{status_emoji} [bold]Task Completed:[/bold] {analysis.task_completed}")
|
|
1001
|
+
self.console.print(f" {analysis.completion_explanation}\n")
|
|
1002
|
+
|
|
1003
|
+
# Overall quality
|
|
1004
|
+
quality_colors = {
|
|
1005
|
+
"excellent": "green",
|
|
1006
|
+
"good": "cyan",
|
|
1007
|
+
"fair": "yellow",
|
|
1008
|
+
"poor": "red"
|
|
1009
|
+
}
|
|
1010
|
+
quality_color = quality_colors.get(analysis.overall_quality, "white")
|
|
1011
|
+
self.console.print(f"[bold]Quality:[/bold] [{quality_color}]{analysis.overall_quality.upper()}[/{quality_color}]\n")
|
|
1012
|
+
|
|
1013
|
+
# Problems identified
|
|
1014
|
+
if analysis.problems_identified:
|
|
1015
|
+
self.console.print("[bold red]⚠️ Problems Identified:[/bold red]")
|
|
1016
|
+
for i, problem in enumerate(analysis.problems_identified, 1):
|
|
1017
|
+
self.console.print(f" {i}. {problem}")
|
|
1018
|
+
self.console.print()
|
|
1019
|
+
|
|
1020
|
+
# System prompt suggestions
|
|
1021
|
+
if analysis.system_prompt_suggestions:
|
|
1022
|
+
panel = Panel(
|
|
1023
|
+
"\n".join(f"• {suggestion}" for suggestion in analysis.system_prompt_suggestions),
|
|
1024
|
+
title="[bold green]💡 System Prompt Suggestions[/bold green]",
|
|
1025
|
+
border_style="green",
|
|
1026
|
+
padding=(1, 2)
|
|
1027
|
+
)
|
|
1028
|
+
self.console.print(panel)
|
|
1029
|
+
self.console.print()
|
|
1030
|
+
|
|
1031
|
+
# Key insights
|
|
1032
|
+
if analysis.key_insights:
|
|
1033
|
+
self.console.print("[bold magenta]🎯 Key Insights:[/bold magenta]")
|
|
1034
|
+
for i, insight in enumerate(analysis.key_insights, 1):
|
|
1035
|
+
self.console.print(f" {i}. {insight}")
|
|
1036
|
+
self.console.print()
|
|
1037
|
+
|
|
1038
|
+
self.console.print("[dim]" + "═" * 60 + "[/dim]\n")
|
|
1039
|
+
input("[dim]Press Enter to continue...[/dim]")
|