This commit is contained in:
2025-05-25 14:01:37 +08:00
commit 8f52a10ae0
42 changed files with 1946 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type OllamaOptions = BaseChatOptions;
/**
* 自定义模型
*/
export class Custom extends BaseChat {
static BASE_URL = 'https://api.deepseek.com/v1/';
constructor(options: OllamaOptions) {
const baseURL = options.baseURL || Custom.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL });
}
}

View File

@@ -0,0 +1,10 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type DeepSeekOptions = Partial<BaseChatOptions>;
export class DeepSeek extends BaseChat {
static BASE_URL = 'https://api.deepseek.com/v1/';
constructor(options: DeepSeekOptions) {
const baseURL = options.baseURL || DeepSeek.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL });
}
}

View File

@@ -0,0 +1,11 @@
// https://api-inference.modelscope.cn/v1/
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type ModelScopeOptions = Partial<BaseChatOptions>;
export class ModelScope extends BaseChat {
static BASE_URL = 'https://api-inference.modelscope.cn/v1/';
constructor(options: ModelScopeOptions) {
const baseURL = options.baseURL || ModelScope.BASE_URL;
super({ ...options, baseURL: baseURL } as any);
}
}

View File

@@ -0,0 +1,47 @@
import { BaseChat, BaseChatOptions } from '../core/index.ts';
import type { ChatMessage, ChatMessageOptions } from '../core/index.ts';
export type OllamaOptions = Partial<BaseChatOptions>;
type OllamaModel = {
name: string;
model: string;
modified_at: string;
size: number;
digest: string;
details: {
parent_model: string;
format: string; // example: gguf
family: string; // example qwen
families: string[];
parameter_size: string;
quantization_level: string; // example: Q4_K_M Q4_0
};
};
export class Ollama extends BaseChat {
static BASE_URL = 'http://localhost:11434/v1';
constructor(options: OllamaOptions) {
const baseURL = options.baseURL || Ollama.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL });
}
async chat(messages: ChatMessage[], options?: ChatMessageOptions) {
const res = await super.chat(messages, options);
console.log('thunk', this.getChatUsage());
return res;
}
/**
* 获取模型列表
* @returns
*/
async listModels(): Promise<{ models: OllamaModel[] }> {
const _url = new URL(this.baseURL);
const tagsURL = new URL('/api/tags', _url);
return this.openai.get(tagsURL.toString());
}
async listRunModels(): Promise<{ models: OllamaModel[] }> {
const _url = new URL(this.baseURL);
const tagsURL = new URL('/api/ps', _url);
return this.openai.get(tagsURL.toString());
}
}

View File

@@ -0,0 +1,39 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
import { OpenAI } from 'openai';
export type SiliconFlowOptions = Partial<BaseChatOptions>;
type SiliconFlowUsageData = {
id: string;
name: string;
image: string;
email: string;
isAdmin: boolean;
balance: string;
status: 'normal' | 'suspended' | 'expired' | string; // 状态
introduce: string;
role: string;
chargeBalance: string;
totalBalance: string;
category: string;
};
type SiliconFlowUsageResponse = {
code: number;
message: string;
status: boolean;
data: SiliconFlowUsageData;
};
export class SiliconFlow extends BaseChat {
static BASE_URL = 'https://api.siliconflow.cn/v1';
constructor(options: SiliconFlowOptions) {
const baseURL = options.baseURL || SiliconFlow.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL });
}
async getUsageInfo(): Promise<SiliconFlowUsageResponse> {
return this.openai.get('/user/info');
}
async chat(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], options?: Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>) {
const res = await super.chat(messages, options);
return res;
}
}

View File

@@ -0,0 +1,10 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type VolcesOptions = Partial<BaseChatOptions>;
export class Volces extends BaseChat {
static BASE_URL = 'https://ark.cn-beijing.volces.com/api/v3/';
constructor(options: VolcesOptions) {
const baseURL = options.baseURL || Volces.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL });
}
}

143
src/provider/core/chat.ts Normal file
View File

@@ -0,0 +1,143 @@
import { OpenAI } from 'openai';
import type {
BaseChatInterface,
ChatMessageComplete,
ChatMessage,
ChatMessageOptions,
BaseChatUsageInterface,
ChatStream,
EmbeddingMessage,
EmbeddingMessageComplete,
} from './type.ts';
export type BaseChatOptions<T = Record<string, any>> = {
/**
* 默认baseURL
*/
baseURL: string;
/**
* 默认模型
*/
model?: string;
/**
* 默认apiKey
*/
apiKey: string;
/**
* 是否在浏览器中使用
*/
isBrowser?: boolean;
/**
* 是否流式输出, 默认 false
*/
stream?: boolean;
} & T;
export class BaseChat implements BaseChatInterface, BaseChatUsageInterface {
/**
* 默认baseURL
*/
baseURL: string;
/**
* 默认模型
*/
model: string;
/**
* 默认apiKey
*/
apiKey: string;
/**
* 是否在浏览器中使用
*/
isBrowser: boolean;
/**
* openai实例
*/
openai: OpenAI;
prompt_tokens: number;
total_tokens: number;
completion_tokens: number;
constructor(options: BaseChatOptions) {
this.baseURL = options.baseURL;
this.model = options.model;
this.apiKey = options.apiKey;
this.isBrowser = options.isBrowser ?? false;
this.openai = new OpenAI({
apiKey: this.apiKey,
baseURL: this.baseURL,
dangerouslyAllowBrowser: this.isBrowser,
});
}
/**
* 聊天
*/
async chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise<ChatMessageComplete> {
const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
model: this.model,
messages,
...options,
stream: false,
};
const res = (await this.openai.chat.completions.create(createParams)) as ChatMessageComplete;
this.prompt_tokens = res.usage?.prompt_tokens ?? 0;
this.total_tokens = res.usage?.total_tokens ?? 0;
this.completion_tokens = res.usage?.completion_tokens ?? 0;
return res;
}
async chatStream(messages: ChatMessage[], options?: ChatMessageOptions) {
const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
model: this.model,
messages,
...options,
stream: true,
};
if (createParams.response_format) {
throw new Error('response_format is not supported in stream mode');
}
return this.openai.chat.completions.create(createParams) as unknown as ChatStream;
}
/**
* 测试
*/
test() {
return this.chat([{ role: 'user', content: 'Hello, world!' }]);
}
/**
* 获取聊天使用情况
* @returns
*/
getChatUsage() {
return {
prompt_tokens: this.prompt_tokens,
total_tokens: this.total_tokens,
completion_tokens: this.completion_tokens,
};
}
getHeaders(headers?: Record<string, string>) {
return {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
...headers,
};
}
/**
* 生成embedding 内部
* @param text
* @returns
*/
async generateEmbeddingCore(text: string | string[], options?: EmbeddingMessage): Promise<EmbeddingMessageComplete> {
const embeddingModel = options?.model || this.model;
const res = await this.openai.embeddings.create({
model: embeddingModel,
input: text,
encoding_format: 'float',
...options,
});
this.prompt_tokens += res.usage.prompt_tokens;
this.total_tokens += res.usage.total_tokens;
return res;
}
}

View File

@@ -0,0 +1,27 @@
import { ChatStream } from './type.ts';
// export type { BaseChat, BaseChatOptions } from './chat.ts';
export * from './chat.ts'
// export {
// ChatMessage,
// ChatMessageOptions, //
// ChatMessageComplete,
// ChatMessageStream,
// BaseChatInterface,
// BaseChatUsageInterface,
// ChatStream,
// EmbeddingMessage,
// EmbeddingMessageComplete,
// } from './type.ts';
export * from './type.ts'
/**
* for await (const chunk of chatStream) {
* console.log(chunk);
* }
* @param chatStream
*/
export const readStream = async (chatStream: ChatStream) => {
for await (const chunk of chatStream) {
console.log(chunk);
}
};

View File

@@ -0,0 +1,105 @@
// Updated: Aug. 20, 2024
// Live demo: https://jina.ai/tokenizer
// LICENSE: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0)
// COPYRIGHT: Jina AI
// Define variables for magic numbers
const MAX_HEADING_LENGTH = 7;
const MAX_HEADING_CONTENT_LENGTH = 200;
const MAX_HEADING_UNDERLINE_LENGTH = 200;
const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100;
const MAX_LIST_ITEM_LENGTH = 200;
const MAX_NESTED_LIST_ITEMS = 6;
const MAX_LIST_INDENT_SPACES = 7;
const MAX_BLOCKQUOTE_LINE_LENGTH = 200;
const MAX_BLOCKQUOTE_LINES = 15;
const MAX_CODE_BLOCK_LENGTH = 1500;
const MAX_CODE_LANGUAGE_LENGTH = 20;
const MAX_INDENTED_CODE_LINES = 20;
const MAX_TABLE_CELL_LENGTH = 200;
const MAX_TABLE_ROWS = 20;
const MAX_HTML_TABLE_LENGTH = 2000;
const MIN_HORIZONTAL_RULE_LENGTH = 3;
const MAX_SENTENCE_LENGTH = 400;
const MAX_QUOTED_TEXT_LENGTH = 300;
const MAX_PARENTHETICAL_CONTENT_LENGTH = 200;
const MAX_NESTED_PARENTHESES = 5;
const MAX_MATH_INLINE_LENGTH = 100;
const MAX_MATH_BLOCK_LENGTH = 500;
const MAX_PARAGRAPH_LENGTH = 1000;
const MAX_STANDALONE_LINE_LENGTH = 800;
const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100;
const MAX_HTML_TAG_CONTENT_LENGTH = 1000;
const LOOKAHEAD_RANGE = 100; // Number of characters to look ahead for a sentence boundary
const AVOID_AT_START = `[\\s\\]})>,']`;
const PUNCTUATION = `[.!?…]|\\.{3}|[\\u2026\\u2047-\\u2049]|[\\p{Emoji_Presentation}\\p{Extended_Pictographic}]`;
const QUOTE_END = `(?:'(?=\`)|''(?=\`\`))`;
const SENTENCE_END = `(?:${PUNCTUATION}(?<!${AVOID_AT_START}(?=${PUNCTUATION}))|${QUOTE_END})(?=\\S|$)`;
const SENTENCE_BOUNDARY = `(?:${SENTENCE_END}|(?=[\\r\\n]|$))`;
const LOOKAHEAD_PATTERN = `(?:(?!${SENTENCE_END}).){1,${LOOKAHEAD_RANGE}}${SENTENCE_END}`;
const NOT_PUNCTUATION_SPACE = `(?!${PUNCTUATION}\\s)`;
const SENTENCE_PATTERN = `${NOT_PUNCTUATION_SPACE}(?:[^\\r\\n]{1,{MAX_LENGTH}}${SENTENCE_BOUNDARY}|[^\\r\\n]{1,{MAX_LENGTH}}(?=${PUNCTUATION}|${QUOTE_END})(?:${LOOKAHEAD_PATTERN})?)${AVOID_AT_START}*`;
export const textSplitter = new RegExp(
"(" +
// 1. Headings (Setext-style, Markdown, and HTML-style, with length constraints)
`(?:^(?:[#*=-]{1,${MAX_HEADING_LENGTH}}|\\w[^\\r\\n]{0,${MAX_HEADING_CONTENT_LENGTH}}\\r?\\n[-=]{2,${MAX_HEADING_UNDERLINE_LENGTH}}|<h[1-6][^>]{0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\\r\\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:</h[1-6]>)?(?:\\r?\\n|$))` +
"|" +
// New pattern for citations
`(?:\\[[0-9]+\\][^\\r\\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` +
"|" +
// 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints)
`(?:(?:^|\\r?\\n)[ \\t]{0,3}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}` +
`(?:(?:\\r?\\n[ \\t]{2,5}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` +
`(?:\\r?\\n[ \\t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` +
"|" +
// 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints)
`(?:(?:^>(?:>|\\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\\r?\\n?){1,${MAX_BLOCKQUOTE_LINES}})` +
"|" +
// 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints)
`(?:(?:^|\\r?\\n)(?:\`\`\`|~~~)(?:\\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\\r?\\n[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:\`\`\`|~~~)\\r?\\n?` +
`|(?:(?:^|\\r?\\n)(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\\r?\\n(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,${MAX_INDENTED_CODE_LINES}}\\r?\\n?)` +
`|(?:<pre>(?:<code>)?[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:</code>)?</pre>))` +
"|" +
// 5. Tables (Markdown, grid tables, and HTML tables, with length constraints)
`(?:(?:^|\\r?\\n)(?:\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|(?:\\r?\\n\\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\\|){0,1}(?:\\r?\\n\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|){0,${MAX_TABLE_ROWS}}` +
`|<table>[\\s\\S]{0,${MAX_HTML_TABLE_LENGTH}}?</table>))` +
"|" +
// 6. Horizontal rules (Markdown and HTML hr tag)
`(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\\s*$|<hr\\s*/?>)` +
"|" +
// 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints)
`(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}(?:</[a-zA-Z]+>)?(?:\\r?\\n|$))` +
"|" +
// 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation)
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` +
"|" +
// 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints)
"(?:" +
`(?<!\\w)\"\"\"[^\"]{0,${MAX_QUOTED_TEXT_LENGTH}}\"\"\"(?!\\w)` +
`|(?<!\\w)(?:['\"\`'"])[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}\\1(?!\\w)` +
`|(?<!\\w)\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}'(?!\\w)` +
`|(?<!\\w)\`\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}''(?!\\w)` +
`|\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\)[^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\)` +
`|\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\][^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\]` +
`|\\$[^\\r\\n$]{0,${MAX_MATH_INLINE_LENGTH}}\\$` +
`|\`[^\`\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\`` +
")" +
"|" +
// 9. Paragraphs (with length constraints)
`(?!${AVOID_AT_START})(?:(?:^|\\r?\\n\\r?\\n)(?:<p>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:</p>)?(?=\\r?\\n\\r?\\n|$))` +
"|" +
// 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints)
`(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\\s\\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?</[a-zA-Z]+>|\\s*/>))` +
"|" +
// 12. LaTeX-style math expressions (inline and block, with length constraints)
`(?:(?:\\$\\$[\\s\\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\\$\\$)|(?:\\$[^\\$\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\\$))` +
"|" +
// 14. Fallback for any remaining content (with length constraints)
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` +
")",
"gmu"
);

29
src/provider/core/type.ts Normal file
View File

@@ -0,0 +1,29 @@
import OpenAI from 'openai';
export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
export type ChatMessageOptions = Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>;
export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion;
export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion;
export type EmbeddingMessage = Partial<OpenAI.Embeddings.EmbeddingCreateParams>;
export type EmbeddingMessageComplete = OpenAI.Embeddings.CreateEmbeddingResponse;
export interface BaseChatInterface {
chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise<ChatMessageComplete>;
}
export interface BaseChatUsageInterface {
/**
* 提示词令牌
*/
prompt_tokens: number;
/**
* 总令牌
*/
total_tokens: number;
/**
* 完成令牌
*/
completion_tokens: number;
}
export type ChatStream = AsyncGenerator<ChatMessageComplete, void, unknown>;

63
src/provider/index.ts Normal file
View File

@@ -0,0 +1,63 @@
export * from './core/index.ts';
import { BaseChat } from './core/chat.ts';
import { Ollama } from './chat-adapter/ollama.ts';
import { SiliconFlow } from './chat-adapter/siliconflow.ts';
import { Custom } from './chat-adapter/custom.ts';
import { Volces } from './chat-adapter/volces.ts';
import { DeepSeek } from './chat-adapter/deepseek.ts';
import { ModelScope } from './chat-adapter/model-scope.ts';
import { ChatMessage } from './core/type.ts';
export const OllamaProvider = Ollama;
export const SiliconFlowProvider = SiliconFlow;
export const CustomProvider = Custom;
export const VolcesProvider = Volces;
export const DeepSeekProvider = DeepSeek;
export const ModelScopeProvider = ModelScope;
export const ProviderMap = {
Ollama: OllamaProvider,
SiliconFlow: SiliconFlowProvider,
Custom: CustomProvider,
Volces: VolcesProvider,
DeepSeek: DeepSeekProvider,
ModelScope: ModelScopeProvider,
BaseChat: BaseChat,
};
type ProviderManagerConfig = {
provider: string;
model: string;
apiKey: string;
baseURL?: string;
};
export class ProviderManager {
provider: BaseChat;
constructor(config: ProviderManagerConfig) {
const { provider, model, apiKey, baseURL } = config;
const Provider = ProviderMap[provider] as typeof BaseChat;
if (!Provider) {
throw new Error(`Provider ${provider} not found`);
}
const providerConfig = {
model,
apiKey,
baseURL,
};
if (!providerConfig.baseURL) {
delete providerConfig.baseURL;
}
this.provider = new Provider(providerConfig);
}
static async createProvider(config: ProviderManagerConfig) {
if (!config.baseURL) {
delete config.baseURL;
}
const pm = new ProviderManager(config);
return pm.provider;
}
async chat(messages: ChatMessage[]) {
return this.provider.chat(messages);
}
}

View File

@@ -0,0 +1,107 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
import { EmbeddingMessage } from '../core/type.ts';
export type KnowledgeOptions<T = Record<string, string>> = BaseChatOptions<
{
embeddingModel: string;
splitSize?: number; // 分块大小 默认 2000
splitOverlap?: number; // 分块重叠 默认 200
batchSize?: number; // 批量大小 默认 4, 4*2000=8000
} & T
>;
/**
* 知识库构建
* 1. Embedding generate
* 2. retriever
* 3. reranker
*/
export class KnowledgeBase extends BaseChat {
embeddingModel: string;
splitSize: number;
splitOverlap: number;
batchSize: number;
constructor(options: KnowledgeOptions) {
super(options);
this.embeddingModel = options.embeddingModel;
this.splitSize = options.splitSize || 2000;
this.splitOverlap = options.splitOverlap || 200;
this.prompt_tokens = 0;
this.total_tokens = 0;
this.batchSize = options.batchSize || 4;
}
/**
* 生成embedding
* @param text
* @returns
*/
async generateEmbedding(text: string | string[]) {
try {
const res = await this.generateEmbeddingCore(text, { model: this.embeddingModel });
return { code: 200, data: res.data };
} catch (error) {
const has413 = error?.message?.includes('413');
if (has413) {
return {
code: 413,
message: '请求过大,请分割文本',
};
}
return {
code: error?.code || 500,
message: '生成embedding失败',
};
}
}
/**
* 批量生成embedding
* @param text
* @returns
*/
async generateEmbeddingBatch(textArray: string[]) {
const batchSize = this.batchSize || 4;
const embeddings: number[][] = [];
for (let i = 0; i < textArray.length; i += batchSize) {
const batch = textArray.slice(i, i + batchSize);
const res = await this.generateEmbedding(batch);
if (res.code === 200) {
embeddings.push(...res.data.map((item) => item.embedding));
}
}
return embeddings;
}
/**
* 分割长文本, 生成对应的embedding
* @param text
* @returns
*/
async splitLongText(text: string) {
// 分割文本
const chunks: string[] = [];
let startIndex = 0;
while (startIndex < text.length) {
// 计算当前chunk的结束位置
const endIndex = Math.min(startIndex + this.splitSize, text.length);
// 提取当前chunk
const chunk = text.substring(startIndex, endIndex);
chunks.push(chunk);
// 移动到下一个起始位置,考虑重叠
startIndex = endIndex - this.splitOverlap;
// 如果下一个起始位置已经超出或者太接近文本结尾,就结束循环
if (startIndex >= text.length - this.splitOverlap) {
break;
}
}
// 为每个chunk生成embedding
const embeddings = await this.generateEmbeddingBatch(chunks);
// 返回文本片段和对应的embedding
return chunks.map((chunk, index) => ({
text: chunk,
embedding: embeddings[index],
}));
}
}

View File

@@ -0,0 +1,7 @@
import { KnowledgeBase, KnowledgeOptions } from './knowledge-base.ts';
export class Knowledge extends KnowledgeBase {
constructor(options: KnowledgeOptions) {
super(options);
}
}

View File

@@ -0,0 +1 @@
export * from './video/siliconflow.ts';

View File

@@ -0,0 +1,37 @@
import { SiliconFlow } from '../../chat-adapter/siliconflow.ts';
export class VideoSiliconFlow extends SiliconFlow {
constructor(opts: any) {
super(opts);
}
async uploadAudioVoice(audioBase64: string | Blob | File) {
const pathname = 'uploads/audio/voice';
const url = `${this.baseURL}/${pathname}`;
const headers = {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${this.apiKey}`,
};
const formData = new FormData();
// formData.append('audio', 'data:audio/mpeg;base64,aGVsbG93b3JsZA==');
// formData.append('audio', audioBase64);
formData.append('file', audioBase64);
formData.append('model', 'FunAudioLLM/CosyVoice2-0.5B');
formData.append('customName', 'test_name');
formData.append('text', '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始');
const res = await fetch(url, {
method: 'POST',
headers,
body: formData,
}).then((res) => res.json());
console.log('uploadAudioVoice', res);
}
async audioSpeech() {
this.openai.audio.speech.create({
model: 'FunAudioLLM/CosyVoice2-0.5B',
voice: 'alloy',
input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始',
response_format: 'mp3',
});
}
}

View File

@@ -0,0 +1,52 @@
import type { Permission } from '@kevisual/permission';
export type AIModel = {
/**
* 提供商
*/
provider: string;
/**
* 模型名称
*/
model: string;
/**
* 模型组
*/
group: string;
/**
* 每日请求频率限制
*/
dayLimit?: number;
/**
* 总的token限制
*/
tokenLimit?: number;
};
export type SecretKey = {
/**
* 组
*/
group: string;
/**
* API密钥
*/
apiKey: string;
/**
* 解密密钥
*/
decryptKey?: string;
};
export type AIConfig = {
title?: string;
description?: string;
models: AIModel[];
secretKeys: SecretKey[];
permission?: Permission;
filter?: {
objectKey: string;
type: 'array' | 'object';
operate: 'removeAttribute' | 'remove';
attribute: string[];
}[];
};

View File

@@ -0,0 +1,86 @@
import { numTokensFromString } from './token.ts';
// 常量定义
const CHUNK_SIZE = 512; // 每个chunk的最大token数
const MAGIC_SEPARATOR = '🦛';
const DELIMITER = [',', '.', '!', '?', '\n', '', '。', '', ''];
const PARAGRAPH_DELIMITER = '\n\n';
export interface Chunk {
chunkId: number;
text: string;
tokens: number;
}
/**
* 确保每个chunk的大小不超过最大token数
* @param chunk 输入的文本块
* @returns 分割后的文本块及其token数的数组
*/
function ensureChunkSize(chunk: string): Array<[string, number]> {
const tokens = numTokensFromString(chunk);
if (tokens <= CHUNK_SIZE) {
return [[chunk, tokens]];
}
// 在分隔符后添加魔法分隔符
let processedChunk = chunk;
for (const delimiter of DELIMITER) {
// 转义特殊字符
const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
processedChunk = processedChunk.replace(new RegExp(escapedDelimiter, 'g'), delimiter + MAGIC_SEPARATOR);
}
const chunks: Array<[string, number]> = [];
let tail = '';
// 按CHUNK_SIZE分割文本
for (let i = 0; i < processedChunk.length; i += CHUNK_SIZE) {
const sentences = (processedChunk.slice(i, i + CHUNK_SIZE) + ' ').split(MAGIC_SEPARATOR);
const currentChunk = tail + sentences.slice(0, -1).join('');
if (currentChunk.trim()) {
const tokenCount = numTokensFromString(currentChunk);
chunks.push([currentChunk, tokenCount]);
}
tail = sentences[sentences.length - 1].trim();
}
// 处理最后剩余的tail
if (tail) {
const tokenCount = numTokensFromString(tail);
chunks.push([tail, tokenCount]);
}
return chunks;
}
/**
* 将文本分割成chunks
* @param text 输入文本
* @returns 分割后的chunks数组
*/
export async function getChunks(text: string): Promise<Chunk[]> {
// 按段落分割文本
const paragraphs = text
.split(PARAGRAPH_DELIMITER)
.map((p) => p.trim())
.filter((p) => p);
const chunks: Chunk[] = [];
let currentIndex = 0;
// 处理每个段落
for (const paragraph of paragraphs) {
const splittedParagraph = ensureChunkSize(paragraph);
for (const [text, tokens] of splittedParagraph) {
chunks.push({
chunkId: currentIndex,
text,
tokens,
});
currentIndex++;
}
}
return chunks;
}

View File

@@ -0,0 +1,206 @@
import { Permission } from '@kevisual/permission';
import AES from 'crypto-js/aes.js';
import Utf8 from 'crypto-js/enc-utf8.js';
const CryptoJS = { AES, enc: { Utf8 } };
// 加密函数
export function encryptAES(plainText: string, secretKey: string) {
return CryptoJS.AES.encrypt(plainText, secretKey).toString();
}
// 解密函数
export function decryptAES(cipherText: string, secretKey: string) {
const bytes = CryptoJS.AES.decrypt(cipherText, secretKey);
return bytes.toString(CryptoJS.enc.Utf8);
}
type AIModel = {
/**
* 提供商
*/
provider: string;
/**
* 模型名称
*/
model: string;
/**
* 模型组
*/
group: string;
/**
* 每日请求频率限制
*/
dayLimit?: number;
/**
* 总的token限制
*/
tokenLimit?: number;
};
type SecretKey = {
/**
* 组
*/
group: string;
/**
* API密钥
*/
apiKey: string;
/**
* 解密密钥
*/
decryptKey?: string;
};
export type GetProviderOpts = {
model: string;
group: string;
decryptKey?: string;
};
export type ProviderResult = {
provider: string;
model: string;
group: string;
apiKey: string;
dayLimit?: number;
tokenLimit?: number;
baseURL?: string;
/**
* 解密密钥
*/
decryptKey?: string;
};
export type AIConfig = {
title?: string;
description?: string;
models: AIModel[];
secretKeys: SecretKey[];
permission?: Permission;
filter?: {
objectKey: string;
type: 'array' | 'object';
operate: 'removeAttribute' | 'remove';
attribute: string[];
}[];
};
export class AIConfigParser {
private config: AIConfig;
result: ProviderResult;
constructor(config: AIConfig) {
this.config = config;
}
/**
* 获取模型配置
* @param opts
* @returns
*/
getProvider(opts: GetProviderOpts): ProviderResult {
const { model, group, decryptKey } = opts;
const modelConfig = this.config.models.find((m) => m.model === model && m.group === group);
const groupConfig = this.config.secretKeys.find((m) => m.group === group);
if (!modelConfig) {
throw new Error(`在模型组 ${group} 中未找到模型 ${model}`);
}
const mergeConfig = {
...modelConfig,
...groupConfig,
decryptKey: decryptKey || groupConfig?.decryptKey,
};
// 验证模型配置
if (!mergeConfig.provider) {
throw new Error(`模型 ${model} 未配置提供商`);
}
if (!mergeConfig.model) {
throw new Error(`模型 ${model} 未配置模型名称`);
}
if (!mergeConfig.apiKey) {
throw new Error(`${group} 未配置 API 密钥`);
}
if (!mergeConfig.group) {
throw new Error(`${group} 未配置`);
}
this.result = mergeConfig;
return mergeConfig;
}
/**
* 获取解密密钥
* @param opts
* @returns
*/
async getSecretKey(opts?: {
getCache?: (key: string) => Promise<string>;
setCache?: (key: string, value: string) => Promise<void>;
providerResult?: ProviderResult;
}) {
const { getCache, setCache, providerResult } = opts || {};
const { apiKey, decryptKey, group = '', model } = providerResult || this.result;
const cacheKey = `${group}--${model}`;
if (!decryptKey) {
return apiKey;
}
if (getCache) {
const cache = await getCache(cacheKey);
if (cache) {
return cache;
}
}
const secretKey = decryptAES(apiKey, decryptKey);
if (setCache) {
await setCache(cacheKey, secretKey);
}
return secretKey;
}
/**
* 加密
* @param plainText
* @param secretKey
* @returns
*/
encrypt(plainText: string, secretKey: string) {
return encryptAES(plainText, secretKey);
}
/**
* 解密
* @param cipherText
* @param secretKey
* @returns
*/
decrypt(cipherText: string, secretKey: string) {
return decryptAES(cipherText, secretKey);
}
/**
* 获取模型配置
* @returns
*/
getSelectOpts() {
const { models, secretKeys = [] } = this.config;
return models.map((model) => {
const selectOpts = secretKeys.find((m) => m.group === model.group);
return {
...model,
...selectOpts,
};
});
}
getConfig(keepSecret?: boolean, config?: AIConfig) {
const chatConfig = config ?? this.config;
if (keepSecret) {
return chatConfig;
}
// 过滤掉secret中的所有apiKey移除掉并返回chatConfig
const { secretKeys = [], ...rest } = chatConfig || {};
return {
...rest,
secretKeys: secretKeys.map((item) => {
return {
...item,
apiKey: undefined,
decryptKey: undefined,
};
}),
};
}
}

View File

@@ -0,0 +1,34 @@
import { encoding_for_model, get_encoding } from 'tiktoken';
const MODEL_TO_ENCODING = {
'gpt-4': 'cl100k_base',
'gpt-4-turbo': 'cl100k_base',
'gpt-3.5-turbo': 'cl100k_base',
'text-embedding-ada-002': 'cl100k_base',
'text-davinci-002': 'p50k_base',
'text-davinci-003': 'p50k_base',
} as const;
export function numTokensFromString(text: string, model: keyof typeof MODEL_TO_ENCODING = 'gpt-3.5-turbo'): number {
try {
// 对于特定模型使用专门的编码器
const encoder = encoding_for_model(model);
const tokens = encoder.encode(text);
const tokenCount = tokens.length;
encoder.free(); // 释放编码器
return tokenCount;
} catch (error) {
try {
// 如果模型特定的编码器失败,尝试使用基础编码器
const encoder = get_encoding(MODEL_TO_ENCODING[model]);
const tokens = encoder.encode(text);
const tokenCount = tokens.length;
encoder.free(); // 释放编码器
return tokenCount;
} catch (error) {
// 如果编码失败使用一个粗略的估计平均每个字符0.25个token
return Math.ceil(text.length * 0.25);
}
}
}