mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
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:
parent
d7d9bc5179
commit
b64d871913
5 changed files with 83 additions and 8 deletions
|
|
@ -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
30
agent/utils/langsmith.py
Normal 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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue