From 8beeecb50f764ac0b0b8975f721d7d94e1895829 Mon Sep 17 00:00:00 2001 From: goulustis Date: Thu, 15 Jan 2026 15:56:37 +0800 Subject: [PATCH] format result as dict --- lang_agent/components/client_tool_manager.py | 57 +++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/lang_agent/components/client_tool_manager.py b/lang_agent/components/client_tool_manager.py index 28e8f57..ff115f9 100644 --- a/lang_agent/components/client_tool_manager.py +++ b/lang_agent/components/client_tool_manager.py @@ -6,6 +6,7 @@ import asyncio import json import os.path as osp from loguru import logger +from mcp.types import ImageContent from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_core.tools import BaseTool, StructuredTool @@ -16,6 +17,7 @@ from pydantic import BaseModel, Field, create_model from lang_agent.config import InstantiateConfig + def _json_default_serializer(obj: Any) -> Any: """ Best-effort fallback serializer for objects that json can't handle. @@ -27,6 +29,9 @@ def _json_default_serializer(obj: Any) -> Any: - Else if it's a dataclass, convert via `asdict`. - Else fall back to `str(obj)`. """ + if isinstance(obj, ImageContent): + return {'image_base64':obj.data} + # Pydantic v2 models if hasattr(obj, "model_dump") and callable(getattr(obj, "model_dump")): try: @@ -65,7 +70,57 @@ def _format_tool_result(result: Any, tool_call_info: dict | None) -> str | ToolM The JSON serialization is made robust to non-serializable objects (e.g. ImageContent) via `_json_default_serializer`. """ - content = json.dumps(list(result), default=_json_default_serializer, ensure_ascii=False) + # Prefer a dict-style JSON payload instead of a generic list-of-objects. + # Special handling for common MCP pattern: (json_string_or_dict, [ImageContent, ...]) + if isinstance(result, tuple): + primary, secondary = result if len(result) == 2 else (result, None) + + # Decode primary part + if isinstance(primary, str): + try: + primary_obj: Any = json.loads(primary) + except Exception: + primary_obj = primary + else: + primary_obj = primary + + # Attach secondary part (e.g. images) in a structured way + if secondary is not None: + # Normalise to list + secondary_list = list(secondary) if not isinstance(secondary, list) else secondary + secondary_serialized = [_json_default_serializer(x) for x in secondary_list] + + if isinstance(primary_obj, dict): + # Prefer a top-level "image_base64" key when there is exactly one image, + # to match expected contract for simple image-returning tools. + if ( + len(secondary_serialized) == 1 + and isinstance(secondary_serialized[0], dict) + and "image_base64" in secondary_serialized[0] + ): + primary_obj = { + **primary_obj, + "image_base64": secondary_serialized[0]["image_base64"], + } + else: + # Fallback: attach all serialized items under "images" + primary_obj = { + **primary_obj, + "images": secondary_serialized, + } + else: + # Fallback: wrap everything into a dict + primary_obj = { + "result": primary_obj, + "images": secondary_serialized, + } + + content_obj = primary_obj + else: + # Non-tuple results are serialized as-is (dict, list, scalar, etc.) + content_obj = result + + content = json.dumps(content_obj, default=_json_default_serializer, ensure_ascii=False) if tool_call_info and tool_call_info.get("id"): return ToolMessage(content=content, name=tool_call_info.get("name"),