Agent Template
Document Q&A
Upload a PDF or text file and ask questions answered only from that document — fully in your browser, nothing uploaded to a server.
documents
pdf
rag
q&a
privacy
ozzo
Jul 10, 2026
2 uses
Preview Mode
This is a preview with sample data. The template uses placeholders like
which will be replaced with actual agent data.
About This Template
Document Q&A is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.
Topics
documents
pdf
rag
q&a
privacy
Template Preview
Template Metadata
- Slug
- document-qa
- Created By
- ozzo
- Created
- Jul 10, 2026
- Usage Count
- 2
Tags
documents
pdf
rag
q&a
privacy
Code Statistics
- HTML Lines
- 34
- CSS Lines
- 29
- JS Lines
- 68
- Python Lines
- 19
Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ agent_name }}</title>
<style>{{ css_code }}</style>
</head>
<body>
<main class="doc-qa">
<header class="doc-qa__head">
<h1>{{ agent_name }}</h1>
<p class="doc-qa__sub">{{ description }}</p>
</header>
<section class="doc-qa__upload">
<label class="doc-qa__drop" for="doc-file">
<input id="doc-file" type="file" accept=".pdf,.txt,.md,.markdown" multiple hidden>
<span>📄 Upload a PDF or text file, then ask questions about it</span>
</label>
<div id="doc-status" class="doc-qa__status" role="status"></div>
</section>
<section id="results-container" class="doc-qa__chat" aria-live="polite"></section>
<form id="doc-qa-form" class="doc-qa__form" autocomplete="off">
<input id="doc-qa-input" type="text"
placeholder="Ask a question about your document…" required disabled>
<button id="doc-qa-send" type="submit" disabled>Ask</button>
</form>
</main>
<script>{{ js_code }}</script>
</body>
</html>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: #f7f7f8; color: #1a1a1a; }
.doc-qa { max-width: 760px; margin: 0 auto; padding: 24px 16px 96px; }
.doc-qa__head h1 { font-size: 1.6rem; margin: 0 0 4px; }
.doc-qa__sub { margin: 0 0 20px; color: #666; }
.doc-qa__drop { display: block; border: 2px dashed #c7c7cc; border-radius: 12px;
padding: 22px; text-align: center; cursor: pointer; background: #fff;
color: #444; transition: border-color .15s, background .15s; }
.doc-qa__drop:hover { border-color: #6366f1; background: #f5f5ff; }
.doc-qa__status { font-size: .85rem; color: #555; margin: 10px 2px; min-height: 1.2em; }
.doc-qa__chat { display: flex; flex-direction: column; gap: 10px; margin: 8px 0 16px; }
.doc-qa__msg { padding: 10px 14px; border-radius: 12px; max-width: 85%;
white-space: pre-wrap; line-height: 1.5; }
.doc-qa__msg--user { align-self: flex-end; background: #6366f1; color: #fff; }
.doc-qa__msg--assistant { align-self: flex-start; background: #fff; border: 1px solid #eee; }
.doc-qa__form { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 8px;
padding: 12px 16px; background: #fff; border-top: 1px solid #eee; }
.doc-qa__form input { flex: 1; padding: 12px 14px; border: 1px solid #d0d0d5;
border-radius: 10px; font-size: 1rem; }
.doc-qa__form button { padding: 0 20px; border: 0; border-radius: 10px;
background: #6366f1; color: #fff; font-weight: 600; cursor: pointer; }
.doc-qa__form button:disabled { background: #b7b7c6; cursor: not-allowed; }
@media (prefers-color-scheme: dark) {
body { background: #16161a; color: #ececf1; }
.doc-qa__drop, .doc-qa__msg--assistant, .doc-qa__form { background: #202028; border-color: #33333c; }
.doc-qa__form input { background: #16161a; color: #ececf1; border-color: #33333c; }
}
// Document Q&A UI wiring. The RAG/embeddings/wllama infrastructure is injected
// by the generator; this only connects the upload + chat controls to it.
(function () {
const statusEl = () => document.getElementById('doc-status');
const results = () => document.getElementById('results-container');
const input = () => document.getElementById('doc-qa-input');
const sendBtn = () => document.getElementById('doc-qa-send');
let pyReady = false;
// Render helper the wllama query bridge also calls for streamed responses.
window.addMessage = function (type, content) {
const el = document.createElement('div');
el.className = 'doc-qa__msg doc-qa__msg--' + (type === 'user' ? 'user' : 'assistant');
el.textContent = content;
results().appendChild(el);
el.scrollIntoView({ behavior: 'smooth', block: 'end' });
return el;
};
function setStatus(msg) { statusEl().textContent = msg; }
function enableChat(on) {
input().disabled = !on;
sendBtn().disabled = !on;
}
// Reading, PDF extraction and indexing are shared (window.AgentOpDocs);
// only the wording of the status line belongs to this template.
async function ingest(file) {
setStatus('Reading "' + file.name + '"…');
const { source, chunks } = await window.AgentOpDocs.ingestFile(file);
setStatus('Indexed ' + chunks + ' passages from "' + source + '". Ask away!');
enableChat(true);
}
document.getElementById('doc-file').addEventListener('change', async (e) => {
if (!pyReady) { setStatus('Still starting up—one moment…'); return; }
for (const file of e.target.files) {
try { await ingest(file); }
catch (err) { setStatus('Could not read "' + file.name + '": ' + err); }
}
});
document.getElementById('doc-qa-form').addEventListener('submit', async (e) => {
e.preventDefault();
const q = input().value.trim();
if (!q || !pyReady) return;
window.addMessage('user', q);
input().value = '';
enableChat(false);
try {
window.pyodide.globals.set('__q', q);
const answer = await window.pyodide.runPythonAsync('await process_user_query(__q)');
if (answer) window.addMessage('assistant', answer);
} catch (err) {
window.addMessage('assistant', 'Sorry, something went wrong: ' + err);
} finally {
enableChat(true);
input().focus();
}
});
// Pyodide is ready after initAgent(); the orchestrator fires this on document.
document.addEventListener('pyodide-ready', () => {
pyReady = true;
setStatus('Ready. Upload a document to begin.');
});
})();
import json
# How many document passages to retrieve per question (tunable at generation).
RAG_TOP_K = [[[RAG_TOP_K|4]]]
async def process_user_query(query):
"""RAG-first answering: retrieve the most relevant passages from the added
document(s), then answer grounded ONLY in them.
Overrides the default router so retrieval is deterministic (small local
models are unreliable at deciding to call a search tool themselves).
agentop_docs.answer() is that whole pre-step, shared by every RAG template.
TEMPLATE_SYSTEM_PROMPT only exists as a JS global; agentop_llm falls back to
it when the system argument is empty (same idiom as the base template).
"""
return await agentop_docs.answer(
query, RAG_TOP_K, globals().get("TEMPLATE_SYSTEM_PROMPT", "")
)
Name your agent
Based on . You'll get your own private copy to try and customize.