Building a Long Document Analyzer with mimo-v2: A Step-by-Step Guide
Introduction
This guide walks through building a complete document analysis tool using Xiaomi mimo-v2’s 1M token context window. We’ll build a tool that can:
- Process PDF documents up to 500+ pages
- Extract structured information
- Answer questions about document content
- Generate summaries at multiple granularity levels
By the end, you’ll have a working tool you can adapt for your own use cases.
Prerequisites
- Basic Python knowledge
- An OpenRouter API key (free at openrouter.ai)
- Python 3.8+
Architecture Overview
Our tool has three main components:
- Document Loader — Converts PDFs to text
- Analysis Engine — Uses mimo-v2 to process and analyze
- Query Interface — Allows asking questions about the document
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ PDF Doc │ → │ Document │ → │ Query │
│ │ │ Analyzer │ │ Results │
└─────────────┘ └──────────────┘ └─────────────┘
Step 1: Setting Up the Environment
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install openai pypdf2 rich
Create a .env file with your API key:
OPENROUTER_API_KEY=your_api_key_here
Step 2: Document Loading
We’ll use PyPDF2 to extract text from PDFs:
from PyPDF2 import PdfReader
from pathlib import Path
def load_pdf(pdf_path: str) -> str:
"""Extract text from a PDF file."""
reader = PdfReader(pdf_path)
text = ""
for page in reader.pages:
text += page.extract_text()
return text
# Usage
document_text = load_pdf("technical-manual.pdf")
print(f"Loaded {len(document_text)} characters")
Step 3: Building the Analysis Engine
Now we’ll create the core analysis class:
from openai import OpenAI
import os
from dotenv import load_dotenv
load_dotenv()
class DocumentAnalyzer:
def __init__(self, document_text: str):
self.document = document_text
self.client = OpenAI(
api_key=os.getenv("OPENROUTER_API_KEY"),
base_url="https://openrouter.ai/api/v1"
)
self.model = "xiaomi/mimo-v2"
def analyze(self, prompt: str) -> str:
"""Send a prompt about the document to mimo-v2."""
full_prompt = f"""Here is a document:
{self.document}
---
{prompt}"""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a document analysis assistant. Answer questions based only on the provided document."},
{"role": "user", "content": full_prompt}
],
max_tokens=4096
)
return response.choices[0].message.content
Step 4: Adding Analysis Methods
Let’s add specific analysis capabilities:
class DocumentAnalyzer:
# ... (previous code)
def get_summary(self, length: str = "medium") -> str:
"""Get a summary of the document."""
length_instructions = {
"short": "Summarize in 2-3 sentences.",
"medium": "Summarize in one paragraph with 5-7 bullet points.",
"long": "Provide a detailed summary with executive overview, key findings, and conclusions."
}
return self.analyze(f"{length_instructions[length]}")
def extract_entities(self, entity_type: str = "all") -> str:
"""Extract specific entities from the document."""
return self.analyze(f"Extract all {entity_type} mentioned in this document. List them in a structured format.")
def find_section(self, topic: str) -> str:
"""Find and extract content related to a specific topic."""
return self.analyze(f"Find all sections discussing '{topic}'. Quote the relevant passages.")
def compare_sections(self, section1: str, section2: str) -> str:
"""Compare two topics within the document."""
return self.analyze(f"Compare how the document discusses '{section1}' vs '{section2}'. What are the similarities and differences?")
def fact_check(self, claim: str) -> str:
"""Verify if a claim is supported by the document."""
return self.analyze(f"Is the following claim supported by this document? '{claim}' Provide evidence from the text.")
Step 5: Building the Query Interface
Create a simple command-line interface:
from rich.console import Console
from rich.markdown import Markdown
def interactive_mode(analyzer: DocumentAnalyzer):
"""Run an interactive query session."""
console = Console()
console.print("[bold green]Document Analyzer Ready![/bold green]")
console.print("Type 'quit' to exit, 'summary' for summary, 'help' for commands.")
while True:
try:
query = console.input("\n[bold blue]You:[/bold blue] ")
if query.lower() == 'quit':
break
elif query.lower() == 'summary':
result = analyzer.get_summary()
elif query.lower() == 'help':
console.print("""
[bold]Available commands:[/bold]
summary [short|medium|long] - Get document summary
entities [type] - Extract entities
find <topic> - Find sections about a topic
compare <topic1> <topic2> - Compare two topics
fact <claim> - Fact check a claim
""")
continue
else:
result = analyzer.analyze(query)
console.print("\n[bold green]Assistant:[/bold green]")
console.print(Markdown(result))
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
# Main execution
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python analyzer.py <pdf_file>")
sys.exit(1)
pdf_path = sys.argv[1]
document_text = load_pdf(pdf_path)
analyzer = DocumentAnalyzer(document_text)
interactive_mode(analyzer)
Step 6: Handling Ultra-Long Documents
For documents approaching the 1M token limit:
def chunk_document(text: str, chunk_size: int = 400000, overlap: int = 10000) -> list:
"""Split a document into overlapping chunks."""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
# Try to break at paragraph boundary
if end < len(text):
break_point = text.rfind('\n\n', start, end)
if break_point > start:
end = break_point
chunks.append(text[start:end])
start = end - overlap
return chunks
class ChunkedAnalyzer:
def __init__(self, document_text: str):
self.chunks = chunk_document(document_text)
self.client = OpenAI(
api_key=os.getenv("OPENROUTER_API_KEY"),
base_url="https://openrouter.ai/api/v1"
)
def analyze_all(self, prompt: str) -> list:
"""Analyze all chunks and return individual results."""
results = []
for i, chunk in enumerate(self.chunks):
response = self.client.chat.completions.create(
model="xiaomi/mimo-v2",
messages=[
{"role": "system", "content": "Answer based only on the provided text."},
{"role": "user", "content": f"Document chunk {i+1}:\n\n{chunk}\n\n---\n\n{prompt}"}
]
)
results.append({
"chunk": i + 1,
"content": response.choices[0].message.content
})
return results
def synthesize(self, results: list) -> str:
"""Combine results from all chunks into a final answer."""
combined = "\n\n".join([r["content"] for r in results])
response = self.client.chat.completions.create(
model="xiaomi/mimo-v2",
messages=[
{"role": "system", "content": "Synthesize the following analysis results into a coherent answer."},
{"role": "user", "content": combined}
]
)
return response.choices[0].message.content
Step 7: Production Considerations
Error Handling
import time
from openai import RateLimitError
def analyze_with_retry(self, prompt: str, max_retries: int = 3):
"""Analyze with exponential backoff retry."""
for attempt in range(max_retries):
try:
return self.analyze(prompt)
except RateLimitError:
if attempt == max_retries - 1:
raise
wait_time = 2 ** attempt
time.sleep(wait_time)
Caching Results
import hashlib
import json
from pathlib import Path
class CachedAnalyzer(DocumentAnalyzer):
def __init__(self, document_text: str, cache_dir: str = ".cache"):
super().__init__(document_text)
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.doc_hash = hashlib.md5(document_text.encode()).hexdigest()
def analyze(self, prompt: str) -> str:
prompt_hash = hashlib.md5(prompt.encode()).hexdigest()
cache_file = self.cache_dir / f"{self.doc_hash}_{prompt_hash}.json"
if cache_file.exists():
with open(cache_file) as f:
return json.load(f)["response"]
response = super().analyze(prompt)
with open(cache_file, "w") as f:
json.dump({"prompt": prompt, "response": response}, f)
return response
Complete Working Example
Here’s a complete script ready to run:
#!/usr/bin/env python3
"""
Document Analyzer using Xiaomi mimo-v2
Usage: python analyzer.py <pdf_file>
"""
from PyPDF2 import PdfReader
from openai import OpenAI
from rich.console import Console
from rich.markdown import Markdown
import os
from dotenv import load_dotenv
load_dotenv()
def load_pdf(pdf_path):
reader = PdfReader(pdf_path)
return "".join(page.extract_text() for page in reader.pages)
def analyze_document(document_text, query):
client = OpenAI(
api_key=os.getenv("OPENROUTER_API_KEY"),
base_url="https://openrouter.ai/api/v1"
)
response = client.chat.completions.create(
model="xiaomi/mimo-v2",
messages=[
{"role": "system", "content": "You are a helpful document assistant."},
{"role": "user", "content": f"Document:\n{document_text}\n\nQuestion: {query}"}
],
max_tokens=4096
)
return response.choices[0].message.content
if __name__ == "__main__":
import sys
console = Console()
if len(sys.argv) < 2:
console.print("[red]Usage: python analyzer.py <pdf_file>[/red]")
sys.exit(1)
console.print("[bold]Loading document...[/bold]")
doc_text = load_pdf(sys.argv[1])
console.print(f"[green]Loaded {len(doc_text)} characters[/green]")
while True:
query = console.input("\n[bold]Query:[/bold] ")
if query == 'quit':
break
console.print("[bold]Analyzing...[/bold]")
result = analyze_document(doc_text, query)
console.print(Markdown(result))
Best Practices
Do’s
- Be specific in prompts — “Extract all API endpoints” vs “What’s in this doc?”
- Use system messages to set context for long sessions
- Verify critical information — LLMs can occasionally misquote
- Cache expensive results to save API calls
Don’ts
- Don’t expect perfect recall beyond 500K tokens
- Don’t skip error handling for production use
- Don’t use for time-sensitive tasks — expect 20-60s responses
Conclusion
You now have a working document analyzer that can process hundreds of pages in a single query. The code is modular — swap in different PDF loaders, add output formats, or integrate into existing workflows.
The key advantage of mimo-v2 is handling documents that would overflow other models’ context windows. For legal docs, technical manuals, or research papers, this tool can save hours of manual work.
Further Reading
Have you built something cool with mimo-v2? Its current page, with pricing, is Xiaomi MiMo-V2.5.