feat(agents): mirror Paperclip interactions in case page
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 47s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 47s
Surface issue_thread_interactions (ask_user_questions / request_confirmation /
suggest_tasks) directly inside legal-ai's case detail feed so the user can
answer agent prompts without switching to Paperclip's UI.
Backend (FastAPI):
- paperclip_client.py: 4 new helpers — get_issue_interactions (DB),
respond_to_interaction / accept_interaction / reject_interaction (REST).
- app.py: extends GET /api/cases/{case_number}/agents to include
`interactions`, and adds POST /api/cases/{case_number}/agents/interaction-response
routing to /respond, /accept, /reject in Paperclip.
- paperclip_client.py: also pulls existing httpx calls onto the centralized
pc_request helper (paperclip_api.py) for consistent auth + run-id headers.
Frontend (web-ui, Next.js 16 + TanStack Query):
- agents.ts: Interaction / InteractionPayload / InteractionStatus types,
useSubmitInteraction mutation hook (invalidates the activity query).
- agent-activity-feed.tsx: InteractionCard renders radio (single) /
checkbox (multi) for ask_user_questions, accept/reject + reason for
request_confirmation, task selection for suggest_tasks. Resolved
interactions show a read-only summary. Cards are interleaved with
comments by created_at, so the feed reads chronologically.
Paperclip auto-wakes the issue assignee on a successful response
(queueResolvedInteractionContinuationWakeup) — no explicit wakeup needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
83
web/paperclip_api.py
Normal file
83
web/paperclip_api.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Paperclip REST API helper.
|
||||
|
||||
All HTTP calls from legal-ai backend (FastAPI) to Paperclip should go through
|
||||
``pc_request`` so that auth + audit headers are applied consistently.
|
||||
|
||||
The bash counterpart for agents lives at ``scripts/pc.sh``.
|
||||
|
||||
Notes
|
||||
-----
|
||||
* Uses ``PAPERCLIP_BOARD_API_KEY`` (long-lived) — these are *board* actions
|
||||
(wakeups, comments-as-user) initiated from outside a heartbeat, not agent
|
||||
actions. Board API keys are not JWTs, so they do **not** carry a ``run_id``
|
||||
claim; pass ``run_id=`` explicitly when you have one (rare for board flows).
|
||||
* For agent actions inside a heartbeat run, agents use the bash helper with
|
||||
the auto-injected ``PAPERCLIP_API_KEY`` JWT — those carry ``run_id`` in
|
||||
claims, so the header is informational/future-proofing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAPERCLIP_API_URL = os.environ.get("PAPERCLIP_API_URL", "http://localhost:3100")
|
||||
PAPERCLIP_BOARD_API_KEY = os.environ.get("PAPERCLIP_BOARD_API_KEY", "")
|
||||
|
||||
DEFAULT_TIMEOUT = 15.0
|
||||
|
||||
|
||||
def _build_headers(run_id: str | None, has_body: bool) -> dict[str, str]:
|
||||
if not PAPERCLIP_BOARD_API_KEY:
|
||||
raise RuntimeError("PAPERCLIP_BOARD_API_KEY not set — cannot call Paperclip API")
|
||||
headers = {
|
||||
"Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}",
|
||||
"X-Paperclip-Run-Id": run_id or "",
|
||||
}
|
||||
if has_body:
|
||||
headers["Content-Type"] = "application/json"
|
||||
return headers
|
||||
|
||||
|
||||
async def pc_request(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
json: dict[str, Any] | None = None,
|
||||
run_id: str | None = None,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
raise_on_error: bool = False,
|
||||
) -> httpx.Response:
|
||||
"""Make a Paperclip REST request.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method : str
|
||||
HTTP method (GET, POST, PATCH, DELETE).
|
||||
path : str
|
||||
Path relative to PAPERCLIP_API_URL (must start with ``/``).
|
||||
json : dict, optional
|
||||
Request body — sent as JSON.
|
||||
run_id : str, optional
|
||||
Heartbeat run ID for audit trail (X-Paperclip-Run-Id header).
|
||||
Rare for board actions; provide when initiating from inside a run.
|
||||
timeout : float
|
||||
httpx timeout (default 15s).
|
||||
raise_on_error : bool
|
||||
If True, calls ``response.raise_for_status()`` on 4xx/5xx.
|
||||
|
||||
Returns
|
||||
-------
|
||||
httpx.Response
|
||||
"""
|
||||
headers = _build_headers(run_id, has_body=json is not None)
|
||||
url = f"{PAPERCLIP_API_URL}{path}"
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
resp = await client.request(method, url, headers=headers, json=json)
|
||||
if raise_on_error:
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
Reference in New Issue
Block a user