merge llm files
This commit is contained in:
parent
75c239bf16
commit
47086720c6
|
|
@ -1,195 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from '@testIsomorphic/events';
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'user' | 'assistant' | 'developer';
|
||||
content: string;
|
||||
displayContent?: string;
|
||||
};
|
||||
|
||||
export interface LLM {
|
||||
chatCompletion(messages: LLMMessage[], signal: AbortSignal): AsyncGenerator<string>;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
|
||||
async function *parseSSE(body: NonNullable<Response['body']>): AsyncGenerator<{ type: string, data: string, id: string }> {
|
||||
const reader = body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
let buffer = '';
|
||||
|
||||
let lastEventId = '';
|
||||
let type: string = '';
|
||||
let data = '';
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done)
|
||||
break;
|
||||
buffer += value;
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop()!; // last line is either empty or incomplete
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length === 0) {
|
||||
if (data === '') {
|
||||
data = '';
|
||||
type = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data[data.length - 1] === '\n')
|
||||
data = data.substring(0, data.length - 1);
|
||||
|
||||
const event = { type: type || 'message', data, id: lastEventId };
|
||||
type = '';
|
||||
data = '';
|
||||
|
||||
yield event;
|
||||
}
|
||||
if (line[0] === ':')
|
||||
continue;
|
||||
|
||||
let name = '';
|
||||
let value = '';
|
||||
const colon = line.indexOf(':');
|
||||
if (colon === -1) {
|
||||
name = line;
|
||||
} else {
|
||||
name = line.substring(0, colon);
|
||||
value = line[colon + 1] === ' ' ? line.substring(colon + 2) : line.substring(colon + 1);
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case 'event':
|
||||
type = value;
|
||||
break;
|
||||
case 'data':
|
||||
data += value + '\n';
|
||||
break;
|
||||
case 'id':
|
||||
lastEventId = value;
|
||||
break;
|
||||
case 'retry':
|
||||
default:
|
||||
// not implemented
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenAI implements LLM {
|
||||
|
||||
constructor(private apiKey: string, private baseURL = 'https://api.openai.com') {}
|
||||
|
||||
async *chatCompletion(messages: LLMMessage[]) {
|
||||
const url = new URL('./v1/chat/completions', this.baseURL);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'x-pw-serviceworker': 'forward',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: messages.map(({ role, content }) => ({ role, content })),
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status !== 200 || !response.body)
|
||||
throw new Error('Failed to chat with OpenAI, unexpected status: ' + response.status + await response.text());
|
||||
|
||||
for await (const sseEvent of parseSSE(response.body)) {
|
||||
const event = JSON.parse(sseEvent.data);
|
||||
if (event.object === 'chat.completion.chunk') {
|
||||
if (event.choices[0].finish_reason)
|
||||
break;
|
||||
yield event.choices[0].delta.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Anthropic implements LLM {
|
||||
constructor(private apiKey: string, private baseURL = 'https://api.anthropic.com') {}
|
||||
async *chatCompletion(messages: LLMMessage[]): AsyncGenerator<string> {
|
||||
const response = await fetch(new URL('./v1/messages', this.baseURL), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'x-pw-serviceworker': 'forward',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
messages: messages.filter(({ role }) => role !== 'developer').map(({ role, content }) => ({ role, content })),
|
||||
system: messages.find(({ role }) => role === 'developer')?.content,
|
||||
max_tokens: 1024,
|
||||
stream: true,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status !== 200 || !response.body)
|
||||
throw new Error('Failed to chat with Anthropic, unexpected status: ' + response.status + await response.text());
|
||||
|
||||
for await (const sseEvent of parseSSE(response.body)) {
|
||||
const event = JSON.parse(sseEvent.data);
|
||||
if (event.type === 'content_block_delta')
|
||||
yield event.delta.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LLMChat {
|
||||
conversations = new Map<string, Conversation>();
|
||||
|
||||
constructor(readonly api: LLM) {}
|
||||
|
||||
getConversation(id: string, systemPrompt: string) {
|
||||
if (!this.conversations.has(id)) {
|
||||
const conversation = new Conversation(this, systemPrompt);
|
||||
this.conversations.set(id, conversation);
|
||||
}
|
||||
return this.conversations.get(id)!;
|
||||
}
|
||||
}
|
||||
|
||||
export class Conversation {
|
||||
history: LLMMessage[];
|
||||
onChange = new EventEmitter<void>();
|
||||
|
||||
constructor(private chat: LLMChat, systemPrompt: string) {
|
||||
this.history = [{ role: 'developer', content: systemPrompt }];
|
||||
}
|
||||
|
||||
async send(content: string, displayContent: string | undefined, signal: AbortSignal) {
|
||||
const response: LLMMessage = { role: 'assistant', content: '' };
|
||||
this.history.push({ role: 'user', content, displayContent }, response);
|
||||
this.onChange.fire();
|
||||
for await (const chunk of this.chat.api.chatCompletion(this.history, signal)) {
|
||||
response.content += chunk;
|
||||
this.onChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this.history.length === 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,200 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { LLMChat, LLM, OpenAI, Anthropic } from '@isomorphic/llm';
|
||||
import { EventEmitter } from '@testIsomorphic/events';
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'user' | 'assistant' | 'developer';
|
||||
content: string;
|
||||
displayContent?: string;
|
||||
};
|
||||
|
||||
interface LLM {
|
||||
chatCompletion(messages: LLMMessage[], signal: AbortSignal): AsyncGenerator<string>;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
|
||||
async function *parseSSE(body: NonNullable<Response['body']>): AsyncGenerator<{ type: string, data: string, id: string }> {
|
||||
const reader = body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
let buffer = '';
|
||||
|
||||
let lastEventId = '';
|
||||
let type: string = '';
|
||||
let data = '';
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done)
|
||||
break;
|
||||
buffer += value;
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop()!; // last line is either empty or incomplete
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length === 0) {
|
||||
if (data === '') {
|
||||
data = '';
|
||||
type = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data[data.length - 1] === '\n')
|
||||
data = data.substring(0, data.length - 1);
|
||||
|
||||
const event = { type: type || 'message', data, id: lastEventId };
|
||||
type = '';
|
||||
data = '';
|
||||
|
||||
yield event;
|
||||
}
|
||||
if (line[0] === ':')
|
||||
continue;
|
||||
|
||||
let name = '';
|
||||
let value = '';
|
||||
const colon = line.indexOf(':');
|
||||
if (colon === -1) {
|
||||
name = line;
|
||||
} else {
|
||||
name = line.substring(0, colon);
|
||||
value = line[colon + 1] === ' ' ? line.substring(colon + 2) : line.substring(colon + 1);
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case 'event':
|
||||
type = value;
|
||||
break;
|
||||
case 'data':
|
||||
data += value + '\n';
|
||||
break;
|
||||
case 'id':
|
||||
lastEventId = value;
|
||||
break;
|
||||
case 'retry':
|
||||
default:
|
||||
// not implemented
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenAI implements LLM {
|
||||
|
||||
constructor(private apiKey: string, private baseURL = 'https://api.openai.com') {}
|
||||
|
||||
async *chatCompletion(messages: LLMMessage[]) {
|
||||
const url = new URL('./v1/chat/completions', this.baseURL);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'x-pw-serviceworker': 'forward',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: messages.map(({ role, content }) => ({ role, content })),
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status !== 200 || !response.body)
|
||||
throw new Error('Failed to chat with OpenAI, unexpected status: ' + response.status + await response.text());
|
||||
|
||||
for await (const sseEvent of parseSSE(response.body)) {
|
||||
const event = JSON.parse(sseEvent.data);
|
||||
if (event.object === 'chat.completion.chunk') {
|
||||
if (event.choices[0].finish_reason)
|
||||
break;
|
||||
yield event.choices[0].delta.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Anthropic implements LLM {
|
||||
constructor(private apiKey: string, private baseURL = 'https://api.anthropic.com') {}
|
||||
async *chatCompletion(messages: LLMMessage[]): AsyncGenerator<string> {
|
||||
const response = await fetch(new URL('./v1/messages', this.baseURL), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'x-pw-serviceworker': 'forward',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
messages: messages.filter(({ role }) => role !== 'developer').map(({ role, content }) => ({ role, content })),
|
||||
system: messages.find(({ role }) => role === 'developer')?.content,
|
||||
max_tokens: 1024,
|
||||
stream: true,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status !== 200 || !response.body)
|
||||
throw new Error('Failed to chat with Anthropic, unexpected status: ' + response.status + await response.text());
|
||||
|
||||
for await (const sseEvent of parseSSE(response.body)) {
|
||||
const event = JSON.parse(sseEvent.data);
|
||||
if (event.type === 'content_block_delta')
|
||||
yield event.delta.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LLMChat {
|
||||
conversations = new Map<string, Conversation>();
|
||||
|
||||
constructor(readonly api: LLM) {}
|
||||
|
||||
getConversation(id: string, systemPrompt: string) {
|
||||
if (!this.conversations.has(id)) {
|
||||
const conversation = new Conversation(this, systemPrompt);
|
||||
this.conversations.set(id, conversation);
|
||||
}
|
||||
return this.conversations.get(id)!;
|
||||
}
|
||||
}
|
||||
|
||||
class Conversation {
|
||||
history: LLMMessage[];
|
||||
onChange = new EventEmitter<void>();
|
||||
|
||||
constructor(private chat: LLMChat, systemPrompt: string) {
|
||||
this.history = [{ role: 'developer', content: systemPrompt }];
|
||||
}
|
||||
|
||||
async send(content: string, displayContent: string | undefined, signal: AbortSignal) {
|
||||
const response: LLMMessage = { role: 'assistant', content: '' };
|
||||
this.history.push({ role: 'user', content, displayContent }, response);
|
||||
this.onChange.fire();
|
||||
for await (const chunk of this.chat.api.chatCompletion(this.history, signal)) {
|
||||
response.content += chunk;
|
||||
this.onChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this.history.length < 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const llmContext = React.createContext<LLMChat | undefined>(undefined);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue