mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
* 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>
174 lines
5.8 KiB
Python
174 lines
5.8 KiB
Python
"""LangSmith trace URL utilities."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import uuid
|
|
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 _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 = _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 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, 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_name)
|
|
return f"{project_url}/t/{thread_id}"
|
|
except Exception: # noqa: BLE001
|
|
logger.warning(
|
|
"Failed to build LangSmith trace URL for thread %s", thread_id, exc_info=True
|
|
)
|
|
return None
|
|
|
|
|
|
def _build_langsmith_feedback_clients() -> tuple[LangSmithClient, ...]:
|
|
"""Build feedback clients from current env. Re-read each call so rotated
|
|
keys / late secret hydration are picked up."""
|
|
clients: list[LangSmithClient] = []
|
|
seen: set[tuple[str, str]] = set()
|
|
|
|
api_endpoint = os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
|
|
client_configs = (
|
|
(
|
|
os.environ.get("LANGSMITH_API_KEY") or os.environ.get("LANGCHAIN_API_KEY"),
|
|
api_endpoint,
|
|
),
|
|
(
|
|
os.environ.get("LANGSMITH_API_KEY_PROD"),
|
|
os.environ.get("LANGSMITH_ENDPOINT_PROD", api_endpoint),
|
|
),
|
|
)
|
|
|
|
for api_key, api_url in client_configs:
|
|
if not api_key or not api_url:
|
|
continue
|
|
identity = (api_key, api_url)
|
|
if identity in seen:
|
|
continue
|
|
clients.append(LangSmithClient(api_key=api_key, api_url=api_url))
|
|
seen.add(identity)
|
|
|
|
return tuple(clients)
|
|
|
|
|
|
def _feedback_id(run_id: str, key: str) -> uuid.UUID:
|
|
return uuid.uuid5(uuid.NAMESPACE_URL, f"langsmith-feedback:{run_id}:{key}")
|
|
|
|
|
|
def create_langsmith_feedback(
|
|
run_id: str,
|
|
key: str,
|
|
*,
|
|
score: float,
|
|
comment: str | None = None,
|
|
source_info: dict[str, Any] | None = None,
|
|
) -> bool:
|
|
"""Create or update deterministic feedback on all configured LangSmith clients."""
|
|
clients = _build_langsmith_feedback_clients()
|
|
if not clients:
|
|
logger.warning("No LangSmith API key configured, skipping feedback")
|
|
return False
|
|
|
|
feedback_id = _feedback_id(run_id, key)
|
|
any_success = False
|
|
for client in clients:
|
|
try:
|
|
client.create_feedback(
|
|
run_id=run_id,
|
|
key=key,
|
|
score=score,
|
|
comment=comment,
|
|
source_info=source_info,
|
|
feedback_source_type="api",
|
|
feedback_id=feedback_id,
|
|
)
|
|
any_success = True
|
|
except Exception:
|
|
try:
|
|
client.update_feedback(feedback_id, score=score, comment=comment)
|
|
any_success = True
|
|
except Exception:
|
|
logger.exception("Failed to create or update LangSmith feedback for run %s", run_id)
|
|
return any_success
|
|
|
|
|
|
def delete_langsmith_feedback(run_id: str, key: str) -> bool:
|
|
"""Delete deterministic feedback from all configured LangSmith clients."""
|
|
clients = _build_langsmith_feedback_clients()
|
|
if not clients:
|
|
logger.warning("No LangSmith API key configured, skipping feedback deletion")
|
|
return False
|
|
|
|
feedback_id = _feedback_id(run_id, key)
|
|
any_success = False
|
|
for client in clients:
|
|
try:
|
|
client.delete_feedback(feedback_id)
|
|
any_success = True
|
|
except LangSmithNotFoundError:
|
|
any_success = True
|
|
except Exception:
|
|
logger.exception("Failed to delete LangSmith feedback for run %s", run_id)
|
|
return any_success
|