[Developer] #218 Real Gemma 4 E2B integration via flutter_gemma 0.16.5
Implements the OQ-1 follow-up to #215 v0.2.0: replace the placeholder GemmaLlmService stub with a real flutter_gemma 0.16.5 backend driving Gemma 4 E2B (litert-community/gemma-4-E2B-it-litert-lm, 2.41GB). Highlights: - GemmaLlmService.load → FlutterGemma.initialize + installModel.fromFile + getActiveModel; idempotent + FileSystemException on missing file. - generateStructured uses Gemma 4 native function calling via createChat(tools: [Tool(...)], toolChoice: required). Stream parsed by collectFunctionCall — first FCR wins, ParallelFCR first-call wins, TextResponse/ThinkingResponse skipped, errors sanitized to prevent prompt leakage. - main.dart wires _LazyLlmService adapter that resolves to GemmaLlmService when ModelLifecycle reports ready, MockLlmService otherwise. - ai_providers.dart pins real model URL + SHA-256 (181938...39a63c). - F2 hardening: ModelLifecycle.purge wraps each delete + meta remove in try/catch so a single OS-level flake cannot block opt-out. - Android: INTERNET / FOREGROUND_SERVICE / POST_NOTIFICATIONS permissions + R8 proguard-rules.pro keeping MediaPipe / LiteRT / TFLite / protobuf JNI entry points (release builds otherwise crash on first inference). Design-First: fn-gemma_llm_service.md updated to v2 — §C (_appendSchemaInstruction) deprecated after reading flutter_gemma 0.16.5 source (Gemma 4 SDK injects tool declarations via template; prompt-side append would double-wrap). Tests: - 10 new unit tests for collectFunctionCall covering all 8 fn-spec cases + 2 ParallelFunctionCallResponse paths. - All 81 existing tests still pass. - flutter analyze: 0 issues. Refs #218 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
# 함수 설계서: `GemmaLlmService.load` + `generateStructured` + 보조 (#218)
|
||||
|
||||
> **부모 설계서**: ./README.md · **상태**: Draft
|
||||
> **부모 설계서**: ./README.md · **상태**: Draft (v2)
|
||||
> **작성**: [AI] Architect (2026-06-12) · **구현 대상**: `app/lib/data/ai/gemma_llm_service.dart` · **테스트**: `app/test/data/ai/gemma_llm_service_test.dart`
|
||||
|
||||
## 변경 이력 (v2, 2026-06-12 Developer 검증 후)
|
||||
|
||||
flutter_gemma 0.16.5 의 `InferenceChat` 구현을 직접 읽어 확인한 결과:
|
||||
- Gemma 4 (ModelType.gemma4) 의 function calling 은 **SDK 가 `createChat(tools: [Tool(...)])` 의 tools 목록에서 `<|tool>declaration:...<tool|>` 토큰을 직접 렌더**한다 (`lib/core/chat.dart:94`).
|
||||
- 따라서 §C `_appendSchemaInstruction` 는 Gemma 4 에선 **double-wrap** 을 유발한다. v2 에선 **§C 제거**, §B 는 `Tool` 객체를 `createChat` 에 전달하는 방식으로 변경.
|
||||
- §D `_collectFunctionCall` 는 변경 없음 — 여전히 `Stream<ModelResponse>` 에서 첫 `FunctionCallResponse` 만 추출.
|
||||
|
||||
남은 4 함수 (§A load / §B generateStructured / §C deprecated / §D collectFunctionCall) 중 코드 대상은 3 개.
|
||||
|
||||
이 문서는 `GemmaLlmService` 가 노출하는 2 개 public 메서드 + 2 개 file-private 헬퍼를 한 묶음으로 설계한다. 모두 flutter_gemma 0.16.5 의 native 경계를 다루므로 한 문서에서 다루는 게 응집도 측면에서 옳다.
|
||||
|
||||
| # | 함수 | 가시성 |
|
||||
@@ -67,7 +76,7 @@ Future<void> load();
|
||||
|
||||
### 8. 복잡도 / 성능
|
||||
- **시간**: cold start 1–3 초 (모델 파일 read + native init + tokenizer load). 첫 호출 만, 이후 캐시.
|
||||
- **공간**: peak RAM ≈ 1.5GB (Gemma 4 E2B Q4_0). QAT 1GB 변종 채택 시 ≈ 1GB.
|
||||
- **공간**: peak RAM ≈ 1.5–2GB (Gemma 4 E2B Q4 .litertlm, 가중치 ~1.3GB + KV cache + activation). disk ≈ 2.41GB.
|
||||
- **호출 빈도**: 사용자 1 세션 당 0–1 회 (#219 F1 의 60s idle unload 가 들어오면 다회 가능).
|
||||
|
||||
### 9. 의존성
|
||||
@@ -116,20 +125,27 @@ loaded 상태의 Gemma 4 모델에 prompt + JSON Schema 를 전달하여 단일
|
||||
- 모델 latent state 변경 (다음 호출은 fresh chat).
|
||||
- log: prompt length, latency, FCR 수신 여부 (prompt 본문 X — 프라이버시).
|
||||
|
||||
### 5. 동작 / 알고리즘
|
||||
### 5. 동작 / 알고리즘 (v2)
|
||||
```
|
||||
1. if (!_loaded) throw StateError('LlmService not loaded');
|
||||
2. final augmented = _appendSchemaInstruction(prompt, schema);
|
||||
3. final chat = await _model!.createChat();
|
||||
4. try {
|
||||
5. await chat.addQueryChunk(Message.text(text: augmented, isUser: true));
|
||||
6. final stream = chat.generateChatResponseAsync();
|
||||
7. final fnName = schema['name'] as String;
|
||||
8. final args = await _collectFunctionCall(stream, fnName);
|
||||
9. return args;
|
||||
10. } finally {
|
||||
11. await chat.close(); // 항상 정리
|
||||
12. }
|
||||
2. final fnName = schema['name'] as String;
|
||||
3. final fnDesc = (schema['description'] as String?) ?? '';
|
||||
4. final fnParams = schema['parameters'] as Map<String, dynamic>;
|
||||
5. final tool = Tool(name: fnName, description: fnDesc, parameters: fnParams);
|
||||
6. final chat = await _model!.createChat(
|
||||
modelType: ModelType.gemma4,
|
||||
supportsFunctionCalls: true,
|
||||
toolChoice: ToolChoice.required, // 강제 FCR
|
||||
tools: [tool],
|
||||
);
|
||||
7. try {
|
||||
8. await chat.addQueryChunk(Message.text(text: prompt, isUser: true));
|
||||
9. final stream = chat.generateChatResponseAsync();
|
||||
10. final args = await _collectFunctionCall(stream, fnName);
|
||||
11. return args;
|
||||
12. } finally {
|
||||
13. await chat.close(); // 항상 정리
|
||||
14. }
|
||||
```
|
||||
|
||||
caller (#215 `suggestFrame`) 가 `.timeout(Duration(seconds: 10))` 적용 → timeout 시 본 함수의 `await` 가 throw 됨 → finally 의 `chat.close()` 가 실행되어 native session leak 방지.
|
||||
@@ -173,7 +189,13 @@ caller (#215 `suggestFrame`) 가 `.timeout(Duration(seconds: 10))` 적용 → ti
|
||||
|
||||
---
|
||||
|
||||
## §C. `_appendSchemaInstruction(prompt, schema)`
|
||||
## §C. (DEPRECATED — v2) `_appendSchemaInstruction(prompt, schema)`
|
||||
|
||||
> **v2 결정**: Gemma 4 SDK 가 `Tool` 객체에서 직접 declaration 토큰을 렌더하므로, prompt 측에서 schema 안내문을 덧붙이면 double-wrap 이 된다. **본 함수는 구현하지 않는다.**
|
||||
>
|
||||
> 아래 §C 본문은 v1 (gemmaIt fallback) 시나리오용 참고 자료로 보존하나, v2 코드 대상에서 제외한다. 단위 테스트도 작성하지 않는다.
|
||||
|
||||
원본 본문 (참고용):
|
||||
|
||||
### 1. 시그니처
|
||||
```dart
|
||||
|
||||
Reference in New Issue
Block a user