@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,48 @@
|
|
|
1
|
+
import type { CoreAITApi } from '../api/nxtlinq-api';
|
|
2
|
+
import type { Attachment, Message } from '../types/ait-api';
|
|
3
|
+
import type { AgentConfig } from '../types/agent-config';
|
|
4
|
+
export type SendMessageOptions = {
|
|
5
|
+
text: string;
|
|
6
|
+
attachments?: Attachment[];
|
|
7
|
+
/** Defaults to `config.defaultModel` or `open-ai`. */
|
|
8
|
+
model?: string;
|
|
9
|
+
/** Skip appending a user bubble (retries / preset messages). */
|
|
10
|
+
skipUserMessage?: boolean;
|
|
11
|
+
isRetry?: boolean;
|
|
12
|
+
clientPipelineTimings?: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Platform-agnostic text chat orchestration (no wallet UI, no TTS, no PII UI).
|
|
19
|
+
*/
|
|
20
|
+
export declare class ChatOrchestrator {
|
|
21
|
+
private readonly config;
|
|
22
|
+
private readonly api;
|
|
23
|
+
private readonly onUpdate;
|
|
24
|
+
private messages;
|
|
25
|
+
private isLoading;
|
|
26
|
+
constructor(config: AgentConfig, api: CoreAITApi, onUpdate: () => void);
|
|
27
|
+
getMessages(): Message[];
|
|
28
|
+
getIsLoading(): boolean;
|
|
29
|
+
setMessages(messages: Message[]): void;
|
|
30
|
+
private emit;
|
|
31
|
+
private requirePseudoId;
|
|
32
|
+
private resolveModel;
|
|
33
|
+
private patchMessage;
|
|
34
|
+
private appendMessage;
|
|
35
|
+
loadHistory(options?: {
|
|
36
|
+
last?: number;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Fetch the latest voice-turn rows from the server and merge by message id
|
|
40
|
+
* (same behavior as web SDK `refreshMessageHistory`).
|
|
41
|
+
*/
|
|
42
|
+
syncVoiceTurnHistory(options?: {
|
|
43
|
+
last?: number;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
sendMessage(options: SendMessageOptions): Promise<void>;
|
|
46
|
+
private handleToolCall;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ChatOrchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatOrchestrator.d.ts","sourceRoot":"","sources":["../../src/agent/ChatOrchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAOrD,OAAO,KAAK,EAAiB,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,uBAAuB,CAAC;AAGlE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrE,CAAC;AAUF;;GAEG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAN3B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,SAAS,CAAS;gBAGP,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,MAAM,IAAI;IAGvC,WAAW,IAAI,OAAO,EAAE;IAIxB,YAAY,IAAI,OAAO;IAIvB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,aAAa;IAMf,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB7D;;;OAGG;IACG,oBAAoB,CAAC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBhE,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;YAwL/C,cAAc;CAoE7B"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { appendServerHistoryIntoMessages, mapServerHistoryToMessages, VOICE_TURN_SYNC_LAST, } from '../history/messageHistory';
|
|
2
|
+
import { extractReplyText } from './extractReplyText';
|
|
3
|
+
/**
|
|
4
|
+
* Platform-agnostic text chat orchestration (no wallet UI, no TTS, no PII UI).
|
|
5
|
+
*/
|
|
6
|
+
export class ChatOrchestrator {
|
|
7
|
+
constructor(config, api, onUpdate) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.api = api;
|
|
10
|
+
this.onUpdate = onUpdate;
|
|
11
|
+
this.messages = [];
|
|
12
|
+
this.isLoading = false;
|
|
13
|
+
}
|
|
14
|
+
getMessages() {
|
|
15
|
+
return this.messages;
|
|
16
|
+
}
|
|
17
|
+
getIsLoading() {
|
|
18
|
+
return this.isLoading;
|
|
19
|
+
}
|
|
20
|
+
setMessages(messages) {
|
|
21
|
+
this.messages = messages;
|
|
22
|
+
this.onUpdate();
|
|
23
|
+
}
|
|
24
|
+
emit() {
|
|
25
|
+
this.onUpdate();
|
|
26
|
+
}
|
|
27
|
+
requirePseudoId() {
|
|
28
|
+
const id = this.config.pseudoId;
|
|
29
|
+
if (!id) {
|
|
30
|
+
throw new Error('pseudoId is required — set AgentConfig.pseudoId before chatting');
|
|
31
|
+
}
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
resolveModel(model) {
|
|
35
|
+
return model ?? this.config.defaultModel ?? 'open-ai';
|
|
36
|
+
}
|
|
37
|
+
patchMessage(id, patch) {
|
|
38
|
+
this.messages = this.messages.map((m) => m.id === id ? { ...m, ...patch } : m);
|
|
39
|
+
this.emit();
|
|
40
|
+
}
|
|
41
|
+
appendMessage(message) {
|
|
42
|
+
this.messages = [...this.messages, message];
|
|
43
|
+
this.config.onMessage?.(message);
|
|
44
|
+
this.emit();
|
|
45
|
+
}
|
|
46
|
+
async loadHistory(options) {
|
|
47
|
+
const pseudoId = this.requirePseudoId();
|
|
48
|
+
const result = await this.api.agent.getMessageHistory({
|
|
49
|
+
apiKey: this.config.apiKey,
|
|
50
|
+
apiSecret: this.config.apiSecret,
|
|
51
|
+
pseudoId,
|
|
52
|
+
externalId: this.config.externalId,
|
|
53
|
+
last: options?.last,
|
|
54
|
+
});
|
|
55
|
+
if (result && typeof result === 'object' && 'error' in result && result.error) {
|
|
56
|
+
throw new Error(String(result.error));
|
|
57
|
+
}
|
|
58
|
+
const rows = result.messages ?? [];
|
|
59
|
+
this.messages = mapServerHistoryToMessages(rows);
|
|
60
|
+
this.emit();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Fetch the latest voice-turn rows from the server and merge by message id
|
|
64
|
+
* (same behavior as web SDK `refreshMessageHistory`).
|
|
65
|
+
*/
|
|
66
|
+
async syncVoiceTurnHistory(options) {
|
|
67
|
+
const pseudoId = this.requirePseudoId();
|
|
68
|
+
const result = await this.api.agent.getMessageHistory({
|
|
69
|
+
apiKey: this.config.apiKey,
|
|
70
|
+
apiSecret: this.config.apiSecret,
|
|
71
|
+
pseudoId,
|
|
72
|
+
externalId: this.config.externalId,
|
|
73
|
+
last: options?.last ?? VOICE_TURN_SYNC_LAST,
|
|
74
|
+
});
|
|
75
|
+
if (result && typeof result === 'object' && 'error' in result && result.error) {
|
|
76
|
+
throw new Error(String(result.error));
|
|
77
|
+
}
|
|
78
|
+
const rows = result.messages ?? [];
|
|
79
|
+
this.messages = appendServerHistoryIntoMessages(this.messages, rows);
|
|
80
|
+
this.emit();
|
|
81
|
+
}
|
|
82
|
+
async sendMessage(options) {
|
|
83
|
+
const text = options.text?.trim() ?? '';
|
|
84
|
+
const hasContent = Boolean(text || (options.attachments && options.attachments.length > 0));
|
|
85
|
+
if (!hasContent || this.isLoading)
|
|
86
|
+
return;
|
|
87
|
+
const model = this.resolveModel(options.model);
|
|
88
|
+
const pseudoId = this.requirePseudoId();
|
|
89
|
+
if (!options.skipUserMessage && !options.isRetry) {
|
|
90
|
+
this.appendMessage({
|
|
91
|
+
id: `${Date.now()}`,
|
|
92
|
+
content: text
|
|
93
|
+
|| (options.attachments?.length
|
|
94
|
+
? `Uploaded ${options.attachments.length} file(s)`
|
|
95
|
+
: ''),
|
|
96
|
+
role: 'user',
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
attachments: options.attachments,
|
|
99
|
+
metadata: { model },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
this.isLoading = true;
|
|
103
|
+
this.emit();
|
|
104
|
+
const isStreamModel = model.endsWith('-stream');
|
|
105
|
+
let streamAssistantId = null;
|
|
106
|
+
let streamedReplyBuffer = '';
|
|
107
|
+
if (isStreamModel && !options.isRetry) {
|
|
108
|
+
streamAssistantId = `stream-assistant-${Date.now()}`;
|
|
109
|
+
this.appendMessage({
|
|
110
|
+
id: streamAssistantId,
|
|
111
|
+
role: 'assistant',
|
|
112
|
+
content: '',
|
|
113
|
+
partialContent: '',
|
|
114
|
+
isStreaming: true,
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
metadata: { model, agentLlmStream: true },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const apiAttachments = options.attachments?.map((att) => ({
|
|
121
|
+
type: att.type,
|
|
122
|
+
url: att.url,
|
|
123
|
+
name: att.name,
|
|
124
|
+
mimeType: att.mimeType,
|
|
125
|
+
}));
|
|
126
|
+
const response = (await this.api.agent.sendMessage({
|
|
127
|
+
model,
|
|
128
|
+
apiKey: this.config.apiKey,
|
|
129
|
+
apiSecret: this.config.apiSecret,
|
|
130
|
+
pseudoId,
|
|
131
|
+
externalId: this.config.externalId,
|
|
132
|
+
customUserInfo: this.config.customUserInfo,
|
|
133
|
+
customUsername: this.config.customUsername,
|
|
134
|
+
message: text
|
|
135
|
+
|| (options.attachments?.length
|
|
136
|
+
? `Uploaded ${options.attachments.length} file(s)`
|
|
137
|
+
: ''),
|
|
138
|
+
attachments: apiAttachments,
|
|
139
|
+
isRetry: options.isRetry,
|
|
140
|
+
...(options.clientPipelineTimings?.length
|
|
141
|
+
? { clientPipelineTimings: options.clientPipelineTimings }
|
|
142
|
+
: {}),
|
|
143
|
+
onStreamDelta: streamAssistantId
|
|
144
|
+
? (delta) => {
|
|
145
|
+
streamedReplyBuffer += delta;
|
|
146
|
+
const current = this.messages.find((m) => m.id === streamAssistantId);
|
|
147
|
+
this.patchMessage(streamAssistantId, {
|
|
148
|
+
partialContent: `${current?.partialContent ?? ''}${delta}`,
|
|
149
|
+
});
|
|
150
|
+
if (delta) {
|
|
151
|
+
this.isLoading = false;
|
|
152
|
+
this.emit();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
: undefined,
|
|
156
|
+
}));
|
|
157
|
+
const actualModel = response.model ?? model;
|
|
158
|
+
if (response.error || response.result === 'error') {
|
|
159
|
+
if (response.requiresWalletConnection || response.requiresPermissionUpdate) {
|
|
160
|
+
const msg = response.message
|
|
161
|
+
?? response.error
|
|
162
|
+
?? 'Additional setup is required before continuing.';
|
|
163
|
+
this.appendMessage({
|
|
164
|
+
id: `${Date.now()}-err`,
|
|
165
|
+
content: msg,
|
|
166
|
+
role: 'assistant',
|
|
167
|
+
timestamp: new Date().toISOString(),
|
|
168
|
+
metadata: {
|
|
169
|
+
model: actualModel,
|
|
170
|
+
toolName: response.toolName,
|
|
171
|
+
requiredPermission: response.requiredPermission,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(response.error ?? response.message ?? 'Failed to send message');
|
|
177
|
+
}
|
|
178
|
+
if (streamAssistantId && response.toolCall?.toolUse) {
|
|
179
|
+
this.messages = this.messages.filter((m) => m.id !== streamAssistantId);
|
|
180
|
+
streamAssistantId = null;
|
|
181
|
+
this.emit();
|
|
182
|
+
}
|
|
183
|
+
if (streamAssistantId && !response.toolCall?.toolUse) {
|
|
184
|
+
const replyText = extractReplyText(response, streamedReplyBuffer);
|
|
185
|
+
this.patchMessage(streamAssistantId, {
|
|
186
|
+
content: replyText,
|
|
187
|
+
isStreaming: false,
|
|
188
|
+
partialContent: undefined,
|
|
189
|
+
metadata: { model: actualModel, agentLlmStream: true },
|
|
190
|
+
piiProtection: response.piiProtection?.anonymizedReply
|
|
191
|
+
? {
|
|
192
|
+
anonymizedContent: response.piiProtection.anonymizedReply ?? undefined,
|
|
193
|
+
mapping: response.piiProtection.mapping ?? undefined,
|
|
194
|
+
}
|
|
195
|
+
: undefined,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (response.toolCall?.toolUse) {
|
|
200
|
+
await this.handleToolCall(response, actualModel);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const replyText = extractReplyText(response, streamedReplyBuffer);
|
|
204
|
+
this.appendMessage({
|
|
205
|
+
id: `${Date.now() + 1}`,
|
|
206
|
+
content: replyText,
|
|
207
|
+
role: 'assistant',
|
|
208
|
+
timestamp: new Date().toISOString(),
|
|
209
|
+
metadata: { model: actualModel },
|
|
210
|
+
piiProtection: response.piiProtection?.anonymizedReply
|
|
211
|
+
? {
|
|
212
|
+
anonymizedContent: response.piiProtection.anonymizedReply ?? undefined,
|
|
213
|
+
mapping: response.piiProtection.mapping ?? undefined,
|
|
214
|
+
}
|
|
215
|
+
: undefined,
|
|
216
|
+
});
|
|
217
|
+
if (response.piiProtection?.anonymizedUserMessage) {
|
|
218
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
219
|
+
if (this.messages[i].role === 'user') {
|
|
220
|
+
this.patchMessage(this.messages[i].id, {
|
|
221
|
+
piiProtection: {
|
|
222
|
+
anonymizedContent: response.piiProtection.anonymizedUserMessage,
|
|
223
|
+
mapping: response.piiProtection.mapping ?? undefined,
|
|
224
|
+
},
|
|
225
|
+
piiStatus: 'complete',
|
|
226
|
+
});
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
234
|
+
this.config.onError?.(err);
|
|
235
|
+
if (streamAssistantId) {
|
|
236
|
+
this.messages = this.messages.filter((m) => m.id !== streamAssistantId);
|
|
237
|
+
}
|
|
238
|
+
this.appendMessage({
|
|
239
|
+
id: `${Date.now()}-err`,
|
|
240
|
+
content: err.message,
|
|
241
|
+
role: 'assistant',
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
error: err.message,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
this.isLoading = false;
|
|
248
|
+
this.emit();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async handleToolCall(response, actualModel) {
|
|
252
|
+
const toolUse = response.toolCall.toolUse;
|
|
253
|
+
const streamingId = `streaming-${Date.now()}`;
|
|
254
|
+
this.appendMessage({
|
|
255
|
+
id: streamingId,
|
|
256
|
+
content: '',
|
|
257
|
+
role: 'assistant',
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
isStreaming: true,
|
|
260
|
+
streamingToolName: toolUse.name,
|
|
261
|
+
streamingStatus: `Starting ${toolUse.name}...`,
|
|
262
|
+
streamingProgress: 0,
|
|
263
|
+
metadata: { model: actualModel, toolUse },
|
|
264
|
+
});
|
|
265
|
+
let finalContent = `Tool ${toolUse.name} executed successfully`;
|
|
266
|
+
if (this.config.onToolUse) {
|
|
267
|
+
const toolResult = await this.config.onToolUse(toolUse, (update) => {
|
|
268
|
+
const current = this.messages.find((m) => m.id === streamingId);
|
|
269
|
+
this.patchMessage(streamingId, {
|
|
270
|
+
content: update.partialResult ?? current?.content ?? '',
|
|
271
|
+
partialContent: update.partialContent,
|
|
272
|
+
streamingProgress: update.progress,
|
|
273
|
+
streamingStatus: update.status,
|
|
274
|
+
streamingSteps: update.steps ?? current?.streamingSteps,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
if (toolResult?.content) {
|
|
278
|
+
finalContent = toolResult.content;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const replyText = extractReplyText(response);
|
|
282
|
+
if (replyText && replyText !== 'Sorry, I cannot understand your question') {
|
|
283
|
+
finalContent = `${finalContent}\n\n${replyText}`;
|
|
284
|
+
}
|
|
285
|
+
this.patchMessage(streamingId, {
|
|
286
|
+
content: finalContent,
|
|
287
|
+
isStreaming: false,
|
|
288
|
+
streamingProgress: 100,
|
|
289
|
+
metadata: { model: actualModel, toolUse },
|
|
290
|
+
piiProtection: response.piiProtection?.anonymizedReply
|
|
291
|
+
? {
|
|
292
|
+
anonymizedContent: response.piiProtection.anonymizedReply ?? undefined,
|
|
293
|
+
mapping: response.piiProtection.mapping ?? undefined,
|
|
294
|
+
}
|
|
295
|
+
: undefined,
|
|
296
|
+
});
|
|
297
|
+
if (response.assistantMessageId) {
|
|
298
|
+
try {
|
|
299
|
+
await this.api.agent.updateMessageContent({
|
|
300
|
+
apiKey: this.config.apiKey,
|
|
301
|
+
apiSecret: this.config.apiSecret,
|
|
302
|
+
messageId: response.assistantMessageId,
|
|
303
|
+
content: finalContent,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
/* best-effort */
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CoreAITApi } from '../api/nxtlinq-api';
|
|
2
|
+
import type { PlatformPorts } from '../ports/PlatformPorts';
|
|
3
|
+
import type { Message } from '../types/ait-api';
|
|
4
|
+
import type { AgentConfig } from '../types/agent-config';
|
|
5
|
+
import { type PostTextTtsResult } from '../api/tts';
|
|
6
|
+
import type { StartVoiceSessionOptions, VoiceGreetingOptions, VoiceSession, VoiceStatus, VoiceUserInputOptions } from '../voice/types';
|
|
7
|
+
import { type SendMessageOptions } from './ChatOrchestrator';
|
|
8
|
+
export type { SendMessageOptions };
|
|
9
|
+
export type NxtlinqAgentSnapshot = {
|
|
10
|
+
messages: Message[];
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
voiceStatus: VoiceStatus;
|
|
13
|
+
voiceSessionId: string | null;
|
|
14
|
+
};
|
|
15
|
+
type Listener = () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Platform-agnostic agent entry (text + voice via {@link PlatformPorts.webrtc}).
|
|
18
|
+
*/
|
|
19
|
+
export declare class NxtlinqAgent {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly ports;
|
|
22
|
+
readonly api: CoreAITApi;
|
|
23
|
+
private readonly chat;
|
|
24
|
+
private voiceStatus;
|
|
25
|
+
private voiceSession;
|
|
26
|
+
private voiceStartInFlight;
|
|
27
|
+
private voiceHistoryRefreshInFlight;
|
|
28
|
+
private readonly listeners;
|
|
29
|
+
private snapshotCache;
|
|
30
|
+
constructor(config: AgentConfig, ports: PlatformPorts);
|
|
31
|
+
getSnapshot(): NxtlinqAgentSnapshot;
|
|
32
|
+
getVoiceSession(): VoiceSession | null;
|
|
33
|
+
subscribe(listener: Listener): () => void;
|
|
34
|
+
private emit;
|
|
35
|
+
setMessages(messages: Message[]): void;
|
|
36
|
+
loadHistory(options?: {
|
|
37
|
+
last?: number;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/** Merge latest persisted voice-turn messages (default last 2) by message id. */
|
|
40
|
+
syncVoiceTurnHistory(options?: {
|
|
41
|
+
last?: number;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
private refreshVoiceTurnHistory;
|
|
44
|
+
private finishVoiceTurnSync;
|
|
45
|
+
private shouldSyncVoiceTurnFromDone;
|
|
46
|
+
sendMessage(text: string, options?: Omit<SendMessageOptions, 'text'>): Promise<void>;
|
|
47
|
+
startVoice(options?: Partial<StartVoiceSessionOptions>): Promise<VoiceSession>;
|
|
48
|
+
stopVoice(reason?: string): Promise<void>;
|
|
49
|
+
/** Berify-compatible text TTS (`POST /api/tts/openai`). */
|
|
50
|
+
postTextTts(text: string): Promise<PostTextTtsResult>;
|
|
51
|
+
/** Data URI for RN `Video` / web `<audio>` playback. */
|
|
52
|
+
buildTextTtsPlaybackUri(result: PostTextTtsResult): string;
|
|
53
|
+
getOutputAudioLevel(): number;
|
|
54
|
+
sendVoiceUserInput(options: VoiceUserInputOptions): void;
|
|
55
|
+
/**
|
|
56
|
+
* Auto-greet during voice mode (synthetic user turn + optional product image).
|
|
57
|
+
* Waits for app data channel when `waitForChannel` is true (default).
|
|
58
|
+
*/
|
|
59
|
+
triggerVoiceGreeting(options: VoiceGreetingOptions, greetOptions?: {
|
|
60
|
+
waitForChannel?: boolean;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
destroy(): void;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=NxtlinqAgent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NxtlinqAgent.d.ts","sourceRoot":"","sources":["../../src/agent/NxtlinqAgent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createNxtlinqApiWithDeps } from '../api/nxtlinq-api';
|
|
2
|
+
import { getAiAgentApiHost, setApiHosts } from '../api/hosts';
|
|
3
|
+
import { STORAGE_KEYS } from '../constants/storageKeys';
|
|
4
|
+
import { VoiceNotSupportedError } from '../ports/WebRTCPort';
|
|
5
|
+
import { buildTextTtsDataUri } from '../api/tts';
|
|
6
|
+
import { startVoiceSessionWithPort } from '../voice/start-voice-session';
|
|
7
|
+
import { triggerVoiceGreeting as runVoiceGreeting } from '../voice/trigger-voice-greeting';
|
|
8
|
+
import { ChatOrchestrator } from './ChatOrchestrator';
|
|
9
|
+
/**
|
|
10
|
+
* Platform-agnostic agent entry (text + voice via {@link PlatformPorts.webrtc}).
|
|
11
|
+
*/
|
|
12
|
+
export class NxtlinqAgent {
|
|
13
|
+
constructor(config, ports) {
|
|
14
|
+
this.voiceStatus = 'idle';
|
|
15
|
+
this.voiceSession = null;
|
|
16
|
+
this.voiceStartInFlight = null;
|
|
17
|
+
this.voiceHistoryRefreshInFlight = null;
|
|
18
|
+
this.listeners = new Set();
|
|
19
|
+
this.snapshotCache = null;
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.ports = ports;
|
|
22
|
+
if (config.environment) {
|
|
23
|
+
setApiHosts(config.environment);
|
|
24
|
+
}
|
|
25
|
+
this.api = createNxtlinqApiWithDeps(config.apiKey, config.apiSecret, {
|
|
26
|
+
storage: ports.storage,
|
|
27
|
+
http: ports.http,
|
|
28
|
+
getTimezone: ports.getTimezone,
|
|
29
|
+
});
|
|
30
|
+
this.chat = new ChatOrchestrator(config, this.api, () => this.emit());
|
|
31
|
+
}
|
|
32
|
+
getSnapshot() {
|
|
33
|
+
if (this.snapshotCache) {
|
|
34
|
+
return this.snapshotCache;
|
|
35
|
+
}
|
|
36
|
+
this.snapshotCache = {
|
|
37
|
+
messages: this.chat.getMessages(),
|
|
38
|
+
isLoading: this.chat.getIsLoading(),
|
|
39
|
+
voiceStatus: this.voiceStatus,
|
|
40
|
+
voiceSessionId: this.voiceSession?.id ?? null,
|
|
41
|
+
};
|
|
42
|
+
return this.snapshotCache;
|
|
43
|
+
}
|
|
44
|
+
getVoiceSession() {
|
|
45
|
+
return this.voiceSession;
|
|
46
|
+
}
|
|
47
|
+
subscribe(listener) {
|
|
48
|
+
this.listeners.add(listener);
|
|
49
|
+
return () => this.listeners.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
emit() {
|
|
52
|
+
this.snapshotCache = null;
|
|
53
|
+
for (const listener of this.listeners) {
|
|
54
|
+
listener();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
setMessages(messages) {
|
|
58
|
+
this.chat.setMessages(messages);
|
|
59
|
+
}
|
|
60
|
+
async loadHistory(options) {
|
|
61
|
+
return this.chat.loadHistory(options);
|
|
62
|
+
}
|
|
63
|
+
/** Merge latest persisted voice-turn messages (default last 2) by message id. */
|
|
64
|
+
async syncVoiceTurnHistory(options) {
|
|
65
|
+
return this.chat.syncVoiceTurnHistory(options);
|
|
66
|
+
}
|
|
67
|
+
refreshVoiceTurnHistory() {
|
|
68
|
+
if (this.voiceHistoryRefreshInFlight) {
|
|
69
|
+
return this.voiceHistoryRefreshInFlight;
|
|
70
|
+
}
|
|
71
|
+
const run = this.syncVoiceTurnHistory()
|
|
72
|
+
.catch((err) => {
|
|
73
|
+
this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
74
|
+
})
|
|
75
|
+
.finally(() => {
|
|
76
|
+
this.voiceHistoryRefreshInFlight = null;
|
|
77
|
+
});
|
|
78
|
+
this.voiceHistoryRefreshInFlight = run;
|
|
79
|
+
return run;
|
|
80
|
+
}
|
|
81
|
+
finishVoiceTurnSync() {
|
|
82
|
+
void this.refreshVoiceTurnHistory();
|
|
83
|
+
}
|
|
84
|
+
shouldSyncVoiceTurnFromDone(event) {
|
|
85
|
+
return !event.guardrailsBlocked && !event.billingBlocked && !event.error;
|
|
86
|
+
}
|
|
87
|
+
async sendMessage(text, options) {
|
|
88
|
+
return this.chat.sendMessage({ text, ...options });
|
|
89
|
+
}
|
|
90
|
+
async startVoice(options) {
|
|
91
|
+
if (this.voiceSession) {
|
|
92
|
+
return this.voiceSession;
|
|
93
|
+
}
|
|
94
|
+
if (this.voiceStartInFlight) {
|
|
95
|
+
return this.voiceStartInFlight;
|
|
96
|
+
}
|
|
97
|
+
if (!this.ports.webrtc?.isSupported()) {
|
|
98
|
+
throw new VoiceNotSupportedError();
|
|
99
|
+
}
|
|
100
|
+
const pseudoId = this.config.pseudoId;
|
|
101
|
+
if (!pseudoId) {
|
|
102
|
+
throw new Error('pseudoId is required — set AgentConfig.pseudoId before startVoice');
|
|
103
|
+
}
|
|
104
|
+
const walletAddress = await this.ports.storage.getItem(STORAGE_KEYS.WALLET_ADDRESS);
|
|
105
|
+
const aitTokenRaw = await this.ports.storage.getItem(STORAGE_KEYS.AIT_SERVICE_ACCESS_TOKEN);
|
|
106
|
+
let aitToken;
|
|
107
|
+
if (aitTokenRaw) {
|
|
108
|
+
try {
|
|
109
|
+
aitToken = JSON.parse(aitTokenRaw);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
aitToken = aitTokenRaw;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const merged = {
|
|
116
|
+
apiKey: this.config.apiKey,
|
|
117
|
+
apiSecret: this.config.apiSecret,
|
|
118
|
+
pseudoId,
|
|
119
|
+
externalId: this.config.externalId,
|
|
120
|
+
walletAddress: walletAddress ?? undefined,
|
|
121
|
+
aitToken,
|
|
122
|
+
metadata: options?.metadata ?? this.config.customUserInfo,
|
|
123
|
+
voiceMode: options?.voiceMode ?? this.config.voiceMode,
|
|
124
|
+
...options,
|
|
125
|
+
onOpen: () => {
|
|
126
|
+
this.voiceStatus = 'listening';
|
|
127
|
+
this.emit();
|
|
128
|
+
options?.onOpen?.();
|
|
129
|
+
},
|
|
130
|
+
onStatus: (status, ts) => {
|
|
131
|
+
const prev = this.voiceStatus;
|
|
132
|
+
this.voiceStatus = status;
|
|
133
|
+
this.emit();
|
|
134
|
+
if (status === 'listening' && prev === 'speaking') {
|
|
135
|
+
this.finishVoiceTurnSync();
|
|
136
|
+
}
|
|
137
|
+
options?.onStatus?.(status, ts);
|
|
138
|
+
},
|
|
139
|
+
onDone: (event) => {
|
|
140
|
+
if (this.shouldSyncVoiceTurnFromDone(event)) {
|
|
141
|
+
this.finishVoiceTurnSync();
|
|
142
|
+
}
|
|
143
|
+
options?.onDone?.(event);
|
|
144
|
+
},
|
|
145
|
+
onClose: (reason) => {
|
|
146
|
+
const session = this.voiceSession;
|
|
147
|
+
this.voiceStatus = 'idle';
|
|
148
|
+
this.voiceSession = null;
|
|
149
|
+
this.emit();
|
|
150
|
+
if (session &&
|
|
151
|
+
reason !== 'peerconnection_closed' &&
|
|
152
|
+
reason !== 'datachannel_closed') {
|
|
153
|
+
void session.stop(typeof reason === 'string' ? reason : 'channel_closed');
|
|
154
|
+
}
|
|
155
|
+
options?.onClose?.(reason);
|
|
156
|
+
},
|
|
157
|
+
onError: (err) => {
|
|
158
|
+
const session = this.voiceSession;
|
|
159
|
+
this.voiceStatus = 'idle';
|
|
160
|
+
this.voiceSession = null;
|
|
161
|
+
this.emit();
|
|
162
|
+
if (session) {
|
|
163
|
+
void session.stop('voice_error');
|
|
164
|
+
}
|
|
165
|
+
this.config.onError?.(err);
|
|
166
|
+
options?.onError?.(err);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
this.voiceStartInFlight = startVoiceSessionWithPort({
|
|
170
|
+
baseUrl: getAiAgentApiHost(),
|
|
171
|
+
webrtc: this.ports.webrtc,
|
|
172
|
+
fetchFn: this.ports.http.fetch,
|
|
173
|
+
}, merged).then((session) => {
|
|
174
|
+
this.voiceSession = session;
|
|
175
|
+
this.emit();
|
|
176
|
+
return session;
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
return await this.voiceStartInFlight;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
this.voiceStartInFlight = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async stopVoice(reason) {
|
|
186
|
+
const session = this.voiceSession;
|
|
187
|
+
this.voiceSession = null;
|
|
188
|
+
this.voiceStatus = 'idle';
|
|
189
|
+
this.emit();
|
|
190
|
+
if (session) {
|
|
191
|
+
await session.stop(reason ?? 'client_stop');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Berify-compatible text TTS (`POST /api/tts/openai`). */
|
|
195
|
+
async postTextTts(text) {
|
|
196
|
+
return this.api.tts.postTextTts(text);
|
|
197
|
+
}
|
|
198
|
+
/** Data URI for RN `Video` / web `<audio>` playback. */
|
|
199
|
+
buildTextTtsPlaybackUri(result) {
|
|
200
|
+
return buildTextTtsDataUri(result.audio, result.mimeType);
|
|
201
|
+
}
|
|
202
|
+
getOutputAudioLevel() {
|
|
203
|
+
return this.voiceSession?.getOutputAudioLevel() ?? 0;
|
|
204
|
+
}
|
|
205
|
+
sendVoiceUserInput(options) {
|
|
206
|
+
const session = this.voiceSession;
|
|
207
|
+
if (!session) {
|
|
208
|
+
throw new Error('No active voice session — call startVoice() first');
|
|
209
|
+
}
|
|
210
|
+
session.sendVoiceUserInput(options);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Auto-greet during voice mode (synthetic user turn + optional product image).
|
|
214
|
+
* Waits for app data channel when `waitForChannel` is true (default).
|
|
215
|
+
*/
|
|
216
|
+
async triggerVoiceGreeting(options, greetOptions) {
|
|
217
|
+
const session = this.voiceSession;
|
|
218
|
+
if (!session) {
|
|
219
|
+
throw new Error('No active voice session — call startVoice() first');
|
|
220
|
+
}
|
|
221
|
+
const waitForChannel = greetOptions?.waitForChannel !== false;
|
|
222
|
+
const timeoutMs = greetOptions?.timeoutMs ?? 8000;
|
|
223
|
+
if (waitForChannel && !session.isAppChannelOpen()) {
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
const started = Date.now();
|
|
226
|
+
const tick = () => {
|
|
227
|
+
if (session.isAppChannelOpen()) {
|
|
228
|
+
resolve();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (Date.now() - started >= timeoutMs) {
|
|
232
|
+
reject(new Error('Voice app channel did not open in time'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
setTimeout(tick, 50);
|
|
236
|
+
};
|
|
237
|
+
tick();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const appendLocal = options.skipUserMessage
|
|
241
|
+
? undefined
|
|
242
|
+
: (message) => {
|
|
243
|
+
const existing = this.chat.getMessages();
|
|
244
|
+
this.chat.setMessages([...existing, message]);
|
|
245
|
+
};
|
|
246
|
+
runVoiceGreeting({
|
|
247
|
+
session,
|
|
248
|
+
options,
|
|
249
|
+
appendLocalUserMessage: appendLocal,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
destroy() {
|
|
253
|
+
void this.stopVoice('destroy');
|
|
254
|
+
this.listeners.clear();
|
|
255
|
+
}
|
|
256
|
+
}
|