@swissspidy/belay-web 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +46 -2
- package/dist/classifier-types.d.ts +63 -0
- package/dist/classifier-types.d.ts.map +1 -0
- package/dist/classifier-types.js +9 -0
- package/dist/classifier-types.js.map +1 -0
- package/dist/classifier.d.ts +39 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +160 -0
- package/dist/classifier.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/judge.d.ts +23 -0
- package/dist/judge.d.ts.map +1 -0
- package/dist/judge.js +39 -0
- package/dist/judge.js.map +1 -0
- package/dist/prompt-types.d.ts +37 -0
- package/dist/prompt-types.d.ts.map +1 -0
- package/dist/prompt-types.js +6 -0
- package/dist/prompt-types.js.map +1 -0
- package/dist/prompt.d.ts +25 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +117 -0
- package/dist/prompt.js.map +1 -0
- package/package.json +46 -4
- package/src/classifier-types.ts +75 -0
- package/src/classifier.ts +229 -0
- package/src/index.ts +8 -0
- package/src/judge.ts +60 -0
- package/src/prompt-types.ts +41 -0
- package/src/prompt.ts +137 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Prompt API (`LanguageModel`), after the explainer at
|
|
3
|
+
* https://github.com/webmachinelearning/prompt-api (Sep 2026). Only the members Belay uses.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Availability } from '@swissspidy/belay-core';
|
|
7
|
+
|
|
8
|
+
export interface LanguageModelMessage {
|
|
9
|
+
role: 'system' | 'user' | 'assistant';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LanguageModelExpected {
|
|
14
|
+
type: 'text' | 'image' | 'audio';
|
|
15
|
+
languages?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LanguageModelCreateOptions {
|
|
19
|
+
initialPrompts?: LanguageModelMessage[];
|
|
20
|
+
expectedInputs?: LanguageModelExpected[];
|
|
21
|
+
expectedOutputs?: LanguageModelExpected[];
|
|
22
|
+
monitor?: (monitor: EventTarget) => void;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LanguageModelPromptOptions {
|
|
27
|
+
responseConstraint?: Record<string, unknown> | RegExp;
|
|
28
|
+
omitResponseConstraintInput?: boolean;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LanguageModelSession {
|
|
33
|
+
prompt(input: string, options?: LanguageModelPromptOptions): Promise<string>;
|
|
34
|
+
clone?(options?: { signal?: AbortSignal }): Promise<LanguageModelSession>;
|
|
35
|
+
destroy(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LanguageModelStatic {
|
|
39
|
+
availability(options?: Omit<LanguageModelCreateOptions, 'monitor' | 'signal' | 'initialPrompts'>): Promise<Availability>;
|
|
40
|
+
create(options?: LanguageModelCreateOptions): Promise<LanguageModelSession>;
|
|
41
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { LocalUnavailableError, buildInstruction, toJsonSchema } from '@swissspidy/belay-core';
|
|
2
|
+
import type { Availability, LocalOutput, LocalRunner, PrepareOptions, RunnerContext, TaskSchema } from '@swissspidy/belay-core';
|
|
3
|
+
import type { LanguageModelCreateOptions, LanguageModelExpected, LanguageModelSession, LanguageModelStatic } from './prompt-types.js';
|
|
4
|
+
|
|
5
|
+
export interface PromptApiOptions {
|
|
6
|
+
/** System prompt. Defaults to the task instruction from `buildInstruction(schema, context)`. */
|
|
7
|
+
systemPrompt?: string | ((ctx: RunnerContext<TaskSchema>) => string);
|
|
8
|
+
/** Defaults to English text in and out. */
|
|
9
|
+
expectedInputs?: LanguageModelExpected[];
|
|
10
|
+
expectedOutputs?: LanguageModelExpected[];
|
|
11
|
+
/** Keep the JSON Schema out of the model's context window (the system prompt already describes the format). */
|
|
12
|
+
omitResponseConstraintInput?: boolean;
|
|
13
|
+
/** The `LanguageModel` implementation. Defaults to `globalThis.LanguageModel`. */
|
|
14
|
+
languageModel?: LanguageModelStatic;
|
|
15
|
+
/** Max number of cached base sessions (one per distinct system prompt). Defaults to 4. */
|
|
16
|
+
maxSessions?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Ctx = RunnerContext<TaskSchema>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Local runner backed by the Prompt API with structured output (`responseConstraint`). It reports
|
|
23
|
+
* no confidence of its own: pair it with a task `judge` (e.g. `classifierJudge()`), and the output
|
|
24
|
+
* is validated against the schema before the judge sees it.
|
|
25
|
+
*
|
|
26
|
+
* Every run prompts a clone of a base session, so runs never see each other's history. Like
|
|
27
|
+
* `classifierApi()`, `run()` never triggers a model download; call `task.prepare()` from a click.
|
|
28
|
+
*/
|
|
29
|
+
export function promptApi(options: PromptApiOptions = {}): LocalRunner<any> {
|
|
30
|
+
const maxSessions = Math.max(1, options.maxSessions ?? 4);
|
|
31
|
+
const sessions = new Map<string, Promise<LanguageModelSession>>();
|
|
32
|
+
const expected = {
|
|
33
|
+
expectedInputs: options.expectedInputs ?? [{ type: 'text' as const, languages: ['en'] }],
|
|
34
|
+
expectedOutputs: options.expectedOutputs ?? [{ type: 'text' as const, languages: ['en'] }],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const api = (): LanguageModelStatic | undefined =>
|
|
38
|
+
options.languageModel ?? (globalThis as { LanguageModel?: LanguageModelStatic }).LanguageModel;
|
|
39
|
+
|
|
40
|
+
const systemPromptFor = (ctx: Ctx): string =>
|
|
41
|
+
typeof options.systemPrompt === 'function'
|
|
42
|
+
? options.systemPrompt(ctx)
|
|
43
|
+
: (options.systemPrompt ?? buildInstruction(ctx.schema, ctx.context));
|
|
44
|
+
|
|
45
|
+
function session(ctx: Ctx, prepare?: PrepareOptions): Promise<LanguageModelSession> {
|
|
46
|
+
const LanguageModel = api();
|
|
47
|
+
if (!LanguageModel) return Promise.reject(new LocalUnavailableError('Prompt API is not available'));
|
|
48
|
+
const key = systemPromptFor(ctx);
|
|
49
|
+
const cached = sessions.get(key);
|
|
50
|
+
if (cached) {
|
|
51
|
+
sessions.delete(key);
|
|
52
|
+
sessions.set(key, cached);
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
// Only prepare()'s signal: the session is cached and shared, so a per-run abort must not destroy it.
|
|
56
|
+
const signal = prepare?.signal;
|
|
57
|
+
const createOptions: LanguageModelCreateOptions = {
|
|
58
|
+
...expected,
|
|
59
|
+
initialPrompts: [{ role: 'system', content: key }],
|
|
60
|
+
...(signal ? { signal } : {}),
|
|
61
|
+
...(prepare?.onProgress
|
|
62
|
+
? {
|
|
63
|
+
monitor: (m: EventTarget) =>
|
|
64
|
+
m.addEventListener('downloadprogress', (e) => prepare.onProgress!((e as Event & { loaded: number }).loaded)),
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
67
|
+
};
|
|
68
|
+
const created = LanguageModel.create(createOptions);
|
|
69
|
+
sessions.set(key, created);
|
|
70
|
+
created.catch(() => {
|
|
71
|
+
if (sessions.get(key) === created) sessions.delete(key);
|
|
72
|
+
});
|
|
73
|
+
while (sessions.size > maxSessions) {
|
|
74
|
+
const [oldestKey, oldest] = sessions.entries().next().value as [string, Promise<LanguageModelSession>];
|
|
75
|
+
sessions.delete(oldestKey);
|
|
76
|
+
oldest.then((s) => s.destroy(), () => {});
|
|
77
|
+
}
|
|
78
|
+
return created;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function availability(ctx: Ctx): Promise<Availability> {
|
|
82
|
+
const LanguageModel = api();
|
|
83
|
+
if (!LanguageModel) return 'unavailable';
|
|
84
|
+
if (sessions.has(systemPromptFor(ctx))) return 'available';
|
|
85
|
+
try {
|
|
86
|
+
return await LanguageModel.availability(expected);
|
|
87
|
+
} catch {
|
|
88
|
+
return 'unavailable';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: 'prompt-api',
|
|
94
|
+
availability,
|
|
95
|
+
|
|
96
|
+
async run(input: string, ctx: Ctx): Promise<LocalOutput> {
|
|
97
|
+
if (!sessions.has(systemPromptFor(ctx))) {
|
|
98
|
+
const state = await availability(ctx);
|
|
99
|
+
if (state !== 'available') throw new LocalUnavailableError(`Prompt API model is ${state}`);
|
|
100
|
+
}
|
|
101
|
+
const base = await session(ctx);
|
|
102
|
+
// A fresh clone per run keeps runs independent. Without clone(), fall back to a new session.
|
|
103
|
+
const LanguageModel = api()!;
|
|
104
|
+
const turn = base.clone
|
|
105
|
+
? await base.clone(ctx.signal ? { signal: ctx.signal } : {})
|
|
106
|
+
: await LanguageModel.create({ ...expected, initialPrompts: [{ role: 'system', content: systemPromptFor(ctx) }], ...(ctx.signal ? { signal: ctx.signal } : {}) });
|
|
107
|
+
try {
|
|
108
|
+
const text = await turn.prompt(input, {
|
|
109
|
+
responseConstraint: toJsonSchema(ctx.schema),
|
|
110
|
+
...(options.omitResponseConstraintInput ? { omitResponseConstraintInput: true } : {}),
|
|
111
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
112
|
+
});
|
|
113
|
+
let value: unknown = text;
|
|
114
|
+
try {
|
|
115
|
+
value = JSON.parse(text);
|
|
116
|
+
} catch {
|
|
117
|
+
// Leave the raw text: the task's schema validation reports it as invalid output.
|
|
118
|
+
}
|
|
119
|
+
return { value, raw: text };
|
|
120
|
+
} finally {
|
|
121
|
+
turn.destroy();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async prepare(ctx: Ctx, prepareOptions?: PrepareOptions) {
|
|
126
|
+
const state = await availability(ctx);
|
|
127
|
+
if (state === 'unavailable') throw new LocalUnavailableError('Prompt API model is unavailable on this device');
|
|
128
|
+
await session(ctx, prepareOptions);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
destroy() {
|
|
132
|
+
for (const s of sessions.values()) s.then((c) => c.destroy(), () => {});
|
|
133
|
+
sessions.clear();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|