feat: Implement AI Chat Agent, Email Notifications, and UI enhancements
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
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.";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user