mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 01:15:09 +03:00
fix(core): adapt tracer to langsmith ls.address agent addressing
- No `default` project when `LANGSMITH_AGENT_*` env vars are set, so the env address isn't sent beside a project. - A project named in code clears the env address on the run. - `LangChainTracer` takes an `address`, filled from `ls.address(...)` tracing contexts and addressed `@traceable` parents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
94ee02b304
commit
2d47e8dad5
4 files changed
+160
-101
No files matched your search
@@ -2427,7 +2427,7 @@ def _configure(
|
||||
|
||||
from langchain_core.tracers.context import ( # noqa: PLC0415
|
||||
_configure_hooks,
|
||||
_get_tracer_project,
|
||||
_get_tracer_destination,
|
||||
_tracing_v2_is_enabled,
|
||||
tracing_v2_callback_var,
|
||||
)
|
||||
@@ -2505,7 +2505,7 @@ def _configure(
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
tracer_project = _get_tracer_project()
|
||||
tracer_project, tracer_address = _get_tracer_destination()
|
||||
debug = _get_debug()
|
||||
if verbose or debug or tracing_v2_enabled_:
|
||||
if verbose and not any(
|
||||
@@ -2531,6 +2531,7 @@ def _configure(
|
||||
try:
|
||||
handler = LangChainTracer(
|
||||
project_name=tracer_project,
|
||||
address=tracer_address,
|
||||
client=(
|
||||
run_tree.client
|
||||
if run_tree is not None
|
||||
|
||||
@@ -111,10 +111,13 @@ def _get_trace_callbacks(
|
||||
callback_manager: CallbackManager | AsyncCallbackManager | None = None,
|
||||
) -> Callbacks:
|
||||
if _tracing_v2_is_enabled():
|
||||
project_name_ = project_name or _get_tracer_project()
|
||||
address = None
|
||||
if not project_name:
|
||||
project_name, address = _get_tracer_destination()
|
||||
tracer = tracing_v2_callback_var.get() or LangChainTracer(
|
||||
project_name=project_name_,
|
||||
project_name=project_name,
|
||||
example_id=example_id,
|
||||
address=address,
|
||||
)
|
||||
if callback_manager is None:
|
||||
cb = cast("Callbacks", [tracer])
|
||||
@@ -139,24 +142,28 @@ def _tracing_v2_is_enabled() -> bool | Literal["local"]:
|
||||
|
||||
|
||||
def _get_tracer_project() -> str | None:
|
||||
return _get_tracer_destination()[0]
|
||||
|
||||
|
||||
def _get_tracer_destination() -> tuple[str | None, Any]:
|
||||
"""Get the `(project, address)` langsmith's tracing context sends runs to.
|
||||
|
||||
At most one is set. Both are `None` when the `LANGSMITH_AGENT_*` env vars
|
||||
address the runs and nothing is named in code.
|
||||
"""
|
||||
tracing_context = ls_rh.get_tracing_context()
|
||||
run_tree = tracing_context["parent"]
|
||||
if run_tree is None and tracing_context["project_name"] is not None:
|
||||
return cast("str", tracing_context["project_name"])
|
||||
return getattr(
|
||||
run_tree,
|
||||
"session_name",
|
||||
getattr(
|
||||
# Note, if people are trying to nest @traceable functions and the
|
||||
# tracing_v2_enabled context manager, this will likely mess up the
|
||||
# tree structure.
|
||||
tracing_v2_callback_var.get(),
|
||||
"project",
|
||||
# `None` only when the environment addresses runs to a LangSmith
|
||||
# agent and no project is configured.
|
||||
_get_default_project_name(),
|
||||
),
|
||||
)
|
||||
if run_tree is None:
|
||||
if tracing_context["project_name"] is not None:
|
||||
return cast("str", tracing_context["project_name"]), None
|
||||
# Only set on langsmith versions with agent addressing.
|
||||
if (address := tracing_context.get("address")) is not None:
|
||||
return None, address
|
||||
return _get_default_project_name(), None
|
||||
# Note, if people are trying to nest @traceable functions and the
|
||||
# tracing_v2_enabled context manager, this will likely mess up the
|
||||
# tree structure.
|
||||
return run_tree.session_name, getattr(run_tree, "address", None)
|
||||
|
||||
|
||||
_configure_hooks: list[
|
||||
|
||||
@@ -81,24 +81,22 @@ def get_client() -> Client:
|
||||
return rt.get_cached_client()
|
||||
|
||||
|
||||
def _env_addresses_agent() -> bool:
|
||||
"""Whether the `LANGSMITH_AGENT_*` env vars address runs to an agent."""
|
||||
return any(
|
||||
ls_utils.get_env_var(name, namespaces=("LANGSMITH",))
|
||||
for name in ("AGENT_ID", "AGENT_ENVIRONMENT")
|
||||
)
|
||||
|
||||
|
||||
def _get_default_project_name() -> str | None:
|
||||
"""Get the project to trace to when none is passed to the tracer.
|
||||
"""Get the project to trace to when none is named in code.
|
||||
|
||||
When the environment addresses runs to a LangSmith agent
|
||||
(`LANGSMITH_AGENT_ID` / `LANGSMITH_AGENT_ENVIRONMENT`), only a project the
|
||||
user configured is returned. Falling back to the invented `'default'`
|
||||
project would send it alongside the agent, and the API rejects a run
|
||||
addressed to both.
|
||||
|
||||
Agent addressing requires `langsmith>=0.14.0`; older versions have no agent
|
||||
to address, so the helpers are looked up rather than imported.
|
||||
`None` when the `LANGSMITH_AGENT_*` env vars are set: langsmith then sends
|
||||
the run to that agent, and the API rejects a run that also names a project.
|
||||
"""
|
||||
get_agent_id = getattr(ls_utils, "get_tracer_agent_id", None)
|
||||
get_agent_environment = getattr(ls_utils, "get_tracer_agent_environment", None)
|
||||
if (get_agent_id is not None and get_agent_id() is not None) or (
|
||||
get_agent_environment is not None and get_agent_environment() is not None
|
||||
):
|
||||
return ls_utils.get_tracer_project(return_default_value=False)
|
||||
if _env_addresses_agent():
|
||||
return None
|
||||
return ls_utils.get_tracer_project()
|
||||
|
||||
|
||||
@@ -165,6 +163,7 @@ class LangChainTracer(BaseTracer):
|
||||
tags: list[str] | None = None,
|
||||
*,
|
||||
metadata: Mapping[str, str] | None = None,
|
||||
address: Any = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the LangChain tracer.
|
||||
@@ -183,13 +182,25 @@ class LangChainTracer(BaseTracer):
|
||||
metadata: Additional metadata to include if it isn't already in the run.
|
||||
|
||||
Defaults to None.
|
||||
address: A `langsmith.address(...)` to send runs to instead of a
|
||||
project.
|
||||
|
||||
Ignored if `project_name` is set.
|
||||
|
||||
!!! warning "Experimental"
|
||||
|
||||
Requires a langsmith version with agent addressing, which
|
||||
is in beta.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.example_id = (
|
||||
UUID(example_id) if isinstance(example_id, str) else example_id
|
||||
)
|
||||
self.project_name = project_name or _get_default_project_name()
|
||||
self.address = None if project_name else address
|
||||
self.project_name = project_name or (
|
||||
None if address is not None else _get_default_project_name()
|
||||
)
|
||||
self.client = client or get_client()
|
||||
self.tags = tags or []
|
||||
self.latest_run: Run | None = None
|
||||
@@ -232,6 +243,7 @@ class LangChainTracer(BaseTracer):
|
||||
project_name=self.project_name,
|
||||
client=self.client,
|
||||
tags=merged_tags,
|
||||
address=self.address,
|
||||
metadata=merged_metadata,
|
||||
run_map=self.run_map,
|
||||
order_map=self.order_map,
|
||||
@@ -241,6 +253,14 @@ class LangChainTracer(BaseTracer):
|
||||
def _start_trace(self, run: Run) -> None:
|
||||
if self.project_name:
|
||||
run.session_name = self.project_name
|
||||
# Absent on langsmith versions without agent addressing.
|
||||
if "address" in type(run).model_fields:
|
||||
if self.project_name:
|
||||
# A project named in code wins over an address from the env.
|
||||
run.address = None
|
||||
elif self.address is not None:
|
||||
run.session_name = None # type: ignore[assignment,unused-ignore]
|
||||
run.address = self.address
|
||||
if self.tags is not None:
|
||||
if run.tags:
|
||||
run.tags = sorted(set(run.tags + self.tags))
|
||||
|
||||
@@ -3,12 +3,13 @@ import threading
|
||||
import time
|
||||
import unittest.mock
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import langsmith
|
||||
import pytest
|
||||
from langsmith import Client
|
||||
from langsmith import utils as ls_utils
|
||||
from langsmith.run_helpers import tracing_context
|
||||
from langsmith.run_trees import RunTree
|
||||
from langsmith.utils import get_env_var, get_tracer_project
|
||||
@@ -157,21 +158,41 @@ def test_correct_get_tracer_project(
|
||||
assert projects == [expected_project_name]
|
||||
|
||||
|
||||
def _clear_tracer_env_caches() -> None:
|
||||
getters: tuple[Any, ...] = (
|
||||
get_env_var,
|
||||
get_tracer_project,
|
||||
getattr(ls_utils, "get_tracer_agent_id", None),
|
||||
getattr(ls_utils, "get_tracer_agent_environment", None),
|
||||
)
|
||||
for getter in getters:
|
||||
if hasattr(getter, "cache_clear"):
|
||||
getter.cache_clear()
|
||||
_TRACER_ENV_VARS = (
|
||||
"LANGSMITH_PROJECT",
|
||||
"LANGCHAIN_PROJECT",
|
||||
"LANGCHAIN_SESSION",
|
||||
"HOSTED_LANGSERVE_PROJECT_NAME",
|
||||
"LANGSMITH_AGENT_ID",
|
||||
"LANGSMITH_AGENT_ENVIRONMENT",
|
||||
)
|
||||
|
||||
_requires_address = pytest.mark.skipif(
|
||||
not hasattr(langsmith, "address"),
|
||||
reason="agent addressing requires a langsmith version with `ls.address`",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tracer_env() -> Iterator[pytest.MonkeyPatch]:
|
||||
"""Start from no tracer env vars, and leave nothing in the env caches."""
|
||||
_clear_env_caches()
|
||||
with pytest.MonkeyPatch.context() as mp:
|
||||
for k in _TRACER_ENV_VARS:
|
||||
mp.delenv(k, raising=False)
|
||||
yield mp
|
||||
_clear_env_caches()
|
||||
|
||||
|
||||
def _clear_env_caches() -> None:
|
||||
get_env_var.cache_clear() # type: ignore[attr-defined]
|
||||
get_tracer_project.cache_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("envvars", "expected_project_name"),
|
||||
[
|
||||
({}, "default"),
|
||||
({"LANGSMITH_AGENT_ID": "my-agent"}, None),
|
||||
({"LANGSMITH_AGENT_ENVIRONMENT": "development"}, None),
|
||||
(
|
||||
@@ -187,70 +208,80 @@ def _clear_tracer_env_caches() -> None:
|
||||
"LANGSMITH_AGENT_ENVIRONMENT": "development",
|
||||
"LANGSMITH_PROJECT": "configured",
|
||||
},
|
||||
"configured",
|
||||
None,
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"no 'default' project with agent id",
|
||||
"no 'default' project with agent environment",
|
||||
"no 'default' project with agent pair",
|
||||
"keep a configured project with agent pair",
|
||||
"'default' project without agent env vars",
|
||||
"no project with agent id",
|
||||
"no project with agent environment",
|
||||
"no project with agent pair",
|
||||
"no env project with agent pair",
|
||||
],
|
||||
)
|
||||
def test_tracer_project_with_agent_addressing(
|
||||
envvars: dict[str, str], expected_project_name: str | None
|
||||
def test_tracer_project_with_agent_env(
|
||||
tracer_env: pytest.MonkeyPatch,
|
||||
envvars: dict[str, str],
|
||||
expected_project_name: str | None,
|
||||
) -> None:
|
||||
if not hasattr(ls_utils, "get_tracer_agent_environment"):
|
||||
pytest.skip("agent addressing requires langsmith>=0.14.0")
|
||||
_clear_tracer_env_caches()
|
||||
try:
|
||||
with pytest.MonkeyPatch.context() as mp:
|
||||
for k in (
|
||||
"LANGSMITH_PROJECT",
|
||||
"LANGCHAIN_PROJECT",
|
||||
"LANGCHAIN_SESSION",
|
||||
"HOSTED_LANGSERVE_PROJECT_NAME",
|
||||
"LANGSMITH_AGENT_ID",
|
||||
"LANGSMITH_AGENT_ENVIRONMENT",
|
||||
):
|
||||
mp.delenv(k, raising=False)
|
||||
for k, v in envvars.items():
|
||||
mp.setenv(k, v)
|
||||
|
||||
tracer = LangChainTracer(client=unittest.mock.MagicMock(spec=Client))
|
||||
assert tracer.project_name == expected_project_name
|
||||
finally:
|
||||
# The env getters are cached, so leave nothing behind for later tests.
|
||||
_clear_tracer_env_caches()
|
||||
for k, v in envvars.items():
|
||||
tracer_env.setenv(k, v)
|
||||
tracer = LangChainTracer(client=unittest.mock.MagicMock(spec=Client))
|
||||
assert tracer.project_name == expected_project_name
|
||||
|
||||
|
||||
def test_automatic_tracing_omits_default_project_with_agent_addressing() -> None:
|
||||
if not hasattr(ls_utils, "get_tracer_agent_environment"):
|
||||
pytest.skip("agent addressing requires langsmith>=0.14.0")
|
||||
_clear_tracer_env_caches()
|
||||
try:
|
||||
with pytest.MonkeyPatch.context() as mp:
|
||||
for k in (
|
||||
"LANGSMITH_PROJECT",
|
||||
"LANGCHAIN_PROJECT",
|
||||
"LANGCHAIN_SESSION",
|
||||
"HOSTED_LANGSERVE_PROJECT_NAME",
|
||||
):
|
||||
mp.delenv(k, raising=False)
|
||||
mp.setenv("LANGSMITH_TRACING", "true")
|
||||
mp.setenv("LANGSMITH_AGENT_ID", "my-agent")
|
||||
mp.setenv("LANGSMITH_AGENT_ENVIRONMENT", "development")
|
||||
def _posted_destinations(client: unittest.mock.MagicMock) -> list[tuple[Any, Any]]:
|
||||
return [
|
||||
(call.kwargs.get("session_name"), call.kwargs.get("address"))
|
||||
for call in client.create_run.call_args_list
|
||||
]
|
||||
|
||||
client = unittest.mock.MagicMock(spec=Client)
|
||||
with tracing_context(client=client):
|
||||
RunnableLambda(lambda x: x).invoke(1)
|
||||
|
||||
assert client.create_run.call_count == 1
|
||||
posted = client.create_run.call_args.kwargs
|
||||
assert posted.get("session_name") is None
|
||||
assert posted.get("agent_id") == "my-agent"
|
||||
finally:
|
||||
_clear_tracer_env_caches()
|
||||
def _nested_chain() -> RunnableLambda[int, int]:
|
||||
inner: RunnableLambda[int, int] = RunnableLambda(lambda x: x)
|
||||
return RunnableLambda(inner.invoke)
|
||||
|
||||
|
||||
@_requires_address
|
||||
def test_automatic_tracing_uses_agent_env(tracer_env: pytest.MonkeyPatch) -> None:
|
||||
tracer_env.setenv("LANGSMITH_TRACING", "true")
|
||||
tracer_env.setenv("LANGSMITH_AGENT_ID", "my-agent")
|
||||
tracer_env.setenv("LANGSMITH_AGENT_ENVIRONMENT", "development")
|
||||
client = unittest.mock.MagicMock(spec=Client)
|
||||
with tracing_context(client=client):
|
||||
_nested_chain().invoke(1)
|
||||
expected = langsmith.address(agent_id="my-agent", agent_environment="development")
|
||||
assert _posted_destinations(client) == [(None, expected)] * 2
|
||||
|
||||
|
||||
@_requires_address
|
||||
def test_project_in_code_wins_over_agent_env(tracer_env: pytest.MonkeyPatch) -> None:
|
||||
tracer_env.setenv("LANGSMITH_AGENT_ID", "my-agent")
|
||||
tracer_env.setenv("LANGSMITH_AGENT_ENVIRONMENT", "development")
|
||||
client = unittest.mock.MagicMock(spec=Client)
|
||||
tracer = LangChainTracer(client=client, project_name="configured")
|
||||
_nested_chain().invoke(1, {"callbacks": [tracer]})
|
||||
assert _posted_destinations(client) == [("configured", None)] * 2
|
||||
|
||||
|
||||
@_requires_address
|
||||
def test_address_from_tracing_context(tracer_env: pytest.MonkeyPatch) -> None:
|
||||
tracer_env.setenv("LANGSMITH_TRACING", "true")
|
||||
client = unittest.mock.MagicMock(spec=Client)
|
||||
support = langsmith.address(agent_id="support", agent_environment="staging")
|
||||
with support.tracing_context(client=client):
|
||||
_nested_chain().invoke(1)
|
||||
assert _posted_destinations(client) == [(None, support)] * 2
|
||||
|
||||
|
||||
@_requires_address
|
||||
def test_address_from_traceable_parent(tracer_env: pytest.MonkeyPatch) -> None:
|
||||
tracer_env.setenv("LANGSMITH_TRACING", "true")
|
||||
client = unittest.mock.MagicMock(spec=Client)
|
||||
support = langsmith.address(agent_id="support", agent_environment="staging")
|
||||
with support.trace("parent", client=client):
|
||||
_nested_chain().invoke(1)
|
||||
assert _posted_destinations(client) == [(None, support)] * 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in new issue
Block a user