diff --git a/backend/src/config/tracing_config.py b/backend/src/config/tracing_config.py index 138f5cc..6de9505 100644 --- a/backend/src/config/tracing_config.py +++ b/backend/src/config/tracing_config.py @@ -25,8 +25,48 @@ class TracingConfig(BaseModel): _tracing_config: TracingConfig | None = None +_TRUTHY_VALUES = {"1", "true", "yes", "on"} + + +def _env_flag_preferred(*names: str) -> bool: + """Return the boolean value of the first env var that is present and non-empty. + + Accepted truthy values (case-insensitive): ``1``, ``true``, ``yes``, ``on``. + Any other non-empty value is treated as falsy. If none of the named + variables is set, returns ``False``. + """ + for name in names: + value = os.environ.get(name) + if value is not None and value.strip(): + return value.strip().lower() in _TRUTHY_VALUES + return False + + +def _first_env_value(*names: str) -> str | None: + """Return the first non-empty environment value from candidate names.""" + for name in names: + value = os.environ.get(name) + if value and value.strip(): + return value.strip() + return None + + def get_tracing_config() -> TracingConfig: """Get the current tracing configuration from environment variables. + + ``LANGSMITH_*`` variables take precedence over their legacy ``LANGCHAIN_*`` + counterparts. For boolean flags (``enabled``), the *first* variable that is + present and non-empty in the priority list is the sole authority – its value + is parsed and returned without consulting the remaining candidates. Accepted + truthy values are ``1``, ``true``, ``yes``, and ``on`` (case-insensitive); + any other non-empty value is treated as falsy. + + Priority order: + enabled : LANGSMITH_TRACING > LANGCHAIN_TRACING_V2 > LANGCHAIN_TRACING + api_key : LANGSMITH_API_KEY > LANGCHAIN_API_KEY + project : LANGSMITH_PROJECT > LANGCHAIN_PROJECT (default: "deer-flow") + endpoint : LANGSMITH_ENDPOINT > LANGCHAIN_ENDPOINT (default: https://api.smith.langchain.com) + Returns: TracingConfig with current settings. """ @@ -37,10 +77,11 @@ def get_tracing_config() -> TracingConfig: if _tracing_config is not None: # Double-check after acquiring lock return _tracing_config _tracing_config = TracingConfig( - enabled=os.environ.get("LANGSMITH_TRACING", "").lower() == "true", - api_key=os.environ.get("LANGSMITH_API_KEY"), - project=os.environ.get("LANGSMITH_PROJECT", "deer-flow"), - endpoint=os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com"), + # Keep compatibility with both legacy LANGCHAIN_* and newer LANGSMITH_* variables. + enabled=_env_flag_preferred("LANGSMITH_TRACING", "LANGCHAIN_TRACING_V2", "LANGCHAIN_TRACING"), + api_key=_first_env_value("LANGSMITH_API_KEY", "LANGCHAIN_API_KEY"), + project=_first_env_value("LANGSMITH_PROJECT", "LANGCHAIN_PROJECT") or "deer-flow", + endpoint=_first_env_value("LANGSMITH_ENDPOINT", "LANGCHAIN_ENDPOINT") or "https://api.smith.langchain.com", ) return _tracing_config diff --git a/backend/tests/test_tracing_config.py b/backend/tests/test_tracing_config.py new file mode 100644 index 0000000..c46e885 --- /dev/null +++ b/backend/tests/test_tracing_config.py @@ -0,0 +1,71 @@ +"""Tests for src.config.tracing_config.""" + +from __future__ import annotations + +from src.config import tracing_config as tracing_module + + +def _reset_tracing_cache() -> None: + tracing_module._tracing_config = None + + +def test_prefers_langsmith_env_names(monkeypatch): + monkeypatch.setenv("LANGSMITH_TRACING", "true") + monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_key") + monkeypatch.setenv("LANGSMITH_PROJECT", "smith-project") + monkeypatch.setenv("LANGSMITH_ENDPOINT", "https://smith.example.com") + + _reset_tracing_cache() + cfg = tracing_module.get_tracing_config() + + assert cfg.enabled is True + assert cfg.api_key == "lsv2_key" + assert cfg.project == "smith-project" + assert cfg.endpoint == "https://smith.example.com" + assert tracing_module.is_tracing_enabled() is True + + +def test_falls_back_to_langchain_env_names(monkeypatch): + monkeypatch.delenv("LANGSMITH_TRACING", raising=False) + monkeypatch.delenv("LANGSMITH_API_KEY", raising=False) + monkeypatch.delenv("LANGSMITH_PROJECT", raising=False) + monkeypatch.delenv("LANGSMITH_ENDPOINT", raising=False) + + monkeypatch.setenv("LANGCHAIN_TRACING_V2", "true") + monkeypatch.setenv("LANGCHAIN_API_KEY", "legacy-key") + monkeypatch.setenv("LANGCHAIN_PROJECT", "legacy-project") + monkeypatch.setenv("LANGCHAIN_ENDPOINT", "https://legacy.example.com") + + _reset_tracing_cache() + cfg = tracing_module.get_tracing_config() + + assert cfg.enabled is True + assert cfg.api_key == "legacy-key" + assert cfg.project == "legacy-project" + assert cfg.endpoint == "https://legacy.example.com" + assert tracing_module.is_tracing_enabled() is True + + +def test_langsmith_tracing_false_overrides_langchain_tracing_v2_true(monkeypatch): + """LANGSMITH_TRACING=false must win over LANGCHAIN_TRACING_V2=true.""" + monkeypatch.setenv("LANGSMITH_TRACING", "false") + monkeypatch.setenv("LANGCHAIN_TRACING_V2", "true") + monkeypatch.setenv("LANGSMITH_API_KEY", "some-key") + + _reset_tracing_cache() + cfg = tracing_module.get_tracing_config() + + assert cfg.enabled is False + assert tracing_module.is_tracing_enabled() is False + + +def test_defaults_when_project_not_set(monkeypatch): + monkeypatch.setenv("LANGSMITH_TRACING", "yes") + monkeypatch.setenv("LANGSMITH_API_KEY", "key") + monkeypatch.delenv("LANGSMITH_PROJECT", raising=False) + monkeypatch.delenv("LANGCHAIN_PROJECT", raising=False) + + _reset_tracing_cache() + cfg = tracing_module.get_tracing_config() + + assert cfg.project == "deer-flow"