148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
import { IStorage } from "./storage";
|
|
import { User } from "../shared/schema";
|
|
|
|
interface ChatMessage {
|
|
role: "system" | "user" | "assistant";
|
|
content: string;
|
|
}
|
|
|
|
export class AiService {
|
|
constructor(private storage: IStorage) { }
|
|
|
|
async chat(messages: ChatMessage[], user: User, context: string): Promise<string> {
|
|
const provider = await this.storage.getSystemSettings("ai_provider") || "openai";
|
|
const apiKey = await this.storage.getSystemSettings("ai_api_key");
|
|
const model = await this.storage.getSystemSettings("ai_model") || "gpt-4o";
|
|
const baseUrl = await this.storage.getSystemSettings("ai_base_url");
|
|
|
|
if (!apiKey && provider !== "ollama") {
|
|
throw new Error("AI API Key not configured");
|
|
}
|
|
|
|
const systemPrompt = `You are TaskFlow AI, an intelligent assistant for the TaskFlow application.
|
|
You have access to the user's current tasks and context.
|
|
User Name: ${user.username}
|
|
Current Context:
|
|
${context}
|
|
|
|
Answer the user's questions based on this context. Be concise, helpful, and friendly.
|
|
If needed, suggest they create tasks or manage their schedule (you cannot perform actions yet, only advise).
|
|
`;
|
|
|
|
const fullMessages = [
|
|
{ role: "system", content: systemPrompt },
|
|
...messages
|
|
];
|
|
|
|
try {
|
|
if (provider === "openai" || provider === "ollama") {
|
|
return await this.chatOpenAI(provider, apiKey || "", model, baseUrl, fullMessages);
|
|
} else if (provider === "anthropic") {
|
|
return await this.chatAnthropic(apiKey || "", model, fullMessages);
|
|
} else if (provider === "google") {
|
|
return await this.chatGemini(apiKey || "", model, fullMessages);
|
|
} else {
|
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
}
|
|
} catch (error: any) {
|
|
console.error("AI Chat Error:", error);
|
|
throw new Error(`AI Service Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
private async chatOpenAI(provider: string, apiKey: string, model: string, baseUrl: string | undefined, messages: any[]): Promise<string> {
|
|
const url = baseUrl || (provider === "ollama" ? "http://localhost:11434/v1" : "https://api.openai.com/v1") + "/chat/completions";
|
|
|
|
// Clean URL
|
|
const cleanUrl = url.replace(/([^:]\/)\/+/g, "$1"); // remove double slashes
|
|
|
|
const response = await fetch(cleanUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${apiKey}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: model,
|
|
messages: messages,
|
|
temperature: 0.7,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.text();
|
|
throw new Error(`OpenAI/Ollama API Error ${response.status}: ${err}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.choices[0]?.message?.content || "No response generated.";
|
|
}
|
|
|
|
private async chatAnthropic(apiKey: string, model: string, messages: any[]): Promise<string> {
|
|
// Anthropic doesn't support "system" role in messages list in the same way, need to extract it
|
|
const systemMessage = messages.find(m => m.role === "system")?.content || "";
|
|
const userAssistantMessages = messages.filter(m => m.role !== "system");
|
|
|
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-api-key": apiKey,
|
|
"anthropic-version": "2023-06-01",
|
|
},
|
|
body: JSON.stringify({
|
|
model: model,
|
|
system: systemMessage,
|
|
messages: userAssistantMessages,
|
|
max_tokens: 1024,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.text();
|
|
throw new Error(`Anthropic API Error ${response.status}: ${err}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.content[0]?.text || "No response generated.";
|
|
}
|
|
|
|
private async chatGemini(apiKey: string, model: string, messages: any[]): Promise<string> {
|
|
// Google Generative AI (Gemini)
|
|
// POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=YOUR_API_KEY
|
|
|
|
// Mapping messages to Gemini format (contents: [{ role, parts: [{ text }] }])
|
|
// System instruction is supported in v1beta/models/...:generateContent?
|
|
// Gemini 1.5 Pro supports systemInstructions.
|
|
// For simplicity, I'll prepend system prompt to first user message.
|
|
|
|
const systemMessage = messages.find(m => m.role === "system")?.content || "";
|
|
const contentMessages = messages.filter(m => m.role !== "system").map(m => ({
|
|
role: m.role === "user" ? "user" : "model",
|
|
parts: [{ text: m.content }]
|
|
}));
|
|
|
|
if (contentMessages.length > 0 && contentMessages[0].role === "user") {
|
|
contentMessages[0].parts[0].text = `[System Instruction: ${systemMessage}]\n\nWait for user input... User Input: ` + contentMessages[0].parts[0].text;
|
|
}
|
|
|
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
contents: contentMessages
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.text();
|
|
throw new Error(`Gemini API Error ${response.status}: ${err}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.candidates?.[0]?.content?.parts?.[0]?.text || "No response generated.";
|
|
}
|
|
}
|