hackagent 0.1.0__py3-none-any.whl

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