mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 07:12:12 +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
|
# 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.
|
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
|
- **Tracing**: all agent runs are logged for debugging and observability
|
||||||
- **Sandboxes**: each task runs in an isolated LangSmith cloud sandbox
|
- **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
|
1. Create a [LangSmith account](https://smith.langchain.com/) if you don't have one
|
||||||
2. Go to **Settings → API Keys → Create API Key**
|
2. Go to **Settings → API Keys → Create API Key**
|
||||||
3. Save it as `LANGSMITH_API_KEY_PROD`
|
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)
|
### 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
|
LANGSMITH_API_KEY_PROD="" # From step 4a
|
||||||
LANGCHAIN_TRACING_V2="true"
|
LANGCHAIN_TRACING_V2="true"
|
||||||
LANGCHAIN_PROJECT="" # LangSmith project name for traces
|
LANGCHAIN_PROJECT="" # LangSmith project name for traces
|
||||||
|
LANGSMITH_TENANT_ID_PROD=""
|
||||||
|
LANGSMITH_TRACING_PROJECT_ID_PROD=""
|
||||||
|
LANGSMITH_URL_PROD="https://smith.langchain.com"
|
||||||
|
|
||||||
# === LLM ===
|
# === LLM ===
|
||||||
ANTHROPIC_API_KEY="" # Anthropic API key (default provider)
|
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
|
import httpx
|
||||||
|
|
||||||
|
from agent.utils.langsmith import get_langsmith_trace_url
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "")
|
LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
async def comment_on_linear_issue(issue_id: str, comment_body: str) -> bool:
|
async def comment_on_linear_issue(
|
||||||
"""Add a comment to a 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:
|
Args:
|
||||||
issue_id: The Linear issue ID
|
issue_id: The Linear issue ID
|
||||||
comment_body: The comment text
|
comment_body: The comment text
|
||||||
|
parent_id: Optional comment ID to reply to
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if successful, False otherwise
|
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"
|
url = "https://api.linear.app/graphql"
|
||||||
|
|
||||||
mutation = """
|
mutation = """
|
||||||
mutation CommentCreate($issueId: String!, $body: String!) {
|
mutation CommentCreate($issueId: String!, $body: String!, $parentId: String) {
|
||||||
commentCreate(input: { issueId: $issueId, body: $body }) {
|
commentCreate(input: { issueId: $issueId, body: $body, parentId: $parentId }) {
|
||||||
success
|
success
|
||||||
comment {
|
comment {
|
||||||
id
|
id
|
||||||
|
|
@ -48,7 +53,11 @@ async def comment_on_linear_issue(issue_id: str, comment_body: str) -> bool:
|
||||||
},
|
},
|
||||||
json={
|
json={
|
||||||
"query": mutation,
|
"query": mutation,
|
||||||
"variables": {"issueId": issue_id, "body": comment_body},
|
"variables": {
|
||||||
|
"issueId": issue_id,
|
||||||
|
"body": comment_body,
|
||||||
|
"parentId": parent_id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
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"))
|
return bool(result.get("data", {}).get("commentCreate", {}).get("success"))
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
return False
|
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
|
import httpx
|
||||||
|
|
||||||
|
from agent.utils.langsmith import get_langsmith_trace_url
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SLACK_API_BASE_URL = "https://slack.com/api"
|
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")))
|
messages.sort(key=lambda item: _parse_ts(item.get("ts")))
|
||||||
return messages
|
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_token import get_github_token_from_thread
|
||||||
from .utils.github_user_email_map import GITHUB_USER_EMAIL_MAP
|
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.linear_team_repo_map import LINEAR_TEAM_TO_REPO
|
||||||
from .utils.multimodal import dedupe_urls, extract_image_urls, fetch_image_block
|
from .utils.multimodal import dedupe_urls, extract_image_urls, fetch_image_block
|
||||||
from .utils.slack import (
|
from .utils.slack import (
|
||||||
|
|
@ -45,6 +46,7 @@ from .utils.slack import (
|
||||||
get_slack_user_info,
|
get_slack_user_info,
|
||||||
get_slack_user_names,
|
get_slack_user_names,
|
||||||
post_slack_thread_reply,
|
post_slack_thread_reply,
|
||||||
|
post_slack_trace_reply,
|
||||||
select_slack_context_messages,
|
select_slack_context_messages,
|
||||||
strip_bot_mention,
|
strip_bot_mention,
|
||||||
verify_slack_signature,
|
verify_slack_signature,
|
||||||
|
|
@ -680,12 +682,16 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
|
||||||
|
|
||||||
if queued:
|
if queued:
|
||||||
logger.info("Message queued for thread %s, will be processed by middleware", thread_id)
|
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:
|
else:
|
||||||
logger.error("Failed to queue message for thread %s", thread_id)
|
logger.error("Failed to queue message for thread %s", thread_id)
|
||||||
else:
|
else:
|
||||||
logger.info("Creating LangGraph run for thread %s", thread_id)
|
logger.info("Creating LangGraph run for thread %s", thread_id)
|
||||||
langgraph_client = get_client(url=LANGGRAPH_URL)
|
langgraph_client = get_client(url=LANGGRAPH_URL)
|
||||||
await langgraph_client.runs.create(
|
run = await langgraph_client.runs.create(
|
||||||
thread_id,
|
thread_id,
|
||||||
"agent",
|
"agent",
|
||||||
input={"messages": [{"role": "user", "content": content_blocks}]},
|
input={"messages": [{"role": "user", "content": content_blocks}]},
|
||||||
|
|
@ -693,6 +699,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
|
||||||
if_not_exists="create",
|
if_not_exists="create",
|
||||||
)
|
)
|
||||||
logger.info("LangGraph run created successfully for thread %s", thread_id)
|
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:
|
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)
|
langgraph_client = get_client(url=LANGGRAPH_URL)
|
||||||
await _upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client)
|
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,
|
thread_id,
|
||||||
"agent",
|
"agent",
|
||||||
input={"messages": [{"role": "user", "content": content_blocks}]},
|
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",
|
if_not_exists="create",
|
||||||
multitask_strategy="interrupt",
|
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:
|
def verify_linear_signature(body: bytes, signature: str, secret: str) -> bool:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue