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,441 @@
|
|
|
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
|
+
Core tracking functionality.
|
|
17
|
+
|
|
18
|
+
This module provides the StepTracker class which handles the lifecycle
|
|
19
|
+
of operation tracking including trace creation, status updates, and
|
|
20
|
+
error handling. It integrates with the HackAgent backend API to maintain
|
|
21
|
+
synchronized state.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from contextlib import contextmanager
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
27
|
+
|
|
28
|
+
from hackagent.api.result import result_partial_update, result_trace_create
|
|
29
|
+
from hackagent.api.run import run_partial_update
|
|
30
|
+
from hackagent.models import (
|
|
31
|
+
EvaluationStatusEnum,
|
|
32
|
+
PatchedResultRequest,
|
|
33
|
+
PatchedRunRequest,
|
|
34
|
+
StatusEnum,
|
|
35
|
+
StepTypeEnum,
|
|
36
|
+
TraceRequest,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from .context import TrackingContext
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class StepTracker:
|
|
43
|
+
"""
|
|
44
|
+
Tracks operation execution and synchronizes with backend API.
|
|
45
|
+
|
|
46
|
+
This class manages the complete lifecycle of operation tracking:
|
|
47
|
+
- Creating trace records for each step
|
|
48
|
+
- Handling exceptions and updating error states
|
|
49
|
+
- Managing sequence counters for ordered operations
|
|
50
|
+
- Updating run and result statuses
|
|
51
|
+
|
|
52
|
+
The tracker is designed to fail gracefully - if tracking is disabled
|
|
53
|
+
or API calls fail, the underlying operations continue unaffected.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
context: TrackingContext containing tracking configuration
|
|
57
|
+
logger: Logger instance for tracking operations
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> context = TrackingContext(client=client, run_id="123", parent_result_id="456")
|
|
61
|
+
>>> tracker = StepTracker(context)
|
|
62
|
+
>>>
|
|
63
|
+
>>> with tracker.track_step("Process Data", "STEP1_PROCESS"):
|
|
64
|
+
... result = process_data()
|
|
65
|
+
>>>
|
|
66
|
+
>>> tracker.update_run_status(StatusEnum.COMPLETED)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, context: TrackingContext):
|
|
70
|
+
"""
|
|
71
|
+
Initialize the step tracker.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
context: TrackingContext instance with tracking configuration
|
|
75
|
+
"""
|
|
76
|
+
self.context = context
|
|
77
|
+
self.logger = context.logger
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def track_step(
|
|
81
|
+
self,
|
|
82
|
+
step_name: str,
|
|
83
|
+
step_type: str,
|
|
84
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
85
|
+
config: Optional[Dict[str, Any]] = None,
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
Context manager for tracking a single operation step.
|
|
89
|
+
|
|
90
|
+
This context manager handles the complete lifecycle of step tracking:
|
|
91
|
+
1. Creates a trace record at step start
|
|
92
|
+
2. Yields control to the caller
|
|
93
|
+
3. Handles exceptions and updates error states
|
|
94
|
+
4. Ensures proper cleanup
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
step_name: Human-readable step name
|
|
98
|
+
step_type: Step type identifier (e.g., "STEP1_GENERATE")
|
|
99
|
+
input_data: Optional input data sample for tracking
|
|
100
|
+
config: Optional configuration snapshot for this step
|
|
101
|
+
|
|
102
|
+
Yields:
|
|
103
|
+
trace_id: ID of the created trace record (or None if tracking disabled)
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> with tracker.track_step("Generate Prefixes", "STEP1_GENERATE"):
|
|
107
|
+
... prefixes = generate_prefixes(goals)
|
|
108
|
+
... # Step automatically tracked
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
Re-raises any exception from the tracked code block after
|
|
112
|
+
recording the error state.
|
|
113
|
+
"""
|
|
114
|
+
if not self.context.is_enabled:
|
|
115
|
+
# Tracking disabled, just yield and return
|
|
116
|
+
yield None
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
trace_id = None
|
|
120
|
+
try:
|
|
121
|
+
# Create trace record at step start
|
|
122
|
+
trace_id = self._create_trace(
|
|
123
|
+
step_name=step_name,
|
|
124
|
+
step_type=step_type,
|
|
125
|
+
input_data=input_data,
|
|
126
|
+
config=config,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Yield control to the tracked code block
|
|
130
|
+
yield trace_id
|
|
131
|
+
|
|
132
|
+
# Step completed successfully
|
|
133
|
+
self.logger.debug(f"Step '{step_name}' completed successfully")
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
# Handle step failure
|
|
137
|
+
self.logger.error(f"Step '{step_name}' failed: {e}", exc_info=True)
|
|
138
|
+
self._handle_step_error(step_name, str(e))
|
|
139
|
+
# Re-raise to allow caller to handle
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
def _create_trace(
|
|
143
|
+
self,
|
|
144
|
+
step_name: str,
|
|
145
|
+
step_type: str,
|
|
146
|
+
input_data: Optional[Dict[str, Any]],
|
|
147
|
+
config: Optional[Dict[str, Any]],
|
|
148
|
+
) -> Optional[str]:
|
|
149
|
+
"""
|
|
150
|
+
Create a trace record on the backend API.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
step_name: Human-readable step name
|
|
154
|
+
step_type: Step type identifier
|
|
155
|
+
input_data: Optional input data sample
|
|
156
|
+
config: Optional configuration snapshot
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Trace ID if successful, None otherwise
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
sequence = self.context.increment_sequence()
|
|
163
|
+
|
|
164
|
+
# Prepare trace content
|
|
165
|
+
trace_content = {
|
|
166
|
+
"step_name": step_name,
|
|
167
|
+
"step_type_identifier": step_type,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if config is not None:
|
|
171
|
+
trace_content["config_snapshot"] = self._sanitize_config(config)
|
|
172
|
+
|
|
173
|
+
if input_data is not None:
|
|
174
|
+
trace_content["input_data_sample"] = input_data
|
|
175
|
+
|
|
176
|
+
# Add any additional metadata
|
|
177
|
+
if self.context.metadata:
|
|
178
|
+
trace_content["context_metadata"] = self.context.metadata
|
|
179
|
+
|
|
180
|
+
# Create trace request
|
|
181
|
+
trace_request = TraceRequest(
|
|
182
|
+
sequence=sequence,
|
|
183
|
+
step_type=StepTypeEnum.OTHER,
|
|
184
|
+
content=trace_content,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Call API
|
|
188
|
+
result_uuid = self.context.get_result_uuid()
|
|
189
|
+
if not result_uuid:
|
|
190
|
+
self.logger.warning(
|
|
191
|
+
f"Cannot create trace for '{step_name}': invalid result UUID"
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
response = result_trace_create.sync_detailed(
|
|
196
|
+
client=self.context.client,
|
|
197
|
+
id=result_uuid,
|
|
198
|
+
body=trace_request,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Parse response
|
|
202
|
+
if response.status_code == 201:
|
|
203
|
+
trace_id = self._extract_trace_id(response, step_name)
|
|
204
|
+
if trace_id:
|
|
205
|
+
self.logger.info(
|
|
206
|
+
f"Created trace for '{step_name}' (ID: {trace_id}, seq: {sequence})"
|
|
207
|
+
)
|
|
208
|
+
return trace_id
|
|
209
|
+
else:
|
|
210
|
+
self.logger.error(
|
|
211
|
+
f"Failed to create trace for '{step_name}': status={response.status_code}, body={response.content}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.logger.error(
|
|
216
|
+
f"Exception creating trace for '{step_name}': {e}", exc_info=True
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def _extract_trace_id(self, response, step_name: str) -> Optional[str]:
|
|
222
|
+
"""
|
|
223
|
+
Extract trace ID from API response.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
response: API response object
|
|
227
|
+
step_name: Step name for logging
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Trace ID if found, None otherwise
|
|
231
|
+
"""
|
|
232
|
+
# Try parsed response first
|
|
233
|
+
if response.parsed and hasattr(response.parsed, "id"):
|
|
234
|
+
return str(response.parsed.id)
|
|
235
|
+
|
|
236
|
+
# Try parsing raw content
|
|
237
|
+
try:
|
|
238
|
+
response_data = json.loads(response.content.decode())
|
|
239
|
+
if "id" in response_data:
|
|
240
|
+
return str(response_data["id"])
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self.logger.warning(f"Could not parse trace ID for '{step_name}': {e}")
|
|
243
|
+
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
def _sanitize_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
247
|
+
"""
|
|
248
|
+
Remove sensitive information from config before sending to API.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
config: Configuration dictionary
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Sanitized configuration dictionary
|
|
255
|
+
"""
|
|
256
|
+
sensitive_keys = {"api_key", "token", "secret", "password", "key"}
|
|
257
|
+
|
|
258
|
+
sanitized = {}
|
|
259
|
+
for key, value in config.items():
|
|
260
|
+
key_lower = key.lower()
|
|
261
|
+
if any(sensitive in key_lower for sensitive in sensitive_keys):
|
|
262
|
+
sanitized[key] = "***REDACTED***"
|
|
263
|
+
elif isinstance(value, dict):
|
|
264
|
+
sanitized[key] = self._sanitize_config(value)
|
|
265
|
+
else:
|
|
266
|
+
sanitized[key] = value
|
|
267
|
+
|
|
268
|
+
return sanitized
|
|
269
|
+
|
|
270
|
+
def _handle_step_error(self, step_name: str, error_message: str) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Update backend with step error information.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
step_name: Name of the failed step
|
|
276
|
+
error_message: Error message
|
|
277
|
+
"""
|
|
278
|
+
if not self.context.is_enabled:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
result_uuid = self.context.get_result_uuid()
|
|
283
|
+
if not result_uuid:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
error_request = PatchedResultRequest(
|
|
287
|
+
evaluation_status=EvaluationStatusEnum.ERROR_TEST_FRAMEWORK,
|
|
288
|
+
evaluation_notes=f"Pipeline failed at '{step_name}': {error_message}",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
result_partial_update.sync_detailed(
|
|
292
|
+
client=self.context.client,
|
|
293
|
+
id=result_uuid,
|
|
294
|
+
body=error_request,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
self.logger.info(f"Updated result with error status for '{step_name}'")
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
self.logger.error(f"Failed to update error status: {e}", exc_info=True)
|
|
301
|
+
|
|
302
|
+
def update_run_status(self, status: StatusEnum) -> bool:
|
|
303
|
+
"""
|
|
304
|
+
Update the run status on the backend.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
status: New status to set
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
True if update was successful, False otherwise
|
|
311
|
+
"""
|
|
312
|
+
if not self.context.is_enabled:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
run_uuid = self.context.get_run_uuid()
|
|
317
|
+
if not run_uuid:
|
|
318
|
+
self.logger.warning("Cannot update run status: invalid run UUID")
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
run_request = PatchedRunRequest(status=status)
|
|
322
|
+
|
|
323
|
+
response = run_partial_update.sync_detailed(
|
|
324
|
+
client=self.context.client,
|
|
325
|
+
id=run_uuid,
|
|
326
|
+
body=run_request,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if response.status_code < 300:
|
|
330
|
+
self.logger.info(f"Updated run {self.context.run_id} to {status.value}")
|
|
331
|
+
return True
|
|
332
|
+
else:
|
|
333
|
+
self.logger.error(
|
|
334
|
+
f"Failed to update run status: status={response.status_code}, body={response.content}"
|
|
335
|
+
)
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
self.logger.error(f"Exception updating run status: {e}", exc_info=True)
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
def update_result_status(
|
|
343
|
+
self,
|
|
344
|
+
evaluation_status: EvaluationStatusEnum,
|
|
345
|
+
evaluation_notes: Optional[str] = None,
|
|
346
|
+
agent_specific_data: Optional[Dict[str, Any]] = None,
|
|
347
|
+
) -> bool:
|
|
348
|
+
"""
|
|
349
|
+
Update the result evaluation status.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
evaluation_status: Evaluation status to set
|
|
353
|
+
evaluation_notes: Optional notes about the evaluation
|
|
354
|
+
agent_specific_data: Optional agent-specific result data
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
True if update was successful, False otherwise
|
|
358
|
+
"""
|
|
359
|
+
if not self.context.is_enabled:
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
result_uuid = self.context.get_result_uuid()
|
|
364
|
+
if not result_uuid:
|
|
365
|
+
self.logger.warning("Cannot update result status: invalid result UUID")
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
result_request = PatchedResultRequest(
|
|
369
|
+
evaluation_status=evaluation_status,
|
|
370
|
+
evaluation_notes=evaluation_notes,
|
|
371
|
+
agent_specific_data=agent_specific_data,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
response = result_partial_update.sync_detailed(
|
|
375
|
+
client=self.context.client,
|
|
376
|
+
id=result_uuid,
|
|
377
|
+
body=result_request,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if response.status_code < 300:
|
|
381
|
+
self.logger.info(
|
|
382
|
+
f"Updated result {self.context.parent_result_id} to {evaluation_status.value}"
|
|
383
|
+
)
|
|
384
|
+
return True
|
|
385
|
+
else:
|
|
386
|
+
self.logger.error(
|
|
387
|
+
f"Failed to update result status: status={response.status_code}, body={response.content}"
|
|
388
|
+
)
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
self.logger.error(f"Exception updating result status: {e}", exc_info=True)
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
def add_step_metadata(self, key: str, value: Any) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Add metadata that will be included in the next trace.
|
|
398
|
+
|
|
399
|
+
This allows steps to record additional information like:
|
|
400
|
+
- Item counts (e.g., "prefixes_generated": 150)
|
|
401
|
+
- Processing stats (e.g., "success_rate": 0.85)
|
|
402
|
+
- Warnings (e.g., "empty_results": True)
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
key: Metadata key
|
|
406
|
+
value: Metadata value (must be JSON-serializable)
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> tracker.add_step_metadata("items_processed", 100)
|
|
410
|
+
>>> tracker.add_step_metadata("warning", "Some items filtered")
|
|
411
|
+
"""
|
|
412
|
+
if "step_metadata" not in self.context.metadata:
|
|
413
|
+
self.context.metadata["step_metadata"] = {}
|
|
414
|
+
self.context.metadata["step_metadata"][key] = value
|
|
415
|
+
|
|
416
|
+
def record_progress(self, message: str, **metrics) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Record progress information during step execution.
|
|
419
|
+
|
|
420
|
+
This is useful for long-running operations to track intermediate
|
|
421
|
+
progress without cluttering logs. Information is added to context
|
|
422
|
+
metadata and will be included in the next trace update.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
message: Progress message
|
|
426
|
+
**metrics: Additional metrics as keyword arguments
|
|
427
|
+
|
|
428
|
+
Example:
|
|
429
|
+
>>> tracker.record_progress("Processing batch 1/10", items=50, errors=0)
|
|
430
|
+
"""
|
|
431
|
+
if "progress_log" not in self.context.metadata:
|
|
432
|
+
self.context.metadata["progress_log"] = []
|
|
433
|
+
|
|
434
|
+
progress_entry = {"message": message, **metrics}
|
|
435
|
+
self.context.metadata["progress_log"].append(progress_entry)
|
|
436
|
+
|
|
437
|
+
# Keep only last 20 entries to avoid bloat
|
|
438
|
+
if len(self.context.metadata["progress_log"]) > 20:
|
|
439
|
+
self.context.metadata["progress_log"] = self.context.metadata[
|
|
440
|
+
"progress_log"
|
|
441
|
+
][-20:]
|
hackagent/types.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Contains some shared types for properties"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, MutableMapping
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
from attrs import define
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Unset:
|
|
11
|
+
def __bool__(self) -> Literal[False]:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UNSET: Unset = Unset()
|
|
16
|
+
|
|
17
|
+
# The types that `httpx.Client(files=)` can accept, copied from that library.
|
|
18
|
+
FileContent = Union[IO[bytes], bytes, str]
|
|
19
|
+
FileTypes = Union[
|
|
20
|
+
# (filename, file (or bytes), content_type)
|
|
21
|
+
tuple[Optional[str], FileContent, Optional[str]],
|
|
22
|
+
# (filename, file (or bytes), content_type, headers)
|
|
23
|
+
tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
|
|
24
|
+
]
|
|
25
|
+
RequestFiles = list[tuple[str, FileTypes]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@define
|
|
29
|
+
class File:
|
|
30
|
+
"""Contains information for file uploads"""
|
|
31
|
+
|
|
32
|
+
payload: BinaryIO
|
|
33
|
+
file_name: Optional[str] = None
|
|
34
|
+
mime_type: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
def to_tuple(self) -> FileTypes:
|
|
37
|
+
"""Return a tuple representation that httpx will accept for multipart/form-data"""
|
|
38
|
+
return self.file_name, self.payload, self.mime_type
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@define
|
|
45
|
+
class Response(Generic[T]):
|
|
46
|
+
"""A response from an endpoint"""
|
|
47
|
+
|
|
48
|
+
status_code: HTTPStatus
|
|
49
|
+
content: bytes
|
|
50
|
+
headers: MutableMapping[str, str]
|
|
51
|
+
parsed: Optional[T]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
|
hackagent/utils.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, Union
|
|
20
|
+
|
|
21
|
+
from dotenv import find_dotenv, load_dotenv
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
|
|
26
|
+
from hackagent.router.types import AgentTypeEnum
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
HACKAGENT = """
|
|
32
|
+
██╗ ██╗ █████╗ ██████╗██╗ ██╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗
|
|
33
|
+
██║ ██║██╔══██╗██╔════╝██║ ██╔╝██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
|
|
34
|
+
███████║███████║██║ █████╔╝ ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║
|
|
35
|
+
██╔══██║██╔══██║██║ ██╔═██╗ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║
|
|
36
|
+
██║ ██║██║ ██║╚██████╗██║ ██╗██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║
|
|
37
|
+
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def display_hackagent_splash():
|
|
42
|
+
"""Displays the HackAgent splash screen using the pre-defined ASCII art."""
|
|
43
|
+
console = Console()
|
|
44
|
+
|
|
45
|
+
# Create a Text object from the HACKAGENT string
|
|
46
|
+
title_content = Text(HACKAGENT, style="bold dark_red")
|
|
47
|
+
|
|
48
|
+
splash_panel = Panel(
|
|
49
|
+
title_content,
|
|
50
|
+
border_style="red",
|
|
51
|
+
padding=(2, 2),
|
|
52
|
+
expand=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
console.print(splash_panel)
|
|
56
|
+
console.print()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_agent_type(agent_type_input: Union[AgentTypeEnum, str]) -> AgentTypeEnum:
|
|
60
|
+
"""Resolves the agent type from a string or AgentTypeEnum member."""
|
|
61
|
+
if isinstance(agent_type_input, str):
|
|
62
|
+
try:
|
|
63
|
+
# Convert to uppercase and replace hyphens with underscores for enum matching
|
|
64
|
+
return AgentTypeEnum[agent_type_input.upper().replace("-", "_")]
|
|
65
|
+
except KeyError:
|
|
66
|
+
logger.warning(
|
|
67
|
+
f"Invalid agent_type string: '{agent_type_input}'. Falling back to UNKNOWN. "
|
|
68
|
+
f"Valid types are: {[member.name for member in AgentTypeEnum]}"
|
|
69
|
+
)
|
|
70
|
+
return AgentTypeEnum.UNKNOWN
|
|
71
|
+
elif isinstance(agent_type_input, AgentTypeEnum):
|
|
72
|
+
return agent_type_input
|
|
73
|
+
else:
|
|
74
|
+
logger.warning(
|
|
75
|
+
f"Invalid agent_type type: {type(agent_type_input)}. Falling back to UNKNOWN."
|
|
76
|
+
)
|
|
77
|
+
return AgentTypeEnum.UNKNOWN
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_api_token(
|
|
81
|
+
direct_api_key_param: Optional[str],
|
|
82
|
+
env_file_path: Optional[str] = None,
|
|
83
|
+
config_file_path: Optional[str] = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Resolves the API token with standardized priority order.
|
|
87
|
+
|
|
88
|
+
Priority order:
|
|
89
|
+
1. Direct api_key parameter (highest priority)
|
|
90
|
+
2. Config file (~/.hackagent/config.json or specified path)
|
|
91
|
+
3. Environment variable (HACKAGENT_API_KEY, with .env file support)
|
|
92
|
+
4. Error if not found (lowest priority)
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
direct_api_key_param: API key provided directly as parameter
|
|
96
|
+
env_file_path: Optional path to .env file to load environment variables from
|
|
97
|
+
config_file_path: Optional path to config file (defaults to ~/.hackagent/config.json)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: The resolved API token
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If no API token can be found from any source
|
|
104
|
+
"""
|
|
105
|
+
# Priority 1: Direct parameter
|
|
106
|
+
if direct_api_key_param is not None:
|
|
107
|
+
logger.debug("Using API token provided directly via 'api_key' parameter.")
|
|
108
|
+
return direct_api_key_param
|
|
109
|
+
|
|
110
|
+
# Priority 2: Config file
|
|
111
|
+
api_token_from_config = _load_api_key_from_config(config_file_path)
|
|
112
|
+
if api_token_from_config:
|
|
113
|
+
logger.debug("Using API token from config file.")
|
|
114
|
+
return api_token_from_config
|
|
115
|
+
|
|
116
|
+
# Priority 3: Environment variable (with .env file support)
|
|
117
|
+
api_token_from_env = _load_api_key_from_env(env_file_path)
|
|
118
|
+
if api_token_from_env:
|
|
119
|
+
logger.debug("Using API token from HACKAGENT_API_KEY environment variable.")
|
|
120
|
+
return api_token_from_env
|
|
121
|
+
|
|
122
|
+
# Priority 4: Error - no token found
|
|
123
|
+
error_message = (
|
|
124
|
+
"API token not found from any source. Tried:\n"
|
|
125
|
+
"1. Direct 'api_key' parameter\n"
|
|
126
|
+
"2. Config file (~/.hackagent/config.json)\n"
|
|
127
|
+
"3. HACKAGENT_API_KEY environment variable\n"
|
|
128
|
+
"\nTo fix: Set HACKAGENT_API_KEY, create config file, or pass api_key directly."
|
|
129
|
+
)
|
|
130
|
+
raise ValueError(error_message)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _load_api_key_from_config(config_file_path: Optional[str] = None) -> Optional[str]:
|
|
134
|
+
"""Load API key from config file with standardized logic."""
|
|
135
|
+
try:
|
|
136
|
+
if config_file_path:
|
|
137
|
+
config_path = Path(config_file_path)
|
|
138
|
+
else:
|
|
139
|
+
config_path = Path.home() / ".hackagent" / "config.json"
|
|
140
|
+
|
|
141
|
+
if not config_path.exists():
|
|
142
|
+
logger.debug(f"Config file not found at: {config_path}")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
logger.debug(f"Loading config from: {config_path}")
|
|
146
|
+
|
|
147
|
+
with open(config_path) as f:
|
|
148
|
+
if config_path.suffix.lower() in [".yaml", ".yml"]:
|
|
149
|
+
try:
|
|
150
|
+
import yaml
|
|
151
|
+
|
|
152
|
+
config_data = yaml.safe_load(f)
|
|
153
|
+
except ImportError:
|
|
154
|
+
logger.warning("PyYAML not available, cannot load YAML config file")
|
|
155
|
+
return None
|
|
156
|
+
else:
|
|
157
|
+
config_data = json.load(f)
|
|
158
|
+
|
|
159
|
+
api_key = config_data.get("api_key")
|
|
160
|
+
if api_key:
|
|
161
|
+
logger.debug(f"Found API key in config file: {config_path}")
|
|
162
|
+
return api_key
|
|
163
|
+
else:
|
|
164
|
+
logger.debug(f"No api_key found in config file: {config_path}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.warning(f"Error loading config file: {e}")
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _load_api_key_from_env(env_file_path: Optional[str] = None) -> Optional[str]:
|
|
173
|
+
"""Load API key from environment variables with .env file support."""
|
|
174
|
+
try:
|
|
175
|
+
# Load .env file if specified or found
|
|
176
|
+
dotenv_to_load = env_file_path or find_dotenv(usecwd=True)
|
|
177
|
+
|
|
178
|
+
if dotenv_to_load:
|
|
179
|
+
logger.debug(f"Loading .env file from: {dotenv_to_load}")
|
|
180
|
+
load_dotenv(dotenv_to_load)
|
|
181
|
+
else:
|
|
182
|
+
logger.debug("No .env file found to load.")
|
|
183
|
+
|
|
184
|
+
api_token = os.getenv("HACKAGENT_API_KEY")
|
|
185
|
+
if api_token:
|
|
186
|
+
logger.debug("Found API key in HACKAGENT_API_KEY environment variable")
|
|
187
|
+
return api_token
|
|
188
|
+
else:
|
|
189
|
+
logger.debug("HACKAGENT_API_KEY environment variable not set")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.warning(f"Error loading environment variables: {e}")
|
|
194
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
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.
|