@alexkroman1/aai 0.3.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/LICENSE +21 -0
- package/dist/cli.js +3436 -0
- package/package.json +78 -0
- package/sdk/_internal_types.ts +89 -0
- package/sdk/_mock_ws.ts +172 -0
- package/sdk/_timeout.ts +24 -0
- package/sdk/builtin_tools.ts +309 -0
- package/sdk/capnweb.ts +341 -0
- package/sdk/define_agent.ts +70 -0
- package/sdk/direct_executor.ts +195 -0
- package/sdk/kv.ts +183 -0
- package/sdk/mod.ts +35 -0
- package/sdk/protocol.ts +313 -0
- package/sdk/runtime.ts +65 -0
- package/sdk/s2s.ts +271 -0
- package/sdk/server.ts +198 -0
- package/sdk/session.ts +438 -0
- package/sdk/system_prompt.ts +47 -0
- package/sdk/types.ts +406 -0
- package/sdk/vector.ts +133 -0
- package/sdk/winterc_server.ts +141 -0
- package/sdk/worker_entry.ts +99 -0
- package/sdk/worker_shim.ts +170 -0
- package/sdk/ws_handler.ts +190 -0
- package/templates/_shared/.env.example +5 -0
- package/templates/_shared/package.json +17 -0
- package/templates/code-interpreter/agent.ts +27 -0
- package/templates/code-interpreter/client.tsx +2 -0
- package/templates/dispatch-center/agent.ts +1536 -0
- package/templates/dispatch-center/client.tsx +504 -0
- package/templates/embedded-assets/agent.ts +49 -0
- package/templates/embedded-assets/client.tsx +2 -0
- package/templates/embedded-assets/knowledge.json +20 -0
- package/templates/health-assistant/agent.ts +160 -0
- package/templates/health-assistant/client.tsx +2 -0
- package/templates/infocom-adventure/agent.ts +164 -0
- package/templates/infocom-adventure/client.tsx +299 -0
- package/templates/math-buddy/agent.ts +21 -0
- package/templates/math-buddy/client.tsx +2 -0
- package/templates/memory-agent/agent.ts +74 -0
- package/templates/memory-agent/client.tsx +2 -0
- package/templates/night-owl/agent.ts +98 -0
- package/templates/night-owl/client.tsx +28 -0
- package/templates/personal-finance/agent.ts +26 -0
- package/templates/personal-finance/client.tsx +2 -0
- package/templates/simple/agent.ts +6 -0
- package/templates/simple/client.tsx +2 -0
- package/templates/smart-research/agent.ts +164 -0
- package/templates/smart-research/client.tsx +2 -0
- package/templates/support/README.md +62 -0
- package/templates/support/agent.ts +19 -0
- package/templates/support/client.tsx +2 -0
- package/templates/travel-concierge/agent.ts +29 -0
- package/templates/travel-concierge/client.tsx +2 -0
- package/templates/web-researcher/agent.ts +17 -0
- package/templates/web-researcher/client.tsx +2 -0
- package/ui/_components/app.tsx +37 -0
- package/ui/_components/chat_view.tsx +36 -0
- package/ui/_components/controls.tsx +32 -0
- package/ui/_components/error_banner.tsx +18 -0
- package/ui/_components/message_bubble.tsx +21 -0
- package/ui/_components/message_list.tsx +61 -0
- package/ui/_components/state_indicator.tsx +17 -0
- package/ui/_components/thinking_indicator.tsx +19 -0
- package/ui/_components/tool_call_block.tsx +110 -0
- package/ui/_components/tool_icons.tsx +101 -0
- package/ui/_components/transcript.tsx +20 -0
- package/ui/audio.ts +170 -0
- package/ui/components.ts +49 -0
- package/ui/components_mod.ts +37 -0
- package/ui/mod.ts +48 -0
- package/ui/mount.tsx +112 -0
- package/ui/mount_context.ts +19 -0
- package/ui/session.ts +456 -0
- package/ui/session_mod.ts +27 -0
- package/ui/signals.ts +111 -0
- package/ui/types.ts +50 -0
- package/ui/worklets/capture-processor.js +62 -0
- package/ui/worklets/playback-processor.js +110 -0
package/ui/signals.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
|
|
3
|
+
import { batch, effect, type Signal, signal } from "@preact/signals";
|
|
4
|
+
import type * as preact from "preact";
|
|
5
|
+
import type { ComponentChildren } from "preact";
|
|
6
|
+
import { createContext, h } from "preact";
|
|
7
|
+
import { useContext } from "preact/hooks";
|
|
8
|
+
import type { VoiceSession } from "./session.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reactive session controls wrapping a {@linkcode VoiceSession} with Preact signals.
|
|
12
|
+
*
|
|
13
|
+
* Components access reactive data via `session` (e.g. `session.state`,
|
|
14
|
+
* `session.messages`). UI-only state (`started`, `running`) and actions
|
|
15
|
+
* (`start`, `toggle`, `reset`) live directly on this object.
|
|
16
|
+
*/
|
|
17
|
+
export type SessionSignals = {
|
|
18
|
+
/** The underlying voice session — all reactive data lives here. */
|
|
19
|
+
session: VoiceSession;
|
|
20
|
+
/** Whether the session has been started by the user. */
|
|
21
|
+
started: Signal<boolean>;
|
|
22
|
+
/** Whether the session is currently running (connected or connecting). */
|
|
23
|
+
running: Signal<boolean>;
|
|
24
|
+
/** Dispose the reactive effect that tracks error state. */
|
|
25
|
+
dispose(): void;
|
|
26
|
+
/** Start the session for the first time (sets `started` and `running`). */
|
|
27
|
+
start(): void;
|
|
28
|
+
/** Toggle between connected and disconnected states. */
|
|
29
|
+
toggle(): void;
|
|
30
|
+
/** Reset the session: clear state and reconnect. */
|
|
31
|
+
reset(): void;
|
|
32
|
+
/** Alias for {@linkcode dispose} for use with `using`. */
|
|
33
|
+
[Symbol.dispose](): void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a {@linkcode VoiceSession} in Preact signals for reactive UI binding.
|
|
38
|
+
*
|
|
39
|
+
* Creates higher-level controls (start, toggle, reset) on top of the raw
|
|
40
|
+
* session, and automatically sets `running` to `false` when the session
|
|
41
|
+
* enters an error state.
|
|
42
|
+
*
|
|
43
|
+
* @param session - The voice session to wrap.
|
|
44
|
+
* @returns A {@linkcode SessionSignals} object for use in Preact components.
|
|
45
|
+
*/
|
|
46
|
+
export function createSessionControls(session: VoiceSession): SessionSignals {
|
|
47
|
+
const started = signal(false);
|
|
48
|
+
const running = signal(true);
|
|
49
|
+
|
|
50
|
+
const dispose = effect(() => {
|
|
51
|
+
if (session.state.value === "error") running.value = false;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
session,
|
|
56
|
+
started,
|
|
57
|
+
running,
|
|
58
|
+
dispose,
|
|
59
|
+
start() {
|
|
60
|
+
batch(() => {
|
|
61
|
+
started.value = true;
|
|
62
|
+
running.value = true;
|
|
63
|
+
});
|
|
64
|
+
session.connect();
|
|
65
|
+
},
|
|
66
|
+
toggle() {
|
|
67
|
+
if (running.value) session.disconnect();
|
|
68
|
+
else session.connect();
|
|
69
|
+
running.value = !running.value;
|
|
70
|
+
},
|
|
71
|
+
reset() {
|
|
72
|
+
session.reset();
|
|
73
|
+
},
|
|
74
|
+
[Symbol.dispose]() {
|
|
75
|
+
dispose();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const Ctx = createContext<SessionSignals | null>(null);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Preact context provider that makes session signals available to descendant
|
|
84
|
+
* components via {@linkcode useSession}.
|
|
85
|
+
*
|
|
86
|
+
* @param props - Provider props.
|
|
87
|
+
* @param props.value - The session signals to provide.
|
|
88
|
+
* @param props.children - Child components that may consume the context.
|
|
89
|
+
* @returns A Preact VNode wrapping children in the session context.
|
|
90
|
+
*/
|
|
91
|
+
export function SessionProvider({
|
|
92
|
+
value,
|
|
93
|
+
children,
|
|
94
|
+
}: {
|
|
95
|
+
value: SessionSignals;
|
|
96
|
+
children: ComponentChildren;
|
|
97
|
+
}): preact.JSX.Element {
|
|
98
|
+
return h(Ctx.Provider, { value }, children);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook to access session signals from within a {@linkcode SessionProvider}.
|
|
103
|
+
*
|
|
104
|
+
* @returns The {@linkcode SessionSignals} from the nearest provider.
|
|
105
|
+
* @throws {Error} If called outside of a `SessionProvider`.
|
|
106
|
+
*/
|
|
107
|
+
export function useSession(): SessionSignals {
|
|
108
|
+
const ctx = useContext(Ctx);
|
|
109
|
+
if (!ctx) throw new Error("Hook useSession() requires a SessionProvider");
|
|
110
|
+
return ctx;
|
|
111
|
+
}
|
package/ui/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/** Microphone buffer duration in seconds before sending to the server. */
|
|
3
|
+
export const MIC_BUFFER_SECONDS = 0.1;
|
|
4
|
+
|
|
5
|
+
/** Current state of the voice agent session. */
|
|
6
|
+
export type AgentState =
|
|
7
|
+
| "disconnected"
|
|
8
|
+
| "connecting"
|
|
9
|
+
| "ready"
|
|
10
|
+
| "listening"
|
|
11
|
+
| "thinking"
|
|
12
|
+
| "speaking"
|
|
13
|
+
| "error";
|
|
14
|
+
|
|
15
|
+
/** A chat message exchanged between user and assistant. */
|
|
16
|
+
export type Message = {
|
|
17
|
+
/** The sender of the message. */
|
|
18
|
+
role: "user" | "assistant";
|
|
19
|
+
/** The text content of the message. */
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Info about a tool call for display in the UI. */
|
|
24
|
+
export type ToolCallInfo = {
|
|
25
|
+
toolCallId: string;
|
|
26
|
+
toolName: string;
|
|
27
|
+
args: Record<string, unknown>;
|
|
28
|
+
status: "pending" | "done";
|
|
29
|
+
result?: string | undefined;
|
|
30
|
+
/** Index in the messages array where this tool call should appear. */
|
|
31
|
+
afterMessageIndex: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
import type { SessionErrorCode } from "../sdk/protocol.ts";
|
|
35
|
+
|
|
36
|
+
export type { SessionErrorCode };
|
|
37
|
+
|
|
38
|
+
/** Error reported by the voice session. */
|
|
39
|
+
export type SessionError = {
|
|
40
|
+
/** The category of the error. */
|
|
41
|
+
readonly code: SessionErrorCode;
|
|
42
|
+
/** A human-readable description of the error. */
|
|
43
|
+
readonly message: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Options for creating a voice session. */
|
|
47
|
+
export type SessionOptions = {
|
|
48
|
+
/** Base URL of the AAI platform server. */
|
|
49
|
+
platformUrl: string;
|
|
50
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Capture worklet: captures mic Float32 samples, resamples to STT rate if
|
|
2
|
+
// needed, converts to Int16 PCM, and sends chunks to the main thread.
|
|
3
|
+
|
|
4
|
+
const CaptureProcessorWorklet = `
|
|
5
|
+
class CaptureProcessor extends AudioWorkletProcessor {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super();
|
|
8
|
+
this.recording = false;
|
|
9
|
+
const opts = options.processorOptions || {};
|
|
10
|
+
this.fromRate = opts.contextRate || sampleRate;
|
|
11
|
+
this.toRate = opts.sttSampleRate || sampleRate;
|
|
12
|
+
this.ratio = this.fromRate / this.toRate;
|
|
13
|
+
this.needsResample = this.fromRate !== this.toRate;
|
|
14
|
+
this.port.onmessage = (e) => {
|
|
15
|
+
if (e.data.event === 'start') this.recording = true;
|
|
16
|
+
else if (e.data.event === 'stop') this.recording = false;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
resample(input) {
|
|
21
|
+
const ratio = this.ratio;
|
|
22
|
+
const outLen = Math.ceil(input.length / ratio);
|
|
23
|
+
const out = new Float32Array(outLen);
|
|
24
|
+
for (let i = 0; i < outLen; i++) {
|
|
25
|
+
const srcIdx = i * ratio;
|
|
26
|
+
const idx = srcIdx | 0;
|
|
27
|
+
const frac = srcIdx - idx;
|
|
28
|
+
const a = input[idx];
|
|
29
|
+
const b = idx + 1 < input.length ? input[idx + 1] : a;
|
|
30
|
+
out[i] = a + frac * (b - a);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process(inputs) {
|
|
36
|
+
const input = inputs[0];
|
|
37
|
+
if (!input || !input[0] || !this.recording) return true;
|
|
38
|
+
|
|
39
|
+
const raw = input[0];
|
|
40
|
+
const samples = this.needsResample ? this.resample(raw) : raw;
|
|
41
|
+
|
|
42
|
+
// Convert Float32 -> Int16
|
|
43
|
+
const buffer = new ArrayBuffer(samples.length * 2);
|
|
44
|
+
const view = new DataView(buffer);
|
|
45
|
+
for (let i = 0; i < samples.length; i++) {
|
|
46
|
+
const s = Math.max(-1, Math.min(1, samples[i]));
|
|
47
|
+
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
registerProcessor('capture-processor', CaptureProcessor);
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const script = new Blob([CaptureProcessorWorklet], {
|
|
59
|
+
type: "application/javascript",
|
|
60
|
+
});
|
|
61
|
+
const src = URL.createObjectURL(script);
|
|
62
|
+
export default src;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Playback worklet: receives raw PCM16 LE bytes, handles byte alignment,
|
|
2
|
+
// converts to float32, and plays with a small jitter buffer.
|
|
3
|
+
|
|
4
|
+
const PlaybackProcessorWorklet = `
|
|
5
|
+
class PlaybackProcessor extends AudioWorkletProcessor {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super();
|
|
8
|
+
this.interrupted = false;
|
|
9
|
+
this.isDone = false;
|
|
10
|
+
this.playing = false;
|
|
11
|
+
const rate = options.processorOptions?.sampleRate ?? 24000;
|
|
12
|
+
// Wait for ~400ms of audio before starting.
|
|
13
|
+
// If 'done' arrives first (short utterance), start immediately.
|
|
14
|
+
this.jitterSamples = Math.floor(rate * 0.4);
|
|
15
|
+
// Carry-over byte for split samples across chunks
|
|
16
|
+
this.carry = null;
|
|
17
|
+
// Float32 sample buffer — 60s at the context sample rate
|
|
18
|
+
this.samples = new Float32Array(rate * 60);
|
|
19
|
+
this.writePos = 0;
|
|
20
|
+
this.readPos = 0;
|
|
21
|
+
// Report playback position every ~50ms for word-level text sync
|
|
22
|
+
this.progressInterval = Math.floor(rate * 0.05);
|
|
23
|
+
this.samplesSinceProgress = 0;
|
|
24
|
+
|
|
25
|
+
this.port.onmessage = (e) => {
|
|
26
|
+
const d = e.data;
|
|
27
|
+
if (d.event === 'write') {
|
|
28
|
+
this.ingestBytes(d.buffer);
|
|
29
|
+
} else if (d.event === 'interrupt') {
|
|
30
|
+
this.interrupted = true;
|
|
31
|
+
} else if (d.event === 'done') {
|
|
32
|
+
this.isDone = true;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ingestBytes(uint8) {
|
|
38
|
+
let bytes = uint8;
|
|
39
|
+
|
|
40
|
+
if (this.carry !== null) {
|
|
41
|
+
const merged = new Uint8Array(1 + bytes.length);
|
|
42
|
+
merged[0] = this.carry;
|
|
43
|
+
merged.set(bytes, 1);
|
|
44
|
+
bytes = merged;
|
|
45
|
+
this.carry = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (bytes.length % 2 !== 0) {
|
|
49
|
+
this.carry = bytes[bytes.length - 1];
|
|
50
|
+
bytes = bytes.subarray(0, bytes.length - 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (bytes.length === 0) return;
|
|
54
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
55
|
+
const numSamples = bytes.length / 2;
|
|
56
|
+
for (let i = 0; i < numSamples; i++) {
|
|
57
|
+
this.samples[this.writePos++] = view.getInt16(i * 2, true) / 0x8000;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process(inputs, outputs) {
|
|
62
|
+
const out = outputs[0][0];
|
|
63
|
+
if (this.interrupted) {
|
|
64
|
+
this.port.postMessage({ event: 'stop' });
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const avail = this.writePos - this.readPos;
|
|
69
|
+
|
|
70
|
+
// Wait for jitter buffer to fill, unless done (short utterance)
|
|
71
|
+
if (!this.playing) {
|
|
72
|
+
if (avail >= this.jitterSamples || this.isDone) {
|
|
73
|
+
this.playing = true;
|
|
74
|
+
} else {
|
|
75
|
+
for (let i = 0; i < out.length; i++) out[i] = 0;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (avail > 0) {
|
|
81
|
+
const n = Math.min(avail, out.length);
|
|
82
|
+
out.set(this.samples.subarray(this.readPos, this.readPos + n));
|
|
83
|
+
this.readPos += n;
|
|
84
|
+
this.samplesSinceProgress += n;
|
|
85
|
+
if (this.samplesSinceProgress >= this.progressInterval) {
|
|
86
|
+
this.port.postMessage({ event: 'progress', readPos: this.readPos });
|
|
87
|
+
this.samplesSinceProgress = 0;
|
|
88
|
+
}
|
|
89
|
+
for (let i = n; i < out.length; i++) out[i] = 0;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// No data: output silence, stop only when done
|
|
94
|
+
for (let i = 0; i < out.length; i++) out[i] = 0;
|
|
95
|
+
if (this.isDone) {
|
|
96
|
+
this.port.postMessage({ event: 'stop' });
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
registerProcessor('playback-processor', PlaybackProcessor);
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const script = new Blob([PlaybackProcessorWorklet], {
|
|
107
|
+
type: "application/javascript",
|
|
108
|
+
});
|
|
109
|
+
const src = URL.createObjectURL(script);
|
|
110
|
+
export default src;
|