feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
"""Backend for the review page's "chat with this PR" feature.
|
|
|
|
|
|
|
|
|
|
A dedicated, sandbox-less ``chat`` graph (``agent/chat.py``) answers questions
|
|
|
|
|
about one PR. This module mints a per-user chat thread, seeds the PR diff,
|
|
|
|
|
review findings, and an overview as virtual files on the first run, and proxies
|
|
|
|
|
the LangGraph stream/commands/state/history protocol the frontend SDK speaks —
|
|
|
|
|
the chat counterpart of ``thread_api``'s agent proxy, pinned to assistant
|
|
|
|
|
``chat`` and scoped to the review's PR.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
from collections.abc import AsyncIterator
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
refactor: consolidate reviewer modules into agent/review/
Part of the domain-reorg adoption (build plan step C2): fork content,
upstream layout. Nine 1:1 module moves (reviewer_diff/eval_store/
findings/groups/publish/reconcile/trace_context + review_style_
collector/guidance) into agent/review/, with internal relative
imports re-wired to the new package depth. agent/review/__init__.py
mirrors upstream's thin re-export shim (one of the 21 verified "A"
structural adds).
Rewrote the 38 grep hits across importer files (agent/{analyzer,
ci_autofix,reviewer,webapp}.py, agent/dashboard/*, agent/middleware/
settle_review_check.py, agent/tools/*, agent/utils/github_feedback.py,
agent/webhooks/github.py, evals/reviewer/*, and the reviewer test
suite) to point at agent.review.*; 4 of the 38 hits were name
collisions (list_reviewer_findings, reviewer_outcomes,
_reviewer_thread_id, reviewer_thread_id — not the moved modules) and
were left untouched. tests/test_github_checks.py's module-alias
import (`from agent import reviewer_publish`) follows upstream's own
`from agent.review import publish as reviewer_publish` pattern so
downstream `reviewer_publish.*` call sites needed no changes.
agent/reviewer.py and agent/webapp.py stay in place per the hard
rule (fork content, import-only rewire) and are not part of this
package.
Gates: ruff check + ruff format --check, pytest --co -q (1637
collected), full unit suite (1637 passed), and the reviewer/findings
suite in isolation (pytest -k "review or finding", 421 passed).
2026-07-17 13:52:03 -04:00
|
|
|
from ..review.diff import fetch_pr_diff
|
|
|
|
|
from ..review.findings import REVIEWER_THREAD_KIND
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
from ..utils.github_app import get_github_app_installation_token
|
|
|
|
|
from ..utils.thread_ops import langgraph_client, langgraph_url
|
|
|
|
|
from .options import SUPPORTED_MODEL_IDS, model_supports_effort
|
2026-06-23 12:32:10 -07:00
|
|
|
from .review_api import classify_finding, get_pr_head_sha, get_review, reviewer_thread_id
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
from .thread_api import (
|
|
|
|
|
_DASHBOARD_STREAM_MODES,
|
|
|
|
|
_langgraph_proxy_headers,
|
|
|
|
|
_require_json_content_type,
|
|
|
|
|
_stream_thread_events,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
_CHAT_ASSISTANT_ID = "chat"
|
|
|
|
|
_CHAT_SOURCE = "review_chat"
|
2026-06-23 12:32:10 -07:00
|
|
|
# Sentinel: caller did not pre-fetch the thread metadata, so fetch it here.
|
|
|
|
|
_UNFETCHED = object()
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
_PROXY_REQUEST_TIMEOUT = httpx.Timeout(30.0, connect=5.0)
|
|
|
|
|
_MAX_DIFF_CHARS = 400_000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _now_ms() -> int:
|
|
|
|
|
return int(datetime.now(UTC).timestamp() * 1000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_TITLE_MAX_CHARS = 60
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _reviewer_thread_exists(owner: str, repo: str, pr_number: int) -> bool:
|
|
|
|
|
try:
|
|
|
|
|
thread = await langgraph_client().threads.get(reviewer_thread_id(owner, repo, pr_number))
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
return False
|
|
|
|
|
metadata = thread.get("metadata") if isinstance(thread, dict) else None
|
|
|
|
|
return isinstance(metadata, dict) and metadata.get("kind") == REVIEWER_THREAD_KIND
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_review_chat(owner: str, repo: str, pr_number: int, login: str) -> dict[str, Any]:
|
|
|
|
|
"""Chat availability for this PR. Threads are minted client-side per chat."""
|
|
|
|
|
return {
|
|
|
|
|
"available": await _reviewer_thread_exists(owner, repo, pr_number),
|
|
|
|
|
"assistant_id": _CHAT_ASSISTANT_ID,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _chat_thread_search_metadata(
|
|
|
|
|
owner: str, repo: str, pr_number: int, login: str
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"kind": _CHAT_SOURCE,
|
|
|
|
|
"github_login": login,
|
|
|
|
|
"repo_owner": owner,
|
|
|
|
|
"repo_name": repo,
|
|
|
|
|
"pr_number": pr_number,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def list_review_chat_threads(
|
|
|
|
|
owner: str, repo: str, pr_number: int, login: str, *, limit: int = 50
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
"""This user's chat conversations for the PR, newest first."""
|
|
|
|
|
client = langgraph_client()
|
|
|
|
|
try:
|
|
|
|
|
threads = await client.threads.search(
|
|
|
|
|
metadata=_chat_thread_search_metadata(owner, repo, pr_number, login),
|
|
|
|
|
limit=limit,
|
|
|
|
|
sort_by="updated_at",
|
|
|
|
|
sort_order="desc",
|
|
|
|
|
)
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
logger.debug(
|
|
|
|
|
"chat thread search failed for %s/%s#%s", owner, repo, pr_number, exc_info=True
|
|
|
|
|
)
|
|
|
|
|
return []
|
|
|
|
|
out: list[dict[str, Any]] = []
|
|
|
|
|
for thread in threads or []:
|
|
|
|
|
if not isinstance(thread, dict):
|
|
|
|
|
continue
|
|
|
|
|
metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {}
|
|
|
|
|
thread_id = thread.get("thread_id") or thread.get("id")
|
|
|
|
|
if not isinstance(thread_id, str):
|
|
|
|
|
continue
|
|
|
|
|
out.append(
|
|
|
|
|
{
|
|
|
|
|
"thread_id": thread_id,
|
|
|
|
|
"title": metadata.get("title") or "New chat",
|
|
|
|
|
"updated_at": thread.get("updated_at")
|
|
|
|
|
if isinstance(thread.get("updated_at"), str)
|
|
|
|
|
else None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def delete_review_chat_thread(
|
|
|
|
|
owner: str, repo: str, pr_number: int, login: str, thread_id: str
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Delete one of the user's chat threads (scoped + ownership-checked)."""
|
|
|
|
|
metadata = await assert_chat_thread_access(thread_id, owner, repo, pr_number, login)
|
|
|
|
|
if metadata is None:
|
|
|
|
|
return # already gone; treat as success
|
|
|
|
|
await langgraph_client().threads.delete(thread_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _first_user_text(params: dict[str, Any]) -> str:
|
|
|
|
|
run_input = params.get("input")
|
|
|
|
|
messages = run_input.get("messages") if isinstance(run_input, dict) else None
|
|
|
|
|
if not isinstance(messages, list):
|
|
|
|
|
return ""
|
|
|
|
|
for message in messages:
|
|
|
|
|
if not isinstance(message, dict) or message.get("type") != "human":
|
|
|
|
|
continue
|
|
|
|
|
content = message.get("content")
|
|
|
|
|
if isinstance(content, str):
|
|
|
|
|
return content.strip()
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
for block in content:
|
|
|
|
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
|
|
|
text = block.get("text")
|
|
|
|
|
if isinstance(text, str) and text.strip():
|
|
|
|
|
return text.strip()
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _derive_title(params: dict[str, Any]) -> str:
|
|
|
|
|
text = _first_user_text(params)
|
|
|
|
|
if not text:
|
|
|
|
|
return "New chat"
|
|
|
|
|
flattened = " ".join(text.split())
|
|
|
|
|
return flattened[:_TITLE_MAX_CHARS] if flattened else "New chat"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_overview(review: dict[str, Any]) -> str:
|
|
|
|
|
pr = review.get("pr") if isinstance(review.get("pr"), dict) else {}
|
|
|
|
|
lines = [
|
|
|
|
|
f"# {review.get('title') or 'Pull request'} (#{review.get('number')})",
|
|
|
|
|
"",
|
|
|
|
|
f"- Repository: {review.get('full_name', '')}",
|
|
|
|
|
f"- Author: {review.get('author', '')}",
|
|
|
|
|
f"- Head: {review.get('head_ref', '')} @ {review.get('head_sha', '')[:12]}",
|
|
|
|
|
f"- Base: {review.get('base_ref', '')}",
|
|
|
|
|
f"- State: {pr.get('state', '')}",
|
|
|
|
|
f"- Changes: +{pr.get('additions', 0)} -{pr.get('deletions', 0)} "
|
|
|
|
|
f"across {pr.get('changed_files', 0)} file(s), {pr.get('commits', 0)} commit(s)",
|
|
|
|
|
"",
|
|
|
|
|
"## Description",
|
|
|
|
|
"",
|
|
|
|
|
str(pr.get("body") or "_No description provided._"),
|
|
|
|
|
]
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_findings(findings: list[dict[str, Any]]) -> str:
|
|
|
|
|
if not findings:
|
|
|
|
|
return "# Review findings\n\n_No findings were published for this PR._"
|
|
|
|
|
out = ["# Review findings", ""]
|
|
|
|
|
for finding in findings:
|
|
|
|
|
group = classify_finding(finding)
|
|
|
|
|
location = finding.get("file") or ""
|
|
|
|
|
start = finding.get("start_line")
|
|
|
|
|
if location and start:
|
|
|
|
|
location = f"{location}:{start}"
|
|
|
|
|
out.append(
|
|
|
|
|
f"## [{group}] {finding.get('title') or 'Untitled'} "
|
|
|
|
|
f"({finding.get('severity', 'low')}/{finding.get('confidence', 'medium')}, "
|
|
|
|
|
f"{finding.get('status', 'open')})"
|
|
|
|
|
)
|
|
|
|
|
if location:
|
|
|
|
|
out.append(f"`{location}`")
|
|
|
|
|
out.append("")
|
|
|
|
|
out.append(str(finding.get("description") or ""))
|
|
|
|
|
suggestion = finding.get("suggestion")
|
|
|
|
|
if suggestion:
|
|
|
|
|
out.append("")
|
|
|
|
|
out.append(f"Suggested change:\n```\n{suggestion}\n```")
|
|
|
|
|
note = finding.get("resolution_note")
|
|
|
|
|
if note:
|
|
|
|
|
out.append("")
|
|
|
|
|
out.append(f"Resolution: {note}")
|
|
|
|
|
out.append("")
|
|
|
|
|
return "\n".join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _review_head_sha(review: dict[str, Any]) -> str:
|
|
|
|
|
pr = review.get("pr") if isinstance(review.get("pr"), dict) else {}
|
|
|
|
|
return str(pr.get("head_sha") or review.get("head_sha") or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _build_pr_context(
|
|
|
|
|
owner: str,
|
|
|
|
|
repo: str,
|
|
|
|
|
pr_number: int,
|
|
|
|
|
token: str,
|
|
|
|
|
*,
|
|
|
|
|
review: dict[str, Any] | None = None,
|
|
|
|
|
) -> tuple[dict[str, Any], str]:
|
|
|
|
|
"""Fetch diff + findings + overview as seedable files; return ``(files, head_sha)``.
|
|
|
|
|
|
|
|
|
|
Accepts an already-fetched ``review`` to avoid re-fetching it when the caller
|
|
|
|
|
has just read it to decide whether a reseed is needed.
|
|
|
|
|
"""
|
feat: managed LangGraph Cloud + Vercel migration (PR2 — code fixes + docs) (#65)
* fix(dashboard): managed-cloud OAuth hardening + admin user-mapping endpoint
Prepare the dashboard backend for the managed LangGraph Cloud + Vercel
runtime, where the API is HTTPS and cross-site from the UI.
- OAuth redirect_uri (#2): coerce a schemeless DASHBOARD_API_BASE_URL to
https:// in _api_base_url() so GitHub stops rejecting login with
"redirect_uri not associated with this application". _cookie_security()
now treats a schemeless (managed) value as Secure; SameSite=None too,
consistent with the coerced scheme.
- OAuth state cookie (#3): document that osw_oauth_state is host-only by
design (a Domain cookie is unsafe across *.vercel.app, a public suffix),
so login must always start on the stable alias to avoid "oauth state
mismatch". Operational contract; no behavioral change.
- Admin user mappings (#4): add POST /admin/user-mappings so an admin can
set the github_login -> work_email link from the dashboard instead of a
raw Store write. New "admin" MappingSource provenance value.
* fix(webapp): refresh user-mapping cache on GitHub webhook paths
On managed LangGraph Cloud the backend runs multiple replicas, so the
per-process GitHub<->work-email mapping cache can be stale on the replica
handling a webhook (a mapping created on another replica is invisible
until refresh). process_github_pr_comment and process_github_issue now
refresh the cache from the durable Store before resolving the author's
email, matching the existing Slack mention path (process_slack_mention).
* perf(webapp): defer deepagents import to speed custom-app cold start
The custom FastAPI app (agent.webapp:app, the langgraph.json http.app)
pulled deepagents -> langchain_anthropic -> anthropic into its import
graph via dashboard.routes, only to build skill/chat seed files. Defer
those create_file_data imports into the functions that use them. Removes
deepagents/langchain_anthropic/anthropic from app import entirely and
roughly halves module-import wall time (~0.6-0.8s -> ~0.35s warm; larger
cold-start saving since native anthropic init is skipped). Behavior
identical. (reviewer_diff already imports deepagents under TYPE_CHECKING.)
* feat(ui): set work_email user mappings from the admin dashboard
Add an "Add / update" form to the admin User mappings section and the
adminUpsertUserMapping API client method, wiring the new
POST /admin/user-mappings endpoint. Admins can now create or update a
github_login -> work_email mapping directly instead of waiting for the
user to self-connect Slack.
* docs: document managed LangGraph Cloud + Vercel deployment
- INSTALLATION §10: add the managed production env triad (LANGGRAPH_URL,
DASHBOARD_BASE_URL + DASHBOARD_API_BASE_URL with https://, empty
VITE_DASHBOARD_API_BASE_URL for same-origin), the stable-alias login
and vercel.json stable-deployment-URL requirements, multi-replica cache
note, plus redirect_uri-scheme and oauth-state-mismatch troubleshooting.
Refresh the langgraph.json snippet to all six graphs.
- README: reframe deployment around the managed migration; link the plan.
- deploy/MIGRATION.md: import the self-hosted -> managed migration plan.
2026-06-29 19:58:38 -04:00
|
|
|
# Deferred import: deepagents pulls langchain_anthropic / anthropic (~0.7s)
|
|
|
|
|
# into the import graph. It is otherwise dragged into the custom FastAPI
|
|
|
|
|
# app's cold start via dashboard.routes; only needed when seeding chat
|
|
|
|
|
# files, so import it lazily here.
|
|
|
|
|
from deepagents.backends.utils import create_file_data
|
|
|
|
|
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
if review is None:
|
|
|
|
|
review = await get_review(owner, repo, pr_number)
|
|
|
|
|
findings = review.get("findings") if isinstance(review.get("findings"), list) else []
|
|
|
|
|
head_sha = _review_head_sha(review)
|
|
|
|
|
diff = await fetch_pr_diff(owner=owner, repo=repo, pr_number=pr_number, token=token) or ""
|
|
|
|
|
if len(diff) > _MAX_DIFF_CHARS:
|
|
|
|
|
diff = diff[:_MAX_DIFF_CHARS] + "\n\n[diff truncated]\n"
|
|
|
|
|
files = {
|
|
|
|
|
"/pr/overview.md": create_file_data(_render_overview(review)),
|
|
|
|
|
"/pr/diff.patch": create_file_data(diff or "[diff unavailable]"),
|
|
|
|
|
"/pr/findings.md": create_file_data(_render_findings(findings)),
|
|
|
|
|
}
|
|
|
|
|
return files, head_sha
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_chat_thread_metadata(thread_id: str) -> dict[str, Any] | None:
|
|
|
|
|
client = langgraph_client()
|
|
|
|
|
try:
|
|
|
|
|
thread = await client.threads.get(thread_id)
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
return None
|
|
|
|
|
return thread.get("metadata") if isinstance(thread, dict) else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def assert_chat_thread_access(
|
|
|
|
|
thread_id: str, owner: str, repo: str, pr_number: int, login: str
|
|
|
|
|
) -> dict[str, Any] | None:
|
|
|
|
|
"""Authorize a client-supplied chat thread id before proxying to LangGraph.
|
|
|
|
|
|
|
|
|
|
Chat threads are private per viewer and their ids come from the client, so
|
|
|
|
|
every proxy route must confirm the caller owns the thread it names. Returns
|
|
|
|
|
the thread metadata when it exists and belongs to ``login`` for this PR, or
|
|
|
|
|
``None`` when the thread doesn't exist yet (it's created lazily on the first
|
|
|
|
|
run, so there is nothing to leak). Raises 404 when a thread exists but is
|
|
|
|
|
owned by someone else or scoped to a different repo/PR — this also rejects
|
|
|
|
|
reviewer (or any non-chat) threads, whose ``kind`` is not ``_CHAT_SOURCE``.
|
|
|
|
|
"""
|
|
|
|
|
metadata = await _get_chat_thread_metadata(thread_id)
|
|
|
|
|
if metadata is None:
|
|
|
|
|
return None
|
|
|
|
|
owns = (
|
|
|
|
|
metadata.get("kind") == _CHAT_SOURCE
|
|
|
|
|
and metadata.get("github_login") == login
|
|
|
|
|
and metadata.get("repo_owner") == owner
|
|
|
|
|
and metadata.get("repo_name") == repo
|
|
|
|
|
and metadata.get("pr_number") == pr_number
|
|
|
|
|
)
|
|
|
|
|
if not owns:
|
|
|
|
|
raise HTTPException(404, "chat not found")
|
|
|
|
|
return metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_chat_thread(
|
|
|
|
|
thread_id: str, owner: str, repo: str, pr_number: int, login: str, *, title: str
|
|
|
|
|
) -> None:
|
|
|
|
|
now_ms = _now_ms()
|
|
|
|
|
metadata = {
|
|
|
|
|
"kind": _CHAT_SOURCE,
|
|
|
|
|
"source": _CHAT_SOURCE,
|
|
|
|
|
"github_login": login,
|
|
|
|
|
"repo_owner": owner,
|
|
|
|
|
"repo_name": repo,
|
|
|
|
|
"pr_number": pr_number,
|
|
|
|
|
"title": title,
|
|
|
|
|
"created_at_ms": now_ms,
|
|
|
|
|
"updated_at_ms": now_ms,
|
|
|
|
|
}
|
|
|
|
|
await langgraph_client().threads.create(
|
|
|
|
|
thread_id=thread_id, metadata=metadata, if_exists="do_nothing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_chat_model(configurable: dict[str, Any]) -> tuple[str | None, str | None]:
|
|
|
|
|
model_id = configurable.get("chat_model_id")
|
|
|
|
|
effort = configurable.get("chat_effort")
|
|
|
|
|
if (
|
|
|
|
|
isinstance(model_id, str)
|
|
|
|
|
and model_id in SUPPORTED_MODEL_IDS
|
|
|
|
|
and isinstance(effort, str)
|
|
|
|
|
and model_supports_effort(model_id, effort)
|
|
|
|
|
):
|
|
|
|
|
return model_id, effort
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _enrich_chat_command(
|
|
|
|
|
command: dict[str, Any],
|
|
|
|
|
*,
|
|
|
|
|
owner: str,
|
|
|
|
|
repo: str,
|
|
|
|
|
pr_number: int,
|
|
|
|
|
login: str,
|
|
|
|
|
thread_id: str,
|
2026-06-23 12:32:10 -07:00
|
|
|
thread_metadata: Any = _UNFETCHED,
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if command.get("method") != "run.start":
|
|
|
|
|
return command
|
|
|
|
|
|
|
|
|
|
params = command.get("params")
|
|
|
|
|
if not isinstance(params, dict):
|
|
|
|
|
params = {}
|
|
|
|
|
command["params"] = params
|
|
|
|
|
|
2026-06-23 12:32:10 -07:00
|
|
|
# Reuse the metadata the access check already fetched to avoid a duplicate
|
|
|
|
|
# thread read on the hot path; fall back to fetching it when not supplied.
|
|
|
|
|
if thread_metadata is _UNFETCHED:
|
|
|
|
|
thread_metadata = await _get_chat_thread_metadata(thread_id)
|
|
|
|
|
metadata = thread_metadata
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
created = metadata is None
|
|
|
|
|
if created:
|
|
|
|
|
await _create_chat_thread(
|
|
|
|
|
thread_id, owner, repo, pr_number, login, title=_derive_title(params)
|
|
|
|
|
)
|
|
|
|
|
metadata = {}
|
|
|
|
|
|
|
|
|
|
client_config = params.get("config")
|
|
|
|
|
if not isinstance(client_config, dict):
|
|
|
|
|
client_config = {}
|
|
|
|
|
client_configurable = client_config.get("configurable")
|
|
|
|
|
if not isinstance(client_configurable, dict):
|
|
|
|
|
client_configurable = {}
|
|
|
|
|
|
|
|
|
|
configurable: dict[str, Any] = {
|
|
|
|
|
"thread_id": thread_id,
|
|
|
|
|
"source": _CHAT_SOURCE,
|
|
|
|
|
"github_login": login,
|
|
|
|
|
"chat_repo_owner": owner,
|
|
|
|
|
"chat_repo_name": repo,
|
|
|
|
|
"chat_pr_number": pr_number,
|
|
|
|
|
"reviewer_thread_id": reviewer_thread_id(owner, repo, pr_number),
|
|
|
|
|
}
|
|
|
|
|
model_id, effort = _normalize_chat_model(client_configurable)
|
|
|
|
|
if model_id and effort:
|
|
|
|
|
configurable["chat_model_id"] = model_id
|
|
|
|
|
configurable["chat_effort"] = effort
|
|
|
|
|
|
|
|
|
|
# Seed PR context on the thread's first run, and reseed whenever the PR head
|
|
|
|
|
# has moved since the last seed — otherwise the chat keeps answering from a
|
|
|
|
|
# stale diff/findings while the review page already shows the current head.
|
|
|
|
|
stored_head = metadata.get("chat_head_sha") if isinstance(metadata, dict) else None
|
|
|
|
|
stored_head = stored_head if isinstance(stored_head, str) else ""
|
|
|
|
|
|
2026-06-23 12:32:10 -07:00
|
|
|
# Detect head movement with a single lightweight PR lookup instead of a full
|
|
|
|
|
# ``get_review`` (which also fetches check runs + the reviewer thread). The
|
|
|
|
|
# full review is only fetched below, by ``_build_pr_context``, when we
|
|
|
|
|
# actually need to reseed. On lookup failure, keep the existing context.
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
review: dict[str, Any] | None = None
|
|
|
|
|
needs_seed = created
|
|
|
|
|
if not created:
|
2026-06-23 12:32:10 -07:00
|
|
|
current_head = await get_pr_head_sha(owner, repo, pr_number)
|
|
|
|
|
needs_seed = bool(current_head) and current_head != stored_head
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
|
|
|
|
|
if needs_seed:
|
|
|
|
|
try:
|
2026-06-23 12:32:10 -07:00
|
|
|
token = await get_github_app_installation_token(repositories=[repo])
|
|
|
|
|
if not token:
|
|
|
|
|
raise HTTPException(503, "GitHub App token unavailable")
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
pr_files, head_sha = await _build_pr_context(
|
|
|
|
|
owner, repo, pr_number, token, review=review
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
2026-06-23 12:32:10 -07:00
|
|
|
# An existing chat can keep answering from its last seeded context, so
|
|
|
|
|
# a transient reseed failure shouldn't break the conversation. A fresh
|
|
|
|
|
# chat (or one never seeded) has nothing to fall back to, so surface it.
|
|
|
|
|
if created or not stored_head:
|
|
|
|
|
if isinstance(exc, HTTPException):
|
|
|
|
|
raise
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Failed to seed PR chat context for %s/%s#%s", owner, repo, pr_number
|
|
|
|
|
)
|
|
|
|
|
raise HTTPException(502, "could not load PR context") from exc
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Failed to reseed PR chat context for %s/%s#%s; keeping last seeded context",
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
pr_number,
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
)
|
|
|
|
|
configurable["chat_head_sha"] = stored_head
|
2026-06-23 12:32:10 -07:00
|
|
|
else:
|
|
|
|
|
if head_sha:
|
|
|
|
|
configurable["chat_head_sha"] = head_sha
|
|
|
|
|
await langgraph_client().threads.update(
|
|
|
|
|
thread_id=thread_id, metadata={"chat_head_sha": head_sha}
|
|
|
|
|
)
|
|
|
|
|
elif stored_head:
|
|
|
|
|
configurable["chat_head_sha"] = stored_head
|
|
|
|
|
run_input = params.get("input")
|
|
|
|
|
if not isinstance(run_input, dict):
|
|
|
|
|
run_input = {}
|
|
|
|
|
existing_files = run_input.get("files")
|
|
|
|
|
run_input["files"] = {
|
|
|
|
|
**(existing_files if isinstance(existing_files, dict) else {}),
|
|
|
|
|
**pr_files,
|
|
|
|
|
}
|
|
|
|
|
params["input"] = run_input
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
elif stored_head:
|
|
|
|
|
configurable["chat_head_sha"] = stored_head
|
|
|
|
|
|
|
|
|
|
params["assistant_id"] = _CHAT_ASSISTANT_ID
|
|
|
|
|
params.setdefault("stream_mode", list(_DASHBOARD_STREAM_MODES))
|
|
|
|
|
params.setdefault("stream_resumable", True)
|
|
|
|
|
params["config"] = {**client_config, "configurable": configurable}
|
|
|
|
|
command["params"] = params
|
|
|
|
|
return command
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def proxy_review_chat_commands(
|
|
|
|
|
owner: str,
|
|
|
|
|
repo: str,
|
|
|
|
|
pr_number: int,
|
|
|
|
|
login: str,
|
|
|
|
|
thread_id: str,
|
|
|
|
|
body: bytes,
|
|
|
|
|
*,
|
|
|
|
|
content_type: str = "application/json",
|
|
|
|
|
) -> tuple[int, bytes, str | None]:
|
|
|
|
|
# Reject threads the caller doesn't own; a missing thread is created lazily
|
2026-06-23 12:32:10 -07:00
|
|
|
# below on the first `run.start` (with the caller as owner). Reuse the
|
|
|
|
|
# fetched metadata to avoid a second thread read during enrichment.
|
|
|
|
|
metadata = await assert_chat_thread_access(thread_id, owner, repo, pr_number, login)
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
_require_json_content_type(content_type)
|
|
|
|
|
try:
|
|
|
|
|
parsed = json.loads(body)
|
|
|
|
|
except json.JSONDecodeError as exc:
|
|
|
|
|
raise HTTPException(400, "command body must be a JSON object") from exc
|
|
|
|
|
if not isinstance(parsed, dict):
|
|
|
|
|
raise HTTPException(400, "command body must be a JSON object")
|
|
|
|
|
|
|
|
|
|
enriched = await _enrich_chat_command(
|
2026-06-23 12:32:10 -07:00
|
|
|
parsed,
|
|
|
|
|
owner=owner,
|
|
|
|
|
repo=repo,
|
|
|
|
|
pr_number=pr_number,
|
|
|
|
|
login=login,
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
thread_metadata=metadata,
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
)
|
|
|
|
|
url = f"{langgraph_url().rstrip('/')}/threads/{thread_id}/commands"
|
|
|
|
|
headers = _langgraph_proxy_headers(content_type=content_type)
|
|
|
|
|
async with httpx.AsyncClient(timeout=_PROXY_REQUEST_TIMEOUT) as client:
|
|
|
|
|
response = await client.post(url, content=json.dumps(enriched).encode(), headers=headers)
|
|
|
|
|
return response.status_code, response.content, response.headers.get("content-type")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def proxy_review_chat_stream_events(
|
|
|
|
|
owner: str,
|
|
|
|
|
repo: str,
|
|
|
|
|
pr_number: int,
|
|
|
|
|
login: str,
|
|
|
|
|
thread_id: str,
|
|
|
|
|
body: bytes,
|
|
|
|
|
*,
|
|
|
|
|
content_type: str = "application/json",
|
|
|
|
|
) -> AsyncIterator[bytes]:
|
|
|
|
|
await assert_chat_thread_access(thread_id, owner, repo, pr_number, login)
|
|
|
|
|
_require_json_content_type(content_type)
|
|
|
|
|
return _stream_thread_events(thread_id, body, content_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _proxy_passthrough(
|
|
|
|
|
method: str, thread_id: str, suffix: str, body: bytes | None, content_type: str
|
|
|
|
|
) -> tuple[int, bytes, str | None]:
|
|
|
|
|
url = f"{langgraph_url().rstrip('/')}/threads/{thread_id}/{suffix}"
|
|
|
|
|
headers = _langgraph_proxy_headers(content_type=content_type)
|
|
|
|
|
async with httpx.AsyncClient(timeout=_PROXY_REQUEST_TIMEOUT) as client:
|
|
|
|
|
if method == "GET":
|
|
|
|
|
response = await client.get(url, headers=headers)
|
|
|
|
|
else:
|
|
|
|
|
response = await client.post(url, content=body or b"{}", headers=headers)
|
|
|
|
|
return response.status_code, response.content, response.headers.get("content-type")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def proxy_review_chat_state(
|
|
|
|
|
owner: str, repo: str, pr_number: int, login: str, thread_id: str
|
|
|
|
|
) -> tuple[int, bytes, str | None]:
|
|
|
|
|
await assert_chat_thread_access(thread_id, owner, repo, pr_number, login)
|
|
|
|
|
status_code, content, media_type = await _proxy_passthrough(
|
|
|
|
|
"GET", thread_id, "state", None, "application/json"
|
|
|
|
|
)
|
|
|
|
|
# The chat thread is created lazily on the first run, so an initial getState
|
|
|
|
|
# hits a missing thread. Return an empty idle state so the SDK hydrates a
|
|
|
|
|
# fresh thread instead of surfacing the 404 as a hard error.
|
|
|
|
|
if status_code == 404:
|
|
|
|
|
empty = json.dumps({"values": {}, "next": []}).encode()
|
|
|
|
|
return 200, empty, "application/json"
|
|
|
|
|
return status_code, content, media_type
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def proxy_review_chat_history(
|
|
|
|
|
owner: str,
|
|
|
|
|
repo: str,
|
|
|
|
|
pr_number: int,
|
|
|
|
|
login: str,
|
|
|
|
|
thread_id: str,
|
|
|
|
|
body: bytes,
|
|
|
|
|
*,
|
|
|
|
|
content_type: str = "application/json",
|
|
|
|
|
) -> tuple[int, bytes, str | None]:
|
|
|
|
|
await assert_chat_thread_access(thread_id, owner, repo, pr_number, login)
|
|
|
|
|
_require_json_content_type(content_type)
|
|
|
|
|
status_code, content, media_type = await _proxy_passthrough(
|
|
|
|
|
"POST", thread_id, "history", body, content_type
|
|
|
|
|
)
|
|
|
|
|
# The thread is created lazily on the first run; before then, hydration
|
|
|
|
|
# history reads hit a missing thread. Return an empty list so the SDK
|
|
|
|
|
# treats it as a fresh thread instead of erroring.
|
|
|
|
|
if status_code == 404:
|
|
|
|
|
return 200, b"[]", "application/json"
|
|
|
|
|
return status_code, content, media_type
|