diff --git a/libs/core/langchain_core/callbacks/manager.py b/libs/core/langchain_core/callbacks/manager.py index 74f4d1a6391..2297e684de1 100644 --- a/libs/core/langchain_core/callbacks/manager.py +++ b/libs/core/langchain_core/callbacks/manager.py @@ -41,6 +41,7 @@ from langchain_core.callbacks.base import ( ) from langchain_core.callbacks.stdout import StdOutCallbackHandler from langchain_core.messages import BaseMessage, get_buffer_string +from langchain_core.tracers.schemas import Run from langchain_core.utils.env import env_var_is_set if TYPE_CHECKING: @@ -1994,15 +1995,22 @@ def _configure( callback_manager.add_handler(tracer_v2, True) else: try: - handler = LangChainTracer(project_name=tracer_project) + handler = LangChainTracer( + project_name=tracer_project, + client=run_tree.client if run_tree is not None else None, + ) callback_manager.add_handler(handler, True) except Exception as e: logger.warning( "Unable to load requested LangChainTracer." " To disable this warning," " unset the LANGCHAIN_TRACING_V2 environment variables.", - e, + f"{repr(e)}", ) + if run_tree is not None: + for handler in callback_manager.handlers: + if isinstance(handler, LangChainTracer): + handler.run_map[str(run_tree.id)] = cast(Run, run_tree) for var, inheritable, handler_class, env_var in _configure_hooks: create_one = ( env_var is not None diff --git a/libs/core/langchain_core/tracers/base.py b/libs/core/langchain_core/tracers/base.py index cdecedb09df..4c77928eae5 100644 --- a/libs/core/langchain_core/tracers/base.py +++ b/libs/core/langchain_core/tracers/base.py @@ -1,4 +1,5 @@ """Base interfaces for tracing runs.""" + from __future__ import annotations import logging @@ -102,9 +103,10 @@ class BaseTracer(BaseCallbackHandler, ABC): parent_run = self.run_map.get(str(run.parent_run_id)) if parent_run: self._add_child_run(parent_run, run) - parent_run.child_execution_order = max( - parent_run.child_execution_order, run.child_execution_order - ) + if hasattr(parent_run, "child_execution_order"): + parent_run.child_execution_order = max( + parent_run.child_execution_order, run.child_execution_order + ) run.trace_id = parent_run.trace_id if parent_run.dotted_order: run.dotted_order = ( @@ -135,7 +137,7 @@ class BaseTracer(BaseCallbackHandler, ABC): logger.debug(f"Parent run with UUID {run.parent_run_id} not found.") elif ( run.child_execution_order is not None - and parent_run.child_execution_order is not None + and getattr(parent_run, "child_execution_order", None) is not None and run.child_execution_order > parent_run.child_execution_order ): parent_run.child_execution_order = run.child_execution_order @@ -151,10 +153,11 @@ class BaseTracer(BaseCallbackHandler, ABC): if parent_run is None: logger.debug(f"Parent run with UUID {parent_run_id} not found.") return 1 - if parent_run.child_execution_order is None: - raise TracerException( - f"Parent run with UUID {parent_run_id} has no child execution order." + if getattr(parent_run, "child_execution_order", None) is None: + logger.debug( + f"Parent run with UUID {parent_run_id} has no child_execution_order." ) + return 1 return parent_run.child_execution_order + 1 diff --git a/libs/core/tests/unit_tests/tracers/test_base_tracer.py b/libs/core/tests/unit_tests/tracers/test_base_tracer.py index cefaa9c2fc3..96a29bd99ad 100644 --- a/libs/core/tests/unit_tests/tracers/test_base_tracer.py +++ b/libs/core/tests/unit_tests/tracers/test_base_tracer.py @@ -1,16 +1,21 @@ """Test Tracer classes.""" + from __future__ import annotations from datetime import datetime, timezone from typing import Any, List +from unittest.mock import MagicMock from uuid import uuid4 +import langsmith import pytest from freezegun import freeze_time +from langsmith import Client, traceable from langchain_core.callbacks import CallbackManager from langchain_core.messages import HumanMessage from langchain_core.outputs import LLMResult +from langchain_core.runnables import chain as as_runnable from langchain_core.tracers.base import BaseTracer, TracerException from langchain_core.tracers.schemas import Run @@ -627,3 +632,33 @@ def test_tracer_nested_runs_on_error() -> None: assert len(tracer.runs) == 3 for run in tracer.runs: _compare_run_with_error(run, compare_run) + + +def _get_mock_client() -> Client: + mock_session = MagicMock() + client = Client(session=mock_session, api_key="test") + return client + + +def test_traceable_to_tracing() -> None: + has_children = False + + def _collect_run(run: Any) -> None: + nonlocal has_children + has_children = bool(run.child_runs) + + @as_runnable + def foo(x: int) -> int: + return x + 1 + + @traceable + def some_parent(a: int, b: int) -> int: + return foo.invoke(a) + foo.invoke(b) + + mock_client_ = _get_mock_client() + with langsmith.run_helpers.tracing_context(enabled=True): + result = some_parent( + 1, 2, langsmith_extra={"client": mock_client_, "on_end": _collect_run} + ) + assert result == 5 + assert has_children, "Child run not collected" diff --git a/libs/core/tests/unit_tests/tracers/test_langchain.py b/libs/core/tests/unit_tests/tracers/test_langchain.py index 820e547c7a9..a98550afdf6 100644 --- a/libs/core/tests/unit_tests/tracers/test_langchain.py +++ b/libs/core/tests/unit_tests/tracers/test_langchain.py @@ -2,11 +2,13 @@ import threading import time import unittest import unittest.mock +import uuid from typing import Any, Dict from uuid import UUID import pytest from langsmith import Client +from langsmith.run_trees import RunTree from langchain_core.outputs import LLMResult from langchain_core.tracers.langchain import LangChainTracer @@ -59,6 +61,24 @@ def test_example_id_assignment_threadsafe() -> None: assert example_ids == expected_example_ids +def test_tracer_with_run_tree_parent() -> None: + mock_session = unittest.mock.MagicMock() + client = Client(session=mock_session, api_key="test") + parent = RunTree(name="parent", inputs={"input": "foo"}, client=client) + run_id = uuid.uuid4() + tracer = LangChainTracer(client=client) + tracer.run_map[str(parent.id)] = parent # type: ignore + tracer.on_chain_start( + {"name": "child"}, {"input": "bar"}, run_id=run_id, parent_run_id=parent.id + ) + tracer.on_chain_end({}, run_id=run_id) + assert parent.child_runs + assert len(parent.child_runs) == 1 + assert parent.child_runs[0].id == run_id + assert parent.child_runs[0].trace_id == parent.id + assert parent.child_runs[0].parent_run_id == parent.id + + def test_log_lock() -> None: """Test that example assigned at callback start/end is honored."""