pageparse.ai / src /pageparse /extract.py
Varun2007's picture
initial clean deployment commit with compilers
8c3e275
Raw
History Blame Contribute Delete
5.21 kB
from __future__ import annotations
from pageparse.config import settings
from pageparse.schema import SourceParseResult, Record
SCHEMA_DETECTION_KEYWORDS = {
"todo": ["todo", "to-do", "task", "deadline", "due", "priority", "action item"],
"meeting": ["meeting", "agenda", "speaker", "minutes", "attendee", "discussion"],
"recipe": ["recipe", "ingredient", "tbsp", "tsp", "cup", "cook", "bake", "preheat"],
"survey": ["survey", "questionnaire", "question", "response", "feedback"],
"notes": ["note", "lecture", "chapter", "section", "topic"],
}
DEFAULT_SCHEMA_PROMPTS = {
"meeting": (
"Extract records of type 'action_item', 'note', or 'transcript_segment' from the following text. "
"Include speaker, timestamp, priority, and due_date where present. "
"Do NOT change any words. Preserve original text exactly."
),
"recipe": (
"Extract ingredients as type 'key_value' (category 'Ingredient') and "
"steps as type 'note' (category 'Instruction'). "
"Do NOT change any words. Preserve original text exactly."
),
"survey": (
"Extract questions as type 'note' (category 'Question') and "
"responses as type 'key_value' (category 'Response'). "
"Do NOT change any words. Preserve original text exactly."
),
"notes": (
"Extract key topics as type 'note' with category from the following notes. "
"Include any action items with priority and due_date where present. "
"Do NOT change any words. Preserve original text exactly."
),
}
class Extractor:
def __init__(self) -> None:
self._grammar: str | None = None
grammar_path = settings.grammar_path()
if grammar_path.exists():
self._grammar = grammar_path.read_text(encoding="utf-8")
def detect_schema_type(self, raw_text: str) -> str:
if not settings.auto_schema:
return "todo"
text_lower = raw_text.lower()
scores: dict[str, int] = {}
for schema_type, keywords in SCHEMA_DETECTION_KEYWORDS.items():
score = sum(text_lower.count(kw) for kw in keywords)
if score > 0:
scores[schema_type] = score
if not scores:
return "todo"
return max(scores, key=scores.get)
def extract(
self, raw_text: str, source_file: str, schema_type: str | None = None
) -> SourceParseResult:
if schema_type is None or schema_type == "auto":
schema_type = self.detect_schema_type(raw_text)
result = SourceParseResult(
source_file=source_file,
source_type="image",
records=[],
)
if not raw_text.strip() or raw_text in ("[UNCLEAR]", "[inaudible]"):
return result
result.records = self._line_based_fallback(raw_text)
if self._grammar and raw_text.strip():
try:
structured = self._ollama_structured_extract(raw_text, source_file, schema_type)
if structured and structured.records:
result = structured
result.source_file = source_file
except Exception:
pass
return result
def _ollama_structured_extract(
self, raw_text: str, source_file: str, schema_type: str
) -> SourceParseResult | None:
import json
import urllib.request
user_msg = DEFAULT_SCHEMA_PROMPTS.get(
schema_type,
"Extract tasks with due_date, priority, status, and category from the following text. "
"Do NOT change any words. Preserve original text exactly."
)
system_msg = "You are a structured data extractor. Output only valid JSON matching the schema."
prompt = f"{system_msg}\n\n{user_msg}\n\nExtracted Text:\n{raw_text}\n\nSource File: {source_file}"
payload = {
"model": settings.slm_model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.1},
}
url = f"{settings.ollama_url}/api/generate"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as response:
body = response.read().decode("utf-8")
res = json.loads(body)
raw = res.get("response", "").strip()
if raw:
try:
return SourceParseResult.model_validate_json(raw)
except Exception:
pass
return None
def _line_based_fallback(self, raw_text: str) -> list[Record]:
records: list[Record] = []
lines = raw_text.strip().split("\n")
for line in lines:
line = line.strip()
if not line:
continue
records.append(
Record(
type="note",
content=line,
confidence=0.95,
)
)
if not records:
records.append(Record(type="note", content=raw_text.strip()))
return records