mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
* feat: server-side Datadog/LangSmith observability tools + team creds Add team-wide observability credential settings (Datadog DD_SITE/API/APP keys, LangSmith API key) stored encrypted server-side, with an admin dashboard section to connect/disconnect each provider. When connected, get_agent loads read-only observability tools server-side: Datadog via its hosted MCP server (langchain-mcp-adapters, toolsets=core) and LangSmith read tools (langsmith_get_trace, langsmith_list_runs). Credentials live in the LangGraph server process and are never exposed to the sandbox. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: address review on observability tools Address PR review feedback: - Authorize observability tools per triggering user (admins + the OBSERVABILITY_AUTHORIZED_EMAILS allowlist) so prompt-injected runs from untrusted contributors can't reach team Datadog/LangSmith data. - Use the documented Datadog MCP auth headers DD_API_KEY / DD_APPLICATION_KEY. - Store each provider's credentials under its own store key to avoid a read-modify-write race dropping the other provider on concurrent saves. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: async email resolution in observability authorization gate --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""Server-side, read-only LangSmith tools.
|
|
|
|
Credentials live in team settings (encrypted at rest). The tools run in the
|
|
LangGraph server process and call the LangSmith API directly — the sandbox never
|
|
holds a LangSmith key. The surface is intentionally read-only: fetch a single
|
|
run/trace and list recent runs in a project.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Any
|
|
|
|
from langchain_core.tools import BaseTool, StructuredTool
|
|
|
|
from ..dashboard.team_credentials import LangSmithCredentials, get_langsmith_credentials
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MAX_LIST_RUNS = 50
|
|
|
|
|
|
def _client(creds: LangSmithCredentials):
|
|
from langsmith import Client
|
|
|
|
return Client(api_key=creds.api_key, api_url=creds.endpoint)
|
|
|
|
|
|
def _serialize_run(run: Any) -> dict[str, Any]:
|
|
def _get(name: str) -> Any:
|
|
value = getattr(run, name, None)
|
|
return str(value) if value is not None else None
|
|
|
|
return {
|
|
"id": _get("id"),
|
|
"name": getattr(run, "name", None),
|
|
"run_type": getattr(run, "run_type", None),
|
|
"status": getattr(run, "status", None),
|
|
"error": getattr(run, "error", None),
|
|
"start_time": _get("start_time"),
|
|
"end_time": _get("end_time"),
|
|
"trace_id": _get("trace_id"),
|
|
"inputs": getattr(run, "inputs", None),
|
|
"outputs": getattr(run, "outputs", None),
|
|
}
|
|
|
|
|
|
def _make_tools(creds: LangSmithCredentials) -> list[BaseTool]:
|
|
async def langsmith_get_trace(run_id: str, load_child_runs: bool = False) -> dict[str, Any]:
|
|
"""Fetch a single LangSmith run (trace) by its run ID.
|
|
|
|
Args:
|
|
run_id: The LangSmith run UUID.
|
|
load_child_runs: Include nested child runs when True.
|
|
|
|
Returns:
|
|
Dictionary with the run details, or an error message.
|
|
"""
|
|
try:
|
|
run = await asyncio.to_thread(
|
|
_client(creds).read_run, run_id, load_child_runs=load_child_runs
|
|
)
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("langsmith_get_trace failed", exc_info=True)
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
|
return {"success": True, "run": _serialize_run(run)}
|
|
|
|
async def langsmith_list_runs(
|
|
project_name: str,
|
|
limit: int = 20,
|
|
filter: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""List recent LangSmith runs in a project.
|
|
|
|
Args:
|
|
project_name: The LangSmith project (tracing project) name.
|
|
limit: Maximum runs to return (capped at 50).
|
|
filter: Optional LangSmith filter string (e.g. "eq(status, 'error')").
|
|
|
|
Returns:
|
|
Dictionary with a list of runs, or an error message.
|
|
"""
|
|
capped = max(1, min(limit, _MAX_LIST_RUNS))
|
|
|
|
def _list() -> list[Any]:
|
|
return list(
|
|
_client(creds).list_runs(
|
|
project_name=project_name,
|
|
filter=filter,
|
|
limit=capped,
|
|
)
|
|
)
|
|
|
|
try:
|
|
runs = await asyncio.to_thread(_list)
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("langsmith_list_runs failed", exc_info=True)
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
|
return {"success": True, "runs": [_serialize_run(r) for r in runs]}
|
|
|
|
return [
|
|
StructuredTool.from_function(coroutine=langsmith_get_trace),
|
|
StructuredTool.from_function(coroutine=langsmith_list_runs),
|
|
]
|
|
|
|
|
|
async def load_langsmith_tools() -> list[BaseTool]:
|
|
"""Return read-only LangSmith tools when the team has connected LangSmith."""
|
|
creds = await get_langsmith_credentials()
|
|
if creds is None:
|
|
return []
|
|
return _make_tools(creds)
|