mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-23 20:23:59 +00:00
feat(core): Update inheritance behavior for tracer metadata for special keys (#36900)
JS equivalent: https://github.com/langchain-ai/langchainjs/pull/10733
This commit is contained in:
@@ -36,6 +36,22 @@ logger = logging.getLogger(__name__)
|
||||
_LOGGED = set()
|
||||
_EXECUTOR: ThreadPoolExecutor | None = None
|
||||
|
||||
OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS: frozenset[str] = frozenset(
|
||||
{"ls_agent_type"}
|
||||
)
|
||||
"""Allowlist of LangSmith-only tracing metadata keys that bypass the default
|
||||
"first wins" merge semantics used when propagating tracer metadata to nested
|
||||
runs.
|
||||
|
||||
Keys in this set are ALWAYS overridden by the nearest enclosing tracer config,
|
||||
so nested callers (e.g. a subagent) can replace a value inherited from an
|
||||
ancestor.
|
||||
|
||||
Keep this list very small: every key here loses the default "first wins"
|
||||
protection and is always clobbered by the nearest enclosing tracer config.
|
||||
Only keys that are strictly for LangSmith tracing bookkeeping should be added.
|
||||
"""
|
||||
|
||||
|
||||
def log_error_once(method: str, exception: Exception) -> None:
|
||||
"""Log an error once.
|
||||
@@ -176,7 +192,16 @@ class LangChainTracer(BaseTracer):
|
||||
else:
|
||||
merged_metadata = dict(base_metadata)
|
||||
for key, value in metadata.items():
|
||||
if key not in merged_metadata:
|
||||
# For allowlisted LangSmith-only inheritable metadata keys
|
||||
# (e.g. ``ls_agent_type``), nested callers are allowed to
|
||||
# OVERRIDE the value inherited from an ancestor. For all
|
||||
# other keys we keep the existing "first wins" behavior so
|
||||
# that ancestor-provided tracing metadata is not accidentally
|
||||
# clobbered by child runs.
|
||||
if (
|
||||
key not in merged_metadata
|
||||
or key in OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS
|
||||
):
|
||||
merged_metadata[key] = value
|
||||
|
||||
merged_tags = sorted(set(self.tags + tags)) if tags else self.tags
|
||||
@@ -448,7 +473,16 @@ def _patch_missing_metadata(self: LangChainTracer, run: Run) -> None:
|
||||
metadata = run.metadata
|
||||
patched = None
|
||||
for k, v in self.tracing_metadata.items():
|
||||
if k not in metadata:
|
||||
# ``OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS`` are a small,
|
||||
# LangSmith-only allowlist that bypasses the "first wins" merge
|
||||
# so a nested caller (e.g. a subagent) can override a parent-set value.
|
||||
if k not in metadata or k in OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS:
|
||||
# Skip the copy when the value already matches (avoids cloning
|
||||
# the shared dict in the common "already set" case). Use a
|
||||
# ``k in metadata`` guard so a legitimate missing key whose
|
||||
# tracer value happens to be ``None`` is still patched in.
|
||||
if k in metadata and metadata[k] == v:
|
||||
continue
|
||||
if patched is None:
|
||||
# Copy on first miss to avoid mutating the shared dict.
|
||||
patched = {**metadata}
|
||||
|
||||
@@ -807,6 +807,28 @@ class TestPatchMissingMetadata:
|
||||
assert run.metadata["env"] == "staging"
|
||||
assert run.metadata["extra"] == "from_tracer"
|
||||
|
||||
def test_allowlisted_key_overrides_existing_run_metadata(self) -> None:
|
||||
"""Allowlisted LangSmith keys override existing run metadata."""
|
||||
tracer = self._make_tracer(metadata={"ls_agent_type": "subagent"})
|
||||
run = self._make_run(metadata={"ls_agent_type": "root", "other": "keep"})
|
||||
|
||||
_patch_missing_metadata(tracer, run)
|
||||
|
||||
assert run.metadata["ls_agent_type"] == "subagent"
|
||||
assert run.metadata["other"] == "keep"
|
||||
|
||||
def test_allowlisted_key_noop_when_values_match(self) -> None:
|
||||
"""Allowlisted keys do not clone run metadata when the value is unchanged."""
|
||||
original = {"ls_agent_type": "root"}
|
||||
tracer = self._make_tracer(metadata={"ls_agent_type": "root"})
|
||||
run = self._make_run(metadata=original)
|
||||
|
||||
_patch_missing_metadata(tracer, run)
|
||||
|
||||
# No-op: the shared dict should not be replaced with a copy.
|
||||
assert run.extra["metadata"] is original
|
||||
assert run.metadata == {"ls_agent_type": "root"}
|
||||
|
||||
|
||||
class TestTracerMetadataCloning:
|
||||
"""Tests for LangChainTracer metadata cloning helpers."""
|
||||
@@ -901,3 +923,29 @@ class TestTracerMetadataCloning:
|
||||
if copied.tracing_metadata is not None
|
||||
}
|
||||
assert copied_services == {"api", "worker"}
|
||||
|
||||
def test_copy_with_metadata_defaults_regular_keys_first_wins(self) -> None:
|
||||
"""Regular (non-allowlisted) metadata keys keep "first wins" semantics."""
|
||||
tracer = self._make_tracer(metadata={"env": "staging", "service": "orig"})
|
||||
|
||||
copied = tracer.copy_with_metadata_defaults(
|
||||
metadata={"env": "prod", "service": "new"},
|
||||
)
|
||||
|
||||
assert copied.tracing_metadata == {"env": "staging", "service": "orig"}
|
||||
|
||||
def test_copy_with_metadata_defaults_allowlisted_key_overrides(self) -> None:
|
||||
"""Allowlisted LangSmith keys are overridden by nested caller metadata."""
|
||||
tracer = self._make_tracer(
|
||||
metadata={"ls_agent_type": "root", "env": "staging"},
|
||||
)
|
||||
|
||||
copied = tracer.copy_with_metadata_defaults(
|
||||
metadata={"ls_agent_type": "subagent", "env": "prod"},
|
||||
)
|
||||
|
||||
# Allowlisted key is overridden, non-allowlisted keeps first-wins.
|
||||
assert copied.tracing_metadata == {
|
||||
"ls_agent_type": "subagent",
|
||||
"env": "staging",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user