@bytexbyte/nxtlinq-ai-agent-core-development 0.2.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/dist/agent/ChatOrchestrator.d.ts +48 -0
- package/dist/agent/ChatOrchestrator.d.ts.map +1 -0
- package/dist/agent/ChatOrchestrator.js +311 -0
- package/dist/agent/NxtlinqAgent.d.ts +65 -0
- package/dist/agent/NxtlinqAgent.d.ts.map +1 -0
- package/dist/agent/NxtlinqAgent.js +256 -0
- package/dist/agent/errors.d.ts +4 -0
- package/dist/agent/errors.d.ts.map +1 -0
- package/dist/agent/errors.js +6 -0
- package/dist/agent/extractReplyText.d.ts +3 -0
- package/dist/agent/extractReplyText.d.ts.map +1 -0
- package/dist/agent/extractReplyText.js +16 -0
- package/dist/api/hosts.d.ts +4 -0
- package/dist/api/hosts.d.ts.map +1 -0
- package/dist/api/hosts.js +18 -0
- package/dist/api/nxtlinq-api.d.ts +9 -0
- package/dist/api/nxtlinq-api.d.ts.map +1 -0
- package/dist/api/nxtlinq-api.js +499 -0
- package/dist/api/parse-sse.d.ts +9 -0
- package/dist/api/parse-sse.d.ts.map +1 -0
- package/dist/api/parse-sse.js +97 -0
- package/dist/api/tts.d.ts +19 -0
- package/dist/api/tts.d.ts.map +1 -0
- package/dist/api/tts.js +46 -0
- package/dist/constants/storageKeys.d.ts +6 -0
- package/dist/constants/storageKeys.d.ts.map +1 -0
- package/dist/constants/storageKeys.js +5 -0
- package/dist/history/messageHistory.d.ts +18 -0
- package/dist/history/messageHistory.d.ts.map +1 -0
- package/dist/history/messageHistory.js +48 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/ports/HttpPort.d.ts +6 -0
- package/dist/ports/HttpPort.d.ts.map +1 -0
- package/dist/ports/HttpPort.js +3 -0
- package/dist/ports/PlatformPorts.d.ts +12 -0
- package/dist/ports/PlatformPorts.d.ts.map +1 -0
- package/dist/ports/PlatformPorts.js +1 -0
- package/dist/ports/StoragePort.d.ts +10 -0
- package/dist/ports/StoragePort.d.ts.map +1 -0
- package/dist/ports/StoragePort.js +33 -0
- package/dist/ports/WebRTCPort.d.ts +68 -0
- package/dist/ports/WebRTCPort.d.ts.map +1 -0
- package/dist/ports/WebRTCPort.js +10 -0
- package/dist/ports/createBrowserWebRTCPort.d.ts +7 -0
- package/dist/ports/createBrowserWebRTCPort.d.ts.map +1 -0
- package/dist/ports/createBrowserWebRTCPort.js +140 -0
- package/dist/ports/index.d.ts +5 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +4 -0
- package/dist/types/agent-config.d.ts +40 -0
- package/dist/types/agent-config.d.ts.map +1 -0
- package/dist/types/agent-config.js +1 -0
- package/dist/types/ait-api.d.ts +393 -0
- package/dist/types/ait-api.d.ts.map +1 -0
- package/dist/types/ait-api.js +1 -0
- package/dist/voice/app-channel-dispatcher.d.ts +14 -0
- package/dist/voice/app-channel-dispatcher.d.ts.map +1 -0
- package/dist/voice/app-channel-dispatcher.js +171 -0
- package/dist/voice/create-voice-session.d.ts +8 -0
- package/dist/voice/create-voice-session.d.ts.map +1 -0
- package/dist/voice/create-voice-session.js +37 -0
- package/dist/voice/output-audio-level.d.ts +26 -0
- package/dist/voice/output-audio-level.d.ts.map +1 -0
- package/dist/voice/output-audio-level.js +132 -0
- package/dist/voice/remote-audio-gain.d.ts +10 -0
- package/dist/voice/remote-audio-gain.d.ts.map +1 -0
- package/dist/voice/remote-audio-gain.js +19 -0
- package/dist/voice/start-voice-session.d.ts +13 -0
- package/dist/voice/start-voice-session.d.ts.map +1 -0
- package/dist/voice/start-voice-session.js +303 -0
- package/dist/voice/transcript.d.ts +10 -0
- package/dist/voice/transcript.d.ts.map +1 -0
- package/dist/voice/transcript.js +50 -0
- package/dist/voice/trigger-voice-greeting.d.ts +14 -0
- package/dist/voice/trigger-voice-greeting.d.ts.map +1 -0
- package/dist/voice/trigger-voice-greeting.js +28 -0
- package/dist/voice/types.d.ts +138 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +1 -0
- package/dist/voice/voice-user-input.d.ts +19 -0
- package/dist/voice/voice-user-input.d.ts.map +1 -0
- package/dist/voice/voice-user-input.js +10 -0
- package/package.json +41 -0
- package/src/agent/ChatOrchestrator.ts +380 -0
- package/src/agent/NxtlinqAgent.ts +325 -0
- package/src/agent/errors.ts +6 -0
- package/src/agent/extractReplyText.ts +22 -0
- package/src/api/hosts.ts +20 -0
- package/src/api/nxtlinq-api.ts +656 -0
- package/src/api/parse-sse.ts +104 -0
- package/src/api/tts.ts +69 -0
- package/src/constants/storageKeys.ts +5 -0
- package/src/history/messageHistory.ts +65 -0
- package/src/index.ts +70 -0
- package/src/ports/HttpPort.ts +12 -0
- package/src/ports/PlatformPorts.ts +12 -0
- package/src/ports/StoragePort.ts +37 -0
- package/src/ports/WebRTCPort.ts +54 -0
- package/src/ports/createBrowserWebRTCPort.ts +163 -0
- package/src/ports/index.ts +4 -0
- package/src/types/agent-config.ts +51 -0
- package/src/types/ait-api.ts +303 -0
- package/src/voice/app-channel-dispatcher.ts +201 -0
- package/src/voice/create-voice-session.ts +53 -0
- package/src/voice/output-audio-level.ts +153 -0
- package/src/voice/remote-audio-gain.ts +31 -0
- package/src/voice/start-voice-session.ts +369 -0
- package/src/voice/transcript.ts +44 -0
- package/src/voice/trigger-voice-greeting.ts +47 -0
- package/src/voice/types.ts +154 -0
- package/src/voice/voice-user-input.ts +32 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type SSEHandlers = {
|
|
2
|
+
onPiiProgress?: (step: 'scan_start' | 'scan_complete' | 'send_start' | 'done', data?: unknown) => void;
|
|
3
|
+
onStreamDelta?: (text: string) => void;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/** Normalize CRLF and split into SSE message blocks. */
|
|
7
|
+
function splitSSEMessages(raw: string): string[] {
|
|
8
|
+
const normalized = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
9
|
+
return normalized.split('\n\n').filter((block) => block.trim().length > 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function dispatchSSEMessage(
|
|
13
|
+
msg: string,
|
|
14
|
+
handlers: SSEHandlers,
|
|
15
|
+
finalDataRef: { value: unknown | null },
|
|
16
|
+
): void {
|
|
17
|
+
if (!msg.trim()) return;
|
|
18
|
+
let eventType = '';
|
|
19
|
+
const dataLines: string[] = [];
|
|
20
|
+
for (const line of msg.split('\n')) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (trimmed.startsWith('event:')) {
|
|
23
|
+
eventType = trimmed.slice(6).trim();
|
|
24
|
+
} else if (trimmed.startsWith('data:')) {
|
|
25
|
+
dataLines.push(trimmed.slice(5).trimStart());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!eventType) return;
|
|
29
|
+
|
|
30
|
+
const data = dataLines.join('\n');
|
|
31
|
+
const onProgress = handlers.onPiiProgress;
|
|
32
|
+
|
|
33
|
+
if (eventType === 'pii_scan_start') {
|
|
34
|
+
onProgress?.('scan_start');
|
|
35
|
+
} else if (eventType === 'pii_scan_complete') {
|
|
36
|
+
let parsed: unknown = undefined;
|
|
37
|
+
try {
|
|
38
|
+
parsed = data ? JSON.parse(data) : undefined;
|
|
39
|
+
} catch {
|
|
40
|
+
/* ignore */
|
|
41
|
+
}
|
|
42
|
+
onProgress?.('scan_complete', parsed);
|
|
43
|
+
} else if (eventType === 'pii_send_start') {
|
|
44
|
+
onProgress?.('send_start');
|
|
45
|
+
} else if (eventType === 'text_delta') {
|
|
46
|
+
let parsed: { text?: string } = {};
|
|
47
|
+
try {
|
|
48
|
+
parsed = data ? JSON.parse(data) : {};
|
|
49
|
+
} catch {
|
|
50
|
+
/* ignore */
|
|
51
|
+
}
|
|
52
|
+
const chunk = typeof parsed.text === 'string' ? parsed.text : '';
|
|
53
|
+
handlers.onStreamDelta?.(chunk);
|
|
54
|
+
} else if (eventType === 'error') {
|
|
55
|
+
let parsed: { error?: string } = {};
|
|
56
|
+
try {
|
|
57
|
+
parsed = data ? JSON.parse(data) : {};
|
|
58
|
+
} catch {
|
|
59
|
+
/* ignore */
|
|
60
|
+
}
|
|
61
|
+
throw new Error(parsed.error || 'SSE stream error');
|
|
62
|
+
} else if (eventType === 'done') {
|
|
63
|
+
try {
|
|
64
|
+
finalDataRef.value = data ? JSON.parse(data) : {};
|
|
65
|
+
} catch {
|
|
66
|
+
/* ignore */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseSSEPayload(raw: string, handlers: SSEHandlers): unknown {
|
|
72
|
+
const finalDataRef = { value: null as unknown | null };
|
|
73
|
+
for (const msg of splitSSEMessages(raw)) {
|
|
74
|
+
dispatchSSEMessage(msg, handlers, finalDataRef);
|
|
75
|
+
}
|
|
76
|
+
if (finalDataRef.value !== null && finalDataRef.value !== undefined) {
|
|
77
|
+
return finalDataRef.value;
|
|
78
|
+
}
|
|
79
|
+
throw new Error('SSE stream ended without done event');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Parse a complete SSE payload (React Native fetch often lacks a usable `response.body`). */
|
|
83
|
+
export function parseSSEText(text: string, handlers: SSEHandlers): unknown {
|
|
84
|
+
return parseSSEPayload(text, handlers);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Parse Agent SSE: PII steps, optional LLM `text_delta`, final `done`. */
|
|
88
|
+
export async function parseSSEResponse(
|
|
89
|
+
body: ReadableStream<Uint8Array>,
|
|
90
|
+
handlers: SSEHandlers,
|
|
91
|
+
): Promise<unknown> {
|
|
92
|
+
const reader = body.getReader();
|
|
93
|
+
const decoder = new TextDecoder();
|
|
94
|
+
let buffer = '';
|
|
95
|
+
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
buffer += decoder.decode(value, { stream: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
buffer += decoder.decode();
|
|
103
|
+
return parseSSEPayload(buffer, handlers);
|
|
104
|
+
}
|
package/src/api/tts.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { HttpFetch } from '../ports/HttpPort';
|
|
2
|
+
import { getAiAgentApiHost } from './hosts';
|
|
3
|
+
|
|
4
|
+
export type PostTextTtsParams = {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
apiSecret: string;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PostTextTtsResult = {
|
|
11
|
+
/** Raw MPEG audio bytes from `/api/tts/openai`. */
|
|
12
|
+
audio: ArrayBuffer;
|
|
13
|
+
mimeType: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
17
|
+
if (typeof globalThis.Buffer !== 'undefined') {
|
|
18
|
+
return globalThis.Buffer.from(bytes).toString('base64');
|
|
19
|
+
}
|
|
20
|
+
let binary = '';
|
|
21
|
+
const chunk = 0x8000;
|
|
22
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
23
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
24
|
+
}
|
|
25
|
+
if (typeof globalThis.btoa !== 'function') {
|
|
26
|
+
throw new Error('No base64 encoder available in this runtime');
|
|
27
|
+
}
|
|
28
|
+
return globalThis.btoa(binary);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Synthesize assistant text via nxtlinq AI Agent OpenAI TTS (Dashboard voice config).
|
|
33
|
+
* Berify-compatible name; returns audio bytes instead of Berify `audio_id` cache.
|
|
34
|
+
*/
|
|
35
|
+
export async function postTextTts(
|
|
36
|
+
params: PostTextTtsParams,
|
|
37
|
+
fetchFn: HttpFetch = globalThis.fetch.bind(globalThis) as HttpFetch,
|
|
38
|
+
): Promise<PostTextTtsResult> {
|
|
39
|
+
const text = params.text.trim();
|
|
40
|
+
if (!text) {
|
|
41
|
+
throw new Error('postTextTts: text is empty');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const res = await fetchFn(`${getAiAgentApiHost()}/api/tts/openai`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
apiKey: params.apiKey,
|
|
49
|
+
apiSecret: params.apiSecret,
|
|
50
|
+
text,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const errBody = await res.text().catch(() => '');
|
|
56
|
+
throw new Error(`postTextTts failed: ${res.status} ${errBody.slice(0, 200)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const audio = await res.arrayBuffer();
|
|
60
|
+
const mimeType =
|
|
61
|
+
res.headers.get('content-type')?.split(';')[0]?.trim() || 'audio/mpeg';
|
|
62
|
+
return { audio, mimeType };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Build a data URI suitable for RN `Audio` / `Video` `source={{ uri }}`. */
|
|
66
|
+
export function buildTextTtsDataUri(audio: ArrayBuffer, mimeType = 'audio/mpeg'): string {
|
|
67
|
+
const bytes = new Uint8Array(audio);
|
|
68
|
+
return `data:${mimeType};base64,${bytesToBase64(bytes)}`;
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Message } from '../types/ait-api';
|
|
2
|
+
|
|
3
|
+
/** One voice turn is typically user + assistant (2 persisted messages). */
|
|
4
|
+
export const VOICE_TURN_SYNC_LAST = 2;
|
|
5
|
+
|
|
6
|
+
export type ServerHistoryMessage = {
|
|
7
|
+
id: string;
|
|
8
|
+
role: 'user' | 'assistant';
|
|
9
|
+
content: string;
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
attachments?: Message['attachments'];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function isVoicePlaceholderMessage(message: Message): boolean {
|
|
15
|
+
if (message.metadata?.voiceRealtime) return true;
|
|
16
|
+
return String(message.id).startsWith('voice-');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function mapServerHistoryToMessages(rows: ServerHistoryMessage[]): Message[] {
|
|
20
|
+
return rows.map((row) => ({
|
|
21
|
+
id: row.id,
|
|
22
|
+
role: row.role,
|
|
23
|
+
content: row.content ?? '',
|
|
24
|
+
timestamp: row.createdAt ?? new Date().toISOString(),
|
|
25
|
+
attachments: row.attachments,
|
|
26
|
+
isStreaming: false,
|
|
27
|
+
partialContent: undefined,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Append the latest server messages onto local history (one voice turn ≈ 2 rows).
|
|
33
|
+
* Strips voice placeholders, upserts by message id, preserves unrelated local rows.
|
|
34
|
+
*/
|
|
35
|
+
export function appendServerHistoryIntoMessages(
|
|
36
|
+
prev: Message[],
|
|
37
|
+
serverRows: ServerHistoryMessage[],
|
|
38
|
+
): Message[] {
|
|
39
|
+
const withoutPlaceholders = prev.filter((m) => !isVoicePlaceholderMessage(m));
|
|
40
|
+
const incoming = mapServerHistoryToMessages(serverRows);
|
|
41
|
+
if (incoming.length === 0) return withoutPlaceholders;
|
|
42
|
+
|
|
43
|
+
const byId = new Map(withoutPlaceholders.map((m) => [m.id, m]));
|
|
44
|
+
for (const row of incoming) {
|
|
45
|
+
const existing = byId.get(row.id);
|
|
46
|
+
if (existing) {
|
|
47
|
+
byId.set(row.id, {
|
|
48
|
+
...existing,
|
|
49
|
+
content: row.content,
|
|
50
|
+
role: row.role,
|
|
51
|
+
attachments: row.attachments ?? existing.attachments,
|
|
52
|
+
isStreaming: false,
|
|
53
|
+
partialContent: undefined,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
byId.set(row.id, row);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const merged = Array.from(byId.values());
|
|
61
|
+
merged.sort(
|
|
62
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
|
63
|
+
);
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// API
|
|
2
|
+
export { setApiHosts, getAiAgentApiHost, getAitServiceApiHost } from './api/hosts';
|
|
3
|
+
export {
|
|
4
|
+
createNxtlinqApiWithDeps,
|
|
5
|
+
type CoreAITApi,
|
|
6
|
+
type ApiClientDeps,
|
|
7
|
+
} from './api/nxtlinq-api';
|
|
8
|
+
export { parseSSEResponse, parseSSEText } from './api/parse-sse';
|
|
9
|
+
export {
|
|
10
|
+
postTextTts,
|
|
11
|
+
buildTextTtsDataUri,
|
|
12
|
+
type PostTextTtsParams,
|
|
13
|
+
type PostTextTtsResult,
|
|
14
|
+
} from './api/tts';
|
|
15
|
+
|
|
16
|
+
// Ports
|
|
17
|
+
export * from './ports';
|
|
18
|
+
export { STORAGE_KEYS } from './constants/storageKeys';
|
|
19
|
+
|
|
20
|
+
// Types
|
|
21
|
+
export type * from './types/ait-api';
|
|
22
|
+
export type { AgentConfig, ToolUse, AgentEnvironment } from './types/agent-config';
|
|
23
|
+
|
|
24
|
+
// History
|
|
25
|
+
export {
|
|
26
|
+
appendServerHistoryIntoMessages,
|
|
27
|
+
isVoicePlaceholderMessage,
|
|
28
|
+
mapServerHistoryToMessages,
|
|
29
|
+
VOICE_TURN_SYNC_LAST,
|
|
30
|
+
type ServerHistoryMessage,
|
|
31
|
+
} from './history/messageHistory';
|
|
32
|
+
|
|
33
|
+
// Voice (protocol + session mint; WebRTC connect in platform packages)
|
|
34
|
+
export * from './voice/types';
|
|
35
|
+
export {
|
|
36
|
+
normalizeTranscriptKey,
|
|
37
|
+
mergeStreamingTranscript,
|
|
38
|
+
appendTranscriptSegment,
|
|
39
|
+
} from './voice/transcript';
|
|
40
|
+
export {
|
|
41
|
+
createVoiceSession,
|
|
42
|
+
appendVoiceModeToSignalingUrl,
|
|
43
|
+
} from './voice/create-voice-session';
|
|
44
|
+
export {
|
|
45
|
+
startVoiceSessionWithPort,
|
|
46
|
+
type StartVoiceSessionDeps,
|
|
47
|
+
} from './voice/start-voice-session';
|
|
48
|
+
export { createAppChannelDispatcher } from './voice/app-channel-dispatcher';
|
|
49
|
+
export { createBrowserWebRTCPort } from './ports/createBrowserWebRTCPort';
|
|
50
|
+
export type { CreateVoiceSessionParams, VoiceGreetingOptions, VoiceUserInputOptions } from './voice/types';
|
|
51
|
+
export { buildUserInputPayload } from './voice/voice-user-input';
|
|
52
|
+
export {
|
|
53
|
+
applyRemoteAudioPlaybackGain,
|
|
54
|
+
applyRemoteAudioPlaybackGainToTrack,
|
|
55
|
+
DEFAULT_REMOTE_AUDIO_GAIN,
|
|
56
|
+
type RemoteAudioTrackLike,
|
|
57
|
+
} from './voice/remote-audio-gain';
|
|
58
|
+
export {
|
|
59
|
+
createOutputAudioLevelMeter,
|
|
60
|
+
createAnalyserOutputAudioLevelMeter,
|
|
61
|
+
createStatsOutputAudioLevelMeter,
|
|
62
|
+
type OutputAudioLevelMeter,
|
|
63
|
+
} from './voice/output-audio-level';
|
|
64
|
+
export { triggerVoiceGreeting } from './voice/trigger-voice-greeting';
|
|
65
|
+
|
|
66
|
+
// Agent
|
|
67
|
+
export { NxtlinqAgent, type NxtlinqAgentSnapshot, type SendMessageOptions } from './agent/NxtlinqAgent';
|
|
68
|
+
export { ChatOrchestrator } from './agent/ChatOrchestrator';
|
|
69
|
+
export { extractReplyText } from './agent/extractReplyText';
|
|
70
|
+
export { AgentNotInitializedError } from './agent/errors';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type HttpFetch = (
|
|
2
|
+
url: string,
|
|
3
|
+
init?: RequestInit,
|
|
4
|
+
) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
export interface HttpPort {
|
|
7
|
+
fetch: HttpFetch;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createDefaultHttpPort(): HttpPort {
|
|
11
|
+
return { fetch: globalThis.fetch.bind(globalThis) };
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HttpPort } from './HttpPort';
|
|
2
|
+
import type { StoragePort } from './StoragePort';
|
|
3
|
+
import type { WebRTCPort } from './WebRTCPort';
|
|
4
|
+
|
|
5
|
+
export interface PlatformPorts {
|
|
6
|
+
storage: StoragePort;
|
|
7
|
+
http: HttpPort;
|
|
8
|
+
/** Optional until M2; when missing, voice connect throws VoiceNotSupportedError. */
|
|
9
|
+
webrtc?: WebRTCPort;
|
|
10
|
+
/** IANA timezone for agent requests (default UTC). */
|
|
11
|
+
getTimezone?: () => string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface StoragePort {
|
|
2
|
+
getItem(key: string): Promise<string | null>;
|
|
3
|
+
setItem(key: string, value: string): Promise<void>;
|
|
4
|
+
removeItem(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Browser `localStorage` adapter (for Web SDK wrapper). */
|
|
8
|
+
export function createBrowserStoragePort(): StoragePort {
|
|
9
|
+
return {
|
|
10
|
+
getItem: async (key) => {
|
|
11
|
+
if (typeof localStorage === 'undefined') return null;
|
|
12
|
+
return localStorage.getItem(key);
|
|
13
|
+
},
|
|
14
|
+
setItem: async (key, value) => {
|
|
15
|
+
if (typeof localStorage === 'undefined') return;
|
|
16
|
+
localStorage.setItem(key, value);
|
|
17
|
+
},
|
|
18
|
+
removeItem: async (key) => {
|
|
19
|
+
if (typeof localStorage === 'undefined') return;
|
|
20
|
+
localStorage.removeItem(key);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** In-memory storage for tests or SSR. */
|
|
26
|
+
export function createMemoryStoragePort(initial: Record<string, string> = {}): StoragePort {
|
|
27
|
+
const map = new Map(Object.entries(initial));
|
|
28
|
+
return {
|
|
29
|
+
getItem: async (key) => map.get(key) ?? null,
|
|
30
|
+
setItem: async (key, value) => {
|
|
31
|
+
map.set(key, value);
|
|
32
|
+
},
|
|
33
|
+
removeItem: async (key) => {
|
|
34
|
+
map.delete(key);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform WebRTC surface. Browser and React Native implementations live in
|
|
3
|
+
* `@nxtlinq/ai-agent-web` and `@bytexbyte/nxtlinq-ai-agent-react-native-development` (M2).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface IceServerConfig {
|
|
7
|
+
urls: string | string[];
|
|
8
|
+
username?: string;
|
|
9
|
+
credential?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MediaStreamLike {
|
|
13
|
+
getAudioTracks(): Array<{ enabled: boolean; stop(): void }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PeerConnectionLike {
|
|
17
|
+
connectionState: string;
|
|
18
|
+
iceConnectionState: string;
|
|
19
|
+
onconnectionstatechange: (() => void) | null;
|
|
20
|
+
oniceconnectionstatechange: (() => void) | null;
|
|
21
|
+
ontrack: ((event: { streams: MediaStreamLike[]; track: { kind: string } }) => void) | null;
|
|
22
|
+
/** Fired when the remote peer creates a data channel (e.g. Pipecat answer side). */
|
|
23
|
+
ondatachannel: ((event: { channel: DataChannelLike }) => void) | null;
|
|
24
|
+
createDataChannel(label: string): DataChannelLike;
|
|
25
|
+
addTrack(track: unknown, stream: MediaStreamLike): void;
|
|
26
|
+
createOffer(): Promise<{ sdp?: string; type?: string }>;
|
|
27
|
+
setLocalDescription(desc: unknown): Promise<void>;
|
|
28
|
+
setRemoteDescription(desc: unknown): Promise<void>;
|
|
29
|
+
getLocalDescription(): { sdp?: string; type?: string } | null;
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DataChannelLike {
|
|
34
|
+
readyState: string;
|
|
35
|
+
onopen: (() => void) | null;
|
|
36
|
+
onmessage: ((event: { data: string }) => void) | null;
|
|
37
|
+
onclose: (() => void) | null;
|
|
38
|
+
send(data: string): void;
|
|
39
|
+
close(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WebRTCPort {
|
|
43
|
+
isSupported(): boolean;
|
|
44
|
+
getUserMedia(constraints: { audio: boolean }): Promise<MediaStreamLike>;
|
|
45
|
+
createPeerConnection(config: { iceServers: IceServerConfig[] }): PeerConnectionLike;
|
|
46
|
+
waitForIceGathering(pc: PeerConnectionLike, timeoutMs: number): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class VoiceNotSupportedError extends Error {
|
|
50
|
+
constructor(message = 'WebRTC voice is not supported on this platform') {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'VoiceNotSupportedError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataChannelLike,
|
|
3
|
+
IceServerConfig,
|
|
4
|
+
MediaStreamLike,
|
|
5
|
+
PeerConnectionLike,
|
|
6
|
+
WebRTCPort,
|
|
7
|
+
} from './WebRTCPort';
|
|
8
|
+
import { VoiceNotSupportedError } from './WebRTCPort';
|
|
9
|
+
|
|
10
|
+
const ICE_GATHERING_TIMEOUT_MS = 5000;
|
|
11
|
+
|
|
12
|
+
function wrapDataChannel(channel: RTCDataChannel): DataChannelLike {
|
|
13
|
+
const wrapper: DataChannelLike = {
|
|
14
|
+
get readyState() {
|
|
15
|
+
return channel.readyState;
|
|
16
|
+
},
|
|
17
|
+
onopen: null,
|
|
18
|
+
onmessage: null,
|
|
19
|
+
onclose: null,
|
|
20
|
+
send: (data) => channel.send(data),
|
|
21
|
+
close: () => channel.close(),
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(wrapper, 'onopen', {
|
|
24
|
+
get: () => null,
|
|
25
|
+
set(handler: (() => void) | null) {
|
|
26
|
+
channel.onopen = handler ? () => handler() : null;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(wrapper, 'onmessage', {
|
|
30
|
+
get: () => null,
|
|
31
|
+
set(handler: ((event: { data: string }) => void) | null) {
|
|
32
|
+
channel.onmessage = handler
|
|
33
|
+
? (event: MessageEvent) => handler({ data: String(event.data) })
|
|
34
|
+
: null;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(wrapper, 'onclose', {
|
|
38
|
+
get: () => null,
|
|
39
|
+
set(handler: (() => void) | null) {
|
|
40
|
+
channel.onclose = handler ? () => handler() : null;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return wrapper;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type BrowserPeerConnectionLike = PeerConnectionLike & {
|
|
47
|
+
__nativePc: RTCPeerConnection;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function wrapPeerConnection(pc: RTCPeerConnection): BrowserPeerConnectionLike {
|
|
51
|
+
const wrapped: BrowserPeerConnectionLike = {
|
|
52
|
+
__nativePc: pc,
|
|
53
|
+
get connectionState() {
|
|
54
|
+
return pc.connectionState;
|
|
55
|
+
},
|
|
56
|
+
get iceConnectionState() {
|
|
57
|
+
return pc.iceConnectionState;
|
|
58
|
+
},
|
|
59
|
+
onconnectionstatechange: null,
|
|
60
|
+
oniceconnectionstatechange: null,
|
|
61
|
+
ontrack: null,
|
|
62
|
+
ondatachannel: null,
|
|
63
|
+
createDataChannel: (label) => wrapDataChannel(pc.createDataChannel(label)),
|
|
64
|
+
addTrack: (track, stream) => pc.addTrack(track as MediaStreamTrack, stream as MediaStream),
|
|
65
|
+
createOffer: () => pc.createOffer(),
|
|
66
|
+
setLocalDescription: (desc) => pc.setLocalDescription(desc as RTCSessionDescriptionInit),
|
|
67
|
+
setRemoteDescription: (desc) => pc.setRemoteDescription(desc as RTCSessionDescriptionInit),
|
|
68
|
+
getLocalDescription: () => {
|
|
69
|
+
const local = pc.localDescription;
|
|
70
|
+
return local ? { sdp: local.sdp ?? undefined, type: local.type } : null;
|
|
71
|
+
},
|
|
72
|
+
close: () => pc.close(),
|
|
73
|
+
};
|
|
74
|
+
Object.defineProperty(wrapped, 'onconnectionstatechange', {
|
|
75
|
+
get: () => null,
|
|
76
|
+
set(handler: (() => void) | null) {
|
|
77
|
+
pc.onconnectionstatechange = handler ? () => handler() : null;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(wrapped, 'oniceconnectionstatechange', {
|
|
81
|
+
get: () => null,
|
|
82
|
+
set(handler: (() => void) | null) {
|
|
83
|
+
pc.oniceconnectionstatechange = handler ? () => handler() : null;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(wrapped, 'ontrack', {
|
|
87
|
+
get: () => null,
|
|
88
|
+
set(handler: PeerConnectionLike['ontrack']) {
|
|
89
|
+
pc.ontrack = handler
|
|
90
|
+
? (event: RTCTrackEvent) => {
|
|
91
|
+
handler({
|
|
92
|
+
streams: Array.from(event.streams) as unknown as MediaStreamLike[],
|
|
93
|
+
track: { kind: event.track.kind },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
: null;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(wrapped, 'ondatachannel', {
|
|
100
|
+
get: () => null,
|
|
101
|
+
set(handler: PeerConnectionLike['ondatachannel']) {
|
|
102
|
+
pc.ondatachannel = handler
|
|
103
|
+
? (event: RTCDataChannelEvent) => {
|
|
104
|
+
handler({ channel: wrapDataChannel(event.channel) });
|
|
105
|
+
}
|
|
106
|
+
: null;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return wrapped;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Browser `RTCPeerConnection` adapter for Web SDK. */
|
|
113
|
+
export function createBrowserWebRTCPort(): WebRTCPort {
|
|
114
|
+
if (
|
|
115
|
+
typeof RTCPeerConnection === 'undefined'
|
|
116
|
+
|| typeof navigator === 'undefined'
|
|
117
|
+
|| !navigator.mediaDevices?.getUserMedia
|
|
118
|
+
) {
|
|
119
|
+
throw new VoiceNotSupportedError(
|
|
120
|
+
'WebRTC is not available — use a browser with RTCPeerConnection support.',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
isSupported: () =>
|
|
126
|
+
typeof RTCPeerConnection !== 'undefined'
|
|
127
|
+
&& typeof navigator !== 'undefined'
|
|
128
|
+
&& !!navigator.mediaDevices?.getUserMedia,
|
|
129
|
+
|
|
130
|
+
getUserMedia: async (constraints) => {
|
|
131
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
132
|
+
return stream as unknown as MediaStreamLike;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
createPeerConnection: (config: { iceServers: IceServerConfig[] }) =>
|
|
136
|
+
wrapPeerConnection(
|
|
137
|
+
new RTCPeerConnection({
|
|
138
|
+
iceServers: config.iceServers as RTCIceServer[],
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
|
|
142
|
+
waitForIceGathering: async (pc, timeoutMs = ICE_GATHERING_TIMEOUT_MS) => {
|
|
143
|
+
const native = (pc as BrowserPeerConnectionLike).__nativePc;
|
|
144
|
+
if (native.iceGatheringState === 'complete') return;
|
|
145
|
+
await new Promise<void>((resolve) => {
|
|
146
|
+
let done = false;
|
|
147
|
+
const finish = () => {
|
|
148
|
+
if (done) return;
|
|
149
|
+
done = true;
|
|
150
|
+
native.removeEventListener('icegatheringstatechange', check);
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
const check = () => {
|
|
155
|
+
if (native.iceGatheringState === 'complete') finish();
|
|
156
|
+
};
|
|
157
|
+
native.addEventListener('icegatheringstatechange', check);
|
|
158
|
+
if (native.iceGatheringState === 'complete') finish();
|
|
159
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Message } from './ait-api';
|
|
2
|
+
|
|
3
|
+
export type ToolUse = {
|
|
4
|
+
name: string;
|
|
5
|
+
input: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type AgentEnvironment = 'production' | 'staging';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Core configuration shared by Web ChatBot and RN custom UI.
|
|
12
|
+
* UI-only fields belong in `@nxtlinq/chatbot-ui` / app layers.
|
|
13
|
+
*/
|
|
14
|
+
export interface AgentConfig {
|
|
15
|
+
serviceId: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
apiSecret: string;
|
|
18
|
+
environment?: AgentEnvironment;
|
|
19
|
+
|
|
20
|
+
onMessage?: (message: Message) => void;
|
|
21
|
+
onError?: (error: Error) => void;
|
|
22
|
+
onToolUse?: (
|
|
23
|
+
toolUse: ToolUse,
|
|
24
|
+
onProgress?: (update: {
|
|
25
|
+
status?: string;
|
|
26
|
+
progress?: number;
|
|
27
|
+
partialResult?: string;
|
|
28
|
+
steps?: string[];
|
|
29
|
+
partialContent?: string;
|
|
30
|
+
}) => void,
|
|
31
|
+
) => Promise<Message | void>;
|
|
32
|
+
|
|
33
|
+
customUserInfo?: Record<string, unknown>;
|
|
34
|
+
customUsername?: string;
|
|
35
|
+
permissionGroup?: string;
|
|
36
|
+
|
|
37
|
+
pseudoId?: string;
|
|
38
|
+
externalId?: string;
|
|
39
|
+
|
|
40
|
+
/** Agent model route, e.g. `open-ai`, `open-ai-stream`. */
|
|
41
|
+
defaultModel?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Voice pipeline: `cascade` (STT → LLM → TTS) or `realtime` (OpenAI Realtime).
|
|
45
|
+
* Passed to `/api/voice/session` when {@link NxtlinqAgent.startVoice} runs.
|
|
46
|
+
*/
|
|
47
|
+
voiceMode?: 'cascade' | 'realtime';
|
|
48
|
+
|
|
49
|
+
maxRetries?: number;
|
|
50
|
+
retryDelay?: number;
|
|
51
|
+
}
|