mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 19:43:15 +00:00
* feat: add Slack reaction feedback to LangSmith Record Slack reaction feedback against explicitly mapped LangGraph runs so user ratings are idempotent and tied to the message they reacted to. * Address review feedback on Slack reaction → LangSmith feedback - langsmith.py: drop lru_cache on _build_langsmith_feedback_clients so rotated keys / late env hydration are picked up; dedupe by (key, url) tuple instead of key alone so the same key pointing at different endpoints (cloud + self-hosted) builds both clients. - langsmith.py: treat LangSmithNotFoundError on delete_feedback as success — out-of-order or redelivered reaction_removed events would otherwise loop forever on Slack's retry policy. - slack_feedback.py: include channel_id in _feedback_key so the same message_ts in two channels can't collide on the same feedback id. - slack_feedback.py: treat conflicting +/- reactions from one user as ambiguous (clear feedback) instead of averaging to a misleading 0.5. - slack_feedback.py + slack.py + webapp.py: gate reaction handling to the user who triggered the run (stored in the slack_run_map mapping alongside run_id). Prevents bystanders in shared channels from polluting eval feedback.
128 lines
4.2 KiB
Python
128 lines
4.2 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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _compose_langsmith_project_url() -> str:
|
|
"""Build the LangSmith project URL base from environment variables."""
|
|
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")
|
|
if not tenant_id or not project_id:
|
|
raise ValueError(
|
|
"LANGSMITH_TENANT_ID_PROD and LANGSMITH_TRACING_PROJECT_ID_PROD must be set"
|
|
)
|
|
return f"{host_url}/o/{tenant_id}/projects/p/{project_id}"
|
|
|
|
|
|
def get_langsmith_trace_url(thread_id: str) -> str | None:
|
|
"""Build the LangSmith thread URL for a given thread ID."""
|
|
try:
|
|
project_url = _compose_langsmith_project_url()
|
|
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
|