Choose how to run this agent
Download Agent
Choose how you want to use this agent:
Use Security Settings Key (Recommended)
Use the API key you've already saved in Security Settings. Quick and convenient!
- No need to re-enter API key
- Works offline after download
- Centralized key management
No API key found in Security Settings. Add one now
Enter API Key Manually
Enter your API key now for this specific agent download.
- Use different key for this agent
- One-time use (not saved)
- Works offline after download
Configure Agent Encryption
Description
Drop your messy pile of booking PDFs and trip notes and get a clean day-by-day itinerary — plus warnings about gaps like a 14:00 landing vs a 16:00 check-in — all in your browser.
Source Code
import json
import re
# How many document passages to retrieve per follow-up question.
RAG_TOP_K = [[[RAG_TOP_K|5]]]
# Cap on the pre-parsed schedule sheet handed to the model when building the
# itinerary (sized for the 8K-token default context of current local models).
MAX_FACTS_CHARS = [[[MAX_FACTS_CHARS|6000]]]
# One entry per ingested file: {"source": name, "facts": [schedule lines]}
_TRIP_FILES = []
# Lines that look schedule-relevant: times, dates, travel keywords…
_TIMEY_RE = re.compile(
r"[0-9]{1,2}:[0-9]{2}" # 14:05
r"|\b[0-9]{1,2}\s?(?:am|pm)\b" # 2 pm
r"|[0-9]{4}-[0-9]{2}-[0-9]{2}" # 2026-08-14
r"|\b[0-9]{1,2}[/.][0-9]{1,2}[/.][0-9]{2,4}\b" # 14/08/2026
r"|\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+[0-9]{1,2}\b"
r"|\b[0-9]{1,2}(?:st|nd|rd|th)?\s+(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b"
r"|\b(?:mon|tues?|wed(?:nes)?|thu(?:rs)?|fri|sat(?:ur)?|sun)day\b"
r"|check[- ]?in|check[- ]?out|departure|departs|arrival|arrives|boarding"
r"|confirmation|booking|reservation|pick[- ]?up",
re.I,
)
# …plus flight numbers (case-sensitive so plain words never match).
_FLIGHT_RE = re.compile(r"\b[A-Z]{2}[0-9]{2,4}\b")
def _extract_pdf_text(pdf_bytes):
"""(JS-callable) Extract plain text from a PDF's raw bytes via pypdf."""
import io
from pypdf import PdfReader
reader = PdfReader(io.BytesIO(pdf_bytes))
return "\n\n".join((page.extract_text() or "") for page in reader.pages)
def _scan_facts(text):
"""Pull the schedule-relevant lines out of one file's text."""
facts = []
for raw in text.splitlines():
line = " ".join(raw.split())
if len(line) < 4:
continue
if _TIMEY_RE.search(line) or _FLIGHT_RE.search(line):
facts.append(line[:200])
return facts
async def _ingest_trip_file(source, text):
"""(JS-callable) Index one booking/note file and scan its schedule lines.
Returns JSON {"chunks": int, "facts": int} for the file list UI.
"""
chunks = await agentop_rag.add_document(source, text)
facts = _scan_facts(text)
_TRIP_FILES.append({"source": source, "facts": facts})
return json.dumps({"chunks": chunks, "facts": len(facts)})
def _facts_sheet():
lines = []
for f in _TRIP_FILES:
lines.append(f"FILE: {f['source']}")
lines.extend(f" - {fact}" for fact in (f["facts"] or ["(no dated lines found)"]))
return "\n".join(lines)[:MAX_FACTS_CHARS]
async def _build_itinerary():
"""(JS-callable) Draft the day-by-day itinerary + gap check via wllama."""
if not _TRIP_FILES:
return "Drop your booking PDFs and notes first, then build the itinerary."
prompt = (
"Below are schedule lines extracted from one traveller's booking files "
"(flights, hotels, notes). Reconstruct the trip as a clean itinerary.\n"
"Format exactly:\n"
"## Itinerary\n"
"One '### <day, date>' section per day, in order; under each, one "
"chronological bullet per event: time — what and where, with flight "
"numbers or booking references when given.\n\n"
"## Gaps & watch-outs\n"
"Bullets for logistical problems you can see: arrival vs check-in "
"mismatches (e.g. landing at 14:00 but check-in from 16:00), "
"connections under 90 minutes, nights with no lodging, missing "
"transport between cities. Write 'None spotted.' if it all lines up.\n\n"
"Use ONLY the lines below. If a date or time is ambiguous, say so "
"rather than guessing.\n\n"
f"SCHEDULE LINES:\n{_facts_sheet()}"
)
# TEMPLATE_SYSTEM_PROMPT only exists as a JS global; the query bridge falls
# back to it when custom_prompt is empty (same idiom as the base template).
return await process_user_query_wllama(
prompt, globals().get("TEMPLATE_SYSTEM_PROMPT", "")
)
async def process_user_query(query):
"""Follow-up Q&A grounded in the uploaded trip documents (RAG).
Overrides the default router so retrieval is deterministic (small local
models are unreliable at deciding to call a search tool themselves).
"""
if not _TRIP_FILES:
return (
"I don't have any trip files yet — drop your flight PDFs, hotel "
"emails, and notes above and I'll piece the trip together."
)
hits = await agentop_rag.search(query, RAG_TOP_K)
excerpts = "\n\n".join(
f"[{i + 1}] (from {h['source']}) {h['text']}" for i, h in enumerate(hits)
) or "(nothing relevant found)"
grounded_prompt = (
"Answer the traveller's QUESTION using ONLY the TRIP DOCUMENT "
"EXCERPTS below. Use exact dates, times, flight numbers, and booking "
"references from the excerpts, and cite them like [1]. If the answer "
"is not in the excerpts, say you could not find it in the trip files.\n\n"
f"TRIP DOCUMENT EXCERPTS:\n{excerpts}\n\nQUESTION: {query}"
)
return await process_user_query_wllama(
grounded_prompt, globals().get("TEMPLATE_SYSTEM_PROMPT", "")
)
More by ozzo
WhatsApp Sales Copilot
Turn a raw WhatsApp chat export into a mini CRM — typed quotes, bookings, payments and boarding pas…
Private Quote & Material Estimator
Drop competing contractor quotes and compare them side by side — totals, inclusions, exclusions — t…
Semantic Search
Search your own notes or documents by meaning, not keywords — instant results with no LLM download.
Meeting Minutes
Turn a meeting recording into a summary, key decisions, and action items, then ask follow-up questi…
Voice Transcriber
Upload a voice note or audio file and get an instant transcript, then ask questions about it — all …
Document Q&A
Upload a PDF or text file and ask questions answered only from that document — fully in your browse…