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", "")
    )