@aparte/provider-scenario 0.27.4 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/showcase.d.ts +2 -1
- package/dist/showcase.d.ts.map +1 -1
- package/dist/showcase.js +60 -1
- package/dist/showcase.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -49,9 +49,40 @@ A tool step calls the tool you registered on the config; the real loop runs its
|
|
|
49
49
|
and calls the provider again with the result, which `after` answers. `pacing:
|
|
50
50
|
{ chunk, delay }` sets the typing speed; `pacing: 'instant'` gives a test the whole
|
|
51
51
|
reply at once. `showcase` is a ready-made set covering markdown, code, reasoning, a
|
|
52
|
-
tool round-trip, a typed question (`ask_user`), an artifact (a
|
|
53
|
-
`@aparte/plugin-artifacts` is set up, prose otherwise) and an error; it is imported
|
|
52
|
+
tool round-trip, a typed question (`ask_user`), an answer with its sources, an artifact (a
|
|
53
|
+
card once `@aparte/plugin-artifacts` is set up, prose otherwise) and an error; it is imported
|
|
54
54
|
from `@aparte/provider-scenario/showcase`, its own entry point, so your own scenarios
|
|
55
55
|
do not pay for it.
|
|
56
56
|
|
|
57
|
+
## Sources and citations
|
|
58
|
+
|
|
59
|
+
A `sources` step lists the sources a reply draws on, and its `citations` say where in the text
|
|
60
|
+
each one is cited. Put it after the text it cites: a citation's `start` and `end` are UTF-16
|
|
61
|
+
offsets into the text the turn has streamed so far (its `text` steps joined, `end` exclusive),
|
|
62
|
+
and a chip goes at the end of the passage. Source ids are unique within the turn, and a citation
|
|
63
|
+
names them.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const first = 'The sky is blue because air scatters short wavelengths.';
|
|
67
|
+
const second = ' Sunsets are red for the opposite reason.';
|
|
68
|
+
|
|
69
|
+
createScenarioProvider({
|
|
70
|
+
turns: [[
|
|
71
|
+
{ text: first + second },
|
|
72
|
+
{
|
|
73
|
+
sources: [
|
|
74
|
+
{ id: 'a', url: 'https://en.wikipedia.org/wiki/Rayleigh_scattering', title: 'Rayleigh scattering', snippet: 'Scattering by particles smaller than the wavelength.' },
|
|
75
|
+
{ id: 'b', url: 'https://www.nasa.gov/', title: 'NASA' },
|
|
76
|
+
],
|
|
77
|
+
// One passage citing two sources shows as "+1". `end` is exclusive.
|
|
78
|
+
citations: [{ start: 0, end: first.length, sources: ['a', 'b'] }],
|
|
79
|
+
},
|
|
80
|
+
]],
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Compute the offsets from the strings (`text.indexOf(passage)`) rather than counting by hand, so
|
|
85
|
+
an edit to a sentence cannot leave a chip on the wrong words. Ask the `showcase` for *"why is
|
|
86
|
+
the sky blue?"* or *"with sources"* to see a three-source answer.
|
|
87
|
+
|
|
57
88
|
Full guide: [apartejs.dev/providers/ai/scenario](https://apartejs.dev/providers/ai/scenario/).
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* An `AparteAIProvider` that REPLAYS turns you wrote instead of calling a model: text at
|
|
5
5
|
* a typing pace, thinking, tool calls (the real loop runs your handler and calls back
|
|
6
|
-
* with the result), errors, pauses. No key, no network, no randomness.
|
|
6
|
+
* with the result), sources and citations, errors, pauses. No key, no network, no randomness.
|
|
7
7
|
*
|
|
8
8
|
* Three things in this repository had written it by hand before this package existed:
|
|
9
9
|
* the e2e mock of the wire, the screenshot harness of the UI audit, and the docs frames
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* }));
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
import type { AparteAIModel, AparteAIProvider, AparteChatRequest, AparteStreamEvent, AparteUsage, AparteFinishReason } from '@aparte/core';
|
|
25
|
+
import type { AparteAIModel, AparteAIProvider, AparteChatRequest, AparteStreamEvent, AparteUsage, AparteFinishReason, AparteSource, AparteCitation } from '@aparte/core';
|
|
26
26
|
/** One thing the scripted model does, in order. */
|
|
27
27
|
export type ScenarioStep =
|
|
28
28
|
/** Stream this text, chunk by chunk at the configured pace. Rendered as markdown once a markdown plugin is installed (plain text otherwise — core ships no markdown renderer); a tagged block (`<artifact>`…) becomes its segment once a grammar for it is registered (`@aparte/plugin-artifacts` does). */
|
|
@@ -47,6 +47,16 @@ export type ScenarioStep =
|
|
|
47
47
|
| {
|
|
48
48
|
wait: number;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Cite sources at this point of the turn: the bubble lists them under the reply, and a citation puts a chip
|
|
52
|
+
* after the passage it supports. A citation's `start` / `end` are UTF-16 offsets into the text THIS turn has streamed
|
|
53
|
+
* (its `text` steps joined, `end` exclusive; thinking does not count), so the step goes after the text it cites.
|
|
54
|
+
* Source ids are unique within the turn and are what a citation names.
|
|
55
|
+
*/
|
|
56
|
+
| {
|
|
57
|
+
sources: AparteSource[];
|
|
58
|
+
citations?: AparteCitation[];
|
|
59
|
+
}
|
|
50
60
|
/** Override the usage reported at the end (merged over the estimate). */
|
|
51
61
|
| {
|
|
52
62
|
usage: Partial<AparteUsage>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAEhB,iBAAiB,EAEjB,iBAAiB,EACjB,WAAW,EACX,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAEhB,iBAAiB,EAEjB,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACjB,MAAM,cAAc,CAAC;AAItB,mDAAmD;AACnD,MAAM,MAAM,YAAY;AACpB,4SAA4S;AAC1S;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,qDAAqD;GACnD;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE;AACtB,wGAAwG;GACtG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE;AAChE,oFAAoF;GAClF;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE;AACnB,0DAA0D;GACxD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB;;;;;GAKG;GACD;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,cAAc,EAAE,CAAA;CAAE;AAC3D,yEAAyE;GACvE;IAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;CAAE;AACjC,oMAAoM;GAClM;IAAE,YAAY,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAE3C,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,EAAE,CAAC;AAEnD,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,YAAY,CAAC;CACtB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC3B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACpC,+FAA+F;IAC/F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAC;IACpD,mGAAmG;IACnG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IAChG,iFAAiF;IACjF,MAAM,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC,6FAA6F;IAC7F,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAiCD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,GAAG,SAAS,CAehH;AAkBD;;;;GAIG;AACH,wBAAgB,QAAQ,CACpB,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,iBAAiB,EAC1B,MAAM,GAAE,cAAc,GAAG,SAAc,EACvC,MAAM,CAAC,EAAE,WAAW,GACrB,cAAc,CAAC,iBAAiB,CAAC,CA4DnC;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAqG9F"}
|
package/dist/index.js
CHANGED
|
@@ -78,6 +78,8 @@ function playTurn(turn, request, pacing = {}, signal) {
|
|
|
78
78
|
controller.enqueue({ type: "error", message: step.error });
|
|
79
79
|
controller.close();
|
|
80
80
|
return;
|
|
81
|
+
} else if ("sources" in step) {
|
|
82
|
+
controller.enqueue({ type: "sources", sources: step.sources, ...step.citations ? { citations: step.citations } : {} });
|
|
81
83
|
} else if ("usage" in step) {
|
|
82
84
|
usage = { ...usage, ...step.usage };
|
|
83
85
|
} else if ("finishReason" in step) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-scenario — a scripted model.\n *\n * An `AparteAIProvider` that REPLAYS turns you wrote instead of calling a model: text at\n * a typing pace, thinking, tool calls (the real loop runs your handler and calls back\n * with the result), errors, pauses. No key, no network, no randomness.\n *\n * Three things in this repository had written it by hand before this package existed:\n * the e2e mock of the wire, the screenshot harness of the UI audit, and the docs frames\n * that seed content. It is also the piece nobody ships for CONSUMERS: a deterministic\n * model for their own tests, and a demo that streams without a backend.\n *\n * It owns its I/O — `chat()` — so `AparteDirectTransport` hands the request straight to\n * it; there is no wire format here to adapt. (This repository's own e2e keeps its\n * network mock on purpose: that suite tests the real wire path, which this provider\n * bypasses by construction.)\n *\n * ```ts\n * import { createScenarioProvider } from '@aparte/provider-scenario';\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({\n * turns: ['Hello! Ask me anything.', [{ thinking: 'Let me see…' }, { text: 'Here is my answer.' }]],\n * }));\n * ```\n */\n\nimport type {\n AparteAIModel,\n AparteAIProvider,\n AparteChatMessage,\n AparteChatRequest,\n AparteChatResponse,\n AparteStreamEvent,\n AparteUsage,\n AparteFinishReason,\n} from '@aparte/core';\n\n// ─── What a turn is made of ──────────────────────────────────────────────────\n\n/** One thing the scripted model does, in order. */\nexport type ScenarioStep =\n /** Stream this text, chunk by chunk at the configured pace. Rendered as markdown once a markdown plugin is installed (plain text otherwise — core ships no markdown renderer); a tagged block (`<artifact>`…) becomes its segment once a grammar for it is registered (`@aparte/plugin-artifacts` does). */\n | { text: string }\n /** Stream this as reasoning (the thinking block). */\n | { thinking: string }\n /** Call a tool. The loop runs the registered handler, then calls the provider again with the result. */\n | { tool: string; input?: Record<string, unknown>; id?: string }\n /** Fail the turn with this message — what a provider error looks like to the UI. */\n | { error: string }\n /** Pause, in milliseconds — a slow model, a long tool. */\n | { wait: number }\n /** Override the usage reported at the end (merged over the estimate). */\n | { usage: Partial<AparteUsage> }\n /** How the turn ends, on its `done` event — `'length'` shows the \"cut off\" note under the reply. A turn that says nothing reports nothing, as a provider that does not know why it stopped does. */\n | { finishReason: AparteFinishReason };\n\n/** What the model does for ONE call. A bare string is one text step. */\nexport type ScenarioTurn = string | ScenarioStep[];\n\n/** A turn with the conditions under which it answers. */\nexport interface Scenario {\n /**\n * Answers when the last user message matches: a string is a case-insensitive\n * substring, a RegExp is tested as is.\n */\n when?: string | RegExp;\n /** Answers the call that follows this tool's result — the second half of a tool round-trip. */\n after?: string;\n turn: ScenarioTurn;\n}\n\n/** How fast text streams. */\nexport interface ScenarioPacing {\n /** Characters per chunk. Default 12. */\n chunk?: number;\n /** Milliseconds between chunks. Default 24. */\n delay?: number;\n}\n\nexport interface ScenarioProviderOptions {\n /** Provider id, as seen by core (key resolution, model picker, events). Default `scenario`. */\n id?: string;\n /** Display name. Default `Scripted model`. */\n name?: string;\n /**\n * Calls answered in order; the last one repeats. The simplest form — a demo that\n * always goes the same way, a test that needs three replies. Every call to the\n * model advances, a tool round-trip included.\n *\n * The cursor belongs to the PROVIDER, not to a chat: a request carries no\n * conversation identity, so two chats registered against one provider take turns\n * from the same script and interleave it (chat A gets `turns[0]`, chat B\n * `turns[1]`). On a page with more than one chat, build a provider per chat —\n * `createScenarioProvider` is cheap and takes an `id` — or use `scenarios`, which\n * answers from the request itself and has no cursor at all.\n */\n turns?: ScenarioTurn[];\n /**\n * Named scenarios, one of which answers each call — picked by `match`, or by the\n * default: the `after` of a tool result, else the first `when` that matches the\n * last user message, else `default`, else the first one. A bare turn value is\n * `{ turn }`.\n */\n scenarios?: Record<string, Scenario | ScenarioTurn>;\n /** Picks the scenario for a call, by name. Return `undefined` to fall back to the default rule. */\n match?: (request: AparteChatRequest, scenarios: Record<string, Scenario>) => string | undefined;\n /** Typing pace, or `'instant'` for a test that wants the whole reply at once. */\n pacing?: ScenarioPacing | 'instant';\n /** The models the picker offers. Default: one, `scripted`, declaring streaming and tools. */\n models?: AparteAIModel[];\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst textOf = (message: AparteChatMessage | undefined): string => {\n if (!message) return '';\n const { content } = message;\n if (typeof content === 'string') return content;\n return content.map((part) => (part.type === 'text' ? part.text : '')).join(' ');\n};\n\nconst stepsOf = (turn: ScenarioTurn): ScenarioStep[] => (typeof turn === 'string' ? [{ text: turn }] : turn);\n\nconst isTurn = (value: Scenario | ScenarioTurn): value is ScenarioTurn => typeof value === 'string' || Array.isArray(value);\n\nconst normalize = (scenarios: Record<string, Scenario | ScenarioTurn>): Record<string, Scenario> => {\n const out: Record<string, Scenario> = {};\n for (const [key, value] of Object.entries(scenarios)) out[key] = isTurn(value) ? { turn: value } : value;\n return out;\n};\n\n/**\n * The name of the tool a `tool` message answers, when the message does not carry it:\n * the call with that id, in the assistant turn before.\n */\nconst toolNameOf = (messages: AparteChatMessage[], toolCallId: string | undefined): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const call = messages[i]!.toolCalls?.find((tc) => tc.id === toolCallId);\n if (call) return call.name;\n }\n return undefined;\n};\n\n/**\n * The default rule: a tool result goes to the scenario that declared `after` for that\n * tool; otherwise the first scenario whose `when` matches the last user message;\n * otherwise `default`; otherwise the first scenario declared.\n */\nexport function defaultMatch(request: AparteChatRequest, scenarios: Record<string, Scenario>): string | undefined {\n const entries = Object.entries(scenarios);\n const last = request.messages[request.messages.length - 1];\n if (last?.role === 'tool') {\n const tool = last.toolName ?? toolNameOf(request.messages, last.toolCallId);\n const hit = entries.find(([, s]) => s.after !== undefined && s.after === tool);\n if (hit) return hit[0];\n }\n const text = textOf(last);\n for (const [key, s] of entries) {\n if (s.when === undefined) continue;\n if (typeof s.when === 'string' ? text.toLowerCase().includes(s.when.toLowerCase()) : s.when.test(text)) return key;\n }\n if ('default' in scenarios) return 'default';\n return entries[0]?.[0];\n}\n\nconst sleep = (ms: number, signal?: AbortSignal): Promise<void> => new Promise((resolve) => {\n if (ms <= 0 || signal?.aborted) return resolve();\n const timer = setTimeout(done, ms);\n function done(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n signal?.addEventListener('abort', done, { once: true });\n});\n\n/** Four characters per token — the usual rough cut, enough for a gauge to move. */\nconst estimateTokens = (chars: number): number => Math.ceil(chars / 4);\n\nlet sequence = 0;\n\n/**\n * Play one turn as the event stream the loop reads.\n *\n * Exported for a test or a tool that wants the events without a provider around them.\n */\nexport function playTurn(\n turn: ScenarioTurn,\n request: AparteChatRequest,\n pacing: ScenarioPacing | 'instant' = {},\n signal?: AbortSignal,\n): ReadableStream<AparteStreamEvent> {\n const chunk = pacing === 'instant' ? Number.POSITIVE_INFINITY : Math.max(1, pacing.chunk ?? 12);\n const delay = pacing === 'instant' ? 0 : Math.max(0, pacing.delay ?? 24);\n const steps = stepsOf(turn);\n const inputChars = request.messages.reduce((n, m) => n + textOf(m).length, 0);\n\n return new ReadableStream<AparteStreamEvent>({\n async start(controller) {\n let outputChars = 0;\n let usage: Partial<AparteUsage> = {};\n let finishReason: AparteFinishReason | undefined;\n // Not `startedAt`: that name is a segment stamp core writes in one place, and\n // `check:segment-stamp` reads any write to it as a third stamping site.\n const turnBegan = Date.now();\n try {\n for (const step of steps) {\n if (signal?.aborted) break;\n if ('wait' in step) {\n await sleep(step.wait, signal);\n } else if ('text' in step || 'thinking' in step) {\n const type = 'text' in step ? 'text' : 'thinking';\n const body = 'text' in step ? step.text : step.thinking;\n for (let i = 0; i < body.length; i += chunk) {\n if (signal?.aborted) break;\n const delta = body.slice(i, i + chunk);\n controller.enqueue({ type, delta });\n outputChars += delta.length;\n if (delay > 0 && i + chunk < body.length) await sleep(delay, signal);\n }\n } else if ('tool' in step) {\n controller.enqueue({ type: 'tool_use', id: step.id ?? `scenario_${++sequence}`, name: step.tool, input: step.input ?? {} });\n } else if ('error' in step) {\n controller.enqueue({ type: 'error', message: step.error });\n controller.close();\n return;\n } else if ('usage' in step) {\n usage = { ...usage, ...step.usage };\n } else if ('finishReason' in step) {\n finishReason = step.finishReason;\n }\n }\n if (!signal?.aborted) {\n const merged = { inputTokens: estimateTokens(inputChars), outputTokens: estimateTokens(outputChars), durationMs: Date.now() - turnBegan, ...usage };\n // The total follows the parts it is the sum of, unless the script set it\n // too: a step overriding `inputTokens` alone left the estimate's total in\n // place, and the context gauge reads the total first.\n controller.enqueue({\n type: 'done',\n usage: { totalTokens: (merged.inputTokens ?? 0) + (merged.outputTokens ?? 0), ...merged },\n ...(finishReason ? { finishReason } : {}),\n });\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n}\n\n// ─── The provider ────────────────────────────────────────────────────────────\n\n/**\n * Build the scripted provider. Register it like any other:\n *\n * ```ts\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({\n * scenarios: {\n * default: 'Ask me for a haiku, a table, or the weather.',\n * haiku: { when: /haiku/i, turn: 'Web components hum — \\nno framework in the wind, \\njust the page, alive.' },\n * weather: { when: 'weather', turn: [{ text: 'Let me check.' }, { tool: 'get_weather', input: { city: 'Lille' } }] },\n * forecast: { after: 'get_weather', turn: 'Cloudy, 14 °C. Bring a jacket.' },\n * },\n * }));\n * ```\n */\nexport function createScenarioProvider(options: ScenarioProviderOptions = {}): AparteAIProvider {\n // A `when` scenario that calls a tool needs its `after` counterpart, or the\n // default match routes the tool RESULT back through the same `when` and the\n // conversation eats its own tail — identical rounds until the client's\n // maxTurns error. Plausible to write, silent to read; the hole is visible at\n // creation, so it is said at creation. Ordered `turns` advance on their own, so\n // they are the one exemption.\n //\n // A custom `match` is NOT exempt, and used to be. It does not replace the default\n // rule, it precedes it — `pick` reads `match(...) ?? defaultMatch(...)` — so a\n // `match` that returns `undefined` for a tool result, the shape the docs and this\n // repo's own examples write, falls straight into the loop this warning describes.\n // The exemption silenced the one case most likely to need the line; a consumer who\n // really owns the routing can ignore a single console line at creation — and the\n // message tells them so, since for THEM the sentence about looping may be false.\n if (options.scenarios) {\n // A record value is a Scenario OR a bare turn (string / steps) — a tool in a\n // BARE turn loops the same way, through the default/first-entry fallback.\n const values = Object.values(options.scenarios);\n const asScenario = (v: Scenario | ScenarioTurn): Scenario | null =>\n typeof v === 'object' && !Array.isArray(v) ? v : null;\n const routed = new Set(values.map((v) => asScenario(v)?.after).filter(Boolean));\n const orphans = new Set<string>();\n for (const v of values) {\n const turn = asScenario(v)?.turn ?? (v as ScenarioTurn);\n const steps = Array.isArray(turn) ? turn : [];\n for (const step of steps) {\n if (typeof step === 'object' && step !== null && 'tool' in step\n && typeof step.tool === 'string' && !routed.has(step.tool)) {\n orphans.add(step.tool);\n }\n }\n }\n if (orphans.size > 0) {\n console.warn(\n `[scenario] No \\`after\\` route for: ${[...orphans].map((t) => `\\`${t}\\``).join(', ')}. `\n + 'A scenario calls the tool, but nothing answers its result — the default match '\n + 'sends it back through the same `when`, and the conversation loops until the '\n + \"client's maxTurns stops it. Declare a scenario with `after: '<tool>'` for each.\"\n // Only said when there IS a `match`, because only then can the sentence above be\n // false: routing the result by value is the documented \"Branching on what the user\n // answered\" shape, and it does not loop. Without a `match` the loop is certain, and\n // an escape hatch named there would only muddy the one instruction that helps.\n + (options.match\n ? ' Your own `match` runs before that rule: if it already routes this result'\n + ' — routing by the value the tool returned is a documented shape — this line'\n + ' is the one to ignore.'\n : ''),\n );\n }\n }\n const id = options.id ?? 'scenario';\n const name = options.name ?? 'Scripted model';\n const models: AparteAIModel[] = options.models ?? [{\n id: 'scripted',\n name,\n description: 'Replays scripted turns — no key, no network.',\n capabilities: ['streaming', 'function_calling'],\n }];\n const scenarios = normalize(options.scenarios ?? {});\n const match = options.match ?? defaultMatch;\n let calls = 0;\n\n const pick = (request: AparteChatRequest): ScenarioTurn => {\n if (options.turns?.length) {\n const turn = options.turns[Math.min(calls, options.turns.length - 1)]!;\n calls++;\n return turn;\n }\n const key = match(request, scenarios) ?? defaultMatch(request, scenarios);\n const scenario = key !== undefined ? scenarios[key] : undefined;\n if (!scenario) {\n // Said, because the alternative was \"Typing…\" forever with an empty reply\n // and nothing in the console (issue #29). The usual cause: `match()`\n // returned the scenario OBJECT — the second argument hands you the objects,\n // so it is an easy slip — where the loop needs its KEY.\n const got = key === undefined ? 'nothing' : typeof key === 'string' ? `\"${key}\"` : `a ${typeof key}`;\n console.warn(\n `[provider-scenario] no scenario for this call: match() returned ${got}, and the keys are `\n + `${Object.keys(scenarios).map((k) => `\"${k}\"`).join(', ') || '(none)'}. Return a key, or `\n + '`undefined` to fall back to the default rule. Streaming an empty turn.',\n );\n }\n return scenario?.turn ?? '';\n };\n\n return {\n id,\n getMetadata: () => ({\n id,\n name,\n description: 'Replays scripted turns — no key, no network.',\n isLocal: true,\n configSchema: { fields: [] },\n }),\n getModels: () => models,\n fetchModels: async () => models,\n async chat(request: AparteChatRequest, _config?: unknown, ctx?: { providerId: string; signal?: AbortSignal }): Promise<AparteChatResponse> {\n return playTurn(pick(request), request, options.pacing, ctx?.signal);\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-scenario — a scripted model.\n *\n * An `AparteAIProvider` that REPLAYS turns you wrote instead of calling a model: text at\n * a typing pace, thinking, tool calls (the real loop runs your handler and calls back\n * with the result), sources and citations, errors, pauses. No key, no network, no randomness.\n *\n * Three things in this repository had written it by hand before this package existed:\n * the e2e mock of the wire, the screenshot harness of the UI audit, and the docs frames\n * that seed content. It is also the piece nobody ships for CONSUMERS: a deterministic\n * model for their own tests, and a demo that streams without a backend.\n *\n * It owns its I/O — `chat()` — so `AparteDirectTransport` hands the request straight to\n * it; there is no wire format here to adapt. (This repository's own e2e keeps its\n * network mock on purpose: that suite tests the real wire path, which this provider\n * bypasses by construction.)\n *\n * ```ts\n * import { createScenarioProvider } from '@aparte/provider-scenario';\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({\n * turns: ['Hello! Ask me anything.', [{ thinking: 'Let me see…' }, { text: 'Here is my answer.' }]],\n * }));\n * ```\n */\n\nimport type {\n AparteAIModel,\n AparteAIProvider,\n AparteChatMessage,\n AparteChatRequest,\n AparteChatResponse,\n AparteStreamEvent,\n AparteUsage,\n AparteFinishReason,\n AparteSource,\n AparteCitation,\n} from '@aparte/core';\n\n// ─── What a turn is made of ──────────────────────────────────────────────────\n\n/** One thing the scripted model does, in order. */\nexport type ScenarioStep =\n /** Stream this text, chunk by chunk at the configured pace. Rendered as markdown once a markdown plugin is installed (plain text otherwise — core ships no markdown renderer); a tagged block (`<artifact>`…) becomes its segment once a grammar for it is registered (`@aparte/plugin-artifacts` does). */\n | { text: string }\n /** Stream this as reasoning (the thinking block). */\n | { thinking: string }\n /** Call a tool. The loop runs the registered handler, then calls the provider again with the result. */\n | { tool: string; input?: Record<string, unknown>; id?: string }\n /** Fail the turn with this message — what a provider error looks like to the UI. */\n | { error: string }\n /** Pause, in milliseconds — a slow model, a long tool. */\n | { wait: number }\n /**\n * Cite sources at this point of the turn: the bubble lists them under the reply, and a citation puts a chip\n * after the passage it supports. A citation's `start` / `end` are UTF-16 offsets into the text THIS turn has streamed\n * (its `text` steps joined, `end` exclusive; thinking does not count), so the step goes after the text it cites.\n * Source ids are unique within the turn and are what a citation names.\n */\n | { sources: AparteSource[]; citations?: AparteCitation[] }\n /** Override the usage reported at the end (merged over the estimate). */\n | { usage: Partial<AparteUsage> }\n /** How the turn ends, on its `done` event — `'length'` shows the \"cut off\" note under the reply. A turn that says nothing reports nothing, as a provider that does not know why it stopped does. */\n | { finishReason: AparteFinishReason };\n\n/** What the model does for ONE call. A bare string is one text step. */\nexport type ScenarioTurn = string | ScenarioStep[];\n\n/** A turn with the conditions under which it answers. */\nexport interface Scenario {\n /**\n * Answers when the last user message matches: a string is a case-insensitive\n * substring, a RegExp is tested as is.\n */\n when?: string | RegExp;\n /** Answers the call that follows this tool's result — the second half of a tool round-trip. */\n after?: string;\n turn: ScenarioTurn;\n}\n\n/** How fast text streams. */\nexport interface ScenarioPacing {\n /** Characters per chunk. Default 12. */\n chunk?: number;\n /** Milliseconds between chunks. Default 24. */\n delay?: number;\n}\n\nexport interface ScenarioProviderOptions {\n /** Provider id, as seen by core (key resolution, model picker, events). Default `scenario`. */\n id?: string;\n /** Display name. Default `Scripted model`. */\n name?: string;\n /**\n * Calls answered in order; the last one repeats. The simplest form — a demo that\n * always goes the same way, a test that needs three replies. Every call to the\n * model advances, a tool round-trip included.\n *\n * The cursor belongs to the PROVIDER, not to a chat: a request carries no\n * conversation identity, so two chats registered against one provider take turns\n * from the same script and interleave it (chat A gets `turns[0]`, chat B\n * `turns[1]`). On a page with more than one chat, build a provider per chat —\n * `createScenarioProvider` is cheap and takes an `id` — or use `scenarios`, which\n * answers from the request itself and has no cursor at all.\n */\n turns?: ScenarioTurn[];\n /**\n * Named scenarios, one of which answers each call — picked by `match`, or by the\n * default: the `after` of a tool result, else the first `when` that matches the\n * last user message, else `default`, else the first one. A bare turn value is\n * `{ turn }`.\n */\n scenarios?: Record<string, Scenario | ScenarioTurn>;\n /** Picks the scenario for a call, by name. Return `undefined` to fall back to the default rule. */\n match?: (request: AparteChatRequest, scenarios: Record<string, Scenario>) => string | undefined;\n /** Typing pace, or `'instant'` for a test that wants the whole reply at once. */\n pacing?: ScenarioPacing | 'instant';\n /** The models the picker offers. Default: one, `scripted`, declaring streaming and tools. */\n models?: AparteAIModel[];\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst textOf = (message: AparteChatMessage | undefined): string => {\n if (!message) return '';\n const { content } = message;\n if (typeof content === 'string') return content;\n return content.map((part) => (part.type === 'text' ? part.text : '')).join(' ');\n};\n\nconst stepsOf = (turn: ScenarioTurn): ScenarioStep[] => (typeof turn === 'string' ? [{ text: turn }] : turn);\n\nconst isTurn = (value: Scenario | ScenarioTurn): value is ScenarioTurn => typeof value === 'string' || Array.isArray(value);\n\nconst normalize = (scenarios: Record<string, Scenario | ScenarioTurn>): Record<string, Scenario> => {\n const out: Record<string, Scenario> = {};\n for (const [key, value] of Object.entries(scenarios)) out[key] = isTurn(value) ? { turn: value } : value;\n return out;\n};\n\n/**\n * The name of the tool a `tool` message answers, when the message does not carry it:\n * the call with that id, in the assistant turn before.\n */\nconst toolNameOf = (messages: AparteChatMessage[], toolCallId: string | undefined): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const call = messages[i]!.toolCalls?.find((tc) => tc.id === toolCallId);\n if (call) return call.name;\n }\n return undefined;\n};\n\n/**\n * The default rule: a tool result goes to the scenario that declared `after` for that\n * tool; otherwise the first scenario whose `when` matches the last user message;\n * otherwise `default`; otherwise the first scenario declared.\n */\nexport function defaultMatch(request: AparteChatRequest, scenarios: Record<string, Scenario>): string | undefined {\n const entries = Object.entries(scenarios);\n const last = request.messages[request.messages.length - 1];\n if (last?.role === 'tool') {\n const tool = last.toolName ?? toolNameOf(request.messages, last.toolCallId);\n const hit = entries.find(([, s]) => s.after !== undefined && s.after === tool);\n if (hit) return hit[0];\n }\n const text = textOf(last);\n for (const [key, s] of entries) {\n if (s.when === undefined) continue;\n if (typeof s.when === 'string' ? text.toLowerCase().includes(s.when.toLowerCase()) : s.when.test(text)) return key;\n }\n if ('default' in scenarios) return 'default';\n return entries[0]?.[0];\n}\n\nconst sleep = (ms: number, signal?: AbortSignal): Promise<void> => new Promise((resolve) => {\n if (ms <= 0 || signal?.aborted) return resolve();\n const timer = setTimeout(done, ms);\n function done(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n signal?.addEventListener('abort', done, { once: true });\n});\n\n/** Four characters per token — the usual rough cut, enough for a gauge to move. */\nconst estimateTokens = (chars: number): number => Math.ceil(chars / 4);\n\nlet sequence = 0;\n\n/**\n * Play one turn as the event stream the loop reads.\n *\n * Exported for a test or a tool that wants the events without a provider around them.\n */\nexport function playTurn(\n turn: ScenarioTurn,\n request: AparteChatRequest,\n pacing: ScenarioPacing | 'instant' = {},\n signal?: AbortSignal,\n): ReadableStream<AparteStreamEvent> {\n const chunk = pacing === 'instant' ? Number.POSITIVE_INFINITY : Math.max(1, pacing.chunk ?? 12);\n const delay = pacing === 'instant' ? 0 : Math.max(0, pacing.delay ?? 24);\n const steps = stepsOf(turn);\n const inputChars = request.messages.reduce((n, m) => n + textOf(m).length, 0);\n\n return new ReadableStream<AparteStreamEvent>({\n async start(controller) {\n let outputChars = 0;\n let usage: Partial<AparteUsage> = {};\n let finishReason: AparteFinishReason | undefined;\n // Not `startedAt`: that name is a segment stamp core writes in one place, and\n // `check:segment-stamp` reads any write to it as a third stamping site.\n const turnBegan = Date.now();\n try {\n for (const step of steps) {\n if (signal?.aborted) break;\n if ('wait' in step) {\n await sleep(step.wait, signal);\n } else if ('text' in step || 'thinking' in step) {\n const type = 'text' in step ? 'text' : 'thinking';\n const body = 'text' in step ? step.text : step.thinking;\n for (let i = 0; i < body.length; i += chunk) {\n if (signal?.aborted) break;\n const delta = body.slice(i, i + chunk);\n controller.enqueue({ type, delta });\n outputChars += delta.length;\n if (delay > 0 && i + chunk < body.length) await sleep(delay, signal);\n }\n } else if ('tool' in step) {\n controller.enqueue({ type: 'tool_use', id: step.id ?? `scenario_${++sequence}`, name: step.tool, input: step.input ?? {} });\n } else if ('error' in step) {\n controller.enqueue({ type: 'error', message: step.error });\n controller.close();\n return;\n } else if ('sources' in step) {\n controller.enqueue({ type: 'sources', sources: step.sources, ...(step.citations ? { citations: step.citations } : {}) });\n } else if ('usage' in step) {\n usage = { ...usage, ...step.usage };\n } else if ('finishReason' in step) {\n finishReason = step.finishReason;\n }\n }\n if (!signal?.aborted) {\n const merged = { inputTokens: estimateTokens(inputChars), outputTokens: estimateTokens(outputChars), durationMs: Date.now() - turnBegan, ...usage };\n // The total follows the parts it is the sum of, unless the script set it\n // too: a step overriding `inputTokens` alone left the estimate's total in\n // place, and the context gauge reads the total first.\n controller.enqueue({\n type: 'done',\n usage: { totalTokens: (merged.inputTokens ?? 0) + (merged.outputTokens ?? 0), ...merged },\n ...(finishReason ? { finishReason } : {}),\n });\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n}\n\n// ─── The provider ────────────────────────────────────────────────────────────\n\n/**\n * Build the scripted provider. Register it like any other:\n *\n * ```ts\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({\n * scenarios: {\n * default: 'Ask me for a haiku, a table, or the weather.',\n * haiku: { when: /haiku/i, turn: 'Web components hum — \\nno framework in the wind, \\njust the page, alive.' },\n * weather: { when: 'weather', turn: [{ text: 'Let me check.' }, { tool: 'get_weather', input: { city: 'Lille' } }] },\n * forecast: { after: 'get_weather', turn: 'Cloudy, 14 °C. Bring a jacket.' },\n * },\n * }));\n * ```\n */\nexport function createScenarioProvider(options: ScenarioProviderOptions = {}): AparteAIProvider {\n // A `when` scenario that calls a tool needs its `after` counterpart, or the\n // default match routes the tool RESULT back through the same `when` and the\n // conversation eats its own tail — identical rounds until the client's\n // maxTurns error. Plausible to write, silent to read; the hole is visible at\n // creation, so it is said at creation. Ordered `turns` advance on their own, so\n // they are the one exemption.\n //\n // A custom `match` is NOT exempt, and used to be. It does not replace the default\n // rule, it precedes it — `pick` reads `match(...) ?? defaultMatch(...)` — so a\n // `match` that returns `undefined` for a tool result, the shape the docs and this\n // repo's own examples write, falls straight into the loop this warning describes.\n // The exemption silenced the one case most likely to need the line; a consumer who\n // really owns the routing can ignore a single console line at creation — and the\n // message tells them so, since for THEM the sentence about looping may be false.\n if (options.scenarios) {\n // A record value is a Scenario OR a bare turn (string / steps) — a tool in a\n // BARE turn loops the same way, through the default/first-entry fallback.\n const values = Object.values(options.scenarios);\n const asScenario = (v: Scenario | ScenarioTurn): Scenario | null =>\n typeof v === 'object' && !Array.isArray(v) ? v : null;\n const routed = new Set(values.map((v) => asScenario(v)?.after).filter(Boolean));\n const orphans = new Set<string>();\n for (const v of values) {\n const turn = asScenario(v)?.turn ?? (v as ScenarioTurn);\n const steps = Array.isArray(turn) ? turn : [];\n for (const step of steps) {\n if (typeof step === 'object' && step !== null && 'tool' in step\n && typeof step.tool === 'string' && !routed.has(step.tool)) {\n orphans.add(step.tool);\n }\n }\n }\n if (orphans.size > 0) {\n console.warn(\n `[scenario] No \\`after\\` route for: ${[...orphans].map((t) => `\\`${t}\\``).join(', ')}. `\n + 'A scenario calls the tool, but nothing answers its result — the default match '\n + 'sends it back through the same `when`, and the conversation loops until the '\n + \"client's maxTurns stops it. Declare a scenario with `after: '<tool>'` for each.\"\n // Only said when there IS a `match`, because only then can the sentence above be\n // false: routing the result by value is the documented \"Branching on what the user\n // answered\" shape, and it does not loop. Without a `match` the loop is certain, and\n // an escape hatch named there would only muddy the one instruction that helps.\n + (options.match\n ? ' Your own `match` runs before that rule: if it already routes this result'\n + ' — routing by the value the tool returned is a documented shape — this line'\n + ' is the one to ignore.'\n : ''),\n );\n }\n }\n const id = options.id ?? 'scenario';\n const name = options.name ?? 'Scripted model';\n const models: AparteAIModel[] = options.models ?? [{\n id: 'scripted',\n name,\n description: 'Replays scripted turns — no key, no network.',\n capabilities: ['streaming', 'function_calling'],\n }];\n const scenarios = normalize(options.scenarios ?? {});\n const match = options.match ?? defaultMatch;\n let calls = 0;\n\n const pick = (request: AparteChatRequest): ScenarioTurn => {\n if (options.turns?.length) {\n const turn = options.turns[Math.min(calls, options.turns.length - 1)]!;\n calls++;\n return turn;\n }\n const key = match(request, scenarios) ?? defaultMatch(request, scenarios);\n const scenario = key !== undefined ? scenarios[key] : undefined;\n if (!scenario) {\n // Said, because the alternative was \"Typing…\" forever with an empty reply\n // and nothing in the console (issue #29). The usual cause: `match()`\n // returned the scenario OBJECT — the second argument hands you the objects,\n // so it is an easy slip — where the loop needs its KEY.\n const got = key === undefined ? 'nothing' : typeof key === 'string' ? `\"${key}\"` : `a ${typeof key}`;\n console.warn(\n `[provider-scenario] no scenario for this call: match() returned ${got}, and the keys are `\n + `${Object.keys(scenarios).map((k) => `\"${k}\"`).join(', ') || '(none)'}. Return a key, or `\n + '`undefined` to fall back to the default rule. Streaming an empty turn.',\n );\n }\n return scenario?.turn ?? '';\n };\n\n return {\n id,\n getMetadata: () => ({\n id,\n name,\n description: 'Replays scripted turns — no key, no network.',\n isLocal: true,\n configSchema: { fields: [] },\n }),\n getModels: () => models,\n fetchModels: async () => models,\n async chat(request: AparteChatRequest, _config?: unknown, ctx?: { providerId: string; signal?: AbortSignal }): Promise<AparteChatResponse> {\n return playTurn(pick(request), request, options.pacing, ctx?.signal);\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/showcase.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Scenario } from './index.js';
|
|
2
2
|
/**
|
|
3
3
|
* A ready-made set of scenarios that shows the whole surface of a chat: markdown, a
|
|
4
|
-
* code block, reasoning, a tool round-trip, a typed question, an
|
|
4
|
+
* code block, reasoning, a tool round-trip, a typed question, an answer with its sources,
|
|
5
|
+
* an artifact, an error.
|
|
5
6
|
* It ships on its own entry point, so a consumer who writes their own scenarios
|
|
6
7
|
* never pays for this one. The docs' live frames and a demo page register it as is:
|
|
7
8
|
*
|
package/dist/showcase.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"showcase.d.ts","sourceRoot":"","sources":["../src/showcase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"showcase.d.ts","sourceRoot":"","sources":["../src/showcase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAiB3C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CA0L7C,CAAC"}
|
package/dist/showcase.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
const SKY = {
|
|
2
|
+
scattering: "The sky is blue because of **Rayleigh scattering**: air molecules scatter the short wavelengths of sunlight far more than the long ones, so blue light reaches your eye from every part of the sky.",
|
|
3
|
+
violet: "Violet is scattered even more, but the sun gives off less of it, the upper atmosphere absorbs some, and our eyes respond weakly to it, so the sky looks blue rather than violet.",
|
|
4
|
+
sunset: "At sunrise and sunset the light crosses much more air, most of the blue is scattered out of the beam before it reaches you, and the reds and oranges are what is left."
|
|
5
|
+
};
|
|
6
|
+
const SKY_TEXT = Object.values(SKY).join("\n\n");
|
|
7
|
+
const skyRange = (passage) => {
|
|
8
|
+
const start = SKY_TEXT.indexOf(passage);
|
|
9
|
+
return { start, end: start + passage.length };
|
|
10
|
+
};
|
|
1
11
|
const showcase = {
|
|
2
12
|
default: {
|
|
3
|
-
turn: "Hi — I am a **scripted** model: no key, no network, the same answer every time.\n\nAsk me for a *haiku*, a *table*, some *code*, the *weather*, or to *ask you a question*."
|
|
13
|
+
turn: "Hi — I am a **scripted** model: no key, no network, the same answer every time.\n\nAsk me for a *haiku*, a *table*, some *code*, some *maths*, a *diagram*, an answer with its *sources*, the *weather*, or to *ask you a question*."
|
|
4
14
|
},
|
|
5
15
|
haiku: {
|
|
6
16
|
when: /haiku/i,
|
|
@@ -10,6 +20,55 @@ const showcase = {
|
|
|
10
20
|
when: /table/i,
|
|
11
21
|
turn: "Two ways to reach a model:\n\n| Transport | Where the key lives | Use it for |\n|---|---|---|\n| `AparteDirectTransport` | in the browser | prototypes, local models |\n| `AparteBackendTransport` | on your server | production |\n\nThe component does not change between the two."
|
|
12
22
|
},
|
|
23
|
+
// Before `code`: "a maths example" is maths. Drawn by @aparte/plugin-katex when it is
|
|
24
|
+
// installed and the Markdown provider recognises maths; shown as TeX otherwise.
|
|
25
|
+
math: {
|
|
26
|
+
when: /math|equation|formula|latex|integral/i,
|
|
27
|
+
turn: "Euler's identity ties five constants together: $e^{i\\pi} + 1 = 0$.\n\nThe area under $x^2$ from 0 to 1:\n\n$$\n\\int_0^1 x^2\\,dx = \\frac{1}{3}\n$$\n\nAnd a price stays a price: it costs $5 and $10."
|
|
28
|
+
},
|
|
29
|
+
// A fence of its own, so @aparte/plugin-mermaid draws it with any Markdown provider,
|
|
30
|
+
// or none; shown as code otherwise.
|
|
31
|
+
diagram: {
|
|
32
|
+
when: /diagram|flowchart|flow chart|mermaid/i,
|
|
33
|
+
turn: "How a reply reaches the screen:\n\n```mermaid\nflowchart LR\n accTitle: How a reply reaches the screen\n A[Model] -->|tokens| B[Markdown provider]\n B --> C{A code language?}\n C -->|math| D[KaTeX]\n C -->|mermaid| E[Mermaid]\n C -->|anything else| F[Highlighter]\n```\n\nEach drawing goes beside its source, which is what copy and the model still read."
|
|
34
|
+
},
|
|
35
|
+
// Before `code` and `artifact`: "cite an example" and "sources for this document" are
|
|
36
|
+
// about sources. The text streams first and the sources follow it, as a provider sends them.
|
|
37
|
+
citations: {
|
|
38
|
+
// "sources", never "source": "show me the source code" belongs to `code`.
|
|
39
|
+
when: /\bsources\b|\bcit(?:e|es|ation|ations)\b|why is the sky|sky blue|rayleigh/i,
|
|
40
|
+
turn: [
|
|
41
|
+
{ text: SKY_TEXT },
|
|
42
|
+
{
|
|
43
|
+
sources: [
|
|
44
|
+
{
|
|
45
|
+
id: "s1",
|
|
46
|
+
url: "https://en.wikipedia.org/wiki/Rayleigh_scattering",
|
|
47
|
+
title: "Rayleigh scattering",
|
|
48
|
+
snippet: "Light scattered by particles much smaller than its wavelength: the shorter the wavelength, the stronger the scattering."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "s2",
|
|
52
|
+
url: "https://spaceplace.nasa.gov/blue-sky/en/",
|
|
53
|
+
title: "Why is the sky blue?",
|
|
54
|
+
snippet: "Gases in the air send sunlight in every direction, and the short blue waves are sent about the most."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "s3",
|
|
58
|
+
url: "https://www.britannica.com/story/why-is-the-sky-blue",
|
|
59
|
+
title: "Why is the sky blue?",
|
|
60
|
+
snippet: "Violet scatters even more, but the sun gives off less of it and the eye is less sensitive to it."
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
citations: [
|
|
64
|
+
// Two sources on one passage: the chip reads "+1".
|
|
65
|
+
{ ...skyRange(SKY.scattering), sources: ["s1", "s2"] },
|
|
66
|
+
{ ...skyRange(SKY.violet), sources: ["s3"] },
|
|
67
|
+
{ ...skyRange(SKY.sunset), sources: ["s2"] }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
13
72
|
code: {
|
|
14
73
|
when: /code|example|snippet/i,
|
|
15
74
|
turn: [
|
package/dist/showcase.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"showcase.js","sources":["../src/showcase.ts"],"sourcesContent":["import type { Scenario } from './index.js';\n\n/**\n * A ready-made set of scenarios that shows the whole surface of a chat: markdown, a\n * code block, reasoning, a tool round-trip, a typed question, an artifact, an error.\n * It ships on its own entry point, so a consumer who writes their own scenarios\n * never pays for this one. The docs' live frames and a demo page register it as is:\n *\n * ```ts\n * import { createScenarioProvider } from '@aparte/provider-scenario';\n * import { showcase } from '@aparte/provider-scenario/showcase';\n *\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));\n * ```\n *\n * The tool names it calls — `get_weather`, `ask_user`, and the release chain's\n * `search_docs` / `read_file` / `write_file` / `run_command` — only do something if the\n * app registered a tool of that name; `@aparte/plugin-ask-user` provides the second.\n */\nexport const showcase: Record<string, Scenario> = {\n default: {\n turn: 'Hi — I am a **scripted** model: no key, no network, the same answer every time.\\n\\nAsk me for a *haiku*, a *table*, some *code*, the *weather*, or to *ask you a question*.',\n },\n haiku: {\n when: /haiku/i,\n turn: 'Web components hum — \\nno framework in the wind, \\njust the page, alive.',\n },\n table: {\n when: /table/i,\n turn: 'Two ways to reach a model:\\n\\n| Transport | Where the key lives | Use it for |\\n|---|---|---|\\n| `AparteDirectTransport` | in the browser | prototypes, local models |\\n| `AparteBackendTransport` | on your server | production |\\n\\nThe component does not change between the two.',\n },\n code: {\n when: /code|example|snippet/i,\n turn: [\n { thinking: 'The shortest useful wiring is three lines: register a provider, set a transport, start the client.' },\n { text: 'The minimal wiring:\\n\\n```ts\\nimport { AparteClient, aparteGlobalConfig, AparteDirectTransport } from \\'@aparte/core\\';\\nimport { createScenarioProvider } from \\'@aparte/provider-scenario\\';\\n\\naparteGlobalConfig.registerAIProvider(createScenarioProvider({ turns: [\\'Hello!\\'] }));\\naparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));\\nnew AparteClient().start();\\n```\\n\\nThree lines that matter.' },\n ],\n },\n weather: {\n when: /weather|forecast/i,\n turn: [\n { text: 'Let me check that for you.' },\n { tool: 'get_weather', input: { city: 'Lille' } },\n ],\n },\n forecast: {\n after: 'get_weather',\n turn: 'Cloudy, 14 °C, a little wind from the west — bring a jacket.',\n },\n // Four tools, in a chain: search, read, write, run. The multi-tool agent turn\n // nothing in this repository could produce before — every other scenario calls one\n // tool and answers its result. It is also what a decision about grouping consecutive\n // tool calls needs before it can be taken: a real turn whose calls can be read on a\n // page, rather than an imagined wall of them.\n //\n // Each tool needs its own `after:` route, or the tool RESULT falls back through the\n // `when` that started the chain and the conversation eats its own tail —\n // `createScenarioProvider` warns about exactly that at creation.\n release: {\n when: /ship it|release|checklist/i,\n turn: [\n { text: 'Running the release checklist.' },\n { tool: 'search_docs', input: { query: 'release checklist' } },\n ],\n },\n releaseRead: {\n after: 'search_docs',\n turn: [{ tool: 'read_file', input: { path: 'CHANGELOG.md' } }],\n },\n releaseWrite: {\n after: 'read_file',\n turn: [{ tool: 'write_file', input: { path: 'CHANGELOG.md', contents: '## 0.17.0\\n\\n- the release\\n' } }],\n },\n releaseRun: {\n after: 'write_file',\n turn: [{ tool: 'run_command', input: { cmd: 'pnpm build' } }],\n },\n releaseDone: {\n after: 'run_command',\n turn: 'Checklist done: docs searched, changelog read and updated, build green.',\n },\n // Several questions in one call: the panel becomes a stepper (1 2 …) with a\n // \"Skip\" per step. Nothing in the repository showed that mode until a consumer\n // reported clipped borders on it — a state no example renders is a state\n // nobody looks at. BEFORE `question`: scenarios are matched in order, and\n // \"two questions\" contains the word that one listens for.\n survey: {\n when: /survey|two questions|a few questions/i,\n turn: [\n { text: 'Two quick ones, then I can tailor it.' },\n {\n tool: 'ask_user',\n input: {\n questions: [\n {\n question: 'Which colour do you favour?',\n header: 'Colour',\n options: [\n { title: 'Blue', description: 'calm, trusted' },\n { title: 'Green', description: 'fresh, natural' },\n { title: 'Yellow', description: 'warm, loud' },\n ],\n },\n {\n question: 'How dense should the layout be?',\n header: 'Density',\n options: [\n { title: 'Airy', description: 'more room, fewer items per screen', recommended: true },\n { title: 'Compact', description: 'more on screen, tighter spacing' },\n ],\n },\n ],\n },\n },\n ],\n },\n question: {\n when: /question|ask me|colou?r scheme/i,\n turn: [\n { text: 'One thing first.' },\n {\n tool: 'ask_user',\n input: {\n questions: [{\n question: 'Which mood for your landing page?',\n header: 'Mood',\n options: [\n { title: 'Dark and quiet', description: 'ink, brass accents', recommended: true },\n { title: 'Light and warm', description: 'cream, soft contrast' },\n { title: 'Maximum contrast', description: 'strict black and white' },\n ],\n }],\n },\n },\n ],\n },\n answered: {\n after: 'ask_user',\n turn: 'Noted — I will go with that.',\n },\n artifact: {\n when: /artifact|document|write me/i,\n turn: 'Here is a first draft:\\n\\n<artifact type=\"text/markdown\" title=\"Launch note\">## aparté\\n\\nA chat in Web Components, with the agent loop inside.\\n\\n- any framework\\n- zero third-party dependencies\\n</artifact>\\n\\nTell me what to change.',\n },\n slow: {\n when: /slow|think hard|take your time/i,\n turn: [\n { thinking: 'This deserves a moment.' },\n { wait: 2500 },\n { text: 'Done thinking. The answer is: it depends on the transport.' },\n ],\n },\n failure: {\n when: /fail|error|crash/i,\n turn: [{ error: 'The scripted model exploded, as requested.' }],\n },\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"showcase.js","sources":["../src/showcase.ts"],"sourcesContent":["import type { Scenario } from './index.js';\n\n// A short answer with its sources, for the chips and the card the bubble draws: three\n// sources, and one passage that cites two of them (the \"+1\"). The offsets are found in the\n// passages rather than counted by hand, so editing a sentence cannot leave a chip on the\n// wrong words. `end` is exclusive and falls after the full stop, where a chip belongs.\nconst SKY = {\n scattering: 'The sky is blue because of **Rayleigh scattering**: air molecules scatter the short wavelengths of sunlight far more than the long ones, so blue light reaches your eye from every part of the sky.',\n violet: 'Violet is scattered even more, but the sun gives off less of it, the upper atmosphere absorbs some, and our eyes respond weakly to it, so the sky looks blue rather than violet.',\n sunset: 'At sunrise and sunset the light crosses much more air, most of the blue is scattered out of the beam before it reaches you, and the reds and oranges are what is left.',\n};\nconst SKY_TEXT = Object.values(SKY).join('\\n\\n');\nconst skyRange = (passage: string): { start: number; end: number } => {\n const start = SKY_TEXT.indexOf(passage);\n return { start, end: start + passage.length };\n};\n\n/**\n * A ready-made set of scenarios that shows the whole surface of a chat: markdown, a\n * code block, reasoning, a tool round-trip, a typed question, an answer with its sources,\n * an artifact, an error.\n * It ships on its own entry point, so a consumer who writes their own scenarios\n * never pays for this one. The docs' live frames and a demo page register it as is:\n *\n * ```ts\n * import { createScenarioProvider } from '@aparte/provider-scenario';\n * import { showcase } from '@aparte/provider-scenario/showcase';\n *\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));\n * ```\n *\n * The tool names it calls — `get_weather`, `ask_user`, and the release chain's\n * `search_docs` / `read_file` / `write_file` / `run_command` — only do something if the\n * app registered a tool of that name; `@aparte/plugin-ask-user` provides the second.\n */\nexport const showcase: Record<string, Scenario> = {\n default: {\n turn: 'Hi — I am a **scripted** model: no key, no network, the same answer every time.\\n\\nAsk me for a *haiku*, a *table*, some *code*, some *maths*, a *diagram*, an answer with its *sources*, the *weather*, or to *ask you a question*.',\n },\n haiku: {\n when: /haiku/i,\n turn: 'Web components hum — \\nno framework in the wind, \\njust the page, alive.',\n },\n table: {\n when: /table/i,\n turn: 'Two ways to reach a model:\\n\\n| Transport | Where the key lives | Use it for |\\n|---|---|---|\\n| `AparteDirectTransport` | in the browser | prototypes, local models |\\n| `AparteBackendTransport` | on your server | production |\\n\\nThe component does not change between the two.',\n },\n // Before `code`: \"a maths example\" is maths. Drawn by @aparte/plugin-katex when it is\n // installed and the Markdown provider recognises maths; shown as TeX otherwise.\n math: {\n when: /math|equation|formula|latex|integral/i,\n turn: 'Euler\\'s identity ties five constants together: $e^{i\\\\pi} + 1 = 0$.\\n\\nThe area under $x^2$ from 0 to 1:\\n\\n$$\\n\\\\int_0^1 x^2\\\\,dx = \\\\frac{1}{3}\\n$$\\n\\nAnd a price stays a price: it costs $5 and $10.',\n },\n // A fence of its own, so @aparte/plugin-mermaid draws it with any Markdown provider,\n // or none; shown as code otherwise.\n diagram: {\n when: /diagram|flowchart|flow chart|mermaid/i,\n turn: 'How a reply reaches the screen:\\n\\n```mermaid\\nflowchart LR\\n accTitle: How a reply reaches the screen\\n A[Model] -->|tokens| B[Markdown provider]\\n B --> C{A code language?}\\n C -->|math| D[KaTeX]\\n C -->|mermaid| E[Mermaid]\\n C -->|anything else| F[Highlighter]\\n```\\n\\nEach drawing goes beside its source, which is what copy and the model still read.',\n },\n // Before `code` and `artifact`: \"cite an example\" and \"sources for this document\" are\n // about sources. The text streams first and the sources follow it, as a provider sends them.\n citations: {\n // \"sources\", never \"source\": \"show me the source code\" belongs to `code`.\n when: /\\bsources\\b|\\bcit(?:e|es|ation|ations)\\b|why is the sky|sky blue|rayleigh/i,\n turn: [\n { text: SKY_TEXT },\n {\n sources: [\n {\n id: 's1',\n url: 'https://en.wikipedia.org/wiki/Rayleigh_scattering',\n title: 'Rayleigh scattering',\n snippet: 'Light scattered by particles much smaller than its wavelength: the shorter the wavelength, the stronger the scattering.',\n },\n {\n id: 's2',\n url: 'https://spaceplace.nasa.gov/blue-sky/en/',\n title: 'Why is the sky blue?',\n snippet: 'Gases in the air send sunlight in every direction, and the short blue waves are sent about the most.',\n },\n {\n id: 's3',\n url: 'https://www.britannica.com/story/why-is-the-sky-blue',\n title: 'Why is the sky blue?',\n snippet: 'Violet scatters even more, but the sun gives off less of it and the eye is less sensitive to it.',\n },\n ],\n citations: [\n // Two sources on one passage: the chip reads \"+1\".\n { ...skyRange(SKY.scattering), sources: ['s1', 's2'] },\n { ...skyRange(SKY.violet), sources: ['s3'] },\n { ...skyRange(SKY.sunset), sources: ['s2'] },\n ],\n },\n ],\n },\n code: {\n when: /code|example|snippet/i,\n turn: [\n { thinking: 'The shortest useful wiring is three lines: register a provider, set a transport, start the client.' },\n { text: 'The minimal wiring:\\n\\n```ts\\nimport { AparteClient, aparteGlobalConfig, AparteDirectTransport } from \\'@aparte/core\\';\\nimport { createScenarioProvider } from \\'@aparte/provider-scenario\\';\\n\\naparteGlobalConfig.registerAIProvider(createScenarioProvider({ turns: [\\'Hello!\\'] }));\\naparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));\\nnew AparteClient().start();\\n```\\n\\nThree lines that matter.' },\n ],\n },\n weather: {\n when: /weather|forecast/i,\n turn: [\n { text: 'Let me check that for you.' },\n { tool: 'get_weather', input: { city: 'Lille' } },\n ],\n },\n forecast: {\n after: 'get_weather',\n turn: 'Cloudy, 14 °C, a little wind from the west — bring a jacket.',\n },\n // Four tools, in a chain: search, read, write, run. The multi-tool agent turn\n // nothing in this repository could produce before — every other scenario calls one\n // tool and answers its result. It is also what a decision about grouping consecutive\n // tool calls needs before it can be taken: a real turn whose calls can be read on a\n // page, rather than an imagined wall of them.\n //\n // Each tool needs its own `after:` route, or the tool RESULT falls back through the\n // `when` that started the chain and the conversation eats its own tail —\n // `createScenarioProvider` warns about exactly that at creation.\n release: {\n when: /ship it|release|checklist/i,\n turn: [\n { text: 'Running the release checklist.' },\n { tool: 'search_docs', input: { query: 'release checklist' } },\n ],\n },\n releaseRead: {\n after: 'search_docs',\n turn: [{ tool: 'read_file', input: { path: 'CHANGELOG.md' } }],\n },\n releaseWrite: {\n after: 'read_file',\n turn: [{ tool: 'write_file', input: { path: 'CHANGELOG.md', contents: '## 0.17.0\\n\\n- the release\\n' } }],\n },\n releaseRun: {\n after: 'write_file',\n turn: [{ tool: 'run_command', input: { cmd: 'pnpm build' } }],\n },\n releaseDone: {\n after: 'run_command',\n turn: 'Checklist done: docs searched, changelog read and updated, build green.',\n },\n // Several questions in one call: the panel becomes a stepper (1 2 …) with a\n // \"Skip\" per step. Nothing in the repository showed that mode until a consumer\n // reported clipped borders on it — a state no example renders is a state\n // nobody looks at. BEFORE `question`: scenarios are matched in order, and\n // \"two questions\" contains the word that one listens for.\n survey: {\n when: /survey|two questions|a few questions/i,\n turn: [\n { text: 'Two quick ones, then I can tailor it.' },\n {\n tool: 'ask_user',\n input: {\n questions: [\n {\n question: 'Which colour do you favour?',\n header: 'Colour',\n options: [\n { title: 'Blue', description: 'calm, trusted' },\n { title: 'Green', description: 'fresh, natural' },\n { title: 'Yellow', description: 'warm, loud' },\n ],\n },\n {\n question: 'How dense should the layout be?',\n header: 'Density',\n options: [\n { title: 'Airy', description: 'more room, fewer items per screen', recommended: true },\n { title: 'Compact', description: 'more on screen, tighter spacing' },\n ],\n },\n ],\n },\n },\n ],\n },\n question: {\n when: /question|ask me|colou?r scheme/i,\n turn: [\n { text: 'One thing first.' },\n {\n tool: 'ask_user',\n input: {\n questions: [{\n question: 'Which mood for your landing page?',\n header: 'Mood',\n options: [\n { title: 'Dark and quiet', description: 'ink, brass accents', recommended: true },\n { title: 'Light and warm', description: 'cream, soft contrast' },\n { title: 'Maximum contrast', description: 'strict black and white' },\n ],\n }],\n },\n },\n ],\n },\n answered: {\n after: 'ask_user',\n turn: 'Noted — I will go with that.',\n },\n artifact: {\n when: /artifact|document|write me/i,\n turn: 'Here is a first draft:\\n\\n<artifact type=\"text/markdown\" title=\"Launch note\">## aparté\\n\\nA chat in Web Components, with the agent loop inside.\\n\\n- any framework\\n- zero third-party dependencies\\n</artifact>\\n\\nTell me what to change.',\n },\n slow: {\n when: /slow|think hard|take your time/i,\n turn: [\n { thinking: 'This deserves a moment.' },\n { wait: 2500 },\n { text: 'Done thinking. The answer is: it depends on the transport.' },\n ],\n },\n failure: {\n when: /fail|error|crash/i,\n turn: [{ error: 'The scripted model exploded, as requested.' }],\n },\n};\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/provider-scenario",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"homepage": "https://apartejs.dev/providers/ai/scenario/",
|
|
5
5
|
"description": "A scripted model for aparté — replays turns you wrote (text at a typing pace, thinking, tool calls, errors) so demos, docs and your own tests stream real-looking conversations with no key and no network.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@aparte/core": ">=0.
|
|
34
|
+
"@aparte/core": ">=0.28.0 <1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.4.0",
|
|
38
38
|
"vite": "^6.0.0",
|
|
39
|
-
"@aparte/core": "0.
|
|
39
|
+
"@aparte/core": "0.28.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"aparte",
|