From 889b2663d95249fb05a5631cf57c69cdba84abe5 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Sat, 13 Jun 2026 09:37:15 -0700 Subject: [PATCH] fix: point View trace link at the correct tracing project (#1521) * fix: resolve trace URL project id by tracing project name Graphs were split into separate LangSmith tracing projects (open-swe-agent, open-swe-review) but the "View trace" link still used a single fixed project-id env var pointing at the old combined project. Resolve the project id from the tracing project name so agent and reviewer links point at their respective projects, falling back to the env var when resolution is unavailable. Co-authored-by: open-swe[bot] * docs: document per-graph tracing projects for trace links View trace links now resolve project IDs from the open-swe-agent / open-swe-review project names. Document this so fresh deployments create the right projects instead of relying on a single project ID. Co-authored-by: open-swe[bot] --------- Co-authored-by: open-swe[bot] --- INSTALLATION.md | 4 +- agent/tools/publish_review.py | 3 +- agent/utils/langsmith.py | 58 +++++++++++++++++++++++--- tests/test_langsmith_trace_url.py | 68 +++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 tests/test_langsmith_trace_url.py diff --git a/INSTALLATION.md b/INSTALLATION.md index c76287b3..916d6395 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -129,6 +129,8 @@ Open SWE uses [LangSmith](https://smith.langchain.com/) for: 4. Get your **Tenant ID**: Visit LangSmith, login, then copy the UUID in the URL. Example: if your URL is `https://smith.langchain.com/o/72184268-01ea-4d29-98cc-6cfcf0f2abb0/agents/chat` -> the tenant ID would be `72184268-01ea-4d29-98cc-6cfcf0f2abb0`. Save it as `LANGSMITH_TENANT_ID_PROD`. 5. Get your **Project ID**: open your tracing project in LangSmith, then click on the **ID** button in the top left, directly next to the project name. Save it as `LANGSMITH_TRACING_PROJECT_ID_PROD` +> **Note on per-graph tracing projects.** The graphs trace into separate projects by name — `open-swe-agent` (main agent) and `open-swe-review` (reviewer/analyzer). "View trace" links resolve the correct project ID from these names automatically (via the `LANGSMITH_API_KEY_PROD` client), so make sure projects with these names exist in your tenant. If a name can't be resolved, links fall back to `LANGSMITH_TRACING_PROJECT_ID_PROD`, so set it to whichever project you want links to point at by default. + ### 4b. Configure GitHub OAuth (optional but recommended) This is the **agent-runtime** OAuth provider: it lets each agent run authenticate with the triggering user's own GitHub account, brokered by LangSmith. (It is separate from the dashboard-login OAuth, which uses `GITHUB_APP_CLIENT_ID`/`GITHUB_APP_CLIENT_SECRET` directly — see step 3c.) Without it, all agent operations use the GitHub App's installation token (a shared bot identity). @@ -399,7 +401,7 @@ LANGSMITH_API_KEY_PROD="" # From step 4a LANGCHAIN_TRACING_V2="true" LANGCHAIN_PROJECT="" # LangSmith project name for traces LANGSMITH_TENANT_ID_PROD="" -LANGSMITH_TRACING_PROJECT_ID_PROD="" +LANGSMITH_TRACING_PROJECT_ID_PROD="" # Fallback project ID for "View trace" links; graphs trace into the open-swe-agent / open-swe-review projects by name LANGSMITH_URL_PROD="https://smith.langchain.com" # === LLM === diff --git a/agent/tools/publish_review.py b/agent/tools/publish_review.py index 19f330ab..c15b90ec 100644 --- a/agent/tools/publish_review.py +++ b/agent/tools/publish_review.py @@ -52,6 +52,7 @@ from ..utils.github_token import ( ) from ..utils.langsmith import get_langsmith_trace_url from ..utils.slack import post_slack_thread_reply +from ..utils.tracing import REVIEW_TRACING_PROJECT def publish_review( @@ -175,7 +176,7 @@ async def _resolve_review_trace_url(thread_id: str, config_override: object) -> return None if not thread_id: return None - return get_langsmith_trace_url(thread_id) + return get_langsmith_trace_url(thread_id, project_name=REVIEW_TRACING_PROJECT) def _is_reviewer_eval_mode(configurable: dict[str, Any]) -> bool: diff --git a/agent/utils/langsmith.py b/agent/utils/langsmith.py index 48a7ae86..c0af0924 100644 --- a/agent/utils/langsmith.py +++ b/agent/utils/langsmith.py @@ -10,25 +10,71 @@ from typing import Any from langsmith import Client as LangSmithClient from langsmith.utils import LangSmithNotFoundError +from .tracing import AGENT_TRACING_PROJECT + logger = logging.getLogger(__name__) +_PROJECT_ID_CACHE: dict[str, str] = {} -def _compose_langsmith_project_url() -> str: - """Build the LangSmith project URL base from environment variables.""" + +def _build_prod_langsmith_client() -> LangSmithClient | None: + """Build a LangSmith client scoped to the prod tenant for project lookups.""" + api_key = ( + os.environ.get("LANGSMITH_API_KEY_PROD") + or os.environ.get("LANGSMITH_API_KEY") + or os.environ.get("LANGCHAIN_API_KEY") + ) + if not api_key: + return None + api_url = os.environ.get("LANGSMITH_ENDPOINT_PROD") or os.environ.get( + "LANGSMITH_ENDPOINT", "https://api.smith.langchain.com" + ) + return LangSmithClient(api_key=api_key, api_url=api_url) + + +def _resolve_project_id_by_name(project_name: str) -> str | None: + """Resolve a LangSmith project id from its name, caching successful lookups.""" + cached = _PROJECT_ID_CACHE.get(project_name) + if cached: + return cached + client = _build_prod_langsmith_client() + if client is None: + return None + try: + project = client.read_project(project_name=project_name) + except LangSmithNotFoundError: + return None + except Exception: # noqa: BLE001 + logger.warning("Failed to resolve LangSmith project id for %s", project_name, exc_info=True) + return None + project_id = getattr(project, "id", None) + if not project_id: + return None + resolved = str(project_id) + _PROJECT_ID_CACHE[project_name] = resolved + return resolved + + +def _compose_langsmith_project_url(project_name: str = AGENT_TRACING_PROJECT) -> str: + """Build the LangSmith project URL base for a given tracing project name.""" host_url = os.environ.get("LANGSMITH_URL_PROD", "https://smith.langchain.com") tenant_id = os.environ.get("LANGSMITH_TENANT_ID_PROD") - project_id = os.environ.get("LANGSMITH_TRACING_PROJECT_ID_PROD") + project_id = _resolve_project_id_by_name(project_name) or os.environ.get( + "LANGSMITH_TRACING_PROJECT_ID_PROD" + ) if not tenant_id or not project_id: raise ValueError( - "LANGSMITH_TENANT_ID_PROD and LANGSMITH_TRACING_PROJECT_ID_PROD must be set" + "LANGSMITH_TENANT_ID_PROD must be set and the tracing project id must be resolvable" ) return f"{host_url}/o/{tenant_id}/projects/p/{project_id}" -def get_langsmith_trace_url(thread_id: str) -> str | None: +def get_langsmith_trace_url( + thread_id: str, project_name: str = AGENT_TRACING_PROJECT +) -> str | None: """Build the LangSmith thread URL for a given thread ID.""" try: - project_url = _compose_langsmith_project_url() + project_url = _compose_langsmith_project_url(project_name) return f"{project_url}/t/{thread_id}" except Exception: # noqa: BLE001 logger.warning( diff --git a/tests/test_langsmith_trace_url.py b/tests/test_langsmith_trace_url.py new file mode 100644 index 00000000..22310072 --- /dev/null +++ b/tests/test_langsmith_trace_url.py @@ -0,0 +1,68 @@ +import pytest + +from agent.utils import langsmith as ls_utils +from agent.utils.tracing import AGENT_TRACING_PROJECT, REVIEW_TRACING_PROJECT + + +@pytest.fixture(autouse=True) +def _clear_cache() -> None: + ls_utils._PROJECT_ID_CACHE.clear() + + +def _set_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_URL_PROD", "https://smith.example") + monkeypatch.setenv("LANGSMITH_TENANT_ID_PROD", "tenant-1") + monkeypatch.delenv("LANGSMITH_TRACING_PROJECT_ID_PROD", raising=False) + + +def test_trace_url_resolves_project_id_by_name(monkeypatch: pytest.MonkeyPatch) -> None: + _set_env(monkeypatch) + monkeypatch.setattr( + ls_utils, + "_resolve_project_id_by_name", + lambda name: "agent-pid" if name == AGENT_TRACING_PROJECT else "review-pid", + ) + + agent_url = ls_utils.get_langsmith_trace_url("t1") + review_url = ls_utils.get_langsmith_trace_url("t2", project_name=REVIEW_TRACING_PROJECT) + + assert agent_url == "https://smith.example/o/tenant-1/projects/p/agent-pid/t/t1" + assert review_url == "https://smith.example/o/tenant-1/projects/p/review-pid/t/t2" + + +def test_trace_url_falls_back_to_env_project_id(monkeypatch: pytest.MonkeyPatch) -> None: + _set_env(monkeypatch) + monkeypatch.setenv("LANGSMITH_TRACING_PROJECT_ID_PROD", "env-pid") + monkeypatch.setattr(ls_utils, "_resolve_project_id_by_name", lambda name: None) + + url = ls_utils.get_langsmith_trace_url("t3") + + assert url == "https://smith.example/o/tenant-1/projects/p/env-pid/t/t3" + + +def test_trace_url_none_when_unresolvable(monkeypatch: pytest.MonkeyPatch) -> None: + _set_env(monkeypatch) + monkeypatch.setattr(ls_utils, "_resolve_project_id_by_name", lambda name: None) + + assert ls_utils.get_langsmith_trace_url("t4") is None + + +def test_resolve_project_id_caches_success(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[str] = [] + + class _FakeProject: + id = "pid-123" + + class _FakeClient: + def read_project(self, *, project_name: str) -> _FakeProject: + calls.append(project_name) + return _FakeProject() + + monkeypatch.setattr(ls_utils, "_build_prod_langsmith_client", lambda: _FakeClient()) + + first = ls_utils._resolve_project_id_by_name(AGENT_TRACING_PROJECT) + second = ls_utils._resolve_project_id_by_name(AGENT_TRACING_PROJECT) + + assert first == "pid-123" + assert second == "pid-123" + assert calls == [AGENT_TRACING_PROJECT]