mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(ai): add AI tools for myday, goals, mood, finance, and times
Expand agent tool coverage from 28 to 47 tools across 16 modules: - myday: get_myday_summary (full daily context in one call) - goals: list_goals, get_goal_progress, create_goal, pause/resume/complete_goal - mood: log_mood, get_mood_today, get_mood_insights (trends + correlations) - finance: extend add_transaction, add get_month_summary + list_transactions - times: extend start/stop_timer, add get_timer_status, get_time_stats, list_projects All tools registered in both AI_TOOL_CATALOG (shared-ai) and webapp init. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
acd7e0d6b0
commit
ed01d24f2d
7 changed files with 1299 additions and 6 deletions
|
|
@ -35,6 +35,9 @@ import { recipesTools } from '$lib/modules/recipes/tools';
|
|||
import { questionsTools } from '$lib/modules/questions/tools';
|
||||
import { meditateTools } from '$lib/modules/meditate/tools';
|
||||
import { sleepTools } from '$lib/modules/sleep/tools';
|
||||
import { mydayTools } from '$lib/modules/myday/tools';
|
||||
import { goalsTools } from '$lib/modules/goals/tools';
|
||||
import { moodTools } from '$lib/modules/mood/tools';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
|
@ -71,5 +74,8 @@ export function initTools(): void {
|
|||
registerTools(questionsTools);
|
||||
registerTools(meditateTools);
|
||||
registerTools(sleepTools);
|
||||
registerTools(mydayTools);
|
||||
registerTools(goalsTools);
|
||||
registerTools(moodTools);
|
||||
initialized = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
/**
|
||||
* Finance Tools — LLM-accessible operations for income/expense tracking.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { financeStore } from './stores/finance.svelte';
|
||||
import { transactionTable, categoryTable } from './collections';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { toTransaction, toCategory, currentMonth, formatCurrency } from './queries';
|
||||
import type { LocalTransaction, LocalFinanceCategory, TransactionType } from './types';
|
||||
|
||||
export const financeTools: ModuleTool[] = [
|
||||
{
|
||||
|
|
@ -16,17 +24,143 @@ export const financeTools: ModuleTool[] = [
|
|||
},
|
||||
{ name: 'amount', type: 'number', description: 'Betrag in Euro', required: true },
|
||||
{ name: 'description', type: 'string', description: 'Beschreibung', required: true },
|
||||
{
|
||||
name: 'date',
|
||||
type: 'string',
|
||||
description: 'Datum (YYYY-MM-DD, Standard: heute)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const tx = await financeStore.addTransaction({
|
||||
type: params.type as 'income' | 'expense',
|
||||
type: params.type as TransactionType,
|
||||
amount: params.amount as number,
|
||||
description: params.description as string,
|
||||
date: params.date as string | undefined,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: tx,
|
||||
message: `${params.type === 'income' ? 'Einnahme' : 'Ausgabe'}: ${params.amount}€ (${params.description})`,
|
||||
message: `${params.type === 'income' ? 'Einnahme' : 'Ausgabe'}: ${formatCurrency(params.amount as number)} (${params.description})`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'get_month_summary',
|
||||
module: 'finance',
|
||||
description:
|
||||
'Gibt die Finanz-Zusammenfassung fuer einen Monat zurueck: Einnahmen, Ausgaben, Bilanz, Ausgaben pro Kategorie.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'month',
|
||||
type: 'string',
|
||||
description: 'Monat im Format YYYY-MM (Standard: aktueller Monat)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const month = (params.month as string) ?? currentMonth();
|
||||
|
||||
const [allTxs, allCats] = await Promise.all([
|
||||
transactionTable.toArray(),
|
||||
categoryTable.toArray(),
|
||||
]);
|
||||
|
||||
const visible = allTxs.filter((t) => !t.deletedAt);
|
||||
const decrypted = await decryptRecords<LocalTransaction>('transactions', visible);
|
||||
const txs = decrypted.map(toTransaction);
|
||||
const cats = allCats.filter((c) => !c.deletedAt).map(toCategory);
|
||||
|
||||
const monthTxs = txs.filter((t) => t.date.startsWith(month));
|
||||
let income = 0;
|
||||
let expenses = 0;
|
||||
const byCategory = new Map<string, number>();
|
||||
|
||||
for (const tx of monthTxs) {
|
||||
if (tx.type === 'income') income += tx.amount;
|
||||
else {
|
||||
expenses += tx.amount;
|
||||
if (tx.categoryId) {
|
||||
byCategory.set(tx.categoryId, (byCategory.get(tx.categoryId) ?? 0) + tx.amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const catBreakdown = [...byCategory.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([catId, amount]) => {
|
||||
const cat = cats.find((c) => c.id === catId);
|
||||
return { category: cat ? `${cat.emoji} ${cat.name}` : 'Sonstiges', amount };
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
month,
|
||||
income,
|
||||
expenses,
|
||||
balance: income - expenses,
|
||||
transactions: monthTxs.length,
|
||||
byCategory: catBreakdown,
|
||||
},
|
||||
message: `${month}: ${formatCurrency(income)} Einnahmen, ${formatCurrency(expenses)} Ausgaben, Bilanz: ${formatCurrency(income - expenses)}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'list_transactions',
|
||||
module: 'finance',
|
||||
description:
|
||||
'Listet die letzten Transaktionen auf. Optional nach Typ (income/expense) und Monat filterbar.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'string',
|
||||
description: 'Nur income oder expense zeigen',
|
||||
required: false,
|
||||
enum: ['income', 'expense'],
|
||||
},
|
||||
{
|
||||
name: 'month',
|
||||
type: 'string',
|
||||
description: 'Monat im Format YYYY-MM',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
description: 'Maximale Anzahl (Standard: 20)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const filterType = params.type as TransactionType | undefined;
|
||||
const month = params.month as string | undefined;
|
||||
const limit = (params.limit as number) ?? 20;
|
||||
|
||||
const all = await transactionTable.toArray();
|
||||
const visible = all.filter((t) => !t.deletedAt);
|
||||
const decrypted = await decryptRecords<LocalTransaction>('transactions', visible);
|
||||
let txs = decrypted.map(toTransaction);
|
||||
|
||||
if (filterType) txs = txs.filter((t) => t.type === filterType);
|
||||
if (month) txs = txs.filter((t) => t.date.startsWith(month));
|
||||
|
||||
txs.sort((a, b) => b.date.localeCompare(a.date) || b.createdAt.localeCompare(a.createdAt));
|
||||
const sliced = txs.slice(0, limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: sliced.map((t) => ({
|
||||
id: t.id,
|
||||
type: t.type,
|
||||
amount: t.amount,
|
||||
description: t.description,
|
||||
date: t.date,
|
||||
})),
|
||||
message: `${sliced.length} Transaktionen${filterType ? ` (${filterType})` : ''}${month ? ` in ${month}` : ''}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
253
apps/mana/apps/web/src/lib/modules/goals/tools.ts
Normal file
253
apps/mana/apps/web/src/lib/modules/goals/tools.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/**
|
||||
* Goals Tools — LLM-accessible operations for the goal system.
|
||||
*
|
||||
* Goals are event-driven: progress auto-increments when matching
|
||||
* domain events fire (e.g. DrinkLogged, TaskCompleted). These tools
|
||||
* let agents read progress, create new goals, and manage status.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { db } from '$lib/data/database';
|
||||
import { goalStore } from '$lib/companion/goals/store';
|
||||
import { GOAL_TEMPLATES, type LocalGoal } from '$lib/companion/goals/types';
|
||||
|
||||
const TABLE = 'companionGoals';
|
||||
|
||||
export const goalsTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'list_goals',
|
||||
module: 'goals',
|
||||
description:
|
||||
'Listet alle Ziele mit aktuellem Fortschritt auf. Zeigt Titel, Fortschritt, Zielwert, Zeitraum und Status.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'filter',
|
||||
type: 'string',
|
||||
description: 'Welche Ziele zeigen',
|
||||
required: false,
|
||||
enum: ['active', 'paused', 'completed', 'all'],
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const filter = (params.filter as string) ?? 'active';
|
||||
const all = await db.table<LocalGoal>(TABLE).toArray();
|
||||
const visible = all.filter((g) => !g.deletedAt);
|
||||
|
||||
const filtered = filter === 'all' ? visible : visible.filter((g) => g.status === filter);
|
||||
|
||||
const items = filtered.map((g) => ({
|
||||
id: g.id,
|
||||
title: g.title,
|
||||
description: g.description,
|
||||
moduleId: g.moduleId,
|
||||
status: g.status,
|
||||
current: g.currentValue,
|
||||
target: g.target.value,
|
||||
period: g.target.period,
|
||||
comparison: g.target.comparison,
|
||||
percent:
|
||||
g.target.comparison === 'gte'
|
||||
? Math.min(Math.round((g.currentValue / g.target.value) * 100), 100)
|
||||
: g.currentValue <= g.target.value
|
||||
? 100
|
||||
: Math.max(
|
||||
0,
|
||||
Math.round((1 - (g.currentValue - g.target.value) / g.target.value) * 100)
|
||||
),
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: items,
|
||||
message: `${items.length} Ziele (${filter}): ${items.map((g) => `${g.title} ${g.current}/${g.target}`).join(', ')}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'get_goal_progress',
|
||||
module: 'goals',
|
||||
description:
|
||||
'Gibt den detaillierten Fortschritt eines einzelnen Ziels zurueck, inklusive Metrik-Details und Periodeninfo.',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
async execute(params) {
|
||||
const goalId = params.goalId as string;
|
||||
const goal = await db.table<LocalGoal>(TABLE).get(goalId);
|
||||
if (!goal || goal.deletedAt) {
|
||||
return { success: false, message: `Ziel ${goalId} nicht gefunden` };
|
||||
}
|
||||
|
||||
const reached =
|
||||
goal.target.comparison === 'gte'
|
||||
? goal.currentValue >= goal.target.value
|
||||
: goal.currentValue <= goal.target.value;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: goal.id,
|
||||
title: goal.title,
|
||||
description: goal.description,
|
||||
moduleId: goal.moduleId,
|
||||
status: goal.status,
|
||||
current: goal.currentValue,
|
||||
target: goal.target.value,
|
||||
period: goal.target.period,
|
||||
comparison: goal.target.comparison,
|
||||
periodStart: goal.currentPeriodStart,
|
||||
reached,
|
||||
metric: goal.metric,
|
||||
},
|
||||
message: `${goal.title}: ${goal.currentValue}/${goal.target.value} (${goal.target.period}) — ${reached ? 'erreicht' : 'offen'}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'create_goal',
|
||||
module: 'goals',
|
||||
description:
|
||||
'Erstellt ein neues Ziel. Kann entweder ein Template verwenden (templateId) oder ein benutzerdefiniertes Ziel erstellen. Verfuegbare Templates: tpl-water-daily, tpl-tasks-daily, tpl-meals-daily, tpl-calories-daily, tpl-places-weekly, tpl-coffee-limit.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'templateId',
|
||||
type: 'string',
|
||||
description:
|
||||
'ID eines Templates (z.B. "tpl-water-daily"). Wenn gesetzt, werden andere Felder ignoriert.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
description: 'Titel des Ziels (nur fuer benutzerdefinierte Ziele)',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
description: 'Beschreibung',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'targetValue',
|
||||
type: 'number',
|
||||
description: 'Zielwert (z.B. 8 fuer "8 Glaeser Wasser")',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'period',
|
||||
type: 'string',
|
||||
description: 'Zeitraum',
|
||||
required: false,
|
||||
enum: ['day', 'week', 'month'],
|
||||
},
|
||||
{
|
||||
name: 'comparison',
|
||||
type: 'string',
|
||||
description: 'Vergleich: gte = mindestens, lte = hoechstens',
|
||||
required: false,
|
||||
enum: ['gte', 'lte'],
|
||||
},
|
||||
{
|
||||
name: 'eventType',
|
||||
type: 'string',
|
||||
description:
|
||||
'Domain-Event zum Zaehlen (z.B. "DrinkLogged", "TaskCompleted", "MealLogged", "WorkoutFinished")',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'moduleId',
|
||||
type: 'string',
|
||||
description: 'Zugehoeriges Modul (z.B. "drink", "todo", "food", "body")',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
// Template-based creation
|
||||
const templateId = params.templateId as string | undefined;
|
||||
if (templateId) {
|
||||
const template = GOAL_TEMPLATES.find((t) => t.id === templateId);
|
||||
if (!template) {
|
||||
return { success: false, message: `Template "${templateId}" nicht gefunden` };
|
||||
}
|
||||
const goal = await goalStore.createFromTemplate(template);
|
||||
return {
|
||||
success: true,
|
||||
data: { id: goal.id, title: goal.title },
|
||||
message: `Ziel "${goal.title}" aus Template erstellt`,
|
||||
};
|
||||
}
|
||||
|
||||
// Custom goal creation
|
||||
const title = params.title as string | undefined;
|
||||
if (!title) {
|
||||
return { success: false, message: 'Entweder templateId oder title ist erforderlich' };
|
||||
}
|
||||
|
||||
const eventType = params.eventType as string | undefined;
|
||||
if (!eventType) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'eventType ist fuer benutzerdefinierte Ziele erforderlich',
|
||||
};
|
||||
}
|
||||
|
||||
const goal = await goalStore.create({
|
||||
title,
|
||||
description: params.description as string | undefined,
|
||||
moduleId: (params.moduleId as string) ?? 'general',
|
||||
metric: {
|
||||
source: 'event_count',
|
||||
eventType,
|
||||
},
|
||||
target: {
|
||||
value: (params.targetValue as number) ?? 1,
|
||||
period: (params.period as 'day' | 'week' | 'month') ?? 'day',
|
||||
comparison: (params.comparison as 'gte' | 'lte') ?? 'gte',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { id: goal.id, title: goal.title },
|
||||
message: `Ziel "${goal.title}" erstellt`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'pause_goal',
|
||||
module: 'goals',
|
||||
description: 'Pausiert ein aktives Ziel. Kann spaeter wieder fortgesetzt werden.',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
async execute(params) {
|
||||
const goalId = params.goalId as string;
|
||||
await goalStore.pause(goalId);
|
||||
return { success: true, message: `Ziel ${goalId} pausiert` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'resume_goal',
|
||||
module: 'goals',
|
||||
description: 'Setzt ein pausiertes Ziel fort.',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
async execute(params) {
|
||||
const goalId = params.goalId as string;
|
||||
await goalStore.resume(goalId);
|
||||
return { success: true, message: `Ziel ${goalId} fortgesetzt` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'complete_goal',
|
||||
module: 'goals',
|
||||
description: 'Markiert ein Ziel als abgeschlossen.',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
async execute(params) {
|
||||
const goalId = params.goalId as string;
|
||||
await goalStore.complete(goalId);
|
||||
return { success: true, message: `Ziel ${goalId} abgeschlossen` };
|
||||
},
|
||||
},
|
||||
];
|
||||
199
apps/mana/apps/web/src/lib/modules/mood/tools.ts
Normal file
199
apps/mana/apps/web/src/lib/modules/mood/tools.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* Mood Tools — LLM-accessible operations for mood tracking.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { moodStore } from './stores/mood.svelte';
|
||||
import { moodEntryTable } from './collections';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import {
|
||||
getAvgLevel,
|
||||
getTopEmotion,
|
||||
getValenceRatio,
|
||||
getActivityInsights,
|
||||
toMoodEntry,
|
||||
} from './queries';
|
||||
import { EMOTION_META, type CoreEmotion, type ActivityContext, type LocalMoodEntry } from './types';
|
||||
|
||||
function todayStr(): string {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
export const moodTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'log_mood',
|
||||
module: 'mood',
|
||||
description:
|
||||
'Erfasst einen Mood-Check-in mit Level (1-10), primaerer Emotion und optionalem Kontext.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'level',
|
||||
type: 'number',
|
||||
description: 'Stimmungs-Level von 1 (schlecht) bis 10 (super)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'emotion',
|
||||
type: 'string',
|
||||
description: 'Primaere Emotion',
|
||||
required: true,
|
||||
enum: [
|
||||
'happy',
|
||||
'calm',
|
||||
'energized',
|
||||
'grateful',
|
||||
'excited',
|
||||
'loved',
|
||||
'hopeful',
|
||||
'neutral',
|
||||
'bored',
|
||||
'tired',
|
||||
'sad',
|
||||
'anxious',
|
||||
'angry',
|
||||
'stressed',
|
||||
'frustrated',
|
||||
'overwhelmed',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'activity',
|
||||
type: 'string',
|
||||
description: 'Was machst du gerade?',
|
||||
required: false,
|
||||
enum: [
|
||||
'work',
|
||||
'exercise',
|
||||
'social',
|
||||
'alone',
|
||||
'commute',
|
||||
'eating',
|
||||
'resting',
|
||||
'creative',
|
||||
'outdoors',
|
||||
'screen',
|
||||
'chores',
|
||||
'other',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
description: 'Optionale Notiz zum Check-in',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const entry = await moodStore.logMood({
|
||||
level: params.level as number,
|
||||
emotion: params.emotion as CoreEmotion,
|
||||
activity: (params.activity as ActivityContext) ?? null,
|
||||
notes: (params.notes as string) ?? '',
|
||||
});
|
||||
const meta = EMOTION_META[params.emotion as CoreEmotion];
|
||||
return {
|
||||
success: true,
|
||||
data: entry,
|
||||
message: `Mood geloggt: ${meta.emoji} ${meta.de} (Level ${params.level}/10)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'get_mood_today',
|
||||
module: 'mood',
|
||||
description: 'Gibt alle heutigen Mood-Eintraege zurueck mit Durchschnitts-Level und Emotionen.',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const today = todayStr();
|
||||
const all = await moodEntryTable.toArray();
|
||||
const todayEntries = all.filter((e) => !e.deletedAt && e.date === today);
|
||||
const decrypted = await decryptRecords<LocalMoodEntry>('moodEntries', todayEntries);
|
||||
const entries = decrypted.map(toMoodEntry);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
data: { entries: [], avgLevel: 0 },
|
||||
message: 'Noch kein Mood-Eintrag heute',
|
||||
};
|
||||
}
|
||||
|
||||
const avgLevel = +(entries.reduce((s, e) => s + e.level, 0) / entries.length).toFixed(1);
|
||||
const emotions = entries.map((e) => {
|
||||
const meta = EMOTION_META[e.emotion];
|
||||
return {
|
||||
emotion: e.emotion,
|
||||
label: meta.de,
|
||||
emoji: meta.emoji,
|
||||
level: e.level,
|
||||
time: e.time,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { entries: emotions, avgLevel, count: entries.length },
|
||||
message: `${entries.length} Check-ins heute, Durchschnitt: ${avgLevel}/10`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'get_mood_insights',
|
||||
module: 'mood',
|
||||
description:
|
||||
'Gibt Mood-Trends und Muster zurueck: Durchschnitt der letzten 7/30 Tage, haeufigste Emotion, Positiv/Negativ-Verhaeltnis, und welche Aktivitaeten mit guter/schlechter Stimmung korrelieren.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'days',
|
||||
type: 'number',
|
||||
description: 'Analyse-Zeitraum in Tagen (Standard: 7)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const days = (params.days as number) ?? 7;
|
||||
const all = await moodEntryTable.toArray();
|
||||
const visible = all.filter((e) => !e.deletedAt);
|
||||
const decrypted = await decryptRecords<LocalMoodEntry>('moodEntries', visible);
|
||||
const entries = decrypted.map(toMoodEntry);
|
||||
|
||||
// Filter to requested time window
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
const cutoffStr = cutoff.toISOString().split('T')[0];
|
||||
const windowEntries = entries.filter((e) => e.date >= cutoffStr);
|
||||
|
||||
if (windowEntries.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
data: null,
|
||||
message: `Keine Mood-Daten in den letzten ${days} Tagen`,
|
||||
};
|
||||
}
|
||||
|
||||
const avgLevel = getAvgLevel(entries, days);
|
||||
const topEmotion = getTopEmotion(windowEntries, windowEntries.length);
|
||||
const valence = getValenceRatio(windowEntries);
|
||||
const activities = getActivityInsights(windowEntries);
|
||||
|
||||
const topEmotionMeta = topEmotion ? EMOTION_META[topEmotion] : null;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
period: `${days} Tage`,
|
||||
totalEntries: windowEntries.length,
|
||||
avgLevel,
|
||||
topEmotion: topEmotion
|
||||
? { emotion: topEmotion, label: topEmotionMeta!.de, emoji: topEmotionMeta!.emoji }
|
||||
: null,
|
||||
valence,
|
||||
activityCorrelations: activities.slice(0, 5),
|
||||
},
|
||||
message: `${days}d: Ø ${avgLevel}/10, ${topEmotionMeta ? `meist ${topEmotionMeta.emoji} ${topEmotionMeta.de}` : '–'}, ${valence.positive}% positiv / ${valence.negative}% negativ`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
198
apps/mana/apps/web/src/lib/modules/myday/tools.ts
Normal file
198
apps/mana/apps/web/src/lib/modules/myday/tools.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* MyDay Tools — LLM-accessible day summary.
|
||||
*
|
||||
* Single read-only tool that aggregates today's snapshot + streaks
|
||||
* into one response. Gives the agent full daily context in one call.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { DEFAULT_DAILY_GOAL_ML } from '$lib/modules/drink/types';
|
||||
import { DEFAULT_DAILY_VALUES } from '$lib/modules/food/constants';
|
||||
import type { LocalTask } from '$lib/modules/todo/types';
|
||||
import type { LocalDrinkEntry } from '$lib/modules/drink/types';
|
||||
import type { LocalMeal, LocalGoal as NutriGoal } from '$lib/modules/food/types';
|
||||
import type { LocalPlace } from '$lib/modules/places/types';
|
||||
import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
|
||||
import type { LocalGoal } from '$lib/companion/goals/types';
|
||||
|
||||
function todayStr(): string {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
export const mydayTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'get_myday_summary',
|
||||
module: 'myday',
|
||||
description:
|
||||
'Gibt eine komplette Tageszusammenfassung zurueck: Tasks, Termine, Trinken, Ernaehrung, Orte, Habits/Streaks und aktive Ziele. Nutze dieses Tool zuerst, um den vollen Tageskontext zu bekommen.',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const today = todayStr();
|
||||
const now = new Date().toISOString();
|
||||
const todayStart = `${today}T00:00:00`;
|
||||
const todayEnd = `${today}T23:59:59`;
|
||||
|
||||
// ── Parallel queries ────────────────────────
|
||||
const [allTasks, blocks, allDrinks, allMeals, foodGoals, allPlaces, streakStates, goals] =
|
||||
await Promise.all([
|
||||
db.table<LocalTask>('tasks').toArray(),
|
||||
db
|
||||
.table<LocalTimeBlock>('timeBlocks')
|
||||
.where('startDate')
|
||||
.between(todayStart, todayEnd + '\uffff')
|
||||
.toArray(),
|
||||
db.table<LocalDrinkEntry>('drinkEntries').toArray(),
|
||||
db.table<LocalMeal>('meals').toArray(),
|
||||
db.table<NutriGoal>('goals').toArray(),
|
||||
db.table<LocalPlace>('places').toArray(),
|
||||
db.table('_streakState').toArray(),
|
||||
db.table<LocalGoal>('companionGoals').toArray(),
|
||||
]);
|
||||
|
||||
// ── Filter + decrypt ────────────────────────
|
||||
const activeTasks = allTasks.filter((t) => !t.deletedAt);
|
||||
const eventBlocks = blocks.filter(
|
||||
(b) => !b.deletedAt && b.type === 'event' && b.sourceModule === 'calendar'
|
||||
);
|
||||
const todayDrinks = allDrinks.filter((d) => !d.deletedAt && d.date === today);
|
||||
const todayMeals = allMeals.filter((m) => !m.deletedAt && m.date === today);
|
||||
|
||||
const [decTasks, decBlocks, decDrinks, decMeals] = await Promise.all([
|
||||
decryptRecords<LocalTask>('tasks', activeTasks),
|
||||
decryptRecords<LocalTimeBlock>('timeBlocks', eventBlocks),
|
||||
decryptRecords<LocalDrinkEntry>('drinkEntries', todayDrinks),
|
||||
decryptRecords<LocalMeal>('meals', todayMeals),
|
||||
]);
|
||||
|
||||
// ── Tasks ───────────────────────────────────
|
||||
const openTasks = decTasks.filter((t) => !t.isCompleted);
|
||||
const completedCount = decTasks.filter((t) => t.isCompleted).length;
|
||||
const overdue = openTasks.filter((t) => t.dueDate != null && (t.dueDate as string) < today);
|
||||
const dueToday = openTasks.filter((t) => (t.dueDate as string) === today);
|
||||
|
||||
// ── Events ──────────────────────────────────
|
||||
const events = decBlocks
|
||||
.sort((a, b) => (a.startDate as string).localeCompare(b.startDate as string))
|
||||
.map((b) => ({
|
||||
title: (b.title as string) ?? '',
|
||||
startTime: b.startDate,
|
||||
endTime: b.endDate ?? b.startDate,
|
||||
isAllDay: b.allDay ?? false,
|
||||
}));
|
||||
const upcoming = events.filter((e) => e.startTime >= now);
|
||||
|
||||
// ── Drinks ──────────────────────────────────
|
||||
let waterMl = 0;
|
||||
let coffeeMl = 0;
|
||||
let coffeeCount = 0;
|
||||
let totalMl = 0;
|
||||
for (const d of decDrinks) {
|
||||
const ml = d.quantityMl ?? 0;
|
||||
totalMl += ml;
|
||||
if (d.drinkType === 'water') waterMl += ml;
|
||||
if (d.drinkType === 'coffee') {
|
||||
coffeeMl += ml;
|
||||
coffeeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Nutrition ───────────────────────────────
|
||||
let totalCalories = 0;
|
||||
let totalProtein = 0;
|
||||
for (const m of decMeals) {
|
||||
const n = m.nutrition as { calories?: number; protein?: number } | null;
|
||||
if (n) {
|
||||
totalCalories += n.calories ?? 0;
|
||||
totalProtein += n.protein ?? 0;
|
||||
}
|
||||
}
|
||||
const activeGoal = foodGoals.find((g) => !g.deletedAt);
|
||||
const calorieGoal = activeGoal?.dailyCalories ?? DEFAULT_DAILY_VALUES.calories;
|
||||
|
||||
// ── Places ──────────────────────────────────
|
||||
const visitedToday = allPlaces.filter(
|
||||
(p) => !p.deletedAt && p.lastVisitedAt && (p.lastVisitedAt as string).startsWith(today)
|
||||
).length;
|
||||
|
||||
// ── Streaks ─────────────────────────────────
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayS = yesterday.toISOString().split('T')[0];
|
||||
|
||||
const streaks = (streakStates as Array<Record<string, unknown>>)
|
||||
.filter((s) => s.lastActiveDate === today || s.lastActiveDate === yesterdayS)
|
||||
.map((s) => ({
|
||||
label: s.label as string,
|
||||
streak: s.currentStreak as number,
|
||||
status: s.lastActiveDate === today ? 'active' : 'at_risk',
|
||||
}));
|
||||
|
||||
// ── Active Goals ────────────────────────────
|
||||
const activeGoals = goals
|
||||
.filter((g) => g.status === 'active' && !g.deletedAt)
|
||||
.map((g) => ({
|
||||
title: g.title,
|
||||
current: g.currentValue,
|
||||
target: g.target.value,
|
||||
period: g.target.period,
|
||||
percent: Math.round((g.currentValue / g.target.value) * 100),
|
||||
}));
|
||||
|
||||
const summary = {
|
||||
date: today,
|
||||
tasks: {
|
||||
open: openTasks.length,
|
||||
completed: completedCount,
|
||||
overdue: overdue.length,
|
||||
dueToday: dueToday.slice(0, 10).map((t) => ({
|
||||
title: (t.title as string) ?? '',
|
||||
priority: t.priority as string | undefined,
|
||||
})),
|
||||
},
|
||||
events: {
|
||||
total: events.length,
|
||||
upcoming: upcoming.slice(0, 5).map((e) => ({
|
||||
title: e.title,
|
||||
startTime: e.startTime,
|
||||
isAllDay: e.isAllDay,
|
||||
})),
|
||||
},
|
||||
drinks: {
|
||||
water: {
|
||||
ml: waterMl,
|
||||
goal: DEFAULT_DAILY_GOAL_ML,
|
||||
percent: Math.round((waterMl / DEFAULT_DAILY_GOAL_ML) * 100),
|
||||
},
|
||||
coffee: { count: coffeeCount },
|
||||
total: { ml: totalMl, count: decDrinks.length },
|
||||
},
|
||||
nutrition: {
|
||||
meals: decMeals.length,
|
||||
calories: { actual: Math.round(totalCalories), goal: calorieGoal },
|
||||
},
|
||||
places: { visitedToday },
|
||||
streaks,
|
||||
goals: activeGoals,
|
||||
};
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push(
|
||||
`${today}: ${openTasks.length} offene Tasks (${completedCount} erledigt, ${overdue.length} ueberfaellig)`
|
||||
);
|
||||
if (upcoming.length > 0)
|
||||
parts.push(`${events.length} Termine (naechster: ${upcoming[0].title})`);
|
||||
parts.push(`Wasser: ${waterMl}/${DEFAULT_DAILY_GOAL_ML}ml, ${coffeeCount} Kaffee`);
|
||||
if (decMeals.length > 0)
|
||||
parts.push(`${decMeals.length} Mahlzeiten, ${Math.round(totalCalories)} kcal`);
|
||||
if (activeGoals.length > 0) parts.push(`${activeGoals.length} aktive Ziele`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: summary,
|
||||
message: parts.join(' · '),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -1,10 +1,18 @@
|
|||
/**
|
||||
* Times Tools — LLM-accessible operations for time tracking.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { db } from '$lib/data/database';
|
||||
import { formatDurationCompact, toTimeEntry, toProject, toClient } from './queries';
|
||||
import type { LocalTimeEntry, LocalProject, LocalClient } from './types';
|
||||
import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
|
||||
|
||||
export const timesTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'start_timer',
|
||||
module: 'times',
|
||||
description: 'Startet einen Zeitmess-Timer',
|
||||
description: 'Startet einen Zeitmess-Timer mit optionaler Beschreibung und Projekt.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'description',
|
||||
|
|
@ -12,20 +20,30 @@ export const timesTools: ModuleTool[] = [
|
|||
description: 'Beschreibung der Taetigkeit',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'projectId',
|
||||
type: 'string',
|
||||
description: 'ID eines Projekts (aus list_projects)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const { timerStore } = await import('./stores/timer.svelte');
|
||||
await timerStore.start({ description: params.description as string | undefined });
|
||||
await timerStore.start({
|
||||
description: params.description as string | undefined,
|
||||
projectId: params.projectId as string | undefined,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: `Timer gestartet${params.description ? `: "${params.description}"` : ''}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'stop_timer',
|
||||
module: 'times',
|
||||
description: 'Stoppt den laufenden Timer',
|
||||
description: 'Stoppt den laufenden Timer und speichert den Zeiteintrag.',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const { timerStore } = await import('./stores/timer.svelte');
|
||||
|
|
@ -34,7 +52,158 @@ export const timesTools: ModuleTool[] = [
|
|||
return {
|
||||
success: true,
|
||||
data: entry,
|
||||
message: `Timer gestoppt (${Math.round(entry.duration / 60)} min)`,
|
||||
message: `Timer gestoppt (${formatDurationCompact(entry.duration)})`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'get_timer_status',
|
||||
module: 'times',
|
||||
description: 'Gibt den Status des laufenden Timers zurueck (ob aktiv, Dauer, Beschreibung).',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const { timerStore } = await import('./stores/timer.svelte');
|
||||
if (!timerStore.isRunning) {
|
||||
return { success: true, data: { running: false }, message: 'Kein Timer aktiv' };
|
||||
}
|
||||
const entry = timerStore.runningEntry;
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
running: true,
|
||||
elapsed: timerStore.elapsedSeconds,
|
||||
elapsedFormatted: formatDurationCompact(timerStore.elapsedSeconds),
|
||||
description: entry?.description ?? '',
|
||||
projectId: entry?.projectId ?? null,
|
||||
},
|
||||
message: `Timer laeuft: ${formatDurationCompact(timerStore.elapsedSeconds)}${entry?.description ? ` — "${entry.description}"` : ''}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'get_time_stats',
|
||||
module: 'times',
|
||||
description:
|
||||
'Gibt Zeiterfassungs-Statistiken zurueck: Stunden heute, diese Woche, und Aufschluesselung nach Projekt.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'period',
|
||||
type: 'string',
|
||||
description: 'Zeitraum (Standard: week)',
|
||||
required: false,
|
||||
enum: ['today', 'week', 'month'],
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const period = (params.period as string) ?? 'week';
|
||||
const now = new Date();
|
||||
const todayStr = now.toISOString().split('T')[0];
|
||||
|
||||
// Determine date range
|
||||
let fromDate: string;
|
||||
if (period === 'today') {
|
||||
fromDate = todayStr;
|
||||
} else if (period === 'week') {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - d.getDay() + (d.getDay() === 0 ? -6 : 1)); // Monday
|
||||
fromDate = d.toISOString().split('T')[0];
|
||||
} else {
|
||||
fromDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
|
||||
}
|
||||
|
||||
// Fetch entries + blocks + projects
|
||||
const [allEntries, allBlocks, allProjects, allClients] = await Promise.all([
|
||||
db.table<LocalTimeEntry>('timeEntries').toArray(),
|
||||
db.table<LocalTimeBlock>('timeBlocks').toArray(),
|
||||
db.table<LocalProject>('timeProjects').toArray(),
|
||||
db.table<LocalClient>('timeClients').toArray(),
|
||||
]);
|
||||
|
||||
const blocksById = new Map(allBlocks.map((b) => [b.id, b]));
|
||||
const entries = allEntries
|
||||
.filter((e) => !e.deletedAt)
|
||||
.map((e) => toTimeEntry(e, blocksById.get(e.timeBlockId)))
|
||||
.filter((e) => e.date >= fromDate && e.date <= todayStr);
|
||||
|
||||
const projects = allProjects.filter((p) => !p.deletedAt).map(toProject);
|
||||
const clients = allClients.filter((c) => !c.deletedAt).map(toClient);
|
||||
|
||||
// Aggregate
|
||||
const totalSeconds = entries.reduce((s, e) => s + e.duration, 0);
|
||||
const billableSeconds = entries
|
||||
.filter((e) => e.isBillable)
|
||||
.reduce((s, e) => s + e.duration, 0);
|
||||
|
||||
// By project
|
||||
const byProject = new Map<string, number>();
|
||||
for (const e of entries) {
|
||||
const key = e.projectId ?? '_none';
|
||||
byProject.set(key, (byProject.get(key) ?? 0) + e.duration);
|
||||
}
|
||||
|
||||
const projectBreakdown = [...byProject.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([projId, secs]) => {
|
||||
const proj = projects.find((p) => p.id === projId);
|
||||
const client = proj?.clientId ? clients.find((c) => c.id === proj.clientId) : null;
|
||||
return {
|
||||
project: proj?.name ?? 'Ohne Projekt',
|
||||
client: client?.name ?? null,
|
||||
duration: formatDurationCompact(secs),
|
||||
seconds: secs,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
period,
|
||||
from: fromDate,
|
||||
to: todayStr,
|
||||
entries: entries.length,
|
||||
total: formatDurationCompact(totalSeconds),
|
||||
totalSeconds,
|
||||
billable: formatDurationCompact(billableSeconds),
|
||||
billableSeconds,
|
||||
byProject: projectBreakdown,
|
||||
},
|
||||
message: `${period}: ${formatDurationCompact(totalSeconds)} gesamt (${formatDurationCompact(billableSeconds)} abrechenbar), ${entries.length} Eintraege`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'list_projects',
|
||||
module: 'times',
|
||||
description: 'Listet alle aktiven Zeiterfassungs-Projekte mit Kunden-Info auf.',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const [allProjects, allClients] = await Promise.all([
|
||||
db.table<LocalProject>('timeProjects').toArray(),
|
||||
db.table<LocalClient>('timeClients').toArray(),
|
||||
]);
|
||||
|
||||
const clients = allClients.filter((c) => !c.deletedAt).map(toClient);
|
||||
const projects = allProjects
|
||||
.filter((p) => !p.deletedAt && !p.isArchived)
|
||||
.map(toProject)
|
||||
.map((p) => {
|
||||
const client = p.clientId ? clients.find((c) => c.id === p.clientId) : null;
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
client: client?.name ?? null,
|
||||
isBillable: p.isBillable,
|
||||
color: p.color,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: projects,
|
||||
message: `${projects.length} aktive Projekte`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -488,6 +488,124 @@ export const AI_TOOL_CATALOG: readonly ToolSchema[] = [
|
|||
parameters: [],
|
||||
},
|
||||
|
||||
// ── MyDay ─────────────────────────────────────────────────
|
||||
{
|
||||
name: 'get_myday_summary',
|
||||
module: 'myday',
|
||||
description:
|
||||
'Gibt eine komplette Tageszusammenfassung zurueck: Tasks, Termine, Trinken, Ernaehrung, Orte, Streaks und aktive Ziele. Nutze dieses Tool zuerst, um den vollen Tageskontext zu bekommen.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [],
|
||||
},
|
||||
|
||||
// ── Goals ─────────────────────────────────────────────────
|
||||
{
|
||||
name: 'list_goals',
|
||||
module: 'goals',
|
||||
description:
|
||||
'Listet alle Ziele mit aktuellem Fortschritt auf. Zeigt Titel, Fortschritt, Zielwert, Zeitraum und Status.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [
|
||||
{
|
||||
name: 'filter',
|
||||
type: 'string',
|
||||
description: 'Welche Ziele zeigen',
|
||||
required: false,
|
||||
enum: ['active', 'paused', 'completed', 'all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'get_goal_progress',
|
||||
module: 'goals',
|
||||
description:
|
||||
'Gibt den detaillierten Fortschritt eines einzelnen Ziels zurueck, inklusive Metrik-Details und Periodeninfo.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'create_goal',
|
||||
module: 'goals',
|
||||
description:
|
||||
'Erstellt ein neues Ziel. Kann entweder ein Template verwenden (templateId) oder ein benutzerdefiniertes Ziel erstellen. Verfuegbare Templates: tpl-water-daily, tpl-tasks-daily, tpl-meals-daily, tpl-calories-daily, tpl-places-weekly, tpl-coffee-limit.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{
|
||||
name: 'templateId',
|
||||
type: 'string',
|
||||
description:
|
||||
'ID eines Templates (z.B. "tpl-water-daily"). Wenn gesetzt, werden andere Felder ignoriert.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
description: 'Titel des Ziels (nur fuer benutzerdefinierte Ziele)',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
description: 'Beschreibung',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'targetValue',
|
||||
type: 'number',
|
||||
description: 'Zielwert (z.B. 8 fuer "8 Glaeser Wasser")',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'period',
|
||||
type: 'string',
|
||||
description: 'Zeitraum',
|
||||
required: false,
|
||||
enum: ['day', 'week', 'month'],
|
||||
},
|
||||
{
|
||||
name: 'comparison',
|
||||
type: 'string',
|
||||
description: 'Vergleich: gte = mindestens, lte = hoechstens',
|
||||
required: false,
|
||||
enum: ['gte', 'lte'],
|
||||
},
|
||||
{
|
||||
name: 'eventType',
|
||||
type: 'string',
|
||||
description:
|
||||
'Domain-Event zum Zaehlen (z.B. "DrinkLogged", "TaskCompleted", "MealLogged", "WorkoutFinished")',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'moduleId',
|
||||
type: 'string',
|
||||
description: 'Zugehoeriges Modul (z.B. "drink", "todo", "food", "body")',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'pause_goal',
|
||||
module: 'goals',
|
||||
description: 'Pausiert ein aktives Ziel. Kann spaeter wieder fortgesetzt werden.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'resume_goal',
|
||||
module: 'goals',
|
||||
description: 'Setzt ein pausiertes Ziel fort.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'complete_goal',
|
||||
module: 'goals',
|
||||
description: 'Markiert ein Ziel als abgeschlossen.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
|
||||
},
|
||||
|
||||
// ── Contacts ──────────────────────────────────────────────
|
||||
{
|
||||
name: 'create_contact',
|
||||
|
|
@ -520,6 +638,222 @@ export const AI_TOOL_CATALOG: readonly ToolSchema[] = [
|
|||
defaultPolicy: 'auto',
|
||||
parameters: [],
|
||||
},
|
||||
|
||||
// ── Mood ──────────────────────────────────────────────────
|
||||
{
|
||||
name: 'log_mood',
|
||||
module: 'mood',
|
||||
description:
|
||||
'Erfasst einen Mood-Check-in mit Level (1-10), primaerer Emotion und optionalem Kontext.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{
|
||||
name: 'level',
|
||||
type: 'number',
|
||||
description: 'Stimmungs-Level von 1 (schlecht) bis 10 (super)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'emotion',
|
||||
type: 'string',
|
||||
description: 'Primaere Emotion',
|
||||
required: true,
|
||||
enum: [
|
||||
'happy',
|
||||
'calm',
|
||||
'energized',
|
||||
'grateful',
|
||||
'excited',
|
||||
'loved',
|
||||
'hopeful',
|
||||
'neutral',
|
||||
'bored',
|
||||
'tired',
|
||||
'sad',
|
||||
'anxious',
|
||||
'angry',
|
||||
'stressed',
|
||||
'frustrated',
|
||||
'overwhelmed',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'activity',
|
||||
type: 'string',
|
||||
description: 'Was machst du gerade?',
|
||||
required: false,
|
||||
enum: [
|
||||
'work',
|
||||
'exercise',
|
||||
'social',
|
||||
'alone',
|
||||
'commute',
|
||||
'eating',
|
||||
'resting',
|
||||
'creative',
|
||||
'outdoors',
|
||||
'screen',
|
||||
'chores',
|
||||
'other',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
description: 'Optionale Notiz zum Check-in',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'get_mood_today',
|
||||
module: 'mood',
|
||||
description: 'Gibt alle heutigen Mood-Eintraege zurueck mit Durchschnitts-Level und Emotionen.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
name: 'get_mood_insights',
|
||||
module: 'mood',
|
||||
description:
|
||||
'Gibt Mood-Trends und Muster zurueck: Durchschnitt der letzten 7/30 Tage, haeufigste Emotion, Positiv/Negativ-Verhaeltnis, und welche Aktivitaeten mit guter/schlechter Stimmung korrelieren.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [
|
||||
{
|
||||
name: 'days',
|
||||
type: 'number',
|
||||
description: 'Analyse-Zeitraum in Tagen (Standard: 7)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Finance ───────────────────────────────────────────────
|
||||
{
|
||||
name: 'add_transaction',
|
||||
module: 'finance',
|
||||
description: 'Erfasst eine Einnahme oder Ausgabe',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'string',
|
||||
description: 'Art',
|
||||
required: true,
|
||||
enum: ['income', 'expense'],
|
||||
},
|
||||
{ name: 'amount', type: 'number', description: 'Betrag in Euro', required: true },
|
||||
{ name: 'description', type: 'string', description: 'Beschreibung', required: true },
|
||||
{
|
||||
name: 'date',
|
||||
type: 'string',
|
||||
description: 'Datum (YYYY-MM-DD, Standard: heute)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'get_month_summary',
|
||||
module: 'finance',
|
||||
description:
|
||||
'Gibt die Finanz-Zusammenfassung fuer einen Monat zurueck: Einnahmen, Ausgaben, Bilanz, Ausgaben pro Kategorie.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [
|
||||
{
|
||||
name: 'month',
|
||||
type: 'string',
|
||||
description: 'Monat im Format YYYY-MM (Standard: aktueller Monat)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list_transactions',
|
||||
module: 'finance',
|
||||
description:
|
||||
'Listet die letzten Transaktionen auf. Optional nach Typ (income/expense) und Monat filterbar.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'string',
|
||||
description: 'Nur income oder expense zeigen',
|
||||
required: false,
|
||||
enum: ['income', 'expense'],
|
||||
},
|
||||
{
|
||||
name: 'month',
|
||||
type: 'string',
|
||||
description: 'Monat im Format YYYY-MM',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
description: 'Maximale Anzahl (Standard: 20)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Times ─────────────────────────────────────────────────
|
||||
{
|
||||
name: 'start_timer',
|
||||
module: 'times',
|
||||
description: 'Startet einen Zeitmess-Timer mit optionaler Beschreibung und Projekt.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
description: 'Beschreibung der Taetigkeit',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'projectId',
|
||||
type: 'string',
|
||||
description: 'ID eines Projekts (aus list_projects)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'stop_timer',
|
||||
module: 'times',
|
||||
description: 'Stoppt den laufenden Timer und speichert den Zeiteintrag.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
name: 'get_timer_status',
|
||||
module: 'times',
|
||||
description: 'Gibt den Status des laufenden Timers zurueck (ob aktiv, Dauer, Beschreibung).',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
name: 'get_time_stats',
|
||||
module: 'times',
|
||||
description:
|
||||
'Gibt Zeiterfassungs-Statistiken zurueck: Stunden heute, diese Woche, und Aufschluesselung nach Projekt.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [
|
||||
{
|
||||
name: 'period',
|
||||
type: 'string',
|
||||
description: 'Zeitraum (Standard: week)',
|
||||
required: false,
|
||||
enum: ['today', 'week', 'month'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list_projects',
|
||||
module: 'times',
|
||||
description: 'Listet alle aktiven Zeiterfassungs-Projekte mit Kunden-Info auf.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [],
|
||||
},
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue