feat: send LangSmith trace URL on run trigger from Slack and Linear (#1057)

* feat: send LangSmith trace URL on run trigger from Slack and Linear

* fix: check LANGSMITH_PROJECT before LANGSMITH_PROJECT_PROD for project name lookup

* fix: pass LangSmith API key explicitly to Client and remove global variable

* Update agent/utils/langsmith.py

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* refactor: move trace notification helpers out of webapp and use env vars for LangSmith URL base

* docs: add LangSmith tenant and project ID env vars to installation guide

* fix: remove unused comment_on_linear_issue import from webapp

* nit: remove lru_cache, make get_langsmith_trace_url sync, and move langsmith import to top level

* Update INSTALLATION.md

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* Update INSTALLATION.md

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* Update INSTALLATION.md

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
Aran Yogesh 2026-03-18 12:17:45 -07:00 • committed by GitHub
parent d7d9bc5179
commit b64d871913
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 83 additions and 8 deletions

View file

@ -1,3 +1,4 @@
# Installation Guide
This guide walks you through setting up Open SWE end-to-end: local development, GitHub App creation, LangSmith configuration, webhooks, and production deployment.
@ -105,11 +106,13 @@ Open SWE uses [LangSmith](https://smith.langchain.com/) for:
- **Tracing**: all agent runs are logged for debugging and observability
- **Sandboxes**: each task runs in an isolated LangSmith cloud sandbox
### 4a. Get your API key
### 4a. Get your API key, project and tenant IDs
1. Create a [LangSmith account](https://smith.langchain.com/) if you don't have one
2. Go to **Settings → API Keys → Create API Key**
3. Save it as `LANGSMITH_API_KEY_PROD`
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`
### 4b. Configure GitHub OAuth (optional but recommended)
@ -300,6 +303,9 @@ Create a `.env` file in the project root. Below is the full list — only fill i
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_URL_PROD="https://smith.langchain.com"
# === LLM ===
ANTHROPIC_API_KEY="" # Anthropic API key (default provider)

30
agent/utils/langsmith.py Normal file
View file

@ -0,0 +1,30 @@
"""LangSmith trace URL utilities."""
from __future__ import annotations
import logging
import os
logger = logging.getLogger(__name__)
def _compose_langsmith_url_base() -> str:
"""Build the LangSmith 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}/r"
def get_langsmith_trace_url(run_id: str) -> str | None:
"""Build the LangSmith trace URL for a given run ID."""
try:
url_base = _compose_langsmith_url_base()
return f"{url_base}/{run_id}?poll=true"
except Exception: # noqa: BLE001
logger.warning("Failed to build LangSmith trace URL for run %s", run_id, exc_info=True)
return None

View file

@ -7,17 +7,22 @@ import os
import httpx
from agent.utils.langsmith import get_langsmith_trace_url
logger = logging.getLogger(__name__)
LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "")
async def comment_on_linear_issue(issue_id: str, comment_body: str) -> bool:
"""Add a comment to a Linear issue.
async def comment_on_linear_issue(
issue_id: str, comment_body: str, parent_id: str | None = None
) -> bool:
"""Add a comment to a Linear issue, optionally as a reply to a specific comment.
Args:
issue_id: The Linear issue ID
comment_body: The comment text
parent_id: Optional comment ID to reply to
Returns:
True if successful, False otherwise
@ -28,8 +33,8 @@ async def comment_on_linear_issue(issue_id: str, comment_body: str) -> bool:
url = "https://api.linear.app/graphql"
mutation = """
mutation CommentCreate($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
mutation CommentCreate($issueId: String!, $body: String!, $parentId: String) {
commentCreate(input: { issueId: $issueId, body: $body, parentId: $parentId }) {
success
comment {
id
@ -48,7 +53,11 @@ async def comment_on_linear_issue(issue_id: str, comment_body: str) -> bool:
},
json={
"query": mutation,
"variables": {"issueId": issue_id, "body": comment_body},
"variables": {
"issueId": issue_id,
"body": comment_body,
"parentId": parent_id,
},
},
)
response.raise_for_status()
@ -56,3 +65,14 @@ async def comment_on_linear_issue(issue_id: str, comment_body: str) -> bool:
return bool(result.get("data", {}).get("commentCreate", {}).get("success"))
except Exception: # noqa: BLE001
return False
async def post_linear_trace_comment(issue_id: str, run_id: str, triggering_comment_id: str) -> None:
"""Post a trace URL comment on a Linear issue."""
trace_url = get_langsmith_trace_url(run_id)
if trace_url:
await comment_on_linear_issue(
issue_id,
f"On it! [View trace]({trace_url})",
parent_id=triggering_comment_id or None,
)

View file

@ -12,6 +12,8 @@ from typing import Any
import httpx
from agent.utils.langsmith import get_langsmith_trace_url
logger = logging.getLogger(__name__)
SLACK_API_BASE_URL = "https://slack.com/api"
@ -355,3 +357,12 @@ async def fetch_slack_thread_messages(channel_id: str, thread_ts: str) -> list[d
messages.sort(key=lambda item: _parse_ts(item.get("ts")))
return messages
async def post_slack_trace_reply(channel_id: str, thread_ts: str, run_id: str) -> None:
"""Post a trace URL reply in a Slack thread."""
trace_url = get_langsmith_trace_url(run_id)
if trace_url:
await post_slack_thread_reply(
channel_id, thread_ts, f"Working on it! <{trace_url}|View trace>"
)

View file

@ -36,6 +36,7 @@ from .utils.github_comments import (
)
from .utils.github_token import get_github_token_from_thread
from .utils.github_user_email_map import GITHUB_USER_EMAIL_MAP
from .utils.linear import post_linear_trace_comment
from .utils.linear_team_repo_map import LINEAR_TEAM_TO_REPO
from .utils.multimodal import dedupe_urls, extract_image_urls, fetch_image_block
from .utils.slack import (
@ -45,6 +46,7 @@ from .utils.slack import (
get_slack_user_info,
get_slack_user_names,
post_slack_thread_reply,
post_slack_trace_reply,
select_slack_context_messages,
strip_bot_mention,
verify_slack_signature,
@ -680,12 +682,16 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
if queued:
logger.info("Message queued for thread %s, will be processed by middleware", thread_id)
langgraph_client = get_client(url=LANGGRAPH_URL)
runs = await langgraph_client.runs.list(thread_id, limit=1)
if runs:
await post_linear_trace_comment(issue_id, runs[0]["run_id"], triggering_comment_id)
else:
logger.error("Failed to queue message for thread %s", thread_id)
else:
logger.info("Creating LangGraph run for thread %s", thread_id)
langgraph_client = get_client(url=LANGGRAPH_URL)
await langgraph_client.runs.create(
run = await langgraph_client.runs.create(
thread_id,
"agent",
input={"messages": [{"role": "user", "content": content_blocks}]},
@ -693,6 +699,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
if_not_exists="create",
)
logger.info("LangGraph run created successfully for thread %s", thread_id)
await post_linear_trace_comment(issue_id, run["run_id"], triggering_comment_id)
async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[str, str]) -> None:
@ -800,7 +807,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
langgraph_client = get_client(url=LANGGRAPH_URL)
await _upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client)
await langgraph_client.runs.create(
run = await langgraph_client.runs.create(
thread_id,
"agent",
input={"messages": [{"role": "user", "content": content_blocks}]},
@ -808,6 +815,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
if_not_exists="create",
multitask_strategy="interrupt",
)
await post_slack_trace_reply(channel_id, thread_ts, run["run_id"])
def verify_linear_signature(body: bytes, signature: str, secret: str) -> bool: