mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
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] <open-swe@users.noreply.github.com> * 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] <open-swe@users.noreply.github.com> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
38dcc37040
commit
889b2663d9
4 changed files with 125 additions and 8 deletions
|
|
@ -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 ===
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
68
tests/test_langsmith_trace_url.py
Normal file
68
tests/test_langsmith_trace_url.py
Normal file
|
|
@ -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]
|
||||
Loading…
Add table
Reference in a new issue