feat(learn): choosing how much an assistant thinks, and the assistants by provider in the field
Halkyon Learn's ADR 0021, D60 and D63: - A chip after the scope's offers the efforts the operator allows for the assistant asked, its own marked; the one picked is kept per assistant on the device (DeviceSettings.askEfforts) and asked at, also by a conversation continued from the history. One the operator no longer allows counts as none. - Every question asks for its effort's ceiling on output, as the thinking counts towards it (8,192 to 65,536 tokens; 16,384 at the model's own), where it asked for 4,096; the flashcards proposed, Explain and Why? ask at the assistant's own effort, a person's own key at none and 4,096. - The field's picker lists the assistants under their providers' names, each in its colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
cc87120bc8
commit
8b5bdfed8c
3 changed files with 474 additions and 50 deletions
|
|
@ -80,6 +80,12 @@ void main() {
|
|||
expect(assistant, 'claude');
|
||||
expect(request.tools, [proposeCardsTool]);
|
||||
expect(request.system, proposalPrompt());
|
||||
expect(
|
||||
request.effort,
|
||||
isNull,
|
||||
reason: "the assistant's own effort: nobody chose one here (D63)",
|
||||
);
|
||||
expect(request.maxOutputTokens, LlmEffort.defaultOutputCeiling);
|
||||
expect(
|
||||
(request.messages.single.parts.single as LlmText).text,
|
||||
'Verbs: 食べる is to eat.',
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ void main() {
|
|||
AssistantGateway? asking,
|
||||
AssistantTask task = AssistantTask.ask,
|
||||
String? title,
|
||||
LlmEffort? effort,
|
||||
}) => ConversationCubit(
|
||||
assistants: AssistantRepository(asking ?? gateway),
|
||||
lookups: lookups,
|
||||
|
|
@ -141,6 +142,7 @@ void main() {
|
|||
conversationId: conversationId,
|
||||
task: task,
|
||||
title: title,
|
||||
effort: effort,
|
||||
answerIn: 'en',
|
||||
);
|
||||
|
||||
|
|
@ -182,7 +184,12 @@ void main() {
|
|||
|
||||
final (assistant, request) = gateway.asked.single;
|
||||
expect(assistant, 'claude');
|
||||
expect(request.maxOutputTokens, ConversationCubit.maxOutputTokens);
|
||||
expect(
|
||||
request.maxOutputTokens,
|
||||
LlmEffort.defaultOutputCeiling,
|
||||
reason: "the model's own effort thinks within the default ceiling",
|
||||
);
|
||||
expect(request.effort, isNull);
|
||||
expect(request.tools, isEmpty, reason: 'only what is ticked goes (D50)');
|
||||
expect(request.system, contains('nothing to look up'));
|
||||
|
||||
|
|
@ -195,6 +202,60 @@ void main() {
|
|||
await conversation.close();
|
||||
});
|
||||
|
||||
test('asks every question at the effort the person chose, with its '
|
||||
'ceiling (D63)', () async {
|
||||
const thinking = OfferedAssistant(
|
||||
id: 'claude',
|
||||
name: 'Claude',
|
||||
protocol: 'anthropic',
|
||||
host: 'api.anthropic.com',
|
||||
model: 'claude-opus',
|
||||
effort: LlmEffort.low,
|
||||
efforts: [LlmEffort.low, LlmEffort.medium, LlmEffort.high],
|
||||
);
|
||||
final conversation = cubit(
|
||||
choice: const GrantedAssistant(thinking),
|
||||
effort: LlmEffort.high,
|
||||
);
|
||||
await conversation.open();
|
||||
gateway.answers
|
||||
..add(_says('Because it joins.'))
|
||||
..add(_says('Yes.'));
|
||||
|
||||
await conversation.ask('Why て-form?', const []);
|
||||
await conversation.ask('Always?', const []);
|
||||
|
||||
for (final (_, request) in gateway.asked) {
|
||||
expect(request.effort, LlmEffort.high);
|
||||
expect(request.maxOutputTokens, LlmEffort.high.outputCeiling);
|
||||
}
|
||||
expect(gateway.asked, hasLength(2));
|
||||
await conversation.close();
|
||||
});
|
||||
|
||||
test("without an effort chosen, it asks at the assistant's, within its "
|
||||
'ceiling', () async {
|
||||
const thinking = OfferedAssistant(
|
||||
id: 'claude',
|
||||
name: 'Claude',
|
||||
protocol: 'anthropic',
|
||||
host: 'api.anthropic.com',
|
||||
model: 'claude-opus',
|
||||
effort: LlmEffort.low,
|
||||
efforts: [LlmEffort.low, LlmEffort.medium],
|
||||
);
|
||||
final conversation = cubit(choice: const GrantedAssistant(thinking));
|
||||
await conversation.open();
|
||||
gateway.answers.add(_says('Because it joins.'));
|
||||
|
||||
await conversation.ask('Why て-form?', const []);
|
||||
|
||||
final (_, request) = gateway.asked.single;
|
||||
expect(request.effort, isNull, reason: 'the instance applies its own');
|
||||
expect(request.maxOutputTokens, LlmEffort.low.outputCeiling);
|
||||
await conversation.close();
|
||||
});
|
||||
|
||||
test('sends the conversation so far with a follow-up, and keeps it in the '
|
||||
'same conversation', () async {
|
||||
final conversation = cubit();
|
||||
|
|
|
|||
|
|
@ -167,11 +167,14 @@ void main() {
|
|||
/// The languages the instance's LanguageTool checks (ADR 0024); none
|
||||
/// without.
|
||||
List<String>? grammarLanguages,
|
||||
|
||||
/// The efforts this device last asked each assistant at (D63).
|
||||
Map<String, LlmEffort> askEfforts = const {},
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final initial = DeviceSettings(assistant: web);
|
||||
final initial = DeviceSettings(assistant: web, askEfforts: askEfforts);
|
||||
store = MemoryDeviceSettingsStore(initial);
|
||||
await pumpApp(
|
||||
tester,
|
||||
|
|
@ -300,6 +303,109 @@ void main() {
|
|||
expect((await store.read()).askWith, 'web');
|
||||
});
|
||||
|
||||
testWidgets('the picker groups the assistants by their provider, each '
|
||||
'under its name in its colour (D60)', (tester) async {
|
||||
assistants.offer = AssistantsOffer(assistants: [_claude(), _seeing]);
|
||||
await showApp(tester, web: true);
|
||||
|
||||
await tester.tap(find.byTooltip('Who answers'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
double top(String text) => tester.getTopLeft(find.text(text).last).dy;
|
||||
expect(find.text('Anthropic'), findsOneWidget);
|
||||
expect(find.text('OpenAI'), findsOneWidget);
|
||||
expect(find.byType(ProviderDot), findsNWidgets(2));
|
||||
expect(top('Web search'), lessThan(top('Anthropic')));
|
||||
expect(top('Anthropic'), lessThan(top('Claude')));
|
||||
expect(top('Claude'), lessThan(top('OpenAI')));
|
||||
expect(top('OpenAI'), lessThan(top('Seeing')));
|
||||
});
|
||||
|
||||
group('effort (D63)', () {
|
||||
const tip = 'Effort: how much it thinks before it answers';
|
||||
final opus = OfferedAssistant(
|
||||
id: 'opus',
|
||||
name: 'Opus',
|
||||
protocol: 'anthropic',
|
||||
host: 'api.anthropic.com',
|
||||
model: 'claude-opus',
|
||||
agreedAt: _agreed,
|
||||
effort: LlmEffort.low,
|
||||
efforts: const [LlmEffort.low, LlmEffort.medium, LlmEffort.high],
|
||||
);
|
||||
|
||||
testWidgets("is chosen in the field among the operator's levels alone, "
|
||||
'remembered for the assistant on this device, and asked at', (
|
||||
tester,
|
||||
) async {
|
||||
assistants.offer = AssistantsOffer(assistants: [opus]);
|
||||
await showApp(tester);
|
||||
expect(find.widgetWithText(FilterChip, 'Low'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip(tip));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Low (default)'), findsOneWidget);
|
||||
expect(find.text('Medium'), findsOneWidget);
|
||||
expect(find.text('High'), findsOneWidget);
|
||||
expect(find.text('Extra high'), findsNothing);
|
||||
expect(find.text('Max'), findsNothing);
|
||||
await tester.tap(find.text('High'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.widgetWithText(FilterChip, 'High'), findsOneWidget);
|
||||
expect((await store.read()).askEfforts, {
|
||||
'assistant:opus': LlmEffort.high,
|
||||
});
|
||||
|
||||
assistants.answers.add(_says('Because it joins clauses.'));
|
||||
await ask(tester, 'Why て-form?');
|
||||
final (_, request) = assistants.asked.single;
|
||||
expect(request.effort, LlmEffort.high);
|
||||
expect(request.maxOutputTokens, LlmEffort.high.outputCeiling);
|
||||
});
|
||||
|
||||
testWidgets("left alone, the question goes at the assistant's own", (
|
||||
tester,
|
||||
) async {
|
||||
assistants.offer = AssistantsOffer(assistants: [opus]);
|
||||
await showApp(tester);
|
||||
|
||||
assistants.answers.add(_says('Because.'));
|
||||
await ask(tester, 'Why?');
|
||||
|
||||
final (_, request) = assistants.asked.single;
|
||||
expect(request.effort, isNull, reason: 'the instance applies its own');
|
||||
expect(request.maxOutputTokens, LlmEffort.low.outputCeiling);
|
||||
});
|
||||
|
||||
testWidgets('one remembered that the operator no longer allows is '
|
||||
"dropped for the assistant's own", (tester) async {
|
||||
assistants.offer = AssistantsOffer(assistants: [opus]);
|
||||
await showApp(tester, askEfforts: {'assistant:opus': LlmEffort.max});
|
||||
expect(find.widgetWithText(FilterChip, 'Low'), findsOneWidget);
|
||||
|
||||
assistants.answers.add(_says('Because.'));
|
||||
await ask(tester, 'Why?');
|
||||
|
||||
expect(assistants.asked.single.$2.effort, isNull);
|
||||
});
|
||||
|
||||
testWidgets('an assistant without levels to choose, and the web search, '
|
||||
'show none', (tester) async {
|
||||
await showApp(tester, web: true);
|
||||
expect(find.text('Ask Claude…'), findsOneWidget);
|
||||
expect(find.byTooltip(tip), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Who answers'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Web search'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip(tip), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('on a studyset, the assistant looks further only once the '
|
||||
"person widens it, and the studyset's name may go along", (
|
||||
tester,
|
||||
|
|
|
|||
Loading…
Reference in a new issue