feat(learn): an assistant reads PDFs' pages, and sees images the person chooses or approves

ADR 0021, D12: text first. Lookups find a studyset's PDFs and read a page's
text as PDFium extracts it. An assistant that reads images is offered a
card's images as chips, and may ask to see a card's image or a PDF's page
drawn whole; the conversation asks the person each time (Show it, Don't
show) and sends nothing meanwhile. Images go only as png, jpeg, gif or webp
of at most 5 MB, are never kept in a turn, and a follow-up goes without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-25 21:54:56 +01:00
commit 2b7d04fd70
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_prompt.dart';
@ -67,6 +69,59 @@ void main() {
});
});
group('an image chosen (D12)', () {
final bowl = LlmImage(
mediaType: 'image/png',
bytes: Uint8List.fromList([1, 2, 3]),
);
const image = AssistantPart(
kind: AssistantPartKind.image,
text: 'asset-bowl',
number: 2,
);
test('goes labelled, the image after its label', () {
final message = questionMessage(
'What is it?',
const [
image,
],
images: {'asset-bowl': bowl},
);
expect(message.parts, hasLength(3));
expect((message.parts[1] as LlmText).text, "\n\nThe card's image 2:\n");
expect(message.parts[2], same(bowl));
});
test('that cannot be had is said to be missing', () {
final message = questionMessage('What is it?', const [image]);
expect(_texts(message).last, "\n\nThe card's image 2:\n$imageNotHad");
});
test('is left as a mark once answered, read back as the part it was', () {
final question = questionMessage(
'What is it?',
const [
image,
],
images: {'asset-bowl': bowl},
);
final answer = LlmMessage.assistant(const [LlmText('A bowl.')]);
final kept = withoutImages([question, answer]);
expect(kept.last, same(answer));
expect(_texts(kept.first).last, imageNotKept);
final asked = askedIn(kept.first);
expect(asked.question, 'What is it?');
expect(asked.parts, [
const AssistantPart(kind: AssistantPartKind.image, text: '', number: 2),
]);
});
});
group('the system prompt', () {
test('offers nothing to look up where only what is ticked goes', () {
final prompt = systemPrompt(const AssistantScope());