hackagent 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hackagent/__init__.py +12 -0
- hackagent/agent.py +214 -0
- hackagent/api/__init__.py +1 -0
- hackagent/api/agent/__init__.py +1 -0
- hackagent/api/agent/agent_create.py +347 -0
- hackagent/api/agent/agent_destroy.py +140 -0
- hackagent/api/agent/agent_list.py +242 -0
- hackagent/api/agent/agent_partial_update.py +361 -0
- hackagent/api/agent/agent_retrieve.py +235 -0
- hackagent/api/agent/agent_update.py +361 -0
- hackagent/api/apilogs/__init__.py +1 -0
- hackagent/api/apilogs/apilogs_list.py +170 -0
- hackagent/api/apilogs/apilogs_retrieve.py +162 -0
- hackagent/api/attack/__init__.py +1 -0
- hackagent/api/attack/attack_create.py +275 -0
- hackagent/api/attack/attack_destroy.py +146 -0
- hackagent/api/attack/attack_list.py +254 -0
- hackagent/api/attack/attack_partial_update.py +289 -0
- hackagent/api/attack/attack_retrieve.py +247 -0
- hackagent/api/attack/attack_update.py +289 -0
- hackagent/api/checkout/__init__.py +1 -0
- hackagent/api/checkout/checkout_create.py +225 -0
- hackagent/api/generate/__init__.py +1 -0
- hackagent/api/generate/generate_create.py +253 -0
- hackagent/api/judge/__init__.py +1 -0
- hackagent/api/judge/judge_create.py +253 -0
- hackagent/api/key/__init__.py +1 -0
- hackagent/api/key/key_create.py +179 -0
- hackagent/api/key/key_destroy.py +103 -0
- hackagent/api/key/key_list.py +170 -0
- hackagent/api/key/key_retrieve.py +162 -0
- hackagent/api/organization/__init__.py +1 -0
- hackagent/api/organization/organization_create.py +208 -0
- hackagent/api/organization/organization_destroy.py +104 -0
- hackagent/api/organization/organization_list.py +170 -0
- hackagent/api/organization/organization_me_retrieve.py +126 -0
- hackagent/api/organization/organization_partial_update.py +222 -0
- hackagent/api/organization/organization_retrieve.py +163 -0
- hackagent/api/organization/organization_update.py +222 -0
- hackagent/api/prompt/__init__.py +1 -0
- hackagent/api/prompt/prompt_create.py +171 -0
- hackagent/api/prompt/prompt_destroy.py +104 -0
- hackagent/api/prompt/prompt_list.py +185 -0
- hackagent/api/prompt/prompt_partial_update.py +185 -0
- hackagent/api/prompt/prompt_retrieve.py +163 -0
- hackagent/api/prompt/prompt_update.py +185 -0
- hackagent/api/result/__init__.py +1 -0
- hackagent/api/result/result_create.py +175 -0
- hackagent/api/result/result_destroy.py +106 -0
- hackagent/api/result/result_list.py +249 -0
- hackagent/api/result/result_partial_update.py +193 -0
- hackagent/api/result/result_retrieve.py +167 -0
- hackagent/api/result/result_trace_create.py +177 -0
- hackagent/api/result/result_update.py +189 -0
- hackagent/api/run/__init__.py +1 -0
- hackagent/api/run/run_create.py +187 -0
- hackagent/api/run/run_destroy.py +112 -0
- hackagent/api/run/run_list.py +291 -0
- hackagent/api/run/run_partial_update.py +201 -0
- hackagent/api/run/run_result_create.py +177 -0
- hackagent/api/run/run_retrieve.py +179 -0
- hackagent/api/run/run_run_tests_create.py +187 -0
- hackagent/api/run/run_update.py +201 -0
- hackagent/api/user/__init__.py +1 -0
- hackagent/api/user/user_create.py +212 -0
- hackagent/api/user/user_destroy.py +106 -0
- hackagent/api/user/user_list.py +174 -0
- hackagent/api/user/user_me_retrieve.py +126 -0
- hackagent/api/user/user_me_update.py +196 -0
- hackagent/api/user/user_partial_update.py +226 -0
- hackagent/api/user/user_retrieve.py +167 -0
- hackagent/api/user/user_update.py +226 -0
- hackagent/attacks/AdvPrefix/__init__.py +41 -0
- hackagent/attacks/AdvPrefix/completions.py +416 -0
- hackagent/attacks/AdvPrefix/config.py +259 -0
- hackagent/attacks/AdvPrefix/evaluation.py +745 -0
- hackagent/attacks/AdvPrefix/evaluators.py +564 -0
- hackagent/attacks/AdvPrefix/generate.py +711 -0
- hackagent/attacks/AdvPrefix/utils.py +307 -0
- hackagent/attacks/__init__.py +35 -0
- hackagent/attacks/advprefix.py +507 -0
- hackagent/attacks/base.py +106 -0
- hackagent/attacks/strategies.py +906 -0
- hackagent/cli/__init__.py +19 -0
- hackagent/cli/commands/__init__.py +20 -0
- hackagent/cli/commands/agent.py +100 -0
- hackagent/cli/commands/attack.py +417 -0
- hackagent/cli/commands/config.py +301 -0
- hackagent/cli/commands/results.py +327 -0
- hackagent/cli/config.py +249 -0
- hackagent/cli/main.py +515 -0
- hackagent/cli/tui/__init__.py +31 -0
- hackagent/cli/tui/actions_logger.py +200 -0
- hackagent/cli/tui/app.py +288 -0
- hackagent/cli/tui/base.py +137 -0
- hackagent/cli/tui/logger.py +318 -0
- hackagent/cli/tui/views/__init__.py +33 -0
- hackagent/cli/tui/views/agents.py +488 -0
- hackagent/cli/tui/views/attacks.py +624 -0
- hackagent/cli/tui/views/config.py +244 -0
- hackagent/cli/tui/views/dashboard.py +307 -0
- hackagent/cli/tui/views/results.py +1210 -0
- hackagent/cli/tui/widgets/__init__.py +24 -0
- hackagent/cli/tui/widgets/actions.py +346 -0
- hackagent/cli/tui/widgets/logs.py +435 -0
- hackagent/cli/utils.py +276 -0
- hackagent/client.py +286 -0
- hackagent/errors.py +37 -0
- hackagent/logger.py +83 -0
- hackagent/models/__init__.py +109 -0
- hackagent/models/agent.py +223 -0
- hackagent/models/agent_request.py +129 -0
- hackagent/models/api_token_log.py +184 -0
- hackagent/models/attack.py +154 -0
- hackagent/models/attack_request.py +82 -0
- hackagent/models/checkout_session_request_request.py +76 -0
- hackagent/models/checkout_session_response.py +59 -0
- hackagent/models/choice.py +81 -0
- hackagent/models/choice_message.py +67 -0
- hackagent/models/evaluation_status_enum.py +14 -0
- hackagent/models/generate_error_response.py +59 -0
- hackagent/models/generate_request_request.py +212 -0
- hackagent/models/generate_success_response.py +115 -0
- hackagent/models/generic_error_response.py +70 -0
- hackagent/models/message_request.py +67 -0
- hackagent/models/organization.py +102 -0
- hackagent/models/organization_minimal.py +68 -0
- hackagent/models/organization_request.py +71 -0
- hackagent/models/paginated_agent_list.py +123 -0
- hackagent/models/paginated_api_token_log_list.py +123 -0
- hackagent/models/paginated_attack_list.py +123 -0
- hackagent/models/paginated_organization_list.py +123 -0
- hackagent/models/paginated_prompt_list.py +123 -0
- hackagent/models/paginated_result_list.py +123 -0
- hackagent/models/paginated_run_list.py +123 -0
- hackagent/models/paginated_user_api_key_list.py +123 -0
- hackagent/models/paginated_user_profile_list.py +123 -0
- hackagent/models/patched_agent_request.py +128 -0
- hackagent/models/patched_attack_request.py +92 -0
- hackagent/models/patched_organization_request.py +71 -0
- hackagent/models/patched_prompt_request.py +125 -0
- hackagent/models/patched_result_request.py +237 -0
- hackagent/models/patched_run_request.py +138 -0
- hackagent/models/patched_user_profile_request.py +99 -0
- hackagent/models/prompt.py +220 -0
- hackagent/models/prompt_request.py +126 -0
- hackagent/models/result.py +294 -0
- hackagent/models/result_list_evaluation_status.py +14 -0
- hackagent/models/result_request.py +232 -0
- hackagent/models/run.py +233 -0
- hackagent/models/run_list_status.py +12 -0
- hackagent/models/run_request.py +133 -0
- hackagent/models/status_enum.py +12 -0
- hackagent/models/step_type_enum.py +14 -0
- hackagent/models/trace.py +121 -0
- hackagent/models/trace_request.py +94 -0
- hackagent/models/usage.py +75 -0
- hackagent/models/user_api_key.py +201 -0
- hackagent/models/user_api_key_request.py +73 -0
- hackagent/models/user_profile.py +135 -0
- hackagent/models/user_profile_minimal.py +76 -0
- hackagent/models/user_profile_request.py +99 -0
- hackagent/router/__init__.py +25 -0
- hackagent/router/adapters/__init__.py +20 -0
- hackagent/router/adapters/base.py +63 -0
- hackagent/router/adapters/google_adk.py +671 -0
- hackagent/router/adapters/litellm_adapter.py +524 -0
- hackagent/router/adapters/openai_adapter.py +426 -0
- hackagent/router/router.py +969 -0
- hackagent/router/types.py +54 -0
- hackagent/tracking/__init__.py +42 -0
- hackagent/tracking/context.py +163 -0
- hackagent/tracking/decorators.py +299 -0
- hackagent/tracking/tracker.py +441 -0
- hackagent/types.py +54 -0
- hackagent/utils.py +194 -0
- hackagent/vulnerabilities/__init__.py +13 -0
- hackagent/vulnerabilities/prompts.py +81 -0
- hackagent-0.3.1.dist-info/METADATA +122 -0
- hackagent-0.3.1.dist-info/RECORD +183 -0
- hackagent-0.3.1.dist-info/WHEEL +4 -0
- hackagent-0.3.1.dist-info/entry_points.txt +2 -0
- hackagent-0.3.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,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
|
+
)
|