Files
task-manager/server/ai.ts
T
2025-12-12 08:35:48 +01:00

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.";
}
}