AI Context from Page Content
AI Context from Page Content
Many applications need an assistant that understands the page the user is looking at — its labels, options, and visible text — not just a generic chatbot.
The idea is simple:
- Wrap page content in a custom tag
- Read the rendered HTML from the tag body
- Turn that into plain text
- Pass it to
createAISession()as the system message (or as part of it)
For AI configuration and core functions, see AI.
Overview
flowchart LR
A[Page template] --> B[Custom tag body renders]
B --> C[Tag reads generatedContent]
C --> D[Clean HTML to text]
D --> E[createAISession with context]
E --> F[User asks via inquiryAISession]
At end execution, a custom tag with a body can read thistag.generatedContent — everything that was output between the opening and closing tag. That string is your page context.
Step 1: Wrap the Page
In your layout or template, wrap the main content:
<cf_pageai pageTitle="Product settings">
<cfoutput>
<h1>Product settings</h1>
<p>Configure shipping and tax options for this store.</p>
<form>
<label>Tax rate (%)
<input type="text" name="taxRate" value="8.5">
</label>
</form>
</cfoutput>
</cf_pageai>
Place the tag file where Lucee resolves custom tags (for example a mapped custom tag folder). Lucee calls it as <cf_pageai> when the file is pageai.cfm.
Step 2: Read the Body in the Tag
pageai.cfm:
<cfif thisTag.executionMode EQ "end" OR !thisTag.hasEndTag>
<cfscript>
param name="attributes.pageTitle" default="";
rawContent = thisTag.hasEndTag ? trim(thistag.generatedContent) : "";
pageContext = cleanPageContext(rawContent);
systemMessage = buildSystemMessage(attributes.pageTitle, pageContext);
aiSession = createAISession(
name: "mychatgpt",
systemMessage: systemMessage,
limit: 8,
temperature: 0.3
);
</cfscript>
<cfoutput>
<div class="page-ai" data-ready="true">
<p class="comment">Ask a question about this page.</p>
<!--- your question form / widget here --->
</div>
</cfoutput>
</cfif>
The tag runs after the body is rendered, so generatedContent already contains the final HTML.
Step 3: Clean the HTML
Strip scripts, styles, and tags so the model gets readable text:
function stripTagBlocks(required string raw, required string tagName) {
var t = arguments.raw;
var openNeedle = "<" & lCase(arguments.tagName);
var closeTag = "</" & lCase(arguments.tagName) & ">";
var openPos = findNoCase(openNeedle, t);
while (openPos) {
var closePos = findNoCase(closeTag, t, openPos);
if (!closePos) break;
t = left(t, openPos - 1) & " " & mid(t, closePos + len(closeTag));
openPos = findNoCase(openNeedle, t, openPos);
}
return t;
}
function cleanPageContext(required string raw) {
var t = trim(arguments.raw);
if (!len(t)) return "";
t = stripTagBlocks(t, "script");
t = stripTagBlocks(t, "style");
t = reReplace(t, "<[^>]+>", " ", "all");
t = reReplace(t, "[ \t]+", " ", "all");
t = reReplace(t, "(\r?\n\s*){2,}", chr(10), "all");
return trim(t);
}
Use string functions (find, findNoCase) on large pages instead of very complex regular expressions.
Step 4: Build the System Message
Prepend instructions, then append the cleaned page text:
function buildSystemMessage(required string pageTitle, required string pageContext) {
var msg = "You are a helpful assistant for the page described below. "
& "Answer clearly and keep answers short. "
& "Only discuss content that appears on this page. "
& "Do not invent fields or options.";
if (len(trim(arguments.pageTitle))) {
msg &= chr(10) & "Page title: #trim(arguments.pageTitle)#";
}
if (len(trim(arguments.pageContext))) {
msg &= chr(10) & chr(10) & "Page content:" & chr(10) & trim(arguments.pageContext);
}
return msg;
}
You can add optional metadata (page id, product name, user role) the same way — as extra lines before Page content:.
Step 5: Ask Questions
Send user input with inquiryAISession(). The session keeps conversation history, so follow-up questions stay in context:
answer = inquiryAISession(aiSession, "What does the tax rate field do?");
writeOutput(answer);
// follow-up — the model still knows the page content from the system message
answer = inquiryAISession(aiSession, "What is a typical value?");
writeOutput(answer);
For streaming output:
inquiryAISession(aiSession, form.question, function(chunk) {
writeOutput(chunk);
cfflush(throwOnError=false);
});
Optional: Split Output Around the Widget
If the AI panel should sit inside the page flow (not only at the end of the tag), split the captured HTML and output in three parts:
- Content before the widget → set
thisTag.generatedContent(tag merge output) - The widget itself
- Content after the widget
Use a marker in the page body, for example {AI-location}, or split after a known intro block:
marker = "{AI-location}";
markerPos = find(marker, rawContent);
if (markerPos) {
contentBefore = left(rawContent, markerPos - 1);
contentAfter = mid(rawContent, markerPos + len(marker));
} else {
contentBefore = "";
contentAfter = rawContent;
}
if (thisTag.hasEndTag) {
thisTag.generatedContent = contentBefore;
}
<div class="highlight"><pre><span></span>
</pre></div>
<br>cfml
<cfoutput>
<!--- widget --->
#contentAfter#
</cfoutput>
Optional: Persist the Session Across Requests
Creating a new session on every page load works for demos. For a chat that survives reloads, serialize the session and store it keyed by page (and user if needed):
serialized = serializeAISession(aiSession);
// save serialized in application scope, a cache, or a database
// on the next request:
aiSession = loadAISession("mychatgpt", serialized);
See AI Session Serialization for maxlength, condense, and cross-model loading.
A common layout is:
- Custom tag — capture content, build system message, render widget
- Separate request handler — load stored session, call
inquiryAISession(), save serialized session again
Keep serialized data out of the CFML session if it grows large — application scope or an external store is usually safer.
Configuration
Use any configured AI connection by name:
"ai": {
"mychatgpt": {
"class": "lucee.runtime.ai.openai.OpenAIEngine",
"custom": {
"secretKey": "${OPENAI_API_KEY}",
"model": "gpt-4o-mini",
"type": "openai",
"timeout": 30000
}
}
}
<div class="highlight"><pre><span></span>
</pre></div>
<br>cfml
if (AIHas("mychatgpt")) {
// render the widget
}
Security
- Do not send password or secret field values to the model
- If the ask handler is a separate endpoint, protect it like the rest of your app
- Review
ai.logto see what page content is sent to external providers
When to Use This
Good fit:
- Help on a specific screen (settings, wizards, dashboards)
- “What does this page do?” without maintaining duplicate documentation
Consider alternatives:
- Broad knowledge across many topics → AI Augmentation with Lucene
- One-off questions with no page context → direct
inquiryAISession()without a custom tag
Related Documentation
- AI — AI configuration and core functions
- AI Session Serialization — save and restore conversation state
- custom-tag-mappings — where to place custom tags and how mappings work