mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(ai): add Library AI tools (create / rate / status / list)
Some checks failed
CD Mac Mini / Detect Changes (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Validate (push) Has been cancelled
CI / Auth flow integration test (push) Has been cancelled
Docker Validate / Validate Dockerfiles (push) Has been cancelled
Mirror to Forgejo / Push to Forgejo (push) Has been cancelled
CD Mac Mini / Deploy (push) Has been cancelled
CI / Build mana-auth (push) Has been cancelled
CI / Build mana-search (push) Has been cancelled
CI / Build mana-sync (push) Has been cancelled
CI / Build mana-notify (push) Has been cancelled
CI / Build mana-api-gateway (push) Has been cancelled
CI / Build mana-crawler (push) Has been cancelled
CI / Build mana-media (push) Has been cancelled
CI / Build mana-credits (push) Has been cancelled
CI / Build mana-web (push) Has been cancelled
CI / Build chat-backend (push) Has been cancelled
CI / Build chat-web (push) Has been cancelled
CI / Build todo-backend (push) Has been cancelled
CI / Build todo-web (push) Has been cancelled
CI / Build calendar-backend (push) Has been cancelled
CI / Build calendar-web (push) Has been cancelled
CI / Build clock-web (push) Has been cancelled
CI / Build contacts-backend (push) Has been cancelled
CI / Build contacts-web (push) Has been cancelled
CI / Build presi-web (push) Has been cancelled
CI / Build storage-backend (push) Has been cancelled
CI / Build storage-web (push) Has been cancelled
CI / Build telegram-stats-bot (push) Has been cancelled
CI / Build food-backend (push) Has been cancelled
CI / Build food-web (push) Has been cancelled
CI / Build skilltree-web (push) Has been cancelled
Docker Validate / Build calendar-web (push) Has been cancelled
Docker Validate / Build quotes-web (push) Has been cancelled
Docker Validate / Build todo-backend (push) Has been cancelled
Docker Validate / Build todo-web (push) Has been cancelled
Docker Validate / Build mana-auth (push) Has been cancelled
Docker Validate / Build mana-sync (push) Has been cancelled
Docker Validate / Build mana-media (push) Has been cancelled
Some checks failed
CD Mac Mini / Detect Changes (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Validate (push) Has been cancelled
CI / Auth flow integration test (push) Has been cancelled
Docker Validate / Validate Dockerfiles (push) Has been cancelled
Mirror to Forgejo / Push to Forgejo (push) Has been cancelled
CD Mac Mini / Deploy (push) Has been cancelled
CI / Build mana-auth (push) Has been cancelled
CI / Build mana-search (push) Has been cancelled
CI / Build mana-sync (push) Has been cancelled
CI / Build mana-notify (push) Has been cancelled
CI / Build mana-api-gateway (push) Has been cancelled
CI / Build mana-crawler (push) Has been cancelled
CI / Build mana-media (push) Has been cancelled
CI / Build mana-credits (push) Has been cancelled
CI / Build mana-web (push) Has been cancelled
CI / Build chat-backend (push) Has been cancelled
CI / Build chat-web (push) Has been cancelled
CI / Build todo-backend (push) Has been cancelled
CI / Build todo-web (push) Has been cancelled
CI / Build calendar-backend (push) Has been cancelled
CI / Build calendar-web (push) Has been cancelled
CI / Build clock-web (push) Has been cancelled
CI / Build contacts-backend (push) Has been cancelled
CI / Build contacts-web (push) Has been cancelled
CI / Build presi-web (push) Has been cancelled
CI / Build storage-backend (push) Has been cancelled
CI / Build storage-web (push) Has been cancelled
CI / Build telegram-stats-bot (push) Has been cancelled
CI / Build food-backend (push) Has been cancelled
CI / Build food-web (push) Has been cancelled
CI / Build skilltree-web (push) Has been cancelled
Docker Validate / Build calendar-web (push) Has been cancelled
Docker Validate / Build quotes-web (push) Has been cancelled
Docker Validate / Build todo-backend (push) Has been cancelled
Docker Validate / Build todo-web (push) Has been cancelled
Docker Validate / Build mana-auth (push) Has been cancelled
Docker Validate / Build mana-sync (push) Has been cancelled
Docker Validate / Build mana-media (push) Has been cancelled
Library module had no AI tool coverage post the M1 skeleton. Adds
four tools so the agent can curate the reading/watch list alongside
other modules:
- create_library_entry (propose) — books/movies/series/comics with
creators, year, status, rating, tags, genres. Default status
"planned" covers the most common flow ("add to watchlist").
- update_library_entry_status (propose) — status transitions
planned → active → completed (also paused / dropped). Auto-
stamps startedAt/completedAt on the matching transitions so the
existing Dexie projections (streaks, progress) fire correctly.
- rate_library_entry (propose) — 1-5 stars, thin wrapper over the
store's rate() method.
- list_library_entries (auto) — id/kind/title/status/rating/year,
filterable by kind + status.
Coverage table in apps/mana/CLAUDE.md updated (+library, +invoices
row that wasn't listed). Total now 67 tools / 21 modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea1c9c1364
commit
8e677c9066
4 changed files with 305 additions and 1 deletions
|
|
@ -210,7 +210,7 @@ The companion is a **second actor** that works alongside the human in every modu
|
|||
- **Scene lens** — workbench scenes can bind to an agent via `scene.viewingAsAgentId` (context menu → "An Agent binden…"). Pure UI lens, not a data-scope change. `SceneAppBar` shows the agent avatar on bound scene tabs.
|
||||
- **Workbench timeline** — `/ai-workbench` renders every AI-attributed event grouped by mission iteration with per-**agent** filter, per-module, per-mission. Each bucket header shows agent avatar + name + mission title. Per-bucket **Revert button** undoes the iteration's writes via `data/ai/revert/` (TaskCreated → delete, TaskCompleted → uncomplete, etc., newest-first). Separate **"Datenzugriff"** tab exposes the server-side decrypt audit (for missions with Key-Grants).
|
||||
|
||||
### Tool Coverage (59 tools, 19 modules)
|
||||
### Tool Coverage (67 tools, 21 modules)
|
||||
|
||||
Agents interact with the app through tools — each one either auto (executes silently during reasoning) or propose (creates a Proposal card the user must approve). Source of truth: `AI_TOOL_CATALOG` in `@mana/shared-ai/src/tools/schemas.ts` — both webapp policy (`src/lib/data/ai/policy.ts`) and server-side planner (`services/mana-ai/src/planner/tools.ts`) derive from it automatically, so drift is structurally impossible.
|
||||
|
||||
|
|
@ -235,6 +235,8 @@ Agents interact with the app through tools — each one either auto (executes si
|
|||
| finance | `add_transaction` | `get_month_summary`, `list_transactions` |
|
||||
| times | `start_timer`, `stop_timer` | `get_time_stats`, `get_timer_status`, `list_projects` |
|
||||
| wetter | — | `get_weather`, `get_rain_forecast` |
|
||||
| invoices | `create_invoice`, `mark_invoice_paid` | `list_invoices`, `get_invoice_stats` |
|
||||
| library | `create_library_entry`, `update_library_entry_status`, `rate_library_entry` | `list_library_entries` |
|
||||
|
||||
**Server-side web-research**: mana-ai calls mana-api's `/api/v1/news-research/discover` + `/search` directly before the planner prompt is built (pre-planning injection). Missions with research-keyword objectives get real article URLs + excerpts injected as a synthetic ResolvedInput. See `services/mana-ai/src/planner/news-research-client.ts`.
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { wishesTools } from '$lib/modules/wishes/tools';
|
|||
import { wetterTools } from '$lib/modules/wetter/tools';
|
||||
import { quizTools } from '$lib/modules/quiz/tools';
|
||||
import { invoicesTools } from '$lib/modules/invoices/tools';
|
||||
import { libraryTools } from '$lib/modules/library/tools';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
|
@ -85,5 +86,6 @@ export function initTools(): void {
|
|||
registerTools(wetterTools);
|
||||
registerTools(quizTools);
|
||||
registerTools(invoicesTools);
|
||||
registerTools(libraryTools);
|
||||
initialized = true;
|
||||
}
|
||||
|
|
|
|||
193
apps/mana/apps/web/src/lib/modules/library/tools.ts
Normal file
193
apps/mana/apps/web/src/lib/modules/library/tools.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/**
|
||||
* Library tools — AI-accessible CRUD over the encrypted library table.
|
||||
*
|
||||
* Propose:
|
||||
* - create_library_entry — new book/movie/series/comic
|
||||
* - update_library_entry_status — planned → active → completed …
|
||||
* - rate_library_entry — 1-5 stars
|
||||
*
|
||||
* Auto:
|
||||
* - list_library_entries — filtered by kind + status
|
||||
*/
|
||||
|
||||
import type { ModuleTool } from '$lib/data/tools/types';
|
||||
import { libraryEntriesStore } from './stores/entries.svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords, VaultLockedError } from '$lib/data/crypto';
|
||||
import { toLibraryEntry } from './queries';
|
||||
import type { LocalLibraryEntry, LibraryKind, LibraryStatus } from './types';
|
||||
|
||||
const KINDS = ['book', 'movie', 'series', 'comic'] as const;
|
||||
const STATUSES = ['planned', 'active', 'completed', 'paused', 'dropped'] as const;
|
||||
|
||||
function splitList(raw: unknown): string[] | undefined {
|
||||
if (typeof raw !== 'string') return undefined;
|
||||
const parts = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return parts.length > 0 ? parts : undefined;
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
export const libraryTools: ModuleTool[] = [
|
||||
{
|
||||
name: 'create_library_entry',
|
||||
module: 'library',
|
||||
description: 'Erstellt einen neuen Bibliotheks-Eintrag',
|
||||
parameters: [
|
||||
{ name: 'kind', type: 'string', description: 'Art', required: true },
|
||||
{ name: 'title', type: 'string', description: 'Titel', required: true },
|
||||
{ name: 'creators', type: 'string', description: 'Autor/Regie, CSV', required: false },
|
||||
{ name: 'year', type: 'number', description: 'Jahr', required: false },
|
||||
{ name: 'status', type: 'string', description: 'Status', required: false },
|
||||
{ name: 'rating', type: 'number', description: 'Bewertung 1-5', required: false },
|
||||
{ name: 'tags', type: 'string', description: 'Tags CSV', required: false },
|
||||
{ name: 'genres', type: 'string', description: 'Genres CSV', required: false },
|
||||
],
|
||||
async execute(params) {
|
||||
const kind = params.kind as LibraryKind;
|
||||
if (!KINDS.includes(kind)) {
|
||||
return { success: false, message: `Unbekannte Art: ${kind}` };
|
||||
}
|
||||
const status = (params.status as LibraryStatus | undefined) ?? 'planned';
|
||||
if (!STATUSES.includes(status)) {
|
||||
return { success: false, message: `Unbekannter Status: ${status}` };
|
||||
}
|
||||
|
||||
const title = String(params.title ?? '').trim();
|
||||
if (!title) return { success: false, message: 'title darf nicht leer sein' };
|
||||
|
||||
const ratingNum = typeof params.rating === 'number' ? params.rating : null;
|
||||
if (ratingNum !== null && (ratingNum < 1 || ratingNum > 5)) {
|
||||
return { success: false, message: 'rating muss zwischen 1 und 5 liegen' };
|
||||
}
|
||||
|
||||
const entry = await libraryEntriesStore.createEntry({
|
||||
kind,
|
||||
title,
|
||||
creators: splitList(params.creators),
|
||||
year: typeof params.year === 'number' ? params.year : null,
|
||||
status,
|
||||
rating: ratingNum,
|
||||
tags: splitList(params.tags),
|
||||
genres: splitList(params.genres),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { entryId: entry.id, kind: entry.kind, title: entry.title },
|
||||
message: `${entry.kind} "${entry.title}" angelegt`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_library_entry_status',
|
||||
module: 'library',
|
||||
description:
|
||||
'Aendert den Status eines Eintrags. Setzt bei active / completed automatisch die passenden Zeitstempel.',
|
||||
parameters: [
|
||||
{ name: 'entryId', type: 'string', description: 'ID des Eintrags', required: true },
|
||||
{ name: 'status', type: 'string', description: 'Neuer Status', required: true },
|
||||
],
|
||||
async execute(params) {
|
||||
const entryId = String(params.entryId ?? '');
|
||||
const status = params.status as LibraryStatus;
|
||||
if (!STATUSES.includes(status)) {
|
||||
return { success: false, message: `Unbekannter Status: ${status}` };
|
||||
}
|
||||
|
||||
const existing = await db.table<LocalLibraryEntry>('libraryEntries').get(entryId);
|
||||
if (!existing || existing.deletedAt) {
|
||||
return { success: false, message: `Eintrag ${entryId} nicht gefunden` };
|
||||
}
|
||||
|
||||
const patch: Partial<LocalLibraryEntry> = { status };
|
||||
if (status === 'active' && !existing.startedAt) patch.startedAt = nowIso();
|
||||
if (status === 'completed' && !existing.completedAt) patch.completedAt = nowIso();
|
||||
|
||||
await libraryEntriesStore.updateEntry(entryId, patch);
|
||||
return {
|
||||
success: true,
|
||||
data: { entryId, status },
|
||||
message: `Status auf "${status}" gesetzt`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rate_library_entry',
|
||||
module: 'library',
|
||||
description: 'Setzt die Bewertung (1-5) eines Eintrags',
|
||||
parameters: [
|
||||
{ name: 'entryId', type: 'string', description: 'ID des Eintrags', required: true },
|
||||
{ name: 'rating', type: 'number', description: 'Bewertung 1-5', required: true },
|
||||
],
|
||||
async execute(params) {
|
||||
const entryId = String(params.entryId ?? '');
|
||||
const rating = params.rating as number;
|
||||
if (typeof rating !== 'number' || rating < 1 || rating > 5) {
|
||||
return { success: false, message: 'rating muss zwischen 1 und 5 liegen' };
|
||||
}
|
||||
|
||||
await libraryEntriesStore.rate(entryId, rating);
|
||||
return {
|
||||
success: true,
|
||||
data: { entryId, rating },
|
||||
message: `Bewertung ${rating}/5 gesetzt`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_library_entries',
|
||||
module: 'library',
|
||||
description:
|
||||
'Listet Bibliotheks-Eintraege (id, kind, title, status, rating). Filterbar nach kind und status.',
|
||||
parameters: [
|
||||
{ name: 'kind', type: 'string', description: 'Nur eine Art', required: false },
|
||||
{ name: 'status', type: 'string', description: 'Nur einen Status', required: false },
|
||||
{ name: 'limit', type: 'number', description: 'Max (Standard 30)', required: false },
|
||||
],
|
||||
async execute(params) {
|
||||
const kindFilter = params.kind as LibraryKind | undefined;
|
||||
const statusFilter = params.status as LibraryStatus | undefined;
|
||||
const limit = Math.min(Math.max(Number(params.limit) || 30, 1), 100);
|
||||
|
||||
try {
|
||||
const all = await db.table<LocalLibraryEntry>('libraryEntries').toArray();
|
||||
const visible = all.filter((e) => !e.deletedAt);
|
||||
const decrypted = await decryptRecords('libraryEntries', visible);
|
||||
const rows = decrypted
|
||||
.map(toLibraryEntry)
|
||||
.filter((e) => (kindFilter ? e.kind === kindFilter : true))
|
||||
.filter((e) => (statusFilter ? e.status === statusFilter : true))
|
||||
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
|
||||
.slice(0, limit)
|
||||
.map((e) => ({
|
||||
id: e.id,
|
||||
kind: e.kind,
|
||||
title: e.title,
|
||||
status: e.status,
|
||||
rating: e.rating,
|
||||
year: e.year,
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { entries: rows, total: rows.length },
|
||||
message: `${rows.length} Eintrag(e) gelistet`,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof VaultLockedError) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Vault ist gesperrt — Bibliothek kann nicht entschlüsselt werden',
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -1216,6 +1216,113 @@ export const AI_TOOL_CATALOG: readonly ToolSchema[] = [
|
|||
defaultPolicy: 'auto',
|
||||
parameters: [],
|
||||
},
|
||||
|
||||
// ── Library ───────────────────────────────────────────────
|
||||
{
|
||||
name: 'create_library_entry',
|
||||
module: 'library',
|
||||
description:
|
||||
'Erstellt einen neuen Eintrag in der Bibliothek (Buch, Film, Serie oder Comic). Default-Status ist "planned" falls nicht anders angegeben.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{
|
||||
name: 'kind',
|
||||
type: 'string',
|
||||
description: 'Art des Eintrags',
|
||||
required: true,
|
||||
enum: ['book', 'movie', 'series', 'comic'],
|
||||
},
|
||||
{ name: 'title', type: 'string', description: 'Titel', required: true },
|
||||
{
|
||||
name: 'creators',
|
||||
type: 'string',
|
||||
description: 'Autor/Regisseur/Creator, mehrere durch Komma trennen',
|
||||
required: false,
|
||||
},
|
||||
{ name: 'year', type: 'number', description: 'Erscheinungsjahr', required: false },
|
||||
{
|
||||
name: 'status',
|
||||
type: 'string',
|
||||
description: 'Anfangsstatus',
|
||||
required: false,
|
||||
enum: ['planned', 'active', 'completed', 'paused', 'dropped'],
|
||||
},
|
||||
{
|
||||
name: 'rating',
|
||||
type: 'number',
|
||||
description: 'Bewertung 1-5 (nur bei completed sinnvoll)',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
description: 'Tags durch Komma getrennt',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'genres',
|
||||
type: 'string',
|
||||
description: 'Genres durch Komma getrennt',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'update_library_entry_status',
|
||||
module: 'library',
|
||||
description:
|
||||
'Aendert den Status eines Bibliotheks-Eintrags (planned/active/completed/paused/dropped). Setzt beim Wechsel auf "active" automatisch startedAt, bei "completed" completedAt.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{ name: 'entryId', type: 'string', description: 'ID des Eintrags', required: true },
|
||||
{
|
||||
name: 'status',
|
||||
type: 'string',
|
||||
description: 'Neuer Status',
|
||||
required: true,
|
||||
enum: ['planned', 'active', 'completed', 'paused', 'dropped'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'rate_library_entry',
|
||||
module: 'library',
|
||||
description: 'Setzt die Bewertung (1-5) eines Bibliotheks-Eintrags.',
|
||||
defaultPolicy: 'propose',
|
||||
parameters: [
|
||||
{ name: 'entryId', type: 'string', description: 'ID des Eintrags', required: true },
|
||||
{ name: 'rating', type: 'number', description: 'Bewertung 1 bis 5', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list_library_entries',
|
||||
module: 'library',
|
||||
description:
|
||||
'Listet Bibliotheks-Eintraege (id, kind, title, status, rating). Optional nach Art und Status filterbar.',
|
||||
defaultPolicy: 'auto',
|
||||
parameters: [
|
||||
{
|
||||
name: 'kind',
|
||||
type: 'string',
|
||||
description: 'Nur eine Art zeigen',
|
||||
required: false,
|
||||
enum: ['book', 'movie', 'series', 'comic'],
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'string',
|
||||
description: 'Nur einen Status zeigen',
|
||||
required: false,
|
||||
enum: ['planned', 'active', 'completed', 'paused', 'dropped'],
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
description: 'Maximale Anzahl (Standard 30)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue