- Add delete_document_chunks for reprocessing, save extracted text to disk - Expand case directory structure (original/extracted/proofread/backup) - Update classifier patterns (תגובה, הודעת עמדה) - Fix proofreader agent paths for new directory layout - Update HEARTBEAT to notify on every task completion - Improve bidi_table with LRE/PDF directional embedding - Add Paperclip project verification and auto-close setup issue - Add auto-sync-cases.sh for Gitea synchronization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
224 lines
8.3 KiB
Python
224 lines
8.3 KiB
Python
"""Paperclip integration via direct DB access (embedded PostgreSQL).
|
|
|
|
Creates projects, issues, and plugin state entries to fully link
|
|
a legal-ai case into Paperclip's workflow.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import uuid
|
|
|
|
import asyncpg
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PAPERCLIP_DB_URL = os.environ.get(
|
|
"PAPERCLIP_DB_URL", "postgresql://paperclip:paperclip@127.0.0.1:54329/paperclip"
|
|
)
|
|
|
|
PLUGIN_ID = "53461b5a-7f58-411a-9952-72f9c8d4a328" # marcusgroup.legal-ai
|
|
|
|
# Company IDs from Paperclip DB
|
|
COMPANIES = {
|
|
"licensing": "42a7acd0-30c5-4cbd-ac97-7424f65df294", # CMP — רישוי ובניה
|
|
"betterment": "8639e837-4c9d-47fa-a76b-95788d651896", # CMPA — היטלי השבחה
|
|
}
|
|
|
|
APPEAL_TYPE_TO_COMPANY = {
|
|
"רישוי": "licensing",
|
|
"licensing": "licensing",
|
|
"היטל השבחה": "betterment",
|
|
"betterment_levy": "betterment",
|
|
"פיצויים": "betterment",
|
|
"compensation": "betterment",
|
|
}
|
|
|
|
|
|
def _get_company_id(appeal_type: str) -> str:
|
|
key = APPEAL_TYPE_TO_COMPANY.get(appeal_type, "licensing")
|
|
return COMPANIES[key]
|
|
|
|
|
|
async def create_project(
|
|
case_number: str,
|
|
title: str,
|
|
description: str = "",
|
|
appeal_type: str = "רישוי",
|
|
color: str = "#6366f1",
|
|
) -> dict:
|
|
"""Create a project in the Paperclip embedded DB, or return existing one."""
|
|
company_id = _get_company_id(appeal_type)
|
|
prefix = "CMP" if _get_company_id(appeal_type) == COMPANIES["licensing"] else "CMPA"
|
|
|
|
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
|
try:
|
|
# Check for existing project with this case number
|
|
existing = await conn.fetchrow(
|
|
"SELECT id, name FROM projects WHERE name LIKE $1 AND company_id = $2::uuid",
|
|
f"%{case_number}%", company_id,
|
|
)
|
|
if existing:
|
|
return {
|
|
"id": str(existing["id"]),
|
|
"company_id": company_id,
|
|
"name": existing["name"],
|
|
"url": f"https://pc.nautilus.marcusgroup.org/{prefix}/projects/{existing['id']}/issues",
|
|
"existing": True,
|
|
}
|
|
|
|
project_id = str(uuid.uuid4())
|
|
project_name = f"ערר {case_number} — {title}"[:200]
|
|
await conn.execute(
|
|
"""INSERT INTO projects (id, company_id, name, description, status, color)
|
|
VALUES ($1, $2::uuid, $3, $4, 'backlog', $5)""",
|
|
project_id, company_id, project_name, description[:500] if description else "", color,
|
|
)
|
|
# Create initial issue linked to the project
|
|
issue_id, identifier = await _create_issue(
|
|
conn, company_id, project_id, case_number, title, prefix,
|
|
)
|
|
|
|
# Link issue to legal-ai case via plugin state
|
|
await _link_case_to_issue(conn, issue_id, case_number)
|
|
|
|
# Verify project creation and close the setup issue
|
|
await _verify_and_close_setup_issue(conn, project_id, issue_id, identifier, case_number)
|
|
|
|
return {
|
|
"id": project_id,
|
|
"company_id": company_id,
|
|
"name": project_name,
|
|
"issue_id": issue_id,
|
|
"issue_identifier": identifier,
|
|
"url": f"https://pc.nautilus.marcusgroup.org/{prefix}/projects/{project_id}/issues",
|
|
"existing": False,
|
|
}
|
|
finally:
|
|
await conn.close()
|
|
|
|
|
|
async def _create_issue(
|
|
conn: asyncpg.Connection,
|
|
company_id: str,
|
|
project_id: str,
|
|
case_number: str,
|
|
title: str,
|
|
prefix: str,
|
|
) -> tuple[str, str]:
|
|
"""Create an issue in the project and return (issue_id, identifier)."""
|
|
issue_id = str(uuid.uuid4())
|
|
|
|
# Get next issue number for this company
|
|
row = await conn.fetchrow(
|
|
"UPDATE companies SET issue_counter = issue_counter + 1 WHERE id = $1::uuid RETURNING issue_counter",
|
|
company_id,
|
|
)
|
|
issue_number = row["issue_counter"]
|
|
identifier = f"{prefix}-{issue_number}"
|
|
|
|
await conn.execute(
|
|
"""INSERT INTO issues (id, company_id, project_id, title, description, status, priority, issue_number, identifier)
|
|
VALUES ($1, $2::uuid, $3::uuid, $4, $5, 'todo', 'medium', $6, $7)""",
|
|
issue_id, company_id, project_id,
|
|
f"[ערר {case_number}] {title}"[:200],
|
|
f"תיק ערר {case_number}\nנוצר אוטומטית מממשק העלאת מסמכים",
|
|
issue_number, identifier,
|
|
)
|
|
|
|
logger.info("Created Paperclip issue %s: [ערר %s] %s", identifier, case_number, title)
|
|
return issue_id, identifier
|
|
|
|
|
|
async def _link_case_to_issue(conn: asyncpg.Connection, issue_id: str, case_number: str) -> None:
|
|
"""Store the legal-ai case number in plugin state, linked to the issue."""
|
|
await conn.execute(
|
|
"""INSERT INTO plugin_state (plugin_id, scope_kind, scope_id, namespace, state_key, value_json)
|
|
VALUES ($1::uuid, 'issue', $2, 'default', 'legal-case-number', $3::jsonb)
|
|
ON CONFLICT (plugin_id, scope_kind, scope_id, namespace, state_key) DO UPDATE SET value_json = $3::jsonb""",
|
|
PLUGIN_ID, issue_id, json.dumps(case_number),
|
|
)
|
|
logger.info("Linked issue %s to case %s via plugin state", issue_id, case_number)
|
|
|
|
|
|
async def _verify_and_close_setup_issue(
|
|
conn: asyncpg.Connection,
|
|
project_id: str,
|
|
issue_id: str,
|
|
identifier: str,
|
|
case_number: str,
|
|
) -> None:
|
|
"""Verify the project was created correctly, then transition the setup issue to done."""
|
|
# Move to in_progress while verifying
|
|
await conn.execute(
|
|
"UPDATE issues SET status = 'in_progress', started_at = now() WHERE id = $1",
|
|
issue_id,
|
|
)
|
|
logger.info("%s: בביצוע — מאמת יצירת פרויקט", identifier)
|
|
|
|
# Verify: project exists, issue is linked, plugin state exists
|
|
checks = []
|
|
|
|
project = await conn.fetchrow("SELECT id, name FROM projects WHERE id = $1::uuid", project_id)
|
|
checks.append(("פרויקט נוצר", project is not None))
|
|
|
|
issue = await conn.fetchrow(
|
|
"SELECT id, project_id FROM issues WHERE id = $1 AND project_id = $2::uuid",
|
|
issue_id, project_id,
|
|
)
|
|
checks.append(("משימה משויכת לפרויקט", issue is not None))
|
|
|
|
plugin_link = await conn.fetchrow(
|
|
"SELECT value_json FROM plugin_state WHERE scope_id = $1 AND state_key = 'legal-case-number'",
|
|
issue_id,
|
|
)
|
|
checks.append(("קישור למערכת המשפטית", plugin_link is not None))
|
|
|
|
all_ok = all(ok for _, ok in checks)
|
|
report_lines = [f"{'✓' if ok else '✕'} {name}" for name, ok in checks]
|
|
report = "\n".join(report_lines)
|
|
|
|
if all_ok:
|
|
await conn.execute(
|
|
"UPDATE issues SET status = 'done', completed_at = now() WHERE id = $1",
|
|
issue_id,
|
|
)
|
|
# Document the verification in a comment
|
|
await conn.execute(
|
|
"""INSERT INTO issue_comments (id, company_id, issue_id, body)
|
|
VALUES ($1, (SELECT company_id FROM issues WHERE id = $2), $2,
|
|
$3)""",
|
|
str(uuid.uuid4()), issue_id,
|
|
f"## אימות יצירת פרויקט — ערר {case_number}\n\n{report}\n\nהפרויקט נוצר בהצלחה. משימה נסגרה אוטומטית.",
|
|
)
|
|
logger.info("%s: הושלם — פרויקט אומת ונסגר", identifier)
|
|
else:
|
|
# Leave in_progress with a warning comment
|
|
failed = [name for name, ok in checks if not ok]
|
|
await conn.execute(
|
|
"""INSERT INTO issue_comments (id, company_id, issue_id, body)
|
|
VALUES ($1, (SELECT company_id FROM issues WHERE id = $2), $2,
|
|
$3)""",
|
|
str(uuid.uuid4()), issue_id,
|
|
f"## אימות יצירת פרויקט — ערר {case_number}\n\n{report}\n\n⚠️ בדיקות שנכשלו: {', '.join(failed)}",
|
|
)
|
|
logger.warning("%s: אימות נכשל — %s", identifier, ", ".join(failed))
|
|
|
|
|
|
async def get_project_url(case_number: str) -> str | None:
|
|
"""Find existing Paperclip project for a case number."""
|
|
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
|
try:
|
|
row = await conn.fetchrow(
|
|
"SELECT id, company_id FROM projects WHERE name LIKE $1",
|
|
f"%{case_number}%",
|
|
)
|
|
if row:
|
|
prefix = "CMP" if row["company_id"] == uuid.UUID(COMPANIES["licensing"]) else "CMPA"
|
|
return f"https://pc.nautilus.marcusgroup.org/{prefix}/projects/{row['id']}/issues"
|
|
return None
|
|
finally:
|
|
await conn.close()
|