Skip to content

Commit e81ed06

Browse files
bloveclaude
andauthored
debug(graph): @Traceable _maybe_write_thread_title so LangSmith captures errors (#492)
LangGraph Platform's stdout (where PR #491's print() lands) isn't reachable via the LangSmith Runs API. After firing a probe against prod (thread 019e475a-4add-79a3-95b1-348525d79b0e) we confirmed the title stays null AND no actionable error surfaces in any API we can query. Wrap the helper with @langsmith.traceable so it becomes a child run in the trace tree, and convert all early-returns to typed dict returns (skipped/wrote_title/error_type/error_message). The error branch now captures type, message, and the sdk_url that was used — all of which show up in the run's `outputs` field when queried via the LangSmith Runs API. Cleaner than stdout grep, gives us the proximate cause without needing platform log access. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent def749a commit e81ed06

1 file changed

Lines changed: 27 additions & 20 deletions

File tree

examples/chat/python/src/graph.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from langchain_core.runnables import RunnableConfig
4242
from langchain_core.tools import tool
4343
from langgraph_sdk import get_client
44+
from langsmith import traceable
4445

4546
from src.streaming.a2ui_partial_handler import A2uiPartialHandler
4647
from src.streaming.envelope_tool import render_a2ui_surface
@@ -81,27 +82,32 @@ def _slice_title(text: str, *, limit: int = 50) -> str:
8182
return sliced
8283

8384

84-
async def _maybe_write_thread_title(state: "State", config: RunnableConfig) -> None:
85+
@traceable(name="_maybe_write_thread_title", run_type="tool")
86+
async def _maybe_write_thread_title(state: "State", config: RunnableConfig) -> dict:
8587
"""Side effect: on the first user message in a thread, persist a
8688
derived title to the thread's LangGraph metadata.
8789
8890
Idempotent — only writes when metadata.title is currently absent.
89-
Errors are swallowed; the title is a UX nicety, never a blocker.
91+
Errors are NOT raised (title is a UX nicety, never a blocker) but
92+
ARE returned in the run output so LangSmith captures them — the
93+
bare `except Exception: return` previously hid a prod failure
94+
where every title write was throwing silently. The @traceable
95+
decorator creates a LangSmith child run with these outputs
96+
visible in the trace UI / runs API.
9097
"""
9198
global _threads_client
9299
thread_id = (config.get("configurable") or {}).get("thread_id")
100+
sdk_url = os.environ.get("LANGGRAPH_API_URL", "http://localhost:2024")
93101
if not isinstance(thread_id, str) or not thread_id:
94-
return
102+
return {"skipped": "no thread_id in config"}
95103

96104
try:
97105
if _threads_client is None:
98-
_threads_client = get_client(
99-
url=os.environ.get("LANGGRAPH_API_URL", "http://localhost:2024"),
100-
)
106+
_threads_client = get_client(url=sdk_url)
101107
thread = await _threads_client.threads.get(thread_id)
102108
existing = (thread.get("metadata") or {}).get("title")
103109
if isinstance(existing, str) and existing.strip():
104-
return # Already titled; don't overwrite.
110+
return {"skipped": "already titled", "title": existing}
105111

106112
# Find the first user message in the current state.
107113
first_user = None
@@ -115,28 +121,29 @@ async def _maybe_write_thread_title(state: "State", config: RunnableConfig) -> N
115121
first_user = content
116122
break
117123
if not first_user:
118-
return
124+
return {"skipped": "no human message in state"}
119125

120126
title = _slice_title(first_user)
121127
if not title:
122-
return
128+
return {"skipped": "title slice empty"}
123129

124130
await _threads_client.threads.update(
125131
thread_id,
126132
metadata={"title": title},
127133
)
134+
return {"wrote_title": title, "sdk_url": sdk_url}
128135
except Exception as e: # noqa: BLE001 — title is a UX nicety; never block
129-
# Don't break the run, but DO log. A bare swallow has hidden a prod
130-
# bug where the title write was failing silently on every thread
131-
# (LANGGRAPH_API_URL fallback to localhost:2024 inside the runtime
132-
# container). Print to stdout so it surfaces in LangGraph Platform
133-
# logs without needing a logger to be configured.
134-
print(
135-
f"[_maybe_write_thread_title] failed for thread {thread_id}: "
136-
f"{type(e).__name__}: {e}",
137-
flush=True,
138-
)
139-
return
136+
# Don't break the run, but DO surface the failure. A bare swallow
137+
# has hidden a prod bug where every title write was throwing
138+
# silently. The @traceable decorator captures this return dict in
139+
# the LangSmith run output (visible via /api/v1/runs/query) so we
140+
# can diagnose without needing platform stdout access.
141+
return {
142+
"error_type": type(e).__name__,
143+
"error_message": str(e),
144+
"sdk_url": sdk_url,
145+
"thread_id": thread_id,
146+
}
140147

141148

142149
SYSTEM_PROMPT = (

0 commit comments

Comments
 (0)