Spaces:
Sleeping
Sleeping
| # COST: Gemini gemini-2.5-flash called ONCE per document on first summarization. | |
| # Result is written to a JSON sidecar file (<filename>.summary.json) beside | |
| # the source document. Every subsequent call reads the sidecar and returns | |
| # immediately β zero additional tokens spent. | |
| # Sidecar is invalidated only by manual deletion. | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from ingestion import parse_document | |
| SUMMARY_SUFFIX = ".summary.json" | |
| MAX_SUMMARY_CHARS = 12_000 # truncation guard before sending to LLM | |
| def _cache_path(file_path: Path) -> Path: | |
| return file_path.with_name(file_path.name + SUMMARY_SUFFIX) | |
| def summarize_document(file_path: str) -> str: | |
| """Return a concise summary of a document, using a local cache when available. | |
| On the first call the function extracts the full text from the document, | |
| sends it to the OpenAI Chat Completions API (gpt-4o-mini), and writes the | |
| resulting summary to a JSON sidecar file next to the original document | |
| (e.g. 'report.pdf.summary.json'). Every subsequent call for the same file | |
| reads the sidecar and returns immediately without making another LLM call, | |
| keeping costs low and responses fast. | |
| The cache is invalidated only by deleting the sidecar file manually. | |
| Args: | |
| file_path: Absolute or relative path to the .pdf or .docx document. | |
| Returns: | |
| A plain-text summary paragraph. If the file cannot be found or parsed, | |
| an error message is returned instead of raising so the agent can handle | |
| it gracefully. | |
| """ | |
| path = Path(file_path) | |
| if not path.exists(): | |
| return f"Error: file not found at '{file_path}'." | |
| cache = _cache_path(path) | |
| if cache.exists(): | |
| try: | |
| data = json.loads(cache.read_text(encoding="utf-8")) | |
| return data["summary"] | |
| except (json.JSONDecodeError, KeyError): | |
| cache.unlink(missing_ok=True) # corrupt cache β regenerate | |
| try: | |
| pages = parse_document(path) | |
| except ValueError as exc: | |
| return f"Error: {exc}" | |
| full_text = "\n\n".join(p["text"] for p in pages) | |
| truncated = full_text[:MAX_SUMMARY_CHARS] | |
| if len(full_text) > MAX_SUMMARY_CHARS: | |
| truncated += "\n\n[...document truncated for summarisation...]" | |
| from openai import OpenAI | |
| client = OpenAI( | |
| api_key=os.environ["GEMINI_API_KEY"], | |
| base_url="https://generativelanguage.googleapis.com/v1beta/openai/", | |
| ) | |
| response = client.chat.completions.create( | |
| model="gemini-2.5-flash", | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You are a document summarisation assistant. " | |
| "Write a clear, concise summary (3-5 paragraphs) of the document " | |
| "the user provides. Focus on key topics, findings, and conclusions." | |
| ), | |
| }, | |
| {"role": "user", "content": f"Summarise this document:\n\n{truncated}"}, | |
| ], | |
| ) | |
| summary = response.choices[0].message.content or "" | |
| cache.write_text( | |
| json.dumps({"source_file": path.name, "summary": summary}, ensure_ascii=False, indent=2), | |
| encoding="utf-8", | |
| ) | |
| return summary | |