diff --git a/agent/canvas.py b/agent/canvas.py index 65303ca9e9e..09538faddc3 100644 --- a/agent/canvas.py +++ b/agent/canvas.py @@ -119,7 +119,11 @@ def __str__(self): for k in self.dsl.keys(): if k in ["components"]: continue - dsl[k] = deepcopy(self.dsl[k]) + try: + dsl[k] = deepcopy(self.dsl[k]) + except Exception as e: + logging.warning("Graph.__str__: deepcopy failed for dsl key '%s' (type=%s): %s. Using shallow reference.", k, type(self.dsl[k]).__name__, e) + dsl[k] = self.dsl[k] for k, cpn in self.components.items(): if k not in dsl["components"]: @@ -128,8 +132,17 @@ def __str__(self): if c == "obj": dsl["components"][k][c] = json.loads(str(cpn["obj"])) continue - dsl["components"][k][c] = deepcopy(cpn[c]) - return json.dumps(dsl, ensure_ascii=False) + try: + dsl["components"][k][c] = deepcopy(cpn[c]) + except Exception as e: + logging.warning("Graph.__str__: deepcopy failed for component '%s' key '%s' (type=%s): %s. Using shallow reference.", k, c, type(cpn[c]).__name__, e) + dsl["components"][k][c] = cpn[c] + def _serialize_default(obj): + if callable(obj): + return None + logging.warning("Graph.__str__: JSON fallback via str() for type=%s", type(obj).__name__) + return str(obj) + return json.dumps(dsl, ensure_ascii=False, default=_serialize_default) def reset(self): self.path = [] diff --git a/agent/component/base.py b/agent/component/base.py index 9bceb4ce6d9..0f5e84a0f2d 100644 --- a/agent/component/base.py +++ b/agent/component/base.py @@ -94,7 +94,12 @@ def _deprecated_params_set(self): return {name: True for name in self.get_feeded_deprecated_params()} def __str__(self): - return json.dumps(self.as_dict(), ensure_ascii=False) + def _serialize_default(obj): + if callable(obj): + return None + logging.warning("ComponentParamBase.__str__: JSON fallback via str() for type=%s", type(obj).__name__) + return str(obj) + return json.dumps(self.as_dict(), ensure_ascii=False, default=_serialize_default) def as_dict(self): def _recursive_convert_obj_to_dict(obj): diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 8c896e36add..e3da60b9402 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -19,6 +19,20 @@ import logging from functools import partial from quart import request, Response, make_response + + +def _canvas_json_default(obj): + """Fallback serializer for canvas SSE events. + + Agent components store functools.partial objects as deferred streaming + handles (see llm.py, agent_with_tools.py, message.py). These leak into + SSE event dicts via component input/output propagation and are not + JSON-serializable. This handler converts them to None so that downstream + consumers never receive opaque ``str(partial(...))`` representations. + """ + if callable(obj): + return None + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") from agent.component import LLM from api.db import CanvasCategory from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService @@ -235,7 +249,7 @@ async def sse(): nonlocal canvas, user_id try: async for ans in canvas.run(query=query, files=files, user_id=user_id, inputs=inputs): - yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" + yield "data:" + json.dumps(ans, ensure_ascii=False, default=_canvas_json_default) + "\n\n" commit_ok = CanvasReplicaService.commit_after_run( canvas_id=req["id"], @@ -293,7 +307,7 @@ async def generate(): } ) ans.setdefault("data", {})["trace"] = trace_items - answer = "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" + answer = "data:" + json.dumps(ans, ensure_ascii=False, default=_canvas_json_default) + "\n\n" yield answer if event not in ["message", "message_end"]: