@applica-software-guru/persona-sdk 0.0.1-preview0
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/.eslintrc.cjs +11 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +8 -0
- package/README.md +66 -0
- package/bitbucket-pipelines.yml +29 -0
- package/dist/bundle.cjs.js +27 -0
- package/dist/bundle.cjs.js.map +1 -0
- package/dist/bundle.es.js +6585 -0
- package/dist/bundle.es.js.map +1 -0
- package/dist/bundle.iife.js +27 -0
- package/dist/bundle.iife.js.map +1 -0
- package/dist/bundle.umd.js +27 -0
- package/dist/bundle.umd.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/logging.d.ts +18 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/messages.d.ts +7 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/protocol/base.d.ts +23 -0
- package/dist/protocol/base.d.ts.map +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/rest.d.ts +22 -0
- package/dist/protocol/rest.d.ts.map +1 -0
- package/dist/protocol/webrtc.d.ts +56 -0
- package/dist/protocol/webrtc.d.ts.map +1 -0
- package/dist/protocol/websocket.d.ts +22 -0
- package/dist/protocol/websocket.d.ts.map +1 -0
- package/dist/runtime.d.ts +21 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/jsconfig.node.json +10 -0
- package/package.json +72 -0
- package/playground/index.html +14 -0
- package/playground/src/app.tsx +10 -0
- package/playground/src/chat.tsx +52 -0
- package/playground/src/components/assistant-ui/assistant-modal.tsx +57 -0
- package/playground/src/components/assistant-ui/markdown-text.tsx +119 -0
- package/playground/src/components/assistant-ui/thread-list.tsx +62 -0
- package/playground/src/components/assistant-ui/thread.tsx +249 -0
- package/playground/src/components/assistant-ui/tool-fallback.tsx +33 -0
- package/playground/src/components/assistant-ui/tooltip-icon-button.tsx +38 -0
- package/playground/src/components/ui/avatar.tsx +35 -0
- package/playground/src/components/ui/button.tsx +43 -0
- package/playground/src/components/ui/tooltip.tsx +32 -0
- package/playground/src/lib/utils.ts +6 -0
- package/playground/src/main.tsx +10 -0
- package/playground/src/styles.css +1 -0
- package/playground/src/vite-env.d.ts +1 -0
- package/preview.sh +13 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +34 -0
- package/src/messages.ts +79 -0
- package/src/protocol/base.ts +55 -0
- package/src/protocol/index.ts +4 -0
- package/src/protocol/rest.ts +64 -0
- package/src/protocol/webrtc.ts +337 -0
- package/src/protocol/websocket.ts +106 -0
- package/src/runtime.tsx +214 -0
- package/src/types.ts +98 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +15 -0
- package/vite.config.ts +69 -0
package/src/logging.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface PersonaLogger {
|
|
2
|
+
log: (message: string, ...args: unknown[]) => void;
|
|
3
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
4
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
5
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
6
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class PersonaConsoleLogger implements PersonaLogger {
|
|
10
|
+
prefix = '[Persona]';
|
|
11
|
+
|
|
12
|
+
log(message: string, ...args: unknown[]) {
|
|
13
|
+
console.log(`${this.prefix} - ${message}`, ...args);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
info(message: string, ...args: unknown[]) {
|
|
17
|
+
console.info(`${this.prefix} - ${message}`, ...args);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
warn(message: string, ...args: unknown[]) {
|
|
21
|
+
console.warn(`${this.prefix} - ${message}`, ...args);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
error(message: string, ...args: unknown[]) {
|
|
25
|
+
console.error(`${this.prefix} - ${message}`, ...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
debug(message: string, ...args: unknown[]) {
|
|
29
|
+
console.debug(`${this.prefix} - ${message}`, ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { PersonaConsoleLogger };
|
|
34
|
+
export type { PersonaLogger };
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { PersonaMessage } from './types';
|
|
2
|
+
import { ThreadMessageLike } from '@assistant-ui/react';
|
|
3
|
+
|
|
4
|
+
function removeEmptyMessages(messages: PersonaMessage[]): PersonaMessage[] {
|
|
5
|
+
return messages.filter((message) => {
|
|
6
|
+
if (message.finishReason === 'stop') {
|
|
7
|
+
return message.text !== null && message.text?.trim() !== '';
|
|
8
|
+
}
|
|
9
|
+
return true;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function parseMessages(messages: PersonaMessage[]): PersonaMessage[] {
|
|
13
|
+
const outputMessages: PersonaMessage[] = [];
|
|
14
|
+
let currentMessage: PersonaMessage | null = null;
|
|
15
|
+
|
|
16
|
+
for (const message of messages) {
|
|
17
|
+
if (message.type === 'reasoning') {
|
|
18
|
+
if (currentMessage != null) {
|
|
19
|
+
outputMessages.push(currentMessage);
|
|
20
|
+
currentMessage = null;
|
|
21
|
+
}
|
|
22
|
+
outputMessages.push(message);
|
|
23
|
+
} else if (message.functionCalls) {
|
|
24
|
+
if (currentMessage) {
|
|
25
|
+
outputMessages.push(currentMessage);
|
|
26
|
+
}
|
|
27
|
+
outputMessages.push(message);
|
|
28
|
+
currentMessage = null;
|
|
29
|
+
} else if (message.functionResponse) {
|
|
30
|
+
outputMessages[outputMessages.length - 1] = {
|
|
31
|
+
...outputMessages[outputMessages.length - 1],
|
|
32
|
+
functionResponse: message.functionResponse,
|
|
33
|
+
};
|
|
34
|
+
} else if (
|
|
35
|
+
currentMessage &&
|
|
36
|
+
message.protocol === currentMessage.protocol &&
|
|
37
|
+
(currentMessage.role === message.role || message.finishReason === 'stop')
|
|
38
|
+
) {
|
|
39
|
+
currentMessage.text += message.text;
|
|
40
|
+
} else {
|
|
41
|
+
if (currentMessage) {
|
|
42
|
+
outputMessages.push(currentMessage);
|
|
43
|
+
}
|
|
44
|
+
currentMessage = {
|
|
45
|
+
...message,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (currentMessage) {
|
|
51
|
+
outputMessages.push(currentMessage);
|
|
52
|
+
}
|
|
53
|
+
return removeEmptyMessages(outputMessages);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function convertMessage(message: PersonaMessage): ThreadMessageLike {
|
|
57
|
+
if (message.role === 'function') {
|
|
58
|
+
return {
|
|
59
|
+
id: message.id!,
|
|
60
|
+
role: 'assistant',
|
|
61
|
+
status: message?.functionResponse === null ? { type: 'running' } : { type: 'complete', reason: 'stop' },
|
|
62
|
+
content:
|
|
63
|
+
message.functionCalls?.map((call) => ({
|
|
64
|
+
type: 'tool-call',
|
|
65
|
+
toolName: call.name,
|
|
66
|
+
toolCallId: call.id,
|
|
67
|
+
args: call.args,
|
|
68
|
+
result: message.functionResponse?.result,
|
|
69
|
+
})) ?? [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
id: message.id!,
|
|
74
|
+
role: message.role,
|
|
75
|
+
content: message.type === 'reasoning' ? [{ type: 'reasoning', text: message.text }] : [{ type: 'text', text: message.text }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { parseMessages, convertMessage, removeEmptyMessages };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Message, MessageListenerCallback, PersonaMessage, PersonaProtocol, ProtocolStatus, Session, StatusChangeCallback } from '../types';
|
|
2
|
+
|
|
3
|
+
abstract class PersonaProtocolBase implements PersonaProtocol {
|
|
4
|
+
abstract status: ProtocolStatus;
|
|
5
|
+
abstract session: Session;
|
|
6
|
+
abstract autostart: boolean;
|
|
7
|
+
|
|
8
|
+
private statusChangeCallbacks: StatusChangeCallback[] = [];
|
|
9
|
+
private messageCallbacks: MessageListenerCallback[] = [];
|
|
10
|
+
|
|
11
|
+
public addStatusChangeListener(callback: StatusChangeCallback) {
|
|
12
|
+
this.statusChangeCallbacks.push(callback);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public addMessageListener(callback: MessageListenerCallback) {
|
|
16
|
+
this.messageCallbacks.push(callback);
|
|
17
|
+
}
|
|
18
|
+
public async syncSession(session: Session): Promise<void> {
|
|
19
|
+
this.session = session;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public async notifyMessage(message: PersonaMessage): Promise<void> {
|
|
23
|
+
this.messageCallbacks.forEach((callback) => callback(message));
|
|
24
|
+
}
|
|
25
|
+
public async notifyMessages(messages: PersonaMessage[]): Promise<void> {
|
|
26
|
+
messages.forEach((message) => {
|
|
27
|
+
this.messageCallbacks.forEach((callback) => callback(message));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async setSession(session: Session): Promise<void> {
|
|
32
|
+
this.session = session;
|
|
33
|
+
}
|
|
34
|
+
public async setStatus(status: ProtocolStatus): Promise<void> {
|
|
35
|
+
const notify = this.status !== status;
|
|
36
|
+
this.status = status;
|
|
37
|
+
if (!notify) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.statusChangeCallbacks.forEach((callback) => callback(status));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public clearListeners(): void {
|
|
44
|
+
this.statusChangeCallbacks = [];
|
|
45
|
+
this.messageCallbacks = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
abstract getName(): string;
|
|
49
|
+
abstract getPriority(): number;
|
|
50
|
+
abstract connect(session?: Session): Promise<Session>;
|
|
51
|
+
abstract disconnect(): Promise<void>;
|
|
52
|
+
abstract send(message: Message): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { PersonaProtocolBase };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { PersonaProtocolBase } from './base';
|
|
2
|
+
import { Message, PersonaResponse, Session, ProtocolStatus, PersonaProtocolBaseConfig } from '../types';
|
|
3
|
+
|
|
4
|
+
type PersonaRESTProtocolConfig = PersonaProtocolBaseConfig & {
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
class PersonaRESTProtocol extends PersonaProtocolBase {
|
|
9
|
+
status: ProtocolStatus;
|
|
10
|
+
autostart: boolean;
|
|
11
|
+
session: Session;
|
|
12
|
+
config: PersonaRESTProtocolConfig;
|
|
13
|
+
notify: boolean = true;
|
|
14
|
+
|
|
15
|
+
constructor(config: PersonaRESTProtocolConfig) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.status = 'disconnected';
|
|
19
|
+
this.autostart = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getName(): string {
|
|
23
|
+
return 'rest';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getPriority(): number {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async connect(session: Session): Promise<Session> {
|
|
31
|
+
this.setStatus('connected');
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async disconnect(): Promise<void> {
|
|
36
|
+
this.setStatus('disconnected');
|
|
37
|
+
this.session = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async syncSession(session: Session): Promise<void> {
|
|
41
|
+
this.session = session;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async send(message: Message): Promise<void> {
|
|
45
|
+
const { apiUrl, apiKey, agentId } = this.config;
|
|
46
|
+
const sessionId = this.session ?? 'new';
|
|
47
|
+
const input = message;
|
|
48
|
+
|
|
49
|
+
const response = await fetch(`${apiUrl}/agents/${agentId}/sessions/${sessionId}/messages`, {
|
|
50
|
+
body: JSON.stringify({ userMessage: input }),
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'x-fox-apikey': apiKey,
|
|
55
|
+
'x-persona-apikey': apiKey,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const personaResponse = (await response.json()) as PersonaResponse;
|
|
59
|
+
this.notifyMessages(personaResponse.response.messages);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { PersonaRESTProtocol };
|
|
64
|
+
export type { PersonaRESTProtocolConfig };
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { PersonaProtocolBase } from './base';
|
|
2
|
+
import { Message, PersonaMessage, PersonaProtocolBaseConfig, ProtocolStatus, Session } from '../types';
|
|
3
|
+
|
|
4
|
+
type AudioAnalysisData = {
|
|
5
|
+
localAmplitude: number;
|
|
6
|
+
remoteAmplitude: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type AudioVisualizerCallback = (data: AudioAnalysisData) => void;
|
|
10
|
+
|
|
11
|
+
type PersonaWebRTCMessageCallback = (data: MessageEvent) => void;
|
|
12
|
+
|
|
13
|
+
type PersonaWebRTCConfig = PersonaProtocolBaseConfig & {
|
|
14
|
+
webrtcUrl: string;
|
|
15
|
+
iceServers?: RTCIceServer[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class PersonaWebRTCClient {
|
|
19
|
+
private config: PersonaWebRTCConfig;
|
|
20
|
+
private pc: RTCPeerConnection | null = null;
|
|
21
|
+
private ws: WebSocket | null = null;
|
|
22
|
+
private localStream: MediaStream | null = null;
|
|
23
|
+
private remoteStream: MediaStream = new MediaStream();
|
|
24
|
+
private audioCtx: AudioContext | null = null;
|
|
25
|
+
|
|
26
|
+
private localAnalyser: AnalyserNode | null = null;
|
|
27
|
+
private remoteAnalyser: AnalyserNode | null = null;
|
|
28
|
+
private analyzerFrame: number | null = null;
|
|
29
|
+
private dataChannel: RTCDataChannel | null = null;
|
|
30
|
+
|
|
31
|
+
private isConnected: boolean = false;
|
|
32
|
+
private visualizerCallbacks: AudioVisualizerCallback[] = [];
|
|
33
|
+
private messageCallbacks: PersonaWebRTCMessageCallback[] = [];
|
|
34
|
+
|
|
35
|
+
constructor(config: PersonaWebRTCConfig) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async connect(session: Session): Promise<Session> {
|
|
40
|
+
if (this.isConnected) return;
|
|
41
|
+
|
|
42
|
+
this.isConnected = true;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
this.config.logger?.error('Error accessing microphone:', err);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.pc = new RTCPeerConnection({
|
|
52
|
+
iceServers: this.config.iceServers || [
|
|
53
|
+
{
|
|
54
|
+
urls: 'stun:34.38.108.251:3478',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
urls: 'turn:34.38.108.251:3478',
|
|
58
|
+
username: 'webrtc',
|
|
59
|
+
credential: 'webrtc',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.localStream.getTracks().forEach((track) => {
|
|
65
|
+
this.pc!.addTrack(track, this.localStream!);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.pc.ontrack = (event) => {
|
|
69
|
+
event.streams[0].getTracks().forEach((track) => {
|
|
70
|
+
this.remoteStream.addTrack(track);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!this.audioCtx) {
|
|
74
|
+
this._startAnalyzers();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const remoteAudio = new Audio();
|
|
78
|
+
remoteAudio.srcObject = this.remoteStream;
|
|
79
|
+
remoteAudio.play().catch((e) => {
|
|
80
|
+
this.config.logger?.error('Error playing remote audio:', e);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
this.pc.onicecandidate = (event) => {
|
|
85
|
+
if (event.candidate && this.ws?.readyState === WebSocket.OPEN) {
|
|
86
|
+
this.ws.send(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
type: 'CANDIDATE',
|
|
89
|
+
src: 'client',
|
|
90
|
+
payload: { candidate: event.candidate },
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.pc.ondatachannel = (event) => {
|
|
97
|
+
const channel = event.channel;
|
|
98
|
+
channel.onmessage = (msg) => {
|
|
99
|
+
this.messageCallbacks.forEach((callback) => {
|
|
100
|
+
callback(msg);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.ws = new WebSocket(this.config.webrtcUrl || 'wss://persona.applica.guru/api/webrtc');
|
|
106
|
+
this.ws.onopen = async () => {
|
|
107
|
+
const offer = await this.pc!.createOffer();
|
|
108
|
+
await this.pc!.setLocalDescription(offer);
|
|
109
|
+
|
|
110
|
+
const metadata = {
|
|
111
|
+
agentId: this.config.agentId,
|
|
112
|
+
sessionCode: session as string,
|
|
113
|
+
};
|
|
114
|
+
this.config.logger?.debug('Opening connection to WebRTC server: ', metadata);
|
|
115
|
+
|
|
116
|
+
const offerMessage = {
|
|
117
|
+
type: 'OFFER',
|
|
118
|
+
src: crypto.randomUUID?.() || 'client_' + Date.now(),
|
|
119
|
+
payload: {
|
|
120
|
+
sdp: {
|
|
121
|
+
sdp: offer.sdp,
|
|
122
|
+
type: offer.type,
|
|
123
|
+
},
|
|
124
|
+
connectionId: (Date.now() % 1000000).toString(),
|
|
125
|
+
metadata,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
this.ws!.send(JSON.stringify(offerMessage));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this.ws.onmessage = async (event) => {
|
|
133
|
+
const data = JSON.parse(event.data);
|
|
134
|
+
if (data.type === 'ANSWER') {
|
|
135
|
+
await this.pc!.setRemoteDescription(new RTCSessionDescription(data.payload.sdp));
|
|
136
|
+
} else if (data.type === 'CANDIDATE') {
|
|
137
|
+
try {
|
|
138
|
+
await this.pc!.addIceCandidate(new RTCIceCandidate(data.payload.candidate));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.config.logger?.error('Error adding ICE candidate:', err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.ws.onclose = () => {
|
|
146
|
+
this._stopAnalyzers();
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async disconnect(): Promise<void> {
|
|
151
|
+
if (!this.isConnected) return;
|
|
152
|
+
|
|
153
|
+
this.isConnected = false;
|
|
154
|
+
|
|
155
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.close();
|
|
156
|
+
if (this.pc) this.pc.close();
|
|
157
|
+
if (this.localStream) {
|
|
158
|
+
this.localStream.getTracks().forEach((track) => track.stop());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.remoteStream = new MediaStream();
|
|
162
|
+
if (this.audioCtx) {
|
|
163
|
+
await this.audioCtx.close();
|
|
164
|
+
this.audioCtx = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._stopAnalyzers();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public addVisualizerCallback(callback: AudioVisualizerCallback): void {
|
|
171
|
+
this.visualizerCallbacks.push(callback);
|
|
172
|
+
}
|
|
173
|
+
public addMessageCallback(callback: PersonaWebRTCMessageCallback): void {
|
|
174
|
+
this.messageCallbacks.push(callback);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public createDataChannel(label = 'messages'): void {
|
|
178
|
+
if (!this.pc) return;
|
|
179
|
+
this.dataChannel = this.pc.createDataChannel(label);
|
|
180
|
+
this.dataChannel.onopen = () => this.config.logger?.info('Data channel opened');
|
|
181
|
+
this.dataChannel.onmessage = (msg: MessageEvent) => {
|
|
182
|
+
this.messageCallbacks.forEach((callback) => {
|
|
183
|
+
callback(msg);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public sendMessage(message: string): void {
|
|
189
|
+
if (!this.dataChannel) {
|
|
190
|
+
this.config.logger?.warn('Data channel is not open, cannot send message');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.dataChannel.send(message);
|
|
195
|
+
this.config.logger?.info('Sent message:', message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _startAnalyzers(): void {
|
|
199
|
+
if (!this.localStream || !this.remoteStream || this.visualizerCallbacks.length === 0) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
205
|
+
|
|
206
|
+
const localSource = this.audioCtx.createMediaStreamSource(this.localStream);
|
|
207
|
+
const remoteSource = this.audioCtx.createMediaStreamSource(this.remoteStream);
|
|
208
|
+
|
|
209
|
+
this.localAnalyser = this.audioCtx.createAnalyser();
|
|
210
|
+
this.remoteAnalyser = this.audioCtx.createAnalyser();
|
|
211
|
+
this.localAnalyser.fftSize = 256;
|
|
212
|
+
this.remoteAnalyser.fftSize = 256;
|
|
213
|
+
|
|
214
|
+
localSource.connect(this.localAnalyser);
|
|
215
|
+
remoteSource.connect(this.remoteAnalyser);
|
|
216
|
+
|
|
217
|
+
const loop = () => {
|
|
218
|
+
if (!this.localAnalyser || !this.remoteAnalyser || this.visualizerCallbacks.length === 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const localArray = new Uint8Array(this.localAnalyser.frequencyBinCount);
|
|
223
|
+
const remoteArray = new Uint8Array(this.remoteAnalyser.frequencyBinCount);
|
|
224
|
+
|
|
225
|
+
this.localAnalyser.getByteFrequencyData(localArray);
|
|
226
|
+
this.remoteAnalyser.getByteFrequencyData(remoteArray);
|
|
227
|
+
|
|
228
|
+
const localAmp = localArray.reduce((a, b) => a + b, 0) / localArray.length;
|
|
229
|
+
const remoteAmp = remoteArray.reduce((a, b) => a + b, 0) / remoteArray.length;
|
|
230
|
+
|
|
231
|
+
if (this.visualizerCallbacks.length > 0) {
|
|
232
|
+
this.visualizerCallbacks.forEach((callback) => {
|
|
233
|
+
callback({
|
|
234
|
+
localAmplitude: localAmp,
|
|
235
|
+
remoteAmplitude: remoteAmp,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.analyzerFrame = requestAnimationFrame(loop);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this.analyzerFrame = requestAnimationFrame(loop);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private _stopAnalyzers(): void {
|
|
247
|
+
if (this.analyzerFrame) {
|
|
248
|
+
cancelAnimationFrame(this.analyzerFrame);
|
|
249
|
+
this.analyzerFrame = null;
|
|
250
|
+
}
|
|
251
|
+
this.localAnalyser = null;
|
|
252
|
+
this.remoteAnalyser = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
type PersonaWebRTCProtocolConfig = PersonaWebRTCConfig & {
|
|
257
|
+
autostart?: boolean;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
class PersonaWebRTCProtocol extends PersonaProtocolBase {
|
|
261
|
+
status: ProtocolStatus;
|
|
262
|
+
session: Session;
|
|
263
|
+
autostart: boolean;
|
|
264
|
+
config: PersonaWebRTCProtocolConfig;
|
|
265
|
+
webRTCClient: PersonaWebRTCClient;
|
|
266
|
+
|
|
267
|
+
constructor(config: PersonaWebRTCProtocolConfig) {
|
|
268
|
+
super();
|
|
269
|
+
this.config = config;
|
|
270
|
+
this.status = 'disconnected';
|
|
271
|
+
this.session = null;
|
|
272
|
+
this.autostart = config?.autostart ?? false;
|
|
273
|
+
this.webRTCClient = new PersonaWebRTCClient(config);
|
|
274
|
+
this.webRTCClient.addMessageCallback((msg: MessageEvent) => {
|
|
275
|
+
config.logger?.debug('Received data message:', msg.data);
|
|
276
|
+
const data = JSON.parse(msg.data) as { type: 'message' | unknown; payload: PersonaMessage | unknown };
|
|
277
|
+
if (data.type === 'message') {
|
|
278
|
+
this.notifyMessage(data.payload as PersonaMessage);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public getName(): string {
|
|
284
|
+
return 'webrtc';
|
|
285
|
+
}
|
|
286
|
+
public getPriority(): number {
|
|
287
|
+
return 10;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public async syncSession(session: Session): Promise<void> {
|
|
291
|
+
super.syncSession(session);
|
|
292
|
+
if (this.status === 'connected') {
|
|
293
|
+
await this.disconnect();
|
|
294
|
+
await this.connect(session);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public async connect(session?: Session): Promise<Session> {
|
|
299
|
+
if (this.status === 'connected') {
|
|
300
|
+
return Promise.resolve(this.session);
|
|
301
|
+
}
|
|
302
|
+
this.session = session || this.session || 'new';
|
|
303
|
+
this.setStatus('connecting');
|
|
304
|
+
|
|
305
|
+
this.config.logger?.debug('Connecting to WebRTC with sessionId:', this.session);
|
|
306
|
+
await this.webRTCClient.connect(this.session);
|
|
307
|
+
this.setStatus('connected');
|
|
308
|
+
|
|
309
|
+
await this.webRTCClient.createDataChannel();
|
|
310
|
+
|
|
311
|
+
return this.session;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
public async disconnect(): Promise<void> {
|
|
315
|
+
if (this.status === 'disconnected') {
|
|
316
|
+
this.config.logger?.warn('Already disconnected');
|
|
317
|
+
return Promise.resolve();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await this.webRTCClient.disconnect();
|
|
321
|
+
|
|
322
|
+
this.setStatus('disconnected');
|
|
323
|
+
this.config?.logger?.debug('Disconnected from WebRTC');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
public send(message: Message): Promise<void> {
|
|
327
|
+
if (this.status !== 'connected') {
|
|
328
|
+
return Promise.reject(new Error('Not connected'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.webRTCClient.sendMessage(message as string);
|
|
332
|
+
return Promise.resolve();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export { PersonaWebRTCProtocol };
|
|
337
|
+
export type { PersonaWebRTCProtocolConfig, AudioVisualizerCallback, AudioAnalysisData };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Message, PersonaMessage, PersonaProtocolBaseConfig, ProtocolStatus, Session } from '../types';
|
|
2
|
+
import { PersonaProtocolBase } from './base';
|
|
3
|
+
|
|
4
|
+
type PersonaWebSocketProtocolConfig = PersonaProtocolBaseConfig & {
|
|
5
|
+
webSocketUrl: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
class PersonaWebSocketProtocol extends PersonaProtocolBase {
|
|
9
|
+
status: ProtocolStatus;
|
|
10
|
+
autostart: boolean;
|
|
11
|
+
session: Session;
|
|
12
|
+
config: PersonaWebSocketProtocolConfig;
|
|
13
|
+
webSocket: WebSocket | null;
|
|
14
|
+
|
|
15
|
+
constructor(config: PersonaWebSocketProtocolConfig) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.status = 'disconnected';
|
|
19
|
+
this.autostart = true;
|
|
20
|
+
this.session = null;
|
|
21
|
+
this.webSocket = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public getName(): string {
|
|
25
|
+
return 'websocket';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public getPriority(): number {
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async syncSession(session: Session): Promise<void> {
|
|
33
|
+
this.config.logger?.debug('Syncing session with WebSocket protocol:', session);
|
|
34
|
+
this.session = session;
|
|
35
|
+
if (this.webSocket && this.status === 'connected') {
|
|
36
|
+
this.disconnect();
|
|
37
|
+
this.connect(session);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public connect(session?: Session): Promise<Session> {
|
|
42
|
+
if (this.webSocket !== null && this.status === 'connected') {
|
|
43
|
+
return Promise.resolve(this.session);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sid = session || this.session || 'new';
|
|
47
|
+
|
|
48
|
+
this.config.logger?.debug('Connecting to WebSocket with sessionId:', sid);
|
|
49
|
+
|
|
50
|
+
const apiKey = encodeURIComponent(this.config.apiKey);
|
|
51
|
+
const agentId = this.config.agentId;
|
|
52
|
+
const webSocketUrl = `${this.config.webSocketUrl}?sessionCode=${sid}&agentId=${agentId}&apiKey=${apiKey}`;
|
|
53
|
+
this.setStatus('connecting');
|
|
54
|
+
this.webSocket = new WebSocket(webSocketUrl);
|
|
55
|
+
this.webSocket.addEventListener('open', () => {
|
|
56
|
+
this.setStatus('connected');
|
|
57
|
+
});
|
|
58
|
+
this.webSocket.addEventListener('message', (event) => {
|
|
59
|
+
const data = JSON.parse(event.data) as { type: 'message' | unknown; payload: PersonaMessage | unknown };
|
|
60
|
+
|
|
61
|
+
if (data.type !== 'message') {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const message = data.payload as PersonaMessage & { thought?: string };
|
|
65
|
+
|
|
66
|
+
this.notifyMessage(message?.thought ? { role: 'assistant', type: 'reasoning', text: message.thought } : message);
|
|
67
|
+
});
|
|
68
|
+
this.webSocket.addEventListener('close', () => {
|
|
69
|
+
this.setStatus('disconnected');
|
|
70
|
+
this.webSocket = null;
|
|
71
|
+
this.config.logger?.warn('WebSocket connection closed');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.webSocket.addEventListener('error', (error) => {
|
|
75
|
+
this.setStatus('disconnected');
|
|
76
|
+
this.webSocket = null;
|
|
77
|
+
this.config.logger?.error('WebSocket error', error);
|
|
78
|
+
|
|
79
|
+
// TODO: Implement reconnection logic
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return Promise.resolve(sid);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public disconnect(): Promise<void> {
|
|
86
|
+
this.config.logger?.debug('Disconnecting WebSocket');
|
|
87
|
+
if (this.webSocket && this.status === 'connected') {
|
|
88
|
+
this.webSocket.close();
|
|
89
|
+
this.setStatus('disconnected');
|
|
90
|
+
this.webSocket = null;
|
|
91
|
+
}
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public send(message: Message): Promise<void> {
|
|
96
|
+
if (this.webSocket && this.status === 'connected') {
|
|
97
|
+
this.webSocket.send(JSON.stringify({ type: 'request', payload: message }));
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
} else {
|
|
100
|
+
return Promise.reject(new Error('WebSocket is not connected'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { PersonaWebSocketProtocol };
|
|
106
|
+
export type { PersonaWebSocketProtocolConfig };
|