
Document Chat Interface
- 133 installs
- 38 repo stars
- Updated January 5, 2026
- qodex-ai/ai-agent-skills
Build a document-grounded chat UI where users upload PDFs or docs and converse with an agent that cites sources, handles streaming replies, and manages session context.
About
Guides implementation of a production document chat interface that connects file ingestion, retrieval-augmented answers, and conversational UI. Covers message threads, source citations, loading states, and patterns agents use to scaffold full-stack doc Q&A experiences.
- RAG-ready chat UX patterns
- Document upload and citation display
- Streaming message and error states
- Session and context management hooks
- Agent-friendly component boundaries
Document Chat Interface by the numbers
- 133 all-time installs (skills.sh)
- Ranked #3,627 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/qodex-ai/ai-agent-skills --skill document-chat-interfaceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 133 |
|---|---|
| repo stars | ★ 38 |
| Last updated | January 5, 2026 |
| Repository | qodex-ai/ai-agent-skills ↗ |
What it does
Build a document-grounded chat UI where users upload PDFs or docs and converse with an agent that cites sources, handles streaming replies, and manages session context.
Files
Document Chat Interface
Build intelligent chat interfaces that allow users to query and interact with documents using natural language, transforming static documents into interactive knowledge sources.
Overview
A document chat interface combines three capabilities: 1. Document Processing - Extract and prepare documents 2. Semantic Understanding - Understand questions and find relevant content 3. Conversational Interface - Maintain context and provide natural responses
Common Applications
- PDF Q&A: Answer questions about research papers, reports, books
- Email Search: Find information in email archives conversationally
- GitHub Explorer: Ask questions about code repositories
- Knowledge Base: Interactive access to company documentation
- Contract Review: Query legal documents with natural language
- Research Assistant: Explore academic papers interactively
Architecture
Document Source
↓
Document Processor
├→ Extract text
├→ Process content
└→ Generate embeddings
↓
Vector Database
↓
Chat Interface ← User Question
├→ Retrieve relevant content
├→ Maintain conversation history
└→ Generate responseCore Components
1. Document Sources
See examples/document_processors.py for implementations:
PDF Documents
- Extract text from PDF pages
- Preserve document structure and metadata
- Handle scanned PDFs with OCR (pytesseract)
- Extract tables (pdfplumber)
GitHub Repositories
- Extract code files from repositories
- Parse repository structure
- Process multiple file types
Email Archives
- Extract email metadata (from, to, subject, date)
- Parse email body content
- Handle multiple mailbox formats
Web Pages
- Extract page text and structure
- Preserve heading hierarchy
- Extract links and navigation
YouTube/Audio
- Get transcripts from YouTube videos
- Transcribe audio files
- Handle multiple formats
2. Document Processing
See examples/text_processor.py for implementations:
Text Extraction & Cleaning
- Remove extra whitespace and special characters
- Smart text chunking with overlap
- Intelligent sentence boundary detection
Metadata Extraction
- Extract title, author, date, language
- Calculate word count and document statistics
- Track document source and format
Structure Preservation
- Keep heading hierarchy in chunks
- Preserve section context
- Enable hierarchical retrieval
3. Chat Interface Design
See examples/conversation_manager.py for implementations:
Conversation Management
- Maintain conversation history with size limits
- Track message metadata (timestamps, roles)
- Provide context for LLM integration
- Clear history as needed
Question Refinement
- Expand implicit references in questions
- Handle pronouns and context references
- Improve question clarity with previous context
Response Generation
- Use document context for answering
- Maintain conversation history in prompts
- Provide source citations
- Handle out-of-scope questions
4. User Experience Features
Citation & Sources
def format_response_with_citations(response: str, sources: List[Dict]) -> str:
"""Add source citations to response"""
formatted = response + "\n\n**Sources:**\n"
for i, source in enumerate(sources, 1):
formatted += f"[{i}] Page {source['page']} of {source['source']}\n"
if 'excerpt' in source:
formatted += f" \"{source['excerpt'][:100]}...\"\n"
return formattedClarifying Questions
def generate_follow_up_questions(context: str, response: str) -> List[str]:
"""Suggest follow-up questions to user"""
prompt = f"""
Based on this Q&A, generate 3 relevant follow-up questions:
Context: {context[:500]}
Response: {response[:500]}
"""
follow_ups = llm.generate(prompt)
return follow_upsError Handling
def handle_query_failure(question: str, error: Exception) -> str:
"""Handle when no relevant documents found"""
if isinstance(error, NoRelevantDocuments):
return (
"I couldn't find information about that in the documents. "
"Try asking about different topics like: "
+ ", ".join(get_main_topics())
)
elif isinstance(error, ContextTooLarge):
return (
"The answer requires too much context. "
"Can you be more specific about what you'd like to know?"
)
else:
return f"I encountered an issue: {str(error)[:100]}"Implementation Frameworks
Using LangChain
from langchain.document_loaders import PDFLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
# Load document
loader = PDFLoader("document.pdf")
documents = loader.load()
# Split into chunks
splitter = CharacterTextSplitter(chunk_size=1000)
chunks = splitter.split_documents(documents)
# Create embeddings
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings)
# Create chat chain
llm = ChatOpenAI(model="gpt-4")
qa = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=vectorstore.as_retriever(),
return_source_documents=True
)
# Chat interface
chat_history = []
while True:
question = input("You: ")
result = qa({"question": question, "chat_history": chat_history})
print(f"Assistant: {result['answer']}")
chat_history.append((question, result['answer']))Using LlamaIndex
from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader, ChatMemoryBuffer
from llama_index.llms import ChatMessage, MessageRole
# Load documents
documents = SimpleDirectoryReader("./docs").load_data()
# Create index
index = GPTVectorStoreIndex.from_documents(documents)
# Create chat engine with memory
chat_engine = index.as_chat_engine(
memory=ChatMemoryBuffer.from_defaults(token_limit=3900),
llm="gpt-4"
)
# Chat loop
while True:
question = input("You: ")
response = chat_engine.chat(question)
print(f"Assistant: {response}")Using RAG-Based Approach
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
# Load and embed documents
model = SentenceTransformer('all-MiniLM-L6-v2')
documents = load_documents("document.pdf")
embeddings = model.encode(documents)
# Create FAISS index
dimension = embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(np.array(embeddings).astype('float32'))
# Chat function
def chat(question):
# Embed question
q_embedding = model.encode(question)
# Retrieve documents
k = 5
distances, indices = index.search(
np.array([q_embedding]).astype('float32'), k
)
# Get relevant documents
context = " ".join([documents[i] for i in indices[0]])
# Generate response
response = llm.generate(
f"Context: {context}\nQuestion: {question}\nAnswer:"
)
return responseBest Practices
Document Handling
- ✓ Support multiple formats (PDF, TXT, docx, etc.)
- ✓ Handle large documents efficiently
- ✓ Preserve document structure
- ✓ Extract metadata
- ✓ Handle multiple languages
- ✓ Implement OCR for scanned PDFs
Conversation Quality
- ✓ Maintain conversation context
- ✓ Ask clarifying questions
- ✓ Cite sources
- ✓ Handle ambiguity
- ✓ Suggest follow-up questions
- ✓ Handle out-of-scope questions
Performance
- ✓ Optimize retrieval speed
- ✓ Implement caching
- ✓ Handle large document sets
- ✓ Batch process documents
- ✓ Monitor latency
- ✓ Implement pagination
User Experience
- ✓ Clear response formatting
- ✓ Ability to cite sources
- ✓ Document browser/explorer
- ✓ Search suggestions
- ✓ Query history
- ✓ Export conversations
Common Challenges & Solutions
Challenge: Irrelevant Answers
Solutions:
- Improve retrieval (more context, better embeddings)
- Validate answer against context
- Ask clarifying questions
- Implement confidence scoring
- Use hybrid search
Challenge: Lost Context Across Turns
Solutions:
- Maintain conversation memory
- Update retrieval based on history
- Summarize long conversations
- Re-weight previous queries
Challenge: Handling Long Documents
Solutions:
- Hierarchical chunking
- Summarize first
- Question refinement
- Multi-hop retrieval
- Document navigation
Challenge: Limited Context Window
Solutions:
- Compress retrieved context
- Use document summarization
- Hierarchical retrieval
- Focus on most relevant sections
- Iterative refinement
Advanced Features
Multi-Document Analysis
def compare_documents(question: str, documents: List[str]):
"""Analyze and compare across multiple documents"""
results = []
for doc in documents:
response = query_document(doc, question)
results.append({
"document": doc.name,
"answer": response
})
# Compare and synthesize
comparison = llm.generate(
f"Compare these answers: {results}"
)
return comparisonInteractive Document Exploration
class DocumentExplorer:
def __init__(self, documents):
self.documents = documents
def browse_by_topic(self, topic):
"""Find documents by topic"""
pass
def get_related_documents(self, doc_id):
"""Find similar documents"""
pass
def get_key_terms(self, document):
"""Extract key terms and concepts"""
passResources
Document Processing Libraries
- PyPDF: PDF handling
- python-docx: Word document handling
- BeautifulSoup: Web scraping
- youtube-transcript-api: YouTube transcripts
Chat Frameworks
- LangChain: Comprehensive framework
- LlamaIndex: Document-focused
- RAG libraries: Vector DB integration
Implementation Checklist
- [ ] Choose document source(s) to support
- [ ] Implement document loading and processing
- [ ] Set up vector database/embeddings
- [ ] Build chat interface
- [ ] Implement conversation management
- [ ] Add source citation
- [ ] Handle edge cases (large docs, OCR, etc.)
- [ ] Implement error handling
- [ ] Add performance monitoring
- [ ] Test with real documents
- [ ] Deploy and monitor
Getting Started
1. Start Simple: Single PDF, basic chat 2. Add Features: Multi-document, conversation history 3. Improve Quality: Better chunking, retrieval 4. Scale: Support more formats, larger documents 5. Polish: UX improvements, error handling
"""
Conversation Management Module
Manages chat conversations and context.
"""
from datetime import datetime
from typing import List, Dict
class ConversationManager:
"""Manages conversation history and context."""
def __init__(self, max_history: int = 10):
"""
Initialize conversation manager.
Args:
max_history: Maximum messages to keep in history
"""
self.messages: List[Dict] = []
self.max_history = max_history
def add_message(self, role: str, content: str):
"""Add message to history."""
self.messages.append({
"role": role,
"content": content,
"timestamp": datetime.now()
})
# Maintain size limit
if len(self.messages) > self.max_history:
self.messages.pop(0)
def get_context(self) -> str:
"""Get conversation context for LLM."""
return "\n".join([
f"{msg['role']}: {msg['content']}"
for msg in self.messages
])
def clear_history(self):
"""Clear conversation history."""
self.messages = []
def get_messages(self) -> List[Dict]:
"""Get all messages."""
return self.messages
def refine_question(current_question: str, conversation: List[str]) -> str:
"""Expand implicit references in question."""
if len(current_question.split()) < 5:
if current_question.startswith(("it ", "that ", "this ")):
# Reference previous context
if conversation:
previous_context = conversation[-1][:100]
refined = f"{previous_context} {current_question}"
return refined
return current_question
def generate_follow_up_questions(context: str, response: str, llm=None) -> List[str]:
"""Generate follow-up questions."""
if not llm:
return [
"Can you provide more details?",
"How does this relate to other topics?",
"What are the next steps?"
]
prompt = f"""
Based on this Q&A, generate 3 relevant follow-up questions:
Context: {context[:500]}
Response: {response[:500]}
"""
follow_ups = llm.generate(prompt)
return follow_ups
"""
Document Processors Module
Handles extraction from various document types.
"""
from pypdf import PdfReader
import requests
from bs4 import BeautifulSoup
import re
from typing import Dict, List
def extract_pdf_content(file_path: str) -> Dict:
"""Extract text from PDF."""
reader = PdfReader(file_path)
text = ""
metadata = {}
for page_num, page in enumerate(reader.pages):
text += page.extract_text()
if reader.metadata:
metadata = {
"title": reader.metadata.get("/Title", ""),
"author": reader.metadata.get("/Author", ""),
"pages": len(reader.pages)
}
return {
"text": text,
"metadata": metadata,
"source": file_path
}
def extract_github_content(repo_url: str) -> List[Dict]:
"""Extract code files from GitHub repository."""
import base64
# Parse repo URL and build API URL
api_url = repo_url.replace("github.com", "api.github.com/repos")
response = requests.get(f"{api_url}/contents")
files = []
for item in response.json():
if item["type"] == "file":
content_response = requests.get(item["url"])
content = base64.b64decode(
content_response.json()["content"]
).decode()
files.append({
"path": item["path"],
"content": content
})
return files
def extract_email_content(mailbox_path: str) -> List[Dict]:
"""Extract emails from mailbox."""
import email
import os
emails = []
for filename in os.listdir(mailbox_path):
with open(os.path.join(mailbox_path, filename), 'rb') as f:
msg = email.message_from_binary_file(f)
emails.append({
"from": msg["From"],
"to": msg["To"],
"subject": msg["Subject"],
"date": msg["Date"],
"body": msg.get_payload()
})
return emails
def extract_web_content(url: str) -> Dict:
"""Extract content from web page."""
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
text = soup.get_text()
headings = [h.get_text() for h in soup.find_all(['h1', 'h2', 'h3'])]
links = [(a.get_text(), a.get('href')) for a in soup.find_all('a')]
return {
"text": text,
"headings": headings,
"links": links,
"url": url
}
def extract_youtube_content(video_id: str) -> str:
"""Extract transcript from YouTube video."""
from youtube_transcript_api import YouTubeTranscriptApi
transcript = YouTubeTranscriptApi.get_transcript(video_id)
text = " ".join([item["text"] for item in transcript])
return text
def extract_audio_content(audio_file: str) -> str:
"""Transcribe audio file."""
import speech_recognition as sr
recognizer = sr.Recognizer()
with sr.AudioFile(audio_file) as source:
audio = recognizer.record(source)
text = recognizer.recognize_google(audio)
return text
"""
Text Processing and Chunking Module
Handles document preprocessing and intelligent chunking.
"""
import re
from typing import List, Dict
def preprocess_document(text: str) -> str:
"""Clean and preprocess document text."""
# Remove extra whitespace
text = re.sub(r'\s+', ' ', text).strip()
# Remove special characters but keep punctuation
text = re.sub(r'[^\w\s\.\!\?\,-]', '', text)
return text
def chunk_text_recursive(text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]:
"""Split text into chunks with overlap."""
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
chunk = text[start:end]
# Try to split on sentence boundary
if end < len(text):
last_period = chunk.rfind('.')
if last_period > chunk_size // 2:
end = start + last_period + 1
chunk = text[start:end]
chunks.append(chunk)
start = end - overlap
return chunks
def extract_metadata(document: Dict) -> Dict:
"""Extract metadata from document."""
return {
"title": document.get("metadata", {}).get("title", "Unknown"),
"author": document.get("metadata", {}).get("author", "Unknown"),
"date": document.get("metadata", {}).get("date", "Unknown"),
"language": "en",
"word_count": len(document.get("text", "").split()),
"source": document.get("source", "unknown")
}
def preserve_document_structure(text: str) -> List[Dict]:
"""Preserve heading hierarchy in chunks."""
chunks = []
current_section = ""
current_context = ""
for line in text.split('\n'):
# Check if line is a heading (simple heuristic)
if line.startswith(('#', '##', '###')) or line.isupper():
if current_section:
chunks.append({
"text": current_section,
"context": current_context,
"heading": current_context
})
current_context = line
current_section = ""
else:
current_section += line + "\n"
return chunks