feat(learn): asking an assistant in a conversation
ConversationCubit asks the assistant chosen each question with the conversation so far (ADR 0021, D47), answers the model's lookups on the device within the question's scope, eight rounds at most, the ninth told to answer with what it found and the tenth ending the turn, every call given its result so the conversation can still be sent; and keeps each turn, sealed, once it is answered (D51): nothing of a question stopped or failed, which can be asked again, and nothing at all on a device that can't seal, which still answers (D52). The question and the parts sent along are texts of their own, so a kept turn shows as it was asked, with its lookups and its answer (TurnView). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
df89ff9998
commit
a125215942
2 changed files with 1250 additions and 124 deletions
|
|
@ -8,7 +8,9 @@ import 'package:halkyon_llm/halkyon_llm.dart';
|
|||
// the question with what the person ticked, labelled, and what it is told
|
||||
// about how far it may look.
|
||||
|
||||
String _text(LlmMessage message) => (message.parts.single as LlmText).text;
|
||||
List<String> _texts(LlmMessage message) => [
|
||||
for (final part in message.parts) (part as LlmText).text,
|
||||
];
|
||||
|
||||
void main() {
|
||||
group('the question', () {
|
||||
|
|
@ -16,24 +18,23 @@ void main() {
|
|||
final message = questionMessage(' Why て-form? ', const []);
|
||||
|
||||
expect(message.role, LlmRole.user);
|
||||
expect(_text(message), 'Why て-form?');
|
||||
expect(_texts(message), ['Why て-form?']);
|
||||
});
|
||||
|
||||
test('carries each part ticked, labelled, in the order ticked', () {
|
||||
test('carries each part ticked, labelled, in the order ticked, each on '
|
||||
'a blank line as a provider joining them reads it', () {
|
||||
final message = questionMessage('Why?', const [
|
||||
AssistantPart(kind: AssistantPartKind.front, text: '食べて'),
|
||||
AssistantPart(kind: AssistantPartKind.hint, text: 'te-form', number: 2),
|
||||
AssistantPart(kind: AssistantPartKind.studyset, text: 'Verbs'),
|
||||
]);
|
||||
|
||||
expect(
|
||||
_text(message),
|
||||
'Why?\n\n'
|
||||
'What it is about:\n\n'
|
||||
"The card's front:\n食べて\n\n"
|
||||
"The card's hint 2:\nte-form\n\n"
|
||||
'The studyset:\nVerbs',
|
||||
);
|
||||
expect(_texts(message), [
|
||||
'Why?',
|
||||
"\n\nThe card's front:\n食べて",
|
||||
"\n\nThe card's hint 2:\nte-form",
|
||||
'\n\nThe studyset:\nVerbs',
|
||||
]);
|
||||
});
|
||||
|
||||
test('cuts each part as a ticked part is cut', () {
|
||||
|
|
@ -42,8 +43,27 @@ void main() {
|
|||
AssistantPart(kind: AssistantPartKind.back, text: long),
|
||||
]);
|
||||
|
||||
expect(_text(message), isNot(contains(long)));
|
||||
expect(_text(message), endsWith('…'));
|
||||
expect(_texts(message).last, isNot(contains(long)));
|
||||
expect(_texts(message).last, endsWith('…'));
|
||||
});
|
||||
|
||||
test('reads back as asked, a question of parts alone too', () {
|
||||
const parts = [
|
||||
AssistantPart(kind: AssistantPartKind.back, text: 'to eat'),
|
||||
AssistantPart(kind: AssistantPartKind.hint, text: 'ichidan', number: 3),
|
||||
AssistantPart(kind: AssistantPartKind.description, text: 'N5 verbs'),
|
||||
];
|
||||
|
||||
final asked = askedIn(questionMessage('Why?', parts));
|
||||
expect(asked.question, 'Why?');
|
||||
expect(asked.parts, parts);
|
||||
final alone = askedIn(questionMessage('', parts));
|
||||
expect(alone.question, isEmpty);
|
||||
expect(alone.parts, parts);
|
||||
expect(
|
||||
_texts(questionMessage('', parts)).first,
|
||||
"The card's back:\nto eat",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_lookup.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_prompt.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topic.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/turn_view.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
// A kept turn as its conversation shows it: what was asked, the lookups,
|
||||
// the answer; read from the messages alone, as they are kept.
|
||||
|
||||
void main() {
|
||||
const front = AssistantPart(kind: AssistantPartKind.front, text: '食べて');
|
||||
|
||||
test('reads the question and what went along back', () {
|
||||
final view = TurnView.of([
|
||||
questionMessage('Why て?', const [front]),
|
||||
LlmMessage.assistant(const [LlmText('Because.')]),
|
||||
]);
|
||||
|
||||
expect(
|
||||
view,
|
||||
const TurnView(question: 'Why て?', sent: [front], answer: 'Because.'),
|
||||
);
|
||||
});
|
||||
|
||||
test('shows each lookup with what it found, and every text the model '
|
||||
'wrote', () {
|
||||
final view = TurnView.of([
|
||||
questionMessage('Which verbs are irregular?', const []),
|
||||
LlmMessage.assistant(const [
|
||||
LlmText('Let me look.'),
|
||||
LlmToolCall(id: 't1', name: 'find_cards', input: {'query': 'する'}),
|
||||
]),
|
||||
LlmMessage.user(const [
|
||||
LlmToolResult(callId: 't1', content: '[{"id":"a"},{"id":"b"}]'),
|
||||
]),
|
||||
LlmMessage.assistant(const [LlmText('する and 来る.')]),
|
||||
]);
|
||||
|
||||
expect(view.lookups, const [
|
||||
AssistantLookup(kind: LookupKind.cards, query: 'する', found: 2),
|
||||
]);
|
||||
expect(view.answer, 'Let me look.\n\nする and 来る.');
|
||||
expect(view.stoppedEarly, isFalse);
|
||||
});
|
||||
|
||||
test('says when a turn ended on lookups, without an answer', () {
|
||||
final view = TurnView.of([
|
||||
questionMessage('Everything?', const []),
|
||||
LlmMessage.assistant(const [
|
||||
LlmToolCall(id: 't1', name: 'list_studysets', input: {}),
|
||||
]),
|
||||
LlmMessage.user(const [
|
||||
LlmToolResult(callId: 't1', content: 'Stopped.', isError: true),
|
||||
]),
|
||||
]);
|
||||
|
||||
expect(view.answer, isEmpty);
|
||||
expect(view.stoppedEarly, isTrue);
|
||||
});
|
||||
|
||||
test('a question on its way shows as asked, nothing answered yet', () {
|
||||
expect(
|
||||
TurnView.of([questionMessage('Hm?', const [])]),
|
||||
const TurnView(question: 'Hm?', answer: ''),
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue