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
21 changed files with 474 additions and 50 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1829,6 +1829,8 @@
|
||||||
"assistantHintAsk": "{name} fragen…",
|
"assistantHintAsk": "{name} fragen…",
|
||||||
"assistantSendAsk": "Fragen",
|
"assistantSendAsk": "Fragen",
|
||||||
"assistantScope": "Was er nachschlagen darf",
|
"assistantScope": "Was er nachschlagen darf",
|
||||||
|
"assistantEffortTooltip": "Denkaufwand: wie viel er nachdenkt, bevor er antwortet",
|
||||||
|
"assistantEffortDefault": "{effort} (Standard)",
|
||||||
"assistantScopeTicked": "Nur was ich auswähle",
|
"assistantScopeTicked": "Nur was ich auswähle",
|
||||||
"assistantScopeStudyset": "Dieses Lernset",
|
"assistantScopeStudyset": "Dieses Lernset",
|
||||||
"assistantScopeTree": "Ganzer Baum",
|
"assistantScopeTree": "Ganzer Baum",
|
||||||
|
|
|
||||||
|
|
@ -2149,6 +2149,10 @@
|
||||||
"assistantSendAsk": "Ask",
|
"assistantSendAsk": "Ask",
|
||||||
"assistantScope": "What it may look up",
|
"assistantScope": "What it may look up",
|
||||||
"@assistantScope": {"description": "Tooltip of the scope chip: how far an assistant may look beyond the parts chosen."},
|
"@assistantScope": {"description": "Tooltip of the scope chip: how far an assistant may look beyond the parts chosen."},
|
||||||
|
"assistantEffortTooltip": "Effort: how much it thinks before it answers",
|
||||||
|
"@assistantEffortTooltip": {"description": "Tooltip of the effort chip in the field (ADR 0021, D63): more effort thinks longer and uses more of the budget."},
|
||||||
|
"assistantEffortDefault": "{effort} (default)",
|
||||||
|
"@assistantEffortDefault": {"description": "The effort the operator set up for an assistant, among those to choose, by its name.", "placeholders": {"effort": {"type": "String"}}},
|
||||||
"assistantScopeTicked": "Only what I choose",
|
"assistantScopeTicked": "Only what I choose",
|
||||||
"assistantScopeStudyset": "This studyset",
|
"assistantScopeStudyset": "This studyset",
|
||||||
"assistantScopeTree": "Its tree",
|
"assistantScopeTree": "Its tree",
|
||||||
|
|
|
||||||
|
|
@ -10791,6 +10791,18 @@ abstract class AppLocalizations {
|
||||||
/// **'What it may look up'**
|
/// **'What it may look up'**
|
||||||
String get assistantScope;
|
String get assistantScope;
|
||||||
|
|
||||||
|
/// Tooltip of the effort chip in the field (ADR 0021, D63): more effort thinks longer and uses more of the budget.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Effort: how much it thinks before it answers'**
|
||||||
|
String get assistantEffortTooltip;
|
||||||
|
|
||||||
|
/// The effort the operator set up for an assistant, among those to choose, by its name.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{effort} (default)'**
|
||||||
|
String assistantEffortDefault(String effort);
|
||||||
|
|
||||||
/// No description provided for @assistantScopeTicked.
|
/// No description provided for @assistantScopeTicked.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -6758,6 +6758,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get assistantScope => 'Was er nachschlagen darf';
|
String get assistantScope => 'Was er nachschlagen darf';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get assistantEffortTooltip =>
|
||||||
|
'Denkaufwand: wie viel er nachdenkt, bevor er antwortet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String assistantEffortDefault(String effort) {
|
||||||
|
return '$effort (Standard)';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get assistantScopeTicked => 'Nur was ich auswähle';
|
String get assistantScopeTicked => 'Nur was ich auswähle';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6694,6 +6694,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get assistantScope => 'What it may look up';
|
String get assistantScope => 'What it may look up';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get assistantEffortTooltip =>
|
||||||
|
'Effort: how much it thinks before it answers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String assistantEffortDefault(String effort) {
|
||||||
|
return '$effort (default)';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get assistantScopeTicked => 'Only what I choose';
|
String get assistantScopeTicked => 'Only what I choose';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
|
|
||||||
/// The device's own settings, starting at what it kept: a choice applies at
|
/// The device's own settings, starting at what it kept: a choice applies at
|
||||||
/// once and is kept for the next start.
|
/// once and is kept for the next start.
|
||||||
|
|
@ -39,6 +40,10 @@ class DeviceSettingsBloc(
|
||||||
on<DeviceSettingsEventAskedWith>(
|
on<DeviceSettingsEventAskedWith>(
|
||||||
(event, emit) => _apply(emit, state.withAskWith(event.key)),
|
(event, emit) => _apply(emit, state.withAskWith(event.key)),
|
||||||
);
|
);
|
||||||
|
on<DeviceSettingsEventEffortChosen>(
|
||||||
|
(event, emit) =>
|
||||||
|
_apply(emit, state.withAskEffort(event.key, event.effort)),
|
||||||
|
);
|
||||||
on<DeviceSettingsEventCheckGrammarChosen>(
|
on<DeviceSettingsEventCheckGrammarChosen>(
|
||||||
(event, emit) => _apply(emit, state.withCheckGrammar(event.on)),
|
(event, emit) => _apply(emit, state.withCheckGrammar(event.on)),
|
||||||
);
|
);
|
||||||
|
|
@ -126,6 +131,16 @@ final class const DeviceSettingsEventAskedWith(final String key)
|
||||||
List<Object?> get props => [key];
|
List<Object?> get props => [key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The person chose to ask the assistant [key] (`DeviceSettings.askWith`)
|
||||||
|
/// at [effort].
|
||||||
|
final class const DeviceSettingsEventEffortChosen(
|
||||||
|
final String key,
|
||||||
|
final LlmEffort effort,
|
||||||
|
) extends DeviceSettingsEvent {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [key, effort];
|
||||||
|
}
|
||||||
|
|
||||||
/// The person turned checking writing as it is written [on] or off.
|
/// The person turned checking writing as it is written [on] or off.
|
||||||
final class const DeviceSettingsEventCheckGrammarChosen(final bool on)
|
final class const DeviceSettingsEventCheckGrammarChosen(final bool on)
|
||||||
extends DeviceSettingsEvent {
|
extends DeviceSettingsEvent {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
|
|
||||||
/// Where a device keeps its own settings.
|
/// Where a device keeps its own settings.
|
||||||
abstract interface class DeviceSettingsStore() {
|
abstract interface class DeviceSettingsStore() {
|
||||||
|
|
@ -70,6 +71,11 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
||||||
final String key => key,
|
final String key => key,
|
||||||
_ => null,
|
_ => null,
|
||||||
},
|
},
|
||||||
|
askEfforts: {
|
||||||
|
if (json['askEfforts'] case final Map<String, Object?> efforts)
|
||||||
|
for (final MapEntry(:key, :value) in efforts.entries)
|
||||||
|
if (value case final String name) key: ?LlmEffort.named(name),
|
||||||
|
},
|
||||||
checkGrammar: json['checkGrammar'] != false,
|
checkGrammar: json['checkGrammar'] != false,
|
||||||
);
|
);
|
||||||
} on FormatException catch (error) {
|
} on FormatException catch (error) {
|
||||||
|
|
@ -94,6 +100,10 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
||||||
'customSearchUrl': ?settings.customSearchUrl,
|
'customSearchUrl': ?settings.customSearchUrl,
|
||||||
'pdfSidebarShare': ?settings.pdfSidebarShare,
|
'pdfSidebarShare': ?settings.pdfSidebarShare,
|
||||||
'askWith': ?settings.askWith,
|
'askWith': ?settings.askWith,
|
||||||
|
'askEfforts': {
|
||||||
|
for (final MapEntry(:key, :value) in settings.askEfforts.entries)
|
||||||
|
key: value.key,
|
||||||
|
},
|
||||||
'checkGrammar': settings.checkGrammar,
|
'checkGrammar': settings.checkGrammar,
|
||||||
}),
|
}),
|
||||||
flush: true,
|
flush: true,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
|
|
||||||
/// How the app looks: as the device does, or always light or dark.
|
/// How the app looks: as the device does, or always light or dark.
|
||||||
enum Appearance() {
|
enum Appearance() {
|
||||||
|
|
@ -12,8 +13,8 @@ enum Appearance() {
|
||||||
/// app looks, the language it speaks, the style it cites sources in,
|
/// app looks, the language it speaks, the style it cites sources in,
|
||||||
/// whether studying shows the Pomodoro timer, which studysets' card
|
/// whether studying shows the Pomodoro timer, which studysets' card
|
||||||
/// browsers show one card at a time, whether and how the person searches
|
/// browsers show one card at a time, whether and how the person searches
|
||||||
/// the web from the app, who answered the field last, and whether writing
|
/// the web from the app, who answered the field last and at which effort,
|
||||||
/// is checked as it is written.
|
/// and whether writing is checked as it is written.
|
||||||
/// Unlike the person's settings, they don't follow anyone to other devices,
|
/// Unlike the person's settings, they don't follow anyone to other devices,
|
||||||
/// and they apply before anyone is signed in.
|
/// and they apply before anyone is signed in.
|
||||||
final class const DeviceSettings({
|
final class const DeviceSettings({
|
||||||
|
|
@ -58,6 +59,11 @@ final class const DeviceSettings({
|
||||||
/// null before the first.
|
/// null before the first.
|
||||||
final String? askWith,
|
final String? askWith,
|
||||||
|
|
||||||
|
/// The effort each assistant was last asked at from the field, by its
|
||||||
|
/// key as [askWith] names it (ADR 0021, D63); one the operator no longer
|
||||||
|
/// allows counts as none chosen.
|
||||||
|
final Map<String, LlmEffort> askEfforts = const {},
|
||||||
|
|
||||||
/// Whether the editors underline what the instance's LanguageTool finds
|
/// Whether the editors underline what the instance's LanguageTool finds
|
||||||
/// as the person writes (ADR 0024, Settings › Writing): on unless turned
|
/// as the person writes (ADR 0024, Settings › Writing): on unless turned
|
||||||
/// off; nothing is checked where the instance checks nothing.
|
/// off; nothing is checked where the instance checks nothing.
|
||||||
|
|
@ -103,6 +109,10 @@ final class const DeviceSettings({
|
||||||
/// These settings with [key] having answered last ([askWith]).
|
/// These settings with [key] having answered last ([askWith]).
|
||||||
DeviceSettings withAskWith(String key) => _copy(askWith: key);
|
DeviceSettings withAskWith(String key) => _copy(askWith: key);
|
||||||
|
|
||||||
|
/// These settings with the assistant [key] asked at [effort].
|
||||||
|
DeviceSettings withAskEffort(String key, LlmEffort effort) =>
|
||||||
|
_copy(askEfforts: {...askEfforts, key: effort});
|
||||||
|
|
||||||
/// These settings with writing checked as it is written [on] or off.
|
/// These settings with writing checked as it is written [on] or off.
|
||||||
DeviceSettings withCheckGrammar(bool on) => _copy(checkGrammar: on);
|
DeviceSettings withCheckGrammar(bool on) => _copy(checkGrammar: on);
|
||||||
|
|
||||||
|
|
@ -117,6 +127,7 @@ final class const DeviceSettings({
|
||||||
Object? customSearchUrl = _keep,
|
Object? customSearchUrl = _keep,
|
||||||
double? pdfSidebarShare,
|
double? pdfSidebarShare,
|
||||||
Object? askWith = _keep,
|
Object? askWith = _keep,
|
||||||
|
Map<String, LlmEffort>? askEfforts,
|
||||||
bool? checkGrammar,
|
bool? checkGrammar,
|
||||||
}) => DeviceSettings(
|
}) => DeviceSettings(
|
||||||
appearance: appearance ?? this.appearance,
|
appearance: appearance ?? this.appearance,
|
||||||
|
|
@ -133,6 +144,7 @@ final class const DeviceSettings({
|
||||||
: customSearchUrl as String?,
|
: customSearchUrl as String?,
|
||||||
pdfSidebarShare: pdfSidebarShare ?? this.pdfSidebarShare,
|
pdfSidebarShare: pdfSidebarShare ?? this.pdfSidebarShare,
|
||||||
askWith: identical(askWith, _keep) ? this.askWith : askWith as String?,
|
askWith: identical(askWith, _keep) ? this.askWith : askWith as String?,
|
||||||
|
askEfforts: askEfforts ?? this.askEfforts,
|
||||||
checkGrammar: checkGrammar ?? this.checkGrammar,
|
checkGrammar: checkGrammar ?? this.checkGrammar,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -148,6 +160,7 @@ final class const DeviceSettings({
|
||||||
customSearchUrl,
|
customSearchUrl,
|
||||||
pdfSidebarShare,
|
pdfSidebarShare,
|
||||||
askWith,
|
askWith,
|
||||||
|
askEfforts,
|
||||||
checkGrammar,
|
checkGrammar,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,6 @@ class CardProposalsCubit({
|
||||||
}) extends Cubit<CardProposalsState> {
|
}) extends Cubit<CardProposalsState> {
|
||||||
this : super(const CardProposalsState());
|
this : super(const CardProposalsState());
|
||||||
|
|
||||||
/// How long the answer may be, in tokens: room for every card of a
|
|
||||||
/// proposal, as a conversation's answer has.
|
|
||||||
static const maxOutputTokens = 4096;
|
|
||||||
|
|
||||||
StreamSubscription<LlmEvent>? _proposing;
|
StreamSubscription<LlmEvent>? _proposing;
|
||||||
Completer<void>? _proposed;
|
Completer<void>? _proposed;
|
||||||
|
|
||||||
|
|
@ -94,7 +90,9 @@ class CardProposalsCubit({
|
||||||
messages: [
|
messages: [
|
||||||
LlmMessage.user([LlmText(text)]),
|
LlmMessage.user([LlmText(text)]),
|
||||||
],
|
],
|
||||||
maxOutputTokens: maxOutputTokens,
|
// At the assistant's own effort, with room for its thinking
|
||||||
|
// and every card of a proposal (D63).
|
||||||
|
maxOutputTokens: _choice.outputCeilingAt(null),
|
||||||
system: proposalPrompt(),
|
system: proposalPrompt(),
|
||||||
tools: const [proposeCardsTool],
|
tools: const [proposeCardsTool],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,10 @@ class ConversationCubit({
|
||||||
/// The language the app speaks to the person, as a BCP 47 tag, which a
|
/// The language the app speaks to the person, as a BCP 47 tag, which a
|
||||||
/// check is answered in.
|
/// check is answered in.
|
||||||
final String? _answerIn,
|
final String? _answerIn,
|
||||||
|
|
||||||
|
/// The effort every question asks at, one [_choice] offers (D63); null
|
||||||
|
/// for the assistant's own.
|
||||||
|
final LlmEffort? _effort,
|
||||||
}) extends Cubit<ConversationState> {
|
}) extends Cubit<ConversationState> {
|
||||||
this
|
this
|
||||||
: super(
|
: super(
|
||||||
|
|
@ -130,10 +134,6 @@ class ConversationCubit({
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// How long an answer may be, in tokens: a long explanation, not an
|
|
||||||
/// essay, so a limit isn't used up by one question.
|
|
||||||
static const maxOutputTokens = 4096;
|
|
||||||
|
|
||||||
/// How many rounds of lookups a question gets: the round after is told
|
/// How many rounds of lookups a question gets: the round after is told
|
||||||
/// to answer with what it found, and lookups asked for after that end
|
/// to answer with what it found, and lookups asked for after that end
|
||||||
/// the turn.
|
/// the turn.
|
||||||
|
|
@ -248,9 +248,12 @@ class ConversationCubit({
|
||||||
choice,
|
choice,
|
||||||
LlmRequest(
|
LlmRequest(
|
||||||
messages: [...history, ...turn],
|
messages: [...history, ...turn],
|
||||||
maxOutputTokens: maxOutputTokens,
|
// Room for the thinking its effort allows, which counts
|
||||||
|
// towards it, and the answer after.
|
||||||
|
maxOutputTokens: choice.outputCeilingAt(_effort),
|
||||||
system: system,
|
system: system,
|
||||||
tools: tools,
|
tools: tools,
|
||||||
|
effort: _effort,
|
||||||
),
|
),
|
||||||
turn,
|
turn,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/bloc/conversation_cubit.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/bloc/conversation_cubit.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
|
|
||||||
/// Who answers a question asked in the field: the web search, or an
|
/// Who answers a question asked in the field: the web search, or an
|
||||||
/// assistant (ADR 0021).
|
/// assistant (ADR 0021).
|
||||||
|
|
@ -37,6 +38,14 @@ AssistantChoice? assistantChoiceOf(AssistantsState state, AssistantRef ref) =>
|
||||||
state,
|
state,
|
||||||
).where((choice) => assistantRefOf(choice) == ref).firstOrNull;
|
).where((choice) => assistantRefOf(choice) == ref).firstOrNull;
|
||||||
|
|
||||||
|
/// The effort [choice] is asked at by the person: the one this device last
|
||||||
|
/// chose for it ([chosen], `DeviceSettings.askEfforts`) while the operator
|
||||||
|
/// still allows it; null for the assistant's own (D63).
|
||||||
|
LlmEffort? effortFor(AssistantChoice choice, Map<String, LlmEffort> chosen) {
|
||||||
|
final effort = chosen[assistantRefOf(choice).key];
|
||||||
|
return choice.efforts.contains(effort) ? effort : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Who answers the field's question among [answerers]: the one [picked]
|
/// Who answers the field's question among [answerers]: the one [picked]
|
||||||
/// here, else the one that answered last ([askWith]), else an assistant
|
/// here, else the one that answered last ([askWith]), else an assistant
|
||||||
/// agreed to, any assistant, the web search; null for none at all.
|
/// agreed to, any assistant, the web search; null for none at all.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topi
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/answerers.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/answerers.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/conversation_sheet.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/conversation_sheet.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/private_sends.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/private_sends.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The assistant asked where the app asks for the person, beyond the field
|
/// The assistant asked where the app asks for the person, beyond the field
|
||||||
|
|
@ -53,8 +54,8 @@ Future<bool> readyToAsk(
|
||||||
|
|
||||||
/// Asks [choice] [question], with the parts [chosen], within [scope] for
|
/// Asks [choice] [question], with the parts [chosen], within [scope] for
|
||||||
/// [task], in the conversation's sheet over the screen (D47), a new
|
/// [task], in the conversation's sheet over the screen (D47), a new
|
||||||
/// conversation called [title] where given, once [readyToAsk]. Whether it
|
/// conversation called [title] where given, at [effort] (D63; the
|
||||||
/// was asked.
|
/// assistant's own where null), once [readyToAsk]. Whether it was asked.
|
||||||
Future<bool> askAssistant(
|
Future<bool> askAssistant(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required AssistantChoice choice,
|
required AssistantChoice choice,
|
||||||
|
|
@ -63,6 +64,7 @@ Future<bool> askAssistant(
|
||||||
List<AssistantPart> chosen = const [],
|
List<AssistantPart> chosen = const [],
|
||||||
AssistantTask task = AssistantTask.ask,
|
AssistantTask task = AssistantTask.ask,
|
||||||
String? title,
|
String? title,
|
||||||
|
LlmEffort? effort,
|
||||||
}) async {
|
}) async {
|
||||||
if (!await readyToAsk(context, choice: choice, scope: scope) ||
|
if (!await readyToAsk(context, choice: choice, scope: scope) ||
|
||||||
!context.mounted) {
|
!context.mounted) {
|
||||||
|
|
@ -82,6 +84,7 @@ Future<bool> askAssistant(
|
||||||
title: title,
|
title: title,
|
||||||
question: question,
|
question: question,
|
||||||
chosen: chosen,
|
chosen: chosen,
|
||||||
|
effort: effort,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/widget/grammar_check
|
||||||
import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings.dart';
|
import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
@ -273,6 +274,10 @@ class _AssistantFieldState() extends State<AssistantField> {
|
||||||
question: question,
|
question: question,
|
||||||
chosen: chosen,
|
chosen: chosen,
|
||||||
task: task,
|
task: task,
|
||||||
|
effort: effortFor(
|
||||||
|
choice,
|
||||||
|
context.read<DeviceSettingsBloc>().state.askEfforts,
|
||||||
|
),
|
||||||
) ||
|
) ||
|
||||||
!mounted) {
|
!mounted) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -298,6 +303,10 @@ class _AssistantFieldState() extends State<AssistantField> {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
final asksAssistant = answerer is AssistantAnswerer;
|
final asksAssistant = answerer is AssistantAnswerer;
|
||||||
|
final askEfforts = context
|
||||||
|
.select<DeviceSettingsBloc, Map<String, LlmEffort>>(
|
||||||
|
(bloc) => bloc.state.askEfforts,
|
||||||
|
);
|
||||||
final offered = _offered(answerer);
|
final offered = _offered(answerer);
|
||||||
final chips = offered.isNotEmpty || asksAssistant;
|
final chips = offered.isNotEmpty || asksAssistant;
|
||||||
final shape = chips
|
final shape = chips
|
||||||
|
|
@ -420,25 +429,41 @@ class _AssistantFieldState() extends State<AssistantField> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_Parts(
|
_Parts(
|
||||||
leading: asksAssistant
|
leading: [
|
||||||
? _ScopeChip(
|
if (asksAssistant)
|
||||||
reaches: [
|
_ScopeChip(
|
||||||
AssistantReach.ticked,
|
reaches: [
|
||||||
if (widget.topic.studysetId != null)
|
AssistantReach.ticked,
|
||||||
AssistantReach.studyset,
|
if (widget.topic.studysetId != null)
|
||||||
if (widget.topic.studysetId != null &&
|
AssistantReach.studyset,
|
||||||
_nested == true)
|
if (widget.topic.studysetId != null &&
|
||||||
AssistantReach.tree,
|
_nested == true)
|
||||||
AssistantReach.library,
|
AssistantReach.tree,
|
||||||
],
|
AssistantReach.library,
|
||||||
reach: _reach,
|
],
|
||||||
includePrivate: _includePrivate,
|
reach: _reach,
|
||||||
onReach: (reach) =>
|
includePrivate: _includePrivate,
|
||||||
setState(() => _reach = reach),
|
onReach: (reach) =>
|
||||||
onIncludePrivate: (on) =>
|
setState(() => _reach = reach),
|
||||||
setState(() => _includePrivate = on),
|
onIncludePrivate: (on) =>
|
||||||
)
|
setState(() => _includePrivate = on),
|
||||||
: null,
|
),
|
||||||
|
if (answerer case AssistantAnswerer(:final choice)
|
||||||
|
when choice.efforts.isNotEmpty)
|
||||||
|
if (choice.effort case final standard?)
|
||||||
|
_EffortChip(
|
||||||
|
efforts: choice.efforts,
|
||||||
|
standard: standard,
|
||||||
|
chosen: effortFor(choice, askEfforts),
|
||||||
|
onChosen: (effort) =>
|
||||||
|
context.read<DeviceSettingsBloc>().add(
|
||||||
|
DeviceSettingsEventEffortChosen(
|
||||||
|
answerer.key,
|
||||||
|
effort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
parts: offered,
|
parts: offered,
|
||||||
chosen: _chosen,
|
chosen: _chosen,
|
||||||
onToggle: (part) => setState(
|
onToggle: (part) => setState(
|
||||||
|
|
@ -480,17 +505,31 @@ class const _Picker({
|
||||||
child: sign,
|
child: sign,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
MenuItemButton item(Answerer answerer, {Color? color}) => MenuItemButton(
|
||||||
|
leadingIcon: Icon(_iconOf(answerer), color: color),
|
||||||
|
trailingIcon: answerer.key == current.key
|
||||||
|
? const Icon(LucideIcons.check)
|
||||||
|
: null,
|
||||||
|
onPressed: () => onPicked(answerer),
|
||||||
|
child: _AnswererLabel(answerer: answerer),
|
||||||
|
);
|
||||||
return MenuAnchor(
|
return MenuAnchor(
|
||||||
menuChildren: [
|
menuChildren: [
|
||||||
for (final answerer in answerers)
|
for (final answerer in answerers.whereType<WebAnswerer>())
|
||||||
MenuItemButton(
|
item(answerer),
|
||||||
leadingIcon: Icon(_iconOf(answerer)),
|
// The assistants under their providers' names, in their colours
|
||||||
trailingIcon: answerer.key == current.key
|
// (D60), so where a question goes shows before it goes.
|
||||||
? const Icon(LucideIcons.check)
|
for (final (provider, group) in byProvider(
|
||||||
: null,
|
answerers.whereType<AssistantAnswerer>(),
|
||||||
onPressed: () => onPicked(answerer),
|
(answerer) => answerer.choice.host,
|
||||||
child: _AnswererLabel(answerer: answerer),
|
)) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 4),
|
||||||
|
child: ProviderLabel(provider),
|
||||||
),
|
),
|
||||||
|
for (final answerer in group)
|
||||||
|
item(answerer, color: providerColorIn(context, provider)),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
builder: (context, controller, _) => IconButton(
|
builder: (context, controller, _) => IconButton(
|
||||||
tooltip: l10n.assistantChoose,
|
tooltip: l10n.assistantChoose,
|
||||||
|
|
@ -612,12 +651,65 @@ class const _ScopeChip({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How much the assistant thinks before it answers, as a chip after the
|
||||||
|
/// scope's (D63): its menu holds the [efforts] the operator allows, the
|
||||||
|
/// assistant's own ([standard]) marked so; [chosen] is the one this device
|
||||||
|
/// asks it at, null for its own.
|
||||||
|
class const _EffortChip({
|
||||||
|
required final List<LlmEffort> efforts,
|
||||||
|
required final LlmEffort standard,
|
||||||
|
required final LlmEffort? chosen,
|
||||||
|
required final ValueChanged<LlmEffort> onChosen,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final words = HalkyonAppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final shown = chosen ?? standard;
|
||||||
|
return MenuAnchor(
|
||||||
|
menuChildren: [
|
||||||
|
for (final each in efforts)
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: each == shown
|
||||||
|
? const Icon(LucideIcons.check)
|
||||||
|
: const SizedBox.square(dimension: 24),
|
||||||
|
onPressed: () => onChosen(each),
|
||||||
|
child: Text(
|
||||||
|
each == standard
|
||||||
|
? l10n.assistantEffortDefault(
|
||||||
|
assistantEffortName(words, each),
|
||||||
|
)
|
||||||
|
: assistantEffortName(words, each),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
builder: (context, controller, _) => FilterChip(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
showCheckmark: false,
|
||||||
|
selected: shown != standard,
|
||||||
|
tooltip: l10n.assistantEffortTooltip,
|
||||||
|
avatar: Icon(LucideIcons.brain, color: theme.colorScheme.primary),
|
||||||
|
label: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(assistantEffortName(words, shown)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(LucideIcons.chevronDown, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onSelected: (_) =>
|
||||||
|
controller.isOpen ? controller.close() : controller.open(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The parts that can go along, as chips flowing onto as many lines as
|
/// The parts that can go along, as chips flowing onto as many lines as
|
||||||
/// they need, after [leading] where there is one, each showing as much of
|
/// they need, after [leading], each showing as much of its text as its
|
||||||
/// its text as its line holds; the whole of it on hovering or a long
|
/// line holds; the whole of it on hovering or a long press.
|
||||||
/// press.
|
|
||||||
class const _Parts({
|
class const _Parts({
|
||||||
required final Widget? leading,
|
required final List<Widget> leading,
|
||||||
required final List<AssistantPart> parts,
|
required final List<AssistantPart> parts,
|
||||||
required final Set<AssistantPart> chosen,
|
required final Set<AssistantPart> chosen,
|
||||||
required final ValueChanged<AssistantPart> onToggle,
|
required final ValueChanged<AssistantPart> onToggle,
|
||||||
|
|
@ -628,7 +720,6 @@ class const _Parts({
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final leading = this.leading;
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 10, 8, 2),
|
padding: const EdgeInsets.fromLTRB(8, 10, 8, 2),
|
||||||
// At most about three lines of chips; more scroll.
|
// At most about three lines of chips; more scroll.
|
||||||
|
|
@ -640,7 +731,7 @@ class const _Parts({
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
?leading,
|
...leading,
|
||||||
for (final part in parts)
|
for (final part in parts)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
// An image's text is its asset row's id.
|
// An image's text is its asset row's id.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/answerers.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/answerers.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_words.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_words.dart';
|
||||||
|
|
@ -157,6 +158,7 @@ class const _ConversationTile({
|
||||||
// The conversation's sheet over the screen, not over this list,
|
// The conversation's sheet over the screen, not over this list,
|
||||||
// opened from the navigator, which outlives the list.
|
// opened from the navigator, which outlives the list.
|
||||||
final navigator = Navigator.of(context)..pop();
|
final navigator = Navigator.of(context)..pop();
|
||||||
|
final efforts = context.read<DeviceSettingsBloc>().state.askEfforts;
|
||||||
unawaited(
|
unawaited(
|
||||||
showConversation(
|
showConversation(
|
||||||
navigator.context,
|
navigator.context,
|
||||||
|
|
@ -166,6 +168,8 @@ class const _ConversationTile({
|
||||||
scope: scope,
|
scope: scope,
|
||||||
task: conversation.task,
|
task: conversation.task,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
|
// Its follow-ups at the effort the person asks it at (D63).
|
||||||
|
effort: choice == null ? null : effortFor(choice, efforts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topi
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/turn_view.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/model/turn_view.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_words.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_words.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/private_sends.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/private_sends.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
@ -40,6 +41,10 @@ Future<void> showConversation(
|
||||||
String? conversationId,
|
String? conversationId,
|
||||||
String? question,
|
String? question,
|
||||||
List<AssistantPart> chosen = const [],
|
List<AssistantPart> chosen = const [],
|
||||||
|
|
||||||
|
/// The effort its questions ask at, one the assistant offers (D63); null
|
||||||
|
/// for its own.
|
||||||
|
LlmEffort? effort,
|
||||||
}) {
|
}) {
|
||||||
final person = PersonDependencies.of(context);
|
final person = PersonDependencies.of(context);
|
||||||
final cubit = ConversationCubit(
|
final cubit = ConversationCubit(
|
||||||
|
|
@ -53,6 +58,7 @@ Future<void> showConversation(
|
||||||
task: task,
|
task: task,
|
||||||
title: title,
|
title: title,
|
||||||
answerIn: Localizations.localeOf(context).toLanguageTag(),
|
answerIn: Localizations.localeOf(context).toLanguageTag(),
|
||||||
|
effort: effort,
|
||||||
);
|
);
|
||||||
unawaited(() async {
|
unawaited(() async {
|
||||||
await cubit.open();
|
await cubit.open();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
|
|
||||||
import '../../support/memory_device_settings_store.dart';
|
import '../../support/memory_device_settings_store.dart';
|
||||||
|
|
||||||
|
|
@ -128,6 +129,44 @@ void main() {
|
||||||
expect(asked.withLanguage('de').askWith, 'assistant:claude');
|
expect(asked.withLanguage('de').askWith, 'assistant:claude');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the effort an assistant is asked at, kept per assistant with the '
|
||||||
|
'rest (D63)', () async {
|
||||||
|
final store = MemoryDeviceSettingsStore();
|
||||||
|
final bloc =
|
||||||
|
DeviceSettingsBloc(
|
||||||
|
store,
|
||||||
|
const DeviceSettings(askWith: 'assistant:opus'),
|
||||||
|
)..add(
|
||||||
|
const DeviceSettingsEventEffortChosen(
|
||||||
|
'assistant:opus',
|
||||||
|
LlmEffort.high,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
addTearDown(bloc.close);
|
||||||
|
|
||||||
|
const chosen = DeviceSettings(
|
||||||
|
askWith: 'assistant:opus',
|
||||||
|
askEfforts: {'assistant:opus': LlmEffort.high},
|
||||||
|
);
|
||||||
|
await next(bloc, chosen);
|
||||||
|
bloc.add(
|
||||||
|
const DeviceSettingsEventEffortChosen('assistant:haiku', LlmEffort.low),
|
||||||
|
);
|
||||||
|
await next(
|
||||||
|
bloc,
|
||||||
|
const DeviceSettings(
|
||||||
|
askWith: 'assistant:opus',
|
||||||
|
askEfforts: {
|
||||||
|
'assistant:opus': LlmEffort.high,
|
||||||
|
'assistant:haiku': LlmEffort.low,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(store.writes.first, chosen);
|
||||||
|
expect(store.writes.last.withLanguage('de').askEfforts, hasLength(2));
|
||||||
|
});
|
||||||
|
|
||||||
test('the search engine chosen, and back to the default, keep the rest', () {
|
test('the search engine chosen, and back to the default, keep the rest', () {
|
||||||
const before = DeviceSettings(assistant: true, language: 'de');
|
const before = DeviceSettings(assistant: true, language: 'de');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||||
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late Directory folder;
|
late Directory folder;
|
||||||
|
|
@ -102,6 +103,29 @@ void main() {
|
||||||
expect((await FileDeviceSettingsStore(file).read()).askWith, 'own:o-1');
|
expect((await FileDeviceSettingsStore(file).read()).askWith, 'own:o-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps the effort each assistant was last asked at, and reads none '
|
||||||
|
'it knows not (D63)', () async {
|
||||||
|
expect((await store.read()).askEfforts, isEmpty);
|
||||||
|
|
||||||
|
await store.write(
|
||||||
|
const DeviceSettings(
|
||||||
|
askEfforts: {'assistant:opus': LlmEffort.high},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect((await FileDeviceSettingsStore(file).read()).askEfforts, {
|
||||||
|
'assistant:opus': LlmEffort.high,
|
||||||
|
});
|
||||||
|
|
||||||
|
file.writeAsStringSync(
|
||||||
|
'{"askEfforts": {"assistant:opus": "deep", "assistant:haiku": "low", '
|
||||||
|
'"assistant:sonnet": 3}}',
|
||||||
|
);
|
||||||
|
expect((await FileDeviceSettingsStore(file).read()).askEfforts, {
|
||||||
|
'assistant:haiku': LlmEffort.low,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('reads a file from before the hand-off went (ADR 0022), and writes '
|
test('reads a file from before the hand-off went (ADR 0022), and writes '
|
||||||
'it anew without the assistant chosen', () async {
|
'it anew without the assistant chosen', () async {
|
||||||
file.writeAsStringSync(
|
file.writeAsStringSync(
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ void main() {
|
||||||
expect(assistant, 'claude');
|
expect(assistant, 'claude');
|
||||||
expect(request.tools, [proposeCardsTool]);
|
expect(request.tools, [proposeCardsTool]);
|
||||||
expect(request.system, proposalPrompt());
|
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(
|
expect(
|
||||||
(request.messages.single.parts.single as LlmText).text,
|
(request.messages.single.parts.single as LlmText).text,
|
||||||
'Verbs: 食べる is to eat.',
|
'Verbs: 食べる is to eat.',
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ void main() {
|
||||||
AssistantGateway? asking,
|
AssistantGateway? asking,
|
||||||
AssistantTask task = AssistantTask.ask,
|
AssistantTask task = AssistantTask.ask,
|
||||||
String? title,
|
String? title,
|
||||||
|
LlmEffort? effort,
|
||||||
}) => ConversationCubit(
|
}) => ConversationCubit(
|
||||||
assistants: AssistantRepository(asking ?? gateway),
|
assistants: AssistantRepository(asking ?? gateway),
|
||||||
lookups: lookups,
|
lookups: lookups,
|
||||||
|
|
@ -141,6 +142,7 @@ void main() {
|
||||||
conversationId: conversationId,
|
conversationId: conversationId,
|
||||||
task: task,
|
task: task,
|
||||||
title: title,
|
title: title,
|
||||||
|
effort: effort,
|
||||||
answerIn: 'en',
|
answerIn: 'en',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -182,7 +184,12 @@ void main() {
|
||||||
|
|
||||||
final (assistant, request) = gateway.asked.single;
|
final (assistant, request) = gateway.asked.single;
|
||||||
expect(assistant, 'claude');
|
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.tools, isEmpty, reason: 'only what is ticked goes (D50)');
|
||||||
expect(request.system, contains('nothing to look up'));
|
expect(request.system, contains('nothing to look up'));
|
||||||
|
|
||||||
|
|
@ -195,6 +202,60 @@ void main() {
|
||||||
await conversation.close();
|
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 '
|
test('sends the conversation so far with a follow-up, and keeps it in the '
|
||||||
'same conversation', () async {
|
'same conversation', () async {
|
||||||
final conversation = cubit();
|
final conversation = cubit();
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue