|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import asyncio |
3 | 4 | from typing import Dict, TYPE_CHECKING |
4 | 5 | from uuid import uuid4 |
5 | 6 |
|
| 7 | +import httpx |
| 8 | +from loguru import logger |
6 | 9 | from pydantic import ValidationError |
7 | 10 | from AgentCrew.modules.a2a.adapters import ( |
8 | 11 | convert_agent_message_to_a2a, |
@@ -130,51 +133,85 @@ async def process_messages( |
130 | 133 | a2a_payload = MessageSendParams( |
131 | 134 | metadata={"id": str(uuid4())}, |
132 | 135 | message=a2a_message, |
133 | | - # acceptedOutputModes can be set here if needed, e.g., based on agent_card.defaultOutputModes |
134 | | - # For now, relying on server defaults or agent's capability. |
135 | 136 | ) |
136 | 137 |
|
137 | 138 | full_response_text = "" |
138 | | - |
139 | | - async for stream_response in self.client.send_message_streaming(a2a_payload): |
140 | | - if isinstance(stream_response.root, JSONRPCErrorResponse): |
141 | | - raise Exception( |
142 | | - f"Remote agent stream error: {stream_response.root.error.code} - {stream_response.root.error.message}" |
143 | | - ) |
144 | | - |
145 | | - if stream_response.root.result: |
146 | | - event = stream_response.root.result |
147 | | - current_content_chunk_text = "" |
148 | | - current_thinking_chunk_text = "" |
149 | | - |
150 | | - if isinstance(event, TaskArtifactUpdateEvent): |
151 | | - self.current_task_id = event.task_id |
152 | | - for part in event.artifact.parts: |
153 | | - if isinstance(part.root, TextPart): |
154 | | - current_content_chunk_text += part.root.text |
155 | | - if current_content_chunk_text: |
156 | | - full_response_text += current_content_chunk_text |
157 | | - yield ( |
158 | | - full_response_text, |
159 | | - current_content_chunk_text, |
160 | | - None, |
| 139 | + max_retries = 3 |
| 140 | + retry_count = 0 |
| 141 | + is_resubscribe = False |
| 142 | + |
| 143 | + while retry_count <= max_retries: |
| 144 | + try: |
| 145 | + if is_resubscribe and self.current_task_id: |
| 146 | + logger.info( |
| 147 | + f"Resubscribing to task {self.current_task_id} " |
| 148 | + f"(attempt {retry_count})" |
| 149 | + ) |
| 150 | + stream = self.client.resubscribe_to_task(self.current_task_id) |
| 151 | + else: |
| 152 | + stream = self.client.send_message_streaming(a2a_payload) |
| 153 | + |
| 154 | + async for stream_response in stream: |
| 155 | + if isinstance(stream_response.root, JSONRPCErrorResponse): |
| 156 | + raise Exception( |
| 157 | + f"Remote agent stream error: " |
| 158 | + f"{stream_response.root.error.code} - " |
| 159 | + f"{stream_response.root.error.message}" |
161 | 160 | ) |
162 | 161 |
|
163 | | - elif isinstance(event, TaskStatusUpdateEvent): |
164 | | - self.current_task_id = event.task_id |
165 | | - if event.status.message and event.status.message.parts: |
166 | | - for part in event.status.message.parts: |
167 | | - if isinstance(part.root, TextPart): |
168 | | - current_content_chunk_text += part.root.text |
169 | | - if current_thinking_chunk_text: |
170 | | - yield ( |
171 | | - full_response_text, |
172 | | - None, |
173 | | - (current_thinking_chunk_text, None), |
174 | | - ) |
175 | | - |
176 | | - # After the loop, the generator stops. If full_response_text is empty, |
177 | | - # it signifies no textual content was streamed as artifacts. |
| 162 | + if stream_response.root.result: |
| 163 | + event = stream_response.root.result |
| 164 | + current_content_chunk_text = "" |
| 165 | + current_thinking_chunk_text = "" |
| 166 | + |
| 167 | + if isinstance(event, TaskArtifactUpdateEvent): |
| 168 | + self.current_task_id = event.task_id |
| 169 | + for part in event.artifact.parts: |
| 170 | + if isinstance(part.root, TextPart): |
| 171 | + current_content_chunk_text += part.root.text |
| 172 | + if current_content_chunk_text: |
| 173 | + full_response_text += current_content_chunk_text |
| 174 | + yield ( |
| 175 | + full_response_text, |
| 176 | + current_content_chunk_text, |
| 177 | + None, |
| 178 | + ) |
| 179 | + |
| 180 | + elif isinstance(event, TaskStatusUpdateEvent): |
| 181 | + self.current_task_id = event.task_id |
| 182 | + if event.status.message and event.status.message.parts: |
| 183 | + for part in event.status.message.parts: |
| 184 | + if isinstance(part.root, TextPart): |
| 185 | + current_content_chunk_text += part.root.text |
| 186 | + if current_thinking_chunk_text: |
| 187 | + yield ( |
| 188 | + full_response_text, |
| 189 | + None, |
| 190 | + (current_thinking_chunk_text, None), |
| 191 | + ) |
| 192 | + |
| 193 | + break |
| 194 | + |
| 195 | + except ( |
| 196 | + httpx.ReadError, |
| 197 | + httpx.RemoteProtocolError, |
| 198 | + httpx.ReadTimeout, |
| 199 | + ConnectionError, |
| 200 | + httpx.ConnectError, |
| 201 | + ) as e: |
| 202 | + retry_count += 1 |
| 203 | + if retry_count > max_retries: |
| 204 | + logger.error( |
| 205 | + f"Failed to reconnect after {max_retries} attempts: {e}" |
| 206 | + ) |
| 207 | + raise |
| 208 | + wait_time = min(2**retry_count, 30) |
| 209 | + logger.warning( |
| 210 | + f"Stream connection lost: {e}. " |
| 211 | + f"Retrying in {wait_time}s (attempt {retry_count}/{max_retries})" |
| 212 | + ) |
| 213 | + await asyncio.sleep(wait_time) |
| 214 | + is_resubscribe = True |
178 | 215 |
|
179 | 216 | def get_process_result(self) -> Tuple: |
180 | 217 | """ |
|
0 commit comments