mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 23:19:40 +02:00
feat(brain): add Tool Layer with LLM-accessible module operations
Phase 4 of the Companion Brain. Introduces a standardized tool interface that gives LLMs read/write access to module operations via function calling. Core (data/tools/): - ModuleTool interface with typed parameters and execute function - Registry with dynamic registration and LLM schema generator - Executor with parameter validation and error handling - Init function wired into app layout startup Module tools (13 tools across 5 modules): - Todo: create_task, complete_task, get_task_stats - Calendar: create_event, get_todays_events - Drink: log_drink, get_drink_progress, undo_last_drink - Nutriphi: log_meal, get_nutrition_summary - Places: create_place, record_visit, get_places, get_current_location Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
777810d0d2
commit
66dd684bba
11 changed files with 626 additions and 0 deletions
53
apps/mana/apps/web/src/lib/data/tools/executor.ts
Normal file
53
apps/mana/apps/web/src/lib/data/tools/executor.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Tool Executor — Validates parameters and runs a tool by name.
|
||||
*/
|
||||
|
||||
import { getTool } from './registry';
|
||||
import type { ToolResult } from './types';
|
||||
|
||||
export async function executeTool(
|
||||
name: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<ToolResult> {
|
||||
const tool = getTool(name);
|
||||
if (!tool) {
|
||||
return { success: false, message: `Unknown tool: ${name}` };
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
for (const p of tool.parameters) {
|
||||
if (p.required && (params[p.name] === undefined || params[p.name] === null)) {
|
||||
return { success: false, message: `Missing required parameter: ${p.name}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate types
|
||||
for (const p of tool.parameters) {
|
||||
const val = params[p.name];
|
||||
if (val === undefined || val === null) continue;
|
||||
|
||||
if (p.type === 'number' && typeof val !== 'number') {
|
||||
const num = Number(val);
|
||||
if (isNaN(num)) {
|
||||
return { success: false, message: `Parameter ${p.name} must be a number` };
|
||||
}
|
||||
params[p.name] = num;
|
||||
}
|
||||
if (p.type === 'boolean' && typeof val !== 'boolean') {
|
||||
params[p.name] = val === 'true' || val === true;
|
||||
}
|
||||
if (p.enum && !p.enum.includes(String(val))) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Parameter ${p.name} must be one of: ${p.enum.join(', ')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.execute(params);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { success: false, message: `Tool execution failed: ${msg}` };
|
||||
}
|
||||
}
|
||||
3
apps/mana/apps/web/src/lib/data/tools/index.ts
Normal file
3
apps/mana/apps/web/src/lib/data/tools/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { registerTools, getTools, getTool, getToolsForModule, getToolsForLlm } from './registry';
|
||||
export { executeTool } from './executor';
|
||||
export type { ModuleTool, ToolParameter, ToolResult, LlmFunctionSchema } from './types';
|
||||
23
apps/mana/apps/web/src/lib/data/tools/init.ts
Normal file
23
apps/mana/apps/web/src/lib/data/tools/init.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Tool initialization — Registers all module tools.
|
||||
* Call once at app startup.
|
||||
*/
|
||||
|
||||
import { registerTools } from './registry';
|
||||
import { todoTools } from '$lib/modules/todo/tools';
|
||||
import { calendarTools } from '$lib/modules/calendar/tools';
|
||||
import { drinkTools } from '$lib/modules/drink/tools';
|
||||
import { nutriphiTools } from '$lib/modules/nutriphi/tools';
|
||||
import { placesTools } from '$lib/modules/places/tools';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function initTools(): void {
|
||||
if (initialized) return;
|
||||
registerTools(todoTools);
|
||||
registerTools(calendarTools);
|
||||
registerTools(drinkTools);
|
||||
registerTools(nutriphiTools);
|
||||
registerTools(placesTools);
|
||||
initialized = true;
|
||||
}
|
||||
53
apps/mana/apps/web/src/lib/data/tools/registry.ts
Normal file
53
apps/mana/apps/web/src/lib/data/tools/registry.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Tool Registry — Collects ModuleTools and generates LLM schemas.
|
||||
*/
|
||||
|
||||
import type { ModuleTool, LlmFunctionSchema } from './types';
|
||||
|
||||
const tools: ModuleTool[] = [];
|
||||
|
||||
/** Register tools from a module. Call once per module at init. */
|
||||
export function registerTools(moduleTools: ModuleTool[]): void {
|
||||
for (const tool of moduleTools) {
|
||||
if (!tools.some((t) => t.name === tool.name)) {
|
||||
tools.push(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get all registered tools. */
|
||||
export function getTools(): readonly ModuleTool[] {
|
||||
return tools;
|
||||
}
|
||||
|
||||
/** Get a tool by name. */
|
||||
export function getTool(name: string): ModuleTool | undefined {
|
||||
return tools.find((t) => t.name === name);
|
||||
}
|
||||
|
||||
/** Get tools for a specific module. */
|
||||
export function getToolsForModule(module: string): ModuleTool[] {
|
||||
return tools.filter((t) => t.module === module);
|
||||
}
|
||||
|
||||
/** Generate LLM function-calling schemas for all registered tools. */
|
||||
export function getToolsForLlm(): LlmFunctionSchema[] {
|
||||
return tools.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: Object.fromEntries(
|
||||
t.parameters.map((p) => [
|
||||
p.name,
|
||||
{
|
||||
type: p.type,
|
||||
description: p.description,
|
||||
...(p.enum ? { enum: p.enum } : {}),
|
||||
},
|
||||
])
|
||||
),
|
||||
required: t.parameters.filter((p) => p.required).map((p) => p.name),
|
||||
},
|
||||
}));
|
||||
}
|
||||
52
apps/mana/apps/web/src/lib/data/tools/types.ts
Normal file
52
apps/mana/apps/web/src/lib/data/tools/types.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Tool Layer types — Standardized LLM access to module operations.
|
||||
*
|
||||
* Each module exports a ModuleTool[] array. The registry collects them
|
||||
* and generates LLM function-calling schemas. The executor validates
|
||||
* parameters and runs the tool.
|
||||
*/
|
||||
|
||||
export interface ModuleTool {
|
||||
/** Unique tool name, e.g. 'create_task', 'log_drink' */
|
||||
name: string;
|
||||
/** Source module, e.g. 'todo', 'drink' */
|
||||
module: string;
|
||||
/** Human-readable description for the LLM function schema */
|
||||
description: string;
|
||||
/** Parameter definitions */
|
||||
parameters: ToolParameter[];
|
||||
/** Execute the tool. Params are pre-validated by the executor. */
|
||||
execute: (params: Record<string, unknown>) => Promise<ToolResult>;
|
||||
}
|
||||
|
||||
export interface ToolParameter {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'boolean';
|
||||
description: string;
|
||||
required: boolean;
|
||||
enum?: string[];
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
/** Human-readable confirmation message */
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** JSON Schema for LLM function calling */
|
||||
export interface LlmFunctionSchema {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: 'object';
|
||||
properties: Record<string, LlmPropertySchema>;
|
||||
required: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface LlmPropertySchema {
|
||||
type: string;
|
||||
description: string;
|
||||
enum?: string[];
|
||||
}
|
||||
84
apps/mana/apps/web/src/lib/modules/calendar/tools.ts
Normal file
84
apps/mana/apps/web/src/lib/modules/calendar/tools.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Calendar Tools — LLM-accessible operations for calendar events.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { eventsStore } from './stores/events.svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
|
||||
|
||||
export const calendarTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'create_event',
|
||||
module: 'calendar',
|
||||
description: 'Erstellt einen neuen Kalender-Termin',
|
||||
parameters: [
|
||||
{ name: 'title', type: 'string', description: 'Titel des Termins', required: true },
|
||||
{ name: 'startTime', type: 'string', description: 'Startzeit (ISO 8601)', required: true },
|
||||
{ name: 'endTime', type: 'string', description: 'Endzeit (ISO 8601)', required: true },
|
||||
{ name: 'isAllDay', type: 'boolean', description: 'Ganztaegig', required: false },
|
||||
{ name: 'location', type: 'string', description: 'Ort', required: false },
|
||||
{ name: 'description', type: 'string', description: 'Beschreibung', required: false },
|
||||
],
|
||||
async execute(params) {
|
||||
// Find default calendar
|
||||
const calendars = await db.table('calendars').toArray();
|
||||
const defaultCal =
|
||||
calendars.find((c: Record<string, unknown>) => !c.deletedAt && c.isDefault) ??
|
||||
calendars.find((c: Record<string, unknown>) => !c.deletedAt);
|
||||
if (!defaultCal) {
|
||||
return { success: false, message: 'Kein Kalender vorhanden' };
|
||||
}
|
||||
|
||||
const result = await eventsStore.createEvent({
|
||||
calendarId: (defaultCal as Record<string, unknown>).id as string,
|
||||
title: params.title as string,
|
||||
startTime: params.startTime as string,
|
||||
endTime: params.endTime as string,
|
||||
isAllDay: (params.isAllDay as boolean) ?? false,
|
||||
location: params.location as string | undefined,
|
||||
description: params.description as string | undefined,
|
||||
});
|
||||
return {
|
||||
success: result.success,
|
||||
data: result.data,
|
||||
message: result.success
|
||||
? `Termin "${params.title}" erstellt`
|
||||
: (result.error ?? 'Fehler beim Erstellen'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_todays_events',
|
||||
module: 'calendar',
|
||||
description: 'Gibt alle Termine fuer heute zurueck',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const blocks = await db
|
||||
.table<LocalTimeBlock>('timeBlocks')
|
||||
.where('startDate')
|
||||
.between(`${today}T00:00:00`, `${today}T23:59:59\uffff`)
|
||||
.toArray();
|
||||
const eventBlocks = blocks.filter(
|
||||
(b) => !b.deletedAt && b.type === 'event' && b.sourceModule === 'calendar'
|
||||
);
|
||||
const decrypted = await decryptRecords<LocalTimeBlock>('timeBlocks', eventBlocks);
|
||||
const events = decrypted
|
||||
.sort((a, b) => (a.startDate as string).localeCompare(b.startDate as string))
|
||||
.map((b) => ({
|
||||
id: b.sourceId,
|
||||
title: b.title,
|
||||
startTime: b.startDate,
|
||||
endTime: b.endDate,
|
||||
allDay: b.allDay,
|
||||
}));
|
||||
return {
|
||||
success: true,
|
||||
data: events,
|
||||
message: `${events.length} Termine heute`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
95
apps/mana/apps/web/src/lib/modules/drink/tools.ts
Normal file
95
apps/mana/apps/web/src/lib/modules/drink/tools.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Drink Tools — LLM-accessible operations for beverage tracking.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { drinkStore } from './stores/drink.svelte';
|
||||
import { drinkEntryTable } from './collections';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { DEFAULT_DAILY_GOAL_ML, type DrinkType, type LocalDrinkEntry } from './types';
|
||||
|
||||
export const drinkTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'log_drink',
|
||||
module: 'drink',
|
||||
description: 'Loggt ein Getraenk (Wasser, Kaffee, Tee, etc.)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'drinkType',
|
||||
type: 'string',
|
||||
description: 'Art des Getraenks',
|
||||
required: true,
|
||||
enum: ['water', 'coffee', 'tea', 'juice', 'alcohol', 'smoothie', 'soda', 'other'],
|
||||
},
|
||||
{ name: 'quantityMl', type: 'number', description: 'Menge in Milliliter', required: true },
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
description: 'Name (z.B. "Latte Macchiato")',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
async execute(params) {
|
||||
const entry = await drinkStore.logDrink({
|
||||
name: (params.name as string) ?? (params.drinkType as string),
|
||||
drinkType: params.drinkType as DrinkType,
|
||||
quantityMl: params.quantityMl as number,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: entry,
|
||||
message: `${params.quantityMl}ml ${params.name ?? params.drinkType} geloggt`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_drink_progress',
|
||||
module: 'drink',
|
||||
description: 'Gibt den heutigen Trink-Fortschritt zurueck (Wasser, Kaffee, gesamt)',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const all = await drinkEntryTable.toArray();
|
||||
const todayEntries = all.filter((e) => !e.deletedAt && e.date === today);
|
||||
const decrypted = await decryptRecords<LocalDrinkEntry>('drinkEntries', todayEntries);
|
||||
|
||||
let waterMl = 0;
|
||||
let coffeeMl = 0;
|
||||
let coffeeCount = 0;
|
||||
let totalMl = 0;
|
||||
for (const d of decrypted) {
|
||||
const ml = d.quantityMl ?? 0;
|
||||
totalMl += ml;
|
||||
if (d.drinkType === 'water') waterMl += ml;
|
||||
if (d.drinkType === 'coffee') {
|
||||
coffeeMl += ml;
|
||||
coffeeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
water: {
|
||||
ml: waterMl,
|
||||
goal: DEFAULT_DAILY_GOAL_ML,
|
||||
percent: Math.round((waterMl / DEFAULT_DAILY_GOAL_ML) * 100),
|
||||
},
|
||||
coffee: { ml: coffeeMl, count: coffeeCount },
|
||||
total: { ml: totalMl, count: decrypted.length },
|
||||
},
|
||||
message: `Wasser: ${waterMl}/${DEFAULT_DAILY_GOAL_ML}ml (${Math.round((waterMl / DEFAULT_DAILY_GOAL_ML) * 100)}%), ${decrypted.length} Getraenke gesamt`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'undo_last_drink',
|
||||
module: 'drink',
|
||||
description: 'Macht den letzten Getraenk-Eintrag rueckgaengig',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
await drinkStore.undoLastEntry();
|
||||
return { success: true, message: 'Letzter Eintrag rueckgaengig gemacht' };
|
||||
},
|
||||
},
|
||||
];
|
||||
80
apps/mana/apps/web/src/lib/modules/nutriphi/tools.ts
Normal file
80
apps/mana/apps/web/src/lib/modules/nutriphi/tools.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Nutriphi Tools — LLM-accessible operations for nutrition tracking.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { mealMutations } from './mutations';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { getDailySummary, toMealWithNutrition } from './queries';
|
||||
import type { LocalMeal, MealType, LocalGoal } from './types';
|
||||
|
||||
export const nutriphiTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'log_meal',
|
||||
module: 'nutriphi',
|
||||
description: 'Loggt eine Mahlzeit mit optionalen Naehrwerten',
|
||||
parameters: [
|
||||
{
|
||||
name: 'mealType',
|
||||
type: 'string',
|
||||
description: 'Art der Mahlzeit',
|
||||
required: true,
|
||||
enum: ['breakfast', 'lunch', 'dinner', 'snack'],
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
description: 'Beschreibung der Mahlzeit',
|
||||
required: true,
|
||||
},
|
||||
{ name: 'calories', type: 'number', description: 'Kalorien (kcal)', required: false },
|
||||
{ name: 'protein', type: 'number', description: 'Protein (g)', required: false },
|
||||
],
|
||||
async execute(params) {
|
||||
const nutrition =
|
||||
params.calories || params.protein
|
||||
? {
|
||||
calories: (params.calories as number) ?? 0,
|
||||
protein: (params.protein as number) ?? 0,
|
||||
carbohydrates: 0,
|
||||
fat: 0,
|
||||
fiber: 0,
|
||||
sugar: 0,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const meal = await mealMutations.create({
|
||||
mealType: params.mealType as MealType,
|
||||
description: params.description as string,
|
||||
nutrition,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: meal,
|
||||
message: `${params.mealType} geloggt: "${params.description}"${nutrition ? ` (${nutrition.calories} kcal)` : ''}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_nutrition_summary',
|
||||
module: 'nutriphi',
|
||||
description:
|
||||
'Gibt die heutige Ernaehrungs-Zusammenfassung zurueck (Mahlzeiten, Kalorien, Protein)',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const allMeals = await db.table<LocalMeal>('meals').toArray();
|
||||
const active = allMeals.filter((m) => !m.deletedAt);
|
||||
const decrypted = await decryptRecords<LocalMeal>('meals', active);
|
||||
const meals = decrypted.map(toMealWithNutrition);
|
||||
const goals = await db.table<LocalGoal>('goals').toArray();
|
||||
const activeGoal = goals.find((g) => !g.deletedAt) ?? null;
|
||||
const summary = getDailySummary(meals, new Date(), activeGoal);
|
||||
return {
|
||||
success: true,
|
||||
data: summary,
|
||||
message: `${summary.meals.length} Mahlzeiten, ${summary.progress.calories.current}/${summary.progress.calories.target} kcal`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
108
apps/mana/apps/web/src/lib/modules/places/tools.ts
Normal file
108
apps/mana/apps/web/src/lib/modules/places/tools.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Places Tools — LLM-accessible operations for location tracking.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { placesStore } from './stores/places.svelte';
|
||||
import { trackingStore } from './stores/tracking.svelte';
|
||||
import { placeTable } from './collections';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { toPlace } from './queries';
|
||||
import type { LocalPlace, PlaceCategory } from './types';
|
||||
|
||||
export const placesTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'create_place',
|
||||
module: 'places',
|
||||
description: 'Erstellt einen neuen Ort',
|
||||
parameters: [
|
||||
{ name: 'name', type: 'string', description: 'Name des Ortes', required: true },
|
||||
{ name: 'latitude', type: 'number', description: 'Breitengrad', required: true },
|
||||
{ name: 'longitude', type: 'number', description: 'Laengengrad', required: true },
|
||||
{
|
||||
name: 'category',
|
||||
type: 'string',
|
||||
description: 'Kategorie',
|
||||
required: false,
|
||||
enum: [
|
||||
'home',
|
||||
'work',
|
||||
'food',
|
||||
'shopping',
|
||||
'sport',
|
||||
'culture',
|
||||
'nature',
|
||||
'transport',
|
||||
'health',
|
||||
'education',
|
||||
'nightlife',
|
||||
'other',
|
||||
],
|
||||
},
|
||||
{ name: 'address', type: 'string', description: 'Adresse', required: false },
|
||||
],
|
||||
async execute(params) {
|
||||
const place = await placesStore.createPlace({
|
||||
name: params.name as string,
|
||||
latitude: params.latitude as number,
|
||||
longitude: params.longitude as number,
|
||||
category: params.category as PlaceCategory | undefined,
|
||||
address: params.address as string | undefined,
|
||||
});
|
||||
return { success: true, data: place, message: `Ort "${params.name}" erstellt` };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'record_visit',
|
||||
module: 'places',
|
||||
description: 'Registriert einen Besuch an einem bekannten Ort',
|
||||
parameters: [{ name: 'placeId', type: 'string', description: 'ID des Ortes', required: true }],
|
||||
async execute(params) {
|
||||
await placesStore.recordVisit(params.placeId as string);
|
||||
return { success: true, message: 'Besuch registriert' };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_places',
|
||||
module: 'places',
|
||||
description: 'Gibt alle gespeicherten Orte zurueck',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const all = await placeTable.toArray();
|
||||
const active = all.filter((p) => !p.deletedAt && !p.isArchived);
|
||||
const decrypted = await decryptRecords<LocalPlace>('places', active);
|
||||
const places = decrypted.map(toPlace);
|
||||
return {
|
||||
success: true,
|
||||
data: places.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
category: p.category,
|
||||
visitCount: p.visitCount,
|
||||
})),
|
||||
message: `${places.length} Orte gespeichert`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_current_location',
|
||||
module: 'places',
|
||||
description: 'Gibt die aktuelle GPS-Position zurueck (erfordert Standort-Berechtigung)',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const pos = await trackingStore.getCurrentPosition();
|
||||
if (!pos) {
|
||||
return { success: false, message: 'Standort nicht verfuegbar' };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
latitude: pos.coords.latitude,
|
||||
longitude: pos.coords.longitude,
|
||||
accuracy: pos.coords.accuracy,
|
||||
},
|
||||
message: `Standort: ${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
73
apps/mana/apps/web/src/lib/modules/todo/tools.ts
Normal file
73
apps/mana/apps/web/src/lib/modules/todo/tools.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Todo Tools — LLM-accessible operations for the task module.
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { tasksStore } from './stores/tasks.svelte';
|
||||
import { taskTable } from './collections';
|
||||
import { toTask, getTaskStats } from './queries';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalTask } from './types';
|
||||
|
||||
export const todoTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'create_task',
|
||||
module: 'todo',
|
||||
description: 'Erstellt einen neuen Task mit optionalem Faelligkeitsdatum und Prioritaet',
|
||||
parameters: [
|
||||
{ name: 'title', type: 'string', description: 'Titel des Tasks', required: true },
|
||||
{
|
||||
name: 'dueDate',
|
||||
type: 'string',
|
||||
description: 'Faelligkeitsdatum (YYYY-MM-DD)',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'string',
|
||||
description: 'Prioritaet',
|
||||
required: false,
|
||||
enum: ['low', 'medium', 'high'],
|
||||
},
|
||||
{ name: 'description', type: 'string', description: 'Beschreibung', required: false },
|
||||
],
|
||||
async execute(params) {
|
||||
const task = await tasksStore.createTask({
|
||||
title: params.title as string,
|
||||
dueDate: params.dueDate as string | undefined,
|
||||
priority: (params.priority as 'low' | 'medium' | 'high') ?? undefined,
|
||||
description: params.description as string | undefined,
|
||||
});
|
||||
return { success: true, data: task, message: `Task "${task.title}" erstellt` };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'complete_task',
|
||||
module: 'todo',
|
||||
description: 'Markiert einen Task als erledigt',
|
||||
parameters: [{ name: 'taskId', type: 'string', description: 'ID des Tasks', required: true }],
|
||||
async execute(params) {
|
||||
await tasksStore.completeTask(params.taskId as string);
|
||||
return { success: true, message: 'Task erledigt' };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_task_stats',
|
||||
module: 'todo',
|
||||
description:
|
||||
'Gibt Statistiken ueber alle Tasks zurueck (total, erledigt, ueberfaellig, heute faellig)',
|
||||
parameters: [],
|
||||
async execute() {
|
||||
const all = await taskTable.toArray();
|
||||
const active = all.filter((t) => !t.deletedAt);
|
||||
const decrypted = await decryptRecords<LocalTask>('tasks', active);
|
||||
const tasks = decrypted.map(toTask);
|
||||
const stats = getTaskStats(tasks);
|
||||
return {
|
||||
success: true,
|
||||
data: stats,
|
||||
message: `${stats.total} Tasks (${stats.completed} erledigt, ${stats.overdue} ueberfaellig)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
import { createReminderScheduler } from '@mana/shared-stores';
|
||||
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
|
||||
import { startEventStore, stopEventStore } from '$lib/data/events/event-store';
|
||||
import { initTools } from '$lib/data/tools/init';
|
||||
import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte';
|
||||
import SessionWarning from '$lib/components/SessionWarning.svelte';
|
||||
import EncryptionIntroBanner from '$lib/components/EncryptionIntroBanner.svelte';
|
||||
|
|
@ -419,6 +420,7 @@
|
|||
]);
|
||||
initSharedUload();
|
||||
startEventStore();
|
||||
initTools();
|
||||
await dashboardStore.initialize();
|
||||
|
||||
// Start the persistent LLM task queue. Idempotent — safe to call
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue