holmesgpt 0.13.2__py3-none-any.whl → 0.18.4__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.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +20 -6
- holmes/common/env_vars.py +58 -3
- holmes/common/openshift.py +1 -1
- holmes/config.py +123 -148
- holmes/core/conversations.py +71 -15
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +31 -39
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +508 -88
- holmes/core/models.py +108 -4
- holmes/core/openai_formatting.py +14 -1
- holmes/core/prompt.py +48 -3
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +8 -6
- holmes/core/supabase_dal.py +295 -100
- holmes/core/tool_calling_llm.py +489 -428
- holmes/core/tools.py +325 -56
- holmes/core/tools_utils/token_counting.py +21 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
- holmes/core/tools_utils/tool_executor.py +0 -13
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +191 -5
- holmes/core/tracing.py +19 -3
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +63 -0
- holmes/core/transformers/llm_summarize.py +175 -0
- holmes/core/transformers/registry.py +123 -0
- holmes/core/transformers/transformer.py +32 -0
- holmes/core/truncation/compaction.py +94 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +219 -0
- holmes/interactive.py +228 -31
- holmes/main.py +23 -40
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
- holmes/plugins/prompts/generic_ask.jinja2 +0 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
- holmes/plugins/runbooks/__init__.py +145 -17
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +44 -27
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/utils.py +0 -32
- holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
- holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
- holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
- holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
- holmes/plugins/toolsets/bash/common/bash.py +23 -13
- holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
- holmes/plugins/toolsets/bash/common/stringify.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
- holmes/plugins/toolsets/bash/parse_command.py +12 -13
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/connectivity_check.py +124 -0
- holmes/plugins/toolsets/coralogix/api.py +132 -119
- holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
- holmes/plugins/toolsets/coralogix/utils.py +15 -79
- holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
- holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
- holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/git.py +54 -50
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +13 -29
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
- holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
- holmes/plugins/toolsets/internet/internet.py +15 -16
- holmes/plugins/toolsets/internet/notion.py +9 -11
- holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +36 -42
- holmes/plugins/toolsets/kubernetes.yaml +317 -113
- holmes/plugins/toolsets/kubernetes_logs.py +9 -9
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
- holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
- holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
- holmes/plugins/toolsets/robusta/robusta.py +239 -68
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +60 -11
- holmes/utils/holmes_status.py +6 -4
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/log.py +15 -0
- holmes/utils/markdown_utils.py +2 -3
- holmes/utils/memory_limit.py +58 -0
- holmes/utils/sentry_helper.py +64 -0
- holmes/utils/stream.py +69 -8
- holmes/utils/tags.py +4 -3
- holmes/version.py +37 -15
- holmesgpt-0.18.4.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
- holmes/plugins/toolsets/newrelic.py +0 -231
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- holmesgpt-0.13.2.dist-info/RECORD +0 -234
- /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
holmes/interactive.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
import subprocess
|
|
4
5
|
import tempfile
|
|
5
6
|
import threading
|
|
@@ -26,11 +27,18 @@ from prompt_toolkit.widgets import TextArea
|
|
|
26
27
|
from pygments.lexers import guess_lexer
|
|
27
28
|
from rich.console import Console
|
|
28
29
|
from rich.markdown import Markdown, Panel
|
|
30
|
+
from rich.markup import escape
|
|
29
31
|
|
|
30
32
|
from holmes.common.env_vars import ENABLE_CLI_TOOL_APPROVAL
|
|
31
33
|
from holmes.core.config import config_path_dir
|
|
34
|
+
from holmes.core.feedback import (
|
|
35
|
+
PRIVACY_NOTICE_BANNER,
|
|
36
|
+
Feedback,
|
|
37
|
+
FeedbackCallback,
|
|
38
|
+
UserFeedback,
|
|
39
|
+
)
|
|
32
40
|
from holmes.core.prompt import build_initial_ask_messages
|
|
33
|
-
from holmes.core.tool_calling_llm import ToolCallingLLM, ToolCallResult
|
|
41
|
+
from holmes.core.tool_calling_llm import LLMResult, ToolCallingLLM, ToolCallResult
|
|
34
42
|
from holmes.core.tools import StructuredToolResult, pretty_print_toolset_status
|
|
35
43
|
from holmes.core.tracing import DummyTracer
|
|
36
44
|
from holmes.utils.colors import (
|
|
@@ -42,6 +50,7 @@ from holmes.utils.colors import (
|
|
|
42
50
|
USER_COLOR,
|
|
43
51
|
)
|
|
44
52
|
from holmes.utils.console.consts import agent_name
|
|
53
|
+
from holmes.utils.file_utils import write_json_file
|
|
45
54
|
from holmes.version import check_version_async
|
|
46
55
|
|
|
47
56
|
|
|
@@ -62,19 +71,25 @@ class SlashCommands(Enum):
|
|
|
62
71
|
)
|
|
63
72
|
CONTEXT = ("/context", "Show conversation context size and token count")
|
|
64
73
|
SHOW = ("/show", "Show specific tool output in scrollable view")
|
|
74
|
+
FEEDBACK = ("/feedback", "Provide feedback on the agent's response")
|
|
65
75
|
|
|
66
76
|
def __init__(self, command, description):
|
|
67
77
|
self.command = command
|
|
68
78
|
self.description = description
|
|
69
79
|
|
|
70
80
|
|
|
71
|
-
SLASH_COMMANDS_REFERENCE = {cmd.command: cmd.description for cmd in SlashCommands}
|
|
72
|
-
ALL_SLASH_COMMANDS = [cmd.command for cmd in SlashCommands]
|
|
73
|
-
|
|
74
|
-
|
|
75
81
|
class SlashCommandCompleter(Completer):
|
|
76
|
-
def __init__(self):
|
|
77
|
-
|
|
82
|
+
def __init__(self, unsupported_commands: Optional[List[str]] = None):
|
|
83
|
+
# Build commands dictionary, excluding unsupported commands
|
|
84
|
+
all_commands = {cmd.command: cmd.description for cmd in SlashCommands}
|
|
85
|
+
if unsupported_commands:
|
|
86
|
+
self.commands = {
|
|
87
|
+
cmd: desc
|
|
88
|
+
for cmd, desc in all_commands.items()
|
|
89
|
+
if cmd not in unsupported_commands
|
|
90
|
+
}
|
|
91
|
+
else:
|
|
92
|
+
self.commands = all_commands
|
|
78
93
|
|
|
79
94
|
def get_completions(self, document, complete_event):
|
|
80
95
|
text = document.text_before_cursor
|
|
@@ -233,6 +248,13 @@ def build_modal_title(tool_call: ToolCallResult, wrap_status: str) -> str:
|
|
|
233
248
|
return f"{tool_call.description} (exit: q, nav: ↑↓/j/k/g/G/d/u/f/b/space, wrap: w [{wrap_status}])"
|
|
234
249
|
|
|
235
250
|
|
|
251
|
+
def strip_ansi_codes(text: str) -> str:
|
|
252
|
+
ansi_escape_pattern = re.compile(
|
|
253
|
+
r"\x1b\[[0-9;]*[a-zA-Z]|\033\[[0-9;]*[a-zA-Z]|\^\[\[[0-9;]*[a-zA-Z]"
|
|
254
|
+
)
|
|
255
|
+
return ansi_escape_pattern.sub("", text)
|
|
256
|
+
|
|
257
|
+
|
|
236
258
|
def detect_lexer(content: str) -> Optional[PygmentsLexer]:
|
|
237
259
|
"""
|
|
238
260
|
Detect appropriate lexer for content using Pygments' built-in detection.
|
|
@@ -314,6 +336,7 @@ def show_tool_output_modal(tool_call: ToolCallResult, console: Console) -> None:
|
|
|
314
336
|
try:
|
|
315
337
|
# Get the full output
|
|
316
338
|
output = tool_call.result.get_stringified_data()
|
|
339
|
+
output = strip_ansi_codes(output)
|
|
317
340
|
title = build_modal_title(tool_call, "off") # Word wrap starts disabled
|
|
318
341
|
|
|
319
342
|
# Detect appropriate syntax highlighting
|
|
@@ -467,10 +490,14 @@ def handle_context_command(messages, ai: ToolCallingLLM, console: Console) -> No
|
|
|
467
490
|
return
|
|
468
491
|
|
|
469
492
|
# Calculate context statistics
|
|
470
|
-
|
|
493
|
+
tokens_metadata = ai.llm.count_tokens(
|
|
494
|
+
messages
|
|
495
|
+
) # TODO: pass tools to also count tokens used by input tools
|
|
471
496
|
max_context_size = ai.llm.get_context_window_size()
|
|
472
497
|
max_output_tokens = ai.llm.get_maximum_output_token()
|
|
473
|
-
available_tokens =
|
|
498
|
+
available_tokens = (
|
|
499
|
+
max_context_size - tokens_metadata.total_tokens - max_output_tokens
|
|
500
|
+
)
|
|
474
501
|
|
|
475
502
|
# Analyze token distribution by role and tool calls
|
|
476
503
|
role_token_usage: DefaultDict[str, int] = defaultdict(int)
|
|
@@ -479,19 +506,21 @@ def handle_context_command(messages, ai: ToolCallingLLM, console: Console) -> No
|
|
|
479
506
|
|
|
480
507
|
for msg in messages:
|
|
481
508
|
role = msg.get("role", "unknown")
|
|
482
|
-
|
|
483
|
-
|
|
509
|
+
message_tokens = ai.llm.count_tokens(
|
|
510
|
+
[msg]
|
|
511
|
+
) # TODO: pass tools to also count tokens used by input tools
|
|
512
|
+
role_token_usage[role] += message_tokens.total_tokens
|
|
484
513
|
|
|
485
514
|
# Track individual tool usage
|
|
486
515
|
if role == "tool":
|
|
487
516
|
tool_name = msg.get("name", "unknown_tool")
|
|
488
|
-
tool_token_usage[tool_name] +=
|
|
517
|
+
tool_token_usage[tool_name] += message_tokens.total_tokens
|
|
489
518
|
tool_call_counts[tool_name] += 1
|
|
490
519
|
|
|
491
520
|
# Display context information
|
|
492
521
|
console.print(f"[bold {STATUS_COLOR}]Conversation Context:[/bold {STATUS_COLOR}]")
|
|
493
522
|
console.print(
|
|
494
|
-
f" Context used: {total_tokens:,} / {max_context_size:,} tokens ({(total_tokens / max_context_size) * 100:.1f}%)"
|
|
523
|
+
f" Context used: {tokens_metadata.total_tokens:,} / {max_context_size:,} tokens ({(tokens_metadata.total_tokens / max_context_size) * 100:.1f}%)"
|
|
495
524
|
)
|
|
496
525
|
console.print(
|
|
497
526
|
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}%)"
|
|
@@ -502,7 +531,11 @@ def handle_context_command(messages, ai: ToolCallingLLM, console: Console) -> No
|
|
|
502
531
|
for role in ["system", "user", "assistant", "tool"]:
|
|
503
532
|
if role in role_token_usage:
|
|
504
533
|
tokens = role_token_usage[role]
|
|
505
|
-
percentage = (
|
|
534
|
+
percentage = (
|
|
535
|
+
(tokens / tokens_metadata.total_tokens) * 100
|
|
536
|
+
if tokens_metadata.total_tokens > 0
|
|
537
|
+
else 0
|
|
538
|
+
)
|
|
506
539
|
role_name = {
|
|
507
540
|
"system": "system prompt",
|
|
508
541
|
"user": "user messages",
|
|
@@ -811,6 +844,88 @@ def handle_last_command(
|
|
|
811
844
|
)
|
|
812
845
|
|
|
813
846
|
|
|
847
|
+
def handle_feedback_command(
|
|
848
|
+
style: Style,
|
|
849
|
+
console: Console,
|
|
850
|
+
feedback: Feedback,
|
|
851
|
+
feedback_callback: FeedbackCallback,
|
|
852
|
+
) -> None:
|
|
853
|
+
"""Handle the /feedback command to collect user feedback."""
|
|
854
|
+
try:
|
|
855
|
+
# Create a temporary session without history for feedback prompts
|
|
856
|
+
temp_session = PromptSession(history=InMemoryHistory()) # type: ignore
|
|
857
|
+
# Prominent privacy notice to users
|
|
858
|
+
console.print(
|
|
859
|
+
f"[bold {HELP_COLOR}]Privacy Notice:[/bold {HELP_COLOR}] {PRIVACY_NOTICE_BANNER}"
|
|
860
|
+
)
|
|
861
|
+
# A "Cancel" button of equal discoverability to "Sent" or "Submit" buttons must be made available
|
|
862
|
+
console.print(
|
|
863
|
+
"[bold yellow]💡 Tip: Press Ctrl+C at any time to cancel feedback[/bold yellow]"
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
# Ask for thumbs up/down rating with validation
|
|
867
|
+
while True:
|
|
868
|
+
rating_prompt = temp_session.prompt(
|
|
869
|
+
[("class:prompt", "Was this response useful to you? 👍(y)/👎(n): ")],
|
|
870
|
+
style=style,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
rating_lower = rating_prompt.lower().strip()
|
|
874
|
+
if rating_lower in ["y", "n"]:
|
|
875
|
+
break
|
|
876
|
+
else:
|
|
877
|
+
console.print(
|
|
878
|
+
"[bold red]Please enter only 'y' for yes or 'n' for no.[/bold red]"
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
# Determine rating
|
|
882
|
+
is_positive = rating_lower == "y"
|
|
883
|
+
|
|
884
|
+
# Ask for additional comments
|
|
885
|
+
comment_prompt = temp_session.prompt(
|
|
886
|
+
[
|
|
887
|
+
(
|
|
888
|
+
"class:prompt",
|
|
889
|
+
"Do you want to provide any additional comments for feedback? (press Enter to skip):\n",
|
|
890
|
+
)
|
|
891
|
+
],
|
|
892
|
+
style=style,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
comment = comment_prompt.strip() if comment_prompt.strip() else None
|
|
896
|
+
|
|
897
|
+
# Create UserFeedback object
|
|
898
|
+
user_feedback = UserFeedback(is_positive, comment)
|
|
899
|
+
|
|
900
|
+
if comment:
|
|
901
|
+
console.print(
|
|
902
|
+
f'[bold green]✓ Feedback recorded (rating={user_feedback.rating_emoji}, "{escape(comment)}")[/bold green]'
|
|
903
|
+
)
|
|
904
|
+
else:
|
|
905
|
+
console.print(
|
|
906
|
+
f"[bold green]✓ Feedback recorded (rating={user_feedback.rating_emoji}, no comment)[/bold green]"
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Final confirmation before submitting
|
|
910
|
+
final_confirmation = temp_session.prompt(
|
|
911
|
+
[("class:prompt", "\nDo you want to submit this feedback? (Y/n): ")],
|
|
912
|
+
style=style,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
# If user says no, cancel the feedback
|
|
916
|
+
if final_confirmation.lower().strip().startswith("n"):
|
|
917
|
+
console.print("[dim]Feedback cancelled.[/dim]")
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
feedback.user_feedback = user_feedback
|
|
921
|
+
feedback_callback(feedback)
|
|
922
|
+
console.print("[bold green]Thank you for your feedback! 🙏[/bold green]")
|
|
923
|
+
|
|
924
|
+
except KeyboardInterrupt:
|
|
925
|
+
console.print("[dim]Feedback cancelled.[/dim]")
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
|
|
814
929
|
def display_recent_tool_outputs(
|
|
815
930
|
tool_calls: List[ToolCallResult],
|
|
816
931
|
console: Console,
|
|
@@ -823,7 +938,10 @@ def display_recent_tool_outputs(
|
|
|
823
938
|
for tool_call in tool_calls:
|
|
824
939
|
tool_index = find_tool_index_in_history(tool_call, all_tool_calls_history)
|
|
825
940
|
preview_output = format_tool_call_output(tool_call, tool_index)
|
|
826
|
-
title =
|
|
941
|
+
title = (
|
|
942
|
+
f"{tool_call.result.status.to_emoji()} {tool_call.description} -> "
|
|
943
|
+
f"returned {tool_call.result.return_code}"
|
|
944
|
+
)
|
|
827
945
|
|
|
828
946
|
console.print(
|
|
829
947
|
Panel(
|
|
@@ -835,17 +953,51 @@ def display_recent_tool_outputs(
|
|
|
835
953
|
)
|
|
836
954
|
|
|
837
955
|
|
|
956
|
+
def save_conversation_to_file(
|
|
957
|
+
json_output_file: str,
|
|
958
|
+
messages: List,
|
|
959
|
+
all_tool_calls_history: List[ToolCallResult],
|
|
960
|
+
console: Console,
|
|
961
|
+
) -> None:
|
|
962
|
+
"""Save the current conversation to a JSON file."""
|
|
963
|
+
try:
|
|
964
|
+
# Create LLMResult-like structure for consistency with non-interactive mode
|
|
965
|
+
conversation_result = LLMResult(
|
|
966
|
+
messages=messages,
|
|
967
|
+
tool_calls=all_tool_calls_history,
|
|
968
|
+
result=None, # No single result in interactive mode
|
|
969
|
+
total_cost=0.0, # TODO: Could aggregate costs from all responses if needed
|
|
970
|
+
total_tokens=0,
|
|
971
|
+
prompt_tokens=0,
|
|
972
|
+
completion_tokens=0,
|
|
973
|
+
metadata={
|
|
974
|
+
"session_type": "interactive",
|
|
975
|
+
"total_turns": len([m for m in messages if m.get("role") == "user"]),
|
|
976
|
+
},
|
|
977
|
+
)
|
|
978
|
+
write_json_file(json_output_file, conversation_result.model_dump())
|
|
979
|
+
console.print(
|
|
980
|
+
f"[bold {STATUS_COLOR}]Conversation saved to {json_output_file}[/bold {STATUS_COLOR}]"
|
|
981
|
+
)
|
|
982
|
+
except Exception as e:
|
|
983
|
+
logging.error(f"Failed to save conversation: {e}", exc_info=e)
|
|
984
|
+
console.print(
|
|
985
|
+
f"[bold {ERROR_COLOR}]Failed to save conversation: {e}[/bold {ERROR_COLOR}]"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
|
|
838
989
|
def run_interactive_loop(
|
|
839
990
|
ai: ToolCallingLLM,
|
|
840
991
|
console: Console,
|
|
841
992
|
initial_user_input: Optional[str],
|
|
842
993
|
include_files: Optional[List[Path]],
|
|
843
|
-
post_processing_prompt: Optional[str],
|
|
844
994
|
show_tool_output: bool,
|
|
845
995
|
tracer=None,
|
|
846
996
|
runbooks=None,
|
|
847
997
|
system_prompt_additions: Optional[str] = None,
|
|
848
998
|
check_version: bool = True,
|
|
999
|
+
feedback_callback: Optional[FeedbackCallback] = None,
|
|
1000
|
+
json_output_file: Optional[str] = None,
|
|
849
1001
|
) -> None:
|
|
850
1002
|
# Initialize tracer - use DummyTracer if no tracer provided
|
|
851
1003
|
if tracer is None:
|
|
@@ -874,7 +1026,11 @@ def run_interactive_loop(
|
|
|
874
1026
|
ai.approval_callback = approval_handler
|
|
875
1027
|
|
|
876
1028
|
# Create merged completer with slash commands, conditional executables, show command, and smart paths
|
|
877
|
-
|
|
1029
|
+
# TODO: remove unsupported_commands support once we implement feedback callback
|
|
1030
|
+
unsupported_commands = []
|
|
1031
|
+
if feedback_callback is None:
|
|
1032
|
+
unsupported_commands.append(SlashCommands.FEEDBACK.command)
|
|
1033
|
+
slash_completer = SlashCommandCompleter(unsupported_commands)
|
|
878
1034
|
executable_completer = ConditionalExecutableCompleter()
|
|
879
1035
|
show_completer = ShowCommandCompleter()
|
|
880
1036
|
path_completer = SmartPathCompleter()
|
|
@@ -891,6 +1047,9 @@ def run_interactive_loop(
|
|
|
891
1047
|
if initial_user_input:
|
|
892
1048
|
history.append_string(initial_user_input)
|
|
893
1049
|
|
|
1050
|
+
feedback = Feedback()
|
|
1051
|
+
feedback.metadata.update_llm(ai.llm)
|
|
1052
|
+
|
|
894
1053
|
# Create custom key bindings for Ctrl+C behavior
|
|
895
1054
|
bindings = KeyBindings()
|
|
896
1055
|
status_message = ""
|
|
@@ -963,7 +1122,15 @@ def run_interactive_loop(
|
|
|
963
1122
|
|
|
964
1123
|
input_prompt = [("class:prompt", "User: ")]
|
|
965
1124
|
|
|
966
|
-
|
|
1125
|
+
# TODO: merge the /feedback command description to WELCOME_BANNER once we implement feedback callback
|
|
1126
|
+
welcome_banner = WELCOME_BANNER
|
|
1127
|
+
if feedback_callback:
|
|
1128
|
+
welcome_banner = (
|
|
1129
|
+
welcome_banner.rstrip(".")
|
|
1130
|
+
+ f", '{SlashCommands.FEEDBACK.command}' to share your thoughts."
|
|
1131
|
+
)
|
|
1132
|
+
console.print(welcome_banner)
|
|
1133
|
+
|
|
967
1134
|
if initial_user_input:
|
|
968
1135
|
console.print(
|
|
969
1136
|
f"[bold {USER_COLOR}]User:[/bold {USER_COLOR}] {initial_user_input}"
|
|
@@ -985,30 +1152,44 @@ def run_interactive_loop(
|
|
|
985
1152
|
if user_input.startswith("/"):
|
|
986
1153
|
original_input = user_input.strip()
|
|
987
1154
|
command = original_input.lower()
|
|
988
|
-
|
|
989
1155
|
# Handle prefix matching for slash commands
|
|
990
|
-
matches = [
|
|
1156
|
+
matches = [
|
|
1157
|
+
cmd
|
|
1158
|
+
for cmd in slash_completer.commands.keys()
|
|
1159
|
+
if cmd.startswith(command)
|
|
1160
|
+
]
|
|
991
1161
|
if len(matches) == 1:
|
|
992
1162
|
command = matches[0]
|
|
993
1163
|
elif len(matches) > 1:
|
|
994
1164
|
console.print(
|
|
995
|
-
f"[bold {ERROR_COLOR}]Ambiguous command '{command}'.
|
|
1165
|
+
f"[bold {ERROR_COLOR}]Ambiguous command '{command}'. "
|
|
1166
|
+
f"Matches: {', '.join(matches)}[/bold {ERROR_COLOR}]"
|
|
996
1167
|
)
|
|
997
1168
|
continue
|
|
998
1169
|
|
|
999
1170
|
if command == SlashCommands.EXIT.command:
|
|
1171
|
+
console.print(
|
|
1172
|
+
f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
|
|
1173
|
+
)
|
|
1000
1174
|
return
|
|
1001
1175
|
elif command == SlashCommands.HELP.command:
|
|
1002
1176
|
console.print(
|
|
1003
1177
|
f"[bold {HELP_COLOR}]Available commands:[/bold {HELP_COLOR}]"
|
|
1004
1178
|
)
|
|
1005
|
-
for cmd, description in
|
|
1179
|
+
for cmd, description in slash_completer.commands.items():
|
|
1180
|
+
# Only show feedback command if callback is available
|
|
1181
|
+
if (
|
|
1182
|
+
cmd == SlashCommands.FEEDBACK.command
|
|
1183
|
+
and feedback_callback is None
|
|
1184
|
+
):
|
|
1185
|
+
continue
|
|
1006
1186
|
console.print(f" [bold]{cmd}[/bold] - {description}")
|
|
1007
1187
|
continue
|
|
1008
1188
|
elif command == SlashCommands.CLEAR.command:
|
|
1009
1189
|
console.clear()
|
|
1010
1190
|
console.print(
|
|
1011
|
-
f"[bold {STATUS_COLOR}]Screen cleared and context reset.
|
|
1191
|
+
f"[bold {STATUS_COLOR}]Screen cleared and context reset. "
|
|
1192
|
+
f"You can now ask a new question.[/bold {STATUS_COLOR}]"
|
|
1012
1193
|
)
|
|
1013
1194
|
messages = None
|
|
1014
1195
|
last_response = None
|
|
@@ -1052,6 +1233,12 @@ def run_interactive_loop(
|
|
|
1052
1233
|
if shared_input is None:
|
|
1053
1234
|
continue # User chose not to share or no output, continue to next input
|
|
1054
1235
|
user_input = shared_input
|
|
1236
|
+
elif (
|
|
1237
|
+
command == SlashCommands.FEEDBACK.command
|
|
1238
|
+
and feedback_callback is not None
|
|
1239
|
+
):
|
|
1240
|
+
handle_feedback_command(style, console, feedback, feedback_callback)
|
|
1241
|
+
continue
|
|
1055
1242
|
else:
|
|
1056
1243
|
console.print(f"Unknown command: {command}")
|
|
1057
1244
|
continue
|
|
@@ -1080,7 +1267,6 @@ def run_interactive_loop(
|
|
|
1080
1267
|
)
|
|
1081
1268
|
response = ai.call(
|
|
1082
1269
|
messages,
|
|
1083
|
-
post_processing_prompt,
|
|
1084
1270
|
trace_span=trace_span,
|
|
1085
1271
|
tool_number_offset=len(all_tool_calls_history),
|
|
1086
1272
|
)
|
|
@@ -1091,6 +1277,7 @@ def run_interactive_loop(
|
|
|
1091
1277
|
|
|
1092
1278
|
messages = response.messages # type: ignore
|
|
1093
1279
|
last_response = response
|
|
1280
|
+
feedback.metadata.add_llm_response(user_input, response.result)
|
|
1094
1281
|
|
|
1095
1282
|
if response.tool_calls:
|
|
1096
1283
|
all_tool_calls_history.extend(response.tool_calls)
|
|
@@ -1111,18 +1298,28 @@ def run_interactive_loop(
|
|
|
1111
1298
|
)
|
|
1112
1299
|
)
|
|
1113
1300
|
|
|
1114
|
-
if trace_url:
|
|
1115
|
-
console.print(f"🔍 View trace: {trace_url}")
|
|
1116
|
-
|
|
1117
1301
|
console.print("")
|
|
1302
|
+
|
|
1303
|
+
# Save conversation after each AI response
|
|
1304
|
+
if json_output_file and messages:
|
|
1305
|
+
save_conversation_to_file(
|
|
1306
|
+
json_output_file, messages, all_tool_calls_history, console
|
|
1307
|
+
)
|
|
1118
1308
|
except typer.Abort:
|
|
1309
|
+
console.print(
|
|
1310
|
+
f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
|
|
1311
|
+
)
|
|
1119
1312
|
break
|
|
1120
1313
|
except EOFError: # Handle Ctrl+D
|
|
1314
|
+
console.print(
|
|
1315
|
+
f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
|
|
1316
|
+
)
|
|
1121
1317
|
break
|
|
1122
1318
|
except Exception as e:
|
|
1123
1319
|
logging.error("An error occurred during interactive mode:", exc_info=e)
|
|
1124
1320
|
console.print(f"[bold {ERROR_COLOR}]Error: {e}[/bold {ERROR_COLOR}]")
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1321
|
+
finally:
|
|
1322
|
+
# Print trace URL for debugging (works for both success and error cases)
|
|
1323
|
+
trace_url = tracer.get_trace_url()
|
|
1324
|
+
if trace_url:
|
|
1325
|
+
console.print(f"🔍 View trace: {trace_url}")
|