holmesgpt 0.11.5__py3-none-any.whl → 0.12.0__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.
Potentially problematic release.
This version of holmesgpt might be problematic. Click here for more details.
- holmes/__init__.py +1 -1
- holmes/common/env_vars.py +8 -4
- holmes/config.py +52 -13
- holmes/core/investigation_structured_output.py +7 -0
- holmes/core/llm.py +14 -4
- holmes/core/models.py +24 -0
- holmes/core/tool_calling_llm.py +48 -6
- holmes/core/tools.py +7 -4
- holmes/core/toolset_manager.py +24 -5
- holmes/core/tracing.py +224 -0
- holmes/interactive.py +761 -44
- holmes/main.py +59 -127
- holmes/plugins/prompts/_fetch_logs.jinja2 +4 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -10
- holmes/plugins/toolsets/__init__.py +10 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +3 -0
- holmes/plugins/toolsets/datadog/datadog_api.py +161 -0
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +26 -0
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +310 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +51 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +267 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +488 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +689 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +3 -0
- holmes/plugins/toolsets/internet/internet.py +1 -1
- holmes/plugins/toolsets/logging_utils/logging_api.py +9 -3
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +3 -0
- holmes/plugins/toolsets/utils.py +6 -2
- holmes/utils/cache.py +4 -4
- holmes/utils/console/consts.py +2 -0
- holmes/utils/console/logging.py +95 -0
- holmes/utils/console/result.py +37 -0
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0.dist-info}/METADATA +3 -4
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0.dist-info}/RECORD +38 -29
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0.dist-info}/WHEEL +1 -1
- holmes/__init__.py.bak +0 -76
- holmes/plugins/toolsets/datadog.py +0 -153
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0.dist-info}/entry_points.txt +0 -0
holmes/interactive.py
CHANGED
|
@@ -1,40 +1,63 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
import threading
|
|
6
|
+
from collections import defaultdict
|
|
2
7
|
from enum import Enum
|
|
3
|
-
from typing import Optional, List
|
|
4
8
|
from pathlib import Path
|
|
9
|
+
from typing import Optional, List, DefaultDict
|
|
5
10
|
|
|
6
11
|
import typer
|
|
7
12
|
from prompt_toolkit import PromptSession
|
|
8
|
-
from prompt_toolkit.
|
|
9
|
-
from prompt_toolkit.
|
|
13
|
+
from prompt_toolkit.application import Application
|
|
14
|
+
from prompt_toolkit.completion import Completer, Completion, merge_completers
|
|
15
|
+
from prompt_toolkit.completion.filesystem import ExecutableCompleter, PathCompleter
|
|
16
|
+
from prompt_toolkit.history import InMemoryHistory, FileHistory
|
|
17
|
+
from prompt_toolkit.document import Document
|
|
18
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
19
|
+
from prompt_toolkit.layout import Layout
|
|
20
|
+
from prompt_toolkit.layout.containers import HSplit, Window
|
|
21
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
22
|
+
from prompt_toolkit.shortcuts.prompt import CompleteStyle
|
|
10
23
|
from prompt_toolkit.styles import Style
|
|
24
|
+
from prompt_toolkit.widgets import TextArea
|
|
25
|
+
|
|
11
26
|
from rich.console import Console
|
|
12
27
|
from rich.markdown import Markdown, Panel
|
|
13
28
|
|
|
14
29
|
from holmes.core.prompt import build_initial_ask_messages
|
|
15
30
|
from holmes.core.tool_calling_llm import ToolCallingLLM, ToolCallResult
|
|
16
31
|
from holmes.core.tools import pretty_print_toolset_status
|
|
32
|
+
from holmes.core.tracing import DummySpan
|
|
17
33
|
|
|
18
34
|
|
|
19
35
|
class SlashCommands(Enum):
|
|
20
|
-
EXIT = "/exit"
|
|
21
|
-
HELP = "/help"
|
|
22
|
-
RESET = "/reset"
|
|
23
|
-
TOOLS_CONFIG = "/
|
|
24
|
-
TOGGLE_TOOL_OUTPUT =
|
|
25
|
-
|
|
36
|
+
EXIT = ("/exit", "Exit interactive mode")
|
|
37
|
+
HELP = ("/help", "Show help message with all commands")
|
|
38
|
+
RESET = ("/reset", "Reset the conversation context")
|
|
39
|
+
TOOLS_CONFIG = ("/tools", "Show available toolsets and their status")
|
|
40
|
+
TOGGLE_TOOL_OUTPUT = (
|
|
41
|
+
"/auto",
|
|
42
|
+
"Toggle auto-display of tool outputs after responses",
|
|
43
|
+
)
|
|
44
|
+
LAST_OUTPUT = ("/last", "Show all tool outputs from last response")
|
|
45
|
+
CLEAR = ("/clear", "Clear the terminal screen")
|
|
46
|
+
RUN = ("/run", "Run a bash command and optionally share with LLM")
|
|
47
|
+
SHELL = (
|
|
48
|
+
"/shell",
|
|
49
|
+
"Drop into interactive shell, then optionally share session with LLM",
|
|
50
|
+
)
|
|
51
|
+
CONTEXT = ("/context", "Show conversation context size and token count")
|
|
52
|
+
SHOW = ("/show", "Show specific tool output in scrollable view")
|
|
26
53
|
|
|
54
|
+
def __init__(self, command, description):
|
|
55
|
+
self.command = command
|
|
56
|
+
self.description = description
|
|
27
57
|
|
|
28
|
-
SLASH_COMMANDS_REFERENCE = {
|
|
29
|
-
SlashCommands.EXIT.value: "Exit interactive mode",
|
|
30
|
-
SlashCommands.HELP.value: "Show help message with all commands",
|
|
31
|
-
SlashCommands.RESET.value: "Reset the conversation context",
|
|
32
|
-
SlashCommands.TOOLS_CONFIG.value: "Show available toolsets and their status",
|
|
33
|
-
SlashCommands.TOGGLE_TOOL_OUTPUT.value: "Toggle tool output display on/off",
|
|
34
|
-
SlashCommands.SHOW_OUTPUT.value: "Show all tool outputs from last response",
|
|
35
|
-
}
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
SLASH_COMMANDS_REFERENCE = {cmd.command: cmd.description for cmd in SlashCommands}
|
|
60
|
+
ALL_SLASH_COMMANDS = [cmd.command for cmd in SlashCommands]
|
|
38
61
|
|
|
39
62
|
|
|
40
63
|
class SlashCommandCompleter(Completer):
|
|
@@ -52,6 +75,71 @@ class SlashCommandCompleter(Completer):
|
|
|
52
75
|
)
|
|
53
76
|
|
|
54
77
|
|
|
78
|
+
class SmartPathCompleter(Completer):
|
|
79
|
+
"""Path completer that works for relative paths starting with ./ or ../"""
|
|
80
|
+
|
|
81
|
+
def __init__(self):
|
|
82
|
+
self.path_completer = PathCompleter()
|
|
83
|
+
|
|
84
|
+
def get_completions(self, document, complete_event):
|
|
85
|
+
text = document.text_before_cursor
|
|
86
|
+
words = text.split()
|
|
87
|
+
if not words:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
last_word = words[-1]
|
|
91
|
+
# Only complete if the last word looks like a relative path (not absolute paths starting with /)
|
|
92
|
+
if last_word.startswith("./") or last_word.startswith("../"):
|
|
93
|
+
# Create a temporary document with just the path part
|
|
94
|
+
path_doc = Document(last_word, len(last_word))
|
|
95
|
+
|
|
96
|
+
for completion in self.path_completer.get_completions(
|
|
97
|
+
path_doc, complete_event
|
|
98
|
+
):
|
|
99
|
+
yield Completion(
|
|
100
|
+
completion.text,
|
|
101
|
+
start_position=completion.start_position - len(last_word),
|
|
102
|
+
display=completion.display,
|
|
103
|
+
display_meta=completion.display_meta,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ConditionalExecutableCompleter(Completer):
|
|
108
|
+
"""Executable completer that only works after /run commands"""
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self.executable_completer = ExecutableCompleter()
|
|
112
|
+
|
|
113
|
+
def get_completions(self, document, complete_event):
|
|
114
|
+
text = document.text_before_cursor
|
|
115
|
+
|
|
116
|
+
# Only provide executable completion if the line starts with /run
|
|
117
|
+
if text.startswith("/run "):
|
|
118
|
+
# Extract the command part after "/run "
|
|
119
|
+
command_part = text[5:] # Remove "/run "
|
|
120
|
+
|
|
121
|
+
# Only complete the first word (the executable name)
|
|
122
|
+
words = command_part.split()
|
|
123
|
+
if len(words) <= 1: # Only when typing the first word
|
|
124
|
+
# Create a temporary document with just the command part
|
|
125
|
+
cmd_doc = Document(command_part, len(command_part))
|
|
126
|
+
|
|
127
|
+
seen_completions = set()
|
|
128
|
+
for completion in self.executable_completer.get_completions(
|
|
129
|
+
cmd_doc, complete_event
|
|
130
|
+
):
|
|
131
|
+
# Remove duplicates based on text only (display can be FormattedText which is unhashable)
|
|
132
|
+
if completion.text not in seen_completions:
|
|
133
|
+
seen_completions.add(completion.text)
|
|
134
|
+
yield Completion(
|
|
135
|
+
completion.text,
|
|
136
|
+
start_position=completion.start_position
|
|
137
|
+
- len(command_part),
|
|
138
|
+
display=completion.display,
|
|
139
|
+
display_meta=completion.display_meta,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
55
143
|
USER_COLOR = "#DEFCC0" # light green
|
|
56
144
|
AI_COLOR = "#00FFFF" # cyan
|
|
57
145
|
TOOLS_COLOR = "magenta"
|
|
@@ -59,15 +147,18 @@ HELP_COLOR = "cyan" # same as AI_COLOR for now
|
|
|
59
147
|
ERROR_COLOR = "red"
|
|
60
148
|
STATUS_COLOR = "yellow"
|
|
61
149
|
|
|
62
|
-
WELCOME_BANNER = f"[bold {HELP_COLOR}]Welcome to HolmesGPT:[/bold {HELP_COLOR}] Type '{SlashCommands.EXIT.
|
|
150
|
+
WELCOME_BANNER = f"[bold {HELP_COLOR}]Welcome to HolmesGPT:[/bold {HELP_COLOR}] Type '{SlashCommands.EXIT.command}' to exit, '{SlashCommands.HELP.command}' for commands."
|
|
63
151
|
|
|
64
152
|
|
|
65
|
-
def format_tool_call_output(
|
|
153
|
+
def format_tool_call_output(
|
|
154
|
+
tool_call: ToolCallResult, tool_index: Optional[int] = None
|
|
155
|
+
) -> str:
|
|
66
156
|
"""
|
|
67
157
|
Format a single tool call result for display in a rich panel.
|
|
68
158
|
|
|
69
159
|
Args:
|
|
70
160
|
tool_call: ToolCallResult object containing the tool execution result
|
|
161
|
+
tool_index: Optional 1-based index of the tool for /show command
|
|
71
162
|
|
|
72
163
|
Returns:
|
|
73
164
|
Formatted string for display in a rich panel
|
|
@@ -82,26 +173,533 @@ def format_tool_call_output(tool_call: ToolCallResult) -> str:
|
|
|
82
173
|
elif len(output_str) > MAX_CHARS:
|
|
83
174
|
truncated = output_str[:MAX_CHARS].strip()
|
|
84
175
|
remaining_chars = len(output_str) - MAX_CHARS
|
|
85
|
-
|
|
176
|
+
show_hint = f"/show {tool_index}" if tool_index else "/show"
|
|
177
|
+
content = f"[{color}]{truncated}[/{color}]\n\n[dim]... truncated ({remaining_chars:,} more chars) - {show_hint} to view full output[/dim]"
|
|
86
178
|
else:
|
|
87
179
|
content = f"[{color}]{output_str}[/{color}]"
|
|
88
180
|
|
|
89
181
|
return content
|
|
90
182
|
|
|
91
183
|
|
|
92
|
-
def
|
|
184
|
+
def build_modal_title(tool_call: ToolCallResult, wrap_status: str) -> str:
|
|
185
|
+
"""Build modal title with navigation instructions."""
|
|
186
|
+
return f"{tool_call.description} (exit: q, nav: ↑↓/j/k/g/G/d/u/f/b/space, wrap: w [{wrap_status}])"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def handle_show_command(
|
|
190
|
+
show_arg: str, all_tool_calls_history: List[ToolCallResult], console: Console
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Handle the /show command to display tool outputs."""
|
|
193
|
+
if not all_tool_calls_history:
|
|
194
|
+
console.print(
|
|
195
|
+
f"[bold {ERROR_COLOR}]No tool calls available in the conversation.[/bold {ERROR_COLOR}]"
|
|
196
|
+
)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if not show_arg:
|
|
200
|
+
# Show list of available tools
|
|
201
|
+
console.print(
|
|
202
|
+
f"[bold {STATUS_COLOR}]Available tool outputs:[/bold {STATUS_COLOR}]"
|
|
203
|
+
)
|
|
204
|
+
for i, tool_call in enumerate(all_tool_calls_history):
|
|
205
|
+
console.print(f" {i+1}. {tool_call.description}")
|
|
206
|
+
console.print("[dim]Usage: /show <number> or /show <tool_name>[/dim]")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# Find tool by number or name
|
|
210
|
+
tool_to_show = None
|
|
211
|
+
try:
|
|
212
|
+
tool_index = int(show_arg) - 1 # Convert to 0-based index
|
|
213
|
+
if 0 <= tool_index < len(all_tool_calls_history):
|
|
214
|
+
tool_to_show = all_tool_calls_history[tool_index]
|
|
215
|
+
else:
|
|
216
|
+
console.print(
|
|
217
|
+
f"[bold {ERROR_COLOR}]Invalid tool index. Use 1-{len(all_tool_calls_history)}[/bold {ERROR_COLOR}]"
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
except ValueError:
|
|
221
|
+
# Try to find by tool name/description
|
|
222
|
+
for tool_call in all_tool_calls_history:
|
|
223
|
+
if show_arg.lower() in tool_call.description.lower():
|
|
224
|
+
tool_to_show = tool_call
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
if not tool_to_show:
|
|
228
|
+
console.print(
|
|
229
|
+
f"[bold {ERROR_COLOR}]Tool not found: {show_arg}[/bold {ERROR_COLOR}]"
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# Show the tool output in modal
|
|
234
|
+
show_tool_output_modal(tool_to_show, console)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def show_tool_output_modal(tool_call: ToolCallResult, console: Console) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Display a tool output in a scrollable modal window.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
tool_call: ToolCallResult object to display
|
|
243
|
+
console: Rich console (for fallback display)
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
# Get the full output
|
|
247
|
+
output = tool_call.result.get_stringified_data()
|
|
248
|
+
title = build_modal_title(tool_call, "off") # Word wrap starts disabled
|
|
249
|
+
|
|
250
|
+
# Create text area with the output
|
|
251
|
+
text_area = TextArea(
|
|
252
|
+
text=output,
|
|
253
|
+
read_only=True,
|
|
254
|
+
scrollbar=True,
|
|
255
|
+
line_numbers=False,
|
|
256
|
+
wrap_lines=False, # Disable word wrap by default
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Create header
|
|
260
|
+
header = Window(
|
|
261
|
+
FormattedTextControl(title),
|
|
262
|
+
height=1,
|
|
263
|
+
style="reverse",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Create layout
|
|
267
|
+
layout = Layout(
|
|
268
|
+
HSplit(
|
|
269
|
+
[
|
|
270
|
+
header,
|
|
271
|
+
text_area,
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Create key bindings
|
|
277
|
+
bindings = KeyBindings()
|
|
278
|
+
|
|
279
|
+
# Exit commands
|
|
280
|
+
@bindings.add("q")
|
|
281
|
+
@bindings.add("escape")
|
|
282
|
+
def _(event):
|
|
283
|
+
event.app.exit()
|
|
284
|
+
|
|
285
|
+
@bindings.add("c-c")
|
|
286
|
+
def _(event):
|
|
287
|
+
event.app.exit()
|
|
288
|
+
|
|
289
|
+
# Vim/less-like navigation
|
|
290
|
+
@bindings.add("j")
|
|
291
|
+
@bindings.add("down")
|
|
292
|
+
def _(event):
|
|
293
|
+
event.app.layout.focus(text_area)
|
|
294
|
+
text_area.buffer.cursor_down()
|
|
295
|
+
|
|
296
|
+
@bindings.add("k")
|
|
297
|
+
@bindings.add("up")
|
|
298
|
+
def _(event):
|
|
299
|
+
event.app.layout.focus(text_area)
|
|
300
|
+
text_area.buffer.cursor_up()
|
|
301
|
+
|
|
302
|
+
@bindings.add("g")
|
|
303
|
+
@bindings.add("home")
|
|
304
|
+
def _(event):
|
|
305
|
+
event.app.layout.focus(text_area)
|
|
306
|
+
text_area.buffer.cursor_position = 0
|
|
307
|
+
|
|
308
|
+
@bindings.add("G")
|
|
309
|
+
@bindings.add("end")
|
|
310
|
+
def _(event):
|
|
311
|
+
event.app.layout.focus(text_area)
|
|
312
|
+
# Go to last line, then to beginning of that line
|
|
313
|
+
text_area.buffer.cursor_position = len(text_area.buffer.text)
|
|
314
|
+
text_area.buffer.cursor_left(
|
|
315
|
+
count=text_area.buffer.document.cursor_position_col
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@bindings.add("d")
|
|
319
|
+
@bindings.add("c-d")
|
|
320
|
+
@bindings.add("pagedown")
|
|
321
|
+
def _(event):
|
|
322
|
+
event.app.layout.focus(text_area)
|
|
323
|
+
# Get current window height and scroll by half
|
|
324
|
+
window_height = event.app.output.get_size().rows - 1 # -1 for header
|
|
325
|
+
scroll_amount = max(1, window_height // 2)
|
|
326
|
+
for _ in range(scroll_amount):
|
|
327
|
+
text_area.buffer.cursor_down()
|
|
328
|
+
|
|
329
|
+
@bindings.add("u")
|
|
330
|
+
@bindings.add("c-u")
|
|
331
|
+
@bindings.add("pageup")
|
|
332
|
+
def _(event):
|
|
333
|
+
event.app.layout.focus(text_area)
|
|
334
|
+
# Get current window height and scroll by half
|
|
335
|
+
window_height = event.app.output.get_size().rows - 1 # -1 for header
|
|
336
|
+
scroll_amount = max(1, window_height // 2)
|
|
337
|
+
for _ in range(scroll_amount):
|
|
338
|
+
text_area.buffer.cursor_up()
|
|
339
|
+
|
|
340
|
+
@bindings.add("f")
|
|
341
|
+
@bindings.add("c-f")
|
|
342
|
+
@bindings.add("space")
|
|
343
|
+
def _(event):
|
|
344
|
+
event.app.layout.focus(text_area)
|
|
345
|
+
# Get current window height and scroll by full page
|
|
346
|
+
window_height = event.app.output.get_size().rows - 1 # -1 for header
|
|
347
|
+
scroll_amount = max(1, window_height)
|
|
348
|
+
for _ in range(scroll_amount):
|
|
349
|
+
text_area.buffer.cursor_down()
|
|
350
|
+
|
|
351
|
+
@bindings.add("b")
|
|
352
|
+
@bindings.add("c-b")
|
|
353
|
+
def _(event):
|
|
354
|
+
event.app.layout.focus(text_area)
|
|
355
|
+
# Get current window height and scroll by full page
|
|
356
|
+
window_height = event.app.output.get_size().rows - 1 # -1 for header
|
|
357
|
+
scroll_amount = max(1, window_height)
|
|
358
|
+
for _ in range(scroll_amount):
|
|
359
|
+
text_area.buffer.cursor_up()
|
|
360
|
+
|
|
361
|
+
@bindings.add("w")
|
|
362
|
+
def _(event):
|
|
363
|
+
# Toggle word wrap
|
|
364
|
+
text_area.wrap_lines = not text_area.wrap_lines
|
|
365
|
+
# Update the header to show current wrap state
|
|
366
|
+
wrap_status = "on" if text_area.wrap_lines else "off"
|
|
367
|
+
new_title = build_modal_title(tool_call, wrap_status)
|
|
368
|
+
header.content = FormattedTextControl(new_title)
|
|
369
|
+
|
|
370
|
+
# Create and run application
|
|
371
|
+
app: Application = Application(
|
|
372
|
+
layout=layout,
|
|
373
|
+
key_bindings=bindings,
|
|
374
|
+
full_screen=True,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
app.run()
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
# Fallback to regular display
|
|
381
|
+
console.print(f"[bold red]Error showing modal: {e}[/bold red]")
|
|
382
|
+
console.print(format_tool_call_output(tool_call))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def handle_context_command(messages, ai: ToolCallingLLM, console: Console) -> None:
|
|
386
|
+
"""Handle the /context command to show conversation context statistics."""
|
|
387
|
+
if messages is None:
|
|
388
|
+
console.print(
|
|
389
|
+
f"[bold {STATUS_COLOR}]No conversation context yet.[/bold {STATUS_COLOR}]"
|
|
390
|
+
)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
# Calculate context statistics
|
|
394
|
+
total_tokens = ai.llm.count_tokens_for_message(messages)
|
|
395
|
+
max_context_size = ai.llm.get_context_window_size()
|
|
396
|
+
max_output_tokens = ai.llm.get_maximum_output_token()
|
|
397
|
+
available_tokens = max_context_size - total_tokens - max_output_tokens
|
|
398
|
+
|
|
399
|
+
# Analyze token distribution by role and tool calls
|
|
400
|
+
role_token_usage: DefaultDict[str, int] = defaultdict(int)
|
|
401
|
+
tool_token_usage: DefaultDict[str, int] = defaultdict(int)
|
|
402
|
+
tool_call_counts: DefaultDict[str, int] = defaultdict(int)
|
|
403
|
+
|
|
404
|
+
for msg in messages:
|
|
405
|
+
role = msg.get("role", "unknown")
|
|
406
|
+
msg_tokens = ai.llm.count_tokens_for_message([msg])
|
|
407
|
+
role_token_usage[role] += msg_tokens
|
|
408
|
+
|
|
409
|
+
# Track individual tool usage
|
|
410
|
+
if role == "tool":
|
|
411
|
+
tool_name = msg.get("name", "unknown_tool")
|
|
412
|
+
tool_token_usage[tool_name] += msg_tokens
|
|
413
|
+
tool_call_counts[tool_name] += 1
|
|
414
|
+
|
|
415
|
+
# Display context information
|
|
416
|
+
console.print(f"[bold {STATUS_COLOR}]Conversation Context:[/bold {STATUS_COLOR}]")
|
|
417
|
+
console.print(
|
|
418
|
+
f" Context used: {total_tokens:,} / {max_context_size:,} tokens ({(total_tokens / max_context_size) * 100:.1f}%)"
|
|
419
|
+
)
|
|
420
|
+
console.print(
|
|
421
|
+
f" Space remaining: {available_tokens:,} for input ({(available_tokens / max_context_size) * 100:.1f}%) + {max_output_tokens:,} reserved for output ({(max_output_tokens / max_context_size) * 100:.1f}%)"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Show token breakdown by role
|
|
425
|
+
console.print(" Token breakdown:")
|
|
426
|
+
for role in ["system", "user", "assistant", "tool"]:
|
|
427
|
+
if role in role_token_usage:
|
|
428
|
+
tokens = role_token_usage[role]
|
|
429
|
+
percentage = (tokens / total_tokens) * 100 if total_tokens > 0 else 0
|
|
430
|
+
role_name = {
|
|
431
|
+
"system": "system prompt",
|
|
432
|
+
"user": "user messages",
|
|
433
|
+
"assistant": "assistant replies",
|
|
434
|
+
"tool": "tool responses",
|
|
435
|
+
}.get(role, role)
|
|
436
|
+
console.print(f" {role_name}: {tokens:,} tokens ({percentage:.1f}%)")
|
|
437
|
+
|
|
438
|
+
# Show top 4 tools breakdown under tool responses
|
|
439
|
+
if role == "tool" and tool_token_usage:
|
|
440
|
+
sorted_tools = sorted(
|
|
441
|
+
tool_token_usage.items(), key=lambda x: x[1], reverse=True
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Show top 4 tools
|
|
445
|
+
for tool_name, tool_tokens in sorted_tools[:4]:
|
|
446
|
+
tool_percentage = (tool_tokens / tokens) * 100 if tokens > 0 else 0
|
|
447
|
+
call_count = tool_call_counts[tool_name]
|
|
448
|
+
console.print(
|
|
449
|
+
f" {tool_name}: {tool_tokens:,} tokens ({tool_percentage:.1f}%) from {call_count} tool calls"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Show "other" category if there are more than 4 tools
|
|
453
|
+
if len(sorted_tools) > 4:
|
|
454
|
+
other_tokens = sum(
|
|
455
|
+
tool_tokens for _, tool_tokens in sorted_tools[4:]
|
|
456
|
+
)
|
|
457
|
+
other_calls = sum(
|
|
458
|
+
tool_call_counts[tool_name] for tool_name, _ in sorted_tools[4:]
|
|
459
|
+
)
|
|
460
|
+
other_percentage = (
|
|
461
|
+
(other_tokens / tokens) * 100 if tokens > 0 else 0
|
|
462
|
+
)
|
|
463
|
+
other_count = len(sorted_tools) - 4
|
|
464
|
+
console.print(
|
|
465
|
+
f" other ({other_count} tools): {other_tokens:,} tokens ({other_percentage:.1f}%) from {other_calls} tool calls"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if available_tokens < 0:
|
|
469
|
+
console.print(
|
|
470
|
+
f"[bold {ERROR_COLOR}]⚠️ Context will be truncated on next LLM call[/bold {ERROR_COLOR}]"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def prompt_for_llm_sharing(
|
|
475
|
+
session: PromptSession, style: Style, content: str, content_type: str
|
|
476
|
+
) -> Optional[str]:
|
|
477
|
+
"""
|
|
478
|
+
Prompt user to share content with LLM and return formatted user input.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
session: PromptSession for user input
|
|
482
|
+
style: Style for prompts
|
|
483
|
+
content: The content to potentially share (command output, shell session, etc.)
|
|
484
|
+
content_type: Description of content type (e.g., "command", "shell session")
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Formatted user input string if user chooses to share, None otherwise
|
|
488
|
+
"""
|
|
489
|
+
# Create a temporary session without history for y/n prompts
|
|
490
|
+
temp_session = PromptSession(history=InMemoryHistory()) # type: ignore
|
|
491
|
+
|
|
492
|
+
share_prompt = temp_session.prompt(
|
|
493
|
+
[("class:prompt", f"Share {content_type} with LLM? (Y/n): ")], style=style
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if not share_prompt.lower().startswith("n"):
|
|
497
|
+
comment_prompt = temp_session.prompt(
|
|
498
|
+
[("class:prompt", "Optional comment/question (press Enter to skip): ")],
|
|
499
|
+
style=style,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
user_input = f"I {content_type}:\\n\\n```\\n{content}\\n```\\n\\n"
|
|
503
|
+
|
|
504
|
+
if comment_prompt.strip():
|
|
505
|
+
user_input += f"Comment/Question: {comment_prompt.strip()}"
|
|
506
|
+
|
|
507
|
+
return user_input
|
|
508
|
+
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def handle_run_command(
|
|
513
|
+
bash_command: str, session: PromptSession, style: Style, console: Console
|
|
514
|
+
) -> Optional[str]:
|
|
515
|
+
"""
|
|
516
|
+
Handle the /run command to execute a bash command.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
bash_command: The bash command to execute
|
|
520
|
+
session: PromptSession for user input
|
|
521
|
+
style: Style for prompts
|
|
522
|
+
console: Rich console for output
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Formatted user input string if user chooses to share, None otherwise
|
|
526
|
+
"""
|
|
527
|
+
if not bash_command:
|
|
528
|
+
console.print(
|
|
529
|
+
f"[bold {ERROR_COLOR}]Usage: /run <bash_command>[/bold {ERROR_COLOR}]"
|
|
530
|
+
)
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
result = None
|
|
534
|
+
output = ""
|
|
535
|
+
error_message = ""
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
console.print(
|
|
539
|
+
f"[bold {STATUS_COLOR}]Running: {bash_command}[/bold {STATUS_COLOR}]"
|
|
540
|
+
)
|
|
541
|
+
result = subprocess.run(
|
|
542
|
+
bash_command, shell=True, capture_output=True, text=True
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
output = result.stdout + result.stderr
|
|
546
|
+
if result.returncode == 0:
|
|
547
|
+
console.print(
|
|
548
|
+
f"[bold green]✓ Command succeeded (exit code: {result.returncode})[/bold green]"
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
console.print(
|
|
552
|
+
f"[bold {ERROR_COLOR}]✗ Command failed (exit code: {result.returncode})[/bold {ERROR_COLOR}]"
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
if output.strip():
|
|
556
|
+
console.print(
|
|
557
|
+
Panel(
|
|
558
|
+
output,
|
|
559
|
+
padding=(1, 2),
|
|
560
|
+
border_style="white",
|
|
561
|
+
title="Command Output",
|
|
562
|
+
title_align="left",
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
except KeyboardInterrupt:
|
|
567
|
+
error_message = "Command interrupted by user"
|
|
568
|
+
console.print(f"[bold {ERROR_COLOR}]{error_message}[/bold {ERROR_COLOR}]")
|
|
569
|
+
except Exception as e:
|
|
570
|
+
error_message = f"Error running command: {e}"
|
|
571
|
+
console.print(f"[bold {ERROR_COLOR}]{error_message}[/bold {ERROR_COLOR}]")
|
|
572
|
+
|
|
573
|
+
# Build command output for sharing
|
|
574
|
+
command_output = f"ran the command: `{bash_command}`\n\n"
|
|
575
|
+
if result is not None:
|
|
576
|
+
command_output += f"Exit code: {result.returncode}\n\n"
|
|
577
|
+
if output.strip():
|
|
578
|
+
command_output += f"Output:\n{output}"
|
|
579
|
+
elif error_message:
|
|
580
|
+
command_output += f"Error: {error_message}"
|
|
581
|
+
|
|
582
|
+
return prompt_for_llm_sharing(session, style, command_output, "ran a command")
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def handle_shell_command(
|
|
586
|
+
session: PromptSession, style: Style, console: Console
|
|
587
|
+
) -> Optional[str]:
|
|
93
588
|
"""
|
|
94
|
-
|
|
589
|
+
Handle the /shell command to start an interactive shell session.
|
|
95
590
|
|
|
96
591
|
Args:
|
|
97
|
-
|
|
592
|
+
session: PromptSession for user input
|
|
593
|
+
style: Style for prompts
|
|
98
594
|
console: Rich console for output
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Formatted user input string if user chooses to share, None otherwise
|
|
99
598
|
"""
|
|
599
|
+
console.print(
|
|
600
|
+
f"[bold {STATUS_COLOR}]Starting interactive shell. Type 'exit' to return to HolmesGPT.[/bold {STATUS_COLOR}]"
|
|
601
|
+
)
|
|
602
|
+
console.print(
|
|
603
|
+
"[dim]Shell session will be recorded and can be shared with LLM when you exit.[/dim]"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Create a temporary file to capture shell session
|
|
607
|
+
with tempfile.NamedTemporaryFile(mode="w+", suffix=".log") as session_file:
|
|
608
|
+
session_log_path = session_file.name
|
|
609
|
+
|
|
610
|
+
try:
|
|
611
|
+
# Start shell with script command to capture session
|
|
612
|
+
shell_env = os.environ.copy()
|
|
613
|
+
shell_env["PS1"] = "\\u@\\h:\\w$ " # Set a clean prompt
|
|
614
|
+
|
|
615
|
+
subprocess.run(f"script -q {session_log_path}", shell=True, env=shell_env)
|
|
616
|
+
|
|
617
|
+
# Read the session log
|
|
618
|
+
session_output = ""
|
|
619
|
+
try:
|
|
620
|
+
with open(session_log_path, "r") as f:
|
|
621
|
+
session_output = f.read()
|
|
622
|
+
except Exception as e:
|
|
623
|
+
console.print(
|
|
624
|
+
f"[bold {ERROR_COLOR}]Error reading session log: {e}[/bold {ERROR_COLOR}]"
|
|
625
|
+
)
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
if session_output.strip():
|
|
629
|
+
console.print(
|
|
630
|
+
f"[bold {STATUS_COLOR}]Shell session ended.[/bold {STATUS_COLOR}]"
|
|
631
|
+
)
|
|
632
|
+
return prompt_for_llm_sharing(
|
|
633
|
+
session, style, session_output, "had an interactive shell session"
|
|
634
|
+
)
|
|
635
|
+
else:
|
|
636
|
+
console.print(
|
|
637
|
+
f"[bold {STATUS_COLOR}]Shell session ended with no output.[/bold {STATUS_COLOR}]"
|
|
638
|
+
)
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
except KeyboardInterrupt:
|
|
642
|
+
console.print(
|
|
643
|
+
f"[bold {STATUS_COLOR}]Shell session interrupted.[/bold {STATUS_COLOR}]"
|
|
644
|
+
)
|
|
645
|
+
return None
|
|
646
|
+
except Exception as e:
|
|
647
|
+
console.print(
|
|
648
|
+
f"[bold {ERROR_COLOR}]Error starting shell: {e}[/bold {ERROR_COLOR}]"
|
|
649
|
+
)
|
|
650
|
+
return None
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def find_tool_index_in_history(
|
|
654
|
+
tool_call: ToolCallResult, all_tool_calls_history: List[ToolCallResult]
|
|
655
|
+
) -> Optional[int]:
|
|
656
|
+
"""Find the 1-based index of a tool call in the complete history."""
|
|
657
|
+
for i, historical_tool in enumerate(all_tool_calls_history):
|
|
658
|
+
if historical_tool.tool_call_id == tool_call.tool_call_id:
|
|
659
|
+
return i + 1 # 1-based index
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def handle_last_command(
|
|
664
|
+
last_response, console: Console, all_tool_calls_history: List[ToolCallResult]
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Handle the /last command to show recent tool outputs."""
|
|
667
|
+
if last_response is None or not last_response.tool_calls:
|
|
668
|
+
console.print(
|
|
669
|
+
f"[bold {ERROR_COLOR}]No tool calls available from the last response.[/bold {ERROR_COLOR}]"
|
|
670
|
+
)
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
console.print(
|
|
674
|
+
f"[bold {TOOLS_COLOR}]Used {len(last_response.tool_calls)} tools[/bold {TOOLS_COLOR}]"
|
|
675
|
+
)
|
|
676
|
+
for tool_call in last_response.tool_calls:
|
|
677
|
+
tool_index = find_tool_index_in_history(tool_call, all_tool_calls_history)
|
|
678
|
+
preview_output = format_tool_call_output(tool_call, tool_index)
|
|
679
|
+
title = f"{tool_call.result.status.to_emoji()} {tool_call.description} -> returned {tool_call.result.return_code}"
|
|
680
|
+
|
|
681
|
+
console.print(
|
|
682
|
+
Panel(
|
|
683
|
+
preview_output,
|
|
684
|
+
padding=(1, 2),
|
|
685
|
+
border_style=TOOLS_COLOR,
|
|
686
|
+
title=title,
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def display_recent_tool_outputs(
|
|
692
|
+
tool_calls: List[ToolCallResult],
|
|
693
|
+
console: Console,
|
|
694
|
+
all_tool_calls_history: List[ToolCallResult],
|
|
695
|
+
) -> None:
|
|
696
|
+
"""Display recent tool outputs in rich panels (for auto-display after responses)."""
|
|
100
697
|
console.print(
|
|
101
698
|
f"[bold {TOOLS_COLOR}]Used {len(tool_calls)} tools[/bold {TOOLS_COLOR}]"
|
|
102
699
|
)
|
|
103
700
|
for tool_call in tool_calls:
|
|
104
|
-
|
|
701
|
+
tool_index = find_tool_index_in_history(tool_call, all_tool_calls_history)
|
|
702
|
+
preview_output = format_tool_call_output(tool_call, tool_index)
|
|
105
703
|
title = f"{tool_call.result.status.to_emoji()} {tool_call.description} -> returned {tool_call.result.return_code}"
|
|
106
704
|
|
|
107
705
|
console.print(
|
|
@@ -122,21 +720,75 @@ def run_interactive_loop(
|
|
|
122
720
|
include_files: Optional[List[Path]],
|
|
123
721
|
post_processing_prompt: Optional[str],
|
|
124
722
|
show_tool_output: bool,
|
|
723
|
+
tracer=None,
|
|
125
724
|
) -> None:
|
|
725
|
+
# Initialize tracer - use DummySpan if no tracer provided
|
|
726
|
+
if tracer is None:
|
|
727
|
+
tracer = DummySpan()
|
|
728
|
+
|
|
126
729
|
style = Style.from_dict(
|
|
127
730
|
{
|
|
128
731
|
"prompt": USER_COLOR,
|
|
732
|
+
"bottom-toolbar": "#000000 bg:#ff0000",
|
|
733
|
+
"bottom-toolbar.text": "#aaaa44 bg:#aa4444",
|
|
129
734
|
}
|
|
130
735
|
)
|
|
131
736
|
|
|
132
|
-
|
|
133
|
-
|
|
737
|
+
# Create merged completer with slash commands, conditional executables, and smart paths
|
|
738
|
+
slash_completer = SlashCommandCompleter()
|
|
739
|
+
executable_completer = ConditionalExecutableCompleter()
|
|
740
|
+
path_completer = SmartPathCompleter()
|
|
741
|
+
|
|
742
|
+
command_completer = merge_completers(
|
|
743
|
+
[slash_completer, executable_completer, path_completer]
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Use file-based history
|
|
747
|
+
history_file = os.path.expanduser("~/.holmes/history")
|
|
748
|
+
os.makedirs(os.path.dirname(history_file), exist_ok=True)
|
|
749
|
+
history = FileHistory(history_file)
|
|
134
750
|
if initial_user_input:
|
|
135
751
|
history.append_string(initial_user_input)
|
|
752
|
+
|
|
753
|
+
# Create custom key bindings for Ctrl+C behavior
|
|
754
|
+
bindings = KeyBindings()
|
|
755
|
+
status_message = ""
|
|
756
|
+
|
|
757
|
+
@bindings.add("c-c")
|
|
758
|
+
def _(event):
|
|
759
|
+
"""Handle Ctrl+C: clear input if text exists, otherwise quit."""
|
|
760
|
+
buffer = event.app.current_buffer
|
|
761
|
+
if buffer.text:
|
|
762
|
+
nonlocal status_message
|
|
763
|
+
status_message = f"Input cleared. Use {SlashCommands.EXIT.command} or Ctrl+C again to quit."
|
|
764
|
+
buffer.reset()
|
|
765
|
+
|
|
766
|
+
# call timer to clear status message after 3 seconds
|
|
767
|
+
def clear_status():
|
|
768
|
+
nonlocal status_message
|
|
769
|
+
status_message = ""
|
|
770
|
+
event.app.invalidate()
|
|
771
|
+
|
|
772
|
+
timer = threading.Timer(3, clear_status)
|
|
773
|
+
timer.start()
|
|
774
|
+
else:
|
|
775
|
+
# Quit if no text
|
|
776
|
+
raise KeyboardInterrupt()
|
|
777
|
+
|
|
778
|
+
def get_bottom_toolbar():
|
|
779
|
+
if status_message:
|
|
780
|
+
return [("bg:#ff0000 fg:#000000", status_message)]
|
|
781
|
+
return None
|
|
782
|
+
|
|
136
783
|
session = PromptSession(
|
|
137
784
|
completer=command_completer,
|
|
138
785
|
history=history,
|
|
786
|
+
complete_style=CompleteStyle.COLUMN,
|
|
787
|
+
reserve_space_for_menu=12,
|
|
788
|
+
key_bindings=bindings,
|
|
789
|
+
bottom_toolbar=get_bottom_toolbar,
|
|
139
790
|
) # type: ignore
|
|
791
|
+
|
|
140
792
|
input_prompt = [("class:prompt", "User: ")]
|
|
141
793
|
|
|
142
794
|
console.print(WELCOME_BANNER)
|
|
@@ -146,6 +798,9 @@ def run_interactive_loop(
|
|
|
146
798
|
)
|
|
147
799
|
messages = None
|
|
148
800
|
last_response = None
|
|
801
|
+
all_tool_calls_history: List[
|
|
802
|
+
ToolCallResult
|
|
803
|
+
] = [] # Track all tool calls throughout conversation
|
|
149
804
|
|
|
150
805
|
while True:
|
|
151
806
|
try:
|
|
@@ -156,40 +811,78 @@ def run_interactive_loop(
|
|
|
156
811
|
user_input = session.prompt(input_prompt, style=style) # type: ignore
|
|
157
812
|
|
|
158
813
|
if user_input.startswith("/"):
|
|
159
|
-
|
|
160
|
-
|
|
814
|
+
original_input = user_input.strip()
|
|
815
|
+
command = original_input.lower()
|
|
816
|
+
|
|
817
|
+
# Handle prefix matching for slash commands
|
|
818
|
+
matches = [cmd for cmd in ALL_SLASH_COMMANDS if cmd.startswith(command)]
|
|
819
|
+
if len(matches) == 1:
|
|
820
|
+
command = matches[0]
|
|
821
|
+
elif len(matches) > 1:
|
|
822
|
+
console.print(
|
|
823
|
+
f"[bold {ERROR_COLOR}]Ambiguous command '{command}'. Matches: {', '.join(matches)}[/bold {ERROR_COLOR}]"
|
|
824
|
+
)
|
|
825
|
+
continue
|
|
826
|
+
|
|
827
|
+
if command == SlashCommands.EXIT.command:
|
|
161
828
|
return
|
|
162
|
-
elif command == SlashCommands.HELP.
|
|
829
|
+
elif command == SlashCommands.HELP.command:
|
|
163
830
|
console.print(
|
|
164
831
|
f"[bold {HELP_COLOR}]Available commands:[/bold {HELP_COLOR}]"
|
|
165
832
|
)
|
|
166
833
|
for cmd, description in SLASH_COMMANDS_REFERENCE.items():
|
|
167
834
|
console.print(f" [bold]{cmd}[/bold] - {description}")
|
|
835
|
+
continue
|
|
168
836
|
elif command == SlashCommands.RESET.value:
|
|
169
837
|
console.print(
|
|
170
838
|
f"[bold {STATUS_COLOR}]Context reset. You can now ask a new question.[/bold {STATUS_COLOR}]"
|
|
171
839
|
)
|
|
172
840
|
messages = None
|
|
841
|
+
last_response = None
|
|
842
|
+
all_tool_calls_history.clear()
|
|
173
843
|
continue
|
|
174
|
-
elif command == SlashCommands.TOOLS_CONFIG.
|
|
844
|
+
elif command == SlashCommands.TOOLS_CONFIG.command:
|
|
175
845
|
pretty_print_toolset_status(ai.tool_executor.toolsets, console)
|
|
176
|
-
|
|
846
|
+
continue
|
|
847
|
+
elif command == SlashCommands.TOGGLE_TOOL_OUTPUT.command:
|
|
177
848
|
show_tool_output = not show_tool_output
|
|
178
849
|
status = "enabled" if show_tool_output else "disabled"
|
|
179
850
|
console.print(
|
|
180
|
-
f"[bold yellow]
|
|
851
|
+
f"[bold yellow]Auto-display of tool outputs {status}.[/bold yellow]"
|
|
181
852
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
853
|
+
continue
|
|
854
|
+
elif command == SlashCommands.LAST_OUTPUT.command:
|
|
855
|
+
handle_last_command(last_response, console, all_tool_calls_history)
|
|
856
|
+
continue
|
|
857
|
+
elif command == SlashCommands.CLEAR.command:
|
|
858
|
+
console.clear()
|
|
859
|
+
continue
|
|
860
|
+
elif command == SlashCommands.CONTEXT.command:
|
|
861
|
+
handle_context_command(messages, ai, console)
|
|
862
|
+
continue
|
|
863
|
+
elif command.startswith(SlashCommands.SHOW.command):
|
|
864
|
+
# Parse the command to extract tool index or name
|
|
865
|
+
show_arg = original_input[len(SlashCommands.SHOW.command) :].strip()
|
|
866
|
+
handle_show_command(show_arg, all_tool_calls_history, console)
|
|
867
|
+
continue
|
|
868
|
+
elif command.startswith(SlashCommands.RUN.command):
|
|
869
|
+
bash_command = original_input[
|
|
870
|
+
len(SlashCommands.RUN.command) :
|
|
871
|
+
].strip()
|
|
872
|
+
shared_input = handle_run_command(
|
|
873
|
+
bash_command, session, style, console
|
|
874
|
+
)
|
|
875
|
+
if shared_input is None:
|
|
876
|
+
continue # User chose not to share, continue to next input
|
|
877
|
+
user_input = shared_input
|
|
878
|
+
elif command == SlashCommands.SHELL.command:
|
|
879
|
+
shared_input = handle_shell_command(session, style, console)
|
|
880
|
+
if shared_input is None:
|
|
881
|
+
continue # User chose not to share or no output, continue to next input
|
|
882
|
+
user_input = shared_input
|
|
190
883
|
else:
|
|
191
884
|
console.print(f"Unknown command: {command}")
|
|
192
|
-
|
|
885
|
+
continue
|
|
193
886
|
elif not user_input.strip():
|
|
194
887
|
continue
|
|
195
888
|
|
|
@@ -201,12 +894,31 @@ def run_interactive_loop(
|
|
|
201
894
|
messages.append({"role": "user", "content": user_input})
|
|
202
895
|
|
|
203
896
|
console.print(f"\n[bold {AI_COLOR}]Thinking...[/bold {AI_COLOR}]\n")
|
|
204
|
-
|
|
897
|
+
|
|
898
|
+
with tracer.start_trace(user_input) as trace_span:
|
|
899
|
+
# Log the user's question as input to the top-level span
|
|
900
|
+
trace_span.log(
|
|
901
|
+
input=user_input,
|
|
902
|
+
metadata={"type": "user_question"},
|
|
903
|
+
)
|
|
904
|
+
response = ai.call(
|
|
905
|
+
messages, post_processing_prompt, trace_span=trace_span
|
|
906
|
+
)
|
|
907
|
+
trace_span.log(
|
|
908
|
+
output=response.result,
|
|
909
|
+
)
|
|
910
|
+
trace_url = tracer.get_trace_url()
|
|
911
|
+
|
|
205
912
|
messages = response.messages # type: ignore
|
|
206
913
|
last_response = response
|
|
207
914
|
|
|
915
|
+
if response.tool_calls:
|
|
916
|
+
all_tool_calls_history.extend(response.tool_calls)
|
|
917
|
+
|
|
208
918
|
if show_tool_output and response.tool_calls:
|
|
209
|
-
|
|
919
|
+
display_recent_tool_outputs(
|
|
920
|
+
response.tool_calls, console, all_tool_calls_history
|
|
921
|
+
)
|
|
210
922
|
console.print(
|
|
211
923
|
Panel(
|
|
212
924
|
Markdown(f"{response.result}"),
|
|
@@ -216,6 +928,10 @@ def run_interactive_loop(
|
|
|
216
928
|
title_align="left",
|
|
217
929
|
)
|
|
218
930
|
)
|
|
931
|
+
|
|
932
|
+
if trace_url:
|
|
933
|
+
console.print(f"🔍 View trace: {trace_url}")
|
|
934
|
+
|
|
219
935
|
console.print("")
|
|
220
936
|
except typer.Abort:
|
|
221
937
|
break
|
|
@@ -224,6 +940,7 @@ def run_interactive_loop(
|
|
|
224
940
|
except Exception as e:
|
|
225
941
|
logging.error("An error occurred during interactive mode:", exc_info=e)
|
|
226
942
|
console.print(f"[bold {ERROR_COLOR}]Error: {e}[/bold {ERROR_COLOR}]")
|
|
943
|
+
|
|
227
944
|
console.print(
|
|
228
945
|
f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
|
|
229
946
|
)
|