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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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)
Name your agent
Based on . You'll get your own private copy to try and customize.