-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance Agents and Refine Prompt Templates for Improved Output Quality #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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). |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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] | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+118
|
||||||||||||||||||||||||||||
| log_step("JSON parse failed, returning raw output snippet") | |
| return { | |
| "error": "Model output was not valid JSON", | |
| "raw_output": response.text[:1500] | |
| } | |
| log_step(f"JSON parse failed on attempt {attempt+1}") | |
| attempt += 1 | |
| if attempt > retries: | |
| log_step("Max retries reached after JSON parse failures, returning raw output snippet") | |
| return { | |
| "error": "Model output was not valid JSON", | |
| "raw_output": response.text[:1500] | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code-fence stripping is incorrect: for a typical response like
json\n{...}\n,cleaned.split("```")[1]yieldsjson\n{...}which is not valid JSON and will reliably causejson.loadsto fail. Use a more robust fence remover (strip leadingjson/and surrounding whitespace) before parsing.