Jun 29 - Improve AI analysis function fix 429 error
This commit is contained in:
+34
-66
@@ -187,10 +187,6 @@ _OFFICE_ADDRESS = "2815 Hartland Road, Falls Church, VA 22043, USA"
|
|||||||
# JSON payload well under Groq's request size limit on all plan tiers.
|
# JSON payload well under Groq's request size limit on all plan tiers.
|
||||||
_TEXT_LIMIT = 14_000
|
_TEXT_LIMIT = 14_000
|
||||||
|
|
||||||
# Summary passed to stage 2. Stage 1 can produce up to ~16 KB of output;
|
|
||||||
# capping it here prevents stage 2's payload from growing unbounded.
|
|
||||||
_SUMMARY_LIMIT = 8_000
|
|
||||||
|
|
||||||
# Analyst persona — injected as the system message in every API call.
|
# Analyst persona — injected as the system message in every API call.
|
||||||
_SYSTEM_PROMPT = (
|
_SYSTEM_PROMPT = (
|
||||||
"You are a senior government procurement analyst supporting a small business BD team. "
|
"You are a senior government procurement analyst supporting a small business BD team. "
|
||||||
@@ -319,34 +315,25 @@ Our office: **{office}** — use as origin for travel estimates.
|
|||||||
DOCUMENTS:
|
DOCUMENTS:
|
||||||
{documents}"""
|
{documents}"""
|
||||||
|
|
||||||
# Stage 2 — criteria evaluation (separate API call, only when active criteria exist).
|
# Criteria evaluation — appended to the extraction prompt in the same API call.
|
||||||
# Receives a capped slice of the stage 1 summary — not the raw documents — so the
|
# A single call avoids the rate-limit (429) that two back-to-back calls trigger
|
||||||
# model can focus on evaluation without re-parsing source noise.
|
# on Groq's free tier.
|
||||||
_CRITERIA_PROMPT = """\
|
_CRITERIA_SUFFIX = """
|
||||||
Below is an extracted summary of a government solicitation. Evaluate whether our
|
|
||||||
company should pursue it based on the criteria listed.
|
|
||||||
|
|
||||||
## OPPORTUNITY SUMMARY
|
|
||||||
|
|
||||||
{summary}
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ALIGNMENT EVALUATION
|
## Opportunity Alignment Evaluation
|
||||||
|
|
||||||
Our criteria:
|
Now evaluate this opportunity against our company's criteria below.
|
||||||
|
|
||||||
|
**Our criteria:**
|
||||||
{criteria_list}
|
{criteria_list}
|
||||||
|
|
||||||
For each criterion, state:
|
For each criterion provide:
|
||||||
- ✅ MEETS / ⚠️ PARTIALLY MEETS / ❌ DOES NOT MEET / ❓ CANNOT DETERMINE
|
- ✅ MEETS / ⚠️ PARTIALLY MEETS / ❌ DOES NOT MEET / ❓ CANNOT DETERMINE
|
||||||
- One or two sentences citing specific details from the summary.
|
- 1–2 sentences with specific evidence from the documents above.
|
||||||
|
|
||||||
---
|
**Overall Recommendation** — write exactly one of these lines (machine-read, do not alter):
|
||||||
|
|
||||||
## Overall Recommendation
|
|
||||||
|
|
||||||
Write your recommendation on its own line in exactly one of these forms
|
|
||||||
(machine-read — do not alter the label):
|
|
||||||
|
|
||||||
RECOMMENDATION: PURSUE
|
RECOMMENDATION: PURSUE
|
||||||
RECOMMENDATION: PASS
|
RECOMMENDATION: PASS
|
||||||
@@ -356,14 +343,26 @@ PURSUE = clearly aligns with most criteria and is competitive.
|
|||||||
PASS = fails one or more critical criteria or presents unacceptable risk.
|
PASS = fails one or more critical criteria or presents unacceptable risk.
|
||||||
UNCLEAR = insufficient information for a confident decision.
|
UNCLEAR = insufficient information for a confident decision.
|
||||||
|
|
||||||
## Executive Summary
|
**Executive Summary:** 3–4 sentences in plain business language explaining the recommendation."""
|
||||||
|
|
||||||
3–4 sentences in plain business language: strongest reasons to pursue or pass,
|
|
||||||
and the single biggest risk or opportunity."""
|
|
||||||
|
|
||||||
|
|
||||||
def _call_groq_api(api_key: str, model: str, user_message: str) -> str:
|
def _call_groq(api_key: str, model: str, text: str, criteria: list) -> dict:
|
||||||
"""Single Groq chat completions call. Raises on HTTP error."""
|
"""Single Groq API call: extraction + optional criteria evaluation in one request."""
|
||||||
|
truncated = len(text) > _TEXT_LIMIT
|
||||||
|
doc_text = text[:_TEXT_LIMIT]
|
||||||
|
n = doc_text.count("=== ") or 1
|
||||||
|
|
||||||
|
prompt = _EXTRACTION_PROMPT.format(
|
||||||
|
n=n, office=_OFFICE_ADDRESS, documents=doc_text
|
||||||
|
)
|
||||||
|
|
||||||
|
if criteria:
|
||||||
|
criteria_list = "\n".join(
|
||||||
|
f"{i+1}. **{c['title']}**: {c['description']}"
|
||||||
|
for i, c in enumerate(criteria)
|
||||||
|
)
|
||||||
|
prompt += _CRITERIA_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={
|
||||||
@@ -374,7 +373,7 @@ def _call_groq_api(api_key: str, model: str, user_message: str) -> str:
|
|||||||
"model": model,
|
"model": model,
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": _SYSTEM_PROMPT},
|
{"role": "system", "content": _SYSTEM_PROMPT},
|
||||||
{"role": "user", "content": user_message},
|
{"role": "user", "content": prompt},
|
||||||
],
|
],
|
||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
"temperature": 0.1,
|
"temperature": 0.1,
|
||||||
@@ -384,50 +383,19 @@ def _call_groq_api(api_key: str, model: str, user_message: str) -> str:
|
|||||||
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 ""
|
||||||
def _call_groq(api_key: str, model: str, text: str, criteria: list) -> dict:
|
|
||||||
"""
|
|
||||||
Two-stage Groq analysis:
|
|
||||||
Stage 1 (always): extract structured fields + summary from the documents.
|
|
||||||
Stage 2 (optional): evaluate criteria against the capped stage-1 summary,
|
|
||||||
not the raw docs, for a focused and reliable result.
|
|
||||||
"""
|
|
||||||
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 ───────────────────────────────────────────
|
|
||||||
criteria_list = "\n".join(
|
|
||||||
f"{i+1}. **{c['title']}**: {c['description']}"
|
|
||||||
for i, c in enumerate(criteria)
|
|
||||||
)
|
|
||||||
stage2_prompt = _CRITERIA_PROMPT.format(
|
|
||||||
summary=summary[:_SUMMARY_LIMIT], # cap to control payload size
|
|
||||||
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()
|
||||||
|
|
||||||
combined = summary + "\n\n---\n\n" + evaluation
|
return {"verdict": verdict, "summary": content, "truncated": truncated}
|
||||||
return {"verdict": verdict, "summary": combined, "truncated": truncated}
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Criteria Management (admin only) ─────────────────────────────────────────
|
# ─── Criteria Management (admin only) ─────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user