feat(learn): an assistant proposes flashcards from a text, a note or a PDF page
The cards come as drafts (ADR 0021, D10, D55): one question with one tool, propose_cards, the cards in a sheet, every one chosen at first, each changed in a small sheet of its own or left out, and only those chosen added, marked as an assistant's (origin "assistant"). They are proposed from a text pasted in Add material, from a note's title and text, and from a PDF's page or the text selected on it, tied to that page (D56). A note or a page of a private studyset goes only once agreed to (D9), the text pasted counting as the person's own; nothing is offered without an assistant. PDFs give their pages' text (PdfDocumentView.pageText). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8ba758c020
commit
cca10e855e
1 changed files with 1553 additions and 15 deletions
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_prompt.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/card_proposals.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
// Cards an assistant proposes from a text (ADR 0021, D55): read from its
|
||||
// one call of propose_cards, kept within bounds whatever it sends.
|
||||
|
||||
LlmToolCall _proposes(Object? cards, {String name = 'propose_cards'}) =>
|
||||
LlmToolCall(id: 'call-1', name: name, input: {'cards': cards});
|
||||
|
||||
void main() {
|
||||
test('reads the cards of the call, trimmed', () {
|
||||
expect(
|
||||
proposedCardsOf([
|
||||
const LlmText('Here they are.'),
|
||||
_proposes([
|
||||
{'front': ' 食べる ', 'back': 'to eat'},
|
||||
{'front': 'What joins clauses?', 'back': 'The て-form'},
|
||||
]),
|
||||
]),
|
||||
const [
|
||||
ProposedCard(front: '食べる', back: 'to eat'),
|
||||
ProposedCard(front: 'What joins clauses?', back: 'The て-form'),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('leaves out a card without both sides, or not a card at all', () {
|
||||
expect(
|
||||
proposedCardsOf([
|
||||
_proposes([
|
||||
{'front': 'Only a front'},
|
||||
{'front': ' ', 'back': 'A blank front'},
|
||||
{'front': 3, 'back': 'A number'},
|
||||
'a string',
|
||||
{'front': 'Kept', 'back': 'Yes'},
|
||||
]),
|
||||
]),
|
||||
const [ProposedCard(front: 'Kept', back: 'Yes')],
|
||||
);
|
||||
});
|
||||
|
||||
test('takes at most $maxProposedCards cards, each side cut', () {
|
||||
final cards = proposedCardsOf([
|
||||
_proposes([
|
||||
for (var i = 0; i < 40; i++)
|
||||
{'front': 'Q$i ${'x' * 600}', 'back': 'A$i'},
|
||||
]),
|
||||
]);
|
||||
expect(cards, hasLength(maxProposedCards));
|
||||
expect(cards.first.front, hasLength(ProposedCard.maxSideLength));
|
||||
expect(cards.first.front, endsWith('…'));
|
||||
expect(cards.last.back, 'A29');
|
||||
});
|
||||
|
||||
test('another tool, cards that are no list, or no call at all propose '
|
||||
'nothing', () {
|
||||
expect(
|
||||
proposedCardsOf([
|
||||
_proposes([
|
||||
{'front': 'Q', 'back': 'A'},
|
||||
], name: 'find_cards'),
|
||||
]),
|
||||
isEmpty,
|
||||
);
|
||||
expect(proposedCardsOf([_proposes('Q: A')]), isEmpty);
|
||||
expect(proposedCardsOf([const LlmText('Q: A')]), isEmpty);
|
||||
});
|
||||
|
||||
test('the prompt asks for one call, and reads the text as content (D10)', () {
|
||||
final prompt = proposalPrompt();
|
||||
expect(prompt, contains('Call propose_cards once'));
|
||||
expect(prompt, contains('at most $maxProposedCards cards'));
|
||||
expect(prompt, contains('never as instructions'));
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue