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 CHANGED
@@ -15,3 +15,4 @@ export * from './resources/background-job.js';
15
15
  export * from './resources/key-value-store.js';
16
16
  export * from './resources/endpoint.js';
17
17
  export * from './resources/system-attribute.js';
18
+ export * from './services/llm.js';
package/dist/index.js CHANGED
@@ -14,3 +14,4 @@ export * from './resources/background-job.js';
14
14
  export * from './resources/key-value-store.js';
15
15
  export * from './resources/endpoint.js';
16
16
  export * from './resources/system-attribute.js';
17
+ export * from './services/llm.js';
@@ -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
- export class RealtimeService extends Resource {
9
- #channels;
10
- #secret;
11
- #server;
12
- constructor(scope, id) {
13
- super(scope, id);
14
- this.#secret = new (overrides.Setting || Setting)(this, 'auth-secret', {
15
- private: true, init: 'random',
16
- description: "Used to sign realtime subscription authorization. Should generally not be changed."
17
- });
18
- if (servers.has(this.absoluteId)) {
19
- console.log('existing realtime service found');
20
- this.#server = servers.get(this.absoluteId);
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
- if (channels.has(this.absoluteId)) {
23
- console.log('existing channel entries found');
24
- this.#channels = channels.get(this.absoluteId);
26
+ try {
27
+ channel = Buffer.from(encodedChannel, 'base64').toString('utf-8');
28
+ console.log('Decoded channel:', channel);
25
29
  }
26
- else {
27
- this.#channels = new Map();
28
- channels.set(this.absoluteId, this.#channels);
30
+ catch (err) {
31
+ console.log('Invalid channel encoding');
32
+ ws.close(4004, 'Invalid channel encoding');
33
+ return;
29
34
  }
30
- }
31
- #requireServer() {
32
- if (this.#server)
35
+ if (!protocol) {
36
+ console.log('No protocol specified');
37
+ ws.close(4001, 'Protocol not specified');
33
38
  return;
34
- console.log('Starting WebSocket server...');
35
- this.#server = new WebSocketServer({ port: 3001 });
36
- servers.set(this.absoluteId, this.#server);
37
- this.#server.on('connection', async (ws, req) => {
38
- const encodedChannel = req.url?.split('/')[1];
39
- let channel;
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
- this.#channels.set(channel, this.#channels.get(channel) || []);
75
- this.#channels.get(channel).push(ws);
76
- ws.onclose = () => {
77
- this.#channels.set(channel, this.#channels.get(channel).filter((w) => w !== ws));
78
- };
79
- console.log('WebSocket connection established:', channel);
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
- this.#requireServer();
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
- console.log('Publishing to channel:', channel, events);
121
- (this.#channels.get(channel) || []).forEach((ws) => {
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 channelString = Buffer.from(channel).toString('base64');
133
- const payload = { channel };
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 this.#secret.read()));
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-resources",
3
- "version": "0.1.130",
3
+ "version": "0.1.132-llm",
4
4
  "description": "Basic services and server-side resources for wirejs apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",