diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d3d930 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# SpecGap Documentation + +## Project Overview +SpecGap is a two-part application: +- Frontend: A React + Vite UI that lets users upload documents, run audits, and review findings. +- Backend: A FastAPI service that parses documents, runs multi-agent analysis with Gemini via LangGraph, and returns structured audit results and patch packs. + +The backend supports three modes: +- Council session (fast flashcards) +- Deep analysis (tech + legal + synthesis) +- Full spectrum (council + deep analysis together) + +## Folder Structure +- Frontend/ + - Vite + React UI, API client, pages, layout components, and UI primitives. +- specgap/ + - Python backend (FastAPI), AI workflows, parsers, and database layer. +- and prompt templates + - Miscellaneous folder (not referenced in code paths). +- test.json + - Standalone file (not referenced in code paths). + +### Frontend Highlights +- Frontend/src/App.tsx: Route setup and application shell. +- Frontend/src/api/client.ts: API client for backend calls. +- Frontend/src/pages/: Core screens (Upload, Audits, Results, Search, etc.). +- Frontend/src/components/: Layout, audit UI, and reusable UI components. + +### Backend Highlights +- specgap/app/main.py: FastAPI app and API endpoints. +- specgap/app/services/workflow.py: Council multi-agent workflow (LangGraph). +- specgap/app/services/parser.py: Document parsing (PDF, DOCX, TXT/MD, OCR). +- specgap/app/services/tech_engine.py: Tech gap analyzer. +- specgap/app/services/biz_engine.py: Legal/negotiation analyzer. +- specgap/app/services/cross_check.py: Orchestrator synthesis. +- specgap/app/services/patch_pack.py: Output file generation. +- specgap/app/core/database.py: SQLAlchemy models + persistence. + +## Architecture and Data Flow +1. Frontend upload (React UI) sends files via multipart form-data to FastAPI. +2. Parser extracts text from PDF/DOCX/TXT/MD; OCR is attempted if needed. +3. Council session (LangGraph): + - Round 1: Independent agent drafts (legal, business, finance). + - Round 2: Cross-check peer drafts. + - Round 3: Generate flashcards. +4. Deep analysis (optional): + - Tech gap analysis (architect agent). + - Legal leverage analysis (lawyer agent). + - Cross-check synthesis + Mermaid diagram output. +5. Patch pack can be generated from selected cards (contract addendum, spec update, negotiation email). + +## Installation and Setup + +### Backend (Python) +Requirements are listed in specgap/requirements.txt. + +```bash +cd specgap +python -m venv .venv +. .venv/Scripts/Activate +pip install -r requirements.txt +``` + +### Frontend (Node) +Dependencies are managed via npm in Frontend/package.json. + +```bash +cd Frontend +npm install +``` + +## Environment Variables + +### Backend +Loaded via python-dotenv in specgap/app/core/config.py. + +- GEMINI_API_KEY (required): Google Gemini API key. +- DATABASE_URL (optional): Overrides SQLite DB path. + +Example .env: +``` +GEMINI_API_KEY=your_key_here +DATABASE_URL=sqlite:///./specgap_audits.db +``` + +### Frontend +Defined in Vite and read in Frontend/src/api/client.ts. + +- VITE_API_URL (optional): Base API URL. Defaults to /api which proxies to http://localhost:8000 in dev via Frontend/vite.config.ts. + +Example .env: +``` +VITE_API_URL=http://localhost:8000 +``` + +## How to Run Locally + +### Start Backend +```bash +cd specgap +python run_backend.py +``` +Default: http://localhost:8000 + +### Start Frontend +```bash +cd Frontend +npm run dev +``` +Default: http://localhost:8080 + +The dev server proxies /api to http://localhost:8000 automatically. + +## API Endpoints + +Implemented in specgap/app/main.py: + +### Health +- GET / + - Returns status and architecture info. + +### Council Session +- POST /audit/council-session + - Query: domain (optional, default Software Engineering) + - Body: multipart form-data with files + - Response: flashcards (council_verdict) + +### Patch Pack Generator +- POST /audit/patch-pack + - Body: JSON { selected_cards: [...], domain?: string } + - Response: generated files (Contract_Addendum.txt, Spec_Update.md, Negotiation_Email.txt) + +### Deep Analysis +- POST /audit/deep-analysis + - Query: domain + - Body: multipart form-data with files + - Response: tech_audit, legal_audit, executive_synthesis + +### Full Spectrum Analysis +- POST /audit/full-spectrum + - Query: domain + - Body: multipart form-data with files + - Response: council verdict + deep analysis bundle + +Note: The frontend client references additional endpoints (audits listing, comments, vector search) in Frontend/src/api/client.ts, but those routes are not present in the backend at this time. + +## Contribution Guidelines +- Keep frontend code in Frontend/src/ with TypeScript, React, and Tailwind conventions. +- Keep backend code in specgap/app/ and follow async FastAPI patterns. +- Favor new endpoints and services in clearly named modules under specgap/app/services/. +- Update environment variable docs whenever introducing new config keys. +- Add unit tests where possible (frontend uses Vitest; backend currently has no test harness). diff --git a/specgap/app/core/prompts.py b/specgap/app/core/prompts.py index d7469b4..74ec057 100644 --- a/specgap/app/core/prompts.py +++ b/specgap/app/core/prompts.py @@ -1,3 +1,15 @@ +COUNCIL_PERSONAS = { + "legal": { + "role": "Corporate General Counsel", + "focus": "Liability, IP, termination, hidden contract traps", + }, + "business": { + "role": "Chief Operating Officer (COO)", + "focus": "Feature completeness, operational viability, timeline realism", + }, + "finance": { + "role": "CFO & Audit Partner", + "focus": "Costs, payment terms, ROI, financial risks", """ Council Prompts for SpecGap Defines personas and prompt templates for the 3-round deliberation. @@ -23,6 +35,26 @@ PROMPT_TEMPLATES = { "ROUND_1": """ +Role: {role} +Domain: {domain} +Task: Identify Risks/Gaps in the provided documents (Contract + Tech Spec). +Focus: {focus} +Output: JSON only +Instructions: +- Cite exact text for every finding. +- Classify gaps as Critical / High / Medium / Low +- Optional: Include "suggested_fix" if obvious. +Format: +{{ + "findings": [ + {{ + "title": "...", + "description": "...", + "severity": "Critical|High|Medium|Low", + "source": "File Name / Section", + "suggested_fix": "..." + }} + ] You are acting as: {role} Domain Context: {domain} @@ -55,6 +87,55 @@ """, "ROUND_2": """ +Role: {role} +Domain: {domain} +Task: Update your findings using peer feedback. +[Your Draft]: {current_draft} +[Peers Drafts]: {peer_drafts} +Output: JSON only +Instructions: +- Merge missing findings from peers +- Resolve contradictions: keep the one with higher severity +- Retain source references +Format same as ROUND_1 +""", + + "ROUND_3": """ +Role: {role} +Domain: {domain} +Task: Convert findings into actionable Flashcards. +[Analysis]: {current_draft} +[Peer Insights]: {peer_drafts} +Output: JSON only +Instructions: +- Max 3-5 flashcards per persona +- Provide: + - id: unique identifier + - card_type: "Risk" | "Opportunity" + - title: short headline + - description: concise explanation (1-2 sentences) + - fix_action: what user should do + - severity: Critical / High / Medium / Low + - impact: High / Medium / Low (for prioritization) + - swipe_right_payload: exact text/action if user accepts +- Do not add extra text or commentary +Format: +{{ + "flashcards": [ + {{ + "id": "...", + "card_type": "...", + "title": "...", + "description": "...", + "fix_action": "...", + "severity": "...", + "impact": "...", + "swipe_right_payload": "..." + }} + ] +}} +""" +} You are acting as: {role} Domain Context: {domain} diff --git a/specgap/app/services/biz_engine.py b/specgap/app/services/biz_engine.py index 264b1e9..dfdac3e 100644 --- a/specgap/app/services/biz_engine.py +++ b/specgap/app/services/biz_engine.py @@ -4,6 +4,124 @@ """ import json +from datetime import datetime +from typing import Dict, Any, List +from app.core.config import model_text + +# ----------------------------- +# Schema guard +# ----------------------------- +REQUIRED_KEYS = { + "leverage_score": int, + "favor_direction": str, + "trap_clauses": list, + "negotiation_tips": list +} + +def log_step(step: str): + print(f"[{datetime.now().isoformat()}] {step}") + +def validate_and_fix(output: dict) -> dict: + """Ensure required keys exist and values are valid.""" + fixed = {} + for key, key_type in REQUIRED_KEYS.items(): + if key not in output: + fixed[key] = [] if key_type == list else None + else: + fixed[key] = output[key] + + # Clamp leverage score + if isinstance(fixed["leverage_score"], int): + fixed["leverage_score"] = max(0, min(100, fixed["leverage_score"])) + + # Normalize favor direction + if fixed["favor_direction"] not in ["Vendor", "Client", "Neutral"]: + fixed["favor_direction"] = "Neutral" + + return fixed + +def chunk_text(text: str, max_len: int = 40000) -> List[str]: + """Split very large proposals into manageable chunks.""" + return [text[i:i+max_len] for i in range(0, len(text), max_len)] + +# ----------------------------- +# Main Function +# ----------------------------- +async def analyze_proposal_leverage(proposal_text: str, retries: int = 2) -> Dict[str, Any]: + """ + Legal Audit / Negotiation Agent: + Detect leverage, hidden risks, and negotiation tips. + Handles large proposals, JSON drift, and retry on failure. + """ + log_step("Preparing system prompt for Legal Audit") + + system_prompt = """ +You are SpecGap, a ruthless corporate lawyer. + +TASK: +Audit provided business documents (may contain multiple files). + +GOALS: +1. Check if Proposal meets Requirements. +2. Score leverage (0–100). +3. Detect hidden or dangerous clauses. +4. Provide exact redline text for High or Critical risks. + +RULES: +- Cite exact clause text. +- Do not invent clauses. +- If no risks exist, return empty arrays. +- Redline text must be legally enforceable. +- This is a hypothetical risk analysis, not legal advice. + +SEVERITY RUBRIC: +Critical = unlimited liability, IP ownership transfer, uncapped indemnity +High = asymmetric termination, vague scope, jurisdiction mismatch +Medium = missing SLAs, unclear payments +Low = ambiguity only + +OUTPUT JSON ONLY: +{ + "leverage_score": 0-100, + "favor_direction": "Vendor|Client|Neutral", + "trap_clauses": [...], + "negotiation_tips": ["..."] +} +""" + + # Chunk if text is too long + chunks = chunk_text(proposal_text) + + # Combine prompt + chunks + prompts = [f"{system_prompt}\n\n--- DOCUMENTS (chunk {i+1}) ---\n{chunk}" + for i, chunk in enumerate(chunks)] + full_prompt = "\n".join(prompts) if len(prompts) > 1 else prompts[0] + + attempt = 0 + while attempt <= retries: + try: + log_step(f"Calling model_text.generate_content_async (attempt {attempt+1})") + response = await model_text.generate_content_async(full_prompt) + + cleaned = response.text.strip() + if cleaned.startswith("```"): + cleaned = cleaned.split("```")[1] + + parsed = json.loads(cleaned) + return validate_and_fix(parsed) + + except json.JSONDecodeError: + log_step("JSON parse failed, returning raw output snippet") + return { + "error": "Model output was not valid JSON", + "raw_output": response.text[:1500] + } + + except Exception as e: + log_step(f"Attempt {attempt+1} failed: {e}") + attempt += 1 + + return {"error": "Proposal leverage analysis failed after retries"} import asyncio from typing import Dict, Any diff --git a/specgap/app/services/cross_check.py b/specgap/app/services/cross_check.py index f086c82..b26956d 100644 --- a/specgap/app/services/cross_check.py +++ b/specgap/app/services/cross_check.py @@ -5,6 +5,50 @@ import json import asyncio +from typing import Optional, Dict, Any, Union, List +from app.core.config import model_vision +from datetime import datetime + +# ----------------------------- +# Helper Functions +# ----------------------------- + +def chunk_text(text: str, max_len: int = 40000) -> List[str]: + """Split large text into manageable chunks.""" + return [text[i:i+max_len] for i in range(0, len(text), max_len)] + +def validate_diagram(diagram: Union[str, Dict[str, Any]]) -> str: + """Ensure diagram is a string for prompt inclusion.""" + if isinstance(diagram, dict): + return json.dumps(diagram, indent=2) + return str(diagram) + +def extract_patch_pack(result: Dict[str, Any]) -> Dict[str, Any]: + """Extract jira tickets and negotiation email from Orchestrator output.""" + patch = result.get("PATCH_PACK", {}) + return { + "jira_tickets": patch.get("jira_tickets", []), + "negotiation_email": patch.get("negotiation_email", "") + } + +def validate_json(raw_text: str) -> Dict[str, Any]: + """Safely parse JSON from model output.""" + try: + if raw_text.startswith("```json"): + raw_text = raw_text[7:] + if raw_text.endswith("```"): + raw_text = raw_text[:-3] + return json.loads(raw_text) + except json.JSONDecodeError: + return {"error": "Failed to parse JSON", "raw_response": raw_text} + +def log_step(step: str): + """Simple timestamped logging.""" + print(f"[{datetime.now().isoformat()}] {step}") + +# ----------------------------- +# Main Orchestrator Function +# ----------------------------- from typing import Dict, Any, Optional from app.core.config import model_vision, settings @@ -85,6 +129,79 @@ def _clean_json_response(text: str) -> str: async def run_cross_check( tech_text: str, proposal_text: str, + diagram_data: Optional[Union[str, Dict[str, Any]]] = None, + tech_report: Optional[Dict[str, Any]] = None, + legal_report: Optional[Dict[str, Any]] = None, + max_text_length: int = 40000, + retries: int = 2 +) -> Dict[str, Any]: + """ + Orchestrator agent: Validates and synthesizes outputs from Tech & Legal agents. + Supports chunking, retries, logging, and safe JSON parsing. + """ + + log_step("Preparing system instructions and input text") + system_instruction = """ + Role: You are SpecGap, the Chief Technology & Legal Officer (The Orchestrator). + + Task: Validate and Synthesize findings from Tech Auditor & Legal Negotiator. + + Goal: + 1. VERIFY contradictions and interactions. + 2. VISUALIZE architecture via Mermaid diagram. + 3. ACTION: Generate final Patch Pack (Jira + negotiation email). + + Output strictly as JSON: + - CONTRADICTIONS + - STRATEGIC_SYNTHESIS + - REALITY_DIAGRAM_MERMAID + - PATCH_PACK + """ + + # Prepare text chunks + tech_chunks = chunk_text(tech_text, max_text_length) + proposal_chunks = chunk_text(proposal_text, max_text_length) + diagram_str = validate_diagram(diagram_data) if diagram_data else None + + prompt_parts = [system_instruction] + + for i, (tech_chunk, proposal_chunk) in enumerate(zip(tech_chunks, proposal_chunks)): + log_step(f"Adding chunk {i+1} to prompt") + prompt_parts.append(f"\n--- TECH SPEC (chunk {i+1}) ---\n{tech_chunk}") + prompt_parts.append(f"\n--- PROPOSAL (chunk {i+1}) ---\n{proposal_chunk}") + + if tech_report: + prompt_parts.append(f"\n--- PRIOR TECH AUDIT ---\n{json.dumps(tech_report, indent=2)}") + if legal_report: + prompt_parts.append(f"\n--- PRIOR LEGAL AUDIT ---\n{json.dumps(legal_report, indent=2)}") + if diagram_str: + prompt_parts.append(f"\n--- ARCHITECTURE DIAGRAM ---\n{diagram_str}") + + prompt_parts.append("\nGenerate the Synthesized JSON Report now.") + + # Retry loop + attempt = 0 + while attempt <= retries: + try: + log_step(f"Calling model_vision (attempt {attempt+1})") + response = await model_vision.generate_content_async(prompt_parts) + result = validate_json(response.text.strip()) + log_step("Cross-check successful") + return result + except Exception as e: + log_step(f"Attempt {attempt+1} failed: {e}") + attempt += 1 + return {"error": "Cross-check failed after retries"} + +# ----------------------------- +# Optional Convenience Function +# ----------------------------- +async def run_and_extract_patch_pack(*args, **kwargs) -> Dict[str, Any]: + """ + Run cross-check and directly return the Patch Pack (jira + email) + """ + result = await run_cross_check(*args, **kwargs) + return extract_patch_pack(result) diagram_data: Optional[dict] = None, tech_report: Optional[dict] = None, legal_report: Optional[dict] = None, diff --git a/specgap/app/services/tech_engine.py b/specgap/app/services/tech_engine.py index a8380fe..1d4e596 100644 --- a/specgap/app/services/tech_engine.py +++ b/specgap/app/services/tech_engine.py @@ -4,6 +4,82 @@ """ import json +from datetime import datetime +from typing import List, Dict, Any +from app.core.config import model_text + +# ----------------------------- +# Helper Functions +# ----------------------------- + +def chunk_text(text: str, max_len: int = 40000) -> List[str]: + """Split large text into manageable chunks.""" + return [text[i:i+max_len] for i in range(0, len(text), max_len)] + +def validate_json(raw_text: str) -> Dict[str, Any]: + """Safely parse JSON from model output.""" + try: + if raw_text.startswith("```json"): + raw_text = raw_text[7:] + if raw_text.endswith("```"): + raw_text = raw_text[:-3] + return json.loads(raw_text) + except json.JSONDecodeError: + return {"error": "Failed to parse JSON", "raw_response": raw_text} + +def log_step(step: str): + """Simple timestamped logging.""" + print(f"[{datetime.now().isoformat()}] {step}") + +# ----------------------------- +# Main Function +# ----------------------------- + +async def analyze_tech_gaps( + spec_text: str, + max_text_length: int = 40000, + retries: int = 2 +) -> Dict[str, Any]: + """ + Tech Gap Analysis Agent: + Detects missing components, consistency errors, and ambiguity in technical specifications. + Supports chunking, retries, and safe JSON parsing. + """ + + log_step("Preparing system prompt for Tech Gap Analysis") + system_prompt = """ + Role: You are SpecGap, a Senior Principal Software Architect. + Task: Perform 'ABSENCE DETECTION' and 'CONSISTENCY CHECK' on the provided documents. + + NOTE: The input may contain MULTIPLE documents (e.g., Requirements and Proposals), + separated by '=== SOURCE DOCUMENT: [Name] ==='. + + Instructions: + 1. CROSS-REFERENCE: If File A (Requirements) asks for a feature, check if File B (Proposal) implements it. + 2. Analyze the text for mentioned features that lack defined logic. + (e.g., If 'Auth' is mentioned but no 'Token Expiry' or 'OAuth provider' is defined, flag it). + 3. Look for 'Happy Path Bias' (where only success scenarios are described, but error states are missing). + + CRITICAL INSTRUCTION: CITATIONS REQUIRED + For every gap found, provide a "source_reference". + - Quote the exact text from the document. + - Mention which SOURCE FILE the text comes from. + + Output Format: + Return ONLY valid JSON. + { + "project_name": "String", + "critical_gaps": [ + { + "feature": "Name", + "missing_component": "What is missing", + "risk_level": "High/Medium/Low", + "recommendation": "Advice", + "source_reference": "In 'Proposal.pdf', section 4 mentions X but 'Requirements.pdf' demanded Y." + } + ], + "ambiguity_score": Integer (0-100) + } import asyncio from typing import Dict, Any @@ -114,6 +190,33 @@ async def analyze_tech_gaps( details="Empty response" ) + # 1. Chunk the spec_text if too long + chunks = chunk_text(spec_text, max_text_length) + + full_prompt = [] + for i, chunk in enumerate(chunks): + log_step(f"Adding chunk {i+1} to prompt") + full_prompt.append(f"{system_prompt}\n--- TECHNICAL SPEC (chunk {i+1}) ---\n{chunk}") + + # Retry mechanism + attempt = 0 + while attempt <= retries: + try: + log_step(f"Calling model_text.generate_content_async (attempt {attempt+1})") + # If multiple chunks, join into one prompt + combined_prompt = "\n".join(full_prompt) if len(full_prompt) > 1 else full_prompt[0] + response = await model_text.generate_content_async(combined_prompt) + + log_step("Cleaning and validating JSON output") + result = validate_json(response.text.strip()) + log_step("Tech Gap Analysis successful") + return result + + except Exception as e: + log_step(f"Attempt {attempt+1} failed: {e}") + attempt += 1 + + return {"error": "Tech Gap Analysis failed after retries"} cleaned = _clean_json_response(response.text) result = json.loads(cleaned) diff --git a/specgap/specgap_audits.db b/specgap/specgap_audits.db new file mode 100644 index 0000000..687e6f4 Binary files /dev/null and b/specgap/specgap_audits.db differ