feat: Complete AI Chat Agent, Admin Settings (MCP/AI), and Translations
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+139
-15
@@ -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 || "";
|
||||
|
||||
Reference in New Issue
Block a user