mcp-use 1.3.11__py3-none-any.whl → 1.3.12__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.
Potentially problematic release.
This version of mcp-use might be problematic. Click here for more details.
- mcp_use/adapters/langchain_adapter.py +3 -48
- mcp_use/agents/mcpagent.py +72 -33
- mcp_use/agents/remote.py +154 -128
- mcp_use/client.py +24 -0
- mcp_use/config.py +5 -0
- mcp_use/connectors/base.py +8 -0
- mcp_use/connectors/http.py +19 -7
- mcp_use/connectors/sandbox.py +12 -3
- mcp_use/connectors/stdio.py +11 -3
- mcp_use/connectors/websocket.py +1 -1
- mcp_use/middleware/__init__.py +50 -0
- mcp_use/middleware/logging.py +31 -0
- mcp_use/middleware/metrics.py +314 -0
- mcp_use/middleware/middleware.py +262 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.12.dist-info}/METADATA +8 -7
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.12.dist-info}/RECORD +19 -15
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.12.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.12.dist-info}/entry_points.txt +0 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,15 +8,12 @@ import re
|
|
|
8
8
|
from typing import Any, NoReturn
|
|
9
9
|
|
|
10
10
|
from jsonschema_pydantic import jsonschema_to_pydantic
|
|
11
|
-
from langchain_core.tools import BaseTool
|
|
11
|
+
from langchain_core.tools import BaseTool
|
|
12
12
|
from mcp.types import (
|
|
13
13
|
CallToolResult,
|
|
14
|
-
EmbeddedResource,
|
|
15
|
-
ImageContent,
|
|
16
14
|
Prompt,
|
|
17
15
|
ReadResourceRequestParams,
|
|
18
16
|
Resource,
|
|
19
|
-
TextContent,
|
|
20
17
|
)
|
|
21
18
|
from pydantic import BaseModel, Field, create_model
|
|
22
19
|
|
|
@@ -60,47 +57,6 @@ class LangChainAdapter(BaseAdapter):
|
|
|
60
57
|
schema[key] = self.fix_schema(value) # Apply recursively
|
|
61
58
|
return schema
|
|
62
59
|
|
|
63
|
-
def _parse_mcp_tool_result(self, tool_result: CallToolResult) -> str:
|
|
64
|
-
"""Parse the content of a CallToolResult into a string.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
tool_result: The result object from calling an MCP tool.
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
A string representation of the tool result content.
|
|
71
|
-
|
|
72
|
-
Raises:
|
|
73
|
-
ToolException: If the tool execution failed, returned no content,
|
|
74
|
-
or contained unexpected content types.
|
|
75
|
-
"""
|
|
76
|
-
if tool_result.isError:
|
|
77
|
-
raise ToolException(f"Tool execution failed: {tool_result.content}")
|
|
78
|
-
|
|
79
|
-
decoded_result = ""
|
|
80
|
-
for item in tool_result.content or []:
|
|
81
|
-
match item.type:
|
|
82
|
-
case "text":
|
|
83
|
-
item: TextContent
|
|
84
|
-
decoded_result += item.text
|
|
85
|
-
case "image":
|
|
86
|
-
item: ImageContent
|
|
87
|
-
decoded_result += item.data # Assuming data is string-like or base64
|
|
88
|
-
case "resource":
|
|
89
|
-
resource: EmbeddedResource = item.resource
|
|
90
|
-
if hasattr(resource, "text"):
|
|
91
|
-
decoded_result += resource.text
|
|
92
|
-
elif hasattr(resource, "blob"):
|
|
93
|
-
# Assuming blob needs decoding or specific handling; adjust as needed
|
|
94
|
-
decoded_result += (
|
|
95
|
-
resource.blob.decode() if isinstance(resource.blob, bytes) else str(resource.blob)
|
|
96
|
-
)
|
|
97
|
-
else:
|
|
98
|
-
raise ToolException(f"Unexpected resource type: {resource.type}")
|
|
99
|
-
case _:
|
|
100
|
-
raise ToolException(f"Unexpected content type: {item.type}")
|
|
101
|
-
|
|
102
|
-
return decoded_result
|
|
103
|
-
|
|
104
60
|
def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> BaseTool:
|
|
105
61
|
"""Convert an MCP tool to LangChain's tool format.
|
|
106
62
|
|
|
@@ -140,7 +96,7 @@ class LangChainAdapter(BaseAdapter):
|
|
|
140
96
|
"""
|
|
141
97
|
raise NotImplementedError("MCP tools only support async operations")
|
|
142
98
|
|
|
143
|
-
async def _arun(self, **kwargs: Any) ->
|
|
99
|
+
async def _arun(self, **kwargs: Any) -> str | dict:
|
|
144
100
|
"""Asynchronously execute the tool with given arguments.
|
|
145
101
|
|
|
146
102
|
Args:
|
|
@@ -157,8 +113,7 @@ class LangChainAdapter(BaseAdapter):
|
|
|
157
113
|
try:
|
|
158
114
|
tool_result: CallToolResult = await self.tool_connector.call_tool(self.name, kwargs)
|
|
159
115
|
try:
|
|
160
|
-
|
|
161
|
-
return adapter_self._parse_mcp_tool_result(tool_result)
|
|
116
|
+
return str(tool_result.content)
|
|
162
117
|
except Exception as e:
|
|
163
118
|
# Log the exception for debugging
|
|
164
119
|
logger.error(f"Error parsing tool result: {e}")
|
mcp_use/agents/mcpagent.py
CHANGED
|
@@ -213,6 +213,42 @@ class MCPAgent:
|
|
|
213
213
|
self._initialized = True
|
|
214
214
|
logger.info("✨ Agent initialization complete")
|
|
215
215
|
|
|
216
|
+
def _normalize_output(self, value: object) -> str:
|
|
217
|
+
"""Normalize model outputs into a plain text string."""
|
|
218
|
+
try:
|
|
219
|
+
if isinstance(value, str):
|
|
220
|
+
return value
|
|
221
|
+
|
|
222
|
+
# LangChain messages may have .content which is str or list-like
|
|
223
|
+
content = getattr(value, "content", None)
|
|
224
|
+
if content is not None:
|
|
225
|
+
return self._normalize_output(content)
|
|
226
|
+
|
|
227
|
+
if isinstance(value, list):
|
|
228
|
+
parts: list[str] = []
|
|
229
|
+
for item in value:
|
|
230
|
+
if isinstance(item, dict):
|
|
231
|
+
if "text" in item and isinstance(item["text"], str):
|
|
232
|
+
parts.append(item["text"])
|
|
233
|
+
elif "content" in item:
|
|
234
|
+
parts.append(self._normalize_output(item["content"]))
|
|
235
|
+
else:
|
|
236
|
+
# Fallback to str for unknown shapes
|
|
237
|
+
parts.append(str(item))
|
|
238
|
+
else:
|
|
239
|
+
# recurse on .content or str
|
|
240
|
+
part_content = getattr(item, "text", None)
|
|
241
|
+
if isinstance(part_content, str):
|
|
242
|
+
parts.append(part_content)
|
|
243
|
+
else:
|
|
244
|
+
parts.append(self._normalize_output(getattr(item, "content", item)))
|
|
245
|
+
return "".join(parts)
|
|
246
|
+
|
|
247
|
+
return str(value)
|
|
248
|
+
|
|
249
|
+
except Exception:
|
|
250
|
+
return str(value)
|
|
251
|
+
|
|
216
252
|
async def _create_system_message_from_tools(self, tools: list[BaseTool]) -> None:
|
|
217
253
|
"""Create the system message based on provided tools using the builder."""
|
|
218
254
|
# Use the override if provided, otherwise use the imported default
|
|
@@ -232,9 +268,12 @@ class MCPAgent:
|
|
|
232
268
|
)
|
|
233
269
|
|
|
234
270
|
# Update conversation history if memory is enabled
|
|
271
|
+
# Note: The system message should not be included in the conversation history,
|
|
272
|
+
# as it will be automatically added using the create_tool_calling_agent function with the prompt parameter
|
|
235
273
|
if self.memory_enabled:
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
self._conversation_history = [
|
|
275
|
+
msg for msg in self._conversation_history if not isinstance(msg, SystemMessage)
|
|
276
|
+
]
|
|
238
277
|
|
|
239
278
|
def _create_agent(self) -> AgentExecutor:
|
|
240
279
|
"""Create the LangChain agent with the configured system message.
|
|
@@ -248,14 +287,25 @@ class MCPAgent:
|
|
|
248
287
|
if self._system_message:
|
|
249
288
|
system_content = self._system_message.content
|
|
250
289
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
290
|
+
if self.memory_enabled:
|
|
291
|
+
# Query already in chat_history — don't re-inject it
|
|
292
|
+
prompt = ChatPromptTemplate.from_messages(
|
|
293
|
+
[
|
|
294
|
+
("system", system_content),
|
|
295
|
+
MessagesPlaceholder(variable_name="chat_history"),
|
|
296
|
+
("human", "{input}"),
|
|
297
|
+
MessagesPlaceholder(variable_name="agent_scratchpad"),
|
|
298
|
+
]
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
# No memory — inject input directly
|
|
302
|
+
prompt = ChatPromptTemplate.from_messages(
|
|
303
|
+
[
|
|
304
|
+
("system", system_content),
|
|
305
|
+
("human", "{input}"),
|
|
306
|
+
MessagesPlaceholder(variable_name="agent_scratchpad"),
|
|
307
|
+
]
|
|
308
|
+
)
|
|
259
309
|
|
|
260
310
|
tool_names = [tool.name for tool in self._tools]
|
|
261
311
|
logger.info(f"🧠 Agent ready with tools: {', '.join(tool_names)}")
|
|
@@ -286,10 +336,6 @@ class MCPAgent:
|
|
|
286
336
|
"""Clear the conversation history."""
|
|
287
337
|
self._conversation_history = []
|
|
288
338
|
|
|
289
|
-
# Re-add the system message if it exists
|
|
290
|
-
if self._system_message and self.memory_enabled:
|
|
291
|
-
self._conversation_history = [self._system_message]
|
|
292
|
-
|
|
293
339
|
def add_to_history(self, message: BaseMessage) -> None:
|
|
294
340
|
"""Add a message to the conversation history.
|
|
295
341
|
|
|
@@ -315,15 +361,6 @@ class MCPAgent:
|
|
|
315
361
|
"""
|
|
316
362
|
self._system_message = SystemMessage(content=message)
|
|
317
363
|
|
|
318
|
-
# Update conversation history if memory is enabled
|
|
319
|
-
if self.memory_enabled:
|
|
320
|
-
# Remove old system message if it exists
|
|
321
|
-
history_without_system = [msg for msg in self._conversation_history if not isinstance(msg, SystemMessage)]
|
|
322
|
-
self._conversation_history = history_without_system
|
|
323
|
-
|
|
324
|
-
# Add new system message
|
|
325
|
-
self._conversation_history.insert(0, self._system_message)
|
|
326
|
-
|
|
327
364
|
# Recreate the agent with the new system message if initialized
|
|
328
365
|
if self._initialized and self._tools:
|
|
329
366
|
self._agent_executor = self._create_agent()
|
|
@@ -467,10 +504,6 @@ class MCPAgent:
|
|
|
467
504
|
display_query = query[:50].replace("\n", " ") + "..." if len(query) > 50 else query.replace("\n", " ")
|
|
468
505
|
logger.info(f"💬 Received query: '{display_query}'")
|
|
469
506
|
|
|
470
|
-
# Add the user query to conversation history if memory is enabled
|
|
471
|
-
if self.memory_enabled:
|
|
472
|
-
self.add_to_history(HumanMessage(content=query))
|
|
473
|
-
|
|
474
507
|
# Use the provided history or the internal history
|
|
475
508
|
history_to_use = external_history if external_history is not None else self._conversation_history
|
|
476
509
|
|
|
@@ -583,7 +616,8 @@ class MCPAgent:
|
|
|
583
616
|
if isinstance(next_step_output, AgentFinish):
|
|
584
617
|
logger.info(f"✅ Agent finished at step {step_num + 1}")
|
|
585
618
|
agent_finished_successfully = True
|
|
586
|
-
|
|
619
|
+
output_value = next_step_output.return_values.get("output", "No output generated")
|
|
620
|
+
result = self._normalize_output(output_value)
|
|
587
621
|
# End the chain if we have a run manager
|
|
588
622
|
if run_manager:
|
|
589
623
|
await run_manager.on_chain_end({"output": result})
|
|
@@ -666,6 +700,7 @@ class MCPAgent:
|
|
|
666
700
|
logger.info(f"🏆 Tool returned directly at step {step_num + 1}")
|
|
667
701
|
agent_finished_successfully = True
|
|
668
702
|
result = tool_return.return_values.get("output", "No output generated")
|
|
703
|
+
result = self._normalize_output(result)
|
|
669
704
|
break
|
|
670
705
|
|
|
671
706
|
except OutputParserException as e:
|
|
@@ -719,8 +754,11 @@ class MCPAgent:
|
|
|
719
754
|
logger.error(f"❌ Final structured output attempt failed: {e}")
|
|
720
755
|
raise RuntimeError(f"Failed to generate structured output after {steps} steps: {str(e)}") from e
|
|
721
756
|
|
|
757
|
+
if self.memory_enabled:
|
|
758
|
+
self.add_to_history(HumanMessage(content=query))
|
|
759
|
+
|
|
722
760
|
if self.memory_enabled and not output_schema:
|
|
723
|
-
self.add_to_history(AIMessage(content=result))
|
|
761
|
+
self.add_to_history(AIMessage(content=self._normalize_output(result)))
|
|
724
762
|
|
|
725
763
|
logger.info(f"🎉 Agent execution complete in {time.time() - start_time} seconds")
|
|
726
764
|
if not success:
|
|
@@ -873,7 +911,7 @@ class MCPAgent:
|
|
|
873
911
|
steps_taken=steps_taken,
|
|
874
912
|
tools_used_count=len(self.tools_used_names),
|
|
875
913
|
tools_used_names=self.tools_used_names,
|
|
876
|
-
response=str(result),
|
|
914
|
+
response=str(self._normalize_output(result)),
|
|
877
915
|
execution_time_ms=int((time.time() - start_time) * 1000),
|
|
878
916
|
error_type=error,
|
|
879
917
|
conversation_history_length=len(self._conversation_history),
|
|
@@ -976,9 +1014,6 @@ class MCPAgent:
|
|
|
976
1014
|
effective_max_steps = max_steps or self.max_steps
|
|
977
1015
|
self._agent_executor.max_iterations = effective_max_steps
|
|
978
1016
|
|
|
979
|
-
if self.memory_enabled:
|
|
980
|
-
self.add_to_history(HumanMessage(content=query))
|
|
981
|
-
|
|
982
1017
|
history_to_use = external_history if external_history is not None else self._conversation_history
|
|
983
1018
|
inputs = {"input": query, "chat_history": history_to_use}
|
|
984
1019
|
|
|
@@ -991,6 +1026,10 @@ class MCPAgent:
|
|
|
991
1026
|
if not isinstance(message, ToolAgentAction):
|
|
992
1027
|
self.add_to_history(message)
|
|
993
1028
|
yield event
|
|
1029
|
+
|
|
1030
|
+
if self.memory_enabled:
|
|
1031
|
+
self.add_to_history(HumanMessage(content=query))
|
|
1032
|
+
|
|
994
1033
|
# 5. House-keeping -------------------------------------------------------
|
|
995
1034
|
# Restrict agent cleanup in _generate_response_chunks_async to only occur
|
|
996
1035
|
# when the agent was initialized in this generator and is not client-managed
|
mcp_use/agents/remote.py
CHANGED
|
@@ -4,6 +4,7 @@ Remote agent implementation for executing agents via API.
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
7
8
|
from typing import Any, TypeVar
|
|
8
9
|
from uuid import UUID
|
|
9
10
|
|
|
@@ -17,7 +18,7 @@ T = TypeVar("T", bound=BaseModel)
|
|
|
17
18
|
|
|
18
19
|
# API endpoint constants
|
|
19
20
|
API_CHATS_ENDPOINT = "/api/v1/chats/get-or-create"
|
|
20
|
-
|
|
21
|
+
API_CHAT_STREAM_ENDPOINT = "/api/v1/chats/{chat_id}/stream"
|
|
21
22
|
API_CHAT_DELETE_ENDPOINT = "/api/v1/chats/{chat_id}"
|
|
22
23
|
|
|
23
24
|
UUID_ERROR_MESSAGE = """A UUID is a 36 character string of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \n
|
|
@@ -129,12 +130,25 @@ class RemoteAgent:
|
|
|
129
130
|
|
|
130
131
|
# Parse into the Pydantic model
|
|
131
132
|
try:
|
|
133
|
+
logger.info(f"🔍 Attempting to validate result_data against {output_schema.__name__}")
|
|
134
|
+
logger.info(f"🔍 Result data type: {type(result_data)}")
|
|
135
|
+
logger.info(f"🔍 Result data: {result_data}")
|
|
132
136
|
return output_schema.model_validate(result_data)
|
|
133
137
|
except Exception as e:
|
|
134
|
-
logger.warning(f"Failed to parse structured output: {e}")
|
|
138
|
+
logger.warning(f"❌ Failed to parse structured output: {e}")
|
|
139
|
+
logger.warning(f"🔍 Validation error details: {type(e).__name__}: {str(e)}")
|
|
140
|
+
logger.warning(f"🔍 Result data that failed validation: {result_data}")
|
|
141
|
+
|
|
135
142
|
# Fallback: try to parse it as raw content if the model has a content field
|
|
136
143
|
if hasattr(output_schema, "model_fields") and "content" in output_schema.model_fields:
|
|
137
|
-
|
|
144
|
+
logger.info("🔄 Attempting fallback with content field")
|
|
145
|
+
try:
|
|
146
|
+
fallback_result = output_schema.model_validate({"content": str(result_data)})
|
|
147
|
+
logger.info("✅ Fallback parsing succeeded")
|
|
148
|
+
return fallback_result
|
|
149
|
+
except Exception as fallback_e:
|
|
150
|
+
logger.error(f"❌ Fallback parsing also failed: {fallback_e}")
|
|
151
|
+
raise
|
|
138
152
|
raise
|
|
139
153
|
|
|
140
154
|
async def _upsert_chat_session(self) -> str:
|
|
@@ -153,7 +167,7 @@ class RemoteAgent:
|
|
|
153
167
|
headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
|
|
154
168
|
chat_url = f"{self.base_url}{API_CHATS_ENDPOINT}"
|
|
155
169
|
|
|
156
|
-
logger.info(f"📝 Upserting chat session for agent {self.agent_id}")
|
|
170
|
+
logger.info(f"📝 [{self.chat_id}] Upserting chat session for agent {self.agent_id}")
|
|
157
171
|
|
|
158
172
|
try:
|
|
159
173
|
chat_response = await self._client.post(chat_url, json=chat_payload, headers=headers)
|
|
@@ -162,9 +176,9 @@ class RemoteAgent:
|
|
|
162
176
|
chat_data = chat_response.json()
|
|
163
177
|
chat_id = chat_data["id"]
|
|
164
178
|
if chat_response.status_code == 201:
|
|
165
|
-
logger.info(f"✅ New chat session created
|
|
179
|
+
logger.info(f"✅ [{self.chat_id}] New chat session created")
|
|
166
180
|
else:
|
|
167
|
-
logger.info(f"✅ Resumed chat session
|
|
181
|
+
logger.info(f"✅ [{self.chat_id}] Resumed chat session")
|
|
168
182
|
|
|
169
183
|
return chat_id
|
|
170
184
|
|
|
@@ -182,144 +196,156 @@ class RemoteAgent:
|
|
|
182
196
|
except Exception as e:
|
|
183
197
|
raise RuntimeError(f"Failed to create chat session: {str(e)}") from e
|
|
184
198
|
|
|
185
|
-
async def
|
|
199
|
+
async def stream(
|
|
186
200
|
self,
|
|
187
201
|
query: str,
|
|
188
202
|
max_steps: int | None = None,
|
|
189
203
|
external_history: list[BaseMessage] | None = None,
|
|
190
204
|
output_schema: type[T] | None = None,
|
|
191
|
-
) -> str
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
query: The query to execute
|
|
196
|
-
max_steps: Maximum number of steps (default: 10)
|
|
197
|
-
external_history: External history (not supported yet for remote execution)
|
|
198
|
-
output_schema: Optional Pydantic model for structured output
|
|
199
|
-
|
|
200
|
-
Returns:
|
|
201
|
-
The result from the remote agent execution (string or structured output)
|
|
202
|
-
"""
|
|
205
|
+
) -> AsyncGenerator[str, None]:
|
|
206
|
+
"""Stream the execution of a query on the remote agent using HTTP streaming."""
|
|
203
207
|
if external_history is not None:
|
|
204
208
|
logger.warning("External history is not yet supported for remote execution")
|
|
205
209
|
|
|
206
|
-
|
|
207
|
-
logger.info(f"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# This happens once per agent instance.
|
|
211
|
-
if not self._session_established:
|
|
212
|
-
logger.info(f"🔧 Establishing chat session for agent {self.agent_id}")
|
|
213
|
-
self.chat_id = await self._upsert_chat_session()
|
|
214
|
-
self._session_established = True
|
|
215
|
-
|
|
216
|
-
chat_id = self.chat_id
|
|
217
|
-
|
|
218
|
-
# Step 2: Execute the agent within the chat context
|
|
219
|
-
execution_payload = {"query": query, "max_steps": max_steps or 10}
|
|
220
|
-
|
|
221
|
-
# Add structured output schema if provided
|
|
222
|
-
if output_schema is not None:
|
|
223
|
-
execution_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
|
|
224
|
-
logger.info(f"🔧 Using structured output with schema: {output_schema.__name__}")
|
|
225
|
-
|
|
226
|
-
headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
|
|
227
|
-
execution_url = f"{self.base_url}{API_CHAT_EXECUTE_ENDPOINT.format(chat_id=chat_id)}"
|
|
228
|
-
logger.info(f"🚀 Executing agent in chat {chat_id}")
|
|
229
|
-
|
|
230
|
-
response = await self._client.post(execution_url, json=execution_payload, headers=headers)
|
|
231
|
-
response.raise_for_status()
|
|
232
|
-
|
|
233
|
-
result = response.json()
|
|
234
|
-
logger.info(f"🔧 Response: {result}")
|
|
235
|
-
logger.info("✅ Remote execution completed successfully")
|
|
236
|
-
|
|
237
|
-
# Check for error responses (even with 200 status)
|
|
238
|
-
if isinstance(result, dict):
|
|
239
|
-
# Check for actual error conditions (not just presence of error field)
|
|
240
|
-
if result.get("status") == "error" or (result.get("error") is not None):
|
|
241
|
-
error_msg = result.get("error", str(result))
|
|
242
|
-
logger.error(f"❌ Remote agent execution failed: {error_msg}")
|
|
243
|
-
raise RuntimeError(f"Remote agent execution failed: {error_msg}")
|
|
244
|
-
|
|
245
|
-
# Check if the response indicates agent initialization failure
|
|
246
|
-
if "failed to initialize" in str(result):
|
|
247
|
-
logger.error(f"❌ Agent initialization failed: {result}")
|
|
248
|
-
raise RuntimeError(
|
|
249
|
-
f"Agent initialization failed on remote server. "
|
|
250
|
-
f"This usually indicates:\n"
|
|
251
|
-
f"• Invalid agent configuration (LLM model, system prompt)\n"
|
|
252
|
-
f"• Missing or invalid MCP server configurations\n"
|
|
253
|
-
f"• Network connectivity issues with MCP servers\n"
|
|
254
|
-
f"• Missing environment variables or credentials\n"
|
|
255
|
-
f"Raw error: {result}"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# Handle structured output
|
|
259
|
-
if output_schema is not None:
|
|
260
|
-
return self._parse_structured_response(result, output_schema)
|
|
210
|
+
if not self._session_established:
|
|
211
|
+
logger.info(f"🔧 [{self.chat_id}] Establishing chat session for agent {self.agent_id}")
|
|
212
|
+
self.chat_id = await self._upsert_chat_session()
|
|
213
|
+
self._session_established = True
|
|
261
214
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
215
|
+
chat_id = self.chat_id
|
|
216
|
+
stream_url = f"{self.base_url}{API_CHAT_STREAM_ENDPOINT.format(chat_id=chat_id)}"
|
|
217
|
+
|
|
218
|
+
# Prepare the request payload
|
|
219
|
+
request_payload = {"messages": [{"role": "user", "content": query}], "max_steps": max_steps or 30}
|
|
220
|
+
if output_schema is not None:
|
|
221
|
+
request_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
|
|
222
|
+
|
|
223
|
+
headers = {"Content-Type": "application/json", "x-api-key": self.api_key, "Accept": "text/event-stream"}
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
logger.info(f"🌐 [{self.chat_id}] Connecting to HTTP stream for agent {self.agent_id}")
|
|
227
|
+
|
|
228
|
+
async with self._client.stream("POST", stream_url, headers=headers, json=request_payload) as response:
|
|
229
|
+
logger.info(f"✅ [{self.chat_id}] HTTP stream connection established")
|
|
230
|
+
|
|
231
|
+
if response.status_code != 200:
|
|
232
|
+
error_text = await response.aread()
|
|
233
|
+
raise RuntimeError(f"Failed to stream from remote agent: {error_text.decode()}")
|
|
234
|
+
|
|
235
|
+
# Read the streaming response line by line
|
|
236
|
+
try:
|
|
237
|
+
async for line in response.aiter_lines():
|
|
238
|
+
if line:
|
|
239
|
+
yield line
|
|
240
|
+
except UnicodeDecodeError as e:
|
|
241
|
+
logger.error(f"❌ [{self.chat_id}] UTF-8 decoding error at position {e.start}: {e.reason}")
|
|
242
|
+
logger.error(f"❌ [{self.chat_id}] Error occurred while reading stream for agent {self.agent_id}")
|
|
243
|
+
# Try to read raw bytes and decode with error handling
|
|
244
|
+
logger.info(f"🔄 [{self.chat_id}] Attempting to read raw bytes with error handling...")
|
|
245
|
+
logger.info(f"✅ [{self.chat_id}] Agent execution stream completed")
|
|
269
246
|
|
|
270
247
|
except httpx.HTTPStatusError as e:
|
|
271
248
|
status_code = e.response.status_code
|
|
272
249
|
response_text = e.response.text
|
|
273
250
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
logger.error(f"❌ Authentication failed: {response_text}")
|
|
277
|
-
raise RuntimeError(
|
|
278
|
-
"Authentication failed: Invalid or missing API key. "
|
|
279
|
-
"Please check your API key and ensure the MCP_USE_API_KEY environment variable is set correctly."
|
|
280
|
-
) from e
|
|
281
|
-
elif status_code == 403:
|
|
282
|
-
logger.error(f"❌ Access forbidden: {response_text}")
|
|
283
|
-
raise RuntimeError(
|
|
284
|
-
f"Access denied: You don't have permission to execute agent '{self.agent_id}'. "
|
|
285
|
-
"Check if the agent exists and you have the necessary permissions."
|
|
286
|
-
) from e
|
|
287
|
-
elif status_code == 404:
|
|
288
|
-
logger.error(f"❌ Agent not found: {response_text}")
|
|
289
|
-
raise RuntimeError(
|
|
290
|
-
f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
|
|
291
|
-
"Please verify the agent ID and ensure it exists in your account."
|
|
292
|
-
) from e
|
|
293
|
-
elif status_code == 422:
|
|
294
|
-
logger.error(f"❌ Validation error: {response_text}")
|
|
295
|
-
raise RuntimeError(
|
|
296
|
-
f"Request validation failed: {response_text}. "
|
|
297
|
-
"Please check your query parameters and output schema format."
|
|
298
|
-
) from e
|
|
299
|
-
elif status_code == 500:
|
|
300
|
-
logger.error(f"❌ Server error: {response_text}")
|
|
301
|
-
raise RuntimeError(
|
|
302
|
-
"Internal server error occurred during agent execution. "
|
|
303
|
-
"Please try again later or contact support if the issue persists."
|
|
304
|
-
) from e
|
|
251
|
+
if status_code == 404:
|
|
252
|
+
raise RuntimeError(f"Chat or agent not found: {response_text}") from e
|
|
305
253
|
else:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
254
|
+
raise RuntimeError(f"Failed to stream from remote agent: {status_code} - {response_text}") from e
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"❌ [{self.chat_id}] An error occurred during HTTP streaming: {e}")
|
|
257
|
+
raise RuntimeError(f"Failed to stream from remote agent: {str(e)}") from e
|
|
258
|
+
|
|
259
|
+
async def run(
|
|
260
|
+
self,
|
|
261
|
+
query: str,
|
|
262
|
+
max_steps: int | None = None,
|
|
263
|
+
external_history: list[BaseMessage] | None = None,
|
|
264
|
+
output_schema: type[T] | None = None,
|
|
265
|
+
) -> str | T:
|
|
266
|
+
"""
|
|
267
|
+
Executes the agent and returns the final result.
|
|
268
|
+
This method uses HTTP streaming to avoid timeouts for long-running tasks.
|
|
269
|
+
It consumes the entire stream and returns only the final result.
|
|
270
|
+
"""
|
|
271
|
+
final_result = None
|
|
272
|
+
steps_taken = 0
|
|
273
|
+
finished = False
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Consume the ENTIRE stream to ensure proper execution
|
|
277
|
+
async for event in self.stream(query, max_steps, external_history, output_schema):
|
|
278
|
+
logger.debug(f"[{self.chat_id}] Processing stream event: {event}...")
|
|
279
|
+
|
|
280
|
+
# Parse AI SDK format events to extract final result
|
|
281
|
+
# The events follow the AI SDK streaming protocol
|
|
282
|
+
if event.startswith("0:"): # Text event
|
|
283
|
+
try:
|
|
284
|
+
text_data = json.loads(event[2:]) # Remove "0:" prefix
|
|
285
|
+
if final_result is None:
|
|
286
|
+
final_result = ""
|
|
287
|
+
final_result += text_data
|
|
288
|
+
result_preview = final_result[:200] if len(final_result) > 200 else final_result
|
|
289
|
+
logger.debug(f"Accumulated text result: {result_preview}...")
|
|
290
|
+
except json.JSONDecodeError:
|
|
291
|
+
logger.warning(f"Failed to parse text event: {event[:100]}")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
elif event.startswith("9:"): # Tool call event
|
|
295
|
+
steps_taken += 1
|
|
296
|
+
logger.debug(f"Tool call executed, total steps: {steps_taken}")
|
|
297
|
+
|
|
298
|
+
elif event.startswith("d:"): # Finish event
|
|
299
|
+
logger.debug("Received finish event, marking as finished")
|
|
300
|
+
finished = True
|
|
301
|
+
# Continue consuming to ensure stream cleanup
|
|
302
|
+
|
|
303
|
+
elif event.startswith("3:"): # Error event
|
|
304
|
+
try:
|
|
305
|
+
error_data = json.loads(event[2:])
|
|
306
|
+
error_msg = error_data if isinstance(error_data, str) else json.dumps(error_data)
|
|
307
|
+
raise RuntimeError(f"Agent execution failed: {error_msg}")
|
|
308
|
+
except json.JSONDecodeError as e:
|
|
309
|
+
raise RuntimeError("Agent execution failed with unknown error") from e
|
|
310
|
+
|
|
311
|
+
# Log completion of stream consumption
|
|
312
|
+
logger.info(f"Stream consumption complete. Finished: {finished}, Steps taken: {steps_taken}")
|
|
313
|
+
|
|
314
|
+
if final_result is None:
|
|
315
|
+
logger.warning(f"No final result captured from stream (structured output: {output_schema is not None})")
|
|
316
|
+
final_result = "" # Return empty string instead of error message
|
|
317
|
+
|
|
318
|
+
# For structured output, try to parse the result
|
|
319
|
+
if output_schema:
|
|
320
|
+
logger.info(f"🔍 Attempting structured output parsing for schema: {output_schema.__name__}")
|
|
321
|
+
logger.info(f"🔍 Raw final result type: {type(final_result)}")
|
|
322
|
+
logger.info(f"🔍 Raw final result length: {len(str(final_result)) if final_result else 0}")
|
|
323
|
+
logger.info(f"🔍 Raw final result preview: {str(final_result)[:500] if final_result else 'None'}...")
|
|
324
|
+
|
|
325
|
+
if isinstance(final_result, str) and final_result:
|
|
326
|
+
try:
|
|
327
|
+
# Try to parse as JSON first
|
|
328
|
+
parsed_result = json.loads(final_result)
|
|
329
|
+
logger.info("✅ Successfully parsed structured result as JSON")
|
|
330
|
+
return self._parse_structured_response(parsed_result, output_schema)
|
|
331
|
+
except json.JSONDecodeError as e:
|
|
332
|
+
logger.warning(f"❌ Could not parse result as JSON: {e}")
|
|
333
|
+
logger.warning(f"🔍 Raw string content: {final_result[:1000]}...")
|
|
334
|
+
# Try to parse directly
|
|
335
|
+
return self._parse_structured_response({"content": final_result}, output_schema)
|
|
336
|
+
else:
|
|
337
|
+
logger.warning(f"❌ Final result is empty or not string: {final_result}")
|
|
338
|
+
# Try to parse the result directly
|
|
339
|
+
return self._parse_structured_response(final_result, output_schema)
|
|
340
|
+
|
|
341
|
+
# Regular string output
|
|
342
|
+
return final_result if isinstance(final_result, str) else str(final_result)
|
|
343
|
+
|
|
344
|
+
except RuntimeError:
|
|
345
|
+
raise
|
|
320
346
|
except Exception as e:
|
|
321
|
-
logger.error(f"
|
|
322
|
-
raise RuntimeError(f"
|
|
347
|
+
logger.error(f"Error executing agent: {e}")
|
|
348
|
+
raise RuntimeError(f"Failed to execute agent: {str(e)}") from e
|
|
323
349
|
|
|
324
350
|
async def close(self) -> None:
|
|
325
351
|
"""Close the HTTP client."""
|