Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions agent/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for k, cpn in self.components.items():
if k not in dsl["components"]:
Expand All @@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def reset(self):
self.path = []
Expand Down
7 changes: 6 additions & 1 deletion agent/component/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
18 changes: 16 additions & 2 deletions api/apps/canvas_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines +24 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Log callable-to-None coercions in _canvas_json_default.

This new fallback path currently drops data silently. Add a warning before coercion so production traceability is preserved when payload fields disappear.

🔧 Proposed fix
 def _canvas_json_default(obj):
@@
     if callable(obj):
+        logging.warning(
+            "canvas_app: SSE JSON fallback coerced callable type=%s to None",
+            type(obj).__name__,
+        )
         return None
     raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

As per coding guidelines, **/*.py: Add logging for new flows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/apps/canvas_app.py` around lines 24 - 35, Update _canvas_json_default to
log a warning before returning None for callables: import or obtain a module
logger (e.g. logging.getLogger(__name__)) and call logger.warning with context
including the object's type and a short repr (e.g. f"Coercing callable to None
in canvas SSE payload: type=%s repr=%s", type(obj).__name__, repr(obj)) right
before the "return None" line in _canvas_json_default so that callable-to-None
coercions are recorded without changing behavior for non-callables.

from agent.component import LLM
from api.db import CanvasCategory
from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"]:
Expand Down