Apply ruff formatting to all Lambda Python files

This commit is contained in:
Adam Moussa 2026-05-17 13:04:06 -04:00
parent 456f0d4252
commit 539878e84e
5 changed files with 390 additions and 243 deletions

View file

@ -132,10 +132,12 @@ def format_proposal_document(proposal: dict, line_items: list[dict]) -> str:
lines.append(f"| {i} | {desc} | {qty} | {unit} | {up_str} | {tp_str} |") lines.append(f"| {i} | {desc} | {qty} | {unit} | {up_str} | {tp_str} |")
lines.extend([ lines.extend(
"", [
f"**Total: ${total:,.2f}**", "",
]) f"**Total: ${total:,.2f}**",
]
)
return "\n".join(lines) return "\n".join(lines)

View file

@ -23,7 +23,14 @@ import pdfplumber
try: try:
from rich.console import Console from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn from rich.progress import (
Progress,
SpinnerColumn,
TextColumn,
BarColumn,
TaskProgressColumn,
)
console = Console() console = Console()
HAS_RICH = True HAS_RICH = True
except ImportError: except ImportError:
@ -39,12 +46,20 @@ SERVICE_CATEGORIES = ["HVAC", "Plumbing", "Electrical", "General", "Renovation"]
def main(): def main():
parser = argparse.ArgumentParser(description="Batch ingest historical proposals into Bedrock KB") parser = argparse.ArgumentParser(
parser.add_argument("--input-dir", required=True, help="Directory containing historical proposal PDFs") description="Batch ingest historical proposals into Bedrock KB"
)
parser.add_argument(
"--input-dir",
required=True,
help="Directory containing historical proposal PDFs",
)
parser.add_argument("--bucket", required=True, help="S3 library bucket name") parser.add_argument("--bucket", required=True, help="S3 library bucket name")
parser.add_argument("--kb-id", required=True, help="Bedrock Knowledge Base ID") parser.add_argument("--kb-id", required=True, help="Bedrock Knowledge Base ID")
parser.add_argument("--ds-id", required=True, help="Bedrock KB Data Source ID") parser.add_argument("--ds-id", required=True, help="Bedrock KB Data Source ID")
parser.add_argument("--dry-run", action="store_true", help="Parse and extract only, don't upload") parser.add_argument(
"--dry-run", action="store_true", help="Parse and extract only, don't upload"
)
args = parser.parse_args() args = parser.parse_args()
input_dir = Path(args.input_dir) input_dir = Path(args.input_dir)
@ -73,7 +88,11 @@ def main():
for pdf_path in pdf_files: for pdf_path in pdf_files:
status = process_pdf(pdf_path, args.bucket, args.dry_run) status = process_pdf(pdf_path, args.bucket, args.dry_run)
results[status] += 1 results[status] += 1
progress.update(task, advance=1, description=f"[{'green' if status == 'success' else 'red'}]{pdf_path.name}") progress.update(
task,
advance=1,
description=f"[{'green' if status == 'success' else 'red'}]{pdf_path.name}",
)
else: else:
for i, pdf_path in enumerate(pdf_files, 1): for i, pdf_path in enumerate(pdf_files, 1):
print(f"[{i}/{len(pdf_files)}] Processing {pdf_path.name}...", end=" ") print(f"[{i}/{len(pdf_files)}] Processing {pdf_path.name}...", end=" ")
@ -81,12 +100,16 @@ def main():
results[status] += 1 results[status] += 1
print(f"[{status.upper()}]") print(f"[{status.upper()}]")
print(f"\nResults: {results['success']} succeeded, {results['failed']} failed, {results['skipped']} skipped") print(
f"\nResults: {results['success']} succeeded, {results['failed']} failed, {results['skipped']} skipped"
)
if not args.dry_run and results["success"] > 0: if not args.dry_run and results["success"] > 0:
print("\nTriggering Knowledge Base sync...") print("\nTriggering Knowledge Base sync...")
trigger_kb_sync(args.kb_id, args.ds_id) trigger_kb_sync(args.kb_id, args.ds_id)
print("Done. KB ingestion job started — check AWS console for completion status.") print(
"Done. KB ingestion job started — check AWS console for completion status."
)
def process_pdf(pdf_path: Path, bucket: str, dry_run: bool) -> str: def process_pdf(pdf_path: Path, bucket: str, dry_run: bool) -> str:
@ -163,23 +186,25 @@ def extract_with_claude(pdf_path: Path) -> dict:
modelId=MODEL_ID, modelId=MODEL_ID,
contentType="application/json", contentType="application/json",
accept="application/json", accept="application/json",
body=json.dumps({ body=json.dumps(
"anthropic_version": "bedrock-2023-05-31", {
"max_tokens": 4096, "anthropic_version": "bedrock-2023-05-31",
"messages": [{ "max_tokens": 4096,
"role": "user", "messages": [
"content": [
{ {
"type": "document", "role": "user",
"source": { "content": [
"type": "base64", {
"media_type": "application/pdf", "type": "document",
"data": pdf_b64, "source": {
}, "type": "base64",
}, "media_type": "application/pdf",
{ "data": pdf_b64,
"type": "text", },
"text": f"""Extract structured data from this proposal PDF. },
{
"type": "text",
"text": f"""Extract structured data from this proposal PDF.
Return a JSON object with: Return a JSON object with:
- customerName: the customer/client name - customerName: the customer/client name
- serviceCategory: one of {SERVICE_CATEGORIES} - serviceCategory: one of {SERVICE_CATEGORIES}
@ -190,11 +215,13 @@ Return a JSON object with:
- totalAmount: the grand total - totalAmount: the grand total
Respond ONLY with the JSON object.""", Respond ONLY with the JSON object.""",
}, },
],
}
], ],
}], "temperature": 0.1,
"temperature": 0.1, }
}), ),
) )
response_body = json.loads(response["body"].read()) response_body = json.loads(response["body"].read())
@ -219,10 +246,16 @@ def parse_tables(tables: list) -> list[dict]:
header = [str(cell).lower().strip() if cell else "" for cell in table[0]] header = [str(cell).lower().strip() if cell else "" for cell in table[0]]
desc_col = _find_column(header, ["description", "item", "service", "work", "scope"]) desc_col = _find_column(
header, ["description", "item", "service", "work", "scope"]
)
qty_col = _find_column(header, ["qty", "quantity", "count"]) qty_col = _find_column(header, ["qty", "quantity", "count"])
price_col = _find_column(header, ["unit price", "rate", "price/unit", "unit cost"]) price_col = _find_column(
total_col = _find_column(header, ["total", "amount", "ext", "extended", "line total"]) header, ["unit price", "rate", "price/unit", "unit cost"]
)
total_col = _find_column(
header, ["total", "amount", "ext", "extended", "line total"]
)
if desc_col is None: if desc_col is None:
continue continue
@ -232,24 +265,43 @@ def parse_tables(tables: list) -> list[dict]:
continue continue
description = str(row[desc_col]).strip() if row[desc_col] else "" description = str(row[desc_col]).strip() if row[desc_col] else ""
if not description or description.lower() in ("", "total", "subtotal", "grand total"): if not description or description.lower() in (
"",
"total",
"subtotal",
"grand total",
):
continue continue
quantity = _parse_number(row[qty_col]) if qty_col is not None and qty_col < len(row) else None quantity = (
unit_price = _parse_number(row[price_col]) if price_col is not None and price_col < len(row) else None _parse_number(row[qty_col])
total = _parse_number(row[total_col]) if total_col is not None and total_col < len(row) else None if qty_col is not None and qty_col < len(row)
else None
)
unit_price = (
_parse_number(row[price_col])
if price_col is not None and price_col < len(row)
else None
)
total = (
_parse_number(row[total_col])
if total_col is not None and total_col < len(row)
else None
)
if total is None and quantity and unit_price: if total is None and quantity and unit_price:
total = quantity * unit_price total = quantity * unit_price
if description and (total or unit_price): if description and (total or unit_price):
line_items.append({ line_items.append(
"description": description, {
"quantity": quantity or 1, "description": description,
"unit": "each", "quantity": quantity or 1,
"unitPrice": unit_price, "unit": "each",
"totalPrice": total or 0, "unitPrice": unit_price,
}) "totalPrice": total or 0,
}
)
return line_items return line_items

View file

@ -138,10 +138,16 @@ def parse_tables(tables: list) -> list[dict]:
header = [str(cell).lower().strip() if cell else "" for cell in table[0]] header = [str(cell).lower().strip() if cell else "" for cell in table[0]]
desc_col = find_column(header, ["description", "item", "service", "work", "scope"]) desc_col = find_column(
header, ["description", "item", "service", "work", "scope"]
)
qty_col = find_column(header, ["qty", "quantity", "count"]) qty_col = find_column(header, ["qty", "quantity", "count"])
price_col = find_column(header, ["unit price", "rate", "price/unit", "unit cost"]) price_col = find_column(
total_col = find_column(header, ["total", "amount", "ext", "extended", "line total"]) header, ["unit price", "rate", "price/unit", "unit cost"]
)
total_col = find_column(
header, ["total", "amount", "ext", "extended", "line total"]
)
if desc_col is None: if desc_col is None:
continue continue
@ -151,23 +157,42 @@ def parse_tables(tables: list) -> list[dict]:
continue continue
description = str(row[desc_col]).strip() if row[desc_col] else "" description = str(row[desc_col]).strip() if row[desc_col] else ""
if not description or description.lower() in ("", "total", "subtotal", "grand total"): if not description or description.lower() in (
"",
"total",
"subtotal",
"grand total",
):
continue continue
quantity = parse_number(row[qty_col]) if qty_col is not None and qty_col < len(row) else None quantity = (
unit_price = parse_number(row[price_col]) if price_col is not None and price_col < len(row) else None parse_number(row[qty_col])
total = parse_number(row[total_col]) if total_col is not None and total_col < len(row) else None if qty_col is not None and qty_col < len(row)
else None
)
unit_price = (
parse_number(row[price_col])
if price_col is not None and price_col < len(row)
else None
)
total = (
parse_number(row[total_col])
if total_col is not None and total_col < len(row)
else None
)
if total is None and quantity and unit_price: if total is None and quantity and unit_price:
total = quantity * unit_price total = quantity * unit_price
if description and (total or unit_price): if description and (total or unit_price):
line_items.append({ line_items.append(
"description": description, {
"quantity": quantity, "description": description,
"unitPrice": unit_price, "quantity": quantity,
"total": total, "unitPrice": unit_price,
}) "total": total,
}
)
return line_items return line_items
@ -203,34 +228,38 @@ def extract_with_claude_multimodal(pdf_path: str) -> dict:
modelId=MODEL_ID, modelId=MODEL_ID,
contentType="application/json", contentType="application/json",
accept="application/json", accept="application/json",
body=json.dumps({ body=json.dumps(
"anthropic_version": "bedrock-2023-05-31", {
"max_tokens": 4096, "anthropic_version": "bedrock-2023-05-31",
"messages": [{ "max_tokens": 4096,
"role": "user", "messages": [
"content": [
{ {
"type": "document", "role": "user",
"source": { "content": [
"type": "base64", {
"media_type": "application/pdf", "type": "document",
"data": pdf_b64, "source": {
}, "type": "base64",
}, "media_type": "application/pdf",
{ "data": pdf_b64,
"type": "text", },
"text": """Extract all line items from this vendor proposal PDF. },
{
"type": "text",
"text": """Extract all line items from this vendor proposal PDF.
Return a JSON object with these fields: Return a JSON object with these fields:
- vendorName: the vendor/company name - vendorName: the vendor/company name
- lineItems: array of objects with: description, quantity (number or null), unitPrice (number or null), total (number or null) - lineItems: array of objects with: description, quantity (number or null), unitPrice (number or null), total (number or null)
- totalVendorCost: the grand total amount - totalVendorCost: the grand total amount
Respond ONLY with the JSON object, no additional text.""", Respond ONLY with the JSON object, no additional text.""",
}, },
],
}
], ],
}], "temperature": 0.1,
"temperature": 0.1, }
}), ),
) )
response_body = json.loads(response["body"].read()) response_body = json.loads(response["body"].read())
@ -251,7 +280,12 @@ Respond ONLY with the JSON object, no additional text.""",
except Exception as e: except Exception as e:
print(f"Claude multimodal extraction failed: {e}") print(f"Claude multimodal extraction failed: {e}")
return {"vendorName": "", "lineItems": [], "rawText": "", "totalVendorCost": 0.0} return {
"vendorName": "",
"lineItems": [],
"rawText": "",
"totalVendorCost": 0.0,
}
def save_extraction(vendor_proposal_id: str, extracted: dict): def save_extraction(vendor_proposal_id: str, extracted: dict):

View file

@ -160,93 +160,113 @@ def build_pdf(proposal: dict, line_items: list[dict]) -> bytes:
def _get_styles(): def _get_styles():
styles = getSampleStyleSheet() styles = getSampleStyleSheet()
styles.add(ParagraphStyle( styles.add(
"CompanyName", ParagraphStyle(
parent=styles["Heading1"], "CompanyName",
fontSize=18, parent=styles["Heading1"],
leading=22, fontSize=18,
textColor=colors.HexColor("#1a237e"), leading=22,
spaceAfter=2, textColor=colors.HexColor("#1a237e"),
)) spaceAfter=2,
)
)
styles.add(ParagraphStyle( styles.add(
"CompanyInfo", ParagraphStyle(
parent=styles["Normal"], "CompanyInfo",
fontSize=9, parent=styles["Normal"],
leading=12, fontSize=9,
textColor=colors.HexColor("#555555"), leading=12,
)) textColor=colors.HexColor("#555555"),
)
)
styles.add(ParagraphStyle( styles.add(
"ProposalTitle", ParagraphStyle(
parent=styles["Heading2"], "ProposalTitle",
fontSize=14, parent=styles["Heading2"],
leading=18, fontSize=14,
textColor=colors.HexColor("#1a237e"), leading=18,
spaceBefore=6, textColor=colors.HexColor("#1a237e"),
spaceAfter=12, spaceBefore=6,
)) spaceAfter=12,
)
)
styles.add(ParagraphStyle( styles.add(
"SectionHeader", ParagraphStyle(
parent=styles["Heading3"], "SectionHeader",
fontSize=11, parent=styles["Heading3"],
leading=14, fontSize=11,
textColor=colors.HexColor("#1a237e"), leading=14,
spaceBefore=8, textColor=colors.HexColor("#1a237e"),
spaceAfter=6, spaceBefore=8,
borderWidth=0, spaceAfter=6,
)) borderWidth=0,
)
)
styles.add(ParagraphStyle( styles.add(
"MetaLabel", ParagraphStyle(
parent=styles["Normal"], "MetaLabel",
fontSize=9, parent=styles["Normal"],
leading=12, fontSize=9,
textColor=colors.HexColor("#666666"), leading=12,
)) textColor=colors.HexColor("#666666"),
)
)
styles.add(ParagraphStyle( styles.add(
"MetaValue", ParagraphStyle(
parent=styles["Normal"], "MetaValue",
fontSize=10, parent=styles["Normal"],
leading=13, fontSize=10,
fontName="Helvetica-Bold", leading=13,
)) fontName="Helvetica-Bold",
)
)
styles.add(ParagraphStyle( styles.add(
"ScopeText", ParagraphStyle(
parent=styles["Normal"], "ScopeText",
fontSize=10, parent=styles["Normal"],
leading=14, fontSize=10,
spaceBefore=4, leading=14,
)) spaceBefore=4,
)
)
styles.add(ParagraphStyle( styles.add(
"TermsText", ParagraphStyle(
parent=styles["Normal"], "TermsText",
fontSize=8, parent=styles["Normal"],
leading=11, fontSize=8,
textColor=colors.HexColor("#555555"), leading=11,
)) textColor=colors.HexColor("#555555"),
)
)
styles.add(ParagraphStyle( styles.add(
"TotalLabel", ParagraphStyle(
parent=styles["Normal"], "TotalLabel",
fontSize=11, parent=styles["Normal"],
leading=14, fontSize=11,
fontName="Helvetica-Bold", leading=14,
alignment=TA_RIGHT, fontName="Helvetica-Bold",
)) alignment=TA_RIGHT,
)
)
styles.add(ParagraphStyle( styles.add(
"FooterText", ParagraphStyle(
parent=styles["Normal"], "FooterText",
fontSize=8, parent=styles["Normal"],
leading=10, fontSize=8,
textColor=colors.HexColor("#888888"), leading=10,
alignment=TA_CENTER, textColor=colors.HexColor("#888888"),
)) alignment=TA_CENTER,
)
)
return styles return styles
@ -267,12 +287,16 @@ def _build_header(proposal: dict, styles) -> list:
] ]
header_table = Table(header_data, colWidths=[3.5 * inch, 3.5 * inch]) header_table = Table(header_data, colWidths=[3.5 * inch, 3.5 * inch])
header_table.setStyle(TableStyle([ header_table.setStyle(
("VALIGN", (0, 0), (-1, -1), "TOP"), TableStyle(
("ALIGN", (1, 0), (1, -1), "RIGHT"), [
("LINEBELOW", (0, -1), (-1, -1), 1.5, colors.HexColor("#1a237e")), ("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOTTOMPADDING", (0, -1), (-1, -1), 8), ("ALIGN", (1, 0), (1, -1), "RIGHT"),
])) ("LINEBELOW", (0, -1), (-1, -1), 1.5, colors.HexColor("#1a237e")),
("BOTTOMPADDING", (0, -1), (-1, -1), 8),
]
)
)
return [header_table] return [header_table]
@ -322,11 +346,15 @@ def _build_metadata(proposal: dict, styles) -> list:
] ]
meta_table = Table(meta_data, colWidths=[3.5 * inch, 3.5 * inch]) meta_table = Table(meta_data, colWidths=[3.5 * inch, 3.5 * inch])
meta_table.setStyle(TableStyle([ meta_table.setStyle(
("VALIGN", (0, 0), (-1, -1), "TOP"), TableStyle(
("TOPPADDING", (0, 0), (-1, -1), 2), [
("BOTTOMPADDING", (0, 0), (-1, -1), 2), ("VALIGN", (0, 0), (-1, -1), "TOP"),
])) ("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]
)
)
return [meta_table] return [meta_table]
@ -381,36 +409,49 @@ def _build_line_items_table(line_items: list[dict], styles) -> list:
] ]
table_data.append(row) table_data.append(row)
col_widths = [0.35 * inch, 3.15 * inch, 0.55 * inch, 0.7 * inch, 1.0 * inch, 1.0 * inch] col_widths = [
0.35 * inch,
3.15 * inch,
0.55 * inch,
0.7 * inch,
1.0 * inch,
1.0 * inch,
]
table = Table(table_data, colWidths=col_widths, repeatRows=1) table = Table(table_data, colWidths=col_widths, repeatRows=1)
table.setStyle(TableStyle([ table.setStyle(
# Header row TableStyle(
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1a237e")), [
("TEXTCOLOR", (0, 0), (-1, 0), colors.white), # Header row
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1a237e")),
("FONTSIZE", (0, 0), (-1, 0), 9), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("BOTTOMPADDING", (0, 0), (-1, 0), 6), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("TOPPADDING", (0, 0), (-1, 0), 6), ("FONTSIZE", (0, 0), (-1, 0), 9),
# Data rows ("BOTTOMPADDING", (0, 0), (-1, 0), 6),
("FONTSIZE", (0, 1), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, 0), 6),
("TOPPADDING", (0, 1), (-1, -1), 4), # Data rows
("BOTTOMPADDING", (0, 1), (-1, -1), 4), ("FONTSIZE", (0, 1), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 1), (-1, -1), 4),
# Alignment ("BOTTOMPADDING", (0, 1), (-1, -1), 4),
("ALIGN", (0, 0), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("ALIGN", (2, 0), (2, -1), "CENTER"), # Alignment
("ALIGN", (3, 0), (3, -1), "CENTER"), ("ALIGN", (0, 0), (0, -1), "CENTER"),
("ALIGN", (4, 0), (4, -1), "RIGHT"), ("ALIGN", (2, 0), (2, -1), "CENTER"),
("ALIGN", (5, 0), (5, -1), "RIGHT"), ("ALIGN", (3, 0), (3, -1), "CENTER"),
# Grid ("ALIGN", (4, 0), (4, -1), "RIGHT"),
("LINEBELOW", (0, 0), (-1, 0), 1, colors.HexColor("#1a237e")), ("ALIGN", (5, 0), (5, -1), "RIGHT"),
("LINEBELOW", (0, 1), (-1, -2), 0.5, colors.HexColor("#e0e0e0")), # Grid
("LINEBELOW", (0, -1), (-1, -1), 1, colors.HexColor("#1a237e")), ("LINEBELOW", (0, 0), (-1, 0), 1, colors.HexColor("#1a237e")),
# Alternating row colors ("LINEBELOW", (0, 1), (-1, -2), 0.5, colors.HexColor("#e0e0e0")),
*[("BACKGROUND", (0, i), (-1, i), colors.HexColor("#f5f5f5")) ("LINEBELOW", (0, -1), (-1, -1), 1, colors.HexColor("#1a237e")),
for i in range(2, len(table_data), 2)], # Alternating row colors
])) *[
("BACKGROUND", (0, i), (-1, i), colors.HexColor("#f5f5f5"))
for i in range(2, len(table_data), 2)
],
]
)
)
elements.append(table) elements.append(table)
elements.append(Spacer(1, 0.15 * inch)) elements.append(Spacer(1, 0.15 * inch))
@ -420,14 +461,18 @@ def _build_line_items_table(line_items: list[dict], styles) -> list:
["", "", "", "", "TOTAL:", f"${subtotal:,.2f}"], ["", "", "", "", "TOTAL:", f"${subtotal:,.2f}"],
] ]
total_table = Table(total_data, colWidths=col_widths) total_table = Table(total_data, colWidths=col_widths)
total_table.setStyle(TableStyle([ total_table.setStyle(
("FONTNAME", (0, 0), (-1, -1), "Helvetica-Bold"), TableStyle(
("FONTSIZE", (0, 0), (-1, -1), 11), [
("ALIGN", (4, 0), (4, 0), "RIGHT"), ("FONTNAME", (0, 0), (-1, -1), "Helvetica-Bold"),
("ALIGN", (5, 0), (5, 0), "RIGHT"), ("FONTSIZE", (0, 0), (-1, -1), 11),
("TOPPADDING", (0, 0), (-1, -1), 4), ("ALIGN", (4, 0), (4, 0), "RIGHT"),
("LINEABOVE", (4, 0), (5, 0), 1.5, colors.HexColor("#1a237e")), ("ALIGN", (5, 0), (5, 0), "RIGHT"),
])) ("TOPPADDING", (0, 0), (-1, -1), 4),
("LINEABOVE", (4, 0), (5, 0), 1.5, colors.HexColor("#1a237e")),
]
)
)
elements.append(total_table) elements.append(total_table)
return elements return elements

View file

@ -100,9 +100,11 @@ def retrieve_similar(scope: str, category: str) -> list[dict]:
return [] return []
try: try:
filter_config = { filter_config = (
"equals": {"key": "service_category", "value": category} {"equals": {"key": "service_category", "value": category}}
} if category else None if category
else None
)
params = { params = {
"knowledgeBaseId": KNOWLEDGE_BASE_ID, "knowledgeBaseId": KNOWLEDGE_BASE_ID,
@ -115,7 +117,9 @@ def retrieve_similar(scope: str, category: str) -> list[dict]:
} }
if filter_config: if filter_config:
params["retrievalConfiguration"]["vectorSearchConfiguration"]["filter"] = filter_config params["retrievalConfiguration"]["vectorSearchConfiguration"]["filter"] = (
filter_config
)
response = bedrock_agent.retrieve(**params) response = bedrock_agent.retrieve(**params)
@ -126,12 +130,14 @@ def retrieve_similar(scope: str, category: str) -> list[dict]:
metadata = result.get("metadata", {}) metadata = result.get("metadata", {})
source_uri = result.get("location", {}).get("s3Location", {}).get("uri", "") source_uri = result.get("location", {}).get("s3Location", {}).get("uri", "")
results.append({ results.append(
"content": content, {
"score": score, "content": content,
"metadata": metadata, "score": score,
"sourceUri": source_uri, "metadata": metadata,
}) "sourceUri": source_uri,
}
)
return results return results
@ -150,7 +156,9 @@ def generate_line_items(
if similar_proposals: if similar_proposals:
context_block = "Here are similar historical proposals and their line items for reference:\n\n" context_block = "Here are similar historical proposals and their line items for reference:\n\n"
for i, sp in enumerate(similar_proposals[:5], 1): for i, sp in enumerate(similar_proposals[:5], 1):
context_block += f"--- Similar Proposal {i} (relevance: {sp['score']:.2f}) ---\n" context_block += (
f"--- Similar Proposal {i} (relevance: {sp['score']:.2f}) ---\n"
)
context_block += sp["content"] + "\n\n" context_block += sp["content"] + "\n\n"
prompt = f"""You are a construction/facilities proposal estimator for Sea Haven Industries. prompt = f"""You are a construction/facilities proposal estimator for Sea Haven Industries.
@ -180,12 +188,14 @@ Respond ONLY with the JSON array, no additional text."""
modelId=MODEL_ID, modelId=MODEL_ID,
contentType="application/json", contentType="application/json",
accept="application/json", accept="application/json",
body=json.dumps({ body=json.dumps(
"anthropic_version": "bedrock-2023-05-31", {
"max_tokens": 4096, "anthropic_version": "bedrock-2023-05-31",
"messages": [{"role": "user", "content": prompt}], "max_tokens": 4096,
"temperature": 0.3, "messages": [{"role": "user", "content": prompt}],
}), "temperature": 0.3,
}
),
) )
response_body = json.loads(response["body"].read()) response_body = json.loads(response["body"].read())
@ -213,17 +223,19 @@ def post_line_items(proposal_id: str, items: list[dict], existing_items: list[di
# Preserve non-AI items (Manual, Vendor, Historical) # Preserve non-AI items (Manual, Vendor, Historical)
preserved = [li for li in existing_items if li.get("source") != "AI"] preserved = [li for li in existing_items if li.get("source") != "AI"]
for i, li in enumerate(preserved): for i, li in enumerate(preserved):
line_items_payload.append({ line_items_payload.append(
"id": li.get("id"), {
"description": li["description"], "id": li.get("id"),
"quantity": float(li.get("quantity", 1)), "description": li["description"],
"unit": li.get("unit", "each"), "quantity": float(li.get("quantity", 1)),
"unitPrice": li.get("unitPrice"), "unit": li.get("unit", "each"),
"totalPrice": float(li.get("totalPrice", 0)), "unitPrice": li.get("unitPrice"),
"pricingMode": li.get("pricingMode", "TotalPrice"), "totalPrice": float(li.get("totalPrice", 0)),
"sortOrder": i + 1, "pricingMode": li.get("pricingMode", "TotalPrice"),
"source": li.get("source", "Manual"), "sortOrder": i + 1,
}) "source": li.get("source", "Manual"),
}
)
# Add new AI-generated items after preserved ones # Add new AI-generated items after preserved ones
offset = len(line_items_payload) offset = len(line_items_payload)
@ -232,17 +244,19 @@ def post_line_items(proposal_id: str, items: list[dict], existing_items: list[di
if pricing_mode not in ("UnitPrice", "TotalPrice", "Both"): if pricing_mode not in ("UnitPrice", "TotalPrice", "Both"):
pricing_mode = "UnitPrice" if item.get("unitPrice") else "TotalPrice" pricing_mode = "UnitPrice" if item.get("unitPrice") else "TotalPrice"
line_items_payload.append({ line_items_payload.append(
"id": None, {
"description": item["description"], "id": None,
"quantity": float(item.get("quantity", 1)), "description": item["description"],
"unit": item.get("unit", "each"), "quantity": float(item.get("quantity", 1)),
"unitPrice": item.get("unitPrice"), "unit": item.get("unit", "each"),
"totalPrice": float(item.get("totalPrice", 0)), "unitPrice": item.get("unitPrice"),
"pricingMode": pricing_mode, "totalPrice": float(item.get("totalPrice", 0)),
"sortOrder": offset + i + 1, "pricingMode": pricing_mode,
"source": "AI", "sortOrder": offset + i + 1,
}) "source": "AI",
}
)
try: try:
resp = httpx.put( resp = httpx.put(