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,671 @@
|
|
|
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 json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Dict, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
from requests.structures import CaseInsensitiveDict
|
|
22
|
+
|
|
23
|
+
from hackagent.router.adapters.base import Agent
|
|
24
|
+
|
|
25
|
+
# Global logger for this module, can be used by utility functions too
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- Custom Exceptions (moved from api.utils.py) ---
|
|
30
|
+
class AgentConfigurationError(Exception):
|
|
31
|
+
"""Custom exception for agent configuration issues."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentInteractionError(Exception):
|
|
37
|
+
"""Custom exception for errors during interaction with the agent API."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ResponseParsingError(Exception):
|
|
43
|
+
"""Custom exception for errors parsing the agent's response."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ADKAgentAdapter(Agent):
|
|
49
|
+
"""
|
|
50
|
+
Adapter for interacting with ADK (Agent Development Kit) based agents.
|
|
51
|
+
|
|
52
|
+
This class implements the common `Agent` interface. It translates requests
|
|
53
|
+
and responses between the router's standard format and the specific format
|
|
54
|
+
required by ADK agents. It encapsulates all logic for ADK communication,
|
|
55
|
+
including session management (optional), request formatting, execution,
|
|
56
|
+
response parsing, and error handling.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
name (str): The name of the ADK application (used for router registration AND as ADK app identifier).
|
|
60
|
+
endpoint (str): The base API endpoint for the ADK agent.
|
|
61
|
+
user_id (str): The user identifier for ADK sessions.
|
|
62
|
+
request_timeout (int): Timeout in seconds for requests to the ADK agent.
|
|
63
|
+
logger (logging.Logger): Logger instance for this adapter.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, id: str, config: Dict[str, Any]):
|
|
67
|
+
"""
|
|
68
|
+
Initializes the ADKAgentAdapter.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
id: The unique identifier for this ADK agent instance.
|
|
72
|
+
config: Configuration dictionary for the ADK agent.
|
|
73
|
+
Expected keys include:
|
|
74
|
+
- 'name': Name of the ADK application (e.g., 'multi_tool_agent').
|
|
75
|
+
- 'endpoint': Base URL of the ADK agent.
|
|
76
|
+
- 'user_id': User ID for the ADK session.
|
|
77
|
+
- 'request_timeout' (optional): Request timeout in seconds
|
|
78
|
+
(defaults to 120).
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
AgentConfigurationError: If any required configuration key (name, endpoint, user_id) is missing.
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(id, config)
|
|
84
|
+
required_keys = ["name", "endpoint", "user_id"]
|
|
85
|
+
for key in required_keys:
|
|
86
|
+
if key not in self.config:
|
|
87
|
+
msg = f"Missing required configuration key '{key}' for ADKAgentAdapter: {self.id}"
|
|
88
|
+
raise AgentConfigurationError(msg)
|
|
89
|
+
|
|
90
|
+
self.name: str = self.config["name"]
|
|
91
|
+
self.endpoint: str = self.config["endpoint"].strip("/")
|
|
92
|
+
self.user_id: str = self.config["user_id"]
|
|
93
|
+
self.request_timeout: int = self.config.get("request_timeout", 120)
|
|
94
|
+
|
|
95
|
+
# Generate a unique session ID for this adapter instance
|
|
96
|
+
# This keeps session state persistent across multiple requests to the same agent
|
|
97
|
+
import uuid
|
|
98
|
+
|
|
99
|
+
self.session_id: str = self.config.get("session_id", str(uuid.uuid4()))
|
|
100
|
+
|
|
101
|
+
# Use hierarchical logger name for TUI handler inheritance
|
|
102
|
+
self.logger = logging.getLogger(
|
|
103
|
+
f"hackagent.router.adapters.ADKAgentAdapter.{self.id}"
|
|
104
|
+
)
|
|
105
|
+
self.logger.info(
|
|
106
|
+
f"ADKAgentAdapter initialized with session_id: {self.session_id}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _initialize_session(
|
|
110
|
+
self, session_id_to_init: str, initial_state: Optional[dict] = None
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
(Optional) Creates or ensures a specific ADK session exists.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
session_id_to_init: The specific session ID to initialize.
|
|
117
|
+
initial_state: An optional dictionary to provide initial state when
|
|
118
|
+
creating the ADK session.
|
|
119
|
+
Returns:
|
|
120
|
+
True if the session was created successfully or already existed.
|
|
121
|
+
Raises:
|
|
122
|
+
AgentInteractionError: If there's an issue.
|
|
123
|
+
"""
|
|
124
|
+
self.logger.info(f"Explicitly initializing ADK session: {session_id_to_init}.")
|
|
125
|
+
try:
|
|
126
|
+
return self._create_session_internal(
|
|
127
|
+
session_id=session_id_to_init, initial_state=initial_state
|
|
128
|
+
)
|
|
129
|
+
except AgentInteractionError as e:
|
|
130
|
+
self.logger.error(
|
|
131
|
+
f"Failed to initialize ADK session {session_id_to_init}: {e}"
|
|
132
|
+
)
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
def _create_session_internal(
|
|
136
|
+
self, session_id: str, initial_state: Optional[dict] = None
|
|
137
|
+
) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Internal helper to create a session on the ADK server.
|
|
140
|
+
|
|
141
|
+
Sends a POST request to the ADK session creation endpoint.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
session_id: The specific session ID to create.
|
|
145
|
+
initial_state: An optional dictionary to provide initial state for the session.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if the session was successfully created or if it already existed (HTTP 409, or HTTP 400 with specific message).
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
AgentInteractionError: If the HTTP request fails or the server returns
|
|
152
|
+
an unexpected error status.
|
|
153
|
+
"""
|
|
154
|
+
target_url = f"{self.endpoint}/apps/{self.name}/users/{self.user_id}/sessions/{session_id}"
|
|
155
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
156
|
+
payload = initial_state or {}
|
|
157
|
+
self.logger.info(f"Attempting to create ADK session: {target_url}")
|
|
158
|
+
try:
|
|
159
|
+
response = requests.post(
|
|
160
|
+
target_url, headers=headers, json=payload, timeout=30
|
|
161
|
+
)
|
|
162
|
+
response.raise_for_status()
|
|
163
|
+
self.logger.info(f"Successfully created ADK session {session_id}")
|
|
164
|
+
return True
|
|
165
|
+
except requests.exceptions.HTTPError as http_err:
|
|
166
|
+
if http_err.response is not None:
|
|
167
|
+
status_code = http_err.response.status_code
|
|
168
|
+
response_text_lower = ""
|
|
169
|
+
original_response_text = "[Could not read body]"
|
|
170
|
+
try:
|
|
171
|
+
original_response_text = http_err.response.text
|
|
172
|
+
response_text_lower = original_response_text.lower()
|
|
173
|
+
except Exception as e_text:
|
|
174
|
+
self.logger.warning(
|
|
175
|
+
f"Could not get text from error response (status {status_code}) for session {session_id}: {e_text}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Condition 1: HTTP 409 Conflict (standard "already exists")
|
|
179
|
+
if status_code == 409:
|
|
180
|
+
self.logger.warning(
|
|
181
|
+
f"ADK session {session_id} already exists (HTTP 409). Proceeding."
|
|
182
|
+
)
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
# Condition 2: HTTP 400 Bad Request + specific message (ADK server's current behavior)
|
|
186
|
+
if (
|
|
187
|
+
status_code == 400
|
|
188
|
+
and "session already exists" in response_text_lower
|
|
189
|
+
):
|
|
190
|
+
self.logger.warning(
|
|
191
|
+
f"ADK session {session_id} already exists (HTTP 400 with 'session already exists' in body). "
|
|
192
|
+
f"Proceeding. Body: {original_response_text}"
|
|
193
|
+
)
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# If neither of the above conditions met, then it's a genuine error
|
|
197
|
+
err_msg_detail_base = f"HTTP error creating ADK session {session_id}"
|
|
198
|
+
err_msg_detail_extended = ""
|
|
199
|
+
current_status_for_exc = "Unknown"
|
|
200
|
+
|
|
201
|
+
if http_err.response is not None:
|
|
202
|
+
try:
|
|
203
|
+
current_status_for_exc = http_err.response.status_code
|
|
204
|
+
# Ensure response_text is defined for logging if it wasn't fetched successfully above
|
|
205
|
+
body_for_log = (
|
|
206
|
+
original_response_text
|
|
207
|
+
if "original_response_text" in locals()
|
|
208
|
+
else "[Could not read body during logging]"
|
|
209
|
+
)
|
|
210
|
+
err_msg_detail_extended = f": {http_err} - Status {current_status_for_exc} - Body: {body_for_log}"
|
|
211
|
+
except Exception as e_resp_attrs:
|
|
212
|
+
self.logger.warning(
|
|
213
|
+
f"Could not get all attributes from error response for session {session_id}: {e_resp_attrs}"
|
|
214
|
+
)
|
|
215
|
+
err_msg_detail_extended = (
|
|
216
|
+
f": {http_err} (Error response attributes inaccessible)"
|
|
217
|
+
)
|
|
218
|
+
else: # http_err.response is None
|
|
219
|
+
err_msg_detail_extended = f": {http_err}"
|
|
220
|
+
|
|
221
|
+
self.logger.error(f"{err_msg_detail_base}{err_msg_detail_extended}")
|
|
222
|
+
raise AgentInteractionError(
|
|
223
|
+
f"HTTP Error {current_status_for_exc} creating session {session_id}"
|
|
224
|
+
) from http_err
|
|
225
|
+
except requests.exceptions.RequestException as e:
|
|
226
|
+
self.logger.error(
|
|
227
|
+
f"Request exception creating ADK session {session_id}: {e}"
|
|
228
|
+
)
|
|
229
|
+
raise AgentInteractionError(
|
|
230
|
+
f"Request failed creating session {session_id}: {e}"
|
|
231
|
+
) from e
|
|
232
|
+
|
|
233
|
+
def _prepare_request_payload(
|
|
234
|
+
self, prompt_text: str, session_id: str
|
|
235
|
+
) -> Tuple[dict, dict]:
|
|
236
|
+
"""
|
|
237
|
+
Prepares the HTTP headers and JSON payload for an ADK agent request.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
prompt_text: The user's prompt text to be sent to the agent.
|
|
241
|
+
session_id: The session identifier for this specific ADK interaction.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
A tuple containing two dictionaries: the headers and the payload.
|
|
245
|
+
"""
|
|
246
|
+
payload = {
|
|
247
|
+
"app_name": self.name,
|
|
248
|
+
"user_id": self.user_id,
|
|
249
|
+
"session_id": session_id,
|
|
250
|
+
"new_message": {"role": "user", "parts": [{"text": prompt_text}]},
|
|
251
|
+
}
|
|
252
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
253
|
+
return headers, payload
|
|
254
|
+
|
|
255
|
+
def _execute_http_post(
|
|
256
|
+
self, url: str, headers: dict, payload: dict
|
|
257
|
+
) -> requests.Response:
|
|
258
|
+
"""
|
|
259
|
+
Executes an HTTP POST request.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
url: The URL to send the POST request to.
|
|
263
|
+
headers: A dictionary of HTTP headers.
|
|
264
|
+
payload: A dictionary to be sent as the JSON payload.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
A `requests.Response` object.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
AgentInteractionError: If the request times out or another request-related
|
|
271
|
+
exception occurs.
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
# Log agent interaction for TUI visibility
|
|
275
|
+
self.logger.info(f"🌐 Sending request to agent endpoint: {url}")
|
|
276
|
+
if "message" in payload:
|
|
277
|
+
msg_preview = str(payload["message"])[:100]
|
|
278
|
+
self.logger.debug(f" Message preview: {msg_preview}...")
|
|
279
|
+
|
|
280
|
+
response = requests.post(
|
|
281
|
+
url, headers=headers, json=payload, timeout=self.request_timeout
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Log response status
|
|
285
|
+
self.logger.info(f"✅ Agent responded with status {response.status_code}")
|
|
286
|
+
self.logger.debug(
|
|
287
|
+
f"Request to {url} completed with status {response.status_code}"
|
|
288
|
+
)
|
|
289
|
+
return response
|
|
290
|
+
except requests.exceptions.Timeout as e:
|
|
291
|
+
self.logger.warning(f"Request timed out accessing {url}: {e}")
|
|
292
|
+
raise AgentInteractionError(f"Request timed out: {e}") from e
|
|
293
|
+
except requests.exceptions.RequestException as e:
|
|
294
|
+
self.logger.error(f"Request exception accessing {url}: {e}")
|
|
295
|
+
raise AgentInteractionError(f"Request failed: {e}") from e
|
|
296
|
+
|
|
297
|
+
def _parse_response_json(
|
|
298
|
+
self, response: requests.Response
|
|
299
|
+
) -> Tuple[Optional[str], Optional[list], str, Optional[CaseInsensitiveDict]]:
|
|
300
|
+
"""
|
|
301
|
+
Parses the JSON response from an ADK agent.
|
|
302
|
+
|
|
303
|
+
It checks for HTTP errors first. Then, it attempts to parse the JSON body,
|
|
304
|
+
expecting a list of events. It iterates through these events (in reverse)
|
|
305
|
+
to find the agent's final text response or an escalation message.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
response: The `requests.Response` object from the ADK agent.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
A tuple containing:
|
|
312
|
+
- final_response_text (Optional[str]): The extracted text response.
|
|
313
|
+
- events (Optional[list]): The full list of ADK events.
|
|
314
|
+
- response_body_str (str): The raw response body as a string.
|
|
315
|
+
- http_headers (Optional[CaseInsensitiveDict]): The response headers.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
AgentInteractionError: If an HTTP error status (4xx or 5xx) is encountered.
|
|
319
|
+
ResponseParsingError: If the response body is not valid JSON, not in the
|
|
320
|
+
expected list format, or if a non-event detail message
|
|
321
|
+
is returned instead of events.
|
|
322
|
+
"""
|
|
323
|
+
response_body_str = response.text
|
|
324
|
+
http_headers = response.headers
|
|
325
|
+
self.logger.debug(
|
|
326
|
+
f"ADK Response Body for parsing: {response_body_str[:1000]}"
|
|
327
|
+
) # Log more of the body
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
response.raise_for_status()
|
|
331
|
+
except requests.exceptions.HTTPError as http_err:
|
|
332
|
+
status = http_err.response.status_code if http_err.response else "Unknown"
|
|
333
|
+
self.logger.error(
|
|
334
|
+
f"HTTP error {status} from {response.url}: {response_body_str}"
|
|
335
|
+
)
|
|
336
|
+
raise AgentInteractionError(f"HTTP Error: {status}") from http_err
|
|
337
|
+
|
|
338
|
+
final_response_text = None
|
|
339
|
+
events = None
|
|
340
|
+
try:
|
|
341
|
+
events = response.json()
|
|
342
|
+
if not isinstance(events, list):
|
|
343
|
+
self.logger.warning(
|
|
344
|
+
f"ADK response was not a JSON list. Type: {type(events)}. Body: {response_body_str[:500]}"
|
|
345
|
+
)
|
|
346
|
+
if isinstance(events, dict) and "detail" in events:
|
|
347
|
+
detail_message = events["detail"]
|
|
348
|
+
self.logger.warning(
|
|
349
|
+
f"ADK returned non-event detail message: {detail_message}"
|
|
350
|
+
)
|
|
351
|
+
raise ResponseParsingError(
|
|
352
|
+
f"ADK returned detail message: {detail_message}"
|
|
353
|
+
)
|
|
354
|
+
self.logger.warning(
|
|
355
|
+
f"ADK response not a JSON list or recognized detail. Body: {response_body_str[:500]}"
|
|
356
|
+
)
|
|
357
|
+
raise ResponseParsingError(
|
|
358
|
+
"ADK response format unrecognized (not a list)."
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
self.logger.debug(f"Received {len(events)} events from ADK for parsing.")
|
|
362
|
+
|
|
363
|
+
for i, event in enumerate(reversed(events)):
|
|
364
|
+
self.logger.debug(
|
|
365
|
+
f"Parsing event {len(events) - 1 - i} (reversed index {i}): {str(event)[:200]}..."
|
|
366
|
+
)
|
|
367
|
+
actions = event.get("actions")
|
|
368
|
+
if actions and isinstance(actions, dict) and actions.get("escalate"):
|
|
369
|
+
error_msg = event.get(
|
|
370
|
+
"error_message",
|
|
371
|
+
"No specific message provided by agent for escalation.",
|
|
372
|
+
)
|
|
373
|
+
final_response_text = f"Agent escalated: {error_msg}"
|
|
374
|
+
self.logger.debug(
|
|
375
|
+
f"Escalation event found as final response: {final_response_text}"
|
|
376
|
+
)
|
|
377
|
+
break # Found escalation, stop parsing
|
|
378
|
+
|
|
379
|
+
content = event.get("content")
|
|
380
|
+
if not content or not isinstance(content, dict):
|
|
381
|
+
self.logger.debug(
|
|
382
|
+
f"Event {len(events) - 1 - i} has no content or content is not a dict. Skipping for text."
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
parts = content.get("parts")
|
|
387
|
+
if not parts or not isinstance(parts, list) or len(parts) == 0:
|
|
388
|
+
self.logger.debug(
|
|
389
|
+
f"Event {len(events) - 1 - i} content has no parts, parts is not a list, or parts is empty. Skipping for text."
|
|
390
|
+
)
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
# Check the first part for text
|
|
394
|
+
first_part = parts[0]
|
|
395
|
+
if not isinstance(first_part, dict):
|
|
396
|
+
self.logger.debug(
|
|
397
|
+
f"Event {len(events) - 1 - i} first part is not a dict: {type(first_part)}. Skipping for text."
|
|
398
|
+
)
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
part_text = first_part.get("text")
|
|
402
|
+
if (
|
|
403
|
+
part_text is None
|
|
404
|
+
): # Explicitly check for None, as empty string is fine if stripped later
|
|
405
|
+
self.logger.debug(
|
|
406
|
+
f"Event {len(events) - 1 - i} first part has no 'text' key. Skipping for text."
|
|
407
|
+
)
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
if not isinstance(part_text, str):
|
|
411
|
+
self.logger.debug(
|
|
412
|
+
f"Event {len(events) - 1 - i} first part 'text' is not a string: {type(part_text)}. Skipping for text."
|
|
413
|
+
)
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
# At this point, part_text is a string (could be empty)
|
|
417
|
+
# The original code also checks `part_text.strip()` to ensure it's not just whitespace.
|
|
418
|
+
# Let's keep that check.
|
|
419
|
+
if part_text.strip():
|
|
420
|
+
final_response_text = part_text # Store the original text, stripping is for check only
|
|
421
|
+
self.logger.debug(
|
|
422
|
+
f"Found text in event {len(events) - 1 - i}, part 0, as final response: '{final_response_text[:100]}...'"
|
|
423
|
+
)
|
|
424
|
+
break # Found usable text, stop parsing
|
|
425
|
+
else:
|
|
426
|
+
self.logger.debug(
|
|
427
|
+
f"Event {len(events) - 1 - i} first part text is empty or whitespace after strip. Skipping for text."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if final_response_text is None:
|
|
431
|
+
self.logger.warning(
|
|
432
|
+
f"No final response text could be extracted from any of the {len(events)} ADK events from {response.url}."
|
|
433
|
+
)
|
|
434
|
+
return final_response_text, events, response_body_str, http_headers
|
|
435
|
+
except (
|
|
436
|
+
json.JSONDecodeError,
|
|
437
|
+
ValueError,
|
|
438
|
+
) as parse_err: # Catch ValueError too for broader JSON issues
|
|
439
|
+
self.logger.warning(
|
|
440
|
+
f"Failed to parse ADK JSON from {response.url}: {parse_err}. Body: {response_body_str[:500]}"
|
|
441
|
+
)
|
|
442
|
+
raise ResponseParsingError(f"JSON parse failed: {parse_err}") from parse_err
|
|
443
|
+
|
|
444
|
+
def _process_agent_interaction(self, prompt_text: str, session_id: str) -> dict:
|
|
445
|
+
"""
|
|
446
|
+
Manages a single interaction (turn) with the ADK agent for a given prompt.
|
|
447
|
+
|
|
448
|
+
This involves preparing the payload, executing the HTTP POST request to the
|
|
449
|
+
correct ADK :runTurn endpoint, and parsing the response.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
prompt_text: The prompt text to send to the ADK agent.
|
|
453
|
+
session_id: The ADK session ID for this interaction.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
A dictionary containing detailed results of the interaction, including:
|
|
457
|
+
- 'generated_text': The agent's final response text.
|
|
458
|
+
- 'adapter_specific_events': Full list of ADK events.
|
|
459
|
+
- 'raw_request': The payload sent to the agent.
|
|
460
|
+
- 'raw_response_status': HTTP status code of the agent's response.
|
|
461
|
+
- 'raw_response_headers': HTTP headers from the agent's response.
|
|
462
|
+
- 'raw_response_body': Raw body of the agent's response.
|
|
463
|
+
- 'error_message': Any error message if an issue occurred.
|
|
464
|
+
"""
|
|
465
|
+
interaction_result: Dict[str, Any] = {
|
|
466
|
+
"generated_text": None,
|
|
467
|
+
"adapter_specific_events": None,
|
|
468
|
+
"raw_request": None,
|
|
469
|
+
"raw_response_status": None,
|
|
470
|
+
"raw_response_headers": None,
|
|
471
|
+
"raw_response_body": None,
|
|
472
|
+
"error_message": None,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
headers, payload = self._prepare_request_payload(prompt_text, session_id)
|
|
477
|
+
interaction_result["raw_request"] = payload
|
|
478
|
+
|
|
479
|
+
# Reverting to the simple /run endpoint as per general ADK docs
|
|
480
|
+
run_turn_url = f"{self.endpoint}/run"
|
|
481
|
+
self.logger.debug(
|
|
482
|
+
f"Sending ADK request to: {run_turn_url} with payload app_name: {payload.get('app_name')}"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
response = self._execute_http_post(run_turn_url, headers, payload)
|
|
486
|
+
interaction_result["raw_response_status"] = response.status_code
|
|
487
|
+
# interaction_result["raw_response_headers"] is set in _parse_response_json
|
|
488
|
+
# interaction_result["raw_response_body"] is set in _parse_response_json
|
|
489
|
+
|
|
490
|
+
(
|
|
491
|
+
final_text,
|
|
492
|
+
events,
|
|
493
|
+
response_body_str,
|
|
494
|
+
resp_headers,
|
|
495
|
+
) = self._parse_response_json(response)
|
|
496
|
+
|
|
497
|
+
interaction_result["generated_text"] = final_text
|
|
498
|
+
interaction_result["adapter_specific_events"] = events
|
|
499
|
+
interaction_result["raw_response_body"] = response_body_str
|
|
500
|
+
interaction_result["raw_response_headers"] = (
|
|
501
|
+
dict(resp_headers) if resp_headers else None
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
except AgentInteractionError as aie:
|
|
505
|
+
self.logger.error(f"AgentInteractionError processing prompt: {aie}")
|
|
506
|
+
interaction_result["error_message"] = f"ADK Error: {aie}"
|
|
507
|
+
except ResponseParsingError as rpe:
|
|
508
|
+
self.logger.error(f"ResponseParsingError processing prompt: {rpe}")
|
|
509
|
+
interaction_result["error_message"] = f"ADK Response Parse Error: {rpe}"
|
|
510
|
+
except Exception as e:
|
|
511
|
+
self.logger.exception(f"Unexpected error during ADK agent interaction: {e}")
|
|
512
|
+
interaction_result["error_message"] = f"Unexpected ADK Adapter Error: {e}"
|
|
513
|
+
|
|
514
|
+
return interaction_result
|
|
515
|
+
|
|
516
|
+
def _build_error_response(
|
|
517
|
+
self,
|
|
518
|
+
error_message: str,
|
|
519
|
+
status_code: Optional[int],
|
|
520
|
+
raw_request: Optional[Dict[str, Any]] = None,
|
|
521
|
+
interaction_details: Optional[Dict[str, Any]] = None,
|
|
522
|
+
) -> Dict[str, Any]:
|
|
523
|
+
"""
|
|
524
|
+
Constructs a standardized error response dictionary for the adapter.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
error_message: The primary error message string.
|
|
528
|
+
status_code: The HTTP status code associated with the error, if applicable.
|
|
529
|
+
raw_request: The original request data that led to the error.
|
|
530
|
+
interaction_details: A dictionary containing details from
|
|
531
|
+
`_process_agent_interaction` if the error occurred
|
|
532
|
+
during ADK processing.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
A dictionary representing a standardized error response.
|
|
536
|
+
"""
|
|
537
|
+
raw_response_headers = None
|
|
538
|
+
raw_response_body = None
|
|
539
|
+
actual_status_code = status_code
|
|
540
|
+
adk_events = None
|
|
541
|
+
|
|
542
|
+
if interaction_details:
|
|
543
|
+
raw_response_headers = interaction_details.get("response_headers")
|
|
544
|
+
raw_response_body = interaction_details.get("response_body_raw")
|
|
545
|
+
adk_events = interaction_details.get("adk_events_list")
|
|
546
|
+
if interaction_details.get("response_status_code") is not None:
|
|
547
|
+
actual_status_code = interaction_details.get("response_status_code")
|
|
548
|
+
if raw_request is None:
|
|
549
|
+
raw_request = interaction_details.get("request_payload")
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
"raw_request": raw_request,
|
|
553
|
+
"processed_response": None,
|
|
554
|
+
"status_code": (
|
|
555
|
+
actual_status_code if actual_status_code is not None else 500
|
|
556
|
+
),
|
|
557
|
+
"raw_response_headers": raw_response_headers,
|
|
558
|
+
"raw_response_body": raw_response_body,
|
|
559
|
+
"agent_specific_data": {"adk_events_list": adk_events},
|
|
560
|
+
"error_message": error_message,
|
|
561
|
+
"agent_id": self.id,
|
|
562
|
+
"adapter_type": "ADKAgentAdapter",
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
566
|
+
"""
|
|
567
|
+
Handles an incoming request by creating an ADK session (if not existing)
|
|
568
|
+
and then processing the request through the ADK agent.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
request_data: A dictionary containing the request data. Must include
|
|
572
|
+
a 'prompt' key with the text to send to the agent.
|
|
573
|
+
Optional keys:
|
|
574
|
+
- 'session_id': Override the adapter's default session_id (advanced usage)
|
|
575
|
+
- 'initial_session_state': Initial state dict for new sessions
|
|
576
|
+
- 'adk_session_id': Deprecated, use 'session_id' instead
|
|
577
|
+
- 'adk_user_id': Deprecated, adapter manages user_id
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
A dictionary representing the agent's response or an error.
|
|
581
|
+
"""
|
|
582
|
+
prompt_text = request_data.get("prompt")
|
|
583
|
+
|
|
584
|
+
# Support both new 'session_id' and legacy 'adk_session_id' for backward compatibility
|
|
585
|
+
session_id_from_request = request_data.get(
|
|
586
|
+
"session_id", request_data.get("adk_session_id")
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Use adapter's instance session_id if not provided in request
|
|
590
|
+
session_id_to_use = (
|
|
591
|
+
session_id_from_request if session_id_from_request else self.session_id
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
initial_session_state = request_data.get("initial_session_state") # Optional
|
|
595
|
+
|
|
596
|
+
if not prompt_text:
|
|
597
|
+
self.logger.warning("No 'prompt' found in request_data.")
|
|
598
|
+
return self._build_error_response(
|
|
599
|
+
error_message="Request data must include a 'prompt' field.",
|
|
600
|
+
status_code=400,
|
|
601
|
+
raw_request=request_data,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
self.logger.info(
|
|
605
|
+
f"Handling request for agent {self.id} with prompt: '{prompt_text[:75]}...' (Session: {session_id_to_use})"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# Step 1: Ensure ADK session exists
|
|
610
|
+
self.logger.info(
|
|
611
|
+
f"Ensuring ADK session '{session_id_to_use}' exists before running turn."
|
|
612
|
+
)
|
|
613
|
+
self._create_session_internal(
|
|
614
|
+
session_id=session_id_to_use, initial_state=initial_session_state
|
|
615
|
+
)
|
|
616
|
+
# If _create_session_internal raises, it will be caught by the outer try-except
|
|
617
|
+
self.logger.info(f"Session '{session_id_to_use}' confirmed/created.")
|
|
618
|
+
|
|
619
|
+
# Step 2: Process the agent interaction (send to /run)
|
|
620
|
+
interaction_details = self._process_agent_interaction(
|
|
621
|
+
prompt_text, session_id=session_id_to_use
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
if interaction_details.get("error_message"):
|
|
625
|
+
self.logger.warning(
|
|
626
|
+
f"ADK interaction for agent {self.id} (session {session_id_to_use}) processed with error: "
|
|
627
|
+
f"{interaction_details['error_message']}"
|
|
628
|
+
)
|
|
629
|
+
# Pass full interaction_details to enrich the error response
|
|
630
|
+
return self._build_error_response(
|
|
631
|
+
error_message=interaction_details["error_message"],
|
|
632
|
+
status_code=interaction_details.get("raw_response_status"),
|
|
633
|
+
interaction_details=interaction_details,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# Success case
|
|
637
|
+
return {
|
|
638
|
+
"raw_request": interaction_details.get(
|
|
639
|
+
"raw_request"
|
|
640
|
+
), # Changed from request_payload
|
|
641
|
+
"generated_text": interaction_details.get("generated_text"),
|
|
642
|
+
"status_code": interaction_details.get("raw_response_status"),
|
|
643
|
+
"raw_response_headers": interaction_details.get("raw_response_headers"),
|
|
644
|
+
"raw_response_body": interaction_details.get("raw_response_body"),
|
|
645
|
+
"agent_specific_data": {
|
|
646
|
+
"adk_events_list": interaction_details.get(
|
|
647
|
+
"adapter_specific_events"
|
|
648
|
+
)
|
|
649
|
+
},
|
|
650
|
+
"error_message": None,
|
|
651
|
+
"agent_id": self.id,
|
|
652
|
+
"adapter_type": "ADKAgentAdapter",
|
|
653
|
+
}
|
|
654
|
+
except AgentInteractionError as aie_session: # Specific catch for session errors from _create_session_internal
|
|
655
|
+
self.logger.error(
|
|
656
|
+
f"Failed to ensure ADK session '{session_id_to_use}': {aie_session}"
|
|
657
|
+
)
|
|
658
|
+
return self._build_error_response(
|
|
659
|
+
error_message=f"Failed to create/verify ADK session '{session_id_to_use}': {aie_session}",
|
|
660
|
+
status_code=500, # Or a more specific code if available from aie_session
|
|
661
|
+
raw_request=request_data,
|
|
662
|
+
)
|
|
663
|
+
except Exception as e:
|
|
664
|
+
self.logger.exception(
|
|
665
|
+
f"Unexpected error in handle_request for agent {self.id} (session {session_id_to_use}): {e}"
|
|
666
|
+
)
|
|
667
|
+
return self._build_error_response(
|
|
668
|
+
error_message=f"Unexpected adapter error: {type(e).__name__} - {str(e)}",
|
|
669
|
+
status_code=500,
|
|
670
|
+
raw_request=request_data,
|
|
671
|
+
)
|