Skip to content

Commit 431ceae

Browse files
committed
improve runTree unname by inspect func or class
1 parent c9f1c35 commit 431ceae

File tree

2 files changed

+107
-5
lines changed

2 files changed

+107
-5
lines changed

python/langsmith/run_trees.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,53 @@ class Config:
233233
@root_validator(pre=True)
234234
def infer_defaults(cls, values: dict) -> dict:
235235
"""Assign name to the run."""
236+
# Try to get name from serialized data (legacy LangChain support)
236237
if values.get("name") is None and values.get("serialized") is not None:
237-
if "name" in values["serialized"]:
238-
values["name"] = values["serialized"]["name"]
239-
elif "id" in values["serialized"]:
240-
values["name"] = values["serialized"]["id"][-1]
238+
serialized = values["serialized"]
239+
values["name"] = serialized.get("name") or (
240+
serialized["id"][-1] if "id" in serialized else None
241+
)
242+
243+
# If still no name, try to infer from caller
241244
if values.get("name") is None:
242-
values["name"] = "Unnamed"
245+
name = None
246+
try:
247+
import inspect
248+
import os
249+
250+
# Look up the stack, skipping internal frames
251+
for frame_info in inspect.stack()[1:8]:
252+
filename = frame_info.filename
253+
norm_path = filename.replace("\\", "/")
254+
255+
# Skip pydantic and langsmith internal modules
256+
if "/pydantic/" in norm_path:
257+
continue
258+
if os.path.basename(filename) in {
259+
"run_trees.py",
260+
"run_helpers.py",
261+
"client.py",
262+
"_context.py",
263+
}:
264+
continue
265+
266+
# Try to get class name from self or cls
267+
frame_locals = frame_info.frame.f_locals
268+
if "self" in frame_locals:
269+
name = frame_locals["self"].__class__.__name__
270+
break
271+
elif "cls" in frame_locals:
272+
name = frame_locals["cls"].__name__
273+
break
274+
except Exception:
275+
pass
276+
277+
# Use inferred name or descriptive fallback
278+
if name:
279+
values["name"] = name
280+
else:
281+
run_type = values.get("run_type", "chain")
282+
values["name"] = f"Unnamed_{run_type}"
243283
if "client" in values: # Handle user-constructed clients
244284
values["ls_client"] = values.pop("client")
245285
elif "_client" in values:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Test name inference for RunTree when name is not provided."""
2+
3+
from unittest import mock
4+
5+
from langsmith import Client, traceable
6+
from langsmith.run_trees import RunTree
7+
8+
9+
class Router:
10+
"""Simple class with __call__ method (like LangGraph nodes)."""
11+
12+
def __call__(self, state: dict) -> dict:
13+
return {"result": "ok"}
14+
15+
16+
def test_runtree_infers_class_name_from_stack():
17+
"""RunTree should infer class name from stack when created in class method."""
18+
19+
class MyRouter:
20+
def process(self):
21+
return RunTree(run_type="chain")
22+
23+
run = MyRouter().process()
24+
assert run.name == "MyRouter"
25+
26+
27+
def test_runtree_falls_back_to_descriptive_name():
28+
"""RunTree should use Unnamed_{run_type} when inference fails."""
29+
run = RunTree(run_type="llm")
30+
31+
# Should NOT be just "Unnamed" - should be descriptive or inferred
32+
assert run.name != "Unnamed"
33+
# Name should be something reasonable (inferred from stack or fallback)
34+
assert len(run.name) > 0
35+
36+
37+
def test_runtree_uses_explicit_name():
38+
"""RunTree should use explicit name when provided."""
39+
run = RunTree(name="MyExplicitName", run_type="chain")
40+
assert run.name == "MyExplicitName"
41+
42+
43+
def test_utils_get_function_name_for_callable_class():
44+
"""_get_function_name should return class name for callable instances."""
45+
from langsmith.utils import _get_function_name
46+
47+
router = Router()
48+
assert _get_function_name(router) == "Router"
49+
50+
51+
def test_traceable_with_callable_class():
52+
"""@traceable should work with callable classes."""
53+
router = Router()
54+
traced = traceable(router)
55+
56+
with mock.patch.object(Client, "create_run") as mock_create:
57+
traced({"input": "test"})
58+
59+
if mock_create.called:
60+
name = mock_create.call_args[1].get("name", "")
61+
assert name == "Router"
62+
assert name != "Unnamed"

0 commit comments

Comments
 (0)