[03-Developer] #260 in-app tool calling (Gemma 4 multi-turn)

ADR-0005 in-process tool runtime — 6 tools (catalog 2 + tracker 2 +
habit 2), ToolDispatcher with JSON-schema validation + modal ConfirmGate
for destructive ops, multi-turn LlmChatSession abstraction wired to
flutter_gemma 0.16.5 (ToolChoice.auto), ChatSessionController with
MAX_TURNS=4 safety + 8-turn history hint, ChatScreen entry behind AI
opt-in. R3/R7/R8 enforced inside handlers. 41 new tests (envelope,
catalog/tracker/habit tools, dispatcher, controller loop) — 151 total
passing.

Refs #260
This commit is contained in:
2026-06-15 10:42:43 +09:00
parent eca097aa2c
commit b1bed4d5ca
21 changed files with 2313 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import 'package:drift/drift.dart' as drift;
import 'package:life_helper/ai/tools/tool_definition.dart';
import 'package:life_helper/core/constants.dart';
import 'package:life_helper/core/time.dart';
import 'package:life_helper/data/catalog/catalog_repository.dart';
import 'package:life_helper/data/db/app_database.dart';
import 'package:life_helper/data/db/daos/habit_dao.dart';
import 'package:life_helper/data/db/daos/tracker_dao.dart';
import 'package:life_helper/data/seed/seed_importer.dart';
import 'package:life_helper/domain/models/frame_pattern.dart';
import '../../data/seed/test_seeds.dart';
/// Tool tests share a tiny in-memory bootstrap. Returns the assembled
/// [ToolDeps] plus the underlying [AppDatabase] so callers can close it
/// in tearDown.
Future<({AppDatabase db, ToolDeps deps})> bootstrapToolDeps() async {
final db = AppDatabase.memory();
// default user (seed importer doesn't insert users — bootstrap does).
await db.into(db.users).insert(UsersCompanion.insert(
id: kLocalDefaultUserId,
displayName: const drift.Value('Test'),
createdAt: nowKst().toIso8601String(),
));
await SeedImporter(db, loadAsset: testStubLoader).importIfNeeded();
final patterns = await db.select(db.framePatterns).get();
final framePatterns = patterns
.map((r) => FramePatternModel(
id: r.id,
domain: r.domain,
avoidanceKeyword: r.avoidanceKeyword,
l0Example: r.l0Example,
l1SimpleReplace: r.l1SimpleReplace,
l2Suggestion: r.l2Suggestion,
l3Identity: r.l3Identity,
))
.toList();
return (
db: db,
deps: ToolDeps(
habitDao: HabitDao(db),
trackerDao: TrackerDao(db),
catalog: CatalogRepository(db),
framePatterns: framePatterns,
userId: kLocalDefaultUserId,
),
);
}