Agent Template

Annual Benefit Limit Extractor

Extract and compare annual benefit limits as integers from two to six product brochure PDFs, with page references and supporting excerpts.

PDF insurance benefits comparison
gary Sep 29, 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

Annual Benefit Limit Extractor 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 PDF insurance benefits comparison
Template Preview

Template Metadata

Slug
annual-benefit-limit-extractor
Created By
gary
Created
Sep 29, 2026
Usage Count
1

Tags

PDF insurance benefits comparison

Code Statistics

HTML Lines
36
CSS Lines
1
JS Lines
67
Python Lines
94

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>
  <div class="app-shell">
    <header class="masthead"><div class="eyebrow">📄 BROCHURE INTELLIGENCE</div><h1>{{ agent_name }}</h1><p>{{ description }}</p></header>
    <main class="workspace">
      <section class="panel input-panel" aria-labelledby="source-title">
        <div class="section-label">01 / SOURCE DOCUMENTS</div><h2 id="source-title">Product brochures</h2>
        <p class="hint">Select 2–6 text-based PDFs. Maximum 15 MB each. Scanned PDFs without selectable text cannot be read.</p>
        <form id="extract-form">
          <label class="upload-zone" for="pdf-files"><span class="upload-symbol">📎</span><strong>Choose product brochure PDFs</strong><small>Multiple PDF files · 2 minimum</small></label>
          <input id="pdf-files" type="file" accept=".pdf,application/pdf" multiple>
          <div id="file-list" class="file-list" aria-live="polite">No product brochures selected yet.</div>
          <label class="field-label" for="focus-input">Extraction focus <span>(optional)</span></label>
          <textarea id="focus-input" rows="3" placeholder="For example: Identify the overall annual benefit limit for every plan tier."></textarea>
          <div id="inline-error" class="error hidden" role="alert"></div>
          <div class="actions"><button id="extract-button" class="primary" type="submit">Extract annual limits →</button><button id="clear-button" class="secondary" type="button">Clear brochures</button></div>
        </form>
        <div id="runtime-status" class="runtime" aria-live="polite">Initializing PDF analysis…</div>
      </section>
      <section class="panel results-panel" aria-labelledby="results-title">
        <div class="section-label">02 / COMPARISON</div><h2 id="results-title">Annual benefit limits</h2>
        <div id="results-container" aria-live="polite"><div class="empty-state"><div class="empty-icon">📊</div><h3>Build your benefit-limit comparison</h3><p>Upload brochures to see:</p><ul><li>Integer annual benefit limits by product</li><li>Separate limits when plan tiers are stated</li><li>Page references and supporting excerpts</li><li>A clear flag where no limit can be confirmed</li></ul></div></div>
      </section>
    </main>
  </div>
  <div id="loading-overlay" class="overlay hidden" role="status" aria-live="polite"><div class="loading-card"><div class="spinner"></div><strong>Reading product brochures…</strong><span>Checking annual limits and supporting pages.</span></div></div>
  <script>{{ js_code }}</script>
</body>
</html>
:root{--ink:#142c37;--muted:#5c7079;--brand:#07685f;--brand-dark:#054e48;--surface:#ffffff;--background:#eff5f3;--line:#d7e5df;--accent:#e0f5ec;--danger:#a42b35;--radius:18px;--shadow:0 14px 38px rgba(20,44,55,.08);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink)}*{box-sizing:border-box}body{margin:0;background:var(--background);min-height:100vh}.hidden{display:none!important}.app-shell{max-width:1440px;margin:0 auto;padding:36px 34px 70px}.masthead{margin-bottom:30px}.eyebrow,.section-label{letter-spacing:.13em;font-size:.72rem;font-weight:800;color:var(--brand)}.masthead h1{font-size:clamp(2rem,4vw,3.4rem);letter-spacing:-.045em;margin:10px 0}.masthead p,.hint{color:var(--muted);line-height:1.55}.masthead p{max-width:760px}.workspace{display:grid;grid-template-columns:minmax(330px,.85fr) minmax(420px,1.15fr);gap:22px;align-items:start}.panel{border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);padding:28px;box-shadow:var(--shadow);min-width:0}.panel h2{font-size:1.5rem;letter-spacing:-.025em;margin:8px 0}.hint{font-size:.9rem;margin:10px 0 22px}.upload-zone{border:2px dashed #8ab6a6;border-radius:14px;background:#f5fbf8;display:flex;flex-direction:column;align-items:center;gap:8px;padding:29px 18px;text-align:center;cursor:pointer}.upload-zone:hover{background:var(--accent)}.upload-symbol{font-size:2rem}.upload-zone small{color:var(--muted)}input[type=file]{width:100%;margin:12px 0 2px;color:var(--muted);font-size:.85rem}.file-list{margin:10px 0 24px;white-space:pre-line;color:var(--muted);font-size:.87rem;line-height:1.7;overflow-wrap:anywhere}.field-label{font-weight:700;display:block;margin-bottom:9px}.field-label span{color:var(--muted);font-weight:400}textarea{font:inherit;width:100%;padding:13px;border:1px solid var(--line);border-radius:11px;resize:vertical;min-height:86px;color:var(--ink)}:focus-visible{outline:3px solid #58bba1;outline-offset:3px}.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}button{font:inherit;font-weight:700;border-radius:10px;padding:12px 17px;cursor:pointer;border:1px solid transparent}.primary{background:var(--brand);color:#fff}.primary:hover{background:var(--brand-dark)}.secondary{background:#fff;color:var(--brand);border-color:var(--line)}button:disabled{opacity:.53;cursor:not-allowed}.runtime{font-size:.8rem;color:var(--muted);border-top:1px solid var(--line);padding-top:15px;margin-top:20px}.error{margin-top:14px;padding:12px;border-radius:10px;color:var(--danger);background:#fff0f0;font-size:.9rem}.empty-state{padding:38px 24px;border:1px dashed var(--line);border-radius:14px;background:#fbfdfc;margin-top:20px}.empty-icon{font-size:2.4rem}.empty-state h3{margin:12px 0 6px}.empty-state p,.empty-state li{color:var(--muted);line-height:1.8}.empty-state ul{padding-left:20px}.result-intro{color:var(--muted);font-size:.9rem}.report-card{border:1px solid var(--line);border-radius:13px;padding:18px;margin-top:14px;background:#fcfefd}.report-card h3{margin:0 0 12px;font-size:1rem;overflow-wrap:anywhere}.report-card pre{font:inherit;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.overlay{position:fixed;inset:0;background:rgba(11,36,39,.42);display:grid;place-items:center;z-index:30}.loading-card{background:#fff;border-radius:16px;padding:32px;box-shadow:var(--shadow);display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center;max-width:330px}.loading-card span{color:var(--muted);font-size:.9rem}.spinner{height:36px;width:36px;border:4px solid var(--accent);border-top-color:var(--brand);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media(max-width:1200px){.workspace{grid-template-columns:1fr}.app-shell{max-width:900px}}@media(max-width:768px){.app-shell{padding:23px 15px 45px}.panel{padding:19px}.actions button{flex:1}.empty-state{padding:22px 16px}}
(() => {
  const form = document.getElementById('extract-form');
  const filesInput = document.getElementById('pdf-files');
  const focusInput = document.getElementById('focus-input');
  const fileList = document.getElementById('file-list');
  const results = document.getElementById('results-container');
  const errorBox = document.getElementById('inline-error');
  const status = document.getElementById('runtime-status');
  const overlay = document.getElementById('loading-overlay');
  const extractButton = document.getElementById('extract-button');
  const clearButton = document.getElementById('clear-button');
  const emptyMarkup = results.innerHTML;
  let busy = false;
  const escapeHtml = value => String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  const ready = () => Boolean(window.pyodide && (window.pyodideReady || window.__brochureReady));
  const modelMissing = () => {
    const provider = String(window.PROVIDER || '').toLowerCase();
    return (provider === 'local' || provider === 'wllama' || Boolean(window.agentManager)) && !(window.agentManager && window.agentManager.isLoaded);
  };
  function refreshStatus(){
    status.textContent = !ready() ? 'Initializing PDF analysis…' : modelMissing() ? 'Load a model from the top bar before extracting annual limits.' : 'Ready to compare product brochure PDFs.';
    extractButton.disabled = busy || !ready() || modelMissing();
  }
  function showError(message){ errorBox.textContent = message; errorBox.classList.remove('hidden'); }
  function clearError(){ errorBox.textContent = ''; errorBox.classList.add('hidden'); }
  function setBusy(value){ busy = value; overlay.classList.toggle('hidden', !value); filesInput.disabled = value; focusInput.disabled = value; clearButton.disabled = value; refreshStatus(); }
  filesInput.addEventListener('change', () => {
    const selected = Array.from(filesInput.files || []);
    fileList.textContent = selected.length ? selected.map((file, index) => `${index + 1}. ${file.name}`).join('\n') : 'No product brochures selected yet.';
    clearError(); results.innerHTML = emptyMarkup;
  });
  clearButton.addEventListener('click', () => {
    filesInput.value = ''; focusInput.value = ''; fileList.textContent = 'No product brochures selected yet.';
    results.innerHTML = emptyMarkup; clearError();
  });
  document.addEventListener('pyodide-ready', () => { window.__brochureReady = true; refreshStatus(); });
  refreshStatus();
  const readinessTimer = setInterval(() => { refreshStatus(); if (ready() && !modelMissing()) clearInterval(readinessTimer); }, 1200);
  form.addEventListener('submit', async event => {
    event.preventDefault();
    const selected = Array.from(filesInput.files || []);
    const focus = focusInput.value.trim();
    clearError();
    if (selected.length < 2 || selected.length > 6) { showError('Choose 2–6 product brochure PDFs.'); return; }
    if (selected.some(file => !/\.pdf$/i.test(file.name) && file.type !== 'application/pdf')) { showError('Every selected brochure must be a PDF.'); return; }
    if (selected.some(file => file.size > 15 * 1024 * 1024 || file.size === 0)) { showError('Each PDF must be nonempty and no larger than 15 MB.'); return; }
    if (!ready()) { showError('PDF analysis is still initializing. Please try again shortly.'); return; }
    if (modelMissing()) { showError('Load a model from the top bar before extracting annual limits.'); return; }
    setBusy(true);
    try {
      await window.pyodide.runPythonAsync('_reset_documents()');
      for (const file of selected) {
        const data = Array.from(new Uint8Array(await file.arrayBuffer()));
        window.pyodide.globals.set('pdf_name', file.name);
        window.pyodide.globals.set('pdf_bytes', data);
        await window.pyodide.runPythonAsync('_add_pdf(pdf_name, pdf_bytes)');
      }
      window.pyodide.globals.set('user_query', focus || 'Extract the overall annual benefit limit as an integer from each brochure; list each plan tier separately if applicable.');
      const report = String(await window.pyodide.runPythonAsync('await process_user_query(user_query)'));
      const blocks = report.split('\n=== BROCHURE ===\n').filter(Boolean);
      results.innerHTML = '<p class="result-intro">Annual benefit limit findings, with values shown as integers where confirmed.</p>' + blocks.map(block => '<article class="report-card"><pre>' + escapeHtml(block.trim()) + '</pre></article>').join('');
    } catch (err) {
      results.innerHTML = emptyMarkup;
      showError('Could not analyze these brochures: ' + (err && err.message ? err.message : String(err)));
    } finally { setBusy(false); }
  });
})();
import io
import re
from pypdf import PdfReader

_documents = []


def _reset_documents():
    _documents.clear()


def _add_pdf(name, raw):
    try:
        reader = PdfReader(io.BytesIO(bytes(raw)))
        if reader.is_encrypted:
            raise ValueError('This PDF is password-protected.')
        pages = []
        for number, page in enumerate(reader.pages, 1):
            content = page.extract_text(extraction_mode='layout') or ''
            content = re.sub(r'[ \t]+', ' ', content)
            pages.append((number, content))
        if sum(len(text.strip()) for _, text in pages) < 40:
            raise ValueError('No selectable text found. Scanned PDFs need OCR before upload.')
        _documents.append((str(name), pages))
        return str(len(pages)) + ' pages loaded'
    except ValueError as exc:
        raise ValueError(str(name) + ': ' + str(exc)) from exc
    except Exception as exc:
        raise ValueError(str(name) + ': Unable to read the PDF; confirm it is a valid text-based brochure.') from exc


def _evidence(pages):
    terms = re.compile(r'annual benefit|annual limit|annual maximum|overall limit|yearly limit|per (?:policy )?year|per annum|sum insured|maximum benefit|benefit limit', re.I)
    candidates = []
    for number, text in pages:
        for match in list(terms.finditer(text))[:28]:
            start = max(0, match.start() - 260)
            end = min(len(text), match.end() + 650)
            fragment = text[start:end].strip()
            weight = 2 if re.search(r'annual|overall|yearly|per annum', match.group(), re.I) else 1
            candidates.append((weight, number, match.start(), fragment))
    candidates.sort(key=lambda item: (-item[0], item[1], item[2]))
    excerpts = []
    used = set()
    size = 0
    for _, number, position, fragment in candidates:
        key = (number, position // 420)
        if key in used:
            continue
        used.add(key)
        part = '[Page ' + str(number) + '] ' + fragment[:1050]
        if size + len(part) > 5400:
            break
        excerpts.append(part)
        size += len(part)
    if not excerpts:
        excerpts = ['[Page ' + str(number) + '] ' + text[:1350] for number, text in pages if text.strip()][:3]
    return '\n\n'.join(excerpts)[:5500]


def _format_answer(name, answer):
    rows = []
    for line in str(answer).splitlines():
        line = line.strip().strip('`* ')
        if not line or 'LIMIT' not in line.upper():
            continue
        match = re.search(r'LIMIT\s*[:=]\s*(NOT_FOUND|[0-9][0-9, ]*(?:\.00)?)\b', line, re.I)
        if not match:
            continue
        raw = match.group(1)
        normalized = 'NOT_FOUND' if raw.upper() == 'NOT_FOUND' else raw.replace(',', '').replace(' ', '').removesuffix('.00')
        if normalized != 'NOT_FOUND' and not normalized.isdigit():
            continue
        rows.append(re.sub(r'LIMIT\s*[:=]\s*(NOT_FOUND|[0-9][0-9, ]*(?:\.00)?)\b', 'LIMIT=' + normalized, line, count=1, flags=re.I)[:900])
    if not rows:
        rows = ['LIMIT=NOT_FOUND | No unambiguous annual benefit limit confirmed in extracted PDF text.']
    return 'BROCHURE: ' + name + '\n' + '\n'.join(rows[:12])


async def process_user_query(query: str) -> str:
    if len(_documents) < 2:
        return 'Upload at least two product brochure PDFs first.'
    instruction = str(query).strip()[:350]
    reports = []
    for name, pages in _documents:
        prompt = ('Read ONLY the quoted PDF excerpts below. Find the overall annual benefit limit, not a specific treatment sublimit, deductible, or premium. '
                  'If the brochure explicitly lists multiple plan tiers, provide one row per tier. '
                  'Each row MUST follow this exact format: PLAN=<name or overall> | LIMIT=<integer digits only or NOT_FOUND> | CURRENCY=<currency or unknown> | PAGE=<page number or unknown> | EVIDENCE=<short literal excerpt>. '
                  'Do not invent amounts. If the overall annual limit is absent or ambiguous, return LIMIT=NOT_FOUND. '
                  'Do not mistake annual wording for a monetary amount. Use only evidence shown. '
                  'Additional extraction focus: ' + instruction + '\nPDF: ' + name + '\nEXCERPTS:\n' + _evidence(pages))
        answer = await agentop_llm.generate(prompt, globals().get('TEMPLATE_SYSTEM_PROMPT', ''))
        reports.append(_format_answer(name, answer))
    return '\n=== BROCHURE ===\n'.join(reports)