feat: Introduce a comprehensive set of AI agent skills, agents, and workflows, along with new UI components and application structure.
This commit is contained in:
1 parent
a2a512d41d
commit
93df87d7e9
317 files changed
+63716
-8727
No files matched your search
@@ -0,0 +1,260 @@
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
ChatMessage,
|
||||
LMModel,
|
||||
ModelsResponse,
|
||||
ServerConfig,
|
||||
StreamChunk,
|
||||
} from './types';
|
||||
|
||||
function buildBaseUrl(config: ServerConfig): string {
|
||||
const host = config.host.replace(/\/+$/, '');
|
||||
const protocol = host.startsWith('http') ? '' : 'http://';
|
||||
return `${protocol}${host}:${config.port}/v1`;
|
||||
}
|
||||
|
||||
function buildHeaders(config: ServerConfig): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (config.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function testConnection(config: ServerConfig): Promise<boolean> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const res = await fetch(`${buildBaseUrl(config)}/models`, {
|
||||
headers: buildHeaders(config),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getModels(config: ServerConfig): Promise<LMModel[]> {
|
||||
const res = await fetch(`${buildBaseUrl(config)}/models`, {
|
||||
headers: buildHeaders(config),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data: ModelsResponse = await res.json();
|
||||
return data.data ?? [];
|
||||
}
|
||||
|
||||
export async function chatCompletion(
|
||||
config: ServerConfig,
|
||||
model: string,
|
||||
messages: ChatMessage[],
|
||||
options?: Partial<Omit<ChatCompletionRequest, 'model' | 'messages' | 'stream'>>,
|
||||
): Promise<ChatCompletionResponse> {
|
||||
const body: ChatCompletionRequest = {
|
||||
model,
|
||||
messages,
|
||||
stream: false,
|
||||
temperature: 0.7,
|
||||
max_tokens: 2048,
|
||||
...options,
|
||||
};
|
||||
|
||||
const res = await fetch(`${buildBaseUrl(config)}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(config),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Chat completion failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Parse SSE lines from a text buffer
|
||||
function parseSSELines(
|
||||
buffer: string,
|
||||
onChunk: (content: string) => void,
|
||||
onDone: () => void,
|
||||
): { remaining: string; isDone: boolean } {
|
||||
const lines = buffer.split('\n');
|
||||
const remaining = lines.pop() ?? '';
|
||||
let isDone = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
||||
|
||||
const payload = trimmed.slice(6);
|
||||
if (payload === '[DONE]') {
|
||||
isDone = true;
|
||||
onDone();
|
||||
return { remaining: '', isDone: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const chunk: StreamChunk = JSON.parse(payload);
|
||||
const content = chunk.choices?.[0]?.delta?.content;
|
||||
if (content) {
|
||||
onChunk(content);
|
||||
}
|
||||
} catch {
|
||||
// skip malformed chunks
|
||||
}
|
||||
}
|
||||
|
||||
return { remaining, isDone };
|
||||
}
|
||||
|
||||
// Native streaming using XMLHttpRequest (works on React Native)
|
||||
function streamWithXHR(
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
body: string,
|
||||
onChunk: (content: string) => void,
|
||||
onDone: () => void,
|
||||
onError: (error: Error) => void,
|
||||
): { abort: () => void } {
|
||||
const xhr = new XMLHttpRequest();
|
||||
let buffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
xhr.open('POST', url);
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
xhr.setRequestHeader(key, value);
|
||||
});
|
||||
|
||||
xhr.onprogress = () => {
|
||||
const newData = xhr.responseText.substring(lastIndex);
|
||||
lastIndex = xhr.responseText.length;
|
||||
buffer += newData;
|
||||
|
||||
const result = parseSSELines(buffer, onChunk, onDone);
|
||||
buffer = result.remaining;
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
// Process any remaining buffer
|
||||
if (buffer.trim()) {
|
||||
parseSSELines(buffer + '\n', onChunk, onDone);
|
||||
}
|
||||
onDone();
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
onError(new Error(`XHR streaming failed: ${xhr.status} ${xhr.statusText}`));
|
||||
};
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
onError(new Error('Request timed out'));
|
||||
};
|
||||
|
||||
xhr.send(body);
|
||||
|
||||
return {
|
||||
abort: () => {
|
||||
try { xhr.abort(); } catch { }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Web streaming using ReadableStream (works in browsers)
|
||||
async function streamWithFetch(
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
body: string,
|
||||
onChunk: (content: string) => void,
|
||||
onDone: () => void,
|
||||
onError: (error: Error) => void,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Stream failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error('ReadableStream not supported');
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const result = parseSSELines(buffer, onChunk, onDone);
|
||||
buffer = result.remaining;
|
||||
if (result.isDone) return;
|
||||
}
|
||||
onDone();
|
||||
}
|
||||
|
||||
export function chatCompletionStream(
|
||||
config: ServerConfig,
|
||||
model: string,
|
||||
messages: ChatMessage[],
|
||||
onChunk: (content: string) => void,
|
||||
onDone: () => void,
|
||||
onError: (error: Error) => void,
|
||||
options?: Partial<Omit<ChatCompletionRequest, 'model' | 'messages' | 'stream'>>,
|
||||
): { abort: () => void } {
|
||||
const body: ChatCompletionRequest = {
|
||||
model,
|
||||
messages,
|
||||
stream: true,
|
||||
temperature: 0.7,
|
||||
max_tokens: 2048,
|
||||
...options,
|
||||
};
|
||||
|
||||
const url = `${buildBaseUrl(config)}/chat/completions`;
|
||||
const headers = buildHeaders(config);
|
||||
const bodyStr = JSON.stringify(body);
|
||||
|
||||
let doneEmitted = false;
|
||||
const safeOnDone = () => {
|
||||
if (!doneEmitted) {
|
||||
doneEmitted = true;
|
||||
onDone();
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
// Web: use fetch + ReadableStream
|
||||
const controller = new AbortController();
|
||||
|
||||
streamWithFetch(url, headers, bodyStr, onChunk, safeOnDone, onError, controller.signal)
|
||||
.catch((err) => {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
onError(err as Error);
|
||||
}
|
||||
});
|
||||
|
||||
return { abort: () => controller.abort() };
|
||||
} else {
|
||||
// Native: use XMLHttpRequest
|
||||
return streamWithXHR(url, headers, bodyStr, onChunk, safeOnDone, onError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export interface ServerConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface LMModel {
|
||||
id: string;
|
||||
object: string;
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export interface ModelsResponse {
|
||||
data: LMModel[];
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatCompletionRequest {
|
||||
model: string;
|
||||
messages: ChatMessage[];
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
stream?: boolean;
|
||||
stop?: string | string[];
|
||||
top_p?: number;
|
||||
frequency_penalty?: number;
|
||||
}
|
||||
|
||||
export interface ChatCompletionChoice {
|
||||
index: number;
|
||||
message: ChatMessage;
|
||||
finish_reason: string | null;
|
||||
}
|
||||
|
||||
export interface ChatCompletionResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: ChatCompletionChoice[];
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StreamDelta {
|
||||
role?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface StreamChoice {
|
||||
index: number;
|
||||
delta: StreamDelta;
|
||||
finish_reason: string | null;
|
||||
}
|
||||
|
||||
export interface StreamChunk {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: StreamChoice[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SERVER_CONFIG: ServerConfig = {
|
||||
host: 'localhost',
|
||||
port: 1234,
|
||||
apiKey: '',
|
||||
};
|
||||
Reference in new issue
Block a user