From 262e85634cc43c922d21fa2b76723fd76235dfa9 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 29 Jun 2026 17:35:53 -0400 Subject: [PATCH] Jun 29 - Improve AI analysis function --- routes/ai_summary.py | 300 +++++++++++++++++++++++++++++++------------ 1 file changed, 217 insertions(+), 83 deletions(-) diff --git a/routes/ai_summary.py b/routes/ai_summary.py index f296076..bcef04e 100644 --- a/routes/ai_summary.py +++ b/routes/ai_summary.py @@ -182,97 +182,199 @@ def _extract_text(file_obj, ext: str) -> str: # Office address used as origin for distance/travel-time estimates. _OFFICE_ADDRESS = "2815 Hartland Road, Falls Church, VA 22043, USA" +# Text character limit sent to the API. llama-3.3-70b supports 128K tokens (~500K chars); +# 60K chars is a safe ceiling that leaves room for the prompt and response. +_TEXT_LIMIT = 60_000 + +# Analyst persona injected as the system message in every API call. +_SYSTEM_PROMPT = ( + "You are a senior government procurement analyst supporting a small business BD team. " + "Your role is to extract, organize, and evaluate information from solicitation documents " + "with precision and clarity. Format all output in well-structured Markdown using tables " + "and headers. Never invent or assume information not present in the source documents — " + "write \"Not specified\" for any missing field." +) + # Stage 1 — extraction prompt (always sent) -_EXTRACTION_PROMPT = """You are an expert government procurement analyst. -The user has provided {n} document(s). Treat all provided content as a single combined source — do not analyze each file individually. Extract the following information once, consolidating details from all documents. +_EXTRACTION_PROMPT = """\ +Analyze the {n} attached government solicitation document(s) as one combined package. +Complete every section below exactly as structured. Write **"Not specified"** for any field +not found in the documents. Do not guess or invent details. -IMPORTANT — Our office is located at: - {office} -Use this as the ORIGIN address for all driving distance and travel time calculations in field #9 below. +Our office is located at: **{office}** — use this as the origin for all travel estimates. -Extract and clearly label the following fields (write "N/A" if not found): +--- - 1. Solicitation Number - 2. Solicitation Type (e.g. RFP, RFQ, IFB, etc.) - 3. Set-Aside (e.g. Small Business, 8(a), N/A) - 4. Description / Scope of Work - 5. Work Site / Location(s) - 6. Pre-Proposal Conference / Site-Visit (date, time, full address) - 7. Point of Contact (POC) (name, phone, email) - 8. Total Square Footage (if applicable) - 9. Driving Distance & Travel Time - - Origin: {office} - - Destination: Pre-Proposal Conference or primary Work Site address - - Provide your best estimate of driving distance (miles) and typical driving time using major highways - - Note that these are AI estimates; actual times may vary with traffic - 10. Last Day to Submit Questions - 11. Due Date & Time - 12. Any other notable requirements or deadlines +## Solicitation Overview -Then provide a detailed OVERALL SUMMARY covering: +| Field | Value | +|---|---| +| **Solicitation Number** | | +| **Solicitation Type** | (RFP / RFQ / IFB / IDIQ / BPA / etc.) | +| **Issuing Agency / Office** | | +| **Set-Aside** | (Small Business / 8(a) / SDVOSB / HUBZone / WOSB / Unrestricted / etc.) | +| **Contract Type** | (Firm-Fixed-Price / T&M / Cost-Plus / IDIQ / etc.) | +| **Contract Period** | | +| **NAICS Code** | | +| **Estimated Value** | | - A. Scope of Work - B. Contract Period - C. Proposal Submission Requirements - D. Key Deadlines & Action Items +--- -Be precise, detailed, and use bullet points throughout. -If information is not explicitly stated in the documents, note it as "Not specified in the document." +## Scope of Work + +*Describe what is required in full detail: services, deliverables, performance standards, and any technical requirements. Use bullet points.* + +--- + +## Performance Location(s) + +*List all work sites. Note whether remote or on-site work is permitted.* + +--- + +## Key Dates & Deadlines + +| Milestone | Date & Time (with timezone) | +|---|---| +| Pre-Proposal Conference / Site Visit | | +| Questions Due | | +| **Proposal Due** | | +| Award Date (if stated) | | +| Period of Performance Start | | +| Other Deadlines | | + +--- + +## Pre-Proposal Conference / Site Visit + +| | | +|---|---| +| **Date & Time** | | +| **Full Address** | | +| **Attendance** | (Mandatory / Optional / Not applicable) | +| **RSVP Required** | (Yes — deadline & method / No) | + +--- + +## Point of Contact (POC) + +| | | +|---|---| +| **Name / Title** | | +| **Phone** | | +| **Email** | | +| **Questions Submission Method** | | + +--- + +## Proposal Requirements + +*Summarize format, page limits, required sections/volumes, submission method (portal/email/mail), and number of copies. Use bullet points.* + +--- + +## Evaluation Factors + +*List evaluation criteria and their weights or order of priority as stated in the solicitation.* + +--- + +## Travel & Logistics + +| | | +|---|---| +| **Origin** | {office} | +| **Destination** | (Pre-Proposal / Primary Work Site address) | +| **Estimated Driving Distance** | | +| **Estimated Drive Time** | (normal traffic conditions) | + +*(AI estimate — verify with a mapping service before scheduling travel.)* + +--- + +## Notable Requirements & Red Flags + +*List anything that affects bid/no-bid decisions: unusual insurance or bonding levels, required security clearances, certifications, teaming or subcontracting restrictions, incumbent advantage indicators, aggressive timelines, or any other risk factors.* + +--- + +## Total Square Footage + +*(If applicable to the scope of work; write "N/A" if not.)* + +--- + +## Overall Summary + +Provide a concise but thorough summary covering: + +- **What is Being Procured** — Scope and key deliverables. +- **Contract Period & Estimated Value** — Duration and any stated ceiling or estimate. +- **Key Constraints & Requirements** — Timeline, location, special certifications, etc. +- **Immediate Action Items** — What the team must do and by when. + +--- DOCUMENTS: {documents}""" -# Stage 2 — criteria evaluation suffix (appended only when active criteria exist) -_CRITERIA_PROMPT_SUFFIX = """ +# Stage 2 — criteria evaluation (sent as a separate API call when active criteria exist). +# Receives the clean Stage 1 extraction output as {summary}, not the raw documents, +# so the model can focus entirely on the evaluation without re-parsing document noise. +_CRITERIA_PROMPT = """\ +Below is an extracted and summarized government solicitation opportunity. +Evaluate whether our company should pursue it based on our evaluation criteria. -================================================================================ -OPPORTUNITY ALIGNMENT EVALUATION -================================================================================ +--- -After completing the extraction and summary above, evaluate whether this -opportunity aligns with our company's interests based on the following criteria. +## OPPORTUNITY SUMMARY + +{summary} + +--- + +## ALIGNMENT EVALUATION + +**Our evaluation criteria:** -OUR EVALUATION CRITERIA: {criteria_list} -For EACH criterion above: - - State whether the opportunity MEETS, DOES NOT MEET, or PARTIALLY MEETS it. - - Provide a brief, specific explanation citing details from the document(s). +For **each criterion** listed above, provide: +- A status: ✅ **MEETS** / ⚠️ **PARTIALLY MEETS** / ❌ **DOES NOT MEET** / ❓ **CANNOT DETERMINE** +- A 1–2 sentence explanation citing specific details from the summary above. -Then provide an OVERALL RECOMMENDATION using EXACTLY one of these three labels -on its own line (this label is machine-read — do not alter it): +--- - RECOMMENDATION: PURSUE - RECOMMENDATION: PASS - RECOMMENDATION: UNCLEAR +## Overall Recommendation -Use PURSUE if the opportunity clearly meets most criteria and presents strong -alignment. Use PASS if it clearly fails key criteria. Use UNCLEAR if the -documents lack sufficient information to make a confident determination. +State your recommendation on its own line in **exactly** this format (required for system parsing — do not alter the label): -End with a 2-3 sentence EXECUTIVE SUMMARY explaining your recommendation -in plain business language.""" +``` +RECOMMENDATION: PURSUE +``` +or +``` +RECOMMENDATION: PASS +``` +or +``` +RECOMMENDATION: UNCLEAR +``` + +**PURSUE** — opportunity clearly aligns with most criteria and is competitive. +**PASS** — fails one or more critical criteria or presents unacceptable risk. +**UNCLEAR** — insufficient information to make a confident determination. + +--- + +## Executive Summary + +Write 3–4 sentences in plain business language explaining your recommendation: +the strongest reasons to pursue or pass, and the single biggest risk or opportunity.""" -def _call_groq(api_key: str, model: str, text: str, criteria: list) -> dict: - """Call the Groq chat completions REST API directly (no SDK required).""" - import re - - n = text.count("=== ") or 1 # count file separators for the prompt header - truncated = len(text) > 14000 - - # Build the two-stage prompt matching the desktop app exactly - prompt = _EXTRACTION_PROMPT.format( - n=n, office=_OFFICE_ADDRESS, documents=text[:14000] - ) - - if criteria: - criteria_list = "\n".join( - f" {i+1}. {c['title']}: {c['description']}" - for i, c in enumerate(criteria) - ) - prompt += _CRITERIA_PROMPT_SUFFIX.format(criteria_list=criteria_list) - +def _call_groq_api(api_key: str, model: str, user_message: str) -> str: + """Single Groq chat completions call. Raises on HTTP error.""" response = http_requests.post( "https://api.groq.com/openai/v1/chat/completions", headers={ @@ -281,29 +383,61 @@ def _call_groq(api_key: str, model: str, text: str, criteria: list) -> dict: }, json={ "model": model, - "messages": [{"role": "user", "content": prompt}], - "max_tokens": 4096, - "temperature": 0.2, + "messages": [ + {"role": "system", "content": _SYSTEM_PROMPT}, + {"role": "user", "content": user_message}, + ], + "max_tokens": 8192, + "temperature": 0.1, }, - timeout=90, + timeout=120, ) if not response.ok: logger.error(f"Groq API {response.status_code}: {response.text[:300]}") response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] or "" - content = response.json()["choices"][0]["message"]["content"] or "" - # Parse the machine-readable RECOMMENDATION label (only present when criteria used) +def _call_groq(api_key: str, model: str, text: str, criteria: list) -> dict: + """ + Two-stage Groq analysis: + Stage 1 (always): extract structured fields + overall summary from documents. + Stage 2 (optional): evaluate criteria against the Stage 1 output (not raw docs), + producing a focused RECOMMENDATION with per-criterion scoring. + """ + truncated = len(text) > _TEXT_LIMIT + doc_text = text[:_TEXT_LIMIT] + n = doc_text.count("=== ") or 1 + + # ── Stage 1: extraction ──────────────────────────────────────────────────── + stage1_prompt = _EXTRACTION_PROMPT.format( + n=n, office=_OFFICE_ADDRESS, documents=doc_text + ) + summary = _call_groq_api(api_key, model, stage1_prompt) + + if not criteria: + return {"verdict": None, "summary": summary, "truncated": truncated} + + # ── Stage 2: criteria evaluation (uses clean summary, not raw docs) ──────── + criteria_list = "\n".join( + f"{i+1}. **{c['title']}**: {c['description']}" + for i, c in enumerate(criteria) + ) + stage2_prompt = _CRITERIA_PROMPT.format( + summary=summary, criteria_list=criteria_list + ) + evaluation = _call_groq_api(api_key, model, stage2_prompt) + verdict = None - if criteria: - match = re.search( - r"RECOMMENDATION\s*:\s*(PURSUE|PASS|UNCLEAR)", - content, re.IGNORECASE, - ) - if match: - verdict = match.group(1).upper() + match = re.search( + r"RECOMMENDATION\s*:\s*(PURSUE|PASS|UNCLEAR)", + evaluation, re.IGNORECASE, + ) + if match: + verdict = match.group(1).upper() - return {"verdict": verdict, "summary": content, "truncated": truncated} + combined = summary + "\n\n---\n\n" + evaluation + return {"verdict": verdict, "summary": combined, "truncated": truncated} # ─── Criteria Management (admin only) ─────────────────────────────────────────