fabro/apps/fabro-web/app/lib/chats-script.ts
Bryan Helmkamp 75e5f34c0d
Expose lithos request and response shapes through the API, server, CLI, and web
The OpenAPI spec adopts the lithos request, response, content part,
tool, usage, and cost schemas. The completions endpoint returns the
lithos `Response` JSON verbatim and SSE carries lithos `StreamEvent`s
verbatim. The models and providers endpoints serve the fabro-types
catalog views, and the install and model-test flows probe providers
through fabro-llm.

The CLI builds its catalog from the operator overlay, drives `fabro exec`
through the server gateway adapter, and parses reasoning effort with the
shared controls. The web app reads content parts as lithos-tagged
objects. The TypeScript client is regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:26:57 -06:00

155 lines
5.5 KiB
TypeScript

import type { ChatContentPart, ChatMessage } from "./chats-types";
function text(value: string): ChatContentPart {
return { type: "text", text: value };
}
function toolResult(toolCallId: string, value: unknown): ChatContentPart {
return {
type: "tool_result",
tool_call_id: toolCallId,
content: [text(JSON.stringify(value))],
};
}
/**
* Scripted assistant replies cycled through per chat. Generic content,
* intentionally not Fabro-specific. Each entry is a single assistant
* ChatMessage; tool calls and their results are siblings in the content
* array so the renderer can pair them.
*/
export const SCRIPTED_REPLIES: ChatMessage[] = [
{
role: "assistant",
content: [
text(
"Hi! I'm a scripted prototype reply. A few things I can show off:\n\n" +
"- Markdown rendering (lists, **bold**, *italics*, `code`)\n" +
"- Streaming text appearing incrementally\n" +
"- Tool calls with arguments and results\n" +
"- Multi-paragraph responses with code blocks\n\n" +
"Send another message to see the next response in the bank.",
),
],
},
{
role: "assistant",
content: [
text(
"Here's a TypeScript snippet that debounces a function:\n\n" +
"```ts\n" +
"export function debounce<T extends (...args: any[]) => void>(\n" +
" fn: T,\n" +
" ms: number,\n" +
"): (...args: Parameters<T>) => void {\n" +
" let handle: ReturnType<typeof setTimeout> | undefined;\n" +
" return (...args) => {\n" +
" if (handle) clearTimeout(handle);\n" +
" handle = setTimeout(() => fn(...args), ms);\n" +
" };\n" +
"}\n" +
"```\n\n" +
"The trailing-edge variant is the most common; a leading-edge variant fires immediately then suppresses subsequent calls.",
),
],
},
{
role: "assistant",
content: [
text("Let me search for that real quick."),
{
type: "tool_call",
id: "call_search_1",
name: "search_web",
input: {
type: "function",
arguments: {
query: "current best practices for rate limiting an HTTP API",
max_results: 5,
},
},
},
toolResult("call_search_1", {
results: [
{
title: "Token bucket vs leaky bucket",
url: "https://example.com/rate-limit-algorithms",
snippet: "Token bucket allows bursts, leaky bucket smooths traffic.",
},
{
title: "Distributed rate limiting with Redis",
url: "https://example.com/redis-rate-limit",
snippet: "INCR + EXPIRE is the simplest fixed-window approach.",
},
],
}),
text(
"\n\nTwo solid starting points. For most APIs, a Redis-backed sliding window keyed by API key gives you per-tenant fairness without a lot of moving parts. For burst tolerance, a token-bucket per route is a nice layer on top.",
),
],
},
{
role: "assistant",
content: [
text(
"## The 4-fold path of refactoring a hook\n\n" +
"When a React hook starts feeling tangled, work the corners in order:\n\n" +
"### 1. Extract pure computation\n" +
"Anything that is a function of inputs (no side effects, no state) leaves the hook entirely.\n\n" +
"### 2. Collapse derived state into `useMemo`\n" +
"State that is computable from other state shouldn't be its own state.\n\n" +
"### 3. Split orthogonal concerns into sibling hooks\n" +
"If two effects don't share dependencies, they don't belong in the same hook.\n\n" +
"### 4. Promote to a reducer\n" +
"Once there are 3+ related `useState` calls coordinating updates, `useReducer` makes the state machine explicit.\n\n" +
"> The honest test: can you write a one-sentence description of what the hook is responsible for? If not, it's doing too much.",
),
],
},
{
role: "assistant",
content: [
text("I'll compute that for you."),
{
type: "tool_call",
id: "call_calc_1",
name: "run_calculation",
input: {
type: "function",
arguments: {
expression: "compound_interest(principal=10000, rate=0.05, years=10)",
},
},
},
toolResult("call_calc_1", {
value: 16288.95,
currency: "USD",
note: "Annual compounding; rounded to cents.",
}),
text(
"\n\n**$16,288.95** after 10 years. Bumping the rate to 7% would put you at roughly $19,672, and continuous compounding at 5% lands at $16,487 — so the extra two points of rate matters more than the compounding cadence.",
),
],
},
{
role: "assistant",
content: [
text(
"Good question. The short answer: it depends on whether you need transactions across multiple writes.\n\n" +
"If you do — Postgres. If everything you do is single-row, SQLite is faster, simpler to operate, and easier to back up. A surprising amount of production traffic can live happily on SQLite if you accept its one-writer-at-a-time constraint.\n\n" +
"Next step: tell me about your read/write ratio and I can be more specific.",
),
],
},
];
const FALLBACK_REPLY: ChatMessage = {
role: "assistant",
content: [text("(No reply available.)")],
};
export function pickReply(scriptIndex: number): ChatMessage {
return (
SCRIPTED_REPLIES[scriptIndex % SCRIPTED_REPLIES.length] ?? FALLBACK_REPLY
);
}