hackagent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. hackagent/__init__.py +23 -0
  2. hackagent/agent.py +193 -0
  3. hackagent/api/__init__.py +1 -0
  4. hackagent/api/agent/__init__.py +1 -0
  5. hackagent/api/agent/agent_create.py +340 -0
  6. hackagent/api/agent/agent_destroy.py +136 -0
  7. hackagent/api/agent/agent_list.py +234 -0
  8. hackagent/api/agent/agent_partial_update.py +354 -0
  9. hackagent/api/agent/agent_retrieve.py +227 -0
  10. hackagent/api/agent/agent_update.py +354 -0
  11. hackagent/api/attack/__init__.py +1 -0
  12. hackagent/api/attack/attack_create.py +264 -0
  13. hackagent/api/attack/attack_destroy.py +140 -0
  14. hackagent/api/attack/attack_list.py +242 -0
  15. hackagent/api/attack/attack_partial_update.py +278 -0
  16. hackagent/api/attack/attack_retrieve.py +235 -0
  17. hackagent/api/attack/attack_update.py +278 -0
  18. hackagent/api/key/__init__.py +1 -0
  19. hackagent/api/key/key_create.py +168 -0
  20. hackagent/api/key/key_destroy.py +97 -0
  21. hackagent/api/key/key_list.py +158 -0
  22. hackagent/api/key/key_retrieve.py +150 -0
  23. hackagent/api/prompt/__init__.py +1 -0
  24. hackagent/api/prompt/prompt_create.py +160 -0
  25. hackagent/api/prompt/prompt_destroy.py +98 -0
  26. hackagent/api/prompt/prompt_list.py +173 -0
  27. hackagent/api/prompt/prompt_partial_update.py +174 -0
  28. hackagent/api/prompt/prompt_retrieve.py +151 -0
  29. hackagent/api/prompt/prompt_update.py +174 -0
  30. hackagent/api/result/__init__.py +1 -0
  31. hackagent/api/result/result_create.py +160 -0
  32. hackagent/api/result/result_destroy.py +98 -0
  33. hackagent/api/result/result_list.py +233 -0
  34. hackagent/api/result/result_partial_update.py +178 -0
  35. hackagent/api/result/result_retrieve.py +151 -0
  36. hackagent/api/result/result_trace_create.py +178 -0
  37. hackagent/api/result/result_update.py +174 -0
  38. hackagent/api/run/__init__.py +1 -0
  39. hackagent/api/run/run_create.py +172 -0
  40. hackagent/api/run/run_destroy.py +104 -0
  41. hackagent/api/run/run_list.py +260 -0
  42. hackagent/api/run/run_partial_update.py +186 -0
  43. hackagent/api/run/run_result_create.py +178 -0
  44. hackagent/api/run/run_retrieve.py +163 -0
  45. hackagent/api/run/run_run_tests_create.py +172 -0
  46. hackagent/api/run/run_update.py +186 -0
  47. hackagent/attacks/AdvPrefix/README.md +7 -0
  48. hackagent/attacks/AdvPrefix/__init__.py +0 -0
  49. hackagent/attacks/AdvPrefix/completer.py +438 -0
  50. hackagent/attacks/AdvPrefix/config.py +59 -0
  51. hackagent/attacks/AdvPrefix/preprocessing.py +521 -0
  52. hackagent/attacks/AdvPrefix/scorer.py +259 -0
  53. hackagent/attacks/AdvPrefix/scorer_parser.py +498 -0
  54. hackagent/attacks/AdvPrefix/selector.py +246 -0
  55. hackagent/attacks/AdvPrefix/step1_generate.py +324 -0
  56. hackagent/attacks/AdvPrefix/step4_compute_ce.py +293 -0
  57. hackagent/attacks/AdvPrefix/step6_get_completions.py +387 -0
  58. hackagent/attacks/AdvPrefix/step7_evaluate_responses.py +289 -0
  59. hackagent/attacks/AdvPrefix/step8_aggregate_evaluations.py +177 -0
  60. hackagent/attacks/AdvPrefix/step9_select_prefixes.py +59 -0
  61. hackagent/attacks/AdvPrefix/utils.py +192 -0
  62. hackagent/attacks/__init__.py +6 -0
  63. hackagent/attacks/advprefix.py +1136 -0
  64. hackagent/attacks/base.py +50 -0
  65. hackagent/attacks/strategies.py +539 -0
  66. hackagent/branding.py +143 -0
  67. hackagent/client.py +328 -0
  68. hackagent/errors.py +31 -0
  69. hackagent/logger.py +67 -0
  70. hackagent/models/__init__.py +71 -0
  71. hackagent/models/agent.py +240 -0
  72. hackagent/models/agent_request.py +169 -0
  73. hackagent/models/agent_type_enum.py +12 -0
  74. hackagent/models/attack.py +154 -0
  75. hackagent/models/attack_request.py +82 -0
  76. hackagent/models/evaluation_status_enum.py +14 -0
  77. hackagent/models/organization_minimal.py +68 -0
  78. hackagent/models/paginated_agent_list.py +123 -0
  79. hackagent/models/paginated_attack_list.py +123 -0
  80. hackagent/models/paginated_prompt_list.py +123 -0
  81. hackagent/models/paginated_result_list.py +123 -0
  82. hackagent/models/paginated_run_list.py +123 -0
  83. hackagent/models/paginated_user_api_key_list.py +123 -0
  84. hackagent/models/patched_agent_request.py +176 -0
  85. hackagent/models/patched_attack_request.py +92 -0
  86. hackagent/models/patched_prompt_request.py +162 -0
  87. hackagent/models/patched_result_request.py +237 -0
  88. hackagent/models/patched_run_request.py +138 -0
  89. hackagent/models/prompt.py +226 -0
  90. hackagent/models/prompt_request.py +155 -0
  91. hackagent/models/result.py +294 -0
  92. hackagent/models/result_list_evaluation_status.py +14 -0
  93. hackagent/models/result_request.py +232 -0
  94. hackagent/models/run.py +233 -0
  95. hackagent/models/run_list_status.py +12 -0
  96. hackagent/models/run_request.py +133 -0
  97. hackagent/models/status_enum.py +12 -0
  98. hackagent/models/step_type_enum.py +14 -0
  99. hackagent/models/trace.py +121 -0
  100. hackagent/models/trace_request.py +94 -0
  101. hackagent/models/user_api_key.py +201 -0
  102. hackagent/models/user_api_key_request.py +73 -0
  103. hackagent/models/user_profile_minimal.py +76 -0
  104. hackagent/py.typed +1 -0
  105. hackagent/router/__init__.py +11 -0
  106. hackagent/router/adapters/__init__.py +5 -0
  107. hackagent/router/adapters/google_adk.py +658 -0
  108. hackagent/router/adapters/litellm_adapter.py +290 -0
  109. hackagent/router/base.py +48 -0
  110. hackagent/router/router.py +753 -0
  111. hackagent/types.py +46 -0
  112. hackagent/utils.py +61 -0
  113. hackagent/vulnerabilities/__init__.py +0 -0
  114. hackagent-0.1.0.dist-info/LICENSE +202 -0
  115. hackagent-0.1.0.dist-info/METADATA +173 -0
  116. hackagent-0.1.0.dist-info/RECORD +117 -0
  117. hackagent-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,290 @@
1
+ from hackagent.router.base import Agent
2
+ from typing import Any, Dict, Optional, List
3
+ import logging
4
+ import asyncio # Required for async handle_request
5
+ import litellm
6
+
7
+ import os
8
+ # from rich.progress import Progress # Removed Progress import
9
+
10
+
11
+ # --- Custom Exceptions ---
12
+ class LiteLLMConfigurationError(Exception):
13
+ """Custom exception for LiteLLM adapter configuration issues."""
14
+
15
+ pass
16
+
17
+
18
+ logger = logging.getLogger(__name__) # Module-level logger
19
+
20
+
21
+ class LiteLLMAgentAdapter(Agent):
22
+ """
23
+ Adapter for interacting with LLMs via the LiteLLM library.
24
+ """
25
+
26
+ def __init__(self, id: str, config: Dict[str, Any]):
27
+ """
28
+ Initializes the LiteLLMAgentAdapter.
29
+
30
+ Args:
31
+ id: The unique identifier for this LiteLLM agent instance.
32
+ config: Configuration dictionary for the LiteLLM agent.
33
+ Expected keys:
34
+ - 'name': Model string for LiteLLM (e.g., "ollama/llama3").
35
+ - 'endpoint' (optional): Base URL for the API.
36
+ - 'api_key' (optional): Name of the environment variable holding the API key.
37
+ - 'max_new_tokens' (optional): Default max tokens for generation (defaults to 100).
38
+ - 'temperature' (optional): Default temperature (defaults to 0.8).
39
+ - 'top_p' (optional): Default top_p (defaults to 0.95).
40
+ """
41
+ super().__init__(id, config)
42
+ self.logger = logging.getLogger(
43
+ f"{__name__}.{self.id}"
44
+ ) # Instance-specific logger
45
+
46
+ if "name" not in self.config:
47
+ msg = (
48
+ f"Missing required configuration key 'name' (for model string) for "
49
+ f"LiteLLMAgentAdapter: {self.id}"
50
+ )
51
+ self.logger.error(msg)
52
+ raise LiteLLMConfigurationError(msg)
53
+
54
+ self.model_name: str = self.config["name"]
55
+ self.api_base_url: Optional[str] = self.config.get("endpoint")
56
+
57
+ api_key: Optional[str] = self.config.get("api_key")
58
+ self.actual_api_key: Optional[str] = (
59
+ os.environ.get(api_key) if api_key else None
60
+ )
61
+
62
+ self.logger.info(
63
+ f"LiteLLMAgentAdapter '{self.id}' initialized for model: '{self.model_name}'"
64
+ + (f" API Base: '{self.api_base_url}'" if self.api_base_url else "")
65
+ + (f" API Key: '{api_key}'" if api_key else "")
66
+ )
67
+
68
+ # Store default generation parameters
69
+ self.default_max_new_tokens = self.config.get("max_new_tokens", 100)
70
+ self.default_temperature = self.config.get("temperature", 0.8)
71
+ self.default_top_p = self.config.get("top_p", 0.95)
72
+
73
+ async def _execute_litellm_completion(
74
+ self,
75
+ texts: List[str],
76
+ max_new_tokens: int,
77
+ temperature: float,
78
+ top_p: float,
79
+ **kwargs,
80
+ ) -> List[str]:
81
+ """
82
+ Internal method to generate completions using litellm.completion.
83
+ """
84
+ if not texts:
85
+ return []
86
+
87
+ completions = []
88
+ self.logger.info(
89
+ f"Sending {len(texts)} requests via LiteLLM to model '{self.model_name}'..."
90
+ )
91
+
92
+ # Removed Progress wrapper as it can conflict with outer progress bars
93
+ for text_prompt in texts:
94
+ messages = [{"role": "user", "content": text_prompt}]
95
+
96
+ try:
97
+ litellm_params = {
98
+ "model": self.model_name,
99
+ "messages": messages,
100
+ "max_tokens": max_new_tokens,
101
+ "temperature": temperature,
102
+ "top_p": top_p,
103
+ "api_base": self.api_base_url,
104
+ "api_key": self.actual_api_key,
105
+ }
106
+ # Merge any additional kwargs passed directly for litellm.completion
107
+ litellm_params.update(kwargs)
108
+
109
+ # Filter out None values from litellm_params as litellm might not like them for all keys
110
+ # Specifically, api_base and api_key can be None if not provided.
111
+ # LiteLLM handles None for api_base and api_key appropriately.
112
+ # litellm_params = {k: v for k, v in litellm_params.items() if v is not None}
113
+
114
+ max_retries = 3
115
+ retry_delay = 2 # seconds
116
+ for attempt in range(max_retries):
117
+ try:
118
+ response = await asyncio.to_thread(
119
+ litellm.completion, **litellm_params
120
+ )
121
+ # response = litellm.completion(**litellm_params) # original sync call
122
+
123
+ if (
124
+ response
125
+ and response.choices
126
+ and response.choices[0].message
127
+ and response.choices[0].message.content
128
+ ):
129
+ completion_text = response.choices[0].message.content
130
+ else:
131
+ self.logger.warning(
132
+ f"LiteLLM received unexpected response structure for model '{self.model_name}'. Response: {response}"
133
+ )
134
+ completion_text = " [GENERATION_ERROR: UNEXPECTED_RESPONSE]"
135
+
136
+ full_text = text_prompt + completion_text
137
+ completions.append(full_text)
138
+ break # Success, exit retry loop
139
+ except Exception as e:
140
+ self.logger.warning(
141
+ f"LiteLLM attempt {attempt + 1}/{max_retries} failed for model '{self.model_name}': {e}"
142
+ )
143
+ if attempt + 1 == max_retries:
144
+ self.logger.error(
145
+ f"LiteLLM completion failed after {max_retries} attempts for model '{self.model_name}'.",
146
+ exc_info=True,
147
+ )
148
+ completions.append(
149
+ text_prompt + " [GENERATION_ERROR: MAX_RETRIES]"
150
+ )
151
+ else:
152
+ # time.sleep(retry_delay) # Can't use time.sleep in async directly
153
+ await asyncio.sleep(retry_delay) # Use asyncio.sleep
154
+ except Exception as outer_e:
155
+ self.logger.error(
156
+ f"Critical error during LiteLLM request preparation or retry logic: {outer_e}",
157
+ exc_info=True,
158
+ )
159
+ completions.append(text_prompt + " [GENERATION_ERROR: SETUP_FAILURE]")
160
+
161
+ self.logger.info(
162
+ f"Finished LiteLLM requests for model '{self.model_name}'. Generated {len(completions)} responses."
163
+ )
164
+ return completions
165
+
166
+ async def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
167
+ """
168
+ Handles an incoming request by processing it through LiteLLM.
169
+
170
+ Args:
171
+ request_data: A dictionary containing the request data.
172
+ Expected keys:
173
+ - 'prompt': The text prompt to send to the LLM.
174
+ - 'max_new_tokens' (optional): Override default max tokens.
175
+ - 'temperature' (optional): Override default temperature.
176
+ - 'top_p' (optional): Override default top_p.
177
+ - Any other kwargs to pass to litellm.completion.
178
+ Returns:
179
+ A dictionary representing the agent's response or an error.
180
+ """
181
+ prompt_text = request_data.get("prompt")
182
+ if not prompt_text:
183
+ self.logger.warning("No 'prompt' found in request_data.")
184
+ return self._build_error_response(
185
+ error_message="Request data must include a 'prompt' field.",
186
+ status_code=400,
187
+ raw_request=request_data,
188
+ )
189
+
190
+ self.logger.info(
191
+ f"Handling request for LiteLLM adapter {self.id} with prompt: '{prompt_text[:75]}...'"
192
+ )
193
+
194
+ max_new_tokens = request_data.get("max_new_tokens", self.default_max_new_tokens)
195
+ temperature = request_data.get("temperature", self.default_temperature)
196
+ top_p = request_data.get("top_p", self.default_top_p)
197
+
198
+ excluded_keys = {"prompt", "max_new_tokens", "temperature", "top_p"}
199
+ additional_kwargs = {
200
+ k: v for k, v in request_data.items() if k not in excluded_keys
201
+ }
202
+
203
+ try:
204
+ # The _execute_litellm_completion method now handles asyncio.to_thread internally for litellm.completion
205
+ # and also the retry loop with asyncio.sleep
206
+ completions = await self._execute_litellm_completion(
207
+ texts=[prompt_text],
208
+ max_new_tokens=max_new_tokens,
209
+ temperature=temperature,
210
+ top_p=top_p,
211
+ **additional_kwargs,
212
+ )
213
+
214
+ if completions and isinstance(completions, list) and len(completions) > 0:
215
+ full_response_text = completions[0]
216
+ processed_response = full_response_text
217
+
218
+ if "[GENERATION_ERROR:" in processed_response:
219
+ self.logger.warning(
220
+ f"LiteLLM generation indicated an error for adapter {self.id}: {processed_response}"
221
+ )
222
+ error_detail = processed_response[
223
+ len(prompt_text) :
224
+ ].strip() # Get the error part
225
+ return self._build_error_response(
226
+ error_message=f"LiteLLM generation error: {error_detail}",
227
+ status_code=500,
228
+ raw_request=request_data,
229
+ )
230
+
231
+ self.logger.info(
232
+ f"Successfully processed request for LiteLLM adapter {self.id}."
233
+ )
234
+ return {
235
+ "raw_request": request_data,
236
+ "processed_response": processed_response,
237
+ "status_code": 200,
238
+ "raw_response_headers": None,
239
+ "raw_response_body": None,
240
+ "agent_specific_data": {
241
+ "model_name": self.model_name, # Updated key
242
+ "invoked_parameters": {
243
+ "max_new_tokens": max_new_tokens,
244
+ "temperature": temperature,
245
+ "top_p": top_p,
246
+ **additional_kwargs,
247
+ },
248
+ },
249
+ "error_message": None,
250
+ "agent_id": self.id,
251
+ "adapter_type": "LiteLLMAgentAdapter",
252
+ }
253
+ else:
254
+ self.logger.error(
255
+ f"LiteLLM returned empty or invalid completions for adapter {self.id}."
256
+ )
257
+ return self._build_error_response(
258
+ error_message="LiteLLM returned empty or invalid result.",
259
+ status_code=500,
260
+ raw_request=request_data,
261
+ )
262
+ except Exception as e:
263
+ self.logger.exception(
264
+ f"Unexpected error in LiteLLMAgentAdapter handle_request for agent {self.id}: {e}"
265
+ )
266
+ return self._build_error_response(
267
+ error_message=f"Unexpected adapter error: {type(e).__name__} - {str(e)}",
268
+ status_code=500,
269
+ raw_request=request_data,
270
+ )
271
+
272
+ def _build_error_response(
273
+ self,
274
+ error_message: str,
275
+ status_code: Optional[int],
276
+ raw_request: Optional[Dict[str, Any]] = None,
277
+ ) -> Dict[str, Any]:
278
+ return {
279
+ "raw_request": raw_request,
280
+ "processed_response": None,
281
+ "status_code": status_code if status_code is not None else 500,
282
+ "raw_response_headers": None,
283
+ "raw_response_body": None,
284
+ "agent_specific_data": {
285
+ "model_name": self.model_name if hasattr(self, "model_name") else "N/A"
286
+ }, # Updated key
287
+ "error_message": error_message,
288
+ "agent_id": self.id,
289
+ "adapter_type": "LiteLLMAgentAdapter",
290
+ }
@@ -0,0 +1,48 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+
5
+ class Agent(ABC):
6
+ """
7
+ Abstract Base Class for all agent implementations.
8
+ It defines a common interface for the router to interact with various agents.
9
+ """
10
+
11
+ @abstractmethod
12
+ def __init__(self, id: str, config: Dict[str, Any]):
13
+ """
14
+ Initializes the agent.
15
+
16
+ Args:
17
+ id: A unique identifier for this specific agent instance or type.
18
+ config: Configuration specific to this agent (e.g., API keys, model names).
19
+ """
20
+ self.id = id
21
+ self.config = config
22
+ pass
23
+
24
+ @abstractmethod
25
+ async def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
26
+ """
27
+ Processes an incoming request and returns a standardized response.
28
+ The response should be suitable for storage via the API and should ideally
29
+ include enough information to reconstruct the interaction.
30
+
31
+ Args:
32
+ request_data: The data for the agent to process. This might include
33
+ the prompt, session information, user details, etc.
34
+
35
+ Returns:
36
+ A dictionary containing the standardized response. This could include:
37
+ - 'raw_request': The original request sent to the underlying agent.
38
+ - 'raw_response': The original response received from the underlying agent.
39
+ - 'processed_response': The key information extracted/processed from the raw response.
40
+ - 'status_code': If applicable, the HTTP status code of the interaction.
41
+ - 'error_message': Any error message encountered.
42
+ - 'metadata': Any other relevant metadata.
43
+ """
44
+ pass
45
+
46
+ def get_identifier(self) -> str:
47
+ """Returns the unique identifier for this agent instance or type."""
48
+ return self.id