hackagent 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hackagent/__init__.py +12 -0
- hackagent/agent.py +214 -0
- hackagent/api/__init__.py +1 -0
- hackagent/api/agent/__init__.py +1 -0
- hackagent/api/agent/agent_create.py +347 -0
- hackagent/api/agent/agent_destroy.py +140 -0
- hackagent/api/agent/agent_list.py +242 -0
- hackagent/api/agent/agent_partial_update.py +361 -0
- hackagent/api/agent/agent_retrieve.py +235 -0
- hackagent/api/agent/agent_update.py +361 -0
- hackagent/api/apilogs/__init__.py +1 -0
- hackagent/api/apilogs/apilogs_list.py +170 -0
- hackagent/api/apilogs/apilogs_retrieve.py +162 -0
- hackagent/api/attack/__init__.py +1 -0
- hackagent/api/attack/attack_create.py +275 -0
- hackagent/api/attack/attack_destroy.py +146 -0
- hackagent/api/attack/attack_list.py +254 -0
- hackagent/api/attack/attack_partial_update.py +289 -0
- hackagent/api/attack/attack_retrieve.py +247 -0
- hackagent/api/attack/attack_update.py +289 -0
- hackagent/api/checkout/__init__.py +1 -0
- hackagent/api/checkout/checkout_create.py +225 -0
- hackagent/api/generate/__init__.py +1 -0
- hackagent/api/generate/generate_create.py +253 -0
- hackagent/api/judge/__init__.py +1 -0
- hackagent/api/judge/judge_create.py +253 -0
- hackagent/api/key/__init__.py +1 -0
- hackagent/api/key/key_create.py +179 -0
- hackagent/api/key/key_destroy.py +103 -0
- hackagent/api/key/key_list.py +170 -0
- hackagent/api/key/key_retrieve.py +162 -0
- hackagent/api/organization/__init__.py +1 -0
- hackagent/api/organization/organization_create.py +208 -0
- hackagent/api/organization/organization_destroy.py +104 -0
- hackagent/api/organization/organization_list.py +170 -0
- hackagent/api/organization/organization_me_retrieve.py +126 -0
- hackagent/api/organization/organization_partial_update.py +222 -0
- hackagent/api/organization/organization_retrieve.py +163 -0
- hackagent/api/organization/organization_update.py +222 -0
- hackagent/api/prompt/__init__.py +1 -0
- hackagent/api/prompt/prompt_create.py +171 -0
- hackagent/api/prompt/prompt_destroy.py +104 -0
- hackagent/api/prompt/prompt_list.py +185 -0
- hackagent/api/prompt/prompt_partial_update.py +185 -0
- hackagent/api/prompt/prompt_retrieve.py +163 -0
- hackagent/api/prompt/prompt_update.py +185 -0
- hackagent/api/result/__init__.py +1 -0
- hackagent/api/result/result_create.py +175 -0
- hackagent/api/result/result_destroy.py +106 -0
- hackagent/api/result/result_list.py +249 -0
- hackagent/api/result/result_partial_update.py +193 -0
- hackagent/api/result/result_retrieve.py +167 -0
- hackagent/api/result/result_trace_create.py +177 -0
- hackagent/api/result/result_update.py +189 -0
- hackagent/api/run/__init__.py +1 -0
- hackagent/api/run/run_create.py +187 -0
- hackagent/api/run/run_destroy.py +112 -0
- hackagent/api/run/run_list.py +291 -0
- hackagent/api/run/run_partial_update.py +201 -0
- hackagent/api/run/run_result_create.py +177 -0
- hackagent/api/run/run_retrieve.py +179 -0
- hackagent/api/run/run_run_tests_create.py +187 -0
- hackagent/api/run/run_update.py +201 -0
- hackagent/api/user/__init__.py +1 -0
- hackagent/api/user/user_create.py +212 -0
- hackagent/api/user/user_destroy.py +106 -0
- hackagent/api/user/user_list.py +174 -0
- hackagent/api/user/user_me_retrieve.py +126 -0
- hackagent/api/user/user_me_update.py +196 -0
- hackagent/api/user/user_partial_update.py +226 -0
- hackagent/api/user/user_retrieve.py +167 -0
- hackagent/api/user/user_update.py +226 -0
- hackagent/attacks/AdvPrefix/__init__.py +41 -0
- hackagent/attacks/AdvPrefix/completions.py +416 -0
- hackagent/attacks/AdvPrefix/config.py +259 -0
- hackagent/attacks/AdvPrefix/evaluation.py +745 -0
- hackagent/attacks/AdvPrefix/evaluators.py +564 -0
- hackagent/attacks/AdvPrefix/generate.py +711 -0
- hackagent/attacks/AdvPrefix/utils.py +307 -0
- hackagent/attacks/__init__.py +35 -0
- hackagent/attacks/advprefix.py +507 -0
- hackagent/attacks/base.py +106 -0
- hackagent/attacks/strategies.py +906 -0
- hackagent/cli/__init__.py +19 -0
- hackagent/cli/commands/__init__.py +20 -0
- hackagent/cli/commands/agent.py +100 -0
- hackagent/cli/commands/attack.py +417 -0
- hackagent/cli/commands/config.py +301 -0
- hackagent/cli/commands/results.py +327 -0
- hackagent/cli/config.py +249 -0
- hackagent/cli/main.py +515 -0
- hackagent/cli/tui/__init__.py +31 -0
- hackagent/cli/tui/actions_logger.py +200 -0
- hackagent/cli/tui/app.py +288 -0
- hackagent/cli/tui/base.py +137 -0
- hackagent/cli/tui/logger.py +318 -0
- hackagent/cli/tui/views/__init__.py +33 -0
- hackagent/cli/tui/views/agents.py +488 -0
- hackagent/cli/tui/views/attacks.py +624 -0
- hackagent/cli/tui/views/config.py +244 -0
- hackagent/cli/tui/views/dashboard.py +307 -0
- hackagent/cli/tui/views/results.py +1210 -0
- hackagent/cli/tui/widgets/__init__.py +24 -0
- hackagent/cli/tui/widgets/actions.py +346 -0
- hackagent/cli/tui/widgets/logs.py +435 -0
- hackagent/cli/utils.py +276 -0
- hackagent/client.py +286 -0
- hackagent/errors.py +37 -0
- hackagent/logger.py +83 -0
- hackagent/models/__init__.py +109 -0
- hackagent/models/agent.py +223 -0
- hackagent/models/agent_request.py +129 -0
- hackagent/models/api_token_log.py +184 -0
- hackagent/models/attack.py +154 -0
- hackagent/models/attack_request.py +82 -0
- hackagent/models/checkout_session_request_request.py +76 -0
- hackagent/models/checkout_session_response.py +59 -0
- hackagent/models/choice.py +81 -0
- hackagent/models/choice_message.py +67 -0
- hackagent/models/evaluation_status_enum.py +14 -0
- hackagent/models/generate_error_response.py +59 -0
- hackagent/models/generate_request_request.py +212 -0
- hackagent/models/generate_success_response.py +115 -0
- hackagent/models/generic_error_response.py +70 -0
- hackagent/models/message_request.py +67 -0
- hackagent/models/organization.py +102 -0
- hackagent/models/organization_minimal.py +68 -0
- hackagent/models/organization_request.py +71 -0
- hackagent/models/paginated_agent_list.py +123 -0
- hackagent/models/paginated_api_token_log_list.py +123 -0
- hackagent/models/paginated_attack_list.py +123 -0
- hackagent/models/paginated_organization_list.py +123 -0
- hackagent/models/paginated_prompt_list.py +123 -0
- hackagent/models/paginated_result_list.py +123 -0
- hackagent/models/paginated_run_list.py +123 -0
- hackagent/models/paginated_user_api_key_list.py +123 -0
- hackagent/models/paginated_user_profile_list.py +123 -0
- hackagent/models/patched_agent_request.py +128 -0
- hackagent/models/patched_attack_request.py +92 -0
- hackagent/models/patched_organization_request.py +71 -0
- hackagent/models/patched_prompt_request.py +125 -0
- hackagent/models/patched_result_request.py +237 -0
- hackagent/models/patched_run_request.py +138 -0
- hackagent/models/patched_user_profile_request.py +99 -0
- hackagent/models/prompt.py +220 -0
- hackagent/models/prompt_request.py +126 -0
- hackagent/models/result.py +294 -0
- hackagent/models/result_list_evaluation_status.py +14 -0
- hackagent/models/result_request.py +232 -0
- hackagent/models/run.py +233 -0
- hackagent/models/run_list_status.py +12 -0
- hackagent/models/run_request.py +133 -0
- hackagent/models/status_enum.py +12 -0
- hackagent/models/step_type_enum.py +14 -0
- hackagent/models/trace.py +121 -0
- hackagent/models/trace_request.py +94 -0
- hackagent/models/usage.py +75 -0
- hackagent/models/user_api_key.py +201 -0
- hackagent/models/user_api_key_request.py +73 -0
- hackagent/models/user_profile.py +135 -0
- hackagent/models/user_profile_minimal.py +76 -0
- hackagent/models/user_profile_request.py +99 -0
- hackagent/router/__init__.py +25 -0
- hackagent/router/adapters/__init__.py +20 -0
- hackagent/router/adapters/base.py +63 -0
- hackagent/router/adapters/google_adk.py +671 -0
- hackagent/router/adapters/litellm_adapter.py +524 -0
- hackagent/router/adapters/openai_adapter.py +426 -0
- hackagent/router/router.py +969 -0
- hackagent/router/types.py +54 -0
- hackagent/tracking/__init__.py +42 -0
- hackagent/tracking/context.py +163 -0
- hackagent/tracking/decorators.py +299 -0
- hackagent/tracking/tracker.py +441 -0
- hackagent/types.py +54 -0
- hackagent/utils.py +194 -0
- hackagent/vulnerabilities/__init__.py +13 -0
- hackagent/vulnerabilities/prompts.py +81 -0
- hackagent-0.3.1.dist-info/METADATA +122 -0
- hackagent-0.3.1.dist-info/RECORD +183 -0
- hackagent-0.3.1.dist-info/WHEEL +4 -0
- hackagent-0.3.1.dist-info/entry_points.txt +2 -0
- hackagent-0.3.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
# Copyright 2025 - AI4I. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Results Tab
|
|
17
|
+
|
|
18
|
+
View and analyze attack results.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
import json
|
|
23
|
+
from typing import Any
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
|
|
26
|
+
from textual.app import ComposeResult
|
|
27
|
+
from textual.binding import Binding
|
|
28
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
|
29
|
+
from textual.widgets import Button, DataTable, Label, Select, Static
|
|
30
|
+
|
|
31
|
+
from hackagent.cli.config import CLIConfig
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ResultsTab(Container):
|
|
35
|
+
"""Results tab for viewing attack results with split view."""
|
|
36
|
+
|
|
37
|
+
DEFAULT_CSS = """
|
|
38
|
+
ResultsTab {
|
|
39
|
+
layout: horizontal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ResultsTab #results-left-panel {
|
|
43
|
+
width: 30%;
|
|
44
|
+
border-right: solid $primary;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ResultsTab #results-right-panel {
|
|
48
|
+
width: 70%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ResultsTab #results-table {
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
BINDINGS = [
|
|
57
|
+
Binding("enter", "view_result", "View Details"),
|
|
58
|
+
Binding("s", "show_summary", "Summary"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def __init__(self, cli_config: CLIConfig):
|
|
62
|
+
"""Initialize results tab.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
cli_config: CLI configuration object
|
|
66
|
+
"""
|
|
67
|
+
super().__init__()
|
|
68
|
+
self.cli_config = cli_config
|
|
69
|
+
self.results_data: list[Any] = []
|
|
70
|
+
self.selected_result: Any = None
|
|
71
|
+
|
|
72
|
+
def compose(self) -> ComposeResult:
|
|
73
|
+
"""Compose the results layout with horizontal split."""
|
|
74
|
+
# Left side - Results list (30%)
|
|
75
|
+
with VerticalScroll(id="results-left-panel"):
|
|
76
|
+
yield Static(
|
|
77
|
+
"[bold cyan]🎯 Attack Results[/bold cyan]",
|
|
78
|
+
classes="section-header",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
with Horizontal(classes="toolbar"):
|
|
82
|
+
yield Button("🔄 Refresh", id="refresh-results", variant="primary")
|
|
83
|
+
yield Button("📊 CSV", id="export-csv", variant="default")
|
|
84
|
+
yield Button("📄 JSON", id="export-json", variant="default")
|
|
85
|
+
|
|
86
|
+
with Horizontal(classes="toolbar"):
|
|
87
|
+
yield Label("Filter:")
|
|
88
|
+
yield Select(
|
|
89
|
+
[
|
|
90
|
+
("All", "all"),
|
|
91
|
+
("Pending", "pending"),
|
|
92
|
+
("Running", "running"),
|
|
93
|
+
("Completed", "completed"),
|
|
94
|
+
("Failed", "failed"),
|
|
95
|
+
],
|
|
96
|
+
id="status-filter",
|
|
97
|
+
value="all",
|
|
98
|
+
)
|
|
99
|
+
yield Label("Limit:")
|
|
100
|
+
yield Select(
|
|
101
|
+
[("10", "10"), ("25", "25"), ("50", "50"), ("100", "100")],
|
|
102
|
+
id="limit-select",
|
|
103
|
+
value="25",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Results table
|
|
107
|
+
yield DataTable(zebra_stripes=True, cursor_type="row", id="results-table")
|
|
108
|
+
|
|
109
|
+
# Right side - Details view (70%)
|
|
110
|
+
with VerticalScroll(id="results-right-panel"):
|
|
111
|
+
yield Static(
|
|
112
|
+
"[bold cyan]📋 Result Details[/bold cyan]",
|
|
113
|
+
classes="section-header",
|
|
114
|
+
)
|
|
115
|
+
yield Static(
|
|
116
|
+
"[dim]💡 Select a result from the list to view full details and logs[/dim]",
|
|
117
|
+
id="result-details",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def on_mount(self) -> None:
|
|
121
|
+
"""Called when the tab is mounted."""
|
|
122
|
+
# Initialize table columns
|
|
123
|
+
try:
|
|
124
|
+
table = self.query_one("#results-table", DataTable)
|
|
125
|
+
table.clear(columns=True)
|
|
126
|
+
table.add_columns("ID", "Status", "Agent", "Created", "Results")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self.app.notify(f"Failed to initialize table: {str(e)}", severity="error")
|
|
129
|
+
|
|
130
|
+
# Show loading message immediately
|
|
131
|
+
try:
|
|
132
|
+
details_widget = self.query_one("#result-details", Static)
|
|
133
|
+
details_widget.update("[cyan]Loading results from API...[/cyan]")
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Initial load - call refresh_data directly to populate initial state
|
|
138
|
+
try:
|
|
139
|
+
self.refresh_data()
|
|
140
|
+
except Exception as e:
|
|
141
|
+
# If initial load fails, show error
|
|
142
|
+
try:
|
|
143
|
+
details_widget = self.query_one("#result-details", Static)
|
|
144
|
+
details_widget.update(
|
|
145
|
+
f"[red]Failed to load data: {str(e)}[/red]\n\n[dim]Press 🔄 Refresh button or F5 to retry[/dim]"
|
|
146
|
+
)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
151
|
+
"""Handle button press events."""
|
|
152
|
+
if event.button.id == "refresh-results":
|
|
153
|
+
self.refresh_data()
|
|
154
|
+
elif event.button.id == "export-csv":
|
|
155
|
+
self._export_results_csv()
|
|
156
|
+
elif event.button.id == "export-json":
|
|
157
|
+
self._export_results_json()
|
|
158
|
+
|
|
159
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
160
|
+
"""Handle select dropdown changes."""
|
|
161
|
+
if event.select.id in ["status-filter", "limit-select"]:
|
|
162
|
+
self.refresh_data()
|
|
163
|
+
|
|
164
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
165
|
+
"""Handle row selection in the results table."""
|
|
166
|
+
table = self.query_one(DataTable)
|
|
167
|
+
row_key = event.row_key
|
|
168
|
+
row_index = table.get_row_index(row_key)
|
|
169
|
+
|
|
170
|
+
if row_index < len(self.results_data):
|
|
171
|
+
self.selected_result = self.results_data[row_index]
|
|
172
|
+
self._show_result_details()
|
|
173
|
+
|
|
174
|
+
def refresh_data(self) -> None:
|
|
175
|
+
"""Refresh results data from API."""
|
|
176
|
+
try:
|
|
177
|
+
from hackagent.api.run import run_list
|
|
178
|
+
from hackagent.client import AuthenticatedClient
|
|
179
|
+
|
|
180
|
+
# Get filter values
|
|
181
|
+
status_sel = self.query_one("#status-filter", Select).value
|
|
182
|
+
limit_sel = self.query_one("#limit-select", Select).value
|
|
183
|
+
|
|
184
|
+
# Ensure we have strings (Select.value can be None/NoSelection)
|
|
185
|
+
status_filter = str(status_sel) if status_sel is not None else "all"
|
|
186
|
+
limit = 25
|
|
187
|
+
if limit_sel is not None:
|
|
188
|
+
try:
|
|
189
|
+
limit = int(str(limit_sel))
|
|
190
|
+
except (ValueError, TypeError):
|
|
191
|
+
limit = 25
|
|
192
|
+
|
|
193
|
+
# Validate configuration
|
|
194
|
+
if not self.cli_config.api_key:
|
|
195
|
+
self._show_empty_state("API key not configured")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
import httpx
|
|
199
|
+
|
|
200
|
+
client = AuthenticatedClient(
|
|
201
|
+
base_url=self.cli_config.base_url,
|
|
202
|
+
token=self.cli_config.api_key,
|
|
203
|
+
prefix="Bearer",
|
|
204
|
+
timeout=httpx.Timeout(5.0, connect=5.0), # 5 second timeout
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Build query parameters with status filter if not "all"
|
|
208
|
+
kwargs = {"client": client, "page_size": limit}
|
|
209
|
+
if status_filter and status_filter != "all":
|
|
210
|
+
# Map filter values to API enum
|
|
211
|
+
from hackagent.models.run_list_status import RunListStatus
|
|
212
|
+
|
|
213
|
+
status_map = {
|
|
214
|
+
"pending": RunListStatus.PENDING,
|
|
215
|
+
"running": RunListStatus.RUNNING,
|
|
216
|
+
"completed": RunListStatus.COMPLETED,
|
|
217
|
+
"failed": RunListStatus.FAILED,
|
|
218
|
+
}
|
|
219
|
+
if status_filter.lower() in status_map:
|
|
220
|
+
kwargs["status"] = status_map[status_filter.lower()]
|
|
221
|
+
|
|
222
|
+
response = run_list.sync_detailed(**kwargs)
|
|
223
|
+
|
|
224
|
+
if response.status_code == 200 and response.parsed:
|
|
225
|
+
# Get all runs - these contain agent_name, attack info, etc.
|
|
226
|
+
all_runs = response.parsed.results if response.parsed.results else []
|
|
227
|
+
|
|
228
|
+
self.results_data = all_runs if all_runs else []
|
|
229
|
+
|
|
230
|
+
if not self.results_data:
|
|
231
|
+
self._show_empty_state(
|
|
232
|
+
"No runs found. Execute an attack to see results here."
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
self._update_table()
|
|
236
|
+
elif response.status_code == 401:
|
|
237
|
+
self._show_empty_state("Authentication failed")
|
|
238
|
+
elif response.status_code == 403:
|
|
239
|
+
self._show_empty_state("Access forbidden")
|
|
240
|
+
else:
|
|
241
|
+
self._show_empty_state(
|
|
242
|
+
f"Failed to fetch results: {response.status_code}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
error_type = type(e).__name__
|
|
247
|
+
error_msg = str(e)
|
|
248
|
+
|
|
249
|
+
# Provide helpful error messages
|
|
250
|
+
if "timeout" in error_msg.lower() or "TimeoutException" in error_type:
|
|
251
|
+
self._show_empty_state(
|
|
252
|
+
f"⚠️ Connection Timeout\n\n"
|
|
253
|
+
f"Cannot reach API: {self.cli_config.base_url}\n"
|
|
254
|
+
f"Check your network connection and retry."
|
|
255
|
+
)
|
|
256
|
+
elif "401" in error_msg or "authentication" in error_msg.lower():
|
|
257
|
+
self._show_empty_state(
|
|
258
|
+
"🔒 Authentication Failed\n\nYour API key is invalid.\nRun: hackagent config set --api-key YOUR_KEY"
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
self._show_empty_state(
|
|
262
|
+
f"Error loading results: {error_type}\n{error_msg}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _show_empty_state(self, message: str) -> None:
|
|
266
|
+
"""Show an empty state message when no data is available.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
message: Message to display
|
|
270
|
+
"""
|
|
271
|
+
table = self.query_one("#results-table", DataTable)
|
|
272
|
+
table.clear()
|
|
273
|
+
|
|
274
|
+
# Show message in details area
|
|
275
|
+
details_widget = self.query_one("#result-details", Static)
|
|
276
|
+
details_widget.update(
|
|
277
|
+
f"[yellow]{message}[/yellow]\n\n[dim]💡 Tip: Press F5 or click 🔄 Refresh to retry[/dim]"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def _update_table(self) -> None:
|
|
281
|
+
"""Update the results table with current data."""
|
|
282
|
+
try:
|
|
283
|
+
table = self.query_one("#results-table", DataTable)
|
|
284
|
+
table.clear()
|
|
285
|
+
|
|
286
|
+
for run in self.results_data:
|
|
287
|
+
# Get status with color coding from Run.status
|
|
288
|
+
status_display = "Unknown"
|
|
289
|
+
if hasattr(run, "status"):
|
|
290
|
+
status_val = run.status
|
|
291
|
+
if hasattr(status_val, "value"):
|
|
292
|
+
status_display = status_val.value
|
|
293
|
+
else:
|
|
294
|
+
status_display = str(status_val)
|
|
295
|
+
|
|
296
|
+
# Color code based on status
|
|
297
|
+
status_upper = status_display.upper()
|
|
298
|
+
if status_upper == "COMPLETED":
|
|
299
|
+
status_display = f"[green]✅ {status_display}[/green]"
|
|
300
|
+
elif status_upper == "RUNNING":
|
|
301
|
+
status_display = f"[cyan]🔄 {status_display}[/cyan]"
|
|
302
|
+
elif status_upper == "FAILED":
|
|
303
|
+
status_display = f"[red]❌ {status_display}[/red]"
|
|
304
|
+
elif status_upper == "PENDING":
|
|
305
|
+
status_display = f"[yellow]⏳ {status_display}[/yellow]"
|
|
306
|
+
else:
|
|
307
|
+
status_display = f"❓ {status_display}"
|
|
308
|
+
|
|
309
|
+
# Get agent name - directly available in Run model
|
|
310
|
+
agent_name = run.agent_name if hasattr(run, "agent_name") else "Unknown"
|
|
311
|
+
|
|
312
|
+
# Get created time from timestamp
|
|
313
|
+
created_time = "N/A"
|
|
314
|
+
if hasattr(run, "timestamp") and run.timestamp:
|
|
315
|
+
try:
|
|
316
|
+
dt = (
|
|
317
|
+
run.timestamp
|
|
318
|
+
if isinstance(run.timestamp, datetime)
|
|
319
|
+
else datetime.fromisoformat(
|
|
320
|
+
str(run.timestamp).replace("Z", "+00:00")
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
created_time = dt.strftime("%Y-%m-%d %H:%M")
|
|
324
|
+
except Exception:
|
|
325
|
+
created_time = str(run.timestamp)[:16]
|
|
326
|
+
|
|
327
|
+
# Get results count as a metric
|
|
328
|
+
results_count = (
|
|
329
|
+
len(run.results) if hasattr(run, "results") and run.results else 0
|
|
330
|
+
)
|
|
331
|
+
results_display = f"{results_count} results"
|
|
332
|
+
|
|
333
|
+
# Add row with all columns: ID, Status, Agent, Created, Results
|
|
334
|
+
table.add_row(
|
|
335
|
+
str(run.id)[:8] + "...",
|
|
336
|
+
status_display,
|
|
337
|
+
agent_name,
|
|
338
|
+
created_time,
|
|
339
|
+
results_display,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Show success message
|
|
343
|
+
details_widget = self.query_one("#result-details", Static)
|
|
344
|
+
details_widget.update(
|
|
345
|
+
f"[green]✅ Loaded {len(self.results_data)} run(s)[/green]\n\n"
|
|
346
|
+
f"[dim]💡 Click any row to view full details including:\n"
|
|
347
|
+
f" • Agent: {self.results_data[0].agent_name if self.results_data else 'N/A'}\n"
|
|
348
|
+
f" • Organization: {self.results_data[0].organization_name if self.results_data else 'N/A'}\n"
|
|
349
|
+
f" • Run configuration\n"
|
|
350
|
+
f" • All result evaluations\n"
|
|
351
|
+
f" • Execution traces & logs[/dim]"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
# If table update fails, show error
|
|
356
|
+
details_widget = self.query_one("#result-details", Static)
|
|
357
|
+
details_widget.update(f"[red]❌ Error updating table: {str(e)}[/red]")
|
|
358
|
+
|
|
359
|
+
def _parse_agent_actions(self, logs_str: str) -> list[dict[str, Any]]:
|
|
360
|
+
"""Parse agent actions from log strings.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
logs_str: Raw log string
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of parsed action dictionaries
|
|
367
|
+
"""
|
|
368
|
+
import re
|
|
369
|
+
|
|
370
|
+
actions = []
|
|
371
|
+
lines = logs_str.split("\n")
|
|
372
|
+
|
|
373
|
+
for i, line in enumerate(lines):
|
|
374
|
+
# HTTP requests
|
|
375
|
+
if "HTTP" in line and (
|
|
376
|
+
"POST" in line or "GET" in line or "PUT" in line or "DELETE" in line
|
|
377
|
+
):
|
|
378
|
+
method_match = re.search(r"(GET|POST|PUT|DELETE|PATCH)", line)
|
|
379
|
+
url_match = re.search(r"(https?://[^\s]+)", line)
|
|
380
|
+
if method_match and url_match:
|
|
381
|
+
actions.append(
|
|
382
|
+
{
|
|
383
|
+
"type": "http_request",
|
|
384
|
+
"method": method_match.group(1),
|
|
385
|
+
"url": url_match.group(1),
|
|
386
|
+
"line_num": i + 1,
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Tool/Function calls
|
|
391
|
+
elif "Tool:" in line or "Function:" in line or "🔧" in line:
|
|
392
|
+
tool_match = re.search(r"(?:Tool|Function):\s*([\w_]+)", line)
|
|
393
|
+
if tool_match:
|
|
394
|
+
tool_name = tool_match.group(1)
|
|
395
|
+
# Look for arguments in next few lines
|
|
396
|
+
args = ""
|
|
397
|
+
for j in range(i + 1, min(i + 5, len(lines))):
|
|
398
|
+
if "Arguments:" in lines[j] or "Input:" in lines[j]:
|
|
399
|
+
args = lines[j]
|
|
400
|
+
break
|
|
401
|
+
actions.append(
|
|
402
|
+
{
|
|
403
|
+
"type": "tool_call",
|
|
404
|
+
"tool_name": tool_name,
|
|
405
|
+
"arguments": args,
|
|
406
|
+
"line_num": i + 1,
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# ADK events
|
|
411
|
+
elif "ADK" in line and (
|
|
412
|
+
"tool_call" in line.lower() or "tool_result" in line.lower()
|
|
413
|
+
):
|
|
414
|
+
if "tool_call" in line.lower():
|
|
415
|
+
actions.append(
|
|
416
|
+
{"type": "adk_tool_call", "content": line, "line_num": i + 1}
|
|
417
|
+
)
|
|
418
|
+
elif "tool_result" in line.lower():
|
|
419
|
+
actions.append(
|
|
420
|
+
{"type": "adk_tool_result", "content": line, "line_num": i + 1}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Model queries
|
|
424
|
+
elif "Querying model" in line or "LLM" in line:
|
|
425
|
+
model_match = re.search(r"model[\s:]+([\w-]+)", line)
|
|
426
|
+
if model_match:
|
|
427
|
+
actions.append(
|
|
428
|
+
{
|
|
429
|
+
"type": "llm_query",
|
|
430
|
+
"model": model_match.group(1),
|
|
431
|
+
"line_num": i + 1,
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return actions
|
|
436
|
+
|
|
437
|
+
def _show_result_details(self) -> None:
|
|
438
|
+
"""Show details of the selected run and its results."""
|
|
439
|
+
if not self.selected_result:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
run = self.selected_result # This is a Run object now
|
|
443
|
+
details_widget = self.query_one("#result-details", Static)
|
|
444
|
+
|
|
445
|
+
# Fetch full run details from API including all results and traces
|
|
446
|
+
try:
|
|
447
|
+
import httpx
|
|
448
|
+
|
|
449
|
+
from hackagent.api.run import run_retrieve
|
|
450
|
+
from hackagent.client import AuthenticatedClient
|
|
451
|
+
|
|
452
|
+
client = AuthenticatedClient(
|
|
453
|
+
base_url=self.cli_config.base_url,
|
|
454
|
+
token=self.cli_config.api_key,
|
|
455
|
+
prefix="Bearer",
|
|
456
|
+
timeout=httpx.Timeout(
|
|
457
|
+
10.0, connect=10.0
|
|
458
|
+
), # 10 second timeout for detailed data
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
run_id = run.id if isinstance(run.id, UUID) else UUID(str(run.id))
|
|
462
|
+
response = run_retrieve.sync_detailed(client=client, id=run_id)
|
|
463
|
+
|
|
464
|
+
if response.status_code == 200 and response.parsed:
|
|
465
|
+
run = (
|
|
466
|
+
response.parsed
|
|
467
|
+
) # Use full run with all details, results, and traces
|
|
468
|
+
except Exception as e:
|
|
469
|
+
# If fetch fails, continue with cached run but show warning
|
|
470
|
+
details_widget.update(
|
|
471
|
+
f"[yellow]⚠️ Could not fetch full details: {str(e)}[/yellow]\n\n[dim]Showing cached data...[/dim]"
|
|
472
|
+
)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
# Format creation date
|
|
476
|
+
created = "Unknown"
|
|
477
|
+
if hasattr(run, "timestamp") and run.timestamp:
|
|
478
|
+
try:
|
|
479
|
+
if isinstance(run.timestamp, datetime):
|
|
480
|
+
created = run.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
481
|
+
else:
|
|
482
|
+
created = str(run.timestamp)
|
|
483
|
+
except (AttributeError, ValueError, TypeError):
|
|
484
|
+
created = str(run.timestamp)
|
|
485
|
+
|
|
486
|
+
# Get status from Run
|
|
487
|
+
status_display = "Unknown"
|
|
488
|
+
if hasattr(run, "status"):
|
|
489
|
+
status_val = run.status
|
|
490
|
+
if hasattr(status_val, "value"):
|
|
491
|
+
status_display = status_val.value
|
|
492
|
+
else:
|
|
493
|
+
status_display = str(status_val)
|
|
494
|
+
|
|
495
|
+
# Status color and icon based on status
|
|
496
|
+
status_color = "yellow"
|
|
497
|
+
status_icon = "🔄"
|
|
498
|
+
if status_display.upper() == "COMPLETED":
|
|
499
|
+
status_color = "green"
|
|
500
|
+
status_icon = "✅"
|
|
501
|
+
elif status_display.upper() == "FAILED":
|
|
502
|
+
status_color = "red"
|
|
503
|
+
status_icon = "❌"
|
|
504
|
+
elif status_display.upper() == "RUNNING":
|
|
505
|
+
status_color = "cyan"
|
|
506
|
+
status_icon = "⚡"
|
|
507
|
+
elif status_display.upper() == "PENDING":
|
|
508
|
+
status_color = "yellow"
|
|
509
|
+
status_icon = "⏳"
|
|
510
|
+
|
|
511
|
+
# Get results count and evaluation summary
|
|
512
|
+
results_count = (
|
|
513
|
+
len(run.results) if hasattr(run, "results") and run.results else 0
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Count evaluation statuses
|
|
517
|
+
eval_summary = {
|
|
518
|
+
"SUCCESSFUL_JAILBREAK": 0,
|
|
519
|
+
"FAILED_JAILBREAK": 0,
|
|
520
|
+
"NOT_EVALUATED": 0,
|
|
521
|
+
"ERROR": 0,
|
|
522
|
+
"OTHER": 0,
|
|
523
|
+
}
|
|
524
|
+
if hasattr(run, "results") and run.results:
|
|
525
|
+
for result in run.results:
|
|
526
|
+
if hasattr(result, "evaluation_status"):
|
|
527
|
+
eval_status = (
|
|
528
|
+
result.evaluation_status.value
|
|
529
|
+
if hasattr(result.evaluation_status, "value")
|
|
530
|
+
else str(result.evaluation_status)
|
|
531
|
+
)
|
|
532
|
+
if (
|
|
533
|
+
"SUCCESSFUL" in eval_status.upper()
|
|
534
|
+
and "JAILBREAK" in eval_status.upper()
|
|
535
|
+
):
|
|
536
|
+
eval_summary["SUCCESSFUL_JAILBREAK"] += 1
|
|
537
|
+
elif (
|
|
538
|
+
"FAILED" in eval_status.upper()
|
|
539
|
+
and "JAILBREAK" in eval_status.upper()
|
|
540
|
+
):
|
|
541
|
+
eval_summary["FAILED_JAILBREAK"] += 1
|
|
542
|
+
elif "NOT_EVALUATED" in eval_status.upper():
|
|
543
|
+
eval_summary["NOT_EVALUATED"] += 1
|
|
544
|
+
elif "ERROR" in eval_status.upper():
|
|
545
|
+
eval_summary["ERROR"] += 1
|
|
546
|
+
else:
|
|
547
|
+
eval_summary["OTHER"] += 1
|
|
548
|
+
|
|
549
|
+
details = f"""[bold bright_white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold bright_white]
|
|
550
|
+
[bold bright_white] RUN DETAILS[/bold bright_white]
|
|
551
|
+
[bold bright_white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold bright_white]
|
|
552
|
+
|
|
553
|
+
[bold bright_cyan]▌ Overview[/bold bright_cyan]
|
|
554
|
+
🆔 [bold]Run ID:[/bold] [dim]{run.id}[/dim]
|
|
555
|
+
🤖 [bold]Agent:[/bold] [bright_cyan]{run.agent_name}[/bright_cyan]
|
|
556
|
+
🏢 [bold]Organization:[/bold] [bright_cyan]{run.organization_name}[/bright_cyan]
|
|
557
|
+
👤 [bold]Owner:[/bold] {run.owner_username or "N/A"}
|
|
558
|
+
{status_icon} [bold]Status:[/bold] [bright_{status_color}]{status_display}[/bright_{status_color}]
|
|
559
|
+
📊 [bold]Results:[/bold] [bright_yellow]{results_count}[/bright_yellow]
|
|
560
|
+
📅 [bold]Created:[/bold] {created}
|
|
561
|
+
|
|
562
|
+
[bold bright_green]▌ Evaluation Summary[/bold bright_green]
|
|
563
|
+
✅ [bold]Successful Jailbreaks:[/bold] [bright_green]{eval_summary["SUCCESSFUL_JAILBREAK"]}[/bright_green]
|
|
564
|
+
❌ [bold]Failed Jailbreaks:[/bold] [bright_red]{eval_summary["FAILED_JAILBREAK"]}[/bright_red]
|
|
565
|
+
⏸️ [bold]Not Evaluated:[/bold] [bright_yellow]{eval_summary["NOT_EVALUATED"]}[/bright_yellow]
|
|
566
|
+
⚠️ [bold]Errors:[/bold] [bright_red]{eval_summary["ERROR"]}[/bright_red]
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
# Add run configuration if available
|
|
570
|
+
if hasattr(run, "run_config") and run.run_config:
|
|
571
|
+
details += (
|
|
572
|
+
"\n[bold bright_yellow]▌ Run Configuration[/bold bright_yellow]\n"
|
|
573
|
+
)
|
|
574
|
+
try:
|
|
575
|
+
if isinstance(run.run_config, dict):
|
|
576
|
+
run_config_str = json.dumps(run.run_config, indent=2)
|
|
577
|
+
# Color-code for readability
|
|
578
|
+
lines = run_config_str.split("\n")
|
|
579
|
+
for line in lines:
|
|
580
|
+
if ":" in line and '"' in line:
|
|
581
|
+
key_part, value_part = line.split(":", 1)
|
|
582
|
+
details += f"[bright_yellow]{key_part}[/bright_yellow]:[bright_white]{value_part}[/bright_white]\n"
|
|
583
|
+
else:
|
|
584
|
+
details += f"{line}\n"
|
|
585
|
+
else:
|
|
586
|
+
details += f"{str(run.run_config)}\n"
|
|
587
|
+
except Exception:
|
|
588
|
+
details += f"{str(run.run_config)}\n"
|
|
589
|
+
|
|
590
|
+
# Add run notes if available
|
|
591
|
+
if hasattr(run, "run_notes") and run.run_notes:
|
|
592
|
+
details += f"\n[bold bright_magenta]▌ Notes[/bold bright_magenta]\n{run.run_notes}\n"
|
|
593
|
+
|
|
594
|
+
# Show all results with their traces and logs
|
|
595
|
+
if hasattr(run, "results") and run.results:
|
|
596
|
+
details += "\n[bold bright_white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold bright_white]\n"
|
|
597
|
+
details += f"[bold bright_white] RESULTS & TRACES ({len(run.results)})[/bold bright_white]\n"
|
|
598
|
+
details += "[bold bright_white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold bright_white]\n\n"
|
|
599
|
+
|
|
600
|
+
for idx, result in enumerate(run.results, 1):
|
|
601
|
+
# Result header
|
|
602
|
+
eval_status = "N/A"
|
|
603
|
+
if hasattr(result, "evaluation_status"):
|
|
604
|
+
eval_status = (
|
|
605
|
+
result.evaluation_status.value
|
|
606
|
+
if hasattr(result.evaluation_status, "value")
|
|
607
|
+
else str(result.evaluation_status)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Color code the result status
|
|
611
|
+
if (
|
|
612
|
+
"SUCCESSFUL" in eval_status.upper()
|
|
613
|
+
and "JAILBREAK" in eval_status.upper()
|
|
614
|
+
):
|
|
615
|
+
status_color = "green"
|
|
616
|
+
status_icon = "✅"
|
|
617
|
+
elif (
|
|
618
|
+
"FAILED" in eval_status.upper()
|
|
619
|
+
and "JAILBREAK" in eval_status.upper()
|
|
620
|
+
):
|
|
621
|
+
status_color = "red"
|
|
622
|
+
status_icon = "❌"
|
|
623
|
+
elif "ERROR" in eval_status.upper():
|
|
624
|
+
status_color = "red"
|
|
625
|
+
status_icon = "⚠️"
|
|
626
|
+
else:
|
|
627
|
+
status_color = "yellow"
|
|
628
|
+
status_icon = "ℹ️"
|
|
629
|
+
|
|
630
|
+
details += f"\n[bold bright_cyan]▌ Result #{idx}[/bold bright_cyan]\n"
|
|
631
|
+
details += f" 🆔 [bold]ID:[/bold] [dim]{result.id}[/dim]\n"
|
|
632
|
+
details += f" {status_icon} [bold]Evaluation:[/bold] [bright_{status_color}]{eval_status}[/bright_{status_color}]\n"
|
|
633
|
+
|
|
634
|
+
if hasattr(result, "prompt_name") and result.prompt_name:
|
|
635
|
+
details += f" 📝 [bold]Prompt:[/bold] [bright_cyan]{result.prompt_name}[/bright_cyan]\n"
|
|
636
|
+
|
|
637
|
+
if hasattr(result, "latency_ms") and result.latency_ms:
|
|
638
|
+
details += f" ⏱️ [bold]Latency:[/bold] [bright_magenta]{result.latency_ms}ms[/bright_magenta]\n"
|
|
639
|
+
|
|
640
|
+
if (
|
|
641
|
+
hasattr(result, "response_status_code")
|
|
642
|
+
and result.response_status_code
|
|
643
|
+
):
|
|
644
|
+
details += f" 🌐 [bold]HTTP Status:[/bold] [bright_green]{result.response_status_code}[/bright_green]\n"
|
|
645
|
+
|
|
646
|
+
# Show evaluation notes if any
|
|
647
|
+
if hasattr(result, "evaluation_notes") and result.evaluation_notes:
|
|
648
|
+
details += f" 💬 [bold]Notes:[/bold] {result.evaluation_notes}\n"
|
|
649
|
+
|
|
650
|
+
# Show evaluation metrics if any
|
|
651
|
+
if hasattr(result, "evaluation_metrics") and result.evaluation_metrics:
|
|
652
|
+
details += " 📊 [bold]Metrics:[/bold]\n"
|
|
653
|
+
try:
|
|
654
|
+
if isinstance(result.evaluation_metrics, dict):
|
|
655
|
+
for key, value in result.evaluation_metrics.items():
|
|
656
|
+
details += f" • {key}: [bright_cyan]{value}[/bright_cyan]\n"
|
|
657
|
+
else:
|
|
658
|
+
details += f" {str(result.evaluation_metrics)}\n"
|
|
659
|
+
except Exception:
|
|
660
|
+
details += f" {str(result.evaluation_metrics)}\n"
|
|
661
|
+
|
|
662
|
+
# Show request payload if available
|
|
663
|
+
if hasattr(result, "request_payload") and result.request_payload:
|
|
664
|
+
details += (
|
|
665
|
+
"\n [bold bright_cyan]📤 Request Payload:[/bold bright_cyan]\n"
|
|
666
|
+
)
|
|
667
|
+
try:
|
|
668
|
+
if isinstance(result.request_payload, dict):
|
|
669
|
+
payload_str = json.dumps(result.request_payload, indent=2)
|
|
670
|
+
lines = payload_str.split("\n")
|
|
671
|
+
for line in lines[:30]: # Show more lines
|
|
672
|
+
if ":" in line and '"' in line:
|
|
673
|
+
key_part, value_part = line.split(":", 1)
|
|
674
|
+
details += f" [yellow]{key_part}:[/yellow][bright_white]{value_part}[/bright_white]\n"
|
|
675
|
+
else:
|
|
676
|
+
details += f" {line}\n"
|
|
677
|
+
if len(lines) > 30:
|
|
678
|
+
details += f" [dim]... ({len(lines) - 30} more lines)[/dim]\n"
|
|
679
|
+
else:
|
|
680
|
+
details += f" {str(result.request_payload)[:500]}\n"
|
|
681
|
+
except Exception:
|
|
682
|
+
details += f" {str(result.request_payload)[:500]}\n"
|
|
683
|
+
|
|
684
|
+
# Show response body if available
|
|
685
|
+
if hasattr(result, "response_body") and result.response_body:
|
|
686
|
+
details += (
|
|
687
|
+
"\n [bold bright_green]📥 Response Body:[/bold bright_green]\n"
|
|
688
|
+
)
|
|
689
|
+
response_lines = str(result.response_body).split("\n")
|
|
690
|
+
for line in response_lines[:30]: # Show more lines
|
|
691
|
+
if line.strip():
|
|
692
|
+
details += f" {line}\n"
|
|
693
|
+
if len(response_lines) > 30:
|
|
694
|
+
details += f" [dim]... ({len(response_lines) - 30} more lines)[/dim]\n"
|
|
695
|
+
|
|
696
|
+
# Show traces for this result - organized by type
|
|
697
|
+
if hasattr(result, "traces") and result.traces:
|
|
698
|
+
# Sort traces by sequence number to show chronological order
|
|
699
|
+
sorted_traces = sorted(
|
|
700
|
+
result.traces,
|
|
701
|
+
key=lambda t: t.sequence if hasattr(t, "sequence") else 0,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
details += "\n[bold bright_magenta]▌ 🔍 EXECUTION TRACES ({} steps)[/bold bright_magenta]\n\n".format(
|
|
705
|
+
len(sorted_traces)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
for trace in sorted_traces:
|
|
709
|
+
# Get step type with proper field name
|
|
710
|
+
step_type = "OTHER"
|
|
711
|
+
step_icon = "📋"
|
|
712
|
+
step_color = "bright_cyan"
|
|
713
|
+
|
|
714
|
+
if hasattr(trace, "step_type"):
|
|
715
|
+
step_val = trace.step_type
|
|
716
|
+
step_type = (
|
|
717
|
+
step_val.value
|
|
718
|
+
if hasattr(step_val, "value")
|
|
719
|
+
else str(step_val)
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Assign icons and colors based on step type
|
|
723
|
+
if step_type == "TOOL_CALL":
|
|
724
|
+
step_icon = "🔧"
|
|
725
|
+
step_color = "bright_green"
|
|
726
|
+
elif step_type == "TOOL_RESPONSE":
|
|
727
|
+
step_icon = "📥"
|
|
728
|
+
step_color = "bright_cyan"
|
|
729
|
+
elif step_type == "AGENT_THOUGHT":
|
|
730
|
+
step_icon = "🧠"
|
|
731
|
+
step_color = "bright_magenta"
|
|
732
|
+
elif step_type == "AGENT_RESPONSE_CHUNK":
|
|
733
|
+
step_icon = "💬"
|
|
734
|
+
step_color = "bright_white"
|
|
735
|
+
elif step_type == "MCP_STEP":
|
|
736
|
+
step_icon = "🔗"
|
|
737
|
+
step_color = "bright_yellow"
|
|
738
|
+
elif step_type == "A2A_COMM":
|
|
739
|
+
step_icon = "🤝"
|
|
740
|
+
step_color = "bright_yellow"
|
|
741
|
+
|
|
742
|
+
# Get sequence number
|
|
743
|
+
seq = trace.sequence if hasattr(trace, "sequence") else "?"
|
|
744
|
+
|
|
745
|
+
# Get timestamp
|
|
746
|
+
trace_time = ""
|
|
747
|
+
if hasattr(trace, "timestamp"):
|
|
748
|
+
try:
|
|
749
|
+
if isinstance(trace.timestamp, datetime):
|
|
750
|
+
trace_time = trace.timestamp.strftime(
|
|
751
|
+
"%H:%M:%S.%f"
|
|
752
|
+
)[:-3]
|
|
753
|
+
else:
|
|
754
|
+
dt = datetime.fromisoformat(
|
|
755
|
+
str(trace.timestamp).replace("Z", "+00:00")
|
|
756
|
+
)
|
|
757
|
+
trace_time = dt.strftime("%H:%M:%S.%f")[:-3]
|
|
758
|
+
except Exception:
|
|
759
|
+
trace_time = str(trace.timestamp)[:12]
|
|
760
|
+
|
|
761
|
+
# Format the trace header
|
|
762
|
+
details += f"[{step_color}]╭───[/] [bold {step_color}]Step {seq}[/bold {step_color}] [{step_color}]{step_icon} {step_type}[/]\n"
|
|
763
|
+
if trace_time:
|
|
764
|
+
details += (
|
|
765
|
+
f"[{step_color}]│[/] [dim]⏰ {trace_time}[/dim]\n"
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Get and format content
|
|
769
|
+
if hasattr(trace, "content") and trace.content:
|
|
770
|
+
content = trace.content
|
|
771
|
+
|
|
772
|
+
# Try to parse JSON content for better display
|
|
773
|
+
try:
|
|
774
|
+
if isinstance(content, str):
|
|
775
|
+
content_obj = json.loads(content)
|
|
776
|
+
elif isinstance(content, dict):
|
|
777
|
+
content_obj = content
|
|
778
|
+
else:
|
|
779
|
+
content_obj = None
|
|
780
|
+
|
|
781
|
+
if content_obj:
|
|
782
|
+
# Show key fields based on step type
|
|
783
|
+
if step_type == "TOOL_CALL" and isinstance(
|
|
784
|
+
content_obj, dict
|
|
785
|
+
):
|
|
786
|
+
if (
|
|
787
|
+
"name" in content_obj
|
|
788
|
+
or "tool" in content_obj
|
|
789
|
+
):
|
|
790
|
+
tool_name = content_obj.get(
|
|
791
|
+
"name"
|
|
792
|
+
) or content_obj.get("tool")
|
|
793
|
+
details += f"[{step_color}]│[/] [bold bright_cyan]Tool:[/bold bright_cyan] [bright_white]{tool_name}[/bright_white]\n"
|
|
794
|
+
if (
|
|
795
|
+
"arguments" in content_obj
|
|
796
|
+
or "input" in content_obj
|
|
797
|
+
):
|
|
798
|
+
args = content_obj.get(
|
|
799
|
+
"arguments"
|
|
800
|
+
) or content_obj.get("input")
|
|
801
|
+
args_str = (
|
|
802
|
+
json.dumps(args, indent=2)
|
|
803
|
+
if isinstance(args, dict)
|
|
804
|
+
else str(args)
|
|
805
|
+
)
|
|
806
|
+
details += f"[{step_color}]│[/] [bold]Arguments:[/bold]\n"
|
|
807
|
+
for line in args_str.split("\n")[
|
|
808
|
+
:20
|
|
809
|
+
]: # Show more lines
|
|
810
|
+
if ":" in line and '"' in line:
|
|
811
|
+
details += f"[{step_color}]│[/] [yellow]{line}[/yellow]\n"
|
|
812
|
+
else:
|
|
813
|
+
details += (
|
|
814
|
+
f"[{step_color}]│[/] {line}\n"
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
elif step_type == "TOOL_RESPONSE" and isinstance(
|
|
818
|
+
content_obj, dict
|
|
819
|
+
):
|
|
820
|
+
if (
|
|
821
|
+
"result" in content_obj
|
|
822
|
+
or "output" in content_obj
|
|
823
|
+
):
|
|
824
|
+
result_data = content_obj.get(
|
|
825
|
+
"result"
|
|
826
|
+
) or content_obj.get("output")
|
|
827
|
+
result_str = (
|
|
828
|
+
json.dumps(result_data, indent=2)
|
|
829
|
+
if isinstance(result_data, dict)
|
|
830
|
+
else str(result_data)
|
|
831
|
+
)
|
|
832
|
+
details += f"[{step_color}]│[/] [bold bright_green]Result:[/bold bright_green]\n"
|
|
833
|
+
for line in result_str.split("\n")[:20]:
|
|
834
|
+
if ":" in line and '"' in line:
|
|
835
|
+
details += f"[{step_color}]│[/] [bright_green]{line}[/bright_green]\n"
|
|
836
|
+
else:
|
|
837
|
+
details += (
|
|
838
|
+
f"[{step_color}]│[/] {line}\n"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
elif step_type == "AGENT_THOUGHT":
|
|
842
|
+
# Show thinking/reasoning
|
|
843
|
+
thought_text = (
|
|
844
|
+
content_obj
|
|
845
|
+
if isinstance(content_obj, str)
|
|
846
|
+
else str(content_obj)
|
|
847
|
+
)
|
|
848
|
+
details += f"[{step_color}]│[/] [bold bright_magenta]Thought:[/bold bright_magenta]\n"
|
|
849
|
+
for line in thought_text.split("\n")[:10]:
|
|
850
|
+
if line.strip():
|
|
851
|
+
details += f"[{step_color}]│[/] {line[:200]}\n"
|
|
852
|
+
|
|
853
|
+
elif step_type == "AGENT_RESPONSE_CHUNK":
|
|
854
|
+
# Show agent response
|
|
855
|
+
response_text = (
|
|
856
|
+
content_obj
|
|
857
|
+
if isinstance(content_obj, str)
|
|
858
|
+
else str(content_obj)
|
|
859
|
+
)
|
|
860
|
+
details += f"[{step_color}]│[/] [bold bright_white]Response:[/bold bright_white]\n"
|
|
861
|
+
for line in response_text.split("\n")[:15]:
|
|
862
|
+
if line.strip():
|
|
863
|
+
details += f"[{step_color}]│[/] {line[:200]}\n"
|
|
864
|
+
|
|
865
|
+
else:
|
|
866
|
+
# Generic JSON display
|
|
867
|
+
content_str = json.dumps(content_obj, indent=2)
|
|
868
|
+
details += f"[{step_color}]│[/] [bold]Content:[/bold]\n"
|
|
869
|
+
lines = content_str.split("\n")
|
|
870
|
+
for line in lines[:20]:
|
|
871
|
+
if ":" in line and '"' in line:
|
|
872
|
+
details += f"[{step_color}]│[/] [yellow]{line}[/yellow]\n"
|
|
873
|
+
else:
|
|
874
|
+
details += (
|
|
875
|
+
f"[{step_color}]│[/] {line}\n"
|
|
876
|
+
)
|
|
877
|
+
if len(lines) > 20:
|
|
878
|
+
details += f"[{step_color}]│[/] [dim]... ({len(lines) - 20} more lines)[/dim]\n"
|
|
879
|
+
else:
|
|
880
|
+
# Not JSON, show as plain text
|
|
881
|
+
content_str = str(content)
|
|
882
|
+
for line in content_str.split("\n")[:15]:
|
|
883
|
+
if line.strip():
|
|
884
|
+
details += (
|
|
885
|
+
f"[{step_color}]│[/] {line[:200]}\n"
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
except (json.JSONDecodeError, TypeError):
|
|
889
|
+
# Not JSON, show as plain text
|
|
890
|
+
content_str = str(content)
|
|
891
|
+
lines = content_str.split("\n")
|
|
892
|
+
details += f"[{step_color}]│[/] [bold]Content:[/bold]\n"
|
|
893
|
+
for line in lines[:15]:
|
|
894
|
+
if line.strip():
|
|
895
|
+
details += (
|
|
896
|
+
f"[{step_color}]│[/] {line[:200]}\n"
|
|
897
|
+
)
|
|
898
|
+
if len(lines) > 15:
|
|
899
|
+
details += f"[{step_color}]│[/] [dim]... ({len(lines) - 15} more lines)[/dim]\n"
|
|
900
|
+
|
|
901
|
+
details += f"[{step_color}]╰───────────────────────────────────────────────[/]\n\n"
|
|
902
|
+
|
|
903
|
+
details += "\n"
|
|
904
|
+
|
|
905
|
+
# Fallback: Show logs if available but no results (legacy support)
|
|
906
|
+
elif hasattr(run, "logs") and run.logs:
|
|
907
|
+
logs_str = str(result.logs)
|
|
908
|
+
log_lines = logs_str.split("\n")
|
|
909
|
+
|
|
910
|
+
# Parse agent actions from logs
|
|
911
|
+
actions = self._parse_agent_actions(logs_str)
|
|
912
|
+
|
|
913
|
+
# Show Agent Actions section if any were found
|
|
914
|
+
if actions:
|
|
915
|
+
details += "\n[bold magenta]╔════════════════════════════════════════╗[/bold magenta]\n"
|
|
916
|
+
details += "[bold magenta]║ 🔧 AGENT ACTIONS ({}) ║[/bold magenta]\n".format(
|
|
917
|
+
len(actions)
|
|
918
|
+
)
|
|
919
|
+
details += "[bold magenta]╚════════════════════════════════════════╝[/bold magenta]\n\n"
|
|
920
|
+
|
|
921
|
+
for idx, action in enumerate(actions, 1):
|
|
922
|
+
if action["type"] == "http_request":
|
|
923
|
+
details += f"[bold yellow]━━━ Action {idx}: HTTP Request ━━━[/bold yellow]\n"
|
|
924
|
+
details += f" 🌐 [bold cyan]{action['method']}[/bold cyan] [blue]{action['url']}[/blue]\n"
|
|
925
|
+
details += f" [dim]Line: {action['line_num']}[/dim]\n\n"
|
|
926
|
+
|
|
927
|
+
elif action["type"] == "tool_call":
|
|
928
|
+
details += f"[bold green]━━━ Action {idx}: Tool Call ━━━[/bold green]\n"
|
|
929
|
+
details += (
|
|
930
|
+
f" 🔧 [bold cyan]{action['tool_name']}[/bold cyan]\n"
|
|
931
|
+
)
|
|
932
|
+
if action.get("arguments"):
|
|
933
|
+
details += f" [yellow]{action['arguments']}[/yellow]\n"
|
|
934
|
+
details += f" [dim]Line: {action['line_num']}[/dim]\n\n"
|
|
935
|
+
|
|
936
|
+
elif action["type"] == "adk_tool_call":
|
|
937
|
+
details += f"[bold blue]━━━ Action {idx}: ADK Tool Call ━━━[/bold blue]\n"
|
|
938
|
+
details += f" 🤖 [cyan]{action['content']}[/cyan]\n"
|
|
939
|
+
details += f" [dim]Line: {action['line_num']}[/dim]\n\n"
|
|
940
|
+
|
|
941
|
+
elif action["type"] == "adk_tool_result":
|
|
942
|
+
details += f"[bold blue]━━━ Action {idx}: ADK Tool Result ━━━[/bold blue]\n"
|
|
943
|
+
details += f" 📤 [green]{action['content']}[/green]\n"
|
|
944
|
+
details += f" [dim]Line: {action['line_num']}[/dim]\n\n"
|
|
945
|
+
|
|
946
|
+
elif action["type"] == "llm_query":
|
|
947
|
+
details += f"[bold magenta]━━━ Action {idx}: LLM Query ━━━[/bold magenta]\n"
|
|
948
|
+
details += f" 🧠 [cyan]Model: {action['model']}[/cyan]\n"
|
|
949
|
+
details += f" [dim]Line: {action['line_num']}[/dim]\n\n"
|
|
950
|
+
|
|
951
|
+
# Show Full Execution Logs
|
|
952
|
+
details += (
|
|
953
|
+
"\n[bold cyan]╔════════════════════════════════════════╗[/bold cyan]\n"
|
|
954
|
+
)
|
|
955
|
+
details += "[bold cyan]║ 📝 FULL EXECUTION LOGS ║[/bold cyan]\n"
|
|
956
|
+
details += (
|
|
957
|
+
"[bold cyan]╚════════════════════════════════════════╝[/bold cyan]\n\n"
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# SHOW ALL LOGS - user can scroll
|
|
961
|
+
display_lines = log_lines
|
|
962
|
+
if status_display.upper() == "RUNNING":
|
|
963
|
+
details += "[bold yellow]⚡ LIVE LOGS[/bold yellow] [dim](Auto-refreshing every 5s)[/dim]\n\n"
|
|
964
|
+
else:
|
|
965
|
+
details += f"[dim]Total: {len(log_lines)} lines | Actions detected: {len(actions)}[/dim]\n\n"
|
|
966
|
+
|
|
967
|
+
# Process and display log lines with enhanced formatting
|
|
968
|
+
for line_num, line in enumerate(display_lines, 1):
|
|
969
|
+
line = line.strip()
|
|
970
|
+
if not line:
|
|
971
|
+
continue
|
|
972
|
+
|
|
973
|
+
# Add line numbers for context
|
|
974
|
+
line_prefix = f"[dim]{line_num:4d}[/dim] "
|
|
975
|
+
|
|
976
|
+
# Enhanced color coding with more patterns
|
|
977
|
+
if "ERROR" in line.upper() or "FAIL" in line.upper() or "❌" in line:
|
|
978
|
+
details += f"{line_prefix}[bold red]❌ {line}[/bold red]\n"
|
|
979
|
+
elif "CRITICAL" in line.upper():
|
|
980
|
+
details += f"{line_prefix}[bold red on white]🔥 {line}[/bold red on white]\n"
|
|
981
|
+
elif "WARN" in line.upper() or "WARNING" in line.upper():
|
|
982
|
+
details += f"{line_prefix}[bold yellow]⚠️ {line}[/bold yellow]\n"
|
|
983
|
+
elif (
|
|
984
|
+
"SUCCESS" in line.upper()
|
|
985
|
+
or "COMPLETE" in line.upper()
|
|
986
|
+
or "✅" in line
|
|
987
|
+
):
|
|
988
|
+
details += f"{line_prefix}[bold green]✅ {line}[/bold green]\n"
|
|
989
|
+
elif "HTTP" in line.upper() or "🌐" in line:
|
|
990
|
+
details += f"{line_prefix}[bold cyan]🌐 {line}[/bold cyan]\n"
|
|
991
|
+
elif "Tool" in line or "Function" in line or "🔧" in line:
|
|
992
|
+
details += f"{line_prefix}[bold green]🔧 {line}[/bold green]\n"
|
|
993
|
+
elif "ADK" in line or "🤖" in line:
|
|
994
|
+
details += f"{line_prefix}[bold blue]🤖 {line}[/bold blue]\n"
|
|
995
|
+
elif "LLM" in line or "model" in line.lower():
|
|
996
|
+
details += f"{line_prefix}[bold magenta]🧠 {line}[/bold magenta]\n"
|
|
997
|
+
elif "INFO" in line.upper() or "START" in line.upper():
|
|
998
|
+
details += f"{line_prefix}[cyan]ℹ️ {line}[/cyan]\n"
|
|
999
|
+
elif "DEBUG" in line.upper():
|
|
1000
|
+
details += f"{line_prefix}[dim]🔍 {line}[/dim]\n"
|
|
1001
|
+
elif line.startswith(">") or line.startswith("+"):
|
|
1002
|
+
details += f"{line_prefix}[green]{line}[/green]\n"
|
|
1003
|
+
elif line.startswith("<") or line.startswith("-"):
|
|
1004
|
+
details += f"{line_prefix}[red]{line}[/red]\n"
|
|
1005
|
+
else:
|
|
1006
|
+
details += f"{line_prefix}[dim]{line}[/dim]\n"
|
|
1007
|
+
|
|
1008
|
+
# Show result data if available - SHOW ALL DATA
|
|
1009
|
+
if hasattr(result, "data") and result.data:
|
|
1010
|
+
details += "\n[bold yellow]━━━ Result Data ━━━[/bold yellow]\n"
|
|
1011
|
+
try:
|
|
1012
|
+
if isinstance(result.data, dict):
|
|
1013
|
+
data_str = json.dumps(result.data, indent=2)
|
|
1014
|
+
# Color-code JSON for better readability - SHOW ALL
|
|
1015
|
+
lines = data_str.split("\n")
|
|
1016
|
+
formatted_lines = []
|
|
1017
|
+
for line in lines: # Show ALL lines
|
|
1018
|
+
if ":" in line and '"' in line:
|
|
1019
|
+
# Color keys and values differently
|
|
1020
|
+
parts = line.split(":", 1)
|
|
1021
|
+
if len(parts) == 2:
|
|
1022
|
+
key = parts[0]
|
|
1023
|
+
value = parts[1]
|
|
1024
|
+
formatted_lines.append(
|
|
1025
|
+
f"[bold yellow]{key}[/bold yellow]:[cyan]{value}[/cyan]"
|
|
1026
|
+
)
|
|
1027
|
+
else:
|
|
1028
|
+
formatted_lines.append(f"[dim]{line}[/dim]")
|
|
1029
|
+
elif line.strip() in ["{", "}", "[", "]", ","]:
|
|
1030
|
+
formatted_lines.append(f"[dim]{line}[/dim]")
|
|
1031
|
+
else:
|
|
1032
|
+
formatted_lines.append(f"{line}")
|
|
1033
|
+
details += "\n".join(formatted_lines) + "\n"
|
|
1034
|
+
else:
|
|
1035
|
+
details += f"[dim]{str(result.data)}[/dim]\n" # Show all
|
|
1036
|
+
except Exception:
|
|
1037
|
+
details += f"[dim]{str(result.data)}[/dim]\n"
|
|
1038
|
+
|
|
1039
|
+
details += (
|
|
1040
|
+
"\n\n[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]\n"
|
|
1041
|
+
)
|
|
1042
|
+
details += "[bold]💡 Quick Tips:[/bold]\n"
|
|
1043
|
+
details += " • [dim]This view auto-refreshes every 5 seconds[/dim]\n"
|
|
1044
|
+
details += " • [dim]Press [cyan]F5[/cyan] to refresh manually[/dim]\n"
|
|
1045
|
+
details += " • [dim]Use [cyan]📊 Export[/cyan] buttons to save results[/dim]\n"
|
|
1046
|
+
details += " • [dim]Select another row to view different results[/dim]\n"
|
|
1047
|
+
|
|
1048
|
+
details_widget.update(details)
|
|
1049
|
+
|
|
1050
|
+
def _export_results_csv(self) -> None:
|
|
1051
|
+
"""Export results to CSV file."""
|
|
1052
|
+
try:
|
|
1053
|
+
import csv
|
|
1054
|
+
from pathlib import Path
|
|
1055
|
+
|
|
1056
|
+
if not self.results_data:
|
|
1057
|
+
self.notify("No results to export", severity="warning")
|
|
1058
|
+
return
|
|
1059
|
+
|
|
1060
|
+
# Generate filename with timestamp
|
|
1061
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1062
|
+
filename = f"hackagent_results_{timestamp}.csv"
|
|
1063
|
+
filepath = Path.cwd() / filename
|
|
1064
|
+
|
|
1065
|
+
# Write CSV
|
|
1066
|
+
with open(filepath, "w", newline="") as csvfile:
|
|
1067
|
+
fieldnames = [
|
|
1068
|
+
"ID",
|
|
1069
|
+
"Agent",
|
|
1070
|
+
"Attack Type",
|
|
1071
|
+
"Status",
|
|
1072
|
+
"Created",
|
|
1073
|
+
"Duration",
|
|
1074
|
+
]
|
|
1075
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
1076
|
+
|
|
1077
|
+
writer.writeheader()
|
|
1078
|
+
for result in self.results_data:
|
|
1079
|
+
# Get status
|
|
1080
|
+
status = "Unknown"
|
|
1081
|
+
if hasattr(result, "evaluation_status"):
|
|
1082
|
+
status_val = result.evaluation_status
|
|
1083
|
+
status = (
|
|
1084
|
+
status_val.value
|
|
1085
|
+
if hasattr(status_val, "value")
|
|
1086
|
+
else str(status_val)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# Get created date
|
|
1090
|
+
created = "Unknown"
|
|
1091
|
+
if hasattr(result, "created_at") and result.created_at:
|
|
1092
|
+
created = str(result.created_at)
|
|
1093
|
+
|
|
1094
|
+
# Calculate duration
|
|
1095
|
+
duration = "N/A"
|
|
1096
|
+
if hasattr(result, "run") and result.run:
|
|
1097
|
+
run = result.run
|
|
1098
|
+
if (
|
|
1099
|
+
hasattr(run, "started_at")
|
|
1100
|
+
and run.started_at
|
|
1101
|
+
and hasattr(run, "completed_at")
|
|
1102
|
+
and run.completed_at
|
|
1103
|
+
):
|
|
1104
|
+
try:
|
|
1105
|
+
if isinstance(run.started_at, datetime) and isinstance(
|
|
1106
|
+
run.completed_at, datetime
|
|
1107
|
+
):
|
|
1108
|
+
delta = run.completed_at - run.started_at
|
|
1109
|
+
duration = f"{delta.total_seconds():.1f}s"
|
|
1110
|
+
except Exception:
|
|
1111
|
+
pass
|
|
1112
|
+
|
|
1113
|
+
writer.writerow(
|
|
1114
|
+
{
|
|
1115
|
+
"ID": str(result.id),
|
|
1116
|
+
"Agent": getattr(result, "agent_name", "Unknown"),
|
|
1117
|
+
"Attack Type": getattr(result, "attack_type", "Unknown"),
|
|
1118
|
+
"Status": status,
|
|
1119
|
+
"Created": created,
|
|
1120
|
+
"Duration": duration,
|
|
1121
|
+
}
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
self.notify(
|
|
1125
|
+
f"✅ Exported {len(self.results_data)} results to {filename}",
|
|
1126
|
+
severity="information",
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
self.notify(f"❌ Export failed: {str(e)}", severity="error")
|
|
1131
|
+
|
|
1132
|
+
def _export_results_json(self) -> None:
|
|
1133
|
+
"""Export results to JSON file."""
|
|
1134
|
+
try:
|
|
1135
|
+
from pathlib import Path
|
|
1136
|
+
|
|
1137
|
+
if not self.results_data:
|
|
1138
|
+
self.notify("No results to export", severity="warning")
|
|
1139
|
+
return
|
|
1140
|
+
|
|
1141
|
+
# Generate filename with timestamp
|
|
1142
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1143
|
+
filename = f"hackagent_results_{timestamp}.json"
|
|
1144
|
+
filepath = Path.cwd() / filename
|
|
1145
|
+
|
|
1146
|
+
# Convert results to dict
|
|
1147
|
+
results_list = []
|
|
1148
|
+
for result in self.results_data:
|
|
1149
|
+
result_dict = {
|
|
1150
|
+
"id": str(result.id),
|
|
1151
|
+
"agent_name": getattr(result, "agent_name", None),
|
|
1152
|
+
"attack_type": getattr(result, "attack_type", None),
|
|
1153
|
+
"created_at": str(result.created_at)
|
|
1154
|
+
if hasattr(result, "created_at")
|
|
1155
|
+
else None,
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
# Add status
|
|
1159
|
+
if hasattr(result, "evaluation_status"):
|
|
1160
|
+
status_val = result.evaluation_status
|
|
1161
|
+
result_dict["status"] = (
|
|
1162
|
+
status_val.value
|
|
1163
|
+
if hasattr(status_val, "value")
|
|
1164
|
+
else str(status_val)
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
# Add run information
|
|
1168
|
+
if hasattr(result, "run") and result.run:
|
|
1169
|
+
result_dict["run"] = {
|
|
1170
|
+
"id": str(result.run.id) if hasattr(result.run, "id") else None,
|
|
1171
|
+
"status": str(result.run.status)
|
|
1172
|
+
if hasattr(result.run, "status")
|
|
1173
|
+
else None,
|
|
1174
|
+
"started_at": str(result.run.started_at)
|
|
1175
|
+
if hasattr(result.run, "started_at")
|
|
1176
|
+
else None,
|
|
1177
|
+
"completed_at": str(result.run.completed_at)
|
|
1178
|
+
if hasattr(result.run, "completed_at")
|
|
1179
|
+
else None,
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
# Add config and data if available
|
|
1183
|
+
if hasattr(result, "attack_config"):
|
|
1184
|
+
result_dict["attack_config"] = result.attack_config
|
|
1185
|
+
if hasattr(result, "data"):
|
|
1186
|
+
result_dict["data"] = result.data
|
|
1187
|
+
if hasattr(result, "logs"):
|
|
1188
|
+
result_dict["logs"] = str(result.logs)
|
|
1189
|
+
|
|
1190
|
+
results_list.append(result_dict)
|
|
1191
|
+
|
|
1192
|
+
# Write JSON
|
|
1193
|
+
with open(filepath, "w") as jsonfile:
|
|
1194
|
+
json.dump(
|
|
1195
|
+
{
|
|
1196
|
+
"exported_at": datetime.now().isoformat(),
|
|
1197
|
+
"total_results": len(results_list),
|
|
1198
|
+
"results": results_list,
|
|
1199
|
+
},
|
|
1200
|
+
jsonfile,
|
|
1201
|
+
indent=2,
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
self.notify(
|
|
1205
|
+
f"✅ Exported {len(results_list)} results to {filename}",
|
|
1206
|
+
severity="information",
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
except Exception as e:
|
|
1210
|
+
self.notify(f"❌ Export failed: {str(e)}", severity="error")
|