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,524 @@
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
+ import logging
17
+ import os
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ # Attempt to import litellm, but catch ImportError if not installed.
21
+ try:
22
+ import litellm
23
+ from litellm.exceptions import (
24
+ APIConnectionError,
25
+ APIError,
26
+ AuthenticationError,
27
+ BadRequestError,
28
+ ContextWindowExceededError,
29
+ NotFoundError,
30
+ PermissionDeniedError,
31
+ RateLimitError,
32
+ ServiceUnavailableError,
33
+ Timeout,
34
+ )
35
+
36
+ LITELLM_AVAILABLE = True
37
+ except ImportError:
38
+ litellm = None # type: ignore
39
+
40
+ # Define dummy exceptions if litellm is not available so the rest of the code can type hint
41
+ class APIConnectionError(Exception):
42
+ pass
43
+
44
+ class RateLimitError(Exception):
45
+ pass
46
+
47
+ class ServiceUnavailableError(Exception):
48
+ pass
49
+
50
+ class Timeout(Exception):
51
+ pass
52
+
53
+ class APIError(Exception):
54
+ pass
55
+
56
+ class AuthenticationError(Exception):
57
+ pass
58
+
59
+ class BadRequestError(Exception):
60
+ pass
61
+
62
+ class NotFoundError(Exception):
63
+ pass
64
+
65
+ class PermissionDeniedError(Exception):
66
+ pass
67
+
68
+ class ContextWindowExceededError(Exception):
69
+ pass
70
+
71
+ LITELLM_AVAILABLE = False
72
+
73
+
74
+ from .base import Agent # Updated import
75
+
76
+
77
+ # --- Custom Exceptions ---
78
+ class LiteLLMConfigurationError(Exception):
79
+ """Custom exception for LiteLLM adapter configuration issues."""
80
+
81
+ pass
82
+
83
+
84
+ logger = logging.getLogger(__name__) # Module-level logger
85
+
86
+
87
+ class LiteLLMAgentAdapter(Agent):
88
+ """
89
+ Adapter for interacting with LLMs via the LiteLLM library.
90
+
91
+ This adapter supports multiple LLM providers through LiteLLM's unified interface.
92
+ For custom/self-hosted endpoints, the endpoint URL must be provided correctly:
93
+
94
+ OpenAI-Compatible Endpoints:
95
+ - Provide the base URL ending with /v1 (e.g., "http://localhost:8000/v1")
96
+ - The OpenAI client will automatically append /chat/completions
97
+ - Example: endpoint="http://localhost:8000/v1" → requests to http://localhost:8000/v1/chat/completions
98
+
99
+ Non-OpenAI Protocols:
100
+ - Use the appropriate agent type (LANGCHAIN, MCP, A2A) instead of routing through LiteLLM
101
+ - LANGCHAIN: Use LangServe endpoints (e.g., "http://localhost:8000/invoke")
102
+ - MCP: Use Model Context Protocol adapter (not LiteLLM)
103
+ - A2A: Use Agent-to-Agent protocol adapter (not LiteLLM)
104
+ """
105
+
106
+ def __init__(self, id: str, config: Dict[str, Any]):
107
+ """
108
+ Initializes the LiteLLMAgentAdapter.
109
+
110
+ Args:
111
+ id: The unique identifier for this LiteLLM agent instance.
112
+ config: Configuration dictionary for the LiteLLM agent.
113
+ Expected keys:
114
+ - 'name': Model string for LiteLLM (e.g., "ollama/llama3").
115
+ - 'endpoint' (optional): Base URL for the API.
116
+ - 'api_key' (optional): Name of the environment variable holding the API key.
117
+ - 'max_new_tokens' (optional): Default max tokens for generation (defaults to 100).
118
+ - 'temperature' (optional): Default temperature (defaults to 0.8).
119
+ - 'top_p' (optional): Default top_p (defaults to 0.95).
120
+ """
121
+ super().__init__(id, config)
122
+ # Use hierarchical logger name for TUI handler inheritance
123
+ self.logger = logging.getLogger(
124
+ f"hackagent.router.adapters.LiteLLMAgentAdapter.{self.id}"
125
+ )
126
+
127
+ if "name" not in self.config:
128
+ msg = f"Missing required configuration key 'name' (for model string) for LiteLLMAgentAdapter: {self.id}"
129
+ self.logger.error(msg)
130
+ raise LiteLLMConfigurationError(msg)
131
+
132
+ self.model_name: str = self.config["name"]
133
+ self.api_base_url: Optional[str] = self.config.get("endpoint")
134
+
135
+ # Handle API key configuration
136
+ # Important: When using a custom endpoint, the agent at that endpoint handles its own auth.
137
+ # Exception: hackagent/* models are HackAgent services that need the HackAgent API key.
138
+ api_key_config: Optional[str] = self.config.get("api_key")
139
+ self.actual_api_key: Optional[str] = None
140
+
141
+ if api_key_config:
142
+ # Explicit api_key provided - try as env var name first, then use value directly
143
+ self.actual_api_key = os.environ.get(api_key_config)
144
+ if self.actual_api_key is None:
145
+ self.actual_api_key = api_key_config
146
+
147
+ elif not self.api_base_url:
148
+ # No custom endpoint and no explicit api_key - try standard env vars for public APIs
149
+ # This only applies when calling public APIs directly (OpenAI, Anthropic, etc.)
150
+ if self.model_name.startswith("openai/") or self.model_name.startswith(
151
+ "gpt-"
152
+ ):
153
+ self.actual_api_key = os.environ.get("OPENAI_API_KEY")
154
+ if self.actual_api_key:
155
+ self.logger.debug(
156
+ "Using OPENAI_API_KEY from environment for public API"
157
+ )
158
+ elif self.model_name.startswith("anthropic/") or self.model_name.startswith(
159
+ "claude-"
160
+ ):
161
+ self.actual_api_key = os.environ.get("ANTHROPIC_API_KEY")
162
+ if self.actual_api_key:
163
+ self.logger.debug(
164
+ "Using ANTHROPIC_API_KEY from environment for public API"
165
+ )
166
+
167
+ # When using custom endpoint, determine auth strategy
168
+ if self.api_base_url and not self.actual_api_key:
169
+ if self.model_name.startswith("hackagent/"):
170
+ # hackagent/* models need the HackAgent API key
171
+ # Try to get from config first (passed by router), then environment
172
+ hackagent_key = self.config.get("hackagent_api_key")
173
+ if not hackagent_key:
174
+ hackagent_key = os.environ.get("HACKAGENT_API_KEY")
175
+
176
+ if hackagent_key:
177
+ self.actual_api_key = hackagent_key
178
+ self.logger.debug(
179
+ f"Using HACKAGENT_API_KEY for {self.model_name} service"
180
+ )
181
+ else:
182
+ self.logger.warning(
183
+ f"HackAgent model '{self.model_name}' requires HACKAGENT_API_KEY but none found. "
184
+ f"Requests will likely fail with authentication errors."
185
+ )
186
+ # Use placeholder to avoid immediate failure, let backend reject with clear error
187
+ self.actual_api_key = "sk-placeholder-missing-hackagent-key"
188
+ else:
189
+ # Other custom endpoints (LangChain, etc.) - use placeholder
190
+ # The actual endpoint will handle its own authentication
191
+ self.actual_api_key = "sk-placeholder-key-for-custom-endpoint"
192
+ self.logger.debug(
193
+ f"Using custom endpoint '{self.api_base_url}' - agent handles its own authentication"
194
+ )
195
+
196
+ self.logger.info(
197
+ f"LiteLLMAgentAdapter '{self.id}' initialized for model: '{self.model_name}'"
198
+ + (f" API Base: '{self.api_base_url}'" if self.api_base_url else "")
199
+ )
200
+
201
+ # Store default generation parameters
202
+ self.default_max_new_tokens = self.config.get("max_new_tokens", 100)
203
+ self.default_temperature = self.config.get("temperature", 0.8)
204
+ self.default_top_p = self.config.get("top_p", 0.95)
205
+
206
+ def _prepare_litellm_params(
207
+ self,
208
+ messages: List[Dict[str, str]],
209
+ max_new_tokens: int,
210
+ temperature: float,
211
+ top_p: float,
212
+ **kwargs,
213
+ ) -> Dict[str, Any]:
214
+ """Prepare parameters for litellm.completion call."""
215
+ litellm_params = {
216
+ "model": self.model_name,
217
+ "messages": messages,
218
+ "max_tokens": max_new_tokens,
219
+ "temperature": temperature,
220
+ "top_p": top_p,
221
+ "api_base": self.api_base_url,
222
+ "api_key": self.actual_api_key,
223
+ }
224
+
225
+ # Handle custom endpoint scenarios (LangChain, custom agents, etc.)
226
+ if self.api_base_url:
227
+ # For custom endpoints, treat as OpenAI-compatible unless model has a known provider prefix
228
+ if not any(
229
+ self.model_name.startswith(prefix)
230
+ for prefix in [
231
+ "openai/",
232
+ "anthropic/",
233
+ "azure/",
234
+ "bedrock/",
235
+ "vertex_ai/",
236
+ "huggingface/",
237
+ "replicate/",
238
+ "together_ai/",
239
+ "anyscale/",
240
+ "ollama/",
241
+ ]
242
+ ):
243
+ # Model name without provider prefix - treat as OpenAI-compatible custom endpoint
244
+ litellm_params["custom_llm_provider"] = "openai"
245
+ # Use the endpoint exactly as provided - user specifies the complete URL
246
+ # For OpenAI-compatible endpoints, this should be the base URL (e.g., http://host:port/v1)
247
+ # and the OpenAI client will append /chat/completions automatically
248
+ litellm_params["api_base"] = self.api_base_url
249
+ elif self.model_name.startswith("hackagent/"):
250
+ # Special handling for hackagent/* models
251
+ litellm_params["custom_llm_provider"] = "openai"
252
+ litellm_params["api_base"] = self.api_base_url
253
+
254
+ litellm_params["extra_headers"] = {"User-Agent": "HackAgent/0.1.0"}
255
+
256
+ litellm_params.update(kwargs)
257
+ return litellm_params
258
+
259
+ def _extract_response_content(self, response: Any, context: str = "") -> str:
260
+ """Extract content from litellm response, handling various response formats."""
261
+ if not (response and response.choices and response.choices[0].message):
262
+ self.logger.warning(
263
+ f"LiteLLM received unexpected response structure for model '{self.model_name}'{context}. Response: {response}"
264
+ )
265
+ return "[GENERATION_ERROR: UNEXPECTED_RESPONSE]"
266
+
267
+ message = response.choices[0].message
268
+ content = message.content if message.content else ""
269
+
270
+ # Try to extract reasoning content from various possible locations
271
+ reasoning_content = None
272
+ if hasattr(message, "reasoning_content") and message.reasoning_content:
273
+ reasoning_content = message.reasoning_content
274
+ elif hasattr(message, "reasoning") and message.reasoning:
275
+ reasoning_content = message.reasoning
276
+ elif (
277
+ hasattr(message, "provider_specific_fields")
278
+ and message.provider_specific_fields
279
+ ):
280
+ reasoning_content = message.provider_specific_fields.get(
281
+ "reasoning_content"
282
+ ) or message.provider_specific_fields.get("reasoning")
283
+
284
+ # Use content if available, otherwise fall back to reasoning content
285
+ if content:
286
+ return content
287
+ elif reasoning_content:
288
+ self.logger.debug(
289
+ f"LiteLLM using reasoning content for model '{self.model_name}' (content field was empty)"
290
+ )
291
+ return reasoning_content
292
+ else:
293
+ self.logger.warning(
294
+ f"LiteLLM received empty content and no reasoning field for model '{self.model_name}'{context}. Message: {message}"
295
+ )
296
+ return "[GENERATION_ERROR: EMPTY_RESPONSE]"
297
+
298
+ def _execute_litellm_completion_with_messages(
299
+ self,
300
+ messages: List[Dict[str, str]],
301
+ max_new_tokens: int,
302
+ temperature: float,
303
+ top_p: float,
304
+ **kwargs,
305
+ ) -> str:
306
+ """Execute a single completion using litellm.completion with messages format."""
307
+ try:
308
+ # Log agent interaction for TUI visibility
309
+ if messages:
310
+ msg_preview = str(messages[-1].get("content", ""))[:100]
311
+ self.logger.info(f"🌐 Querying model {self.model_name}")
312
+ self.logger.debug(f" Message preview: {msg_preview}...")
313
+
314
+ litellm_params = self._prepare_litellm_params(
315
+ messages, max_new_tokens, temperature, top_p, **kwargs
316
+ )
317
+ response = litellm.completion(**litellm_params)
318
+
319
+ content = self._extract_response_content(response)
320
+ self.logger.info(f"✅ Model responded ({len(content)} chars)")
321
+ return content
322
+
323
+ except AuthenticationError as e:
324
+ error_msg = f"Authentication failed for model '{self.model_name}': {str(e)}"
325
+ self.logger.error(error_msg)
326
+ llm_provider = e.llm_provider if hasattr(e, "llm_provider") else "unknown"
327
+ raise AuthenticationError(error_msg, llm_provider, self.model_name) from e
328
+ except Exception as e:
329
+ self.logger.error(
330
+ f"LiteLLM completion call failed for model '{self.model_name}': {e}",
331
+ exc_info=True,
332
+ )
333
+ return f"[GENERATION_ERROR: {type(e).__name__}]"
334
+
335
+ def _execute_litellm_completion(
336
+ self,
337
+ texts: List[str],
338
+ max_new_tokens: int,
339
+ temperature: float,
340
+ top_p: float,
341
+ **kwargs,
342
+ ) -> List[str]:
343
+ """Generate completions for multiple text prompts using litellm.completion."""
344
+ if not texts:
345
+ return []
346
+
347
+ completions = []
348
+ self.logger.info(
349
+ f"Sending {len(texts)} requests via LiteLLM to model '{self.model_name}'..."
350
+ )
351
+
352
+ for text_prompt in texts:
353
+ messages = [{"role": "user", "content": text_prompt}]
354
+
355
+ try:
356
+ litellm_params = self._prepare_litellm_params(
357
+ messages, max_new_tokens, temperature, top_p, **kwargs
358
+ )
359
+ response = litellm.completion(**litellm_params)
360
+ completion_text = self._extract_response_content(
361
+ response, context=f" for prompt '{text_prompt[:50]}...'"
362
+ )
363
+
364
+ except AuthenticationError as e:
365
+ error_msg = (
366
+ f"Authentication failed for model '{self.model_name}': {str(e)}"
367
+ )
368
+ self.logger.error(error_msg)
369
+ llm_provider = (
370
+ e.llm_provider if hasattr(e, "llm_provider") else "unknown"
371
+ )
372
+ raise AuthenticationError(
373
+ error_msg, llm_provider, self.model_name
374
+ ) from e
375
+ except Exception as e:
376
+ self.logger.error(
377
+ f"LiteLLM completion call failed for model '{self.model_name}' for prompt '{text_prompt[:50]}...': {e}",
378
+ exc_info=True,
379
+ )
380
+ completion_text = f" [GENERATION_ERROR: {type(e).__name__}]"
381
+
382
+ full_text = text_prompt + completion_text
383
+ completions.append(full_text)
384
+
385
+ self.logger.info(
386
+ f"Finished LiteLLM requests for model '{self.model_name}'. Generated {len(completions)} responses."
387
+ )
388
+ return completions
389
+
390
+ def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
391
+ """Handle an incoming request by processing it through LiteLLM.
392
+
393
+ Args:
394
+ request_data: Dictionary containing request data with keys:
395
+ - 'prompt': Text prompt to send to the LLM
396
+ - 'messages': Pre-formatted messages list (takes precedence over prompt)
397
+ - 'max_new_tokens'/'max_tokens': Override default max tokens
398
+ - 'temperature': Override default temperature
399
+ - 'top_p': Override default top_p
400
+ - Other kwargs to pass to litellm.completion
401
+
402
+ Returns:
403
+ Dictionary representing the agent's response or an error
404
+ """
405
+ messages = request_data.get("messages")
406
+ prompt_text = request_data.get("prompt")
407
+
408
+ if not messages and not prompt_text:
409
+ return self._build_error_response(
410
+ error_message="Request data must include either 'messages' or 'prompt' field.",
411
+ status_code=400,
412
+ raw_request=request_data,
413
+ )
414
+
415
+ # Convert prompt to messages if not provided
416
+ if not messages:
417
+ messages = [{"role": "user", "content": prompt_text}]
418
+ log_text = str(prompt_text)[:75]
419
+ else:
420
+ log_text = str(messages[0].get("content", ""))[:75] if messages else ""
421
+
422
+ self.logger.info(
423
+ f"Handling request for LiteLLM adapter {self.id} with prompt: '{log_text}...'"
424
+ )
425
+
426
+ max_new_tokens = request_data.get("max_new_tokens") or request_data.get(
427
+ "max_tokens", self.default_max_new_tokens
428
+ )
429
+ temperature = request_data.get("temperature", self.default_temperature)
430
+ top_p = request_data.get("top_p", self.default_top_p)
431
+
432
+ excluded_keys = {
433
+ "prompt",
434
+ "messages",
435
+ "max_new_tokens",
436
+ "max_tokens",
437
+ "temperature",
438
+ "top_p",
439
+ }
440
+ additional_kwargs = {
441
+ k: v for k, v in request_data.items() if k not in excluded_keys
442
+ }
443
+
444
+ try:
445
+ completion_text = self._execute_litellm_completion_with_messages(
446
+ messages=messages,
447
+ max_new_tokens=max_new_tokens,
448
+ temperature=temperature,
449
+ top_p=top_p,
450
+ **additional_kwargs,
451
+ )
452
+
453
+ if not completion_text:
454
+ return self._build_error_response(
455
+ error_message="LiteLLM returned empty result.",
456
+ status_code=500,
457
+ raw_request=request_data,
458
+ )
459
+
460
+ if "[GENERATION_ERROR:" in completion_text:
461
+ return self._build_error_response(
462
+ error_message=f"LiteLLM generation error: {completion_text}",
463
+ status_code=500,
464
+ raw_request=request_data,
465
+ )
466
+
467
+ self.logger.info(
468
+ f"Successfully processed request for LiteLLM adapter {self.id}."
469
+ )
470
+ return {
471
+ "raw_request": request_data,
472
+ "processed_response": completion_text,
473
+ "status_code": 200,
474
+ "raw_response_headers": None,
475
+ "raw_response_body": None,
476
+ "agent_specific_data": {
477
+ "model_name": self.model_name,
478
+ "invoked_parameters": {
479
+ "max_new_tokens": max_new_tokens,
480
+ "temperature": temperature,
481
+ "top_p": top_p,
482
+ **additional_kwargs,
483
+ },
484
+ },
485
+ "error_message": None,
486
+ "agent_id": self.id,
487
+ "adapter_type": "LiteLLMAgentAdapter",
488
+ }
489
+
490
+ except AuthenticationError as e:
491
+ error_msg = f"Authentication failed for model '{self.model_name}': {str(e)}"
492
+ self.logger.error(error_msg)
493
+ llm_provider = e.llm_provider if hasattr(e, "llm_provider") else "unknown"
494
+ raise AuthenticationError(error_msg, llm_provider, self.model_name) from e
495
+ except Exception as e:
496
+ self.logger.exception(
497
+ f"Unexpected error in LiteLLMAgentAdapter handle_request for agent {self.id}: {e}"
498
+ )
499
+ return self._build_error_response(
500
+ error_message=f"Unexpected adapter error: {type(e).__name__} - {str(e)}",
501
+ status_code=500,
502
+ raw_request=request_data,
503
+ )
504
+
505
+ def _build_error_response(
506
+ self,
507
+ error_message: str,
508
+ status_code: Optional[int],
509
+ raw_request: Optional[Dict[str, Any]] = None,
510
+ ) -> Dict[str, Any]:
511
+ """Build a standardized error response dictionary."""
512
+ return {
513
+ "raw_request": raw_request,
514
+ "processed_response": None,
515
+ "status_code": status_code if status_code is not None else 500,
516
+ "raw_response_headers": None,
517
+ "raw_response_body": None,
518
+ "agent_specific_data": {
519
+ "model_name": self.model_name if hasattr(self, "model_name") else "N/A"
520
+ },
521
+ "error_message": error_message,
522
+ "agent_id": self.id,
523
+ "adapter_type": "LiteLLMAgentAdapter",
524
+ }