Compare commits
8 Commits
fix/write-
...
feature/ho
| Author | SHA1 | Date | |
|---|---|---|---|
| 015e553d06 | |||
| 6bdf9786ac | |||
| d87f9c5a5f | |||
| a0fab1f6de | |||
| d5043100a7 | |||
| 932cc7191c | |||
| d983cfdd3b | |||
| 50649baeed |
@@ -123,7 +123,7 @@ SUMMARY_STRATEGIES = {
|
|||||||
|
|
||||||
DISCUSSION_RULES: dict[str, list[str]] = {
|
DISCUSSION_RULES: dict[str, list[str]] = {
|
||||||
"universal": [
|
"universal": [
|
||||||
"פרק הדיון = אסה רציפה. אין כותרות משנה (H2/H3). מעברים רק עם ביטויי מעבר טקסטואליים.",
|
"פרק הדיון = מאסה רציפה. אין כותרות משנה (H2/H3). מעברים רק עם ביטויי מעבר טקסטואליים.",
|
||||||
"חריג יחיד לכותרות משנה: נושאים נפרדים לחלוטין (למשל: הקלה בגובה + התייחסות לטענות נוספות).",
|
"חריג יחיד לכותרות משנה: נושאים נפרדים לחלוטין (למשל: הקלה בגובה + התייחסות לטענות נוספות).",
|
||||||
"טווח אורך סעיפים: 20 עד 600+ מילים. סעיף עם ציטוט מקיף = בלוק אחד שלם, לא שבירה לסעיפים קצרים.",
|
"טווח אורך סעיפים: 20 עד 600+ מילים. סעיף עם ציטוט מקיף = בלוק אחד שלם, לא שבירה לסעיפים קצרים.",
|
||||||
],
|
],
|
||||||
|
|||||||
50
web/app.py
50
web/app.py
@@ -20,7 +20,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "mcp-server" / "
|
|||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
from fastapi import BackgroundTasks, FastAPI, File, Form, HTTPException, UploadFile
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -44,7 +44,7 @@ from web.mcp_env_catalog import (
|
|||||||
normalize_for_compare,
|
normalize_for_compare,
|
||||||
)
|
)
|
||||||
from web.progress_store import ProgressStore
|
from web.progress_store import ProgressStore
|
||||||
from web.paperclip_api import pc_request
|
from web.paperclip_api import emit_case_status_webhook, pc_request
|
||||||
from web.paperclip_client import (
|
from web.paperclip_client import (
|
||||||
COMPANIES as PAPERCLIP_COMPANIES,
|
COMPANIES as PAPERCLIP_COMPANIES,
|
||||||
accept_interaction as pc_accept_interaction,
|
accept_interaction as pc_accept_interaction,
|
||||||
@@ -1337,8 +1337,12 @@ async def api_case_get(case_number: str):
|
|||||||
|
|
||||||
|
|
||||||
@app.put("/api/cases/{case_number}")
|
@app.put("/api/cases/{case_number}")
|
||||||
async def api_case_update(case_number: str, req: CaseUpdateRequest):
|
async def api_case_update(case_number: str, req: CaseUpdateRequest, background_tasks: BackgroundTasks):
|
||||||
"""Update case details."""
|
"""Update case details."""
|
||||||
|
# Capture old status before the update so we can detect changes.
|
||||||
|
existing = await db.get_case_by_number(case_number)
|
||||||
|
old_status = (existing or {}).get("status", "")
|
||||||
|
|
||||||
result = await cases_tools.case_update(
|
result = await cases_tools.case_update(
|
||||||
case_number=case_number,
|
case_number=case_number,
|
||||||
status=req.status,
|
status=req.status,
|
||||||
@@ -1351,10 +1355,30 @@ async def api_case_update(case_number: str, req: CaseUpdateRequest):
|
|||||||
expected_outcome=req.expected_outcome,
|
expected_outcome=req.expected_outcome,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return json.loads(result)
|
parsed = json.loads(result)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise HTTPException(404, result)
|
raise HTTPException(404, result)
|
||||||
|
|
||||||
|
# Emit webhook when status changes (fire-and-forget via BackgroundTasks).
|
||||||
|
new_status = req.status
|
||||||
|
if new_status and old_status != new_status:
|
||||||
|
prefix = case_number[:1]
|
||||||
|
company_id = (
|
||||||
|
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
|
||||||
|
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
emit_case_status_webhook,
|
||||||
|
case_number=case_number,
|
||||||
|
old_status=old_status,
|
||||||
|
new_status=new_status,
|
||||||
|
company_id=company_id, # None is safe — plugin handles unknown company gracefully
|
||||||
|
)
|
||||||
|
logger.debug("webhook scheduled: case %s %s → %s", case_number, old_status, new_status)
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/cases")
|
@app.delete("/api/cases")
|
||||||
async def api_case_delete(case_number: str, remove_files: bool = False):
|
async def api_case_delete(case_number: str, remove_files: bool = False):
|
||||||
@@ -3057,8 +3081,16 @@ async def api_get_methodology(category: str):
|
|||||||
items = {}
|
items = {}
|
||||||
for key, default_val in defaults.items():
|
for key, default_val in defaults.items():
|
||||||
if key in overrides:
|
if key in overrides:
|
||||||
|
raw = overrides[key]["rule_value"]
|
||||||
|
# asyncpg returns JSONB as a raw JSON string when no codec is registered.
|
||||||
|
# Parse it back to a Python object so the frontend receives the correct type.
|
||||||
|
if isinstance(raw, str):
|
||||||
|
try:
|
||||||
|
raw = json.loads(raw)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
items[key] = {
|
items[key] = {
|
||||||
"value": overrides[key]["rule_value"],
|
"value": raw,
|
||||||
"is_override": True,
|
"is_override": True,
|
||||||
"updated_at": overrides[key]["created_at"].isoformat() if overrides[key]["created_at"] else None,
|
"updated_at": overrides[key]["created_at"].isoformat() if overrides[key]["created_at"] else None,
|
||||||
}
|
}
|
||||||
@@ -3095,10 +3127,14 @@ async def api_update_methodology(category: str, key: str, req: MethodologyUpdate
|
|||||||
raise HTTPException(422, "content_checklists value must be a non-empty string")
|
raise HTTPException(422, "content_checklists value must be a non-empty string")
|
||||||
|
|
||||||
pool = await db.get_pool()
|
pool = await db.get_pool()
|
||||||
|
# json.dumps → text, then PostgreSQL casts text→jsonb.
|
||||||
|
# Passing a Python list directly causes "expected str, got list" in asyncpg;
|
||||||
|
# passing a str with ::jsonb causes double-encoding (stored as JSONB string).
|
||||||
|
# ::text::jsonb bypasses asyncpg's codec and lets PostgreSQL parse the JSON.
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
|
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
|
||||||
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::jsonb) "
|
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::text::jsonb) "
|
||||||
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::jsonb",
|
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::text::jsonb",
|
||||||
category, key, json.dumps(req.value, ensure_ascii=False),
|
category, key, json.dumps(req.value, ensure_ascii=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -81,3 +82,35 @@ async def pc_request(
|
|||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
async def emit_case_status_webhook(
|
||||||
|
case_number: str,
|
||||||
|
old_status: str,
|
||||||
|
new_status: str,
|
||||||
|
company_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Notify the Paperclip plugin that a case status changed.
|
||||||
|
|
||||||
|
Fire-and-forget: logs errors but never raises, so callers aren't blocked.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await pc_request(
|
||||||
|
"POST",
|
||||||
|
"/api/plugins/marcusgroup.legal-ai/webhooks/case-status",
|
||||||
|
json={
|
||||||
|
"caseNumber": case_number,
|
||||||
|
"oldStatus": old_status,
|
||||||
|
"newStatus": new_status,
|
||||||
|
"companyId": company_id,
|
||||||
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||||
|
},
|
||||||
|
run_id=run_id,
|
||||||
|
timeout=5.0,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"emit_case_status_webhook failed for case %s (%s → %s): %s",
|
||||||
|
case_number, old_status, new_status, exc,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user