"""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()