Agent Template

New Hire Handbook Q&A

A grounded onboarding assistant that lets new hires upload one or more company documents, then ask policy questions answered strictly from those uploaded materials with source citations and clear 'not covered' responses.

onboarding hr employee handbook policy qa rag pdf
ozzo Jul 13, 2026 1 use

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

New Hire Handbook 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 onboarding hr employee handbook policy qa rag pdf
Template Preview

Template Metadata

Slug
new-hire-handbook-qa
Created By
ozzo
Created
Jul 13, 2026
Usage Count
1

Tags

onboarding hr employee handbook policy qa rag pdf

Code Statistics

HTML Lines
93
CSS Lines
320
JS Lines
218
Python Lines
110

Source Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>{{ agent_name }}</title>
  <style>{{ css_code }}</style>
</head>
<body>
  <div class="app-shell">
    <header class="hero">
      <div>
        <div class="eyebrow">📘 New Hire Onboarding</div>
        <h1>{{ agent_name }}</h1>
        <p class="subtitle">{{ description }}</p>
      </div>
      <div class="hero-card">
        <div class="hero-stat">🧾 Grounded answers only</div>
        <div class="hero-stat">📎 Source citations by document</div>
        <div class="hero-stat">🚫 Says clearly when not covered</div>
      </div>
    </header>

    <main class="workspace">
      <section class="panel intake-panel">
        <div class="panel-head">
          <h2>📂 Document Library</h2>
          <p>Upload handbooks, benefits guides, and onboarding policies.</p>
        </div>

        <div class="upload-box">
          <label for="file-input" class="upload-label">Choose handbook files</label>
          <input id="file-input" type="file" accept=".pdf,.txt,.md" multiple />
          <p class="hint">Supports PDF, TXT, and Markdown. Upload multiple files for better coverage.</p>
        </div>

        <div class="button-row">
          <button id="upload-button" class="primary-btn">Add documents</button>
          <button id="clear-button" class="secondary-btn">Clear library</button>
        </div>

        <div id="upload-error" class="inline-error hidden" aria-live="polite"></div>
        <div id="upload-status" class="status-text" aria-live="polite">No documents uploaded yet.</div>

        <div class="library-box">
          <h3>📚 Uploaded Sources</h3>
          <div id="doc-list" class="doc-list empty-list">No source documents yet.</div>
        </div>
      </section>

      <section class="panel qa-panel">
        <div class="panel-head">
          <h2>💬 Handbook Questions</h2>
          <p>Ask about leave, benefits, reimbursement, conduct, travel, or any other policy in your uploaded documents.</p>
        </div>

        <div class="question-box">
          <label for="question-input">Your onboarding question</label>
          <textarea id="question-input" rows="4" placeholder="Example: What is the expense reimbursement policy, and which document explains it?"></textarea>
        </div>

        <div class="button-row">
          <button id="ask-button" class="primary-btn">Answer from documents</button>
        </div>

        <div id="question-error" class="inline-error hidden" aria-live="polite"></div>

        <div id="results-container" class="results-container">
          <div class="empty-state">
            <div class="empty-icon">🧭</div>
            <h3>Ready for onboarding Q&A</h3>
            <ul>
              <li>Upload one or more employee documents</li>
              <li>Ask a policy question in plain language</li>
              <li>Get answers grounded in uploaded text only</li>
              <li>See which document each answer came from</li>
            </ul>
          </div>
        </div>
      </section>
    </main>
  </div>

  <div id="loading-overlay" class="loading-overlay hidden" aria-hidden="true">
    <div class="spinner-card">
      <div class="spinner"></div>
      <div id="loading-text">Working on your onboarding request...</div>
    </div>
  </div>

  <script>{{ js_code }}</script>
</body>
</html>
:root {
  --bg: #f4f7fb;
  --surface: #ffffff;
  --surface-2: #eef4ff;
  --text: #1f2937;
  --muted: #5b6472;
  --primary: #2563eb;
  --primary-strong: #1d4ed8;
  --accent: #0f766e;
  --border: #d7e0ee;
  --danger: #b42318;
  --danger-bg: #fef3f2;
  --shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
  --radius: 18px;
  --radius-sm: 12px;
  --font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: var(--font);
  background: linear-gradient(180deg, #f8fbff 0%, var(--bg) 100%);
  color: var(--text);
}

.hidden { display: none !important; }

.app-shell {
  max-width: 1400px;
  margin: 0 auto;
  padding: 24px;
}

.hero {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: 20px;
  align-items: stretch;
  margin-bottom: 24px;
}

.eyebrow {
  display: inline-block;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--primary);
  background: #e8f0ff;
  padding: 8px 12px;
  border-radius: 999px;
  margin-bottom: 12px;
}

h1 {
  margin: 0 0 8px;
  font-size: clamp(2rem, 4vw, 3rem);
  line-height: 1.05;
}

.subtitle {
  margin: 0;
  color: var(--muted);
  font-size: 1.05rem;
  max-width: 760px;
}

.hero-card, .panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.hero-card {
  padding: 20px;
  display: grid;
  gap: 12px;
}

.hero-stat {
  padding: 14px 16px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-weight: 600;
}

.workspace {
  display: grid;
  grid-template-columns: 420px 1fr;
  gap: 24px;
}

.panel {
  padding: 20px;
}

.panel-head h2 {
  margin: 0 0 6px;
  font-size: 1.25rem;
}

.panel-head p {
  margin: 0 0 18px;
  color: var(--muted);
}

.upload-box, .question-box, .library-box {
  margin-bottom: 18px;
}

.upload-label, label {
  display: block;
  font-weight: 700;
  margin-bottom: 8px;
}

input[type="file"], textarea {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px;
  background: #fff;
  font: inherit;
}

textarea {
  resize: vertical;
  min-height: 120px;
}

.hint, .status-text {
  color: var(--muted);
  font-size: 0.95rem;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 14px;
}

button {
  appearance: none;
  border: none;
  border-radius: 12px;
  padding: 12px 16px;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: transform 0.15s ease, opacity 0.15s ease, background 0.15s ease;
}

button:hover:not(:disabled) { transform: translateY(-1px); }
button:disabled { opacity: 0.55; cursor: not-allowed; }

.primary-btn {
  background: var(--primary);
  color: white;
}

.primary-btn:hover:not(:disabled) { background: var(--primary-strong); }

.secondary-btn {
  background: #e9eef7;
  color: var(--text);
}

.doc-list {
  display: grid;
  gap: 10px;
}

.doc-item {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  background: #fbfdff;
}

.doc-name {
  font-weight: 700;
  margin-bottom: 4px;
}

.doc-meta {
  color: var(--muted);
  font-size: 0.92rem;
}

.empty-list {
  color: var(--muted);
}

.inline-error {
  background: var(--danger-bg);
  color: var(--danger);
  border: 1px solid #fecdca;
  border-radius: 12px;
  padding: 12px 14px;
  margin-bottom: 14px;
}

.results-container {
  min-height: 360px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
  padding: 18px;
}

.empty-state {
  text-align: left;
  color: var(--muted);
  max-width: 560px;
}

.empty-icon {
  font-size: 2rem;
  margin-bottom: 8px;
}

.empty-state h3 {
  color: var(--text);
  margin-bottom: 10px;
}

.empty-state ul {
  margin: 0;
  padding-left: 18px;
}

.message {
  border-radius: 14px;
  padding: 14px 16px;
  margin-bottom: 14px;
  border: 1px solid var(--border);
}

.message.user {
  background: #edf4ff;
}

.message.assistant {
  background: #ffffff;
}

.message.system {
  background: #eefbf8;
}

.message-role {
  font-size: 0.82rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 8px;
  color: var(--muted);
}

.message-content {
  white-space: pre-wrap;
  line-height: 1.6;
}

.loading-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.38);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999;
}

.spinner-card {
  background: white;
  border-radius: 18px;
  padding: 24px 28px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

.spinner {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 4px solid #dbe7ff;
  border-top-color: var(--primary);
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

button:focus-visible,
textarea:focus-visible,
input:focus-visible {
  outline: 3px solid rgba(37, 99, 235, 0.22);
  outline-offset: 2px;
}

@media (max-width: 1200px) {
  .workspace, .hero {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 768px) {
  .app-shell { padding: 16px; }
  .panel { padding: 16px; }
  .button-row { flex-direction: column; }
  button { width: 100%; }
}
const state = {
  pyodideReady: false,
  docs: []
};

const els = {};

function escapeHtml(value) {
  return String(value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function setLoading(isLoading, text) {
  els.loadingOverlay.classList.toggle('hidden', !isLoading);
  els.loadingText.textContent = text || 'Working on your onboarding request...';
  els.askButton.disabled = isLoading;
  els.uploadButton.disabled = isLoading;
  els.clearButton.disabled = isLoading;
}

function showError(target, message) {
  target.textContent = message;
  target.classList.remove('hidden');
}

function clearError(target) {
  target.textContent = '';
  target.classList.add('hidden');
}

function ensureReady() {
  if (!state.pyodideReady || !window.pyodide) {
    return 'The agent is still initializing. Please wait a moment and try again.';
  }
  if (window.PROVIDER === 'local') {
    if (!window.agentManager || !window.agentManager.isLoaded) {
      return 'Please load a local model from the selector bar at the top, then try again.';
    }
  }
  return '';
}

function renderDocList() {
  if (!state.docs.length) {
    els.docList.className = 'doc-list empty-list';
    els.docList.innerHTML = 'No source documents yet.';
    return;
  }
  els.docList.className = 'doc-list';
  els.docList.innerHTML = state.docs.map((doc) => {
    return '<div class="doc-item">'
      + '<div class="doc-name">📄 ' + escapeHtml(doc.name) + '</div>'
      + '<div class="doc-meta">Type: ' + escapeHtml(doc.type) + ' · Indexed chunks: ' + escapeHtml(doc.chunks) + '</div>'
      + '</div>';
  }).join('');
}

function addMessage(type, content) {
  const safeType = escapeHtml(type);
  const safeContent = escapeHtml(content);
  if (els.resultsContainer.querySelector('.empty-state')) {
    els.resultsContainer.innerHTML = '';
  }
  const wrapper = document.createElement('div');
  wrapper.className = 'message ' + safeType;
  wrapper.innerHTML = '<div class="message-role">' + safeType + '</div><div class="message-content">' + safeContent + '</div>';
  els.resultsContainer.appendChild(wrapper);
  els.resultsContainer.scrollTop = els.resultsContainer.scrollHeight;
}
window.addMessage = addMessage;

async function fileToData(file) {
  const buffer = await file.arrayBuffer();
  const bytes = new Uint8Array(buffer);
  let binary = '';
  const chunkSize = 0x8000;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

async function handleUpload() {
  clearError(els.uploadError);
  const readyError = ensureReady();
  if (readyError) {
    showError(els.uploadError, readyError);
    return;
  }

  const files = Array.from(els.fileInput.files || []);
  if (!files.length) {
    showError(els.uploadError, 'Please choose at least one handbook or policy document to upload.');
    return;
  }

  setLoading(true, 'Indexing onboarding documents...');
  try {
    let added = 0;
    for (const file of files) {
      const base64Data = await fileToData(file);
      window.pyodide.globals.set('upload_name', file.name);
      window.pyodide.globals.set('upload_mime', file.type || 'application/octet-stream');
      window.pyodide.globals.set('upload_data', base64Data);
      const resultJson = await window.pyodide.runPythonAsync('await _add_uploaded_document(upload_name, upload_mime, upload_data)');
      const result = JSON.parse(resultJson);
      if (result.ok) {
        state.docs.push({ name: result.name, type: result.type, chunks: result.chunks });
        added += 1;
      } else {
        throw new Error(result.error || ('Failed to upload ' + file.name));
      }
    }
    renderDocList();
    els.uploadStatus.textContent = 'Added ' + added + ' document' + (added === 1 ? '' : 's') + '. Your onboarding library is ready for questions.';
    els.fileInput.value = '';
    if (els.resultsContainer.querySelector('.empty-state')) {
      addMessage('system', 'Documents indexed successfully. Ask a question about company policies, benefits, time off, or reimbursement.');
    } else {
      addMessage('system', 'Added ' + added + ' new source document' + (added === 1 ? '' : 's') + ' to the onboarding library.');
    }
  } catch (error) {
    showError(els.uploadError, 'Upload failed: ' + (error && error.message ? error.message : 'Unknown error'));
  } finally {
    setLoading(false);
  }
}

async function handleAsk() {
  clearError(els.questionError);
  const readyError = ensureReady();
  if (readyError) {
    showError(els.questionError, readyError);
    return;
  }

  const question = els.questionInput.value.trim();
  if (!question) {
    showError(els.questionError, 'Please enter an onboarding or policy question.');
    return;
  }
  if (!state.docs.length) {
    showError(els.questionError, 'Please upload at least one handbook or onboarding document before asking a question.');
    return;
  }

  addMessage('user', question);
  setLoading(true, 'Searching uploaded documents and drafting a grounded answer...');
  try {
    window.pyodide.globals.set('user_query', question);
    const answer = await window.pyodide.runPythonAsync('await process_user_query(user_query)');
    addMessage('assistant', answer);
  } catch (error) {
    showError(els.questionError, 'Question failed: ' + (error && error.message ? error.message : 'Unknown error'));
  } finally {
    setLoading(false);
  }
}

async function handleClear() {
  clearError(els.uploadError);
  clearError(els.questionError);
  const readyError = ensureReady();
  if (readyError) {
    showError(els.uploadError, readyError);
    return;
  }

  setLoading(true, 'Clearing onboarding library...');
  try {
    await window.pyodide.runPythonAsync('await _clear_all_documents()');
    state.docs = [];
    renderDocList();
    els.uploadStatus.textContent = 'No documents uploaded yet.';
    els.resultsContainer.innerHTML = '<div class="empty-state"><div class="empty-icon">🧭</div><h3>Ready for onboarding Q&A</h3><ul><li>Upload one or more employee documents</li><li>Ask a policy question in plain language</li><li>Get answers grounded in uploaded text only</li><li>See which document each answer came from</li></ul></div>';
  } catch (error) {
    showError(els.uploadError, 'Could not clear the library: ' + (error && error.message ? error.message : 'Unknown error'));
  } finally {
    setLoading(false);
  }
}

function bindUI() {
  els.fileInput = document.getElementById('file-input');
  els.uploadButton = document.getElementById('upload-button');
  els.clearButton = document.getElementById('clear-button');
  els.askButton = document.getElementById('ask-button');
  els.questionInput = document.getElementById('question-input');
  els.resultsContainer = document.getElementById('results-container');
  els.uploadError = document.getElementById('upload-error');
  els.questionError = document.getElementById('question-error');
  els.docList = document.getElementById('doc-list');
  els.uploadStatus = document.getElementById('upload-status');
  els.loadingOverlay = document.getElementById('loading-overlay');
  els.loadingText = document.getElementById('loading-text');

  els.uploadButton.addEventListener('click', handleUpload);
  els.clearButton.addEventListener('click', handleClear);
  els.askButton.addEventListener('click', handleAsk);
  els.fileInput.addEventListener('change', () => clearError(els.uploadError));
  els.questionInput.addEventListener('input', () => clearError(els.questionError));
}

function markReady() {
  state.pyodideReady = true;
}

bindUI();
if (window.pyodideReady) {
  markReady();
} else {
  document.addEventListener('pyodide-ready', markReady, { once: true });
}
renderDocList();
import base64
import io
import json
from typing import List

from pypdf import PdfReader

MAX_CHARS = int([[[MAX_CHARS|6000]]])

DOCUMENT_REGISTRY: List[dict] = []


def _truncate_text(text: str, limit: int = MAX_CHARS) -> str:
    text = text or ""
    text = text.replace("\x00", " ")
    text = "\n".join(line.rstrip() for line in text.splitlines())
    text = text.strip()
    if len(text) <= limit:
        return text
    return text[:limit] + "\n\n[Truncated for indexing]"


def _extract_pdf_text(pdf_bytes: bytes) -> str:
    reader = PdfReader(io.BytesIO(pdf_bytes))
    pages = []
    for page in reader.pages:
        try:
            pages.append(page.extract_text() or "")
        except Exception:
            pages.append("")
    return "\n\n".join(pages)


async def _add_uploaded_document(name: str, mime_type: str, base64_data: str) -> str:
    raw = base64.b64decode(base64_data)
    lower_name = (name or "document").lower()
    doc_type = "text"

    if lower_name.endswith(".pdf") or mime_type == "application/pdf":
        text = _extract_pdf_text(raw)
        doc_type = "pdf"
    else:
        text = raw.decode("utf-8", errors="ignore")
        doc_type = "text"

    text = _truncate_text(text)
    if not text.strip():
        return json.dumps({"ok": False, "error": f"No readable text found in {name}."})

    chunks = await agentop_rag.add_document(name, text)
    DOCUMENT_REGISTRY.append({"name": name, "type": doc_type, "chunks": chunks})
    return json.dumps({"ok": True, "name": name, "type": doc_type, "chunks": chunks})


async def _clear_all_documents() -> str:
    await agentop_rag.clear()
    DOCUMENT_REGISTRY.clear()
    return "ok"


async def process_user_query(query: str) -> str:
    q = (query or "").strip()
    if not q:
        return "Please ask a question about your uploaded onboarding documents."
    if not DOCUMENT_REGISTRY:
        return "No documents are loaded yet. Please upload your employee handbook or related guides first."

    hits = await agentop_rag.search(q, k=6)
    if not hits:
        return (
            "Not covered in the uploaded documents.\n\n"
            "I could not find relevant language in the handbook library you uploaded. "
            "Try uploading additional documents or rephrasing your question."
        )

    usable_hits = [h for h in hits if (h.get("text") or "").strip()]
    if not usable_hits:
        return (
            "Not covered in the uploaded documents.\n\n"
            "I found no readable supporting text for this question in the uploaded files."
        )

    context_blocks = []
    for i, hit in enumerate(usable_hits[:6], start=1):
        source = hit.get("source", "Unknown document")
        text = (hit.get("text", "") or "").strip()
        score = hit.get("score", 0)
        snippet = text[:1200]
        context_blocks.append(f"[{i}] Source: {source}\nRelevance: {score}\nExcerpt: {snippet}")

    grounded_prompt = (
        "Answer the employee onboarding question using only the provided document excerpts.\n"
        "Rules:\n"
        "1. Do not use outside knowledge.\n"
        "2. If the answer is not stated or not clear in the excerpts, say exactly: Not covered in the uploaded documents.\n"
        "3. Cite supporting documents by source name in a Sources section.\n"
        "4. Be concise and factual.\n"
        "5. Do not invent policy details, numbers, dates, or eligibility rules.\n\n"
        f"Question:\n{q}\n\n"
        f"Document excerpts:\n\n{'\n\n'.join(context_blocks)}\n\n"
        "Required output format:\n"
        "Answer: <short grounded answer or 'Not covered in the uploaded documents.'>\n"
        "Sources:\n- <document name>\n- <document name>"
    )

    reply = await process_user_query_wllama(
        grounded_prompt,
        globals().get("TEMPLATE_SYSTEM_PROMPT", "")
    )
    return (reply or "").strip()