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,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
|
+
}
|