A FastAPI + vanilla JS chat app fronting an Anthropic Claude agent for order status, returns, and policy questions. Architecture: - agent.py: system prompt, runtime reminder injection, output validation, agentic tool-use loop with prompt caching on the system prompt block - tools.py: four tools (lookup_order, check_return_eligibility, initiate_return, lookup_policy) with per-session SessionGuardState enforcing protocol ordering on the tool side - mock_data.py: orders, return policy, and FAQ entries used as the single source of truth by both the prompt and the tools - server.py: FastAPI app exposing /api/chat, /health, and the static UI - static/: vanilla HTML/CSS/JS chat UI, no build step - tests/: 30 tests covering tool-side enforcement, the privacy boundary, output validation, and the agent loop with a mocked Anthropic client - deploy/: systemd unit and nginx site config for production
83 lines
2.4 KiB
JavaScript
83 lines
2.4 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const messagesEl = document.getElementById("messages");
|
|
const formEl = document.getElementById("composer");
|
|
const inputEl = document.getElementById("input");
|
|
const sendEl = document.getElementById("send");
|
|
|
|
const SESSION_KEY = "bookly_session_id";
|
|
let sessionId = sessionStorage.getItem(SESSION_KEY);
|
|
if (!sessionId) {
|
|
sessionId = crypto.randomUUID();
|
|
sessionStorage.setItem(SESSION_KEY, sessionId);
|
|
}
|
|
|
|
const GREETING =
|
|
"Hi! I'm the Bookly support assistant. I can help you check on an order, start a return, or answer questions about shipping, returns, or password reset. How can I help today?";
|
|
|
|
function appendMessage(role, text) {
|
|
const el = document.createElement("div");
|
|
el.className = "message message--" + role;
|
|
el.textContent = text;
|
|
messagesEl.appendChild(el);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
return el;
|
|
}
|
|
|
|
function appendTypingIndicator() {
|
|
const el = document.createElement("div");
|
|
el.className = "message message--assistant message--typing";
|
|
el.setAttribute("aria-label", "Assistant is typing");
|
|
el.innerHTML = "<span></span><span></span><span></span>";
|
|
messagesEl.appendChild(el);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
return el;
|
|
}
|
|
|
|
async function sendMessage(text) {
|
|
const response = await fetch("/api/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ session_id: sessionId, message: text }),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error("Server returned " + response.status);
|
|
}
|
|
const data = await response.json();
|
|
return data.reply;
|
|
}
|
|
|
|
formEl.addEventListener("submit", async function (event) {
|
|
event.preventDefault();
|
|
const text = inputEl.value.trim();
|
|
if (!text) return;
|
|
|
|
appendMessage("user", text);
|
|
inputEl.value = "";
|
|
inputEl.disabled = true;
|
|
sendEl.disabled = true;
|
|
|
|
const typing = appendTypingIndicator();
|
|
try {
|
|
const reply = await sendMessage(text);
|
|
typing.remove();
|
|
appendMessage("assistant", reply);
|
|
} catch (err) {
|
|
typing.remove();
|
|
appendMessage(
|
|
"assistant",
|
|
"Sorry, I couldn't reach the server. Please try again in a moment."
|
|
);
|
|
console.error(err);
|
|
} finally {
|
|
inputEl.disabled = false;
|
|
sendEl.disabled = false;
|
|
inputEl.focus();
|
|
}
|
|
});
|
|
|
|
appendMessage("assistant", GREETING);
|
|
inputEl.focus();
|
|
})();
|