Freelancer Tax Q&A Helper
A friendly, plain-language tax-question helper for freelancers and independent contractors. Ask about deductions, quarterly estimated taxes, and self-employment tax. The agent cites the general rule, explains steps or quick calculations when helpful, and always ends with a short reminder that this isn't professional tax advice.
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
Freelancer Tax Q&A Helper 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.
Template Metadata
- Slug
- freelancer-tax-qa-helper
- Created By
- ozzo
- Created
- Jun 18, 2026
- Usage Count
- 1
Tags
Code Statistics
- HTML Lines
- 65
- CSS Lines
- 45
- JS Lines
- 199
- Python Lines
- 121
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">
<header class="app-header">
<div class="title-wrap">
<h1>🧾 {{ agent_name }}</h1>
<p class="subtitle">{{ description }}</p>
</div>
<div class="controls">
<label for="jurisdiction">Tax jurisdiction</label>
<select id="jurisdiction" aria-label="Select tax jurisdiction">
<option value="US" selected>United States (default)</option>
<option value="UK">United Kingdom</option>
<option value="Canada">Canada</option>
<option value="Australia">Australia</option>
<option value="EU">European Union</option>
<option value="Other">Other / not listed</option>
</select>
<button id="clear-btn" class="secondary" type="button" title="Clear conversation">Clear</button>
</div>
</header>
<main class="chat-layout">
<section class="chat-panel" aria-live="polite" aria-label="Freelancer tax chat">
<div id="results-container" class="messages">
<div id="empty-state" class="empty">
<h2>Ask about freelancer taxes</h2>
<ul>
<li>What counts as an ordinary and necessary business expense?</li>
<li>How to estimate quarterly payments and avoid penalties</li>
<li>Home office, vehicle, and equipment deductions</li>
<li>Self-employment tax basics and quick math examples</li>
</ul>
<div class="chips" id="example-chips"></div>
</div>
</div>
</section>
<footer class="composer">
<div class="input-wrap">
<textarea id="user-input" rows="1" placeholder="Ask a freelancer tax question..." aria-label="Ask a freelancer tax question"></textarea>
<button id="send-btn" class="primary" type="button">Send</button>
</div>
<p class="helper-text">Friendly guidance only. For complex or high-stakes issues, consult a tax professional.</p>
</footer>
</main>
<div id="overlay" class="overlay hidden" aria-hidden="true">
<div class="spinner" aria-label="Loading"></div>
<div class="overlay-text" id="overlay-text">Initializing Python…</div>
</div>
<div id="model-hint" class="model-hint hidden">Load a local model using the selector at the top to start chatting, or switch to a cloud provider.</div>
</div>
<script>{{ js_code }}</script>
</body>
</html>
:root{--bg:#0b1520;--panel:#101c2a;--muted:#96a4b5;--text:#e8eef5;--accent:#4cc38a;--accent-strong:#3db175;--warning:#ffb74d;--danger:#ff6b6b;--ring:0 0 0 3px rgba(76,195,138,0.35);--radius:14px;--shadow:0 10px 30px rgba(0,0,0,0.35);--font:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:linear-gradient(180deg,#0b1520, #0a1926);color:var(--text);font-family:var(--font)}
.app{max-width:1100px;margin:0 auto;padding:18px 18px 28px}
.app-header{background:rgba(16,28,42,0.8);backdrop-filter:saturate(140%) blur(10px);border:1px solid rgba(255,255,255,0.06);border-radius:var(--radius);padding:16px 18px;display:flex;flex-wrap:wrap;gap:14px;align-items:center;box-shadow:var(--shadow)}
.title-wrap{flex:1 1 360px}
.title-wrap h1{margin:0;font-size:22px;letter-spacing:0.2px}
.subtitle{margin:6px 0 0 0;color:var(--muted);font-size:14px}
.controls{display:flex;gap:10px;align-items:center}
.controls label{font-size:12px;color:var(--muted)}
select,button,textarea{font-family:var(--font);font-size:14px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:#0f2234;color:var(--text)}
select{padding:8px 10px}
button{cursor:pointer;padding:10px 14px}
button.primary{background:var(--accent);color:#0a1a27;border:none}
button.primary:hover{background:var(--accent-strong)}
button.secondary{background:#0f2234}
button:disabled{opacity:0.6;cursor:not-allowed;filter:grayscale(20%)}
button:focus-visible,select:focus-visible,textarea:focus-visible{outline:none;box-shadow:var(--ring)}
.chat-layout{display:grid;grid-template-rows:1fr auto;margin-top:16px;gap:10px}
.chat-panel{background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:var(--radius);padding:16px;min-height:380px;max-height:62vh;overflow:auto;box-shadow:var(--shadow)}
.messages{display:flex;flex-direction:column;gap:14px}
.msg{display:flex;gap:12px;align-items:flex-start}
.msg .bubble{padding:12px 14px;border-radius:12px;max-width:82%;line-height:1.45;white-space:pre-wrap;word-wrap:break-word}
.msg.user .bubble{background:#143652;border:1px solid rgba(255,255,255,0.05)}
.msg.assistant .bubble{background:#0e2a1f;border:1px solid rgba(76,195,138,0.25)}
.role{font-size:12px;color:var(--muted);min-width:70px;text-align:right;padding-top:4px}
.empty{opacity:0.9;text-align:left}
.empty h2{margin:6px 0 8px 0;font-size:18px}
.empty ul{margin:8px 0 12px 18px;color:var(--muted)}
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{background:#0f2234;border:1px solid rgba(255,255,255,0.08);border-radius:999px;padding:8px 12px;font-size:13px;color:#dfe8f2;cursor:pointer}
.chip:hover{border-color:rgba(76,195,138,0.5);color:#fff}
.composer{position:relative}
.input-wrap{display:flex;gap:10px;align-items:flex-end;background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:var(--radius);padding:10px 10px;box-shadow:var(--shadow)}
textarea#user-input{flex:1;resize:vertical;min-height:38px;max-height:160px;padding:10px 12px;background:#0f2234;border:1px solid rgba(255,255,255,0.08)}
.helper-text{color:var(--muted);font-size:12px;margin:8px 4px 0}
.overlay{position:fixed;inset:0;background:rgba(5,10,16,0.6);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;z-index:50}
.spinner{width:36px;height:36px;border-radius:50%;border:3px solid rgba(255,255,255,0.15);border-top-color:var(--accent);animation:spin 1s linear infinite}
.overlay-text{color:#cdd8e5;font-size:14px}
@keyframes spin{to{transform:rotate(360deg)}}
.model-hint{margin-top:10px;background:#2a1a0a;color:#ffd9a8;border:1px solid rgba(255,183,77,0.5);border-radius:10px;padding:10px 12px}
.hidden{display:none !important}
@media (max-width:1200px){.msg .bubble{max-width:88%}}
@media (max-width:768px){.app{padding:12px}.title-wrap h1{font-size:20px}.controls{width:100%;justify-content:flex-start;flex-wrap:wrap}.controls label{display:none}.chat-panel{max-height:58vh}}
(function(){
const qs = (s, el=document)=>el.querySelector(s);
const qsa = (s, el=document)=>Array.from(el.querySelectorAll(s));
const results = qs('#results-container');
const inputEl = qs('#user-input');
const sendBtn = qs('#send-btn');
const clearBtn = qs('#clear-btn');
const overlay = qs('#overlay');
const overlayText = qs('#overlay-text');
const modelHint = qs('#model-hint');
const jurisdictionEl = qs('#jurisdiction');
const exampleChips = qs('#example-chips');
let lastAssistantBubble = null;
function escapeHtml(str){
if(str === null || str === undefined) return '';
return String(str)
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"')
.replace(/'/g,''');
}
function setProcessing(on){
if(on){
overlay.classList.remove('hidden');
overlayText.textContent = 'Thinking…';
sendBtn.disabled = true;
inputEl.disabled = true;
} else {
overlay.classList.add('hidden');
sendBtn.disabled = false;
inputEl.disabled = false;
inputEl.focus();
}
}
function hideEmpty(){ const es = qs('#empty-state'); if(es) es.remove(); }
function msgTemplate(role, content){
const wrap = document.createElement('div');
wrap.className = `msg ${role}`;
const roleEl = document.createElement('div');
roleEl.className = 'role';
roleEl.textContent = role === 'user' ? 'You' : 'Assistant';
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.innerHTML = escapeHtml(content);
wrap.appendChild(roleEl); wrap.appendChild(bubble);
return {wrap, bubble};
}
window.addMessage = function(type, content){
hideEmpty();
// Streaming support for local models
if(type === 'assistant'){
if(lastAssistantBubble && lastAssistantBubble.isConnected){
lastAssistantBubble.innerHTML += escapeHtml(content);
results.scrollTop = results.scrollHeight;
return lastAssistantBubble.parentElement;
} else {
const m = msgTemplate('assistant', content);
lastAssistantBubble = m.bubble;
results.appendChild(m.wrap);
results.scrollTop = results.scrollHeight;
return m.wrap;
}
}
const m = msgTemplate(type === 'user' ? 'user' : 'assistant', content);
if(type !== 'assistant') lastAssistantBubble = null;
results.appendChild(m.wrap);
results.scrollTop = results.scrollHeight;
return m.wrap;
}
function addExamples(){
const ex = [
'Can I deduct my home office if I also use a coworking space sometimes?',
'How do I figure my quarterly estimated payments this year?',
'Do I owe self-employment tax on side-gig income if I have a W-2 job?',
'What records should I keep to support software and equipment deductions?'
];
ex.forEach(t=>{
const c = document.createElement('button');
c.className = 'chip';
c.type = 'button';
c.textContent = t;
c.addEventListener('click',()=>{ inputEl.value = t; inputEl.focus(); });
exampleChips.appendChild(c);
});
}
function updateModelStatus(){
try{
const isLocal = (typeof window.PROVIDER !== 'undefined') && String(window.PROVIDER).toLowerCase().includes('local');
const loaded = window.agentManager && window.agentManager.isLoaded;
if(isLocal && !loaded){
modelHint.classList.remove('hidden');
sendBtn.disabled = true; inputEl.disabled = false; // allow typing but disable send until loaded
} else {
modelHint.classList.add('hidden');
sendBtn.disabled = false; inputEl.disabled = false;
}
}catch(e){ /* ignore */ }
}
async function ensureReady(){
return new Promise((resolve)=>{
if(window.pyodideReady){ resolve(); return; }
document.addEventListener('pyodide-ready', ()=> resolve(), {once:true});
});
}
async function send(){
const q = (inputEl.value || '').trim();
if(!q) return;
updateModelStatus();
const isLocal = (typeof window.PROVIDER !== 'undefined') && String(window.PROVIDER).toLowerCase().includes('local');
const loaded = window.agentManager && window.agentManager.isLoaded;
if(isLocal && !loaded){ modelHint.classList.remove('hidden'); return; }
hideEmpty();
window.addMessage('user', q);
setProcessing(true);
try{
window.pyodide.globals.set('user_query', q);
window.pyodide.globals.set('ui_jurisdiction', jurisdictionEl.value || 'US');
const py = `
result = await process_user_query(user_query, ui_jurisdiction)
result
`;
const answer = await window.pyodide.runPythonAsync(py);
window.addMessage('assistant', String(answer||''));
}catch(err){
console.error(err);
window.addMessage('assistant', 'Sorry, something went wrong while generating a response. Please try again.');
}finally{
setProcessing(false);
inputEl.value = '';
}
}
async function clearChat(){
try{
await window.pyodide.runPythonAsync('_reset_state()');
}catch(e){}
results.innerHTML = '';
const empty = document.createElement('div');
empty.id = 'empty-state'; empty.className = 'empty';
empty.innerHTML = `
<h2>Ask about freelancer taxes</h2>
<ul>
<li>What counts as an ordinary and necessary business expense?</li>
<li>How to estimate quarterly payments and avoid penalties</li>
<li>Home office, vehicle, and equipment deductions</li>
<li>Self-employment tax basics and quick math examples</li>
</ul>
<div class="chips" id="example-chips"></div>
`;
results.appendChild(empty);
lastAssistantBubble = null;
// Reattach example chips
const chipsHost = qs('#example-chips');
if(chipsHost){
exampleChips.innerHTML='';
chipsHost.replaceWith(exampleChips);
addExamples();
}
}
function bind(){
sendBtn.addEventListener('click', send);
clearBtn.addEventListener('click', clearChat);
inputEl.addEventListener('keydown', (e)=>{
if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send(); }
});
jurisdictionEl.addEventListener('change', ()=>updateModelStatus());
addExamples();
}
async function init(){
overlay.classList.remove('hidden');
overlayText.textContent = 'Initializing Python…';
await ensureReady();
overlayText.textContent = 'Ready';
overlay.classList.add('hidden');
bind();
updateModelStatus();
// Show welcome message
const welcome = "Hi! I’m here to help with freelancer taxes in plain language. Ask about deductions, quarterly estimated payments, or self-employment tax. I’ll explain the general rule and steps. This isn’t professional tax advice, but I’ll do my best to guide you.";
window.addMessage('assistant', welcome);
inputEl.focus();
}
if(document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', init);
} else { init(); }
})();
# Freelancer Tax Q&A Helper - Python side
# Public functions (no leading underscore) become tools; we expose only process_user_query.
# Do not import heavy packages; keep prompt compact for local models.
from typing import List, Tuple
import asyncio
from textwrap import shorten
import js
# Tunables (use triple-bracket templating with defaults)
MAX_CONTEXT_CHARS = int('[[[MAX_CHARS|6000]]]')
MAX_MEMORY_MESSAGES = 8 # keep it small for local models
# Conversation memory: list of (user, assistant)
HISTORY: List[Tuple[str, str]] = []
SYSTEM_PROMPT = (
"You are a plain-language tax-question helper for freelancers and independent contractors. "
"Answer questions about deductions, quarterly estimated taxes, and self-employment tax. "
"Instructions:\n"
"- Give clear, friendly explanations in simple sentences.\n"
"- Start with a direct answer in 1-2 sentences.\n"
"- Then include: 'Why:' with the general rule.\n"
"- If helpful, add 'How to figure it:' with steps or a small example or quick math.\n"
"- Ask 1-2 clarifying questions if key info is missing.\n"
"- If the user states a jurisdiction, adapt to it. If not, assume U.S. federal rules and note when rules vary by state/country.\n"
"- Do not invent precise statutory citations. You may reference high-level sources like 'IRS Publication 334/535' as examples.\n"
"- If the topic is not tax, politely steer back to freelancer tax topics.\n"
"- Always end with exactly this one-line reminder: This isn't professional tax advice.\n"
"Formatting:\n"
"- Use short paragraphs or bullet points.\n"
"- Show simple calculations clearly.\n"
)
async def _llm_call(prompt: str, system_prompt: str) -> str:
"""Call the injected provider-neutral LLM from Python via JS.
Args:
prompt: The user/context prompt string.
system_prompt: The system instruction string.
"""
# window.callLLM(prompt, systemPrompt) is injected by the platform
try:
resp = await js.callLLM(prompt, system_prompt)
except Exception as e:
return f"Sorry, I couldn't reach the language model service. Please try again. (Error: {e})"
# resp should be a string
try:
return str(resp)
except Exception:
return "Sorry, I couldn't parse the model response. Please try again."
def _reset_state():
"""Reset in-memory conversation state (helper; not exposed as a tool)."""
HISTORY.clear()
def _build_context(jurisdiction: str) -> str:
"""Build textual conversation context limited by character budget.
Args:
jurisdiction: The jurisdiction hint selected by the user.
"""
parts: List[str] = []
jur = jurisdiction.strip() or "US"
parts.append(f"Jurisdiction: {jur}")
parts.append("You are assisting a self-employed freelancer with taxes.")
# Add recent history
if HISTORY:
parts.append("Conversation so far (most recent last):")
for u, a in HISTORY[-MAX_MEMORY_MESSAGES:]:
parts.append(f"User: {u}")
parts.append(f"Assistant: {a}")
context = "\n".join(parts)
# Truncate if needed
if len(context) > MAX_CONTEXT_CHARS:
context = context[-MAX_CONTEXT_CHARS:]
return context
async def process_user_query(query: str, jurisdiction: str = "US") -> str:
"""Handle a user query end-to-end: build context, call the LLM, update memory.
Args:
query: The user's tax question.
jurisdiction: Jurisdiction hint (e.g., 'US', 'UK').
"""
q = (query or "").strip()
if not q:
return "Please enter a freelancer tax question to begin. This isn't professional tax advice."
context = _build_context(jurisdiction)
# Compose final prompt: context + current turn
prompt = (
f"{context}\n\n"
f"User: {q}\n"
f"Assistant:"
)
answer = await _llm_call(prompt, SYSTEM_PROMPT)
# Ensure the mandatory reminder is present (add if missing)
reminder = "This isn't professional tax advice."
if reminder not in answer:
if answer.endswith("."):
answer = f"{answer}\n\n{reminder}"
else:
answer = f"{answer}.\n\n{reminder}"
# Update memory
try:
HISTORY.append((q, answer))
# Trim memory if it grows too large
if len(HISTORY) > MAX_MEMORY_MESSAGES * 2:
del HISTORY[: len(HISTORY) - MAX_MEMORY_MESSAGES]
except Exception:
pass
return answer
Name your agent
Based on . You'll get your own private copy to try and customize.