Skip to content

Commit af552a7

Browse files
committed
a2a protocol depending on the request
1 parent 689cc8b commit af552a7

File tree

8 files changed

+695
-169
lines changed

8 files changed

+695
-169
lines changed

src/a2a/agent/agent_adapters.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ def _initialize_agent(self) -> None:
7171
remote_endpoint = os.getenv("AZURE_AI_AGENT_ENDPOINT") or os.getenv("AZURE_AI_PROJECT_ENDPOINT")
7272

7373
# Try remote first if available
74-
if (remote_endpoint and agent_id and
75-
agent_id.startswith("asst_") and
76-
not agent_id.startswith("asst_local_")):
74+
# Real Foundry agent IDs are not guaranteed to start with "asst_".
75+
# Only treat explicit "asst_local_*" IDs as local simulation.
76+
if (remote_endpoint and agent_id and not agent_id.startswith("asst_local_")):
7777
try:
7878
self._agent_processor = AgentProcessor(
7979
agent_id=agent_id,

src/app/agents/agent_processor.py

Lines changed: 174 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from azure.ai.projects import AIProjectClient # type: ignore
1010
from azure.identity import DefaultAzureCredential # type: ignore
1111
from services.azure_auth import get_default_credential, get_inference_credential # type: ignore
12+
try:
13+
# Preferred runtime client for threads/messages/runs
14+
from azure.ai.agents import AgentsClient # type: ignore
15+
except Exception:
16+
AgentsClient = None # type: ignore
1217
_REMOTE_AVAILABLE = True
1318
except Exception:
1419
_REMOTE_AVAILABLE = False
@@ -113,6 +118,7 @@ def __init__(self, agent_id: str, project_endpoint: str = None):
113118
project_endpoint: Optional project endpoint (reads from env if not provided)
114119
"""
115120
self.agent_id = agent_id
121+
self._runtime_agent_id = agent_id
116122

117123
raw_endpoint = (
118124
project_endpoint
@@ -139,6 +145,142 @@ def __init__(self, agent_id: str, project_endpoint: str = None):
139145

140146
self.project_endpoint = full_project_endpoint
141147
self.client = AIProjectClient(endpoint=self.project_endpoint, credential=get_default_credential())
148+
149+
# Best-effort: resolve the underlying OpenAI-style assistant id (asst_...)
150+
# when the configured id is a friendly/name-based id.
151+
self._runtime_agent_id = self._maybe_resolve_assistant_id(self._runtime_agent_id)
152+
153+
# Some azure-ai-projects builds expose only agent-management operations on .agents.
154+
# In that case, use azure-ai-agents AgentsClient for thread/message/run operations.
155+
self._agents_api = None
156+
try:
157+
if (
158+
hasattr(self.client, "agents")
159+
and hasattr(self.client.agents, "threads")
160+
and hasattr(self.client.agents.threads, "create")
161+
and hasattr(self.client.agents, "messages")
162+
and hasattr(self.client.agents.messages, "create")
163+
and hasattr(self.client.agents, "runs")
164+
and hasattr(self.client.agents.runs, "create_and_process")
165+
):
166+
self._agents_api = self.client.agents
167+
except Exception:
168+
self._agents_api = None
169+
170+
if self._agents_api is None:
171+
if AgentsClient is None:
172+
raise ValueError(
173+
"Remote agent support unavailable: this SDK build doesn't expose threads on AIProjectClient.agents "
174+
"and azure-ai-agents is not installed."
175+
)
176+
# AgentsClient expects the project endpoint (per Microsoft docs snippets).
177+
self._agents_api = AgentsClient(endpoint=self.project_endpoint, credential=get_default_credential())
178+
179+
def _maybe_resolve_assistant_id(self, configured_id: str) -> str:
180+
if not configured_id:
181+
return configured_id
182+
# Local simulation stays untouched.
183+
if configured_id.startswith("asst_local_"):
184+
return configured_id
185+
# Already an assistant id.
186+
if configured_id.startswith("asst"):
187+
return configured_id
188+
189+
try:
190+
agents = getattr(self.client, "agents", None)
191+
if not agents or not hasattr(agents, "list"):
192+
return configured_id
193+
for agent in agents.list():
194+
agent_id = getattr(agent, "id", None)
195+
agent_name = getattr(agent, "name", None)
196+
if configured_id not in {agent_id, agent_name}:
197+
continue
198+
for attr in (
199+
"assistant_id",
200+
"assistantId",
201+
"openai_assistant_id",
202+
"openaiAssistantId",
203+
"assistantID",
204+
):
205+
value = getattr(agent, attr, None)
206+
if isinstance(value, str) and value.startswith("asst"):
207+
return value
208+
# Some SDKs only populate `id` with an assistant id.
209+
if isinstance(agent_id, str) and agent_id.startswith("asst"):
210+
return agent_id
211+
except Exception:
212+
# Best-effort only; keep configured value.
213+
return configured_id
214+
215+
return configured_id
216+
217+
@staticmethod
218+
def _get_obj_id(obj: Any) -> str | None:
219+
if obj is None:
220+
return None
221+
# SDK models can be rich objects or MutableMapping
222+
if hasattr(obj, "id"):
223+
return getattr(obj, "id")
224+
if isinstance(obj, dict):
225+
return obj.get("id")
226+
return None
227+
228+
def _create_thread(self):
229+
agents = self._agents_api
230+
if hasattr(agents, "threads") and hasattr(agents.threads, "create"):
231+
return agents.threads.create()
232+
if hasattr(agents, "create_thread"):
233+
return agents.create_thread()
234+
raise AttributeError("No supported thread creation method on agents client")
235+
236+
def _delete_thread(self, thread_id: str) -> None:
237+
agents = self._agents_api
238+
if hasattr(agents, "threads") and hasattr(agents.threads, "delete"):
239+
agents.threads.delete(thread_id)
240+
return
241+
if hasattr(agents, "delete_thread"):
242+
agents.delete_thread(thread_id)
243+
return
244+
245+
def _create_message(self, thread_id: str, role: str, content: str) -> None:
246+
agents = self._agents_api
247+
if hasattr(agents, "messages") and hasattr(agents.messages, "create"):
248+
agents.messages.create(thread_id=thread_id, role=role, content=content)
249+
return
250+
if hasattr(agents, "create_message"):
251+
agents.create_message(thread_id=thread_id, role=role, content=content)
252+
return
253+
raise AttributeError("No supported message creation method on agents client")
254+
255+
def _run_and_process(self, thread_id: str):
256+
agents = self._agents_api
257+
runtime_id = self._runtime_agent_id
258+
# Preferred (azure-ai-agents style)
259+
if hasattr(agents, "runs") and hasattr(agents.runs, "create_and_process"):
260+
# Different SDK builds use either `agent_id` or `assistant_id`.
261+
try:
262+
return agents.runs.create_and_process(thread_id=thread_id, agent_id=runtime_id)
263+
except TypeError:
264+
return agents.runs.create_and_process(thread_id=thread_id, assistant_id=runtime_id)
265+
# Older helper naming
266+
if hasattr(agents, "create_and_process_run"):
267+
# This helper is typically OpenAI-assistants shaped.
268+
return agents.create_and_process_run(thread_id=thread_id, assistant_id=runtime_id)
269+
# Some clients expose a one-shot convenience
270+
if hasattr(agents, "create_thread_and_process_run"):
271+
try:
272+
return agents.create_thread_and_process_run(agent_id=runtime_id)
273+
except TypeError:
274+
return agents.create_thread_and_process_run(assistant_id=runtime_id)
275+
raise AttributeError("No supported run method on agents client")
276+
277+
def _list_messages(self, thread_id: str):
278+
agents = self._agents_api
279+
if hasattr(agents, "messages") and hasattr(agents.messages, "list"):
280+
return agents.messages.list(thread_id=thread_id)
281+
if hasattr(agents, "list_messages"):
282+
return agents.list_messages(thread_id=thread_id)
283+
raise AttributeError("No supported message listing method on agents client")
142284

143285
def run_conversation_with_text_stream(
144286
self,
@@ -157,40 +299,56 @@ def run_conversation_with_text_stream(
157299
Yields:
158300
Chunks of the agent's response
159301
"""
302+
thread_id: str | None = None
160303
try:
161304
# Create a thread for this conversation
162-
thread = self.client.agents.create_thread()
305+
thread = self._create_thread()
306+
thread_id = self._get_obj_id(thread)
307+
if not thread_id:
308+
raise RuntimeError("Agent thread creation returned no id")
163309

164310
# Build the message content
165311
message_content = user_message
166312
if additional_context:
167313
message_content = f"Context: {json.dumps(additional_context)}\n\nUser: {user_message}"
168314

169315
# Add message to thread
170-
self.client.agents.create_message(
171-
thread_id=thread.id,
172-
role="user",
173-
content=message_content
174-
)
316+
self._create_message(thread_id=thread_id, role="user", content=message_content)
175317

176318
# Run the agent
177-
run = self.client.agents.create_and_process_run(
178-
thread_id=thread.id,
179-
assistant_id=self.agent_id
180-
)
319+
self._run_and_process(thread_id=thread_id)
181320

182321
# Get messages
183-
messages = self.client.agents.list_messages(thread_id=thread.id)
322+
messages = self._list_messages(thread_id=thread_id)
184323

185324
# Find the assistant's response
186325
for message in messages:
187326
if message.role == "assistant":
188-
for content in message.content:
189-
if hasattr(content, 'text'):
327+
# Message content can be a list of blocks or a mapping
328+
contents = getattr(message, "content", None)
329+
if isinstance(message, dict) and contents is None:
330+
contents = message.get("content")
331+
if not contents:
332+
continue
333+
for content in contents:
334+
# SDK content blocks commonly expose .text.value
335+
if hasattr(content, "text") and hasattr(content.text, "value"):
190336
yield content.text.value
191-
192-
# Clean up
193-
self.client.agents.delete_thread(thread.id)
337+
elif isinstance(content, dict):
338+
text = content.get("text")
339+
if isinstance(text, dict) and isinstance(text.get("value"), str):
340+
yield text["value"]
341+
elif isinstance(text, str):
342+
yield text
343+
elif isinstance(content, str):
344+
yield content
194345

195346
except Exception as e:
196347
yield f"Error communicating with agent: {str(e)}"
348+
finally:
349+
if thread_id:
350+
try:
351+
self._delete_thread(thread_id)
352+
except Exception:
353+
# Best-effort cleanup; ignore failures
354+
pass

0 commit comments

Comments
 (0)