feat: enhance audit logging, add MCP settings, and production docker setup
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Implemented comprehensive audit logging for Tasks, Users, Settings, Goals, Labels, AI Chat, and Rewards. - Added Admin UI for MCP Server settings and Audit Logs. - Created docker-compose-production.yml with Traefik configuration. - Fixed backend bugs (missing storage methods, route closure). - Added Audit Logging Guidelines.
This commit is contained in:
+512
-34
@@ -1,5 +1,5 @@
|
||||
import { IStorage } from "./storage";
|
||||
import { User } from "../shared/schema";
|
||||
import { User, InsertTask, insertTaskSchema } from "../shared/schema";
|
||||
|
||||
interface ChatMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
@@ -26,7 +26,33 @@ 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).
|
||||
|
||||
### 🛠️ AVAILABLE TOOLS
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
**3. Gamification**
|
||||
- Check achievements/XP with 'getAchievements'.
|
||||
- Check highscores with 'getLeaderboard'.
|
||||
|
||||
### 📅 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").
|
||||
|
||||
IMPORTANT: Do NOT show Task IDs to the user. Reference tasks by Title.
|
||||
CRITICAL: After executing tools, provide a concise summary of your actions.
|
||||
`;
|
||||
|
||||
const fullMessages = [
|
||||
@@ -36,7 +62,7 @@ If needed, suggest they create tasks or manage their schedule (you cannot perfor
|
||||
|
||||
try {
|
||||
if (provider === "openai" || provider === "ollama") {
|
||||
return await this.chatOpenAI(provider, apiKey || "", model, baseUrl, fullMessages);
|
||||
return await this.chatOpenAI(provider, apiKey || "", model, baseUrl, fullMessages, user);
|
||||
} else if (provider === "anthropic") {
|
||||
return await this.chatAnthropic(apiKey || "", model, fullMessages);
|
||||
} else if (provider === "google") {
|
||||
@@ -50,36 +76,496 @@ If needed, suggest they create tasks or manage their schedule (you cannot perfor
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
async generateTitle(messages: ChatMessage[]): 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");
|
||||
|
||||
// Clean URL
|
||||
const cleanUrl = url.replace(/([^:]\/)\/+/g, "$1"); // remove double slashes
|
||||
if (!apiKey && provider !== "ollama") return "New Conversation";
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
const conversationText = messages.slice(0, 3).map(m => `${m.role}: ${m.content}`).join('\n');
|
||||
const prompt = `Based on the following conversation, generate a short, concise title (max 5 words). Return ONLY the title text, no quotes or labels.\n\nConversation:\n${conversationText}`;
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
throw new Error(`OpenAI/Ollama API Error ${response.status}: ${err}`);
|
||||
const contextMessages = [
|
||||
{ role: "user", content: prompt }
|
||||
];
|
||||
|
||||
try {
|
||||
let generatedTitle = "";
|
||||
if (provider === "openai" || provider === "ollama") {
|
||||
generatedTitle = await this.chatOpenAI(provider, apiKey || "", model, baseUrl, contextMessages, undefined, []);
|
||||
} else if (provider === "anthropic") {
|
||||
generatedTitle = await this.chatAnthropic(apiKey || "", model, contextMessages);
|
||||
} else if (provider === "google") {
|
||||
generatedTitle = await this.chatGemini(apiKey || "", model, contextMessages);
|
||||
}
|
||||
|
||||
generatedTitle = generatedTitle.trim();
|
||||
if (generatedTitle && generatedTitle !== "No response generated." && generatedTitle.length < 60) {
|
||||
return generatedTitle.replace(/^["']|["']$/g, '').replace(/[*_#`]/g, '').trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Title generation failed:", error);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0]?.message?.content || "No response generated.";
|
||||
return "New Conversation";
|
||||
}
|
||||
|
||||
// New Public Method for Smart Scheduling
|
||||
async scheduleTask(taskId: string, userId: string, startAfterStr?: string): Promise<{ success: boolean, scheduledDate?: string, message?: string, error?: string }> {
|
||||
const task = await this.storage.getTask(taskId);
|
||||
if (!task) {
|
||||
return { success: false, error: "Task not found." };
|
||||
}
|
||||
|
||||
const startAfter = startAfterStr ? new Date(startAfterStr) : new Date();
|
||||
const durationMins = task.estimatedDuration || 60; // Default to 1h if not set
|
||||
|
||||
const workStartHour = 9;
|
||||
// PRIORITY LOGIC: High priority tasks can be scheduled until 20:00 (8 PM)
|
||||
const workEndHour = task.priority === 'high' ? 20 : 18;
|
||||
|
||||
let scheduledDate: Date | null = null;
|
||||
|
||||
// PLANNED TIME LOGIC: If startDate is set, do not schedule before it.
|
||||
// If starteAfterStr is provided (e.g. "tomorrow"), use the max of both.
|
||||
let effectiveStart = startAfter;
|
||||
if (task.startDate) {
|
||||
const plannedStart = new Date(task.startDate);
|
||||
if (plannedStart > effectiveStart) {
|
||||
effectiveStart = plannedStart;
|
||||
}
|
||||
}
|
||||
|
||||
let currentDay = new Date(effectiveStart);
|
||||
|
||||
// Reset to next slot if passed
|
||||
if (currentDay.getHours() >= workEndHour) {
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
} else if (currentDay.getHours() < workStartHour) {
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
}
|
||||
|
||||
for (let dayOffset = 0; dayOffset < 3; dayOffset++) { // Look ahead 3 days
|
||||
const dayStart = new Date(currentDay);
|
||||
dayStart.setHours(workStartHour, 0, 0, 0);
|
||||
const dayEnd = new Date(currentDay);
|
||||
dayEnd.setHours(workEndHour, 0, 0, 0);
|
||||
|
||||
// Get all tasks for this day that have a due date (and time)
|
||||
const allTasks = await this.storage.searchTasks("", userId);
|
||||
|
||||
// Filter for tasks on this day
|
||||
const dayTasks = allTasks.filter(t => {
|
||||
if (!t.dueDate) return false;
|
||||
const d = new Date(t.dueDate);
|
||||
return d.getDate() === currentDay.getDate() &&
|
||||
d.getMonth() === currentDay.getMonth() &&
|
||||
d.getFullYear() === currentDay.getFullYear();
|
||||
});
|
||||
|
||||
// Find gaps
|
||||
// Sort by time
|
||||
dayTasks.sort((a, b) => (a.dueDate!.getTime() - b.dueDate!.getTime()));
|
||||
|
||||
// Check slots
|
||||
// Start checking from 'currentDay' time (if today) or 9am
|
||||
let attemptTime = new Date(currentDay);
|
||||
if (attemptTime < dayStart) attemptTime = dayStart;
|
||||
|
||||
while (attemptTime.getTime() + (durationMins * 60000) <= dayEnd.getTime()) {
|
||||
const attemptEnd = new Date(attemptTime.getTime() + (durationMins * 60000));
|
||||
|
||||
// Check collision
|
||||
const hasCollision = dayTasks.some(t => {
|
||||
const tStart = new Date(t.dueDate!);
|
||||
const tDuration = t.estimatedDuration || 60;
|
||||
const tEnd = new Date(tStart.getTime() + (tDuration * 60000));
|
||||
|
||||
return (attemptTime < tEnd && attemptEnd > tStart);
|
||||
});
|
||||
|
||||
if (!hasCollision) {
|
||||
scheduledDate = attemptTime;
|
||||
break;
|
||||
}
|
||||
|
||||
// specific increment? 30 mins
|
||||
attemptTime = new Date(attemptTime.getTime() + 30 * 60000);
|
||||
}
|
||||
|
||||
if (scheduledDate) break;
|
||||
|
||||
// Move to next day
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
}
|
||||
|
||||
if (scheduledDate) {
|
||||
await this.storage.updateTask(taskId, { dueDate: scheduledDate });
|
||||
return {
|
||||
success: true,
|
||||
scheduledDate: scheduledDate.toISOString(),
|
||||
message: `Scheduled for ${scheduledDate.toLocaleDateString()} at ${scheduledDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}.`
|
||||
};
|
||||
} else {
|
||||
return { success: false, error: "Could not find a free slot in the next 3 days." };
|
||||
}
|
||||
}
|
||||
|
||||
private getTaskTools() {
|
||||
return [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "createTask",
|
||||
description: "Create a new task for the user.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "The title of the task (required)." },
|
||||
description: { type: "string" },
|
||||
priority: { type: "string", enum: ["low", "medium", "high"] },
|
||||
status: { type: "string", enum: ["todo", "inProgress", "done"] },
|
||||
dueDate: { type: "string", description: "ISO 8601 format (YYYY-MM-DD)." },
|
||||
labelId: { type: "string" },
|
||||
estimatedDuration: { type: "integer", description: "Estimated duration in minutes." },
|
||||
parentTaskId: { type: "string" },
|
||||
startDate: { type: "string" }
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "searchTasks",
|
||||
description: "Search for tasks by title, description, or label.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string" },
|
||||
label: { type: "string" }
|
||||
},
|
||||
required: ["query"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "updateTask",
|
||||
description: "Update an existing task.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
title: { type: "string" },
|
||||
description: { type: "string" },
|
||||
priority: { type: "string", enum: ["low", "medium", "high"] },
|
||||
status: { type: "string", enum: ["todo", "inProgress", "done"] },
|
||||
dueDate: { type: "string" },
|
||||
timeTracked: { type: "number" },
|
||||
labelId: { type: "string" },
|
||||
estimatedDuration: { type: "integer" },
|
||||
startDate: { type: "string" }
|
||||
},
|
||||
required: ["id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "deleteTask",
|
||||
description: "Delete an existing task.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "getLabels",
|
||||
description: "Get all available labels.",
|
||||
parameters: { type: "object", properties: {}, required: [] }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "getAchievements",
|
||||
description: "Get user's XP and rewards.",
|
||||
parameters: { type: "object", properties: {}, required: [] }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "getLeaderboard",
|
||||
description: "Get the highscore leaderboard.",
|
||||
parameters: { type: "object", properties: {}, required: [] }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "scheduleTask",
|
||||
description: "Finds the first available time slot for a task based on its duration and existing schedule.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
taskId: { type: "string", description: "The ID of the task to schedule." },
|
||||
startAfter: { type: "string", description: "ISO 8601 Date to start searching from (default: now)." }
|
||||
},
|
||||
required: ["taskId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private async chatOpenAI(provider: string, apiKey: string, model: string, baseUrl: string | undefined, messages: any[], user?: User, tools: any[] = this.getTaskTools()): Promise<string> {
|
||||
let apiBase = baseUrl;
|
||||
|
||||
if (!apiBase) {
|
||||
if (provider === "ollama") {
|
||||
apiBase = "http://host.docker.internal:11434/v1";
|
||||
} else {
|
||||
apiBase = "https://api.openai.com/v1";
|
||||
}
|
||||
}
|
||||
|
||||
if (apiBase.endsWith('/')) {
|
||||
apiBase = apiBase.slice(0, -1);
|
||||
}
|
||||
|
||||
if (provider === "ollama" && !apiBase.includes("/v1") && !apiBase.includes("/api")) {
|
||||
apiBase += "/v1";
|
||||
}
|
||||
|
||||
const url = `${apiBase}/chat/completions`;
|
||||
console.log(`[AI] Sending Chat Request: provider=${provider} url=${url} model=${model}`);
|
||||
|
||||
const body: any = {
|
||||
model: model,
|
||||
messages: messages,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
if (tools && tools.length > 0) {
|
||||
body.tools = tools;
|
||||
body.tool_choice = "auto";
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
console.error(`[AI] API Error: ${response.status} - ${err}`);
|
||||
throw new Error(`${provider} API Error ${response.status}: ${err}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const choice = data.choices[0];
|
||||
const message = choice.message;
|
||||
|
||||
if (choice.finish_reason === "tool_calls" && message.tool_calls) {
|
||||
console.log(`[AI] Tool Calls detected: ${message.tool_calls.length}`);
|
||||
|
||||
if (!user) throw new Error("Tools invoked but no user context provided.");
|
||||
|
||||
messages.push(message);
|
||||
|
||||
for (const toolCall of message.tool_calls) {
|
||||
const fnName = toolCall.function.name;
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
let result: any = {};
|
||||
|
||||
try {
|
||||
console.log(`[AI] Executing ${fnName}:`, args);
|
||||
|
||||
if (fnName === "createTask") {
|
||||
const taskData = {
|
||||
title: args.title,
|
||||
description: args.description || null,
|
||||
priority: args.priority || "medium",
|
||||
status: args.status || "todo",
|
||||
dueDate: args.dueDate ? new Date(args.dueDate) : null,
|
||||
labelId: args.labelId || null,
|
||||
estimatedDuration: args.estimatedDuration || null,
|
||||
parentTaskId: args.parentTaskId || null,
|
||||
startDate: args.startDate ? new Date(args.startDate) : null,
|
||||
userId: user.id
|
||||
};
|
||||
const createdTask = await this.storage.createTask(taskData);
|
||||
await this.storage.createAuditLog({
|
||||
userId: user.id,
|
||||
action: "CREATE",
|
||||
entityType: "TASK",
|
||||
entityId: createdTask.id,
|
||||
details: { title: createdTask.title },
|
||||
source: "AI"
|
||||
});
|
||||
result = { success: true, taskId: createdTask.id, message: "Task created." };
|
||||
} else if (fnName === "searchTasks") {
|
||||
const tasks = await this.storage.searchTasks(args.query, user.id);
|
||||
const labels = await this.storage.getLabels(user.id);
|
||||
const labelMap = new Map(labels.map(l => [l.id, l.name]));
|
||||
|
||||
let filteredTasks = tasks;
|
||||
if (args.label) {
|
||||
const labelId = labels.find(l => l.name.toLowerCase() === args.label.toLowerCase())?.id;
|
||||
if (labelId) {
|
||||
filteredTasks = tasks.filter(t => t.labelId === labelId);
|
||||
}
|
||||
}
|
||||
|
||||
result = {
|
||||
success: true,
|
||||
tasks: filteredTasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
dueDate: t.dueDate,
|
||||
label: t.labelId ? labelMap.get(t.labelId) || "No Label" : "No Label"
|
||||
}))
|
||||
};
|
||||
} else if (fnName === "updateTask") {
|
||||
const updates: any = {};
|
||||
if (args.title) updates.title = args.title;
|
||||
if (args.description) updates.description = args.description;
|
||||
if (args.priority) updates.priority = args.priority;
|
||||
if (args.status) updates.status = args.status;
|
||||
if (args.dueDate !== undefined) updates.dueDate = args.dueDate ? new Date(args.dueDate) : null;
|
||||
if (args.timeTracked !== undefined) updates.timeTracked = args.timeTracked;
|
||||
if (args.labelId !== undefined) updates.labelId = args.labelId;
|
||||
if (args.estimatedDuration !== undefined) updates.estimatedDuration = args.estimatedDuration;
|
||||
if (args.startDate !== undefined) updates.startDate = args.startDate ? new Date(args.startDate) : null;
|
||||
|
||||
const updatedTask = await this.storage.updateTask(args.id, updates);
|
||||
if (updatedTask) {
|
||||
await this.storage.createAuditLog({
|
||||
userId: user.id,
|
||||
action: "UPDATE",
|
||||
entityType: "TASK",
|
||||
entityId: updatedTask.id,
|
||||
details: updates,
|
||||
source: "AI"
|
||||
});
|
||||
result = { success: true, message: "Task updated successfully." };
|
||||
} else {
|
||||
result = { success: false, error: "Task not found." };
|
||||
}
|
||||
} else if (fnName === "deleteTask") {
|
||||
const deleted = await this.storage.deleteTask(args.id);
|
||||
if (deleted) {
|
||||
await this.storage.createAuditLog({
|
||||
userId: user.id,
|
||||
action: "DELETE",
|
||||
entityType: "TASK",
|
||||
entityId: args.id,
|
||||
details: null,
|
||||
source: "AI"
|
||||
});
|
||||
result = { success: true, message: "Task deleted successfully." };
|
||||
} else {
|
||||
result = { success: false, error: "Task not found or could not be deleted." };
|
||||
}
|
||||
} else if (fnName === "getLabels") {
|
||||
const labels = await this.storage.getLabels(user.id);
|
||||
result = {
|
||||
success: true,
|
||||
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color }))
|
||||
};
|
||||
} else if (fnName === "getAchievements") {
|
||||
const goals = await this.storage.getGoals();
|
||||
const userGoals = goals.filter(g => g.userId === user.id);
|
||||
const rewards = await this.storage.getUserRewards(user.id);
|
||||
const xpEvents = await this.storage.getXpEvents(user.id);
|
||||
|
||||
result = {
|
||||
success: true,
|
||||
xp: user.xp,
|
||||
level: user.level,
|
||||
goals: userGoals.map(g => ({ title: g.title, current: g.current, target: g.target, completed: g.completed })),
|
||||
rewards: rewards.length,
|
||||
recentXp: xpEvents.slice(0, 5).map(e => ({ source: e.source, amount: e.amount }))
|
||||
};
|
||||
} else if (fnName === "getLeaderboard") {
|
||||
const leaderboard = await this.storage.getLeaderboard();
|
||||
result = {
|
||||
success: true,
|
||||
topUsers: leaderboard.slice(0, 10).map((u, index) => ({
|
||||
rank: index + 1,
|
||||
username: u.username,
|
||||
xp: u.xp,
|
||||
level: u.level
|
||||
}))
|
||||
};
|
||||
} else if (fnName === "scheduleTask") {
|
||||
// Call the reusable public method from inside the tool
|
||||
result = await this.scheduleTask(args.taskId, user.id, args.startAfter);
|
||||
} else {
|
||||
result = { success: false, error: "Unknown tool function." };
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: toolCall.id,
|
||||
name: fnName,
|
||||
content: JSON.stringify(result)
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`[AI] Tool Execution Error:`, err);
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: toolCall.id,
|
||||
name: fnName,
|
||||
content: JSON.stringify({ success: false, error: err.message })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[AI] Sending follow-up request with tool results...`);
|
||||
return await this.chatOpenAI(provider, apiKey, model, baseUrl, messages, user, tools);
|
||||
}
|
||||
|
||||
if (!message.content) {
|
||||
console.warn("[AI] Warning: Empty content received from OpenAI.");
|
||||
const hasExecutedTools = messages.some(m => m.role === 'tool');
|
||||
if (hasExecutedTools) {
|
||||
return "I have successfully processed your request and updated your tasks.";
|
||||
}
|
||||
}
|
||||
return message.content || "No response generated.";
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`[AI] Request Failed: ${error.message}`);
|
||||
if (error.code === 'ECONNREFUSED' && url.includes('localhost')) {
|
||||
throw new Error("Connection refused. If running in Docker, try using 'http://host.docker.internal:11434/v1' as Base URL.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
|
||||
@@ -108,14 +594,6 @@ If needed, suggest they create tasks or manage their schedule (you cannot perfor
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user