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,753 @@
1
+ import logging
2
+ from typing import Any, Dict, Type, Optional, Union
3
+ from uuid import UUID
4
+
5
+ from hackagent.router.base import Agent
6
+ from hackagent.router.adapters import ADKAgentAdapter
7
+ from hackagent.router.adapters.litellm_adapter import LiteLLMAgentAdapter
8
+ from hackagent.client import AuthenticatedClient
9
+ from hackagent.models import (
10
+ AgentTypeEnum,
11
+ Agent as BackendAgentModel,
12
+ AgentRequest,
13
+ PatchedAgentRequest,
14
+ UserAPIKey,
15
+ )
16
+ from ..types import Unset, UNSET
17
+ from hackagent.api.agent import agent_list, agent_create, agent_partial_update
18
+ from hackagent.api.key import key_list
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # --- Agent Type to Adapter Mapping ---
23
+ AGENT_TYPE_TO_ADAPTER_MAP: Dict[AgentTypeEnum, Type[Agent]] = {
24
+ AgentTypeEnum.GOOGLE_ADK: ADKAgentAdapter,
25
+ AgentTypeEnum.LITELMM: LiteLLMAgentAdapter,
26
+ # AgentTypeEnum.OPENAI: OpenAIAgentAdapter, # Example for future
27
+ # Add other agent types and their corresponding adapters here
28
+ }
29
+
30
+
31
+ class AgentRouter:
32
+ """
33
+ Manages a single agent's configuration and routes requests to its adapter.
34
+
35
+ The router is initialized with the details of an agent, registers it with the
36
+ backend (if not already present or if metadata needs an update), and instantiates
37
+ the appropriate adapter. It then uses this adapter for request routing.
38
+ """
39
+
40
+ def _fetch_organization_id(self) -> UUID:
41
+ """Fetches and returns the organization ID (UUID) associated with the API key.
42
+ Raises RuntimeError if not found or if the organization attribute is not a UUID.
43
+ """
44
+ try:
45
+ logger.debug(
46
+ "AgentRouter: Attempting to retrieve Organization ID by listing API keys..."
47
+ )
48
+ keys_response = key_list.sync_detailed(client=self.client)
49
+
50
+ if (
51
+ keys_response.status_code == 200
52
+ and keys_response.parsed
53
+ and keys_response.parsed.results
54
+ ):
55
+ current_token = self.client.token
56
+ key_results: list[UserAPIKey] = keys_response.parsed.results
57
+ for key_obj in key_results:
58
+ if current_token.startswith(key_obj.prefix):
59
+ if hasattr(key_obj, "organization") and isinstance(
60
+ key_obj.organization, UUID
61
+ ):
62
+ logger.info(
63
+ f"AgentRouter: Successfully determined Organization ID: {key_obj.organization} from key prefix '{key_obj.prefix}'."
64
+ )
65
+ return key_obj.organization
66
+ else:
67
+ org_type = (
68
+ type(key_obj.organization).__name__
69
+ if hasattr(key_obj, "organization")
70
+ else "Missing"
71
+ )
72
+ logger.warning(
73
+ f"AgentRouter: Key prefix '{key_obj.prefix}' matched, but 'organization' is not UUID (type: {org_type}). Skipping for Org ID."
74
+ )
75
+ logger.error(
76
+ f"AgentRouter: No API key found with a valid Organization (UUID) for token prefix '{current_token[:8]}...'."
77
+ )
78
+ raise RuntimeError(
79
+ "AgentRouter: Could not determine Organization ID (UUID) from API keys."
80
+ )
81
+ elif keys_response.parsed and not keys_response.parsed.results:
82
+ logger.error(
83
+ "AgentRouter: API key list empty. Cannot find Organization ID."
84
+ )
85
+ raise RuntimeError(
86
+ "AgentRouter: API key list empty for Organization ID retrieval."
87
+ )
88
+ else:
89
+ content = (
90
+ keys_response.content.decode() if keys_response.content else "N/A"
91
+ )
92
+ logger.error(
93
+ f"AgentRouter: Failed to list keys for Org ID. Status: {keys_response.status_code}, Body: {content}"
94
+ )
95
+ raise RuntimeError(
96
+ f"AgentRouter: API key list failed for Organization ID (status {keys_response.status_code})."
97
+ )
98
+ except RuntimeError:
99
+ raise
100
+ except Exception as e:
101
+ logger.error(
102
+ f"AgentRouter: Exception fetching Organization ID: {e}", exc_info=True
103
+ )
104
+ raise RuntimeError(f"AgentRouter: Exception fetching Organization ID: {e}")
105
+
106
+ def _fetch_user_id_str(self) -> str:
107
+ """Fetches and returns the user ID (as a string from UserAPIKey.user)
108
+ associated with the API key. Raises RuntimeError if not found or user attribute is not an int.
109
+ """
110
+ try:
111
+ logger.debug(
112
+ "AgentRouter: Attempting to retrieve User ID by listing API keys..."
113
+ )
114
+ keys_response = key_list.sync_detailed(client=self.client)
115
+
116
+ if (
117
+ keys_response.status_code == 200
118
+ and keys_response.parsed
119
+ and keys_response.parsed.results
120
+ ):
121
+ current_token = self.client.token
122
+ key_results: list[UserAPIKey] = keys_response.parsed.results
123
+ for key_obj in key_results:
124
+ if current_token.startswith(key_obj.prefix):
125
+ if hasattr(key_obj, "user") and isinstance(key_obj.user, int):
126
+ user_id_as_str = str(key_obj.user)
127
+ logger.info(
128
+ f"AgentRouter: Successfully determined User ID (str): {user_id_as_str} from key prefix '{key_obj.prefix}'."
129
+ )
130
+ return user_id_as_str
131
+ else:
132
+ user_type = (
133
+ type(key_obj.user).__name__
134
+ if hasattr(key_obj, "user")
135
+ else "Missing"
136
+ )
137
+ logger.warning(
138
+ f"AgentRouter: Key prefix '{key_obj.prefix}' matched, but 'user' is not int (type: {user_type}). Skipping for User ID."
139
+ )
140
+ logger.error(
141
+ f"AgentRouter: No API key found with a valid User (int) for token prefix '{current_token[:8]}...'."
142
+ )
143
+ raise RuntimeError(
144
+ "AgentRouter: Could not determine User ID (int) from API keys."
145
+ )
146
+ elif keys_response.parsed and not keys_response.parsed.results:
147
+ logger.error("AgentRouter: API key list empty. Cannot find User ID.")
148
+ raise RuntimeError(
149
+ "AgentRouter: API key list empty for User ID retrieval."
150
+ )
151
+ else:
152
+ content = (
153
+ keys_response.content.decode() if keys_response.content else "N/A"
154
+ )
155
+ logger.error(
156
+ f"AgentRouter: Failed to list keys for User ID. Status: {keys_response.status_code}, Body: {content}"
157
+ )
158
+ raise RuntimeError(
159
+ f"AgentRouter: API key list failed for User ID (status {keys_response.status_code})."
160
+ )
161
+ except RuntimeError:
162
+ raise
163
+ except Exception as e:
164
+ logger.error(f"AgentRouter: Exception fetching User ID: {e}", exc_info=True)
165
+ raise RuntimeError(f"AgentRouter: Exception fetching User ID: {e}")
166
+
167
+ def __init__(
168
+ self,
169
+ client: AuthenticatedClient,
170
+ name: str,
171
+ agent_type: AgentTypeEnum,
172
+ endpoint: str,
173
+ metadata: Optional[Dict[str, Any]] = None,
174
+ adapter_operational_config: Optional[Dict[str, Any]] = None,
175
+ overwrite_metadata: bool = True, # Controls if backend agent metadata is updated if agent exists
176
+ ):
177
+ """
178
+ Initializes the AgentRouter and registers a single agent.
179
+
180
+ Ensures the specified agent exists in the backend (creating or updating as needed),
181
+ then instantiates and stores its adapter in the router's registry.
182
+
183
+ Args:
184
+ client: Authenticated client for backend API interaction.
185
+ name: Name for the agent in the backend.
186
+ agent_type: The AgentTypeEnum for the agent (e.g., AgentTypeEnum.GOOGLE_ADK).
187
+ endpoint: API endpoint URL for the agent service itself (used for backend registration
188
+ and potentially by the adapter if not overridden by backend_agent.endpoint).
189
+ metadata: Metadata for the backend agent record.
190
+ For ADK, adk_app_name is no longer explicitly managed here if it's same as agent name.
191
+ For LiteLLM, SHOULD include {'name': 'model_name',
192
+ 'endpoint': 'endpoint',
193
+ 'api_key': 'optional_env_var_for_api_key', ...}
194
+ adapter_operational_config: Runtime config for the adapter instance.
195
+ Overrides or augments values from backend_agent.metadata.
196
+ For ADK, may include {'user_id': ..., 'session_id': ...}.
197
+ For LiteLLM, MUST provide 'name' (model string) if not in backend metadata.
198
+ overwrite_metadata: If True, and an agent exists, its backend metadata is updated.
199
+
200
+ Raises:
201
+ ValueError: If agent_type is unsupported or adapter instantiation fails.
202
+ RuntimeError: If backend communication or agent processing fails.
203
+ """
204
+ self.client = client
205
+ self._agent_registry: Dict[str, Agent] = {}
206
+
207
+ self.organization_id = self._fetch_organization_id()
208
+ self.user_id_str = self._fetch_user_id_str()
209
+ logger.info(
210
+ f"AgentRouter initialized with Organization ID (UUID): {self.organization_id} and User ID (str): {self.user_id_str}"
211
+ )
212
+
213
+ if agent_type not in AGENT_TYPE_TO_ADAPTER_MAP:
214
+ raise ValueError(
215
+ f"Unsupported agent type: {agent_type}. "
216
+ f"Supported types: {list(AGENT_TYPE_TO_ADAPTER_MAP.keys())}"
217
+ )
218
+
219
+ actual_metadata = metadata.copy() if metadata is not None else {}
220
+
221
+ # adapter_operational_config is passed in, merge with any defaults we set here
222
+ current_adapter_op_config = (
223
+ adapter_operational_config.copy() if adapter_operational_config else {}
224
+ )
225
+
226
+ if agent_type == AgentTypeEnum.GOOGLE_ADK:
227
+ # Ensure user_id is in the op_config for ADK, using the one fetched from API key
228
+ if "user_id" not in current_adapter_op_config:
229
+ current_adapter_op_config["user_id"] = self.user_id_str
230
+ logger.info(
231
+ f"ADK Agent: Using fetched User ID '{self.user_id_str}' for adapter operational config."
232
+ )
233
+ else:
234
+ logger.warning(
235
+ 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."
236
+ )
237
+ # session_id will be handled later, as it depends on run_id
238
+
239
+ self.backend_agent = self.ensure_agent_in_backend(
240
+ name=name,
241
+ agent_type=agent_type,
242
+ endpoint_for_backend=endpoint,
243
+ metadata_for_backend=actual_metadata,
244
+ update_metadata_if_exists=overwrite_metadata,
245
+ )
246
+
247
+ registration_key = str(self.backend_agent.id)
248
+
249
+ self._configure_and_instantiate_adapter(
250
+ name=name,
251
+ agent_type=agent_type,
252
+ registration_key=registration_key,
253
+ adapter_operational_config=current_adapter_op_config,
254
+ )
255
+
256
+ def _configure_and_instantiate_adapter(
257
+ self,
258
+ name: str,
259
+ agent_type: AgentTypeEnum,
260
+ registration_key: str,
261
+ adapter_operational_config: Optional[Dict[str, Any]],
262
+ ) -> None:
263
+ """
264
+ Configures and instantiates the appropriate agent adapter based on agent_type
265
+ and stores it in the router's registry.
266
+ """
267
+ adapter_class = AGENT_TYPE_TO_ADAPTER_MAP[
268
+ agent_type
269
+ ] # agent_type already validated in __init__
270
+
271
+ # Start with the operational config passed in
272
+ adapter_instance_config = (
273
+ adapter_operational_config.copy() if adapter_operational_config else {}
274
+ )
275
+
276
+ # Type-specific adapter configuration
277
+ if agent_type == AgentTypeEnum.GOOGLE_ADK:
278
+ adapter_instance_config["name"] = self.backend_agent.name
279
+ adapter_instance_config["endpoint"] = self.backend_agent.endpoint
280
+ if "user_id" not in adapter_instance_config:
281
+ logger.error(
282
+ 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__."
283
+ )
284
+ # Fallback, though this indicates a logic flaw if reached.
285
+ adapter_instance_config["user_id"] = self.user_id_str
286
+
287
+ elif agent_type == AgentTypeEnum.LITELMM:
288
+ if (
289
+ "name" not in adapter_instance_config
290
+ ): # 'name' is the model string for LiteLLM
291
+ if (
292
+ isinstance(self.backend_agent.metadata, dict)
293
+ and "name" in self.backend_agent.metadata
294
+ ):
295
+ adapter_instance_config["name"] = self.backend_agent.metadata[
296
+ "name"
297
+ ]
298
+ else:
299
+ raise ValueError(
300
+ f"LiteLLM agent '{name}' (ID: {registration_key}) missing "
301
+ f"'name' (model string) in adapter_operational_config or backend metadata. "
302
+ f"Cannot configure LiteLLMAgentAdapter."
303
+ )
304
+
305
+ # Copy other relevant LiteLLM settings from backend_agent.metadata if not already in adapter_instance_config
306
+ optional_litellm_keys = [
307
+ "endpoint",
308
+ "api_key",
309
+ "max_new_tokens",
310
+ "temperature",
311
+ "top_p",
312
+ ]
313
+ if isinstance(self.backend_agent.metadata, dict):
314
+ for key in optional_litellm_keys:
315
+ if (
316
+ key not in adapter_instance_config
317
+ and key in self.backend_agent.metadata
318
+ ):
319
+ adapter_instance_config[key] = self.backend_agent.metadata[key]
320
+
321
+ # Instantiate and register the adapter
322
+ try:
323
+ adapter_instance = adapter_class(
324
+ id=registration_key, config=adapter_instance_config
325
+ )
326
+ self._agent_registry[registration_key] = adapter_instance
327
+ logger.info(
328
+ f"Agent '{name}' (Backend ID: {registration_key}, Type: {agent_type.value}) "
329
+ f"successfully initialized and registered with adapter {adapter_class.__name__}. "
330
+ f"Adapter config keys: {list(adapter_instance_config.keys())}" # Log keys for debug
331
+ )
332
+ except Exception as e:
333
+ logger.error(
334
+ f"Failed to instantiate adapter for agent '{name}' "
335
+ f"(Backend ID: {registration_key}): {e}",
336
+ exc_info=True,
337
+ )
338
+ raise ValueError(
339
+ f"Failed to instantiate adapter {adapter_class.__name__}: {e}"
340
+ ) from e
341
+
342
+ def _find_existing_agent(
343
+ self,
344
+ name: str,
345
+ agent_type: AgentTypeEnum,
346
+ ) -> Optional[BackendAgentModel]:
347
+ """
348
+ Finds an existing agent by name, type, and organization in the backend.
349
+ Uses self.organization_id (UUID) for matching.
350
+ """
351
+ logger.debug(
352
+ f"SYNC_DEBUG: Entered _find_existing_agent for Name='{name}', Type='{agent_type.value}', OrgID='{self.organization_id}' (UUID)"
353
+ )
354
+
355
+ current_page: Union[Unset, int] = UNSET
356
+ agents_processed_count = 0
357
+
358
+ while True:
359
+ list_response = None
360
+ try:
361
+ list_response = agent_list.sync_detailed(
362
+ client=self.client, page=current_page
363
+ )
364
+ logger.debug(
365
+ f"SYNC_DEBUG: Fetched page of agents. Status: {list_response.status_code if list_response else 'N/A'}"
366
+ )
367
+
368
+ except Exception as e:
369
+ logger.error(
370
+ 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}",
371
+ exc_info=True,
372
+ )
373
+ return None # Or handle error more gracefully
374
+
375
+ if (
376
+ list_response
377
+ and list_response.status_code == 200
378
+ and list_response.parsed
379
+ ):
380
+ paginated_result = list_response.parsed
381
+ results_list = getattr(paginated_result, "results", [])
382
+ logger.debug(
383
+ 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}"
384
+ )
385
+ if not isinstance(results_list, list):
386
+ logger.warning(
387
+ f"SYNC_DEBUG: Expected 'results' to be a list, but got {type(results_list)}. Full parsed response: {paginated_result}"
388
+ )
389
+ results_list = []
390
+
391
+ for agent_model in results_list:
392
+ agents_processed_count += 1
393
+ logger.debug(
394
+ 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')}"
395
+ )
396
+
397
+ name_matches = (
398
+ hasattr(agent_model, "name") and agent_model.name == name
399
+ )
400
+
401
+ org_matches = False
402
+ # agent_model.organization is UUID as per hackagent.models.Agent
403
+ if hasattr(agent_model, "organization") and isinstance(
404
+ agent_model.organization, UUID
405
+ ):
406
+ if agent_model.organization == self.organization_id:
407
+ org_matches = True
408
+ # else: # No need for else here, org_matches remains false
409
+ # logger.debug(f"SYNC_DEBUG: OrgID (UUID) mismatch: agent_model.organization ('{agent_model.organization}') != expected self.organization_id ('{self.organization_id}') for agent '{agent_model.name}'")
410
+ # Check organization_detail.id as a fallback, though agent_model.organization should be primary
411
+ elif (
412
+ hasattr(agent_model, "organization_detail")
413
+ and hasattr(agent_model.organization_detail, "id")
414
+ and isinstance(agent_model.organization_detail.id, UUID)
415
+ ):
416
+ if agent_model.organization_detail.id == self.organization_id:
417
+ org_matches = True
418
+ logger.debug(
419
+ f"SYNC_DEBUG: Matched OrgID via organization_detail.id for agent '{agent_model.name}'"
420
+ )
421
+ # else:
422
+ # logger.debug(f"SYNC_DEBUG: OrgID (UUID) mismatch via organization_detail.id: ('{agent_model.organization_detail.id}') != expected self.organization_id ('{self.organization_id}') for agent '{agent_model.name}'")
423
+ # The case where agent_model.organization is an int should not happen if model is correct, but good to log if it does.
424
+ elif hasattr(agent_model, "organization") and isinstance(
425
+ agent_model.organization, int
426
+ ):
427
+ logger.warning(
428
+ 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}')."
429
+ )
430
+ # else: # Log if no organization attribute could be reliably checked
431
+ # logger.debug(f"SYNC_DEBUG: Could not determine organization ID for comparison for agent '{agent_model.name}'. Expected UUID: {self.organization_id}")
432
+
433
+ type_matches = False
434
+ # The `agent_model` from the list might have `agent_type` (as per model def) or just `type`.
435
+ # The `type` attribute from `BackendAgentModel` (aliased as `Agent`) is `agent_type` in its definition.
436
+ # `AgentTypeEnum` is what `agent_type` (parameter) is.
437
+ current_agent_type_val = None
438
+ if (
439
+ hasattr(agent_model, "agent_type")
440
+ and agent_model.agent_type is not None
441
+ and not isinstance(agent_model.agent_type, Unset)
442
+ ):
443
+ if isinstance(agent_model.agent_type, AgentTypeEnum):
444
+ current_agent_type_val = agent_model.agent_type.value
445
+ elif isinstance(
446
+ agent_model.agent_type, str
447
+ ): # If it's already a string
448
+ current_agent_type_val = agent_model.agent_type
449
+ elif (
450
+ hasattr(agent_model, "type") and agent_model.type is not None
451
+ ): # Fallback for older/different field name
452
+ if isinstance(agent_model.type, AgentTypeEnum):
453
+ current_agent_type_val = agent_model.type.value
454
+ elif isinstance(agent_model.type, str):
455
+ current_agent_type_val = agent_model.type
456
+
457
+ if (
458
+ current_agent_type_val is not None
459
+ and current_agent_type_val == agent_type.value
460
+ ):
461
+ type_matches = True
462
+
463
+ if not name_matches:
464
+ logger.debug(
465
+ f"SYNC_DEBUG: Agent ID '{agent_model.id}' ('{agent_model.name}') failed name match (expected '{name}')"
466
+ )
467
+ elif not org_matches:
468
+ logger.debug(
469
+ 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'}')"
470
+ )
471
+ elif not type_matches:
472
+ logger.debug(
473
+ f"SYNC_DEBUG: Agent ID '{agent_model.id}' ('{agent_model.name}') failed type match (expected '{agent_type.value}', found '{current_agent_type_val}')"
474
+ )
475
+
476
+ if name_matches and org_matches and type_matches:
477
+ logger.info(
478
+ f"SYNC_DEBUG: Found existing backend agent '{name}' (Type: {agent_type.value}, OrgID: {self.organization_id}) "
479
+ 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."
480
+ )
481
+ return agent_model
482
+
483
+ if (
484
+ hasattr(paginated_result, "next_")
485
+ and paginated_result.next_
486
+ and not isinstance(paginated_result.next_, Unset)
487
+ ):
488
+ next_page_url = paginated_result.next_
489
+ # Extract page number if it's a full URL. This is a bit simplistic.
490
+ # A more robust way would be to parse URL params if the API returns full URLs for next.
491
+ # If the API just returns the next page number, this is simpler.
492
+ # Assuming API might return simple page numbers or full URLs with ?page=NUMBER
493
+ try:
494
+ if isinstance(next_page_url, str) and "page=" in next_page_url:
495
+ current_page = int(
496
+ next_page_url.split("page=")[-1].split("&")[0]
497
+ )
498
+ elif isinstance(
499
+ next_page_url, int
500
+ ): # If API directly gives next page number
501
+ current_page = next_page_url
502
+ else: # Fallback for simple increment if only a URL string is given without obvious page number
503
+ current_page = (
504
+ current_page if isinstance(current_page, int) else 1
505
+ ) + 1
506
+ except ValueError:
507
+ logger.warning(
508
+ f"Could not parse next page number from URL: {next_page_url}. Using simple increment."
509
+ )
510
+ current_page = (
511
+ current_page if isinstance(current_page, int) else 1
512
+ ) + 1
513
+
514
+ logger.debug(
515
+ f"SYNC_DEBUG: Moving to next page of agents: {current_page}"
516
+ )
517
+ else:
518
+ logger.debug(
519
+ f"SYNC_DEBUG: No more pages of agents to fetch. Processed {agents_processed_count} agents in total."
520
+ )
521
+ break
522
+
523
+ elif list_response and list_response.status_code != 200:
524
+ logger.error(
525
+ f"SYNC_DEBUG: Failed to list agents on page {current_page if not isinstance(current_page, Unset) else 'initial'}. Status: {list_response.status_code}, "
526
+ f"Body: {list_response.content.decode() if list_response.content else 'N/A'}"
527
+ )
528
+ return None
529
+ elif not list_response:
530
+ logger.error(
531
+ f"SYNC_DEBUG: Failed to get any response from agents_list API for page {current_page if not isinstance(current_page, Unset) else 'initial'}."
532
+ )
533
+ return None
534
+ else:
535
+ logger.warning(
536
+ f"SYNC_DEBUG: Unexpected state after trying to fetch page {current_page if not isinstance(current_page, Unset) else 'initial'}. list_response: {list_response}"
537
+ )
538
+ return None
539
+
540
+ logger.debug(
541
+ f"SYNC_DEBUG: No existing backend agent found matching Name='{name}', Type='{agent_type.value}', OrgID='{self.organization_id}' after searching all pages."
542
+ )
543
+ return None
544
+
545
+ def _update_agent_metadata(
546
+ self, agent_id: UUID, metadata_to_update: Dict[str, Any]
547
+ ) -> BackendAgentModel:
548
+ """Updates the metadata of an existing backend agent."""
549
+ logger.info(f"Attempting to update metadata for backend agent ID: {agent_id}")
550
+ patch_body = PatchedAgentRequest(metadata=metadata_to_update)
551
+ try:
552
+ update_response = agent_partial_update.sync_detailed(
553
+ client=self.client, id=agent_id, body=patch_body
554
+ )
555
+ if update_response.status_code == 200 and update_response.parsed:
556
+ logger.info(
557
+ f"Successfully updated metadata for backend agent ID: {agent_id}."
558
+ )
559
+ return update_response.parsed
560
+ else:
561
+ err_msg = (
562
+ f"Failed to update metadata for backend agent ID: {agent_id}. "
563
+ f"Status: {update_response.status_code}, "
564
+ f"Body: {update_response.content.decode() if update_response.content else 'N/A'}"
565
+ )
566
+ logger.error(err_msg)
567
+ raise RuntimeError(err_msg)
568
+ except Exception as e:
569
+ err_msg_ex = f"Exception during backend agent metadata update for ID: {agent_id}: {e}"
570
+ logger.error(err_msg_ex, exc_info=True)
571
+ raise RuntimeError(err_msg_ex) from e
572
+
573
+ def _create_new_agent(
574
+ self,
575
+ name: str,
576
+ agent_type: AgentTypeEnum,
577
+ endpoint: str,
578
+ metadata: Dict[str, Any],
579
+ description: str,
580
+ ) -> BackendAgentModel:
581
+ """Creates a new agent in the backend."""
582
+ logger.info(
583
+ f"Creating new backend agent: Name='{name}', Type='{agent_type.value}', OrgID='{self.organization_id}' (UUID)"
584
+ )
585
+
586
+ # IMPORTANT: AgentRequest.organization might expect an int or string representation of UUID.
587
+ # If AgentRequest model expects an int, str(self.organization_id) or another conversion will be needed,
588
+ # or the AgentRequest model itself needs to be updated to accept UUID.
589
+ # For now, passing the UUID directly. This might require AgentRequest model adjustment.
590
+ agent_req_body = AgentRequest(
591
+ name=name,
592
+ endpoint=endpoint,
593
+ agent_type=agent_type,
594
+ metadata=metadata,
595
+ description=description,
596
+ organization=self.organization_id, # Passing UUID here.
597
+ )
598
+
599
+ try:
600
+ create_response = agent_create.sync_detailed(
601
+ client=self.client, body=agent_req_body
602
+ )
603
+ if create_response.status_code == 201 and create_response.parsed:
604
+ logger.info(
605
+ f"Created backend agent '{name}' (Type: {agent_type.value}) "
606
+ f"with ID {create_response.parsed.id}."
607
+ )
608
+ return create_response.parsed
609
+ else:
610
+ body_content = (
611
+ create_response.content.decode()
612
+ if create_response.content
613
+ else "N/A"
614
+ )
615
+ err_msg = (
616
+ f"Failed to create backend agent '{name}'. Status: {create_response.status_code}, "
617
+ f"Body: {body_content}"
618
+ )
619
+ logger.error(err_msg)
620
+ raise RuntimeError(err_msg)
621
+ except Exception as e:
622
+ err_msg_ex = (
623
+ f"Exception during backend agent creation for Name='{name}': {e}"
624
+ )
625
+ logger.error(err_msg_ex, exc_info=True)
626
+ raise RuntimeError(err_msg_ex) from e
627
+
628
+ def ensure_agent_in_backend(
629
+ self,
630
+ name: str,
631
+ agent_type: AgentTypeEnum,
632
+ endpoint_for_backend: str,
633
+ metadata_for_backend: Dict[str, Any],
634
+ description_prefix: str = "Agent managed by router",
635
+ update_metadata_if_exists: bool = True,
636
+ ) -> BackendAgentModel:
637
+ """
638
+ Ensures an agent with the given specifications exists in the backend.
639
+ Uses self.organization_id (UUID) from the router instance.
640
+ """
641
+ logger.info(
642
+ f"Ensuring backend agent presence: Name='{name}', Type='{agent_type.value}', OrgID='{self.organization_id}' (UUID)"
643
+ )
644
+
645
+ existing_agent = self._find_existing_agent(name=name, agent_type=agent_type)
646
+
647
+ if existing_agent:
648
+ needs_metadata_update = False
649
+ current_metadata = (
650
+ existing_agent.metadata
651
+ if isinstance(existing_agent.metadata, dict)
652
+ else {}
653
+ )
654
+ metadata_to_patch = {}
655
+
656
+ for key, value_for_backend in metadata_for_backend.items():
657
+ if current_metadata.get(key) != value_for_backend:
658
+ metadata_to_patch[key] = value_for_backend
659
+ needs_metadata_update = True
660
+
661
+ if needs_metadata_update and update_metadata_if_exists:
662
+ logger.info(
663
+ f"Backend agent '{name}' exists and metadata needs update. Proceeding with update."
664
+ )
665
+ final_patch_payload = current_metadata.copy()
666
+ final_patch_payload.update(metadata_to_patch)
667
+
668
+ return self._update_agent_metadata(
669
+ agent_id=existing_agent.id, metadata_to_update=final_patch_payload
670
+ )
671
+ else:
672
+ if needs_metadata_update and not update_metadata_if_exists:
673
+ logger.info(
674
+ f"Backend agent '{name}' exists and metadata differs, but update_metadata_if_exists is False. Skipping update."
675
+ )
676
+ else:
677
+ logger.info(
678
+ f"Backend agent '{name}' exists and metadata is current or update is skipped."
679
+ )
680
+ return existing_agent
681
+
682
+ description = f"{description_prefix}: {name}"
683
+ return self._create_new_agent(
684
+ name=name,
685
+ agent_type=agent_type,
686
+ endpoint=endpoint_for_backend,
687
+ metadata=metadata_for_backend,
688
+ description=description,
689
+ )
690
+
691
+ def get_agent_instance(self, registration_key: str) -> Agent | None:
692
+ """
693
+ Retrieves an instantiated agent adapter from the router's registry.
694
+
695
+ Args:
696
+ registration_key: The backend agent's UUID string.
697
+
698
+ Returns:
699
+ An instance of the agent adapter, or None if not found.
700
+ """
701
+ instance = self._agent_registry.get(registration_key)
702
+ if not instance:
703
+ logger.warning(
704
+ f"No agent adapter found in router registry for key: {registration_key}"
705
+ )
706
+ return instance
707
+
708
+ async def route_request(
709
+ self, registration_key: str, request_data: Dict[str, Any]
710
+ ) -> Dict[str, Any]:
711
+ """
712
+ Routes a request to the specified agent and returns its standardized response.
713
+
714
+ Args:
715
+ registration_key: The backend agent's UUID string.
716
+ request_data: Data for the agent's handle_request method.
717
+
718
+ Returns:
719
+ Agent's response or an error dictionary.
720
+ """
721
+ logger.info(
722
+ f"Routing request for agent with registration key: {registration_key}"
723
+ )
724
+ agent_instance = self.get_agent_instance(registration_key)
725
+
726
+ if not agent_instance:
727
+ logger.error(f"Could not find agent adapter for key: {registration_key}")
728
+ return {
729
+ "error": "AgentNotRegisteredInRouter",
730
+ "message": f"Agent key '{registration_key}' not in router instances.",
731
+ "registration_key": registration_key,
732
+ "status_code": 404,
733
+ }
734
+
735
+ try:
736
+ response = await agent_instance.handle_request(request_data)
737
+ logger.info(
738
+ f"Successfully processed request for agent key '{registration_key}'"
739
+ )
740
+ return response
741
+ except Exception as e:
742
+ logger.error(
743
+ f"Error during request handling by adapter for agent key '{registration_key}': {e}",
744
+ exc_info=True,
745
+ )
746
+ return {
747
+ "error": "RequestHandlingError",
748
+ "message": (
749
+ f"Adapter error for agent key '{registration_key}': {str(e)}"
750
+ ),
751
+ "registration_key": registration_key,
752
+ "status_code": 500,
753
+ }