What We're Building
A Python service that accepts documents and runs three operations:
- Summarization — condense long documents to key points
- Extraction — pull structured data from unstructured text
- Classification — categorize documents by type or topic
Requirements
- VPS with 8 GB RAM
- Ollama with Llama 3.3 8B
- Python 3.10+
Setup
pip3 install ollama pdfplumber fastapi uvicorn --break-system-packages
ollama pull llama3.1:8bThe Processing Service
# processor.py
import ollama
import pdfplumber
import json
from pathlib import Path
MODEL = "llama3.1:8b"
def extract_text(file_path: str) -> str:
path = Path(file_path)
if path.suffix.lower() == '.pdf':
with pdfplumber.open(file_path) as pdf:
return "\n".join(page.extract_text() or "" for page in pdf.pages)
else:
return path.read_text()
def summarize(text: str, max_words: int = 150) -> str:
response = ollama.generate(
model=MODEL,
prompt=f"""Summarize the following document in {max_words} words or less.
Focus on the key points and main conclusions.
Document:
{text[:4000]}
Summary:"""
)
return response['response'].strip()
def extract_structured(text: str, fields: list) -> dict:
fields_str = "\n".join(f"- {f}" for f in fields)
response = ollama.generate(
model=MODEL,
prompt=f"""Extract the following fields from the document.
Return ONLY valid JSON, no other text.
Fields to extract:
{fields_str}
Document:
{text[:4000]}
JSON:"""
)
try:
raw = response['response'].strip()
# Find JSON in response
start = raw.find('{')
end = raw.rfind('}') + 1
return json.loads(raw[start:end])
except:
return {}
def classify(text: str, categories: list) -> str:
cats = ", ".join(categories)
response = ollama.generate(
model=MODEL,
prompt=f"""Classify the following document into exactly one of these categories: {cats}
Return ONLY the category name, nothing else.
Document:
{text[:2000]}
Category:"""
)
return response['response'].strip()
# Example usage
if __name__ == "__main__":
text = extract_text("contract.pdf")
print("Summary:")
print(summarize(text))
print("\nExtracted data:")
data = extract_structured(text, [
"party names",
"contract date",
"total value",
"expiry date"
])
print(json.dumps(data, indent=2))
print("\nClassification:")
category = classify(text, [
"employment contract",
"service agreement",
"NDA",
"purchase order",
"other"
])
print(category)Building an API Around It
# api.py
from fastapi import FastAPI, UploadFile
from processor import extract_text, summarize, extract_structured, classify
import tempfile, os
app = FastAPI()
@app.post("/process")
async def process_document(
file: UploadFile,
operation: str = "summarize"
):
with tempfile.NamedTemporaryFile(
suffix=os.path.splitext(file.filename)[1],
delete=False
) as tmp:
tmp.write(await file.read())
tmp_path = tmp.name
try:
text = extract_text(tmp_path)
if operation == "summarize":
result = summarize(text)
elif operation == "extract":
result = extract_structured(text, [
"date", "author", "subject", "key points"
])
elif operation == "classify":
result = classify(text, [
"invoice", "contract", "report", "email", "other"
])
else:
result = "Unknown operation"
return {"result": result, "operation": operation}
finally:
os.unlink(tmp_path)Run it:
uvicorn api:app --host 0.0.0.0 --port 8000What it costs
The pipeline's footprint is the model plus a small FastAPI service, so the Standard plan (2 vCPU, 4 GB) handles a 7B model and overnight batches well; step up to the Pro plan (4 vCPU, 8 GB) for larger models or faster throughput. NoctHost bills hourly from a prepaid, crypto-funded balance, no card and no KYC, which suits a service that mostly runs in bursts.