document-engine / services.py
basudevm23's picture
Upload 5 files
a9b921f verified
Raw
History Blame Contribute Delete
6.48 kB
# Full LLM translation and processing combined with BM25 which helps idenitfy key words.
import pytesseract
from pdf2image import convert_from_path
import requests
import json
import time
import asyncio
import pypdf
import re
import hashlib
import base64
from io import BytesIO
ollama_semaphore = asyncio.Semaphore(1)
cloud_lock = asyncio.Lock()
LAST_CLOUD_CALL_TIME = 0.0
CLOUD_DELAY_SECONDS = 13.0
def get_file_hash(filepath):
h = hashlib.sha256()
with open(filepath, 'rb') as file:
while chunk := file.read(8192):
h.update(chunk)
return h.hexdigest()
def extract_text_from_pdf(doc_path):
extracted_chunks = []
try:
reader = pypdf.PdfReader(doc_path)
for page_idx, page in enumerate(reader.pages):
text = page.extract_text()
if text and len(text.strip()) > 50:
_chunk_text_layout_aware(text, page_idx, extracted_chunks)
else:
images = convert_from_path(doc_path, first_page=page_idx+1, last_page=page_idx+1)
if images:
ocr_text = pytesseract.image_to_string(images[0], lang="eng+spa+ara+chi_sim+ita+msa")
_chunk_text_layout_aware(ocr_text, page_idx, extracted_chunks)
except Exception as e:
print(f"Extraction error: {e}")
return extracted_chunks
def _chunk_text_layout_aware(text, page_idx, extracted_chunks):
text = re.sub(r'\n{3,}', '\n\n', text)
blocks = text.split('\n\n')
current_chunk = ""
for block in blocks:
if len(current_chunk) + len(block) < 800:
current_chunk += block + "\n\n"
else:
if current_chunk.strip():
extracted_chunks.append({"page": page_idx + 1, "text": current_chunk.strip()})
current_chunk = current_chunk[-150:] + block + "\n\n"
if current_chunk.strip():
extracted_chunks.append({"page": page_idx + 1, "text": current_chunk.strip()})
def get_page_image_b64(doc_path, page_num):
images = convert_from_path(doc_path, first_page=page_num, last_page=page_num)
if not images: return None
buffered = BytesIO()
images[0].save(buffered, format="JPEG", quality=85)
return base64.b64encode(buffered.getvalue()).decode("utf-8")
async def expand_query_async(query, document_anchor, api_key=""):
"""Dynamically detects document language via Anchor and expands the query."""
sys_prompt = (
"You are an expert multilingual legal translator. "
"1. Read the document excerpt to detect its language and jurisdiction.\n"
"2. Translate the 'Target Field' into 5 exact synonyms used in that specific language context.\n"
"3. Output ONLY the synonyms separated by spaces. No other text."
)
user_prompt = f"Document Excerpt: {document_anchor[:1000]}\n\nTarget Field: {query}"
try:
if api_key:
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={api_key}"
payload = {"contents": [{"parts": [{"text": f"{sys_prompt}\n\n{user_prompt}"}]}]}
res = await asyncio.to_thread(requests.post, url, headers={"Content-Type": "application/json"}, json=payload, timeout=10)
return res.json()['candidates'][0]['content']['parts'][0]['text'].strip()
return query
except:
return query
async def query_llm_async(model_name, system_prompt, user_prompt, response_type="extraction", api_key="", doc_path=None, page_nums=None):
"""CLOUD-ONLY ROUTER"""
if not api_key:
return "Error: Gemini API Key Required", 0.0
# Set schemas
if response_type == "extraction":
schema = {
"type": "OBJECT",
"properties": {
"step_1_evidence": {"type": "STRING"},
"step_2_math_and_logic": {"type": "STRING"},
"extracted_value": {"type": "STRING"}
},
"required": ["step_1_evidence", "step_2_math_and_logic", "extracted_value"]
}
target_key = "extracted_value"
else:
schema = {
"type": "OBJECT",
"properties": {
"internal_calculations_do_not_show_user": {"type": "STRING"},
"final_response": {"type": "STRING"}
},
"required": ["internal_calculations_do_not_show_user", "final_response"]
}
target_key = "final_response"
# Rate limiting lock
global LAST_CLOUD_CALL_TIME
async with cloud_lock:
current_time = time.time()
if current_time - LAST_CLOUD_CALL_TIME < CLOUD_DELAY_SECONDS:
await asyncio.sleep(CLOUD_DELAY_SECONDS - (current_time - LAST_CLOUD_CALL_TIME))
LAST_CLOUD_CALL_TIME = time.time()
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}"
parts = [{"text": f"SYSTEM INSTRUCTIONS:\n{system_prompt}\n\nUSER PROMPT:\n{user_prompt}"}]
if doc_path and page_nums:
parts[0]["text"] += "\n\nLook at the provided document images to find the exact value."
for p_num in page_nums[:2]:
b64_img = await asyncio.to_thread(get_page_image_b64, doc_path, p_num)
if b64_img: parts.append({"inlineData": {"mimeType": "image/jpeg", "data": b64_img}})
payload = {
"contents": [{"parts": parts}],
"generationConfig": {
"temperature": 0.0,
"responseMimeType": "application/json",
"responseSchema": schema
}
}
start_time = time.time()
try:
response = await asyncio.to_thread(requests.post, url, headers={"Content-Type": "application/json"}, json=payload, timeout=90)
if response.status_code == 429:
return "RATE_LIMIT_EXCEEDED", round(time.time() - start_time, 2)
response.raise_for_status()
raw_content = response.json()['candidates'][0]['content']['parts'][0]['text']
latency = round(time.time() - start_time, 2)
try:
return str(json.loads(raw_content).get(target_key, "Not Found")).strip(), latency
except:
return "Parse Error", latency
except Exception as e:
return f"API Error", round(time.time() - start_time, 2)