Chat with a page
Open a page once in a cloud browser, then ask questions about it in the terminal. Each answer is read from the page that is open in the session, takes the chat so far into account, and quotes the words on the page it is based on. When the page does not say, the answer says so instead of guessing.
When to use it: You want to ask questions about one page and get answers that quote the page, or say it does not say.
| Uses | a session, goto, content, the extract action on the open page |
|---|---|
| Needs | a model on the API (no model key of your own) |
| Site | a books.toscrape.com product page (a demo shop), or any page you give it |
| Output | output/result.json (the questions, answers and quotes), output/page.md (the page as Markdown) |
| Languages | Node and Python |
The session stays open while you chat (it ends after 5 idle minutes, at most 15 minutes in all), so the page is loaded once and every question reads the same page, scripts and all.
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Chat with a page: open a page once in a session, then ask questions about it. Each answer comes from the page * that is open in the browser (session.extract), with the chat so far, and quotes the sentence it is based on. * * npx tsx node/index.ts (BOXLINE_API_KEY; PAGE_URL for another page) * * QUESTIONS="first?|second?" answers those and stops; without it, type questions and "exit" to stop. * Writes output/result.json and output/page.md (the page as the model read it). */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { createInterface } from "node:readline/promises";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const url = process.env.PAGE_URL ?? "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html";const preset = process.env.QUESTIONS?.split("|").map((q) => q.trim()).filter(Boolean);const bx = new Boxline();type Answer = { answer: string; quote: string; found: boolean };const SCHEMA = { type: "object", properties: { answer: { type: "string", description: "a short answer, from this page only" }, quote: { type: "string", description: "the exact words on the page the answer is based on, copied as they are; empty when the page does not say" }, found: { type: "boolean", description: "false when the page does not answer the question" }, }, required: ["answer", "quote", "found"],};const session = await bx.sessions.create({ timeout: 900, idleTimeout: 300, userMetadata: { example: "chat-with-a-page" } });console.log(`Session: ${session.id}`);const turns: { question: string; answer: string; quote: string; found: boolean }[] = [];let modelUsd = 0;try { const opened = await session.goto(url, { waitUntil: "load" }); const page = await session.content("markdown"); console.log(`Opened ${page.url} ("${page.title}"), ${page.content.length} characters. Ask about it (type "exit" to stop).\n`); mkdirSync(out, { recursive: true }); writeFileSync(join(out, "page.md"), page.content); const rl = preset ? null : createInterface({ input: process.stdin, output: process.stdout }); for (let i = 0; ; i++) { const question = preset ? preset[i] : (await rl!.question("You: ")).trim(); if (!question || question.toLowerCase() === "exit") break; if (preset) console.log(`You: ${question}`); // The chat so far goes into the instruction, so follow-up questions ("and the one after that?") make sense. const history = turns.map((t) => `Q: ${t.question}\nA: ${t.answer}`).join("\n"); const r = await session.extract<Answer>( `${history ? `The conversation so far:\n${history}\n\n` : ""}Answer this question about the page: ${question}`, { schema: SCHEMA }, ); modelUsd += r.usage.costUsd; if (!r.data.found && !r.data.answer.trim()) r.data.answer = "The page does not say."; turns.push({ question, ...r.data }); console.log(`AI: ${r.data.answer}${r.data.quote ? `\n (the page: "${r.data.quote}")` : ""}\n`); } rl?.close(); writeFileSync(join(out, "result.json"), JSON.stringify({ url, finalUrl: page.url, title: page.title, status: opened.status, turns, usage: { modelUsd } }, null, 2)); console.log(`${turns.length} answers for $${modelUsd.toFixed(4)} of model use.`);} finally { await session.stop();}"""Chat with a page: open a page once in a session, then ask questions about it. Each answer comes from the pagethat is open in the browser (session.extract), with the chat so far, and quotes the sentence it is based on. python python/main.py (BOXLINE_API_KEY; PAGE_URL for another page)QUESTIONS="first?|second?" answers those and stops; without it, type questions and "exit" to stop.Writes output/result.json and output/page.md (the page as the model read it)."""import jsonimport osfrom pathlib import Pathfrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))url = os.environ.get("PAGE_URL", "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html")preset = [q.strip() for q in os.environ["QUESTIONS"].split("|") if q.strip()] if os.environ.get("QUESTIONS") else Nonebx = Boxline()SCHEMA = { "type": "object", "properties": { "answer": {"type": "string", "description": "a short answer, from this page only"}, "quote": {"type": "string", "description": "the exact words on the page the answer is based on, copied as they are; empty when the page does not say"}, "found": {"type": "boolean", "description": "false when the page does not answer the question"}, }, "required": ["answer", "quote", "found"],}turns, model_usd = [], 0.0with bx.sessions.create(timeout=900, idle_timeout=300, user_metadata={"example": "chat-with-a-page"}) as session: print(f"Session: {session.id}", flush=True) opened = session.goto(url, wait_until="load") page = session.content("markdown") print(f'Opened {page["url"]} ("{page["title"]}"), {len(page["content"])} characters. Ask about it (type "exit" to stop).\n', flush=True) out.mkdir(parents=True, exist_ok=True) (out / "page.md").write_text(page["content"]) i = 0 while True: if preset is not None: question = preset[i] if i < len(preset) else "" if question: print(f"You: {question}") else: question = input("You: ").strip() i += 1 if not question or question.lower() == "exit": break # The chat so far goes into the instruction, so follow-up questions ("and the one after that?") make sense. history = "\n".join(f"Q: {t['question']}\nA: {t['answer']}" for t in turns) r = session.extract( (f"The conversation so far:\n{history}\n\n" if history else "") + f"Answer this question about the page: {question}", schema=SCHEMA, ) model_usd += r["usage"]["costUsd"] if not r["data"]["found"] and not r["data"]["answer"].strip(): r["data"]["answer"] = "The page does not say." turns.append({"question": question, **r["data"]}) quote = f'\n (the page: "{r["data"]["quote"]}")' if r["data"]["quote"] else "" print(f'AI: {r["data"]["answer"]}{quote}\n', flush=True) result = {"url": url, "finalUrl": page["url"], "title": page["title"], "status": opened["status"], "turns": turns, "usage": {"modelUsd": model_usd}} (out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False)) print(f"{len(turns)} answers for ${model_usd:.4f} of model use.")Inputs
Environment variables the example reads:
PAGE_URL: the page to chat about (defaulthttps://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html).QUESTIONS: questions separated by|to answer without typing (the runner uses this); without it, type your questions andexitto stop.
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/chat-with-a-page. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
Every answer the page supports quotes words that are really in page.md; for the default page and the runner's three questions: the price is £51.77, 22 are in stock, and the publisher's phone number is reported as not on the page.
Source
github.com/Boxline-dev/examples/chat-with-a-page on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.