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.
Files changed (183) hide show
  1. hackagent/__init__.py +12 -0
  2. hackagent/agent.py +214 -0
  3. hackagent/api/__init__.py +1 -0
  4. hackagent/api/agent/__init__.py +1 -0
  5. hackagent/api/agent/agent_create.py +347 -0
  6. hackagent/api/agent/agent_destroy.py +140 -0
  7. hackagent/api/agent/agent_list.py +242 -0
  8. hackagent/api/agent/agent_partial_update.py +361 -0
  9. hackagent/api/agent/agent_retrieve.py +235 -0
  10. hackagent/api/agent/agent_update.py +361 -0
  11. hackagent/api/apilogs/__init__.py +1 -0
  12. hackagent/api/apilogs/apilogs_list.py +170 -0
  13. hackagent/api/apilogs/apilogs_retrieve.py +162 -0
  14. hackagent/api/attack/__init__.py +1 -0
  15. hackagent/api/attack/attack_create.py +275 -0
  16. hackagent/api/attack/attack_destroy.py +146 -0
  17. hackagent/api/attack/attack_list.py +254 -0
  18. hackagent/api/attack/attack_partial_update.py +289 -0
  19. hackagent/api/attack/attack_retrieve.py +247 -0
  20. hackagent/api/attack/attack_update.py +289 -0
  21. hackagent/api/checkout/__init__.py +1 -0
  22. hackagent/api/checkout/checkout_create.py +225 -0
  23. hackagent/api/generate/__init__.py +1 -0
  24. hackagent/api/generate/generate_create.py +253 -0
  25. hackagent/api/judge/__init__.py +1 -0
  26. hackagent/api/judge/judge_create.py +253 -0
  27. hackagent/api/key/__init__.py +1 -0
  28. hackagent/api/key/key_create.py +179 -0
  29. hackagent/api/key/key_destroy.py +103 -0
  30. hackagent/api/key/key_list.py +170 -0
  31. hackagent/api/key/key_retrieve.py +162 -0
  32. hackagent/api/organization/__init__.py +1 -0
  33. hackagent/api/organization/organization_create.py +208 -0
  34. hackagent/api/organization/organization_destroy.py +104 -0
  35. hackagent/api/organization/organization_list.py +170 -0
  36. hackagent/api/organization/organization_me_retrieve.py +126 -0
  37. hackagent/api/organization/organization_partial_update.py +222 -0
  38. hackagent/api/organization/organization_retrieve.py +163 -0
  39. hackagent/api/organization/organization_update.py +222 -0
  40. hackagent/api/prompt/__init__.py +1 -0
  41. hackagent/api/prompt/prompt_create.py +171 -0
  42. hackagent/api/prompt/prompt_destroy.py +104 -0
  43. hackagent/api/prompt/prompt_list.py +185 -0
  44. hackagent/api/prompt/prompt_partial_update.py +185 -0
  45. hackagent/api/prompt/prompt_retrieve.py +163 -0
  46. hackagent/api/prompt/prompt_update.py +185 -0
  47. hackagent/api/result/__init__.py +1 -0
  48. hackagent/api/result/result_create.py +175 -0
  49. hackagent/api/result/result_destroy.py +106 -0
  50. hackagent/api/result/result_list.py +249 -0
  51. hackagent/api/result/result_partial_update.py +193 -0
  52. hackagent/api/result/result_retrieve.py +167 -0
  53. hackagent/api/result/result_trace_create.py +177 -0
  54. hackagent/api/result/result_update.py +189 -0
  55. hackagent/api/run/__init__.py +1 -0
  56. hackagent/api/run/run_create.py +187 -0
  57. hackagent/api/run/run_destroy.py +112 -0
  58. hackagent/api/run/run_list.py +291 -0
  59. hackagent/api/run/run_partial_update.py +201 -0
  60. hackagent/api/run/run_result_create.py +177 -0
  61. hackagent/api/run/run_retrieve.py +179 -0
  62. hackagent/api/run/run_run_tests_create.py +187 -0
  63. hackagent/api/run/run_update.py +201 -0
  64. hackagent/api/user/__init__.py +1 -0
  65. hackagent/api/user/user_create.py +212 -0
  66. hackagent/api/user/user_destroy.py +106 -0
  67. hackagent/api/user/user_list.py +174 -0
  68. hackagent/api/user/user_me_retrieve.py +126 -0
  69. hackagent/api/user/user_me_update.py +196 -0
  70. hackagent/api/user/user_partial_update.py +226 -0
  71. hackagent/api/user/user_retrieve.py +167 -0
  72. hackagent/api/user/user_update.py +226 -0
  73. hackagent/attacks/AdvPrefix/__init__.py +41 -0
  74. hackagent/attacks/AdvPrefix/completions.py +416 -0
  75. hackagent/attacks/AdvPrefix/config.py +259 -0
  76. hackagent/attacks/AdvPrefix/evaluation.py +745 -0
  77. hackagent/attacks/AdvPrefix/evaluators.py +564 -0
  78. hackagent/attacks/AdvPrefix/generate.py +711 -0
  79. hackagent/attacks/AdvPrefix/utils.py +307 -0
  80. hackagent/attacks/__init__.py +35 -0
  81. hackagent/attacks/advprefix.py +507 -0
  82. hackagent/attacks/base.py +106 -0
  83. hackagent/attacks/strategies.py +906 -0
  84. hackagent/cli/__init__.py +19 -0
  85. hackagent/cli/commands/__init__.py +20 -0
  86. hackagent/cli/commands/agent.py +100 -0
  87. hackagent/cli/commands/attack.py +417 -0
  88. hackagent/cli/commands/config.py +301 -0
  89. hackagent/cli/commands/results.py +327 -0
  90. hackagent/cli/config.py +249 -0
  91. hackagent/cli/main.py +515 -0
  92. hackagent/cli/tui/__init__.py +31 -0
  93. hackagent/cli/tui/actions_logger.py +200 -0
  94. hackagent/cli/tui/app.py +288 -0
  95. hackagent/cli/tui/base.py +137 -0
  96. hackagent/cli/tui/logger.py +318 -0
  97. hackagent/cli/tui/views/__init__.py +33 -0
  98. hackagent/cli/tui/views/agents.py +488 -0
  99. hackagent/cli/tui/views/attacks.py +624 -0
  100. hackagent/cli/tui/views/config.py +244 -0
  101. hackagent/cli/tui/views/dashboard.py +307 -0
  102. hackagent/cli/tui/views/results.py +1210 -0
  103. hackagent/cli/tui/widgets/__init__.py +24 -0
  104. hackagent/cli/tui/widgets/actions.py +346 -0
  105. hackagent/cli/tui/widgets/logs.py +435 -0
  106. hackagent/cli/utils.py +276 -0
  107. hackagent/client.py +286 -0
  108. hackagent/errors.py +37 -0
  109. hackagent/logger.py +83 -0
  110. hackagent/models/__init__.py +109 -0
  111. hackagent/models/agent.py +223 -0
  112. hackagent/models/agent_request.py +129 -0
  113. hackagent/models/api_token_log.py +184 -0
  114. hackagent/models/attack.py +154 -0
  115. hackagent/models/attack_request.py +82 -0
  116. hackagent/models/checkout_session_request_request.py +76 -0
  117. hackagent/models/checkout_session_response.py +59 -0
  118. hackagent/models/choice.py +81 -0
  119. hackagent/models/choice_message.py +67 -0
  120. hackagent/models/evaluation_status_enum.py +14 -0
  121. hackagent/models/generate_error_response.py +59 -0
  122. hackagent/models/generate_request_request.py +212 -0
  123. hackagent/models/generate_success_response.py +115 -0
  124. hackagent/models/generic_error_response.py +70 -0
  125. hackagent/models/message_request.py +67 -0
  126. hackagent/models/organization.py +102 -0
  127. hackagent/models/organization_minimal.py +68 -0
  128. hackagent/models/organization_request.py +71 -0
  129. hackagent/models/paginated_agent_list.py +123 -0
  130. hackagent/models/paginated_api_token_log_list.py +123 -0
  131. hackagent/models/paginated_attack_list.py +123 -0
  132. hackagent/models/paginated_organization_list.py +123 -0
  133. hackagent/models/paginated_prompt_list.py +123 -0
  134. hackagent/models/paginated_result_list.py +123 -0
  135. hackagent/models/paginated_run_list.py +123 -0
  136. hackagent/models/paginated_user_api_key_list.py +123 -0
  137. hackagent/models/paginated_user_profile_list.py +123 -0
  138. hackagent/models/patched_agent_request.py +128 -0
  139. hackagent/models/patched_attack_request.py +92 -0
  140. hackagent/models/patched_organization_request.py +71 -0
  141. hackagent/models/patched_prompt_request.py +125 -0
  142. hackagent/models/patched_result_request.py +237 -0
  143. hackagent/models/patched_run_request.py +138 -0
  144. hackagent/models/patched_user_profile_request.py +99 -0
  145. hackagent/models/prompt.py +220 -0
  146. hackagent/models/prompt_request.py +126 -0
  147. hackagent/models/result.py +294 -0
  148. hackagent/models/result_list_evaluation_status.py +14 -0
  149. hackagent/models/result_request.py +232 -0
  150. hackagent/models/run.py +233 -0
  151. hackagent/models/run_list_status.py +12 -0
  152. hackagent/models/run_request.py +133 -0
  153. hackagent/models/status_enum.py +12 -0
  154. hackagent/models/step_type_enum.py +14 -0
  155. hackagent/models/trace.py +121 -0
  156. hackagent/models/trace_request.py +94 -0
  157. hackagent/models/usage.py +75 -0
  158. hackagent/models/user_api_key.py +201 -0
  159. hackagent/models/user_api_key_request.py +73 -0
  160. hackagent/models/user_profile.py +135 -0
  161. hackagent/models/user_profile_minimal.py +76 -0
  162. hackagent/models/user_profile_request.py +99 -0
  163. hackagent/router/__init__.py +25 -0
  164. hackagent/router/adapters/__init__.py +20 -0
  165. hackagent/router/adapters/base.py +63 -0
  166. hackagent/router/adapters/google_adk.py +671 -0
  167. hackagent/router/adapters/litellm_adapter.py +524 -0
  168. hackagent/router/adapters/openai_adapter.py +426 -0
  169. hackagent/router/router.py +969 -0
  170. hackagent/router/types.py +54 -0
  171. hackagent/tracking/__init__.py +42 -0
  172. hackagent/tracking/context.py +163 -0
  173. hackagent/tracking/decorators.py +299 -0
  174. hackagent/tracking/tracker.py +441 -0
  175. hackagent/types.py +54 -0
  176. hackagent/utils.py +194 -0
  177. hackagent/vulnerabilities/__init__.py +13 -0
  178. hackagent/vulnerabilities/prompts.py +81 -0
  179. hackagent-0.3.1.dist-info/METADATA +122 -0
  180. hackagent-0.3.1.dist-info/RECORD +183 -0
  181. hackagent-0.3.1.dist-info/WHEEL +4 -0
  182. hackagent-0.3.1.dist-info/entry_points.txt +2 -0
  183. 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.