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