@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
613 lines (527 loc) • 15.7 kB
text/typescript
import {
LLMProvider,
LLMConfig,
Document,
ChatbotError,
} from "../contexts/ChatbotContext";
// LLM response interface
export interface LLMResponse {
content: string;
metadata?: {
model?: string;
tokens?: {
prompt: number;
completion: number;
total: number;
};
finishReason?: string;
confidence?: number;
};
}
// RAG response interface
export interface RAGResponse {
response: string;
sources: Document[];
metadata?: {
retrievedDocCount: number;
similarity: number[];
model?: string;
tokens?: {
prompt: number;
completion: number;
total: number;
};
};
}
// Abstract base class for LLM operations
export abstract class BaseLLM {
protected config: LLMConfig;
protected isConnected: boolean = false;
protected availableModels: string[] = [];
constructor(config: LLMConfig) {
this.config = config;
}
abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;
abstract generateResponse(
prompt: string,
context?: Document[]
): Promise<LLMResponse>;
abstract listModels(): Promise<string[]>;
abstract validateApiKey(): Promise<boolean>;
abstract healthCheck(): Promise<boolean>;
public getStatus(): {
isConnected: boolean;
config: LLMConfig;
availableModels: string[];
} {
return {
isConnected: this.isConnected,
config: this.config,
availableModels: this.availableModels,
};
}
public updateConfig(updates: Partial<LLMConfig>): void {
this.config = { ...this.config, ...updates };
}
}
// OpenAI LLM Implementation
export class OpenAILLM extends BaseLLM {
private client: any = null;
constructor(config: LLMConfig) {
super(config);
}
async connect(): Promise<void> {
try {
if (!this.config.apiKey) {
throw new Error("OpenAI API key is required");
}
// Dynamic import to avoid bundling issues
const { OpenAI } = await import("openai");
this.client = new OpenAI({
apiKey: this.config.apiKey,
dangerouslyAllowBrowser: true, // For client-side usage
});
// Test connection
await this.validateApiKey();
// Get available models
this.availableModels = await this.listModels();
this.isConnected = true;
} catch (error) {
this.isConnected = false;
throw new Error(
`Failed to connect to OpenAI: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
async disconnect(): Promise<void> {
this.client = null;
this.isConnected = false;
this.availableModels = [];
}
async generateResponse(
prompt: string,
context?: Document[]
): Promise<LLMResponse> {
if (!this.isConnected || !this.client) {
throw new Error("LLM is not connected");
}
try {
// Build context if provided
let contextualPrompt = prompt;
if (context && context.length > 0) {
const contextText = context
.map((doc) => `Document: ${doc.title}\nContent: ${doc.content}`)
.join("\n\n");
contextualPrompt = `Context:\n${contextText}\n\nQuestion: ${prompt}\n\nPlease answer the question using the provided context. If the context doesn't contain relevant information, say so.`;
}
const systemPrompt =
this.config.systemPrompt || "You are a helpful AI assistant.";
const response = await this.client.chat.completions.create({
model: this.config.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: contextualPrompt },
],
temperature: this.config.temperature || 0.7,
max_tokens: this.config.maxTokens || 1000,
top_p: this.config.topP || 1,
frequency_penalty: this.config.frequencyPenalty || 0,
presence_penalty: this.config.presencePenalty || 0,
});
const choice = response.choices[0];
if (!choice || !choice.message) {
throw new Error("No response generated");
}
return {
content: choice.message.content || "",
metadata: {
model: response.model,
tokens: {
prompt: response.usage?.prompt_tokens || 0,
completion: response.usage?.completion_tokens || 0,
total: response.usage?.total_tokens || 0,
},
finishReason: choice.finish_reason || undefined,
},
};
} catch (error) {
throw new Error(
`Failed to generate response: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
async listModels(): Promise<string[]> {
if (!this.client) {
return ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"]; // Fallback models
}
try {
const models = await this.client.models.list();
return models.data
.filter((model: any) => model.id.includes("gpt"))
.map((model: any) => model.id)
.sort();
} catch (error) {
console.warn("Failed to list OpenAI models, using fallback:", error);
return ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"];
}
}
async validateApiKey(): Promise<boolean> {
if (!this.client) return false;
try {
await this.client.models.list();
return true;
} catch (error) {
return false;
}
}
async healthCheck(): Promise<boolean> {
return this.isConnected && (await this.validateApiKey());
}
}
// Anthropic LLM Implementation
export class AnthropicLLM extends BaseLLM {
private client: any = null;
constructor(config: LLMConfig) {
super(config);
}
async connect(): Promise<void> {
try {
if (!this.config.apiKey) {
throw new Error("Anthropic API key is required");
}
// Dynamic import to avoid bundling issues
const { Anthropic } = await import("@anthropic-ai/sdk");
this.client = new Anthropic({
apiKey: this.config.apiKey,
dangerouslyAllowBrowser: true,
});
// Test connection
if (await this.validateApiKey()) {
this.availableModels = await this.listModels();
this.isConnected = true;
} else {
throw new Error("Invalid API key");
}
} catch (error) {
this.isConnected = false;
throw new Error(
`Failed to connect to Anthropic: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
async disconnect(): Promise<void> {
this.client = null;
this.isConnected = false;
this.availableModels = [];
}
async generateResponse(
prompt: string,
context?: Document[]
): Promise<LLMResponse> {
if (!this.isConnected || !this.client) {
throw new Error("LLM is not connected");
}
try {
// Build context if provided
let contextualPrompt = prompt;
if (context && context.length > 0) {
const contextText = context
.map((doc) => `Document: ${doc.title}\nContent: ${doc.content}`)
.join("\n\n");
contextualPrompt = `Context:\n${contextText}\n\nQuestion: ${prompt}\n\nPlease answer the question using the provided context.`;
}
const systemPrompt =
this.config.systemPrompt || "You are a helpful AI assistant.";
const response = await this.client.messages.create({
model: this.config.model,
system: systemPrompt,
messages: [{ role: "user", content: contextualPrompt }],
temperature: this.config.temperature || 0.7,
max_tokens: this.config.maxTokens || 1000,
top_p: this.config.topP || 1,
});
if (!response.content || response.content.length === 0) {
throw new Error("No response generated");
}
const content = response.content[0];
if (content.type !== "text") {
throw new Error("Unexpected response type");
}
return {
content: content.text,
metadata: {
model: response.model,
tokens: {
prompt: response.usage.input_tokens,
completion: response.usage.output_tokens,
total: response.usage.input_tokens + response.usage.output_tokens,
},
finishReason: response.stop_reason || undefined,
},
};
} catch (error) {
throw new Error(
`Failed to generate response: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
async listModels(): Promise<string[]> {
// Anthropic doesn't have a models API, so return known models
return [
"claude-3-haiku-20240307",
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-5-sonnet-20241022",
];
}
async validateApiKey(): Promise<boolean> {
if (!this.client) return false;
try {
// Simple validation request
await this.client.messages.create({
model: "claude-3-haiku-20240307",
messages: [{ role: "user", content: "test" }],
max_tokens: 1,
});
return true;
} catch (error) {
return false;
}
}
async healthCheck(): Promise<boolean> {
return this.isConnected && (await this.validateApiKey());
}
}
// Google LLM Implementation (placeholder)
export class GoogleLLM extends BaseLLM {
async connect(): Promise<void> {
throw new Error("Google LLM not implemented yet");
}
async disconnect(): Promise<void> {
this.isConnected = false;
}
async generateResponse(
prompt: string,
context?: Document[]
): Promise<LLMResponse> {
throw new Error("Google LLM not implemented yet");
}
async listModels(): Promise<string[]> {
return [];
}
async validateApiKey(): Promise<boolean> {
return false;
}
async healthCheck(): Promise<boolean> {
return false;
}
}
// Mistral LLM Implementation (placeholder)
export class MistralLLM extends BaseLLM {
async connect(): Promise<void> {
throw new Error("Mistral LLM not implemented yet");
}
async disconnect(): Promise<void> {
this.isConnected = false;
}
async generateResponse(
prompt: string,
context?: Document[]
): Promise<LLMResponse> {
throw new Error("Mistral LLM not implemented yet");
}
async listModels(): Promise<string[]> {
return [];
}
async validateApiKey(): Promise<boolean> {
return false;
}
async healthCheck(): Promise<boolean> {
return false;
}
}
// Factory function to create LLM instances
export function createLLM(config: LLMConfig): BaseLLM {
switch (config.provider) {
case LLMProvider.OPENAI:
return new OpenAILLM(config);
case LLMProvider.ANTHROPIC:
return new AnthropicLLM(config);
case LLMProvider.GOOGLE:
return new GoogleLLM(config);
case LLMProvider.MISTRAL:
return new MistralLLM(config);
default:
throw new Error(`Unsupported LLM provider: ${config.provider}`);
}
}
// RAG Chain Service
export class RAGChainService {
private llm: BaseLLM;
private vectorStore: any;
constructor(llm: BaseLLM, vectorStore: any) {
this.llm = llm;
this.vectorStore = vectorStore;
}
async generateRAGResponse(
query: string,
options: {
maxRetrievedDocs?: number;
similarityThreshold?: number;
} = {}
): Promise<RAGResponse> {
const { maxRetrievedDocs = 5, similarityThreshold = 0.7 } = options;
try {
// Step 1: Retrieve relevant documents
const retrievedDocs = await this.vectorStore.similaritySearch(
query,
maxRetrievedDocs
);
// Step 2: Filter by similarity threshold if needed
let filteredDocs = retrievedDocs;
if (similarityThreshold > 0) {
// Note: This would require similarity scores from the vector store
// For now, we'll use all retrieved documents
filteredDocs = retrievedDocs;
}
// Step 3: Generate response using LLM with context
const llmResponse = await this.llm.generateResponse(query, filteredDocs);
return {
response: llmResponse.content,
sources: filteredDocs,
metadata: {
retrievedDocCount: retrievedDocs.length,
similarity: [], // Would be populated with actual similarity scores
model: llmResponse.metadata?.model,
tokens: llmResponse.metadata?.tokens,
},
};
} catch (error) {
throw new Error(
`Failed to generate RAG response: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
updateLLM(llm: BaseLLM): void {
this.llm = llm;
}
updateVectorStore(vectorStore: any): void {
this.vectorStore = vectorStore;
}
}
// LLM utilities
export class LLMUtils {
static validateConfig(config: LLMConfig): boolean {
if (!config.provider || !config.model) return false;
switch (config.provider) {
case LLMProvider.OPENAI:
case LLMProvider.ANTHROPIC:
case LLMProvider.GOOGLE:
case LLMProvider.MISTRAL:
return !!config.apiKey;
default:
return false;
}
}
static getDefaultConfig(provider: LLMProvider): Partial<LLMConfig> {
const baseConfig = {
provider,
temperature: 0.7,
maxTokens: 1000,
topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
};
switch (provider) {
case LLMProvider.OPENAI:
return {
...baseConfig,
model: "gpt-3.5-turbo",
};
case LLMProvider.ANTHROPIC:
return {
...baseConfig,
model: "claude-3-haiku-20240307",
};
case LLMProvider.GOOGLE:
return {
...baseConfig,
model: "gemini-pro",
};
case LLMProvider.MISTRAL:
return {
...baseConfig,
model: "mistral-small",
};
default:
return baseConfig;
}
}
static buildContextPrompt(
query: string,
documents: Document[],
systemPrompt?: string
): string {
const contextText = documents
.map((doc, index) => `[${index + 1}] ${doc.title}\n${doc.content}`)
.join("\n\n");
const prompt =
systemPrompt ||
"You are a helpful AI assistant. Use the provided context to answer questions accurately.";
return `${prompt}
Context:
${contextText}
Question: ${query}
Please answer the question using the information from the provided context. If the context doesn't contain enough information to answer the question, please say so clearly.`;
}
static estimateTokens(text: string): number {
// Rough estimation: ~4 characters per token for English text
return Math.ceil(text.length / 4);
}
static truncateContext(documents: Document[], maxTokens: number): Document[] {
let totalTokens = 0;
const truncatedDocs: Document[] = [];
for (const doc of documents) {
const docTokens = this.estimateTokens(doc.content);
if (totalTokens + docTokens <= maxTokens) {
truncatedDocs.push(doc);
totalTokens += docTokens;
} else {
// Truncate the document content to fit
const remainingTokens = maxTokens - totalTokens;
const remainingChars = remainingTokens * 4;
if (remainingChars > 100) {
// Only include if we have meaningful content
const truncatedDoc = {
...doc,
content: doc.content.substring(0, remainingChars) + "...",
};
truncatedDocs.push(truncatedDoc);
}
break;
}
}
return truncatedDocs;
}
}
export default {
BaseLLM,
OpenAILLM,
AnthropicLLM,
GoogleLLM,
MistralLLM,
createLLM,
RAGChainService,
LLMUtils,
};