MVP / doc_parser.py
cigawa's picture
Upload 2 files
9f4cbec verified
Raw
History Blame Contribute Delete
5.75 kB
"""
Document / file ingestion layer.
Active path (MVP): Claude-native parsing. Uploaded PDFs and images are sent
directly to Claude (which reads PDFs and images natively) with an extraction
prompt that pulls vibration-relevant parameters into structured fields.
Scaffolded paths (for later, per the architecture plan):
- LlamaParse: AI-vision parsing of complex technical PDFs -> Markdown for RAG
- Unstructured.io: PDF/DOCX/PPT/image -> structured JSON elements
Both are stubbed with clear TODOs and a consistent return contract so they can
be dropped in without touching the rest of the app.
"""
import base64
import os
from typing import Optional
# --------------------------------------------------------------------------- #
# Active: Claude-native extraction
# --------------------------------------------------------------------------- #
EXTRACTION_SYSTEM = """\
You are extracting vibration-test-relevant parameters from an uploaded document \
(a mission spec, launch vehicle user's guide excerpt, ICD, or test article \
datasheet). Return ONLY a compact JSON object with any of these keys you can \
find (omit keys you cannot determine; never invent values):
{
"launch_vehicle": str,
"orbit": str,
"test_article_name": str,
"mass_kg": number,
"envelope_mm": str,
"random_vibration_psd": str, // any stated PSD breakpoints / Grms
"sine_environment": str,
"quasi_static_g": str,
"shock_srs": str,
"min_structural_freq_hz": number,
"mounting_interface": str, // bolt pattern, hole count/size if stated
"operating_modes": str,
"notes": str
}
Output the JSON and nothing else."""
def _encode_file(path: str) -> tuple[str, str]:
"""Return (base64_data, media_type) for a PDF or image."""
ext = os.path.splitext(path)[1].lower()
media = {
".pdf": "application/pdf",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
}.get(ext)
if media is None:
raise ValueError(f"Unsupported file type for extraction: {ext}")
with open(path, "rb") as f:
data = base64.standard_b64encode(f.read()).decode("utf-8")
return data, media
def extract_with_claude(path: str, api_key: Optional[str] = None) -> dict:
"""
Send the file to Claude and parse the returned JSON.
Returns {"ok": bool, "data": dict|None, "raw": str, "error": str|None}.
"""
api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
return {"ok": False, "data": None, "raw": "",
"error": "No ANTHROPIC_API_KEY set; cannot parse documents."}
try:
import json
import anthropic
data, media = _encode_file(path)
if media == "application/pdf":
source_block = {"type": "document",
"source": {"type": "base64",
"media_type": media, "data": data}}
else:
source_block = {"type": "image",
"source": {"type": "base64",
"media_type": media, "data": data}}
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1500,
system=EXTRACTION_SYSTEM,
messages=[{"role": "user", "content": [
source_block,
{"type": "text",
"text": "Extract the vibration-relevant parameters as JSON."},
]}],
)
raw = "".join(b.text for b in msg.content if b.type == "text").strip()
# strip code fences if present
cleaned = raw.replace("```json", "").replace("```", "").strip()
parsed = json.loads(cleaned)
return {"ok": True, "data": parsed, "raw": raw, "error": None}
except Exception as e: # noqa: BLE001
return {"ok": False, "data": None, "raw": "", "error": str(e)}
# --------------------------------------------------------------------------- #
# Scaffolded: LlamaParse (TODO - requires LLAMA_CLOUD_API_KEY)
# --------------------------------------------------------------------------- #
def extract_with_llamaparse(path: str) -> dict:
"""
TODO (post-MVP): use LlamaParse for AI-vision parsing of complex technical
PDFs into Markdown, then index with LlamaIndex for RAG.
from llama_parse import LlamaParse
parser = LlamaParse(api_key=os.environ["LLAMA_CLOUD_API_KEY"],
result_type="markdown",
parsing_instruction="Extract test requirements "
"and acceptance criteria.")
docs = parser.load_data(path)
return {"ok": True, "markdown": docs[0].text, ...}
"""
return {"ok": False, "data": None, "raw": "",
"error": "LlamaParse not configured (scaffold). "
"Set LLAMA_CLOUD_API_KEY and implement."}
# --------------------------------------------------------------------------- #
# Scaffolded: Unstructured.io (TODO)
# --------------------------------------------------------------------------- #
def extract_with_unstructured(path: str) -> dict:
"""
TODO (post-MVP): use Unstructured.io to break PDF/DOCX/PPT/image into
structured elements (paragraphs, tables, lists, images) -> JSON for
LangChain ingestion.
from unstructured.partition.auto import partition
elements = partition(filename=path)
return {"ok": True, "elements": [el.to_dict() for el in elements], ...}
"""
return {"ok": False, "data": None, "raw": "",
"error": "Unstructured.io not configured (scaffold)."}