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
50 changes: 45 additions & 5 deletions python/langsmith/run_trees.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Schemas for the LangSmith API."""

from __future__ import annotations
Expand Down Expand Up @@ -233,13 +233,53 @@
@root_validator(pre=True)
def infer_defaults(cls, values: dict) -> dict:
"""Assign name to the run."""
# Try to get name from serialized data (legacy LangChain support)
if values.get("name") is None and values.get("serialized") is not None:
if "name" in values["serialized"]:
values["name"] = values["serialized"]["name"]
elif "id" in values["serialized"]:
values["name"] = values["serialized"]["id"][-1]
serialized = values["serialized"]
values["name"] = serialized.get("name") or (
serialized["id"][-1] if "id" in serialized else None
)

# If still no name, try to infer from caller
if values.get("name") is None:
values["name"] = "Unnamed"
name = None
try:
import inspect
Copy link
Collaborator

Choose a reason for hiding this comment

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

Imports like this should be top level right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it should be rare case we even need to enter "make a name" mode. so i don't worry too much about perf.
yeah inspect can go to top

import os

# Look up the stack, skipping internal frames
for frame_info in inspect.stack()[1:8]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any perf concerns here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it should be rare case we even need to enter "make a name" mode. i.e, this conditoin should not be entered.
so i don't worry too much about perf.

filename = frame_info.filename
norm_path = filename.replace("\\", "/")

# Skip pydantic and langsmith internal modules
if "/pydantic/" in norm_path:
continue
if os.path.basename(filename) in {
"run_trees.py",
"run_helpers.py",
"client.py",
"_context.py",
}:
continue

# Try to get class name from self or cls
frame_locals = frame_info.frame.f_locals
if "self" in frame_locals:
name = frame_locals["self"].__class__.__name__
break
elif "cls" in frame_locals:
name = frame_locals["cls"].__name__
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know enough about Python to know if this is a good idea

Is there no easier fallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there's try catch so at least it won't error, no better fallback afaik

break
except Exception:
pass

# Use inferred name or descriptive fallback
if name:
values["name"] = name
else:
run_type = values.get("run_type", "chain")
values["name"] = f"Unnamed_{run_type}"
if "client" in values: # Handle user-constructed clients
values["ls_client"] = values.pop("client")
elif "_client" in values:
Expand Down
62 changes: 62 additions & 0 deletions python/tests/unit_tests/test_unnamed_trace_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Test name inference for RunTree when name is not provided."""

from unittest import mock

from langsmith import Client, traceable
from langsmith.run_trees import RunTree


class Router:
"""Simple class with __call__ method (like LangGraph nodes)."""

def __call__(self, state: dict) -> dict:
return {"result": "ok"}


def test_runtree_infers_class_name_from_stack():
"""RunTree should infer class name from stack when created in class method."""

class MyRouter:
def process(self):
return RunTree(run_type="chain")

run = MyRouter().process()
assert run.name == "MyRouter"


def test_runtree_falls_back_to_descriptive_name():
"""RunTree should use Unnamed_{run_type} when inference fails."""
run = RunTree(run_type="llm")

# Should NOT be just "Unnamed" - should be descriptive or inferred
assert run.name != "Unnamed"
# Name should be something reasonable (inferred from stack or fallback)
assert len(run.name) > 0


def test_runtree_uses_explicit_name():
"""RunTree should use explicit name when provided."""
run = RunTree(name="MyExplicitName", run_type="chain")
assert run.name == "MyExplicitName"


def test_utils_get_function_name_for_callable_class():
"""_get_function_name should return class name for callable instances."""
from langsmith.utils import _get_function_name

router = Router()
assert _get_function_name(router) == "Router"


def test_traceable_with_callable_class():
"""@traceable should work with callable classes."""
router = Router()
traced = traceable(router)

with mock.patch.object(Client, "create_run") as mock_create:
traced({"input": "test"})

if mock_create.called:
name = mock_create.call_args[1].get("name", "")
assert name == "Router"
assert name != "Unnamed"
Loading