feat: Complete AI Chat Agent, Admin Settings (MCP/AI), and Translations
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-15 21:52:36 +01:00
parent 71d5c825c5
commit 9736864425
24 changed files with 1679 additions and 401 deletions
+139 -15
View File
@@ -6,6 +6,28 @@ interface ChatMessage {
content: string;
}
export const DEFAULT_SYSTEM_PROMPT = `You are TaskFlow AI, an intelligent productivity assistant integrated into a personal task management system.
Your goal is to help the user organize their life, manage tasks, and achieve their goals.
Available Tools:
- create_task: Create a new task with title, description, due date, priority.
- list_tasks: Search and list tasks.
- update_task: Modify existing tasks.
- create_label: Create organization labels.
- create_note: Save quick notes or ideas.
Context Variables:
- \${user.username}: The current user's name.
- \${currentDate}: The current date and time.
- \${context}: The page the user is currently looking at.
Personality:
- Be concise, helpful, and direct.
- Proactively suggest organization improvements.
- When creating tasks, infer priorities and due dates if not specified.
- Use emoji sparingly but effectively to make the chat friendly.
`;
export class AiService {
constructor(private storage: IStorage) { }
@@ -19,11 +41,19 @@ export class AiService {
throw new Error("AI API Key not configured");
}
const systemPrompt = `You are TaskFlow AI, an intelligent assistant for the TaskFlow application.
const currentDate = new Date();
// Fetch system prompt from DB or use default/fallback
const storedPrompt = await this.storage.getSystemSettings("ai_system_prompt");
let systemPrompt = storedPrompt;
if (!systemPrompt) {
// Fallback (should be seeded, but just in case)
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}
User Name: \${user.username}
Current Date/Time: \${currentDate.toLocaleString('de-DE')} (Day: \${currentDate.toLocaleDateString('en-US', { weekday: 'long' })})
Current Context:
${context}
\${context}
Answer the user's questions based on this context. Be concise, helpful, and friendly.
@@ -32,15 +62,18 @@ You can create, search, update, and delete tasks using the provided tools.
**1. Task Management**
- **Create**: Use 'createTask'. Title is required.
- *Subtasks*: To break a task down, create new tasks with 'parentTaskId' set to the main task's ID.
- *Planning*: You can set 'estimatedDuration' (minutes) and 'startDate'.
- **Update/Delete**: First SEARCH for the task ID using 'searchTasks', then use 'updateTask' or 'deleteTask'.
- **Labels**: Use 'getLabels' to see available tags.
- *Bulk Creation*: If the user provides a list of tasks, call 'createTask' multiple times in parallel.
- *Relative Dates*: Understand natural language! "Morgen" = Tomorrow, "Next Friday" = Date of next Friday. Always calculate the specific ISO string based on 'Current Date/Time'.
- *Returns*: The tool returns the created Task ID. Remember this ID for immediate edits.
- **Update/Delete**: First SEARCH for the task ID using 'searchTasks' (search by title), then use 'updateTask' or 'deleteTask'.
- *Editing Recently Created*: If the user says "Change that to...", refer to the ID of the task you just created.
- *Delete All*: Search all, then delete each.
- **Labels**: Use 'getLabels' tags.
**2. 🧠 Smart Planning & Scheduling**
- **"Break this down"**: If a user asks to break down a project, create strictly hierarchical subtasks using 'createTask' with 'parentTaskId'.
- **"Find time for this"**: Use 'scheduleTask'. This tool automatically finds free slots in the user's calendar (based on task duration) and sets the due date.
- **Time Boxing**: If a user mentions how long something takes ("...will take 2 hours"), ALWAYS set 'estimatedDuration' (in minutes) when creating/updating.
- **"Break this down"**: Create subtasks with 'parentTaskId'.
- **"Find time for this"**: Use 'scheduleTask'.
- **Time Boxing**: Set 'estimatedDuration' (minutes) if mentioned.
**3. Gamification**
- Check achievements/XP with 'getAchievements'.
@@ -48,12 +81,21 @@ You can create, search, update, and delete tasks using the provided tools.
### 📅 DATE & TIME RULES
- **"Today"**: Use the 'Current Date/Time' context.
- **Queries**: When asked for "today's tasks", only list tasks where 'dueDate' matches today.
- **Scheduling**: When using 'scheduleTask', inform the user specifically *when* you scheduled it (e.g., "I've scheduled this for tomorrow at 2:00 PM").
- **"Morgen" / "Tomorrow"**: Add 1 day to Current Date.
- **Scheduling**: When using 'scheduleTask', inform the user specifically *when* you scheduled it.
IMPORTANT: Do NOT show Task IDs to the user. Reference tasks by Title.
CRITICAL: After executing tools, provide a concise summary of your actions.
`;
CRITICAL: After executing tools, provide a concise summary of your actions.`;
}
// Variable replacement if using stored template
// Note: The stored prompt will have ${user.username} as text. We need to replace it.
// Simple manual replacement for the keys we expect.
systemPrompt = systemPrompt
.replace(/\${user.username}/g, user.username)
.replace(/\${currentDate.toLocaleString\('de-DE'\)}/g, currentDate.toLocaleString('de-DE'))
.replace(/\${currentDate.toLocaleDateString\('en-US', { weekday: 'long' }\)}/g, currentDate.toLocaleDateString('en-US', { weekday: 'long' }))
.replace(/\${context}/g, context);
const fullMessages = [
{ role: "system", content: systemPrompt },
@@ -296,6 +338,21 @@ CRITICAL: After executing tools, provide a concise summary of your actions.
parameters: { type: "object", properties: {}, required: [] }
}
},
{
type: "function",
function: {
name: "createLabel",
description: "Create a new label or get existing one if name matches.",
parameters: {
type: "object",
properties: {
name: { type: "string" },
color: { type: "string", description: "Hex color code (optional)" }
},
required: ["name"]
}
}
},
{
type: "function",
function: {
@@ -398,7 +455,25 @@ CRITICAL: After executing tools, provide a concise summary of your actions.
try {
console.log(`[AI] Executing ${fnName}:`, args);
if (fnName === "createTask") {
if (fnName === "createLabel") {
const labels = await this.storage.getLabels(user.id);
const existing = labels.find(l => l.name.toLowerCase() === args.name.toLowerCase());
if (existing) {
result = { success: true, labelId: existing.id, message: "Label already exists." };
} else {
const newLabel = await this.storage.createLabel({
name: args.name,
color: args.color || "#6366f1", // Default Indigo
// userId: user.id // Removed as it's not in InsertLabelSchema ?? Wait, check schema.
// Actually, let's check schema. If schema demands it, I must provide it.
// But lint said: 'userId' does not exist in type '{ name: string; color: string; creatorId?: string | null | undefined; }'.
// So it's probably 'creatorId' or implied by context?
// Start with assuming it is creatorId based on error message hint.
creatorId: user.id
});
result = { success: true, labelId: newLabel.id, message: "Label created." };
}
} else if (fnName === "createTask") {
const taskData = {
title: args.title,
description: args.description || null,
@@ -564,6 +639,55 @@ CRITICAL: After executing tools, provide a concise summary of your actions.
}
}
async analyzeTaskCompletion(task: InsertTask, context: string): Promise<{ needed: boolean; title?: string; description?: 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") return { needed: false };
const prompt = `Analyze this COMPLETED task and determine if a follow-up task is logically required.
Task Title: ${task.title}
Task Description: ${task.description || "None"}
Context: ${context}
Examples:
- "Send contract to client" -> Follow-up: "Check if client signed contract"
- "Buy groceries" -> No follow-up usually.
- "Email Sarah about project" -> Follow-up: "Follow up with Sarah if no reply"
Return ONLY a JSON object. Do not include markdown formatting.
Format:
{
"needed": boolean,
"title": "string (optional, required if needed is true)",
"description": "string (optional)"
}`;
const messages: ChatMessage[] = [
{ role: "system", content: "You are a helpful task management assistant. Output valid JSON only." },
{ role: "user", content: prompt }
];
try {
let responseText = "";
if (provider === "openai" || provider === "ollama") {
responseText = await this.chatOpenAI(provider, apiKey || "", model, baseUrl, messages, undefined, []);
} else if (provider === "anthropic") {
responseText = await this.chatAnthropic(apiKey || "", model, messages);
} else if (provider === "google") {
responseText = await this.chatGemini(apiKey || "", model, messages);
}
// Clean response (sometimes LMs add backticks)
const cleanJson = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
return JSON.parse(cleanJson);
} catch (error) {
console.error("AI Analysis Error:", error);
return { needed: false };
}
}
private async chatAnthropic(apiKey: string, model: string, messages: any[]): Promise<string> {
const systemMessage = messages.find(m => m.role === "system")?.content || "";