Jun 29 - Improve AI analysis function
This commit is contained in:
+217
-83
@@ -182,97 +182,199 @@ def _extract_text(file_obj, ext: str) -> str:
|
|||||||
# Office address used as origin for distance/travel-time estimates.
|
# Office address used as origin for distance/travel-time estimates.
|
||||||
_OFFICE_ADDRESS = "2815 Hartland Road, Falls Church, VA 22043, USA"
|
_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)
|
# Stage 1 — extraction prompt (always sent)
|
||||||
_EXTRACTION_PROMPT = """You are an expert government procurement analyst.
|
_EXTRACTION_PROMPT = """\
|
||||||
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.
|
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:
|
Our office is located at: **{office}** — use this as the origin for all travel estimates.
|
||||||
{office}
|
|
||||||
Use this as the ORIGIN address for all driving distance and travel time calculations in field #9 below.
|
|
||||||
|
|
||||||
Extract and clearly label the following fields (write "N/A" if not found):
|
---
|
||||||
|
|
||||||
1. Solicitation Number
|
## Solicitation Overview
|
||||||
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
|
|
||||||
|
|
||||||
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.
|
## Scope of Work
|
||||||
If information is not explicitly stated in the documents, note it as "Not specified in the document."
|
|
||||||
|
*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:
|
||||||
{documents}"""
|
{documents}"""
|
||||||
|
|
||||||
# Stage 2 — criteria evaluation suffix (appended only when active criteria exist)
|
# Stage 2 — criteria evaluation (sent as a separate API call when active criteria exist).
|
||||||
_CRITERIA_PROMPT_SUFFIX = """
|
# 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 SUMMARY
|
||||||
opportunity aligns with our company's interests based on the following criteria.
|
|
||||||
|
{summary}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ALIGNMENT EVALUATION
|
||||||
|
|
||||||
|
**Our evaluation criteria:**
|
||||||
|
|
||||||
OUR EVALUATION CRITERIA:
|
|
||||||
{criteria_list}
|
{criteria_list}
|
||||||
|
|
||||||
For EACH criterion above:
|
For **each criterion** listed above, provide:
|
||||||
- State whether the opportunity MEETS, DOES NOT MEET, or PARTIALLY MEETS it.
|
- A status: ✅ **MEETS** / ⚠️ **PARTIALLY MEETS** / ❌ **DOES NOT MEET** / ❓ **CANNOT DETERMINE**
|
||||||
- Provide a brief, specific explanation citing details from the document(s).
|
- 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
|
## Overall Recommendation
|
||||||
RECOMMENDATION: PASS
|
|
||||||
RECOMMENDATION: UNCLEAR
|
|
||||||
|
|
||||||
Use PURSUE if the opportunity clearly meets most criteria and presents strong
|
State your recommendation on its own line in **exactly** this format (required for system parsing — do not alter the label):
|
||||||
alignment. Use PASS if it clearly fails key criteria. Use UNCLEAR if the
|
|
||||||
documents lack sufficient information to make a confident determination.
|
|
||||||
|
|
||||||
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:
|
def _call_groq_api(api_key: str, model: str, user_message: str) -> str:
|
||||||
"""Call the Groq chat completions REST API directly (no SDK required)."""
|
"""Single Groq chat completions call. Raises on HTTP error."""
|
||||||
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)
|
|
||||||
|
|
||||||
response = http_requests.post(
|
response = http_requests.post(
|
||||||
"https://api.groq.com/openai/v1/chat/completions",
|
"https://api.groq.com/openai/v1/chat/completions",
|
||||||
headers={
|
headers={
|
||||||
@@ -281,29 +383,61 @@ def _call_groq(api_key: str, model: str, text: str, criteria: list) -> dict:
|
|||||||
},
|
},
|
||||||
json={
|
json={
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [
|
||||||
"max_tokens": 4096,
|
{"role": "system", "content": _SYSTEM_PROMPT},
|
||||||
"temperature": 0.2,
|
{"role": "user", "content": user_message},
|
||||||
|
],
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"temperature": 0.1,
|
||||||
},
|
},
|
||||||
timeout=90,
|
timeout=120,
|
||||||
)
|
)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
logger.error(f"Groq API {response.status_code}: {response.text[:300]}")
|
logger.error(f"Groq API {response.status_code}: {response.text[:300]}")
|
||||||
response.raise_for_status()
|
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
|
verdict = None
|
||||||
if criteria:
|
match = re.search(
|
||||||
match = re.search(
|
r"RECOMMENDATION\s*:\s*(PURSUE|PASS|UNCLEAR)",
|
||||||
r"RECOMMENDATION\s*:\s*(PURSUE|PASS|UNCLEAR)",
|
evaluation, re.IGNORECASE,
|
||||||
content, re.IGNORECASE,
|
)
|
||||||
)
|
if match:
|
||||||
if match:
|
verdict = match.group(1).upper()
|
||||||
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) ─────────────────────────────────────────
|
# ─── Criteria Management (admin only) ─────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user