wirejs-resources 0.1.130 → 0.1.132-llm
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/overrides.d.ts +2 -0
- package/dist/services/llm.d.ts +21 -0
- package/dist/services/llm.js +117 -0
- package/dist/services/realtime.d.ts +1 -0
- package/dist/services/realtime.js +62 -77
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/overrides.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { DistributedTable } from "./resources/distributed-table";
|
|
|
4
4
|
import type { Endpoint } from "./resources/endpoint";
|
|
5
5
|
import type { FileService } from "./services/file";
|
|
6
6
|
import type { KeyValueStore } from "./resources/key-value-store";
|
|
7
|
+
import type { LLM } from "./services/llm";
|
|
7
8
|
import type { RealtimeService } from "./services/realtime";
|
|
8
9
|
import type { Setting } from "./resources/setting";
|
|
9
10
|
/**
|
|
@@ -16,6 +17,7 @@ export declare const overrides: {
|
|
|
16
17
|
Endpoint?: typeof Endpoint;
|
|
17
18
|
FileService?: typeof FileService;
|
|
18
19
|
KeyValueStore?: typeof KeyValueStore;
|
|
20
|
+
LLM?: typeof LLM;
|
|
19
21
|
RealtimeService?: typeof RealtimeService;
|
|
20
22
|
Setting?: typeof Setting;
|
|
21
23
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Resource } from '../resource.js';
|
|
2
|
+
export type LLMMessage = {
|
|
3
|
+
role: 'assistant' | 'user' | 'system' | 'tool';
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export type LLMChunk = {
|
|
7
|
+
created_at: string;
|
|
8
|
+
message: LLMMessage;
|
|
9
|
+
done: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare class LLM extends Resource {
|
|
12
|
+
models: string[];
|
|
13
|
+
constructor(scope: Resource | string, id: string, options: {
|
|
14
|
+
models: string[];
|
|
15
|
+
});
|
|
16
|
+
private stream;
|
|
17
|
+
private checkOllamaAvailable;
|
|
18
|
+
private checkModelExists;
|
|
19
|
+
private createStreamedString;
|
|
20
|
+
continueConversation(history: LLMMessage[], onChunk?: (chunk: LLMChunk) => void | Promise<void>): Promise<LLMMessage>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Resource } from '../resource.js';
|
|
2
|
+
export class LLM extends Resource {
|
|
3
|
+
models;
|
|
4
|
+
constructor(scope, id, options) {
|
|
5
|
+
super(scope, id);
|
|
6
|
+
this.models = options.models;
|
|
7
|
+
}
|
|
8
|
+
async stream(response, onChunk) {
|
|
9
|
+
if (!response.ok || !response.body) {
|
|
10
|
+
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
|
11
|
+
}
|
|
12
|
+
const reader = response.body.getReader();
|
|
13
|
+
const decoder = new TextDecoder('utf-8');
|
|
14
|
+
let role = 'assistant';
|
|
15
|
+
let content = '';
|
|
16
|
+
while (true) {
|
|
17
|
+
const { value, done } = await reader.read();
|
|
18
|
+
if (done)
|
|
19
|
+
break;
|
|
20
|
+
const chunk = JSON.parse(decoder.decode(value, { stream: true }));
|
|
21
|
+
if (onChunk) {
|
|
22
|
+
try {
|
|
23
|
+
await onChunk(chunk);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
role = chunk.message.role;
|
|
30
|
+
content += chunk.message.content;
|
|
31
|
+
}
|
|
32
|
+
return { role, content };
|
|
33
|
+
}
|
|
34
|
+
async checkOllamaAvailable() {
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch('http://localhost:11434/api/tags', {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
});
|
|
39
|
+
return response.ok;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async checkModelExists(model) {
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch('http://localhost:11434/api/tags');
|
|
48
|
+
if (!response.ok)
|
|
49
|
+
return false;
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
return data.models?.some((m) => m.name === model || m.name.startsWith(model + ':'));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
createStreamedString(message, onChunk) {
|
|
58
|
+
if (onChunk) {
|
|
59
|
+
try {
|
|
60
|
+
onChunk({
|
|
61
|
+
created_at: new Date().toISOString(),
|
|
62
|
+
done: true,
|
|
63
|
+
message: {
|
|
64
|
+
content: message,
|
|
65
|
+
role: 'assistant'
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
role: 'assistant',
|
|
75
|
+
content: message
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async continueConversation(history, onChunk) {
|
|
79
|
+
const ollamaAvailable = await this.checkOllamaAvailable();
|
|
80
|
+
if (!ollamaAvailable) {
|
|
81
|
+
return this.createStreamedString('Ollama is not running locally. Please install and start Ollama:\n\n' +
|
|
82
|
+
'1. Visit https://ollama.com/ to download and install Ollama\n' +
|
|
83
|
+
'2. Start Ollama by running: ollama serve\n' +
|
|
84
|
+
'3. Pull a model (e.g., llama2): ollama pull llama2\n\n' +
|
|
85
|
+
'Models to try installing: ' + this.models.join(', '), onChunk);
|
|
86
|
+
}
|
|
87
|
+
// models should be in priority order. so, first one that works is the one we want.
|
|
88
|
+
for (const model of this.models) {
|
|
89
|
+
const modelExists = await this.checkModelExists(model);
|
|
90
|
+
if (!modelExists)
|
|
91
|
+
continue;
|
|
92
|
+
const stream = typeof onChunk === 'function';
|
|
93
|
+
const response = await fetch('http://localhost:11434/api/chat', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
model: model,
|
|
98
|
+
messages: [...history],
|
|
99
|
+
stream,
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
if (stream) {
|
|
104
|
+
return this.stream(response, onChunk);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const chunk = await response.json();
|
|
108
|
+
return chunk.message;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// if nothing works, we want to tell the user (the dev) how to install the dep.
|
|
113
|
+
return this.createStreamedString('None of the configured models are available. Please ensure Ollama is running and at least one of these models is installed:\n\n' +
|
|
114
|
+
this.models.map(m => `ollama pull ${m}`).join('\n') + '\n\n' +
|
|
115
|
+
'For more information, visit: https://ollama.com/', onChunk);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -18,6 +18,7 @@ export declare class RealtimeService<T = any> extends Resource {
|
|
|
18
18
|
* The address the client will need to connect to.
|
|
19
19
|
*/
|
|
20
20
|
address(context: Context): string;
|
|
21
|
+
getFullChannelName(channel: string): string;
|
|
21
22
|
publish(channel: string, events: T[]): Promise<void>;
|
|
22
23
|
getStream(context: Context, channel: string): Promise<MessageStream<T>>;
|
|
23
24
|
}
|
|
@@ -3,81 +3,65 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
3
3
|
import { Resource } from '../resource.js';
|
|
4
4
|
import { Setting } from '../resources/setting.js';
|
|
5
5
|
import { overrides } from '../overrides.js';
|
|
6
|
-
const servers = new Map();
|
|
7
6
|
const channels = new Map();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
const secret = new (overrides.Setting || Setting)('wirejs', 'realtime-secret', {
|
|
8
|
+
private: true, init: 'random',
|
|
9
|
+
description: "Used to sign realtime subscription authorization. Should generally not be changed."
|
|
10
|
+
});
|
|
11
|
+
let _server;
|
|
12
|
+
const server = () => {
|
|
13
|
+
if (_server)
|
|
14
|
+
return _server;
|
|
15
|
+
_server = new WebSocketServer({ port: 3001 });
|
|
16
|
+
_server.on('connection', async (ws, req) => {
|
|
17
|
+
const encodedChannel = req.url?.split('/')[1];
|
|
18
|
+
let channel;
|
|
19
|
+
const protocol = ws.protocol;
|
|
20
|
+
console.log('ws connection', { encodedChannel, protocol });
|
|
21
|
+
if (!encodedChannel) {
|
|
22
|
+
console.log('No channel specified');
|
|
23
|
+
ws.close(4000, 'Channel not specified');
|
|
24
|
+
return;
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
try {
|
|
27
|
+
channel = Buffer.from(encodedChannel, 'base64').toString('utf-8');
|
|
28
|
+
console.log('Decoded channel:', channel);
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.log('Invalid channel encoding');
|
|
32
|
+
ws.close(4004, 'Invalid channel encoding');
|
|
33
|
+
return;
|
|
29
34
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if (!protocol) {
|
|
36
|
+
console.log('No protocol specified');
|
|
37
|
+
ws.close(4001, 'Protocol not specified');
|
|
33
38
|
return;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const protocol = ws.protocol;
|
|
41
|
-
console.log('ws connection', { encodedChannel, protocol });
|
|
42
|
-
if (!encodedChannel) {
|
|
43
|
-
console.log('No channel specified');
|
|
44
|
-
ws.close(4000, 'Channel not specified');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
channel = Buffer.from(encodedChannel, 'base64').toString('utf-8');
|
|
49
|
-
console.log('Decoded channel:', channel);
|
|
50
|
-
}
|
|
51
|
-
catch (err) {
|
|
52
|
-
console.log('Invalid channel encoding');
|
|
53
|
-
ws.close(4004, 'Invalid channel encoding');
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (!protocol) {
|
|
57
|
-
console.log('No protocol specified');
|
|
58
|
-
ws.close(4001, 'Protocol not specified');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const { payload } = await jose.jwtVerify(protocol, new TextEncoder().encode(await this.#secret.read()));
|
|
63
|
-
if (payload.channel !== channel) {
|
|
64
|
-
console.log('Channel mismatch');
|
|
65
|
-
ws.close(4003, 'Channel mismatch');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
console.log('Invalid token', err);
|
|
71
|
-
ws.close(4002, 'Invalid token');
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const { payload } = await jose.jwtVerify(protocol, new TextEncoder().encode(await secret.read()));
|
|
42
|
+
if (payload.channel !== channel) {
|
|
43
|
+
console.log('Channel mismatch', payload.channel, 'is not', channel);
|
|
44
|
+
ws.close(4003, 'Channel mismatch');
|
|
72
45
|
return;
|
|
73
46
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.log('Invalid token', err);
|
|
50
|
+
ws.close(4002, 'Invalid token');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
channels.set(channel, channels.get(channel) || []);
|
|
54
|
+
channels.get(channel).push(ws);
|
|
55
|
+
ws.onclose = () => {
|
|
56
|
+
channels.set(channel, channels.get(channel).filter((w) => w !== ws));
|
|
57
|
+
};
|
|
58
|
+
console.log('WebSocket connection established:', channel);
|
|
59
|
+
});
|
|
60
|
+
return _server;
|
|
61
|
+
};
|
|
62
|
+
export class RealtimeService extends Resource {
|
|
63
|
+
constructor(scope, id) {
|
|
64
|
+
super(scope, id);
|
|
81
65
|
}
|
|
82
66
|
/**
|
|
83
67
|
* Ensures channel name will be supported by known, major cloud provers using
|
|
@@ -101,8 +85,7 @@ export class RealtimeService extends Resource {
|
|
|
101
85
|
* The address the client will need to connect to.
|
|
102
86
|
*/
|
|
103
87
|
address(context) {
|
|
104
|
-
|
|
105
|
-
const address = this.#server.address();
|
|
88
|
+
const address = server().address();
|
|
106
89
|
if (typeof address === 'string') {
|
|
107
90
|
return address;
|
|
108
91
|
}
|
|
@@ -114,11 +97,13 @@ export class RealtimeService extends Resource {
|
|
|
114
97
|
throw new Error('Server address is not available');
|
|
115
98
|
}
|
|
116
99
|
}
|
|
100
|
+
getFullChannelName(channel) {
|
|
101
|
+
return `${this.absoluteId}/${channel}`;
|
|
102
|
+
}
|
|
117
103
|
async publish(channel, events) {
|
|
118
|
-
this.#requireServer();
|
|
119
104
|
this.#validateChannelName(channel);
|
|
120
|
-
|
|
121
|
-
(
|
|
105
|
+
const fullChannelName = this.getFullChannelName(channel);
|
|
106
|
+
(channels.get(fullChannelName) || []).forEach((ws) => {
|
|
122
107
|
if (ws.readyState === WebSocket.OPEN) {
|
|
123
108
|
for (const event of events) {
|
|
124
109
|
ws.send(JSON.stringify({ data: event }));
|
|
@@ -127,15 +112,15 @@ export class RealtimeService extends Resource {
|
|
|
127
112
|
});
|
|
128
113
|
}
|
|
129
114
|
async getStream(context, channel) {
|
|
130
|
-
this.#requireServer();
|
|
131
115
|
this.#validateChannelName(channel);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
116
|
+
const fullChannelName = this.getFullChannelName(channel);
|
|
117
|
+
const channelString = Buffer.from(fullChannelName).toString('base64');
|
|
118
|
+
const payload = { channel: fullChannelName };
|
|
134
119
|
const jwt = await new jose.SignJWT(payload)
|
|
135
120
|
.setProtectedHeader({ alg: 'HS256' })
|
|
136
121
|
.setIssuedAt()
|
|
137
122
|
.setExpirationTime(`10s`)
|
|
138
|
-
.sign(new TextEncoder().encode(await
|
|
123
|
+
.sign(new TextEncoder().encode(await secret.read()));
|
|
139
124
|
// The type we return is not at all what we actually return.
|
|
140
125
|
// It's the metadata needed to satisfy the contract client-side.
|
|
141
126
|
return {
|