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,969 @@
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
+ import logging
16
+ from typing import Any, Dict, Optional, Type, Union
17
+ from uuid import UUID
18
+
19
+ from hackagent.api.agent import agent_create, agent_list, agent_partial_update
20
+ from hackagent.client import AuthenticatedClient
21
+ from hackagent.models import (
22
+ Agent as BackendAgentModel,
23
+ )
24
+ from hackagent.models import (
25
+ AgentRequest,
26
+ PatchedAgentRequest,
27
+ )
28
+ from hackagent.router.adapters import ADKAgentAdapter
29
+ from hackagent.router.adapters.base import Agent
30
+ from hackagent.router.adapters.litellm_adapter import LiteLLMAgentAdapter
31
+ from hackagent.router.adapters.openai_adapter import OpenAIAgentAdapter
32
+ from hackagent.router.types import AgentTypeEnum
33
+
34
+ from ..types import UNSET, Unset
35
+
36
+ # Use explicit hierarchical logger name for clarity
37
+ logger = logging.getLogger("hackagent.router")
38
+
39
+ # --- Agent Type to Adapter Mapping ---
40
+ AGENT_TYPE_TO_ADAPTER_MAP: Dict[AgentTypeEnum, Type[Agent]] = {
41
+ AgentTypeEnum.GOOGLE_ADK: ADKAgentAdapter,
42
+ AgentTypeEnum.LITELLM: LiteLLMAgentAdapter,
43
+ AgentTypeEnum.OPENAI_SDK: OpenAIAgentAdapter,
44
+ AgentTypeEnum.LANGCHAIN: LiteLLMAgentAdapter, # LangChain agents can use LiteLLM adapter
45
+ # Add other agent types and their corresponding adapters here
46
+ }
47
+
48
+
49
+ class AgentRouter:
50
+ """
51
+ Manages the configuration and request routing for a single agent instance.
52
+
53
+ The `AgentRouter` is responsible for initializing an agent, which includes:
54
+ 1. Fetching necessary contextual information like Organization ID and User ID
55
+ based on the provided authenticated client's API key.
56
+ 2. Ensuring the agent is registered in the HackAgent backend. This involves
57
+ checking if an agent with the specified name, type, and organization
58
+ already exists. If not, it creates a new agent. If it exists, it may
59
+ update its metadata based on the `overwrite_metadata` flag.
60
+ 3. Instantiating the appropriate adapter (e.g., `ADKAgentAdapter`,
61
+ `LiteLLMAgentAdapter`) based on the `agent_type`.
62
+ 4. Storing this adapter for subsequent request routing.
63
+
64
+ Once initialized, the router uses the adapter to handle requests directed
65
+ to the managed agent.
66
+
67
+ Attributes:
68
+ client: An `AuthenticatedClient` instance for API communication.
69
+ organization_id: The UUID of the organization associated with the API key.
70
+ user_id_str: The string representation of the user ID associated with the API key.
71
+ backend_agent: The `BackendAgentModel` instance representing the agent
72
+ in the HackAgent backend (after creation or retrieval).
73
+ _agent_registry: A dictionary mapping agent registration keys (backend ID)
74
+ to their instantiated adapter `Agent` objects.
75
+ """
76
+
77
+ def _fetch_organization_id(self) -> UUID:
78
+ """
79
+ Fetches the organization ID (UUID) associated with the API key.
80
+
81
+ This method lists agents accessible by the current client's token and
82
+ extracts the organization ID from any agent. All agents for a given API key
83
+ belong to the same organization, so we can use any agent's organization field.
84
+
85
+ Note: We use the /agent endpoint instead of /key because /key is Auth0-only
86
+ for security reasons (prevents API keys from managing other API keys).
87
+
88
+ Returns:
89
+ The UUID of the organization.
90
+
91
+ Raises:
92
+ RuntimeError: If the organization ID cannot be determined (e.g., no agents
93
+ exist, organization field missing, or API call fails).
94
+ """
95
+ try:
96
+ logger.debug(
97
+ "AgentRouter: Attempting to retrieve Organization ID by listing agents..."
98
+ )
99
+ agents_response = agent_list.sync_detailed(client=self.client)
100
+
101
+ if (
102
+ agents_response.status_code == 200
103
+ and agents_response.parsed
104
+ and agents_response.parsed.results
105
+ ):
106
+ first_agent = agents_response.parsed.results[0]
107
+ if hasattr(first_agent, "organization") and isinstance(
108
+ first_agent.organization, UUID
109
+ ):
110
+ logger.info(
111
+ f"AgentRouter: Successfully determined Organization ID: {first_agent.organization} from agent '{first_agent.name}'."
112
+ )
113
+ return first_agent.organization
114
+ else:
115
+ org_type = (
116
+ type(first_agent.organization).__name__
117
+ if hasattr(first_agent, "organization")
118
+ else "Missing"
119
+ )
120
+ logger.error(
121
+ f"AgentRouter: Agent '{first_agent.name}' has invalid organization field (type: {org_type})."
122
+ )
123
+ raise RuntimeError(
124
+ f"AgentRouter: Could not determine Organization ID (invalid type: {org_type})."
125
+ )
126
+ elif agents_response.parsed and not agents_response.parsed.results:
127
+ logger.error(
128
+ "AgentRouter: No agents found. Cannot determine Organization ID."
129
+ )
130
+ raise RuntimeError(
131
+ "AgentRouter: No agents exist for Organization ID retrieval. Create an agent first."
132
+ )
133
+ else:
134
+ content = (
135
+ agents_response.content.decode()
136
+ if agents_response.content
137
+ else "N/A"
138
+ )
139
+ logger.error(
140
+ f"AgentRouter: Failed to list agents for Org ID. Status: {agents_response.status_code}, Body: {content}"
141
+ )
142
+ raise RuntimeError(
143
+ f"AgentRouter: Agent list failed for Organization ID (status {agents_response.status_code})."
144
+ )
145
+ except RuntimeError:
146
+ raise
147
+ except Exception as e:
148
+ logger.error(
149
+ f"AgentRouter: Exception fetching Organization ID: {e}", exc_info=True
150
+ )
151
+ raise RuntimeError(f"AgentRouter: Exception fetching Organization ID: {e}")
152
+
153
+ def _fetch_user_id_str(self) -> str:
154
+ """
155
+ Fetches the user ID associated with the API key and returns it as a string.
156
+
157
+ This method lists agents accessible by the current client's token and
158
+ extracts the owner (user ID) from any agent. All agents for a given API key
159
+ belong to the same user, so we can use any agent's owner field.
160
+
161
+ Note: We use the /agent endpoint instead of /key because /key is Auth0-only
162
+ for security reasons (prevents API keys from managing other API keys).
163
+
164
+ Returns:
165
+ The string representation of the user ID.
166
+
167
+ Raises:
168
+ RuntimeError: If the user ID cannot be determined (e.g., no agents
169
+ exist, owner field missing/null, or API call fails).
170
+ """
171
+ try:
172
+ logger.debug(
173
+ "AgentRouter: Attempting to retrieve User ID by listing agents..."
174
+ )
175
+ agents_response = agent_list.sync_detailed(client=self.client)
176
+
177
+ if (
178
+ agents_response.status_code == 200
179
+ and agents_response.parsed
180
+ and agents_response.parsed.results
181
+ ):
182
+ first_agent = agents_response.parsed.results[0]
183
+ if hasattr(first_agent, "owner") and isinstance(first_agent.owner, int):
184
+ user_id_as_str = str(first_agent.owner)
185
+ logger.info(
186
+ f"AgentRouter: Successfully determined User ID (str): {user_id_as_str} from agent '{first_agent.name}'."
187
+ )
188
+ return user_id_as_str
189
+ else:
190
+ owner_type = (
191
+ type(first_agent.owner).__name__
192
+ if hasattr(first_agent, "owner")
193
+ else "Missing"
194
+ )
195
+ logger.error(
196
+ f"AgentRouter: Agent '{first_agent.name}' has invalid owner field (type: {owner_type})."
197
+ )
198
+ raise RuntimeError(
199
+ f"AgentRouter: Could not determine User ID (invalid type: {owner_type})."
200
+ )
201
+ elif agents_response.parsed and not agents_response.parsed.results:
202
+ logger.error("AgentRouter: No agents found. Cannot determine User ID.")
203
+ raise RuntimeError(
204
+ "AgentRouter: No agents exist for User ID retrieval. Create an agent first."
205
+ )
206
+ else:
207
+ content = (
208
+ agents_response.content.decode()
209
+ if agents_response.content
210
+ else "N/A"
211
+ )
212
+ logger.error(
213
+ f"AgentRouter: Failed to list agents for User ID. Status: {agents_response.status_code}, Body: {content}"
214
+ )
215
+ raise RuntimeError(
216
+ f"AgentRouter: Agent list failed for User ID (status {agents_response.status_code})."
217
+ )
218
+ except RuntimeError:
219
+ raise
220
+ except Exception as e:
221
+ logger.error(f"AgentRouter: Exception fetching User ID: {e}", exc_info=True)
222
+ raise RuntimeError(f"AgentRouter: Exception fetching User ID: {e}")
223
+
224
+ def __init__(
225
+ self,
226
+ client: AuthenticatedClient,
227
+ name: str,
228
+ agent_type: AgentTypeEnum,
229
+ endpoint: str,
230
+ metadata: Optional[Dict[str, Any]] = None,
231
+ adapter_operational_config: Optional[Dict[str, Any]] = None,
232
+ overwrite_metadata: bool = True,
233
+ ):
234
+ """
235
+ Initializes the AgentRouter and configures a single agent.
236
+
237
+ This constructor performs several key setup steps:
238
+ 1. Fetches the organization and user IDs using the provided client.
239
+ 2. Validates the `agent_type` against supported adapters.
240
+ 3. Prepares metadata and operational configurations for the agent and its adapter.
241
+ For `AgentTypeEnum.GOOGLE_ADK`, it ensures `user_id` is set in the
242
+ adapter's operational config, using the fetched User ID if not provided.
243
+ 4. Calls `ensure_agent_in_backend` to create or update the agent's record
244
+ in the HackAgent backend.
245
+ 5. Calls `_configure_and_instantiate_adapter` to set up the specific adapter
246
+ for the agent type.
247
+
248
+ Args:
249
+ client: An `AuthenticatedClient` for backend API interactions.
250
+ name: The desired name for the agent in the backend.
251
+ agent_type: The type of the agent (e.g., `AgentTypeEnum.GOOGLE_ADK`).
252
+ endpoint: The API endpoint URL for the agent service itself. This is used
253
+ for backend registration and can also be used by the adapter.
254
+ metadata: Optional. Metadata to be stored with the agent's record in the
255
+ backend. Structure can vary by agent type. For example, for
256
+ `AgentTypeEnum.LITELLM`, this might include `{'model_name': ..., 'api_key_env_var': ...}`.
257
+ adapter_operational_config: Optional. Runtime configuration specific to the
258
+ adapter instance. This can override or augment values derived from
259
+ the backend agent's metadata. For `AgentTypeEnum.GOOGLE_ADK`, this might
260
+ include `{'user_id': ..., 'session_id': ...}`. For `AgentTypeEnum.LITELLM`,
261
+ it must provide the model string ('name') if not in backend metadata.
262
+ overwrite_metadata: If `True` (default), and an agent with the same name,
263
+ type, and organization already exists in the backend, its metadata
264
+ will be updated with the provided `metadata`. If `False`, existing
265
+ metadata is preserved.
266
+
267
+ Raises:
268
+ ValueError: If the `agent_type` is unsupported, if adapter instantiation fails,
269
+ or if critical configuration for an adapter type (e.g., model name for LiteLLM)
270
+ is missing.
271
+ RuntimeError: If backend communication (e.g., fetching org/user ID, creating/
272
+ updating agent) fails.
273
+ """
274
+ self.client = client
275
+ self._agent_registry: Dict[str, Agent] = {}
276
+
277
+ self.organization_id = self._fetch_organization_id()
278
+ self.user_id_str = self._fetch_user_id_str()
279
+ logger.info(
280
+ f"AgentRouter initialized with Organization ID (UUID): {self.organization_id} and User ID (str): {self.user_id_str}"
281
+ )
282
+
283
+ if agent_type not in AGENT_TYPE_TO_ADAPTER_MAP:
284
+ raise ValueError(
285
+ f"Unsupported agent type: {agent_type}. Supported types: {list(AGENT_TYPE_TO_ADAPTER_MAP.keys())}"
286
+ )
287
+
288
+ actual_metadata = metadata.copy() if metadata is not None else {}
289
+
290
+ current_adapter_op_config = (
291
+ adapter_operational_config.copy() if adapter_operational_config else {}
292
+ )
293
+
294
+ if agent_type == AgentTypeEnum.GOOGLE_ADK:
295
+ if "user_id" not in current_adapter_op_config:
296
+ current_adapter_op_config["user_id"] = self.user_id_str
297
+ logger.info(
298
+ f"ADK Agent: Using fetched User ID '{self.user_id_str}' for adapter operational config."
299
+ )
300
+ else:
301
+ logger.warning(
302
+ f"ADK Agent: 'user_id' was already present in adapter_operational_config ('{current_adapter_op_config['user_id']}'). Using that value instead of fetched one."
303
+ )
304
+
305
+ self.backend_agent = self.ensure_agent_in_backend(
306
+ name=name,
307
+ agent_type=agent_type,
308
+ endpoint_for_backend=endpoint,
309
+ metadata_for_backend=actual_metadata,
310
+ update_metadata_if_exists=overwrite_metadata,
311
+ )
312
+
313
+ registration_key = str(self.backend_agent.id)
314
+
315
+ self._configure_and_instantiate_adapter(
316
+ name=name,
317
+ agent_type=agent_type,
318
+ registration_key=registration_key,
319
+ adapter_operational_config=current_adapter_op_config,
320
+ )
321
+
322
+ def _configure_and_instantiate_adapter(
323
+ self,
324
+ name: str,
325
+ agent_type: AgentTypeEnum,
326
+ registration_key: str,
327
+ adapter_operational_config: Optional[Dict[str, Any]],
328
+ ) -> None:
329
+ """
330
+ Configures, instantiates, and registers the appropriate agent adapter.
331
+
332
+ This method selects the adapter class based on `agent_type`, prepares its
333
+ specific configuration by merging `adapter_operational_config` with details
334
+ from `self.backend_agent` (like name, endpoint, or specific metadata fields
335
+ depending on the agent type), and then creates an instance of the adapter.
336
+ The instantiated adapter is stored in `self._agent_registry` using the
337
+ `registration_key` (backend agent ID).
338
+
339
+ Args:
340
+ name: The name of the agent (primarily for logging/identification).
341
+ agent_type: The `AgentTypeEnum` of the agent.
342
+ registration_key: The backend ID of the agent, used as the key for
343
+ storing the adapter in the registry.
344
+ adapter_operational_config: The base operational configuration for the
345
+ adapter, which will be augmented with type-specific details.
346
+
347
+ Raises:
348
+ ValueError: If essential configuration for an adapter type is missing
349
+ (e.g., model name for LiteLLM) or if adapter instantiation fails.
350
+ """
351
+ adapter_class = AGENT_TYPE_TO_ADAPTER_MAP[agent_type]
352
+
353
+ logger.debug(
354
+ f"ROUTER_DEBUG: adapter_class is: {adapter_class}, type: {type(adapter_class)}, id: {id(adapter_class)}"
355
+ )
356
+
357
+ adapter_instance_config = (
358
+ adapter_operational_config.copy() if adapter_operational_config else {}
359
+ )
360
+
361
+ if agent_type == AgentTypeEnum.GOOGLE_ADK:
362
+ adapter_instance_config["name"] = self.backend_agent.name
363
+ adapter_instance_config["endpoint"] = self.backend_agent.endpoint
364
+ if "user_id" not in adapter_instance_config:
365
+ logger.error(
366
+ f"CRITICAL: user_id not found in adapter_instance_config for ADK agent '{self.backend_agent.name}' just before adapter instantiation. This should have been set in __init__."
367
+ )
368
+ adapter_instance_config["user_id"] = self.user_id_str
369
+
370
+ elif agent_type in [AgentTypeEnum.LITELLM, AgentTypeEnum.LANGCHAIN]:
371
+ if "name" not in adapter_instance_config:
372
+ if (
373
+ isinstance(self.backend_agent.metadata, dict)
374
+ and "name" in self.backend_agent.metadata
375
+ ):
376
+ adapter_instance_config["name"] = self.backend_agent.metadata[
377
+ "name"
378
+ ]
379
+ else:
380
+ logger.warning(
381
+ f"Agent '{name}' (Type: {agent_type.value}) missing 'name' (model string) in metadata. "
382
+ f"Defaulting to agent name '{self.backend_agent.name}'."
383
+ )
384
+ adapter_instance_config["name"] = self.backend_agent.name
385
+
386
+ # Always use backend agent's endpoint if not already in config
387
+ if (
388
+ "endpoint" not in adapter_instance_config
389
+ and self.backend_agent.endpoint
390
+ ):
391
+ adapter_instance_config["endpoint"] = self.backend_agent.endpoint
392
+
393
+ optional_litellm_keys = [
394
+ "api_key",
395
+ "max_new_tokens",
396
+ "temperature",
397
+ "top_p",
398
+ ]
399
+ if isinstance(self.backend_agent.metadata, dict):
400
+ for key in optional_litellm_keys:
401
+ if (
402
+ key not in adapter_instance_config
403
+ and key in self.backend_agent.metadata
404
+ ):
405
+ adapter_instance_config[key] = self.backend_agent.metadata[key]
406
+
407
+ # For hackagent/* models, pass the HackAgent API key for authentication
408
+ model_name = adapter_instance_config.get("name", "")
409
+ if model_name.startswith("hackagent/"):
410
+ adapter_instance_config["hackagent_api_key"] = self.client.token
411
+
412
+ elif agent_type == AgentTypeEnum.OPENAI_SDK:
413
+ if "name" not in adapter_instance_config:
414
+ if (
415
+ isinstance(self.backend_agent.metadata, dict)
416
+ and "name" in self.backend_agent.metadata
417
+ ):
418
+ adapter_instance_config["name"] = self.backend_agent.metadata[
419
+ "name"
420
+ ]
421
+ else:
422
+ raise ValueError(
423
+ f"OpenAI SDK agent '{name}' (ID: {registration_key}) missing "
424
+ f"'name' (model string) in adapter_operational_config or backend metadata. "
425
+ f"Cannot configure OpenAIAgentAdapter."
426
+ )
427
+
428
+ # Always use backend agent's endpoint if not already in config
429
+ if (
430
+ "endpoint" not in adapter_instance_config
431
+ and self.backend_agent.endpoint
432
+ ):
433
+ adapter_instance_config["endpoint"] = self.backend_agent.endpoint
434
+
435
+ optional_openai_keys = [
436
+ "api_key",
437
+ "max_tokens",
438
+ "temperature",
439
+ "tools",
440
+ "tool_choice",
441
+ ]
442
+ if isinstance(self.backend_agent.metadata, dict):
443
+ for key in optional_openai_keys:
444
+ if (
445
+ key not in adapter_instance_config
446
+ and key in self.backend_agent.metadata
447
+ ):
448
+ adapter_instance_config[key] = self.backend_agent.metadata[key]
449
+
450
+ try:
451
+ logger.debug(
452
+ f"ROUTER_DEBUG: About to call adapter_class(id='{registration_key}', config_keys={list(adapter_instance_config.keys())})"
453
+ )
454
+ adapter_instance = adapter_class(
455
+ id=registration_key, config=adapter_instance_config
456
+ )
457
+ logger.debug(
458
+ f"ROUTER_DEBUG: Called adapter_class. Resulting instance: {adapter_instance}, type: {type(adapter_instance)}"
459
+ )
460
+ self._agent_registry[registration_key] = adapter_instance
461
+ logger.info(
462
+ f"Agent '{name}' (Backend ID: {registration_key}, Type: {agent_type.value}) "
463
+ f"successfully initialized and registered with adapter {adapter_class.__name__}. "
464
+ f"Adapter config keys: {list(adapter_instance_config.keys())}"
465
+ )
466
+ except Exception as e:
467
+ logger.error(
468
+ f"Failed to instantiate adapter for agent '{name}' (Backend ID: {registration_key}): {e}",
469
+ exc_info=True,
470
+ )
471
+ raise ValueError(
472
+ f"Failed to instantiate adapter {adapter_class.__name__}: {e}"
473
+ ) from e
474
+
475
+ def _find_existing_agent(
476
+ self,
477
+ name: str,
478
+ ) -> Optional[BackendAgentModel]:
479
+ """
480
+ Finds an existing agent in the backend by its name and organization.
481
+
482
+ This method paginates through the list of all agents accessible via the
483
+ client's API key. It matches agents based on the provided `name`
484
+ and the `self.organization_id` (UUID) of the router instance.
485
+ The organization ID match is crucial for ensuring the correct agent is
486
+ identified in a multi-tenant environment.
487
+
488
+ The method checks both `agent_model.organization` (expected to be a UUID)
489
+ and falls back to `agent_model.organization_detail.id` if necessary.
490
+
491
+ Args:
492
+ name: The name of the agent to find.
493
+
494
+ Returns:
495
+ A `BackendAgentModel` instance if a matching agent is found, otherwise `None`.
496
+ """
497
+ logger.debug(
498
+ f"SYNC_DEBUG: Entered _find_existing_agent for Name='{name}', OrgID='{self.organization_id}' (UUID)"
499
+ )
500
+
501
+ current_page: Union[Unset, int] = UNSET
502
+ agents_processed_count = 0
503
+
504
+ while True:
505
+ list_response = None
506
+ try:
507
+ list_response = agent_list.sync_detailed(
508
+ client=self.client, page=current_page
509
+ )
510
+ logger.debug(
511
+ f"SYNC_DEBUG: Fetched page of agents. Status: {list_response.status_code if list_response else 'N/A'}"
512
+ )
513
+
514
+ except Exception as e:
515
+ logger.error(
516
+ f"SYNC_DEBUG: An unexpected error occurred during 'agents_list.sync_detailed' while fetching page {current_page if not isinstance(current_page, Unset) else 'initial'}: {e}",
517
+ exc_info=True,
518
+ )
519
+ return None
520
+
521
+ if (
522
+ list_response
523
+ and list_response.status_code == 200
524
+ and list_response.parsed
525
+ ):
526
+ paginated_result = list_response.parsed
527
+ results_list = getattr(paginated_result, "results", [])
528
+ logger.debug(
529
+ f"SYNC_DEBUG: Page {current_page if not isinstance(current_page, Unset) else 'initial'} - Number of results on page: {len(results_list) if results_list else 0}"
530
+ )
531
+ if not isinstance(results_list, list):
532
+ logger.warning(
533
+ f"SYNC_DEBUG: Expected 'results' to be a list, but got {type(results_list)}. Full parsed response: {paginated_result}"
534
+ )
535
+ results_list = []
536
+
537
+ for agent_model in results_list:
538
+ agents_processed_count += 1
539
+ logger.debug(
540
+ f"SYNC_DEBUG: Checking agent: ID={agent_model.id}, Name={getattr(agent_model, 'name', 'N/A')}, Type={getattr(agent_model, 'agent_type', getattr(agent_model, 'type', 'N/A'))}, Org={getattr(agent_model, 'organization', 'N/A')}"
541
+ )
542
+
543
+ name_matches = (
544
+ hasattr(agent_model, "name") and agent_model.name == name
545
+ )
546
+
547
+ org_matches = False
548
+ if hasattr(agent_model, "organization") and isinstance(
549
+ agent_model.organization, UUID
550
+ ):
551
+ if agent_model.organization == self.organization_id:
552
+ org_matches = True
553
+ elif (
554
+ hasattr(agent_model, "organization_detail")
555
+ and hasattr(agent_model.organization_detail, "id")
556
+ and isinstance(agent_model.organization_detail.id, UUID)
557
+ ):
558
+ if agent_model.organization_detail.id == self.organization_id:
559
+ org_matches = True
560
+ logger.debug(
561
+ f"SYNC_DEBUG: Matched OrgID via organization_detail.id for agent '{agent_model.name}'"
562
+ )
563
+ elif hasattr(agent_model, "organization") and isinstance(
564
+ agent_model.organization, int
565
+ ):
566
+ logger.warning(
567
+ f"SYNC_DEBUG: agent_model.organization is an int ('{agent_model.organization}') for agent '{agent_model.name}'. Schema mismatch with expected UUID ('{self.organization_id}')."
568
+ )
569
+
570
+ if not name_matches:
571
+ logger.debug(
572
+ f"SYNC_DEBUG: Agent ID '{agent_model.id}' ('{agent_model.name}') failed name match (expected '{name}')"
573
+ )
574
+ elif not org_matches:
575
+ logger.debug(
576
+ f"SYNC_DEBUG: Agent ID '{agent_model.id}' ('{agent_model.name}') failed organization match (expected UUID '{self.organization_id}', found '{getattr(agent_model, 'organization', 'N/A')}' or detail '{getattr(agent_model.organization_detail, 'id', 'N/A') if hasattr(agent_model, 'organization_detail') else 'N/A'}')"
577
+ )
578
+
579
+ if name_matches and org_matches:
580
+ logger.info(
581
+ f"SYNC_DEBUG: Found existing backend agent '{name}' (OrgID: {self.organization_id}) "
582
+ f"with ID {agent_model.id} on page {current_page if not isinstance(current_page, Unset) else 'initial'}. Processed {agents_processed_count} agents total so far."
583
+ )
584
+ return agent_model
585
+
586
+ if (
587
+ hasattr(paginated_result, "next_")
588
+ and paginated_result.next_
589
+ and not isinstance(paginated_result.next_, Unset)
590
+ ):
591
+ next_page_url = paginated_result.next_
592
+ try:
593
+ if isinstance(next_page_url, str) and "page=" in next_page_url:
594
+ current_page = int(
595
+ next_page_url.split("page=")[-1].split("&")[0]
596
+ )
597
+ elif isinstance(next_page_url, int):
598
+ current_page = next_page_url
599
+ else:
600
+ current_page = (
601
+ current_page if isinstance(current_page, int) else 1
602
+ ) + 1
603
+ except ValueError:
604
+ logger.warning(
605
+ f"Could not parse next page number from URL: {next_page_url}. Using simple increment."
606
+ )
607
+ current_page = (
608
+ current_page if isinstance(current_page, int) else 1
609
+ ) + 1
610
+
611
+ logger.debug(
612
+ f"SYNC_DEBUG: Moving to next page of agents: {current_page}"
613
+ )
614
+ else:
615
+ logger.debug(
616
+ f"SYNC_DEBUG: No more pages of agents to fetch. Processed {agents_processed_count} agents in total."
617
+ )
618
+ break
619
+
620
+ elif list_response and list_response.status_code != 200:
621
+ logger.error(
622
+ f"SYNC_DEBUG: Failed to list agents on page {current_page if not isinstance(current_page, Unset) else 'initial'}. Status: {list_response.status_code}, "
623
+ f"Body: {list_response.content.decode() if list_response.content else 'N/A'}"
624
+ )
625
+ return None
626
+ elif not list_response:
627
+ logger.error(
628
+ f"SYNC_DEBUG: Failed to get any response from agents_list API for page {current_page if not isinstance(current_page, Unset) else 'initial'}."
629
+ )
630
+ return None
631
+ else:
632
+ logger.warning(
633
+ f"SYNC_DEBUG: Unexpected state after trying to fetch page {current_page if not isinstance(current_page, Unset) else 'initial'}. list_response: {list_response}"
634
+ )
635
+ return None
636
+
637
+ logger.debug(
638
+ f"SYNC_DEBUG: No existing backend agent found matching Name='{name}', OrgID='{self.organization_id}' after searching all pages."
639
+ )
640
+ return None
641
+
642
+ def _update_agent(self, agent_id: UUID, **kwargs) -> BackendAgentModel:
643
+ """
644
+ Updates an existing agent in the backend.
645
+
646
+ Args:
647
+ agent_id: The UUID of the agent to update.
648
+ **kwargs: Fields to update (e.g., metadata, agent_type).
649
+ """
650
+ logger.info(
651
+ f"Attempting to update backend agent ID: {agent_id} with fields: {list(kwargs.keys())}"
652
+ )
653
+ patch_body = PatchedAgentRequest(**kwargs)
654
+ try:
655
+ update_response = agent_partial_update.sync_detailed(
656
+ client=self.client, id=agent_id, body=patch_body
657
+ )
658
+ if update_response.status_code == 200 and update_response.parsed:
659
+ logger.info(f"Successfully updated backend agent ID: {agent_id}.")
660
+ return update_response.parsed
661
+ else:
662
+ err_msg = (
663
+ f"Failed to update backend agent ID: {agent_id}. "
664
+ f"Status: {update_response.status_code}, "
665
+ f"Body: {update_response.content.decode() if update_response.content else 'N/A'}"
666
+ )
667
+ logger.error(err_msg)
668
+ raise RuntimeError(err_msg)
669
+ except Exception as e:
670
+ err_msg_ex = (
671
+ f"Exception during backend agent update for ID: {agent_id}: {e}"
672
+ )
673
+ logger.error(err_msg_ex, exc_info=True)
674
+ raise RuntimeError(err_msg_ex) from e
675
+
676
+ def _create_new_agent(
677
+ self,
678
+ name: str,
679
+ agent_type: AgentTypeEnum,
680
+ endpoint: str,
681
+ metadata: Dict[str, Any],
682
+ description: str,
683
+ ) -> BackendAgentModel:
684
+ """
685
+ Creates a new agent in the backend.
686
+
687
+ The new agent is associated with the `self.organization_id` (UUID) of the router.
688
+
689
+ Args:
690
+ name: The name for the new agent.
691
+ agent_type: The `AgentTypeEnum` for the new agent.
692
+ endpoint: The endpoint URL for the new agent.
693
+ metadata: A dictionary of metadata for the new agent.
694
+ description: A descriptive string for the new agent.
695
+
696
+ Returns:
697
+ The created `BackendAgentModel` instance.
698
+
699
+ Raises:
700
+ RuntimeError: If the API call to create the agent fails.
701
+ """
702
+ logger.info(
703
+ f"Creating new backend agent: Name='{name}', Type='{agent_type.value}', OrgID='{self.organization_id}' (UUID)"
704
+ )
705
+
706
+ agent_req_body = AgentRequest(
707
+ name=name,
708
+ endpoint=endpoint,
709
+ agent_type=agent_type.value,
710
+ metadata=metadata,
711
+ description=description,
712
+ )
713
+
714
+ try:
715
+ create_response = agent_create.sync_detailed(
716
+ client=self.client, body=agent_req_body
717
+ )
718
+ if create_response.status_code == 201 and create_response.parsed:
719
+ logger.info(
720
+ f"Created backend agent '{name}' (Type: {agent_type.value}) with ID {create_response.parsed.id}."
721
+ )
722
+ return create_response.parsed
723
+ else:
724
+ body_content = (
725
+ create_response.content.decode()
726
+ if create_response.content
727
+ else "N/A"
728
+ )
729
+ err_msg = (
730
+ f"Failed to create backend agent '{name}'. Status: {create_response.status_code}, "
731
+ f"Body: {body_content}"
732
+ )
733
+ logger.error(err_msg)
734
+ raise RuntimeError(err_msg)
735
+ except Exception as e:
736
+ err_msg_ex = (
737
+ f"Exception during backend agent creation for Name='{name}': {e}"
738
+ )
739
+ logger.error(err_msg_ex, exc_info=True)
740
+ raise RuntimeError(err_msg_ex) from e
741
+
742
+ def ensure_agent_in_backend(
743
+ self,
744
+ name: str,
745
+ agent_type: AgentTypeEnum,
746
+ endpoint_for_backend: str,
747
+ metadata_for_backend: Dict[str, Any],
748
+ description_prefix: str = "Agent managed by router",
749
+ update_metadata_if_exists: bool = True,
750
+ ) -> BackendAgentModel:
751
+ """
752
+ Ensures an agent with the given specifications exists in the backend.
753
+
754
+ This method first attempts to find an existing agent matching the name,
755
+ type, and the router's `self.organization_id`. If found, it checks if its
756
+ metadata needs updating based on `metadata_for_backend`. If an update is
757
+ needed and `update_metadata_if_exists` is `True`, it performs the update.
758
+ If the agent is not found, a new one is created.
759
+
760
+ Args:
761
+ name: The name of the agent.
762
+ agent_type: The `AgentTypeEnum` of the agent.
763
+ endpoint_for_backend: The endpoint URL for the agent.
764
+ metadata_for_backend: The desired metadata for the agent in the backend.
765
+ description_prefix: A prefix for the description of a newly created agent.
766
+ The agent's name will be appended to this prefix.
767
+ update_metadata_if_exists: If `True` and the agent exists, its metadata
768
+ will be updated if it differs from `metadata_for_backend`.
769
+
770
+ Returns:
771
+ The `BackendAgentModel` of the existing (possibly updated) or newly
772
+ created agent.
773
+ """
774
+ logger.info(
775
+ f"Ensuring backend agent presence: Name='{name}', Type='{agent_type.value}', OrgID='{self.organization_id}' (UUID)"
776
+ )
777
+
778
+ existing_agent = self._find_existing_agent(name=name)
779
+
780
+ if existing_agent:
781
+ needs_update = False
782
+ patch_kwargs: Dict[str, Any] = {}
783
+
784
+ # Check metadata
785
+ current_metadata = (
786
+ existing_agent.metadata
787
+ if isinstance(existing_agent.metadata, dict)
788
+ else {}
789
+ )
790
+ metadata_to_patch = {}
791
+
792
+ for key, value_for_backend in metadata_for_backend.items():
793
+ if current_metadata.get(key) != value_for_backend:
794
+ metadata_to_patch[key] = value_for_backend
795
+ needs_update = True
796
+
797
+ if metadata_to_patch:
798
+ final_metadata = current_metadata.copy()
799
+ final_metadata.update(metadata_to_patch)
800
+ patch_kwargs["metadata"] = final_metadata
801
+
802
+ # Check agent_type
803
+ current_type_val = None
804
+ if isinstance(existing_agent.agent_type, AgentTypeEnum):
805
+ current_type_val = existing_agent.agent_type.value
806
+ elif isinstance(existing_agent.agent_type, str):
807
+ current_type_val = existing_agent.agent_type
808
+
809
+ if current_type_val != agent_type.value:
810
+ logger.info(
811
+ f"Backend agent '{name}' exists but type differs. Current: '{current_type_val}', Requested: '{agent_type.value}'. Will update."
812
+ )
813
+ patch_kwargs["agent_type"] = agent_type.value
814
+ needs_update = True
815
+
816
+ if needs_update and update_metadata_if_exists:
817
+ logger.info(
818
+ f"Backend agent '{name}' exists and needs update. Proceeding with update."
819
+ )
820
+ return self._update_agent(agent_id=existing_agent.id, **patch_kwargs)
821
+ else:
822
+ if needs_update and not update_metadata_if_exists:
823
+ logger.info(
824
+ f"Backend agent '{name}' exists and differs, but update_metadata_if_exists is False. Skipping update."
825
+ )
826
+ else:
827
+ logger.info(
828
+ f"Backend agent '{name}' exists and is current or update is skipped."
829
+ )
830
+ return existing_agent
831
+
832
+ description = f"{description_prefix}: {name}"
833
+ return self._create_new_agent(
834
+ name=name,
835
+ agent_type=agent_type,
836
+ endpoint=endpoint_for_backend,
837
+ metadata=metadata_for_backend,
838
+ description=description,
839
+ )
840
+
841
+ def get_agent_instance(self, registration_key: str) -> Optional[Agent]:
842
+ """
843
+ Retrieves a registered agent adapter instance by its registration key.
844
+
845
+ The registration key is typically the backend ID of the agent.
846
+
847
+ Args:
848
+ registration_key: The key (backend ID string) of the registered agent adapter.
849
+
850
+ Returns:
851
+ The `Agent` adapter instance if found, otherwise `None`.
852
+ """
853
+ return self._agent_registry.get(registration_key)
854
+
855
+ def _build_error_response(
856
+ self,
857
+ error_message: str,
858
+ error_category: str,
859
+ status_code: int,
860
+ raw_request: Optional[Dict[str, Any]] = None,
861
+ registration_key: Optional[str] = None,
862
+ ) -> Dict[str, Any]:
863
+ """
864
+ Constructs a standardized error response dictionary for the router.
865
+
866
+ This ensures that router-level errors follow the same format as adapter errors,
867
+ providing consistency across the entire request handling pipeline.
868
+
869
+ Args:
870
+ error_message: The primary error message string.
871
+ error_category: Category/type of error (e.g., "AgentNotFound", "AdapterException").
872
+ status_code: The HTTP status code associated with the error.
873
+ raw_request: The original request data that led to the error.
874
+ registration_key: The registration key of the agent that failed, if applicable.
875
+
876
+ Returns:
877
+ A dictionary representing a standardized error response compatible with adapter responses.
878
+ """
879
+ return {
880
+ "raw_request": raw_request,
881
+ "processed_response": None,
882
+ "generated_text": None,
883
+ "raw_response_status": status_code,
884
+ "raw_response_headers": None,
885
+ "raw_response_body": None,
886
+ "agent_specific_data": None,
887
+ "error_message": error_message,
888
+ "error_category": error_category,
889
+ "agent_id": registration_key,
890
+ "adapter_type": "AgentRouter",
891
+ }
892
+
893
+ def route_request(
894
+ self,
895
+ registration_key: str,
896
+ request_data: Dict[str, Any],
897
+ raise_on_error: bool = False,
898
+ ) -> Dict[str, Any]:
899
+ """
900
+ Routes a request to the appropriate agent adapter and returns its response.
901
+
902
+ This method now follows a consistent error handling pattern: it returns standardized
903
+ error response dictionaries instead of raising exceptions by default. This ensures
904
+ that all code using the router can handle errors uniformly without try/except blocks.
905
+
906
+ Args:
907
+ registration_key: The key (backend ID string) used to register the agent,
908
+ which identifies the target adapter.
909
+ request_data: A dictionary containing the data to be sent to the agent's
910
+ `handle_request` method.
911
+ raise_on_error: If True, raises exceptions for errors (legacy behavior).
912
+ If False (default), returns standardized error response dictionaries.
913
+
914
+ Returns:
915
+ A dictionary containing either:
916
+ - The successful response from the agent adapter, or
917
+ - A standardized error response dictionary with error_message field
918
+
919
+ Raises:
920
+ ValueError: Only if raise_on_error=True and no agent found for registration_key.
921
+ RuntimeError: Only if raise_on_error=True and agent's handle_request fails.
922
+
923
+ Note:
924
+ When raise_on_error=False (default), this method never raises exceptions,
925
+ making it safer to use in pipelines where continuity is important.
926
+ """
927
+ logger.debug(
928
+ f"Routing request for agent key: {registration_key}. Request data keys: {list(request_data.keys())}"
929
+ )
930
+ agent_instance = self.get_agent_instance(registration_key)
931
+
932
+ if not agent_instance:
933
+ error_msg = f"Agent not found for key: {registration_key}"
934
+ logger.error(error_msg)
935
+
936
+ if raise_on_error:
937
+ raise ValueError(error_msg)
938
+
939
+ return self._build_error_response(
940
+ error_message=error_msg,
941
+ error_category="AgentNotFound",
942
+ status_code=404,
943
+ raw_request=request_data,
944
+ registration_key=registration_key,
945
+ )
946
+
947
+ try:
948
+ response = agent_instance.handle_request(request_data)
949
+ logger.debug(
950
+ f"Successfully routed request for agent key: {registration_key}"
951
+ )
952
+ return response
953
+ except Exception as e:
954
+ error_msg = f"Agent {registration_key} failed to handle request: {e}"
955
+ logger.error(
956
+ f"Error handling request for agent {registration_key}: {e}",
957
+ exc_info=True,
958
+ )
959
+
960
+ if raise_on_error:
961
+ raise RuntimeError(error_msg) from e
962
+
963
+ return self._build_error_response(
964
+ error_message=error_msg,
965
+ error_category="AdapterException",
966
+ status_code=500,
967
+ raw_request=request_data,
968
+ registration_key=registration_key,
969
+ )