mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-11-23 13:38:14 +00:00
Compare commits
5 Commits
a00f2231d4
...
f44ad973aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f44ad973aa | ||
|
|
4bcbdad6cb | ||
|
|
5272c7fd3e | ||
|
|
657a577ec8 | ||
|
|
f6dac43d7a |
@@ -20,9 +20,9 @@ const Copy = ({
|
|||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1000);
|
setTimeout(() => setCopied(false), 1000);
|
||||||
}}
|
}}
|
||||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{copied ? <Check size={18} /> : <ClipboardList size={18} />}
|
{copied ? <Check size={16} /> : <ClipboardList size={16} />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeftRight } from 'lucide-react';
|
import { ArrowLeftRight, Repeat } from 'lucide-react';
|
||||||
|
|
||||||
const Rewrite = ({
|
const Rewrite = ({
|
||||||
rewrite,
|
rewrite,
|
||||||
@@ -10,12 +10,11 @@ const Rewrite = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => rewrite(messageId)}
|
onClick={() => rewrite(messageId)}
|
||||||
className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
|
className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
|
||||||
>
|
>
|
||||||
<ArrowLeftRight size={18} />
|
<Repeat size={16} />
|
||||||
<p className="text-xs font-medium">Rewrite</p>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
1;
|
||||||
export default Rewrite;
|
export default Rewrite;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
StopCircle,
|
StopCircle,
|
||||||
Layers3,
|
Layers3,
|
||||||
Plus,
|
Plus,
|
||||||
|
CornerDownRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
|
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
|
||||||
import Copy from './MessageActions/Copy';
|
import Copy from './MessageActions/Copy';
|
||||||
@@ -122,14 +123,14 @@ const MessageBox = ({
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
|
|
||||||
{loading && isLast ? null : (
|
{loading && isLast ? null : (
|
||||||
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
|
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4">
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center -ml-2">
|
||||||
<Rewrite
|
<Rewrite
|
||||||
rewrite={rewrite}
|
rewrite={rewrite}
|
||||||
messageId={section.assistantMessage.messageId}
|
messageId={section.assistantMessage.messageId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center -mr-2">
|
||||||
<Copy
|
<Copy
|
||||||
initialMessage={section.assistantMessage.content}
|
initialMessage={section.assistantMessage.content}
|
||||||
section={section}
|
section={section}
|
||||||
@@ -142,12 +143,12 @@ const MessageBox = ({
|
|||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{speechStatus === 'started' ? (
|
{speechStatus === 'started' ? (
|
||||||
<StopCircle size={18} />
|
<StopCircle size={16} />
|
||||||
) : (
|
) : (
|
||||||
<Volume2 size={18} />
|
<Volume2 size={16} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +160,7 @@ const MessageBox = ({
|
|||||||
section.suggestions.length > 0 &&
|
section.suggestions.length > 0 &&
|
||||||
section.assistantMessage &&
|
section.assistantMessage &&
|
||||||
!loading && (
|
!loading && (
|
||||||
<div className="mt-8 pt-6 border-t border-light-200/50 dark:border-dark-200/50">
|
<div className="mt-6">
|
||||||
<div className="flex flex-row items-center space-x-2 mb-4">
|
<div className="flex flex-row items-center space-x-2 mb-4">
|
||||||
<Layers3
|
<Layers3
|
||||||
className="text-black dark:text-white"
|
className="text-black dark:text-white"
|
||||||
@@ -173,20 +174,24 @@ const MessageBox = ({
|
|||||||
{section.suggestions.map(
|
{section.suggestions.map(
|
||||||
(suggestion: string, i: number) => (
|
(suggestion: string, i: number) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
{i > 0 && (
|
<div className="h-px bg-light-200/40 dark:bg-dark-200/40" />
|
||||||
<div className="h-px bg-light-200/40 dark:bg-dark-200/40 mx-3" />
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => sendMessage(suggestion)}
|
onClick={() => sendMessage(suggestion)}
|
||||||
className="group w-full px-3 py-4 text-left transition-colors duration-200"
|
className="group w-full py-4 text-left transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<p className="text-sm text-black/70 dark:text-white/70 group-hover:text-[#24A0ED] transition-colors duration-200 leading-relaxed">
|
<div className="flex flex-row space-x-3 items-center ">
|
||||||
|
<CornerDownRight
|
||||||
|
size={17}
|
||||||
|
className="group-hover:text-sky-400 transition-colors duration-200"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-black/70 dark:text-white/70 group-hover:text-sky-400 transition-colors duration-200 leading-relaxed">
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<Plus
|
<Plus
|
||||||
size={16}
|
size={16}
|
||||||
className="text-black/40 dark:text-white/40 group-hover:text-[#24A0ED] transition-colors duration-200 flex-shrink-0"
|
className="text-black/40 dark:text-white/40 group-hover:text-sky-400 transition-colors duration-200 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
7
src/lib/models/base/embedding.ts
Normal file
7
src/lib/models/base/embedding.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
abstract class BaseEmbedding<CONFIG> {
|
||||||
|
constructor(protected config: CONFIG) {}
|
||||||
|
abstract embedText(texts: string[]): Promise<number[][]>;
|
||||||
|
abstract embedChunks(chunks: Chunk[]): Promise<number[][]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseEmbedding;
|
||||||
26
src/lib/models/base/llm.ts
Normal file
26
src/lib/models/base/llm.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateObjectOutput,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamObjectOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
abstract class BaseLLM<CONFIG> {
|
||||||
|
constructor(protected config: CONFIG) {}
|
||||||
|
abstract withOptions(options: GenerateOptions): this;
|
||||||
|
abstract generateText(input: GenerateTextInput): Promise<GenerateTextOutput>;
|
||||||
|
abstract streamText(
|
||||||
|
input: GenerateTextInput,
|
||||||
|
): AsyncGenerator<StreamTextOutput>;
|
||||||
|
abstract generateObject<T>(
|
||||||
|
input: GenerateObjectInput,
|
||||||
|
): Promise<GenerateObjectOutput<T>>;
|
||||||
|
abstract streamObject<T>(
|
||||||
|
input: GenerateObjectInput,
|
||||||
|
): AsyncGenerator<StreamObjectOutput<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseLLM;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Embeddings } from '@langchain/core/embeddings';
|
import { Embeddings } from '@langchain/core/embeddings';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
import { ModelList, ProviderMetadata } from '../types';
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
|
import BaseLLM from './llm';
|
||||||
|
import BaseEmbedding from './embedding';
|
||||||
|
|
||||||
abstract class BaseModelProvider<CONFIG> {
|
abstract class BaseModelProvider<CONFIG> {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -11,8 +13,8 @@ abstract class BaseModelProvider<CONFIG> {
|
|||||||
) {}
|
) {}
|
||||||
abstract getDefaultModels(): Promise<ModelList>;
|
abstract getDefaultModels(): Promise<ModelList>;
|
||||||
abstract getModelList(): Promise<ModelList>;
|
abstract getModelList(): Promise<ModelList>;
|
||||||
abstract loadChatModel(modelName: string): Promise<BaseChatModel>;
|
abstract loadChatModel(modelName: string): Promise<BaseLLM<any>>;
|
||||||
abstract loadEmbeddingModel(modelName: string): Promise<Embeddings>;
|
abstract loadEmbeddingModel(modelName: string): Promise<BaseEmbedding<any>>;
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
static getProviderConfigFields(): UIConfigField[] {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,11 @@
|
|||||||
import { ModelProviderUISection } from '@/lib/config/types';
|
import { ModelProviderUISection } from '@/lib/config/types';
|
||||||
import { ProviderConstructor } from './baseProvider';
|
import { ProviderConstructor } from '../base/provider';
|
||||||
import OpenAIProvider from './openai';
|
import OpenAIProvider from './openai';
|
||||||
import OllamaProvider from './ollama';
|
import OllamaProvider from './ollama';
|
||||||
import TransformersProvider from './transformers';
|
|
||||||
import AnthropicProvider from './anthropic';
|
|
||||||
import GeminiProvider from './gemini';
|
|
||||||
import GroqProvider from './groq';
|
|
||||||
import DeepSeekProvider from './deepseek';
|
|
||||||
import LMStudioProvider from './lmstudio';
|
|
||||||
import LemonadeProvider from './lemonade';
|
|
||||||
import AimlProvider from '@/lib/models/providers/aiml';
|
|
||||||
|
|
||||||
export const providers: Record<string, ProviderConstructor<any>> = {
|
export const providers: Record<string, ProviderConstructor<any>> = {
|
||||||
openai: OpenAIProvider,
|
openai: OpenAIProvider,
|
||||||
ollama: OllamaProvider,
|
ollama: OllamaProvider,
|
||||||
transformers: TransformersProvider,
|
|
||||||
anthropic: AnthropicProvider,
|
|
||||||
gemini: GeminiProvider,
|
|
||||||
groq: GroqProvider,
|
|
||||||
deepseek: DeepSeekProvider,
|
|
||||||
aiml: AimlProvider,
|
|
||||||
lmstudio: LMStudioProvider,
|
|
||||||
lemonade: LemonadeProvider,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModelProvidersUIConfigSection =
|
export const getModelProvidersUIConfigSection =
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOllama, OllamaEmbeddings } from '@langchain/ollama';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
||||||
|
import BaseModelProvider from '../../base/provider';
|
||||||
|
import { Model, ModelList, ProviderMetadata } from '../../types';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
import OllamaLLM from './ollamaLLM';
|
||||||
|
import OllamaEmbedding from './ollamaEmbedding';
|
||||||
|
|
||||||
interface OllamaConfig {
|
interface OllamaConfig {
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
@@ -76,7 +77,7 @@ class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
async loadChatModel(key: string): Promise<BaseLLM<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
const exists = modelList.chat.find((m) => m.key === key);
|
||||||
@@ -87,14 +88,13 @@ class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChatOllama({
|
return new OllamaLLM({
|
||||||
temperature: 0.7,
|
baseURL: this.config.baseURL,
|
||||||
model: key,
|
model: key,
|
||||||
baseUrl: this.config.baseURL,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
const exists = modelList.embedding.find((m) => m.key === key);
|
||||||
|
|
||||||
@@ -104,9 +104,9 @@ class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OllamaEmbeddings({
|
return new OllamaEmbedding({
|
||||||
model: key,
|
model: key,
|
||||||
baseUrl: this.config.baseURL,
|
baseURL: this.config.baseURL,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
39
src/lib/models/providers/ollama/ollamaEmbedding.ts
Normal file
39
src/lib/models/providers/ollama/ollamaEmbedding.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Ollama } from 'ollama';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
|
||||||
|
type OllamaConfig = {
|
||||||
|
model: string;
|
||||||
|
baseURL?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OllamaEmbedding extends BaseEmbedding<OllamaConfig> {
|
||||||
|
ollamaClient: Ollama;
|
||||||
|
|
||||||
|
constructor(protected config: OllamaConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.ollamaClient = new Ollama({
|
||||||
|
host: this.config.baseURL || 'http://localhost:11434',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedText(texts: string[]): Promise<number[][]> {
|
||||||
|
const response = await this.ollamaClient.embed({
|
||||||
|
input: texts,
|
||||||
|
model: this.config.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedChunks(chunks: Chunk[]): Promise<number[][]> {
|
||||||
|
const response = await this.ollamaClient.embed({
|
||||||
|
input: chunks.map((c) => c.content),
|
||||||
|
model: this.config.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.embeddings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OllamaEmbedding;
|
||||||
149
src/lib/models/providers/ollama/ollamaLLM.ts
Normal file
149
src/lib/models/providers/ollama/ollamaLLM.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import {
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
} from '../../types';
|
||||||
|
import { Ollama } from 'ollama';
|
||||||
|
import { parse } from 'partial-json';
|
||||||
|
|
||||||
|
type OllamaConfig = {
|
||||||
|
baseURL: string;
|
||||||
|
model: string;
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||||
|
ollamaClient: Ollama;
|
||||||
|
|
||||||
|
constructor(protected config: OllamaConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.ollamaClient = new Ollama({
|
||||||
|
host: this.config.baseURL || 'http://localhost:11434',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
withOptions(options: GenerateOptions) {
|
||||||
|
this.config.options = {
|
||||||
|
...this.config.options,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateText(input: GenerateTextInput): Promise<GenerateTextOutput> {
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const res = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
options: {
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
temperature: this.config.options?.temperature,
|
||||||
|
num_predict: this.config.options?.maxTokens,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: res.message.content,
|
||||||
|
additionalInfo: {
|
||||||
|
reasoning: res.message.thinking,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamText(
|
||||||
|
input: GenerateTextInput,
|
||||||
|
): AsyncGenerator<StreamTextOutput> {
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const stream = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
stream: true,
|
||||||
|
options: {
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
temperature: this.config.options?.temperature,
|
||||||
|
num_predict: this.config.options?.maxTokens,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
yield {
|
||||||
|
contentChunk: chunk.message.content,
|
||||||
|
done: chunk.done,
|
||||||
|
additionalInfo: {
|
||||||
|
reasoning: chunk.message.thinking,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const response = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
format: z.toJSONSchema(input.schema),
|
||||||
|
options: {
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
temperature: this.config.options?.temperature,
|
||||||
|
num_predict: this.config.options?.maxTokens,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return input.schema.parse(JSON.parse(response.message.content)) as T;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error parsing response from Ollama: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamObject<T>(input: GenerateObjectInput): AsyncGenerator<T> {
|
||||||
|
let recievedObj: string = '';
|
||||||
|
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const stream = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
format: z.toJSONSchema(input.schema),
|
||||||
|
stream: true,
|
||||||
|
options: {
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
temperature: this.config.options?.temperature,
|
||||||
|
num_predict: this.config.options?.maxTokens,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
recievedObj += chunk.message.content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield parse(recievedObj) as T;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error parsing partial object from Ollama:', err);
|
||||||
|
yield {} as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OllamaLLM;
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
import { Embeddings } from '@langchain/core/embeddings';
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
||||||
|
import { Model, ModelList, ProviderMetadata } from '../../types';
|
||||||
|
import OpenAIEmbedding from './openaiEmbedding';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
import BaseModelProvider from '../../base/provider';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import OpenAILLM from './openaiLLM';
|
||||||
|
|
||||||
interface OpenAIConfig {
|
interface OpenAIConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -145,7 +148,7 @@ class OpenAIProvider extends BaseModelProvider<OpenAIConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
async loadChatModel(key: string): Promise<BaseLLM<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
const exists = modelList.chat.find((m) => m.key === key);
|
||||||
@@ -156,17 +159,14 @@ class OpenAIProvider extends BaseModelProvider<OpenAIConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChatOpenAI({
|
return new OpenAILLM({
|
||||||
apiKey: this.config.apiKey,
|
apiKey: this.config.apiKey,
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
model: key,
|
||||||
configuration: {
|
|
||||||
baseURL: this.config.baseURL,
|
baseURL: this.config.baseURL,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
const exists = modelList.embedding.find((m) => m.key === key);
|
||||||
|
|
||||||
@@ -176,12 +176,10 @@ class OpenAIProvider extends BaseModelProvider<OpenAIConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OpenAIEmbeddings({
|
return new OpenAIEmbedding({
|
||||||
apiKey: this.config.apiKey,
|
apiKey: this.config.apiKey,
|
||||||
model: key,
|
model: key,
|
||||||
configuration: {
|
|
||||||
baseURL: this.config.baseURL,
|
baseURL: this.config.baseURL,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
41
src/lib/models/providers/openai/openaiEmbedding.ts
Normal file
41
src/lib/models/providers/openai/openaiEmbedding.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
|
||||||
|
type OpenAIConfig = {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
baseURL?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpenAIEmbedding extends BaseEmbedding<OpenAIConfig> {
|
||||||
|
openAIClient: OpenAI;
|
||||||
|
|
||||||
|
constructor(protected config: OpenAIConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.openAIClient = new OpenAI({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedText(texts: string[]): Promise<number[][]> {
|
||||||
|
const response = await this.openAIClient.embeddings.create({
|
||||||
|
model: this.config.model,
|
||||||
|
input: texts,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.map((embedding) => embedding.embedding);
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedChunks(chunks: Chunk[]): Promise<number[][]> {
|
||||||
|
const response = await this.openAIClient.embeddings.create({
|
||||||
|
model: this.config.model,
|
||||||
|
input: chunks.map((c) => c.content),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.map((embedding) => embedding.embedding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenAIEmbedding;
|
||||||
163
src/lib/models/providers/openai/openaiLLM.ts
Normal file
163
src/lib/models/providers/openai/openaiLLM.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import { zodTextFormat, zodResponseFormat } from 'openai/helpers/zod';
|
||||||
|
import {
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
} from '../../types';
|
||||||
|
import { parse } from 'partial-json';
|
||||||
|
|
||||||
|
type OpenAIConfig = {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
baseURL?: string;
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpenAILLM extends BaseLLM<OpenAIConfig> {
|
||||||
|
openAIClient: OpenAI;
|
||||||
|
|
||||||
|
constructor(protected config: OpenAIConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.openAIClient = new OpenAI({
|
||||||
|
apiKey: this.config.apiKey,
|
||||||
|
baseURL: this.config.baseURL || 'https://api.openai.com/v1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
withOptions(options: GenerateOptions) {
|
||||||
|
this.config.options = {
|
||||||
|
...this.config.options,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateText(input: GenerateTextInput): Promise<GenerateTextOutput> {
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const response = await this.openAIClient.chat.completions.create({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
temperature: this.config.options?.temperature || 1.0,
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
max_completion_tokens: this.config.options?.maxTokens,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.choices && response.choices.length > 0) {
|
||||||
|
return {
|
||||||
|
content: response.choices[0].message.content!,
|
||||||
|
additionalInfo: {
|
||||||
|
finishReason: response.choices[0].finish_reason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No response from OpenAI');
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamText(
|
||||||
|
input: GenerateTextInput,
|
||||||
|
): AsyncGenerator<StreamTextOutput> {
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const stream = await this.openAIClient.chat.completions.create({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
temperature: this.config.options?.temperature || 1.0,
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
max_completion_tokens: this.config.options?.maxTokens,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
stream: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.choices && chunk.choices.length > 0) {
|
||||||
|
yield {
|
||||||
|
contentChunk: chunk.choices[0].delta.content || '',
|
||||||
|
done: chunk.choices[0].finish_reason !== null,
|
||||||
|
additionalInfo: {
|
||||||
|
finishReason: chunk.choices[0].finish_reason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const response = await this.openAIClient.chat.completions.parse({
|
||||||
|
messages: input.messages,
|
||||||
|
model: this.config.model,
|
||||||
|
temperature: this.config.options?.temperature || 1.0,
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
max_completion_tokens: this.config.options?.maxTokens,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
response_format: zodResponseFormat(input.schema, 'object'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.choices && response.choices.length > 0) {
|
||||||
|
try {
|
||||||
|
return input.schema.parse(response.choices[0].message.parsed) as T;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error parsing response from OpenAI: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No response from OpenAI');
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamObject<T>(input: GenerateObjectInput): AsyncGenerator<T> {
|
||||||
|
let recievedObj: string = '';
|
||||||
|
|
||||||
|
this.withOptions(input.options || {});
|
||||||
|
|
||||||
|
const stream = this.openAIClient.responses.stream({
|
||||||
|
model: this.config.model,
|
||||||
|
input: input.messages,
|
||||||
|
temperature: this.config.options?.temperature || 1.0,
|
||||||
|
top_p: this.config.options?.topP,
|
||||||
|
max_completion_tokens: this.config.options?.maxTokens,
|
||||||
|
stop: this.config.options?.stopSequences,
|
||||||
|
frequency_penalty: this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty: this.config.options?.presencePenalty,
|
||||||
|
text: {
|
||||||
|
format: zodTextFormat(input.schema, 'object'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.type === 'response.output_text.delta' && chunk.delta) {
|
||||||
|
recievedObj += chunk.delta;
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield parse(recievedObj) as T;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error parsing partial object from OpenAI:', err);
|
||||||
|
yield {} as T;
|
||||||
|
}
|
||||||
|
} else if (chunk.type === 'response.output_text.done' && chunk.text) {
|
||||||
|
try {
|
||||||
|
yield parse(chunk.text) as T;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error parsing response from OpenAI: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenAILLM;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
type Model = {
|
type Model = {
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -25,10 +27,59 @@ type ModelWithProvider = {
|
|||||||
providerId: string;
|
providerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GenerateOptions = {
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
topP?: number;
|
||||||
|
stopSequences?: string[];
|
||||||
|
frequencyPenalty?: number;
|
||||||
|
presencePenalty?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateTextInput = {
|
||||||
|
messages: Message[];
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateTextOutput = {
|
||||||
|
content: string;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamTextOutput = {
|
||||||
|
contentChunk: string;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
done?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateObjectInput = {
|
||||||
|
schema: z.ZodTypeAny;
|
||||||
|
messages: Message[];
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateObjectOutput<T> = {
|
||||||
|
object: T;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamObjectOutput<T> = {
|
||||||
|
objectChunk: Partial<T>;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
done?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
Model,
|
Model,
|
||||||
ModelList,
|
ModelList,
|
||||||
ProviderMetadata,
|
ProviderMetadata,
|
||||||
MinimalProvider,
|
MinimalProvider,
|
||||||
ModelWithProvider,
|
ModelWithProvider,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateObjectOutput,
|
||||||
|
StreamObjectOutput,
|
||||||
};
|
};
|
||||||
|
|||||||
9
src/lib/types.ts
Normal file
9
src/lib/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
type Message = {
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Chunk = {
|
||||||
|
content: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user