feat(app,learn,todo): the assistants in Settings and on the operator's screen
ADR 0021, step 2's app half. halkyon_app asks the module's assistant endpoints (ServerpodAssistantGateway, ServerpodAssistantOperatorGateway) and a person's own provider directly (halkyon_llm), behind AssistantRepository: an AssistantChoice is a GrantedAssistant, asked through the instance, which counts it, or a PersonalAssistant with the person's own key, which the instance never sees. assistant_messages.dart turns a conversation into the endpoint's models and back. AssistantsCard, for an app's settings: what is granted and left of its limits, agreed to once on a sheet saying what goes where, the person's use by UTC month, the instance's privacy page, and their own assistants where the operator allows own keys (kept ones stay listed, unasked, where it doesn't). AssistantGrantsCard on InstanceOperatorScreen: grants by handle with their limits in the pricing's unit, changed and taken back after asking, everyone's use by month; a found account's menu grants too. Nothing shows where nothing is offered or can be read. package:halkyon_app/assistant_models.dart holds the models in pure Dart. Learn keeps own assistants in the person's settings (ownAssistants, schema 12, SettingsOwnAssistants); SettingsRepository.update refuses (OwnAssistantRefused) to write a key where the settings would not be sealed (SettingsGateway.seals). Learn depends on halkyon_llm directly. Todo wires the gateways; a member offers no assistant. No feature asks an assistant yet (step 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1e5d7040fd
commit
36899cdc3b
40 changed files with 7505 additions and 46 deletions
|
|
@ -67,6 +67,23 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
refused; who operates, named by handle and taken back (never the last);
|
||||
accounts found by the start of their handle, suspended or named. Only the
|
||||
register's service answers: on a member, nothing shows.
|
||||
- **Assistants** (`lib/src/assistant/`, Halkyon Learn's ADR 0021, step 2):
|
||||
`AssistantRepository` on `ServerpodAssistantGateway` (the module's
|
||||
`assistant` endpoint; refusals as `AssistantFailed`) asks an
|
||||
`AssistantChoice` in halkyon_llm's terms: a `GrantedAssistant` through the
|
||||
instance, which counts it, or a `PersonalAssistant` with the person's own
|
||||
key directly (`LlmClient`), the instance seeing neither key nor question;
|
||||
`assistant_messages.dart` turns a conversation into the endpoint's models
|
||||
and back, part by part. `AssistantsCard` is an app's settings' card: what
|
||||
is granted and left of its limits, agreed to once (the consent sheet),
|
||||
the person's use by UTC month, the instance's privacy page, and the
|
||||
person's own assistants where the operator allows them, kept where the
|
||||
app says (`OwnAssistantStore`: Learn's sealed settings). For the operator,
|
||||
`AssistantOperatorRepository` (grants by exact handle) and
|
||||
`AssistantGrantsCard` on `InstanceOperatorScreen`: grants with their
|
||||
limits, granted, changed and taken back, and everyone's use by month.
|
||||
`package:halkyon_app/assistant_models.dart` holds the models alone, in
|
||||
pure Dart, for an app's own models.
|
||||
- **About** (`showAppAbout`): the app's name (its frame's), its version,
|
||||
icon and legalese where it gives them, where the source of what the
|
||||
instance runs is (`InstanceSourceCode`, from `AppDependencies.sourceCodeUrl`;
|
||||
|
|
|
|||
10
packages/halkyon_app/lib/assistant_models.dart
Normal file
10
packages/halkyon_app/lib/assistant_models.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/// The assistants' models alone (Halkyon Learn's ADR 0021, step 2), in pure
|
||||
/// Dart, for an app's own models to hold: a person's own assistant in its
|
||||
/// settings, without Flutter in the model layer.
|
||||
library;
|
||||
|
||||
export 'src/assistant/model/assistant_amount.dart';
|
||||
export 'src/assistant/model/assistant_failed.dart';
|
||||
export 'src/assistant/model/assistant_grant.dart';
|
||||
export 'src/assistant/model/assistant_offer.dart';
|
||||
export 'src/assistant/model/own_assistant.dart';
|
||||
|
|
@ -18,6 +18,11 @@
|
|||
/// operates the instance, on `OperatingCubit`, leading to
|
||||
/// `InstanceOperatorScreen`, where accounts are approved, refused and
|
||||
/// suspended and operators named (Halkyon Learn's ADR 0021, D21 to D24).
|
||||
/// - **Assistants**: `AssistantsCard` for an app's settings, what the
|
||||
/// operator grants a person and their own keys where allowed, on
|
||||
/// `AssistantRepository`; `AssistantGrantsCard` on the operator's screen,
|
||||
/// on `AssistantOperatorRepository` (Halkyon Learn's ADR 0021, step 2).
|
||||
/// `package:halkyon_app/assistant_models.dart` holds the models alone.
|
||||
/// - **Signing in**: with SimpleLogin (ADR 0020: `SimpleLoginRepository`,
|
||||
/// its sheet and `SimpleLoginSection`), with a password and TOTP (ADR
|
||||
/// 0017: `PasswordRepository`, its sheets and `AccountSecuritySection`),
|
||||
|
|
@ -39,6 +44,29 @@ export 'src/app/app_locale.dart';
|
|||
export 'src/app/app_scope.dart';
|
||||
export 'src/app/instance_gate.dart';
|
||||
export 'src/app/person_scope.dart';
|
||||
export 'src/assistant/bloc/assistant_grants_cubit.dart';
|
||||
export 'src/assistant/bloc/assistant_use_cubit.dart';
|
||||
export 'src/assistant/bloc/assistants_cubit.dart';
|
||||
export 'src/assistant/data/assistant_gateway.dart';
|
||||
export 'src/assistant/data/assistant_messages.dart'
|
||||
show
|
||||
eventsOf,
|
||||
failureOf,
|
||||
llmMessageOf,
|
||||
messageOf,
|
||||
ownKeyFailureOf,
|
||||
questionOf;
|
||||
export 'src/assistant/data/assistant_repository.dart';
|
||||
export 'src/assistant/data/serverpod_assistant_gateway.dart';
|
||||
export 'src/assistant/model/assistant_amount.dart';
|
||||
export 'src/assistant/model/assistant_failed.dart';
|
||||
export 'src/assistant/model/assistant_grant.dart';
|
||||
export 'src/assistant/model/assistant_offer.dart';
|
||||
export 'src/assistant/model/own_assistant.dart';
|
||||
export 'src/assistant/widget/assistant_grants_card.dart';
|
||||
export 'src/assistant/widget/assistant_use_view.dart';
|
||||
export 'src/assistant/widget/assistant_words.dart';
|
||||
export 'src/assistant/widget/assistants_card.dart';
|
||||
export 'src/auth/bloc/session_bloc.dart';
|
||||
export 'src/auth/bloc/sign_in_ways_cubit.dart';
|
||||
export 'src/auth/bloc/wait_cubit.dart';
|
||||
|
|
|
|||
|
|
@ -345,5 +345,87 @@
|
|||
"operatorIsOperatorProblem": "Admins können nicht gesperrt werden. Entzieh zuerst die Rolle.",
|
||||
"operatorLastOperator": "Die Instanz braucht mindestens einen Admin.",
|
||||
"operatorNotAllowed": "Das können nur die Admins der Instanz.",
|
||||
"operatorFailed": "Das hat nicht geklappt. Versuch es noch einmal."
|
||||
"operatorFailed": "Das hat nicht geklappt. Versuch es noch einmal.",
|
||||
"operatorAssistantNotSet": "Dieser Assistent ist hier nicht mehr eingerichtet.",
|
||||
"operatorInvalidLimit": "Eine Grenze kann nicht unter null liegen.",
|
||||
"assistantGrantsTitle": "Assistenten",
|
||||
"assistantGrantsNone": "Noch hat niemand einen Assistenten.",
|
||||
"assistantGrant": "Assistenten freigeben",
|
||||
"assistantGrantSave": "Freigeben",
|
||||
"assistantGrantChange": "Grenzen ändern",
|
||||
"assistantGrantAssistant": "Assistent",
|
||||
"assistantGrantDaily": "Grenze pro Tag",
|
||||
"assistantGrantMonthly": "Grenze pro Monat",
|
||||
"assistantGrantLimitHelper": "Leer für keine. Tage und Monate nach UTC.",
|
||||
"assistantGrantLimitInvalid": "Keine Zahl, die das nimmt",
|
||||
"assistantGrantNoHandle": "Kein Konto hat dieses Handle.",
|
||||
"assistantGrantTokens": "Tokens",
|
||||
"assistantGrantNote": "Vor der ersten Frage stimmen sie zu, was wohin geht. Du siehst, wie viel sie nutzen, nie, was sie fragen.",
|
||||
"assistantPerDay": "{amount} pro Tag",
|
||||
"assistantPerMonth": "{amount} pro Monat",
|
||||
"assistantNoLimits": "Ohne Grenzen",
|
||||
"assistantGrantAgreed": "Zugestimmt",
|
||||
"assistantGrantNotAgreed": "Noch nicht zugestimmt",
|
||||
"assistantGrantOutdated": "Die Preise haben sich geändert: Grenzen neu setzen",
|
||||
"assistantGrantUnset": "Nicht mehr eingerichtet",
|
||||
"assistantRevoke": "Zurücknehmen",
|
||||
"assistantRevokeTitle": "{assistant} zurücknehmen?",
|
||||
"assistantRevokeMessage": "Sie können ihn nicht mehr fragen. Was sie genutzt haben, bleibt gezählt.",
|
||||
"assistantUseByMonth": "Nutzung nach Monat",
|
||||
"assistantUseTitle": "Nutzung der Assistenten",
|
||||
"assistantUseNone": "In diesem Monat nichts genutzt.",
|
||||
"assistantUseKept": "Je UTC-Tag gezählt, aufbewahrt für diesen Monat und die zwei davor. Nie, was gefragt wurde.",
|
||||
"assistantRequests": "{count, plural, =1{1 Anfrage} other{{count} Anfragen}}",
|
||||
"assistantTokens": "{amount} Tokens",
|
||||
"assistantsTitle": "Assistenten",
|
||||
"assistantModelAt": "{model} bei {host}",
|
||||
"assistantLeftToday": "{left} von {limit} heute übrig",
|
||||
"assistantLeftThisMonth": "{left} von {limit} diesen Monat übrig",
|
||||
"assistantAgree": "Lesen und zustimmen",
|
||||
"assistantWaitsForLimits": "Wartet darauf, dass seine Grenzen neu gesetzt werden",
|
||||
"assistantYourUse": "Deine Nutzung",
|
||||
"assistantPrivacyPage": "Was auf {instance} wohin geht",
|
||||
"assistantOwnTitle": "Mit deinem eigenen Schlüssel",
|
||||
"assistantOwnAdd": "Assistenten mit eigenem Schlüssel hinzufügen",
|
||||
"assistantOwnNote": "Die App fragt deinen Anbieter direkt: Diese Instanz sieht weder deinen Schlüssel noch deine Fragen und zählt nichts. Der Schlüssel liegt in deinen Einstellungen, auf deinen Geräten verschlüsselt.",
|
||||
"assistantOwnEdit": "Dein eigener Schlüssel",
|
||||
"assistantOwnName": "Name",
|
||||
"assistantOwnProtocol": "API",
|
||||
"assistantOwnBaseUrl": "Adresse",
|
||||
"assistantOwnBaseUrlHelper": "Leer für die des Anbieters. Mistral, OpenRouter oder ein eigenes Modell: seine Adresse mit /v1.",
|
||||
"assistantOwnBaseUrlInvalid": "Nur https, oder http im eigenen Netz",
|
||||
"assistantOwnModel": "Modell",
|
||||
"assistantOwnKey": "Schlüssel",
|
||||
"assistantOwnKeyHelper": "Leer für ein eigenes Modell, das keinen braucht.",
|
||||
"assistantOwnImages": "Liest Bilder",
|
||||
"assistantOwnSave": "Speichern",
|
||||
"assistantOwnRemove": "Entfernen",
|
||||
"assistantOwnRemoveTitle": "{name} entfernen?",
|
||||
"assistantOwnRemoveMessage": "Sein Schlüssel verschwindet auf allen Geräten aus deinen Einstellungen.",
|
||||
"assistantOwnRefused": "Der Schlüssel wurde nicht gespeichert: Dieses Gerät kann deine Einstellungen noch nicht verschlüsseln. Öffne zuerst hier die Schlüssel deines Kontos.",
|
||||
"assistantOwnNotAllowed": "Wer {instance} betreibt, erlaubt hier keine eigenen Schlüssel, also fragt die App diese nicht.",
|
||||
"assistantConsentTitle": "Bevor du {name} fragst",
|
||||
"assistantConsentModel": "Es antwortet das Modell {model} bei {host}.",
|
||||
"assistantConsentWhatGoes": "Dorthin geht nur, was du sendest: deine Frage und was du davon auswählst, worum es geht. Für den Anbieter gelten seine eigenen Bedingungen.",
|
||||
"assistantConsentImages": "Es liest die Bilder, die du hinzufügst.",
|
||||
"assistantConsentInstance": "Es geht auf dem Weg durch {instance}, das nichts davon aufbewahrt.",
|
||||
"assistantConsentOperator": "Wer {instance} betreibt, sieht, wie viel du es nutzt (Anfragen, Tokens, Kosten), nie, was du fragst.",
|
||||
"assistantConsentConversations": "Deine Unterhaltungen bleiben auf deinen Geräten, verschlüsselt.",
|
||||
"assistantConsentAgree": "Zustimmen",
|
||||
"assistantConsentAgreedOn": "Du hast am {date} zugestimmt.",
|
||||
"assistantProblemNotGranted": "Dieser Assistent ist für dich nicht mehr freigegeben.",
|
||||
"assistantProblemNotAgreed": "Stimm zuerst zu, was wohin geht.",
|
||||
"assistantProblemLimitReached": "Seine Grenze ist bis {time} erreicht.",
|
||||
"assistantProblemLimitReachedNow": "Seine Grenze ist vorerst erreicht.",
|
||||
"assistantProblemLimitOutdated": "Es wartet darauf, dass seine Grenzen neu gesetzt werden.",
|
||||
"assistantProblemImagesNotRead": "Es liest keine Bilder.",
|
||||
"assistantProblemMalformed": "Diese Frage kann nicht gesendet werden.",
|
||||
"assistantProblemTooLarge": "Diese Frage ist zu lang oder enthält zu viele oder zu große Bilder.",
|
||||
"assistantProblemTooManyAtOnce": "Warte zuerst auf die Antworten, die unterwegs sind.",
|
||||
"assistantProblemProviderKey": "Der Anbieter hat den Schlüssel abgelehnt.",
|
||||
"assistantProblemProviderBusy": "Der Anbieter ist ausgelastet. Versuch es gleich noch einmal.",
|
||||
"assistantProblemProviderRefused": "Der Anbieter hat diese Frage abgelehnt.",
|
||||
"assistantProblemProviderFailed": "Beim Anbieter ist etwas schiefgegangen. Versuch es noch einmal.",
|
||||
"assistantProblemNotAllowed": "Das können nur die, für die er freigegeben ist.",
|
||||
"assistantProblemFailed": "Das hat nicht geklappt. Versuch es noch einmal."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -906,5 +906,431 @@
|
|||
"operatorFailed": "That didn't work. Try again.",
|
||||
"@operatorFailed": {
|
||||
"description": "Any other failure."
|
||||
},
|
||||
"operatorAssistantNotSet": "That assistant isn't set up here any more.",
|
||||
"@operatorAssistantNotSet": {
|
||||
"description": "The assistant asked for is no longer in deploy/assistants.env."
|
||||
},
|
||||
"operatorInvalidLimit": "A limit can't be below zero.",
|
||||
"@operatorInvalidLimit": {
|
||||
"description": "A limit below 0 was refused."
|
||||
},
|
||||
"assistantGrantsTitle": "Assistants",
|
||||
"@assistantGrantsTitle": {
|
||||
"description": "The operator's card of the assistants granted (Halkyon Learn's ADR 0021)."
|
||||
},
|
||||
"assistantGrantsNone": "Nobody has an assistant yet.",
|
||||
"@assistantGrantsNone": {
|
||||
"description": "No assistant is granted to anyone."
|
||||
},
|
||||
"assistantGrant": "Grant an assistant",
|
||||
"@assistantGrant": {
|
||||
"description": "Opens the sheet granting an assistant to an account."
|
||||
},
|
||||
"assistantGrantSave": "Grant",
|
||||
"@assistantGrantSave": {
|
||||
"description": "The grant sheet's button."
|
||||
},
|
||||
"assistantGrantChange": "Change limits",
|
||||
"@assistantGrantChange": {
|
||||
"description": "A grant's menu item, and the sheet's title, changing its limits."
|
||||
},
|
||||
"assistantGrantAssistant": "Assistant",
|
||||
"@assistantGrantAssistant": {
|
||||
"description": "Above the choice of assistant in the grant sheet."
|
||||
},
|
||||
"assistantGrantDaily": "Limit a day",
|
||||
"@assistantGrantDaily": {
|
||||
"description": "The daily limit field."
|
||||
},
|
||||
"assistantGrantMonthly": "Limit a month",
|
||||
"@assistantGrantMonthly": {
|
||||
"description": "The monthly limit field."
|
||||
},
|
||||
"assistantGrantLimitHelper": "Empty for none. Days and months are UTC's.",
|
||||
"@assistantGrantLimitHelper": {
|
||||
"description": "Under the limit fields: the instance counts by UTC days and months (D42)."
|
||||
},
|
||||
"assistantGrantLimitInvalid": "Not a number this takes",
|
||||
"@assistantGrantLimitInvalid": {
|
||||
"description": "A limit typed that is no amount or number of tokens."
|
||||
},
|
||||
"assistantGrantNoHandle": "No account has this handle.",
|
||||
"@assistantGrantNoHandle": {
|
||||
"description": "The grant sheet: the handle typed is nobody's."
|
||||
},
|
||||
"assistantGrantTokens": "tokens",
|
||||
"@assistantGrantTokens": {
|
||||
"description": "After a limit in tokens, for an assistant without prices."
|
||||
},
|
||||
"assistantGrantNote": "They agree once to what goes where before they ask it. You see how much they use, never what they ask.",
|
||||
"@assistantGrantNote": {
|
||||
"description": "In the grant sheet: consent (D40) and what the operator sees (D3)."
|
||||
},
|
||||
"assistantPerDay": "{amount} a day",
|
||||
"@assistantPerDay": {
|
||||
"description": "A daily limit.",
|
||||
"placeholders": {
|
||||
"amount": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantPerMonth": "{amount} a month",
|
||||
"@assistantPerMonth": {
|
||||
"description": "A monthly limit.",
|
||||
"placeholders": {
|
||||
"amount": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantNoLimits": "No limits",
|
||||
"@assistantNoLimits": {
|
||||
"description": "A grant without limits."
|
||||
},
|
||||
"assistantGrantAgreed": "Agreed",
|
||||
"@assistantGrantAgreed": {
|
||||
"description": "A grant whose person agreed to what goes where."
|
||||
},
|
||||
"assistantGrantNotAgreed": "Not agreed yet",
|
||||
"@assistantGrantNotAgreed": {
|
||||
"description": "A grant whose person hasn't agreed yet."
|
||||
},
|
||||
"assistantGrantOutdated": "Its prices changed: set its limits again",
|
||||
"@assistantGrantOutdated": {
|
||||
"description": "A grant whose limits are in another unit than the pricing now; it answers nothing until set again."
|
||||
},
|
||||
"assistantGrantUnset": "No longer set up",
|
||||
"@assistantGrantUnset": {
|
||||
"description": "A grant of an assistant taken out of deploy/assistants.env."
|
||||
},
|
||||
"assistantRevoke": "Take back",
|
||||
"@assistantRevoke": {
|
||||
"description": "A grant's menu item."
|
||||
},
|
||||
"assistantRevokeTitle": "Take back {assistant}?",
|
||||
"@assistantRevokeTitle": {
|
||||
"description": "Asks before taking a grant back.",
|
||||
"placeholders": {
|
||||
"assistant": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantRevokeMessage": "They can no longer ask it. What they used stays counted.",
|
||||
"@assistantRevokeMessage": {
|
||||
"description": "Under the question taking a grant back."
|
||||
},
|
||||
"assistantUseByMonth": "Use by month",
|
||||
"@assistantUseByMonth": {
|
||||
"description": "Opens everyone's figures (D3)."
|
||||
},
|
||||
"assistantUseTitle": "Use of assistants",
|
||||
"@assistantUseTitle": {
|
||||
"description": "The title of the figures' screen and sheet."
|
||||
},
|
||||
"assistantUseNone": "Nothing used in this month.",
|
||||
"@assistantUseNone": {
|
||||
"description": "A month without use."
|
||||
},
|
||||
"assistantUseKept": "Counted per UTC day, kept for this month and the two before. Never what was asked.",
|
||||
"@assistantUseKept": {
|
||||
"description": "Under the figures: what is counted and for how long (D41, D42)."
|
||||
},
|
||||
"assistantRequests": "{count, plural, =1{1 request} other{{count} requests}}",
|
||||
"@assistantRequests": {
|
||||
"description": "How many requests were counted.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantTokens": "{amount} tokens",
|
||||
"@assistantTokens": {
|
||||
"description": "How many tokens were counted, the number formatted.",
|
||||
"placeholders": {
|
||||
"amount": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantsTitle": "Assistants",
|
||||
"@assistantsTitle": {
|
||||
"description": "Settings' card of the assistants a person may ask (Halkyon Learn's ADR 0021, point 9)."
|
||||
},
|
||||
"assistantModelAt": "{model} at {host}",
|
||||
"@assistantModelAt": {
|
||||
"description": "Which model answers, and the host that receives what is asked.",
|
||||
"placeholders": {
|
||||
"model": {
|
||||
"type": "String"
|
||||
},
|
||||
"host": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantLeftToday": "{left} of {limit} left today",
|
||||
"@assistantLeftToday": {
|
||||
"description": "What is left of a day's limit.",
|
||||
"placeholders": {
|
||||
"left": {
|
||||
"type": "String"
|
||||
},
|
||||
"limit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantLeftThisMonth": "{left} of {limit} left this month",
|
||||
"@assistantLeftThisMonth": {
|
||||
"description": "What is left of a month's limit.",
|
||||
"placeholders": {
|
||||
"left": {
|
||||
"type": "String"
|
||||
},
|
||||
"limit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantAgree": "Read and agree",
|
||||
"@assistantAgree": {
|
||||
"description": "Opens the consent sheet of an assistant not agreed to yet."
|
||||
},
|
||||
"assistantWaitsForLimits": "Waits for its limits to be set again",
|
||||
"@assistantWaitsForLimits": {
|
||||
"description": "An assistant whose limits are outdated: it answers nothing until the operator sets them again."
|
||||
},
|
||||
"assistantYourUse": "Your use",
|
||||
"@assistantYourUse": {
|
||||
"description": "Opens the person's own figures."
|
||||
},
|
||||
"assistantPrivacyPage": "What goes where on {instance}",
|
||||
"@assistantPrivacyPage": {
|
||||
"description": "Opens the instance's privacy page (D39).",
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantOwnTitle": "With your own key",
|
||||
"@assistantOwnTitle": {
|
||||
"description": "Above a person's own assistants (point 10)."
|
||||
},
|
||||
"assistantOwnAdd": "Add an assistant with your own key",
|
||||
"@assistantOwnAdd": {
|
||||
"description": "Opens the sheet adding an assistant with a personal key."
|
||||
},
|
||||
"assistantOwnNote": "The app asks your provider directly: this instance sees neither your key nor your questions, and counts nothing. The key is kept in your settings, encrypted on your devices.",
|
||||
"@assistantOwnNote": {
|
||||
"description": "What a personal key means (D5, D6)."
|
||||
},
|
||||
"assistantOwnEdit": "Your own key",
|
||||
"@assistantOwnEdit": {
|
||||
"description": "The personal key sheet's title."
|
||||
},
|
||||
"assistantOwnName": "Name",
|
||||
"@assistantOwnName": {
|
||||
"description": "The name the person gives their assistant."
|
||||
},
|
||||
"assistantOwnProtocol": "API",
|
||||
"@assistantOwnProtocol": {
|
||||
"description": "Above the choice of protocol."
|
||||
},
|
||||
"assistantOwnBaseUrl": "Address",
|
||||
"@assistantOwnBaseUrl": {
|
||||
"description": "The provider's address field."
|
||||
},
|
||||
"assistantOwnBaseUrlHelper": "Empty for the provider's own. Mistral, OpenRouter or a model of your own: its address with /v1.",
|
||||
"@assistantOwnBaseUrlHelper": {
|
||||
"description": "Under the address field: which services take which address."
|
||||
},
|
||||
"assistantOwnBaseUrlInvalid": "Only https, or http on your own network",
|
||||
"@assistantOwnBaseUrlInvalid": {
|
||||
"description": "An address the app refuses: keys and questions would travel in the clear."
|
||||
},
|
||||
"assistantOwnModel": "Model",
|
||||
"@assistantOwnModel": {
|
||||
"description": "The model's name at the provider."
|
||||
},
|
||||
"assistantOwnKey": "Key",
|
||||
"@assistantOwnKey": {
|
||||
"description": "The API key field."
|
||||
},
|
||||
"assistantOwnKeyHelper": "Empty for a model of your own that takes none.",
|
||||
"@assistantOwnKeyHelper": {
|
||||
"description": "Under the key field."
|
||||
},
|
||||
"assistantOwnImages": "Reads images",
|
||||
"@assistantOwnImages": {
|
||||
"description": "Whether the model reads images (D12)."
|
||||
},
|
||||
"assistantOwnSave": "Save",
|
||||
"@assistantOwnSave": {
|
||||
"description": "The personal key sheet's button."
|
||||
},
|
||||
"assistantOwnRemove": "Remove",
|
||||
"@assistantOwnRemove": {
|
||||
"description": "Removes an assistant with a personal key."
|
||||
},
|
||||
"assistantOwnRemoveTitle": "Remove {name}?",
|
||||
"@assistantOwnRemoveTitle": {
|
||||
"description": "Asks before removing an assistant with a personal key.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantOwnRemoveMessage": "Its key goes from your settings on every device.",
|
||||
"@assistantOwnRemoveMessage": {
|
||||
"description": "Under the question removing a personal key."
|
||||
},
|
||||
"assistantOwnRefused": "The key wasn't kept: this device can't encrypt your settings yet. Open your account's keys here first.",
|
||||
"@assistantOwnRefused": {
|
||||
"description": "A personal key refused, as the settings wouldn't be sealed."
|
||||
},
|
||||
"assistantOwnNotAllowed": "Whoever runs {instance} doesn't allow own keys here, so the app doesn't ask these.",
|
||||
"@assistantOwnNotAllowed": {
|
||||
"description": "Below the person's own assistants, where the operator doesn't allow them (ADR 0021, D6).",
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String",
|
||||
"example": "learn.halkyon.eu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentTitle": "Before you ask {name}",
|
||||
"@assistantConsentTitle": {
|
||||
"description": "The consent sheet's title (D40).",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentModel": "It answers with the model {model} at {host}.",
|
||||
"@assistantConsentModel": {
|
||||
"description": "Who answers (D27).",
|
||||
"placeholders": {
|
||||
"model": {
|
||||
"type": "String"
|
||||
},
|
||||
"host": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentWhatGoes": "Only what you send goes there: your question and what you pick of what it is about. The provider's own terms apply to it.",
|
||||
"@assistantConsentWhatGoes": {
|
||||
"description": "What goes to the provider (D27)."
|
||||
},
|
||||
"assistantConsentImages": "It reads the images you add.",
|
||||
"@assistantConsentImages": {
|
||||
"description": "For an assistant that reads images (D12)."
|
||||
},
|
||||
"assistantConsentInstance": "It passes through {instance} on its way, which keeps none of it.",
|
||||
"@assistantConsentInstance": {
|
||||
"description": "The instance sees it in passing and keeps nothing (D27).",
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentOperator": "Whoever runs {instance} sees how much you use it (requests, tokens, cost), never what you ask.",
|
||||
"@assistantConsentOperator": {
|
||||
"description": "The operator's figures (D3).",
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentConversations": "Your conversations are kept on your devices, encrypted.",
|
||||
"@assistantConsentConversations": {
|
||||
"description": "Conversations are sealed on the devices (D13)."
|
||||
},
|
||||
"assistantConsentAgree": "Agree",
|
||||
"@assistantConsentAgree": {
|
||||
"description": "The consent sheet's button."
|
||||
},
|
||||
"assistantConsentAgreedOn": "You agreed on {date}.",
|
||||
"@assistantConsentAgreedOn": {
|
||||
"description": "An assistant agreed to, and when.",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantProblemNotGranted": "This assistant isn't granted to you any more.",
|
||||
"@assistantProblemNotGranted": {
|
||||
"description": "notGranted."
|
||||
},
|
||||
"assistantProblemNotAgreed": "Agree to what goes where first.",
|
||||
"@assistantProblemNotAgreed": {
|
||||
"description": "notAgreed."
|
||||
},
|
||||
"assistantProblemLimitReached": "Its limit is reached until {time}.",
|
||||
"@assistantProblemLimitReached": {
|
||||
"description": "limitReached, with when it resets.",
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantProblemLimitReachedNow": "Its limit is reached for now.",
|
||||
"@assistantProblemLimitReachedNow": {
|
||||
"description": "Refused: a limit is reached, the instance not saying until when."
|
||||
},
|
||||
"assistantProblemLimitOutdated": "It waits for its limits to be set again.",
|
||||
"@assistantProblemLimitOutdated": {
|
||||
"description": "limitOutdated."
|
||||
},
|
||||
"assistantProblemImagesNotRead": "It reads no images.",
|
||||
"@assistantProblemImagesNotRead": {
|
||||
"description": "imagesNotRead."
|
||||
},
|
||||
"assistantProblemMalformed": "This question can't be sent.",
|
||||
"@assistantProblemMalformed": {
|
||||
"description": "malformed."
|
||||
},
|
||||
"assistantProblemTooLarge": "This question is too long, or holds too many or too large images.",
|
||||
"@assistantProblemTooLarge": {
|
||||
"description": "tooLarge."
|
||||
},
|
||||
"assistantProblemTooManyAtOnce": "Wait for the answers on their way first.",
|
||||
"@assistantProblemTooManyAtOnce": {
|
||||
"description": "tooManyAtOnce."
|
||||
},
|
||||
"assistantProblemProviderKey": "The provider refused the key.",
|
||||
"@assistantProblemProviderKey": {
|
||||
"description": "providerKey."
|
||||
},
|
||||
"assistantProblemProviderBusy": "The provider is busy. Try again in a moment.",
|
||||
"@assistantProblemProviderBusy": {
|
||||
"description": "providerBusy."
|
||||
},
|
||||
"assistantProblemProviderRefused": "The provider refused this question.",
|
||||
"@assistantProblemProviderRefused": {
|
||||
"description": "providerRefused."
|
||||
},
|
||||
"assistantProblemProviderFailed": "The provider failed. Try again.",
|
||||
"@assistantProblemProviderFailed": {
|
||||
"description": "providerFailed."
|
||||
},
|
||||
"assistantProblemNotAllowed": "Only people it is granted to can do this.",
|
||||
"@assistantProblemNotAllowed": {
|
||||
"description": "notAllowed."
|
||||
},
|
||||
"assistantProblemFailed": "That didn't work. Try again.",
|
||||
"@assistantProblemFailed": {
|
||||
"description": "failed."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2178,6 +2178,498 @@ abstract class HalkyonAppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'That didn\'t work. Try again.'**
|
||||
String get operatorFailed;
|
||||
|
||||
/// The assistant asked for is no longer in deploy/assistants.env.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'That assistant isn\'t set up here any more.'**
|
||||
String get operatorAssistantNotSet;
|
||||
|
||||
/// A limit below 0 was refused.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A limit can\'t be below zero.'**
|
||||
String get operatorInvalidLimit;
|
||||
|
||||
/// The operator's card of the assistants granted (Halkyon Learn's ADR 0021).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Assistants'**
|
||||
String get assistantGrantsTitle;
|
||||
|
||||
/// No assistant is granted to anyone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nobody has an assistant yet.'**
|
||||
String get assistantGrantsNone;
|
||||
|
||||
/// Opens the sheet granting an assistant to an account.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Grant an assistant'**
|
||||
String get assistantGrant;
|
||||
|
||||
/// The grant sheet's button.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Grant'**
|
||||
String get assistantGrantSave;
|
||||
|
||||
/// A grant's menu item, and the sheet's title, changing its limits.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change limits'**
|
||||
String get assistantGrantChange;
|
||||
|
||||
/// Above the choice of assistant in the grant sheet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Assistant'**
|
||||
String get assistantGrantAssistant;
|
||||
|
||||
/// The daily limit field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Limit a day'**
|
||||
String get assistantGrantDaily;
|
||||
|
||||
/// The monthly limit field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Limit a month'**
|
||||
String get assistantGrantMonthly;
|
||||
|
||||
/// Under the limit fields: the instance counts by UTC days and months (D42).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Empty for none. Days and months are UTC\'s.'**
|
||||
String get assistantGrantLimitHelper;
|
||||
|
||||
/// A limit typed that is no amount or number of tokens.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not a number this takes'**
|
||||
String get assistantGrantLimitInvalid;
|
||||
|
||||
/// The grant sheet: the handle typed is nobody's.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No account has this handle.'**
|
||||
String get assistantGrantNoHandle;
|
||||
|
||||
/// After a limit in tokens, for an assistant without prices.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'tokens'**
|
||||
String get assistantGrantTokens;
|
||||
|
||||
/// In the grant sheet: consent (D40) and what the operator sees (D3).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'They agree once to what goes where before they ask it. You see how much they use, never what they ask.'**
|
||||
String get assistantGrantNote;
|
||||
|
||||
/// A daily limit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{amount} a day'**
|
||||
String assistantPerDay(String amount);
|
||||
|
||||
/// A monthly limit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{amount} a month'**
|
||||
String assistantPerMonth(String amount);
|
||||
|
||||
/// A grant without limits.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No limits'**
|
||||
String get assistantNoLimits;
|
||||
|
||||
/// A grant whose person agreed to what goes where.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Agreed'**
|
||||
String get assistantGrantAgreed;
|
||||
|
||||
/// A grant whose person hasn't agreed yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not agreed yet'**
|
||||
String get assistantGrantNotAgreed;
|
||||
|
||||
/// A grant whose limits are in another unit than the pricing now; it answers nothing until set again.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its prices changed: set its limits again'**
|
||||
String get assistantGrantOutdated;
|
||||
|
||||
/// A grant of an assistant taken out of deploy/assistants.env.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No longer set up'**
|
||||
String get assistantGrantUnset;
|
||||
|
||||
/// A grant's menu item.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Take back'**
|
||||
String get assistantRevoke;
|
||||
|
||||
/// Asks before taking a grant back.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Take back {assistant}?'**
|
||||
String assistantRevokeTitle(String assistant);
|
||||
|
||||
/// Under the question taking a grant back.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'They can no longer ask it. What they used stays counted.'**
|
||||
String get assistantRevokeMessage;
|
||||
|
||||
/// Opens everyone's figures (D3).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use by month'**
|
||||
String get assistantUseByMonth;
|
||||
|
||||
/// The title of the figures' screen and sheet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use of assistants'**
|
||||
String get assistantUseTitle;
|
||||
|
||||
/// A month without use.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nothing used in this month.'**
|
||||
String get assistantUseNone;
|
||||
|
||||
/// Under the figures: what is counted and for how long (D41, D42).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Counted per UTC day, kept for this month and the two before. Never what was asked.'**
|
||||
String get assistantUseKept;
|
||||
|
||||
/// How many requests were counted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 request} other{{count} requests}}'**
|
||||
String assistantRequests(int count);
|
||||
|
||||
/// How many tokens were counted, the number formatted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{amount} tokens'**
|
||||
String assistantTokens(String amount);
|
||||
|
||||
/// Settings' card of the assistants a person may ask (Halkyon Learn's ADR 0021, point 9).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Assistants'**
|
||||
String get assistantsTitle;
|
||||
|
||||
/// Which model answers, and the host that receives what is asked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{model} at {host}'**
|
||||
String assistantModelAt(String model, String host);
|
||||
|
||||
/// What is left of a day's limit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{left} of {limit} left today'**
|
||||
String assistantLeftToday(String left, String limit);
|
||||
|
||||
/// What is left of a month's limit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{left} of {limit} left this month'**
|
||||
String assistantLeftThisMonth(String left, String limit);
|
||||
|
||||
/// Opens the consent sheet of an assistant not agreed to yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Read and agree'**
|
||||
String get assistantAgree;
|
||||
|
||||
/// An assistant whose limits are outdated: it answers nothing until the operator sets them again.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Waits for its limits to be set again'**
|
||||
String get assistantWaitsForLimits;
|
||||
|
||||
/// Opens the person's own figures.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your use'**
|
||||
String get assistantYourUse;
|
||||
|
||||
/// Opens the instance's privacy page (D39).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'What goes where on {instance}'**
|
||||
String assistantPrivacyPage(String instance);
|
||||
|
||||
/// Above a person's own assistants (point 10).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'With your own key'**
|
||||
String get assistantOwnTitle;
|
||||
|
||||
/// Opens the sheet adding an assistant with a personal key.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add an assistant with your own key'**
|
||||
String get assistantOwnAdd;
|
||||
|
||||
/// What a personal key means (D5, D6).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The app asks your provider directly: this instance sees neither your key nor your questions, and counts nothing. The key is kept in your settings, encrypted on your devices.'**
|
||||
String get assistantOwnNote;
|
||||
|
||||
/// The personal key sheet's title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your own key'**
|
||||
String get assistantOwnEdit;
|
||||
|
||||
/// The name the person gives their assistant.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Name'**
|
||||
String get assistantOwnName;
|
||||
|
||||
/// Above the choice of protocol.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'API'**
|
||||
String get assistantOwnProtocol;
|
||||
|
||||
/// The provider's address field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Address'**
|
||||
String get assistantOwnBaseUrl;
|
||||
|
||||
/// Under the address field: which services take which address.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Empty for the provider\'s own. Mistral, OpenRouter or a model of your own: its address with /v1.'**
|
||||
String get assistantOwnBaseUrlHelper;
|
||||
|
||||
/// An address the app refuses: keys and questions would travel in the clear.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Only https, or http on your own network'**
|
||||
String get assistantOwnBaseUrlInvalid;
|
||||
|
||||
/// The model's name at the provider.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Model'**
|
||||
String get assistantOwnModel;
|
||||
|
||||
/// The API key field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Key'**
|
||||
String get assistantOwnKey;
|
||||
|
||||
/// Under the key field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Empty for a model of your own that takes none.'**
|
||||
String get assistantOwnKeyHelper;
|
||||
|
||||
/// Whether the model reads images (D12).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reads images'**
|
||||
String get assistantOwnImages;
|
||||
|
||||
/// The personal key sheet's button.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get assistantOwnSave;
|
||||
|
||||
/// Removes an assistant with a personal key.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove'**
|
||||
String get assistantOwnRemove;
|
||||
|
||||
/// Asks before removing an assistant with a personal key.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove {name}?'**
|
||||
String assistantOwnRemoveTitle(String name);
|
||||
|
||||
/// Under the question removing a personal key.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its key goes from your settings on every device.'**
|
||||
String get assistantOwnRemoveMessage;
|
||||
|
||||
/// A personal key refused, as the settings wouldn't be sealed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The key wasn\'t kept: this device can\'t encrypt your settings yet. Open your account\'s keys here first.'**
|
||||
String get assistantOwnRefused;
|
||||
|
||||
/// Below the person's own assistants, where the operator doesn't allow them (ADR 0021, D6).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Whoever runs {instance} doesn\'t allow own keys here, so the app doesn\'t ask these.'**
|
||||
String assistantOwnNotAllowed(String instance);
|
||||
|
||||
/// The consent sheet's title (D40).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Before you ask {name}'**
|
||||
String assistantConsentTitle(String name);
|
||||
|
||||
/// Who answers (D27).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It answers with the model {model} at {host}.'**
|
||||
String assistantConsentModel(String model, String host);
|
||||
|
||||
/// What goes to the provider (D27).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Only what you send goes there: your question and what you pick of what it is about. The provider\'s own terms apply to it.'**
|
||||
String get assistantConsentWhatGoes;
|
||||
|
||||
/// For an assistant that reads images (D12).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It reads the images you add.'**
|
||||
String get assistantConsentImages;
|
||||
|
||||
/// The instance sees it in passing and keeps nothing (D27).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It passes through {instance} on its way, which keeps none of it.'**
|
||||
String assistantConsentInstance(String instance);
|
||||
|
||||
/// The operator's figures (D3).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Whoever runs {instance} sees how much you use it (requests, tokens, cost), never what you ask.'**
|
||||
String assistantConsentOperator(String instance);
|
||||
|
||||
/// Conversations are sealed on the devices (D13).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your conversations are kept on your devices, encrypted.'**
|
||||
String get assistantConsentConversations;
|
||||
|
||||
/// The consent sheet's button.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Agree'**
|
||||
String get assistantConsentAgree;
|
||||
|
||||
/// An assistant agreed to, and when.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You agreed on {date}.'**
|
||||
String assistantConsentAgreedOn(String date);
|
||||
|
||||
/// notGranted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This assistant isn\'t granted to you any more.'**
|
||||
String get assistantProblemNotGranted;
|
||||
|
||||
/// notAgreed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Agree to what goes where first.'**
|
||||
String get assistantProblemNotAgreed;
|
||||
|
||||
/// limitReached, with when it resets.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its limit is reached until {time}.'**
|
||||
String assistantProblemLimitReached(String time);
|
||||
|
||||
/// Refused: a limit is reached, the instance not saying until when.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its limit is reached for now.'**
|
||||
String get assistantProblemLimitReachedNow;
|
||||
|
||||
/// limitOutdated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It waits for its limits to be set again.'**
|
||||
String get assistantProblemLimitOutdated;
|
||||
|
||||
/// imagesNotRead.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It reads no images.'**
|
||||
String get assistantProblemImagesNotRead;
|
||||
|
||||
/// malformed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This question can\'t be sent.'**
|
||||
String get assistantProblemMalformed;
|
||||
|
||||
/// tooLarge.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This question is too long, or holds too many or too large images.'**
|
||||
String get assistantProblemTooLarge;
|
||||
|
||||
/// tooManyAtOnce.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Wait for the answers on their way first.'**
|
||||
String get assistantProblemTooManyAtOnce;
|
||||
|
||||
/// providerKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The provider refused the key.'**
|
||||
String get assistantProblemProviderKey;
|
||||
|
||||
/// providerBusy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The provider is busy. Try again in a moment.'**
|
||||
String get assistantProblemProviderBusy;
|
||||
|
||||
/// providerRefused.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The provider refused this question.'**
|
||||
String get assistantProblemProviderRefused;
|
||||
|
||||
/// providerFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The provider failed. Try again.'**
|
||||
String get assistantProblemProviderFailed;
|
||||
|
||||
/// notAllowed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Only people it is granted to can do this.'**
|
||||
String get assistantProblemNotAllowed;
|
||||
|
||||
/// failed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'That didn\'t work. Try again.'**
|
||||
String get assistantProblemFailed;
|
||||
}
|
||||
|
||||
class _HalkyonAppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -1304,4 +1304,319 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get operatorFailed =>
|
||||
'Das hat nicht geklappt. Versuch es noch einmal.';
|
||||
|
||||
@override
|
||||
String get operatorAssistantNotSet =>
|
||||
'Dieser Assistent ist hier nicht mehr eingerichtet.';
|
||||
|
||||
@override
|
||||
String get operatorInvalidLimit =>
|
||||
'Eine Grenze kann nicht unter null liegen.';
|
||||
|
||||
@override
|
||||
String get assistantGrantsTitle => 'Assistenten';
|
||||
|
||||
@override
|
||||
String get assistantGrantsNone => 'Noch hat niemand einen Assistenten.';
|
||||
|
||||
@override
|
||||
String get assistantGrant => 'Assistenten freigeben';
|
||||
|
||||
@override
|
||||
String get assistantGrantSave => 'Freigeben';
|
||||
|
||||
@override
|
||||
String get assistantGrantChange => 'Grenzen ändern';
|
||||
|
||||
@override
|
||||
String get assistantGrantAssistant => 'Assistent';
|
||||
|
||||
@override
|
||||
String get assistantGrantDaily => 'Grenze pro Tag';
|
||||
|
||||
@override
|
||||
String get assistantGrantMonthly => 'Grenze pro Monat';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitHelper =>
|
||||
'Leer für keine. Tage und Monate nach UTC.';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitInvalid => 'Keine Zahl, die das nimmt';
|
||||
|
||||
@override
|
||||
String get assistantGrantNoHandle => 'Kein Konto hat dieses Handle.';
|
||||
|
||||
@override
|
||||
String get assistantGrantTokens => 'Tokens';
|
||||
|
||||
@override
|
||||
String get assistantGrantNote =>
|
||||
'Vor der ersten Frage stimmen sie zu, was wohin geht. Du siehst, wie viel sie nutzen, nie, was sie fragen.';
|
||||
|
||||
@override
|
||||
String assistantPerDay(String amount) {
|
||||
return '$amount pro Tag';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantPerMonth(String amount) {
|
||||
return '$amount pro Monat';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantNoLimits => 'Ohne Grenzen';
|
||||
|
||||
@override
|
||||
String get assistantGrantAgreed => 'Zugestimmt';
|
||||
|
||||
@override
|
||||
String get assistantGrantNotAgreed => 'Noch nicht zugestimmt';
|
||||
|
||||
@override
|
||||
String get assistantGrantOutdated =>
|
||||
'Die Preise haben sich geändert: Grenzen neu setzen';
|
||||
|
||||
@override
|
||||
String get assistantGrantUnset => 'Nicht mehr eingerichtet';
|
||||
|
||||
@override
|
||||
String get assistantRevoke => 'Zurücknehmen';
|
||||
|
||||
@override
|
||||
String assistantRevokeTitle(String assistant) {
|
||||
return '$assistant zurücknehmen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantRevokeMessage =>
|
||||
'Sie können ihn nicht mehr fragen. Was sie genutzt haben, bleibt gezählt.';
|
||||
|
||||
@override
|
||||
String get assistantUseByMonth => 'Nutzung nach Monat';
|
||||
|
||||
@override
|
||||
String get assistantUseTitle => 'Nutzung der Assistenten';
|
||||
|
||||
@override
|
||||
String get assistantUseNone => 'In diesem Monat nichts genutzt.';
|
||||
|
||||
@override
|
||||
String get assistantUseKept =>
|
||||
'Je UTC-Tag gezählt, aufbewahrt für diesen Monat und die zwei davor. Nie, was gefragt wurde.';
|
||||
|
||||
@override
|
||||
String assistantRequests(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Anfragen',
|
||||
one: '1 Anfrage',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantTokens(String amount) {
|
||||
return '$amount Tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantsTitle => 'Assistenten';
|
||||
|
||||
@override
|
||||
String assistantModelAt(String model, String host) {
|
||||
return '$model bei $host';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantLeftToday(String left, String limit) {
|
||||
return '$left von $limit heute übrig';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantLeftThisMonth(String left, String limit) {
|
||||
return '$left von $limit diesen Monat übrig';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantAgree => 'Lesen und zustimmen';
|
||||
|
||||
@override
|
||||
String get assistantWaitsForLimits =>
|
||||
'Wartet darauf, dass seine Grenzen neu gesetzt werden';
|
||||
|
||||
@override
|
||||
String get assistantYourUse => 'Deine Nutzung';
|
||||
|
||||
@override
|
||||
String assistantPrivacyPage(String instance) {
|
||||
return 'Was auf $instance wohin geht';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantOwnTitle => 'Mit deinem eigenen Schlüssel';
|
||||
|
||||
@override
|
||||
String get assistantOwnAdd => 'Assistenten mit eigenem Schlüssel hinzufügen';
|
||||
|
||||
@override
|
||||
String get assistantOwnNote =>
|
||||
'Die App fragt deinen Anbieter direkt: Diese Instanz sieht weder deinen Schlüssel noch deine Fragen und zählt nichts. Der Schlüssel liegt in deinen Einstellungen, auf deinen Geräten verschlüsselt.';
|
||||
|
||||
@override
|
||||
String get assistantOwnEdit => 'Dein eigener Schlüssel';
|
||||
|
||||
@override
|
||||
String get assistantOwnName => 'Name';
|
||||
|
||||
@override
|
||||
String get assistantOwnProtocol => 'API';
|
||||
|
||||
@override
|
||||
String get assistantOwnBaseUrl => 'Adresse';
|
||||
|
||||
@override
|
||||
String get assistantOwnBaseUrlHelper =>
|
||||
'Leer für die des Anbieters. Mistral, OpenRouter oder ein eigenes Modell: seine Adresse mit /v1.';
|
||||
|
||||
@override
|
||||
String get assistantOwnBaseUrlInvalid =>
|
||||
'Nur https, oder http im eigenen Netz';
|
||||
|
||||
@override
|
||||
String get assistantOwnModel => 'Modell';
|
||||
|
||||
@override
|
||||
String get assistantOwnKey => 'Schlüssel';
|
||||
|
||||
@override
|
||||
String get assistantOwnKeyHelper =>
|
||||
'Leer für ein eigenes Modell, das keinen braucht.';
|
||||
|
||||
@override
|
||||
String get assistantOwnImages => 'Liest Bilder';
|
||||
|
||||
@override
|
||||
String get assistantOwnSave => 'Speichern';
|
||||
|
||||
@override
|
||||
String get assistantOwnRemove => 'Entfernen';
|
||||
|
||||
@override
|
||||
String assistantOwnRemoveTitle(String name) {
|
||||
return '$name entfernen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantOwnRemoveMessage =>
|
||||
'Sein Schlüssel verschwindet auf allen Geräten aus deinen Einstellungen.';
|
||||
|
||||
@override
|
||||
String get assistantOwnRefused =>
|
||||
'Der Schlüssel wurde nicht gespeichert: Dieses Gerät kann deine Einstellungen noch nicht verschlüsseln. Öffne zuerst hier die Schlüssel deines Kontos.';
|
||||
|
||||
@override
|
||||
String assistantOwnNotAllowed(String instance) {
|
||||
return 'Wer $instance betreibt, erlaubt hier keine eigenen Schlüssel, also fragt die App diese nicht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentTitle(String name) {
|
||||
return 'Bevor du $name fragst';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentModel(String model, String host) {
|
||||
return 'Es antwortet das Modell $model bei $host.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantConsentWhatGoes =>
|
||||
'Dorthin geht nur, was du sendest: deine Frage und was du davon auswählst, worum es geht. Für den Anbieter gelten seine eigenen Bedingungen.';
|
||||
|
||||
@override
|
||||
String get assistantConsentImages =>
|
||||
'Es liest die Bilder, die du hinzufügst.';
|
||||
|
||||
@override
|
||||
String assistantConsentInstance(String instance) {
|
||||
return 'Es geht auf dem Weg durch $instance, das nichts davon aufbewahrt.';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentOperator(String instance) {
|
||||
return 'Wer $instance betreibt, sieht, wie viel du es nutzt (Anfragen, Tokens, Kosten), nie, was du fragst.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantConsentConversations =>
|
||||
'Deine Unterhaltungen bleiben auf deinen Geräten, verschlüsselt.';
|
||||
|
||||
@override
|
||||
String get assistantConsentAgree => 'Zustimmen';
|
||||
|
||||
@override
|
||||
String assistantConsentAgreedOn(String date) {
|
||||
return 'Du hast am $date zugestimmt.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantProblemNotGranted =>
|
||||
'Dieser Assistent ist für dich nicht mehr freigegeben.';
|
||||
|
||||
@override
|
||||
String get assistantProblemNotAgreed => 'Stimm zuerst zu, was wohin geht.';
|
||||
|
||||
@override
|
||||
String assistantProblemLimitReached(String time) {
|
||||
return 'Seine Grenze ist bis $time erreicht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantProblemLimitReachedNow =>
|
||||
'Seine Grenze ist vorerst erreicht.';
|
||||
|
||||
@override
|
||||
String get assistantProblemLimitOutdated =>
|
||||
'Es wartet darauf, dass seine Grenzen neu gesetzt werden.';
|
||||
|
||||
@override
|
||||
String get assistantProblemImagesNotRead => 'Es liest keine Bilder.';
|
||||
|
||||
@override
|
||||
String get assistantProblemMalformed =>
|
||||
'Diese Frage kann nicht gesendet werden.';
|
||||
|
||||
@override
|
||||
String get assistantProblemTooLarge =>
|
||||
'Diese Frage ist zu lang oder enthält zu viele oder zu große Bilder.';
|
||||
|
||||
@override
|
||||
String get assistantProblemTooManyAtOnce =>
|
||||
'Warte zuerst auf die Antworten, die unterwegs sind.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderKey =>
|
||||
'Der Anbieter hat den Schlüssel abgelehnt.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderBusy =>
|
||||
'Der Anbieter ist ausgelastet. Versuch es gleich noch einmal.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderRefused =>
|
||||
'Der Anbieter hat diese Frage abgelehnt.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderFailed =>
|
||||
'Beim Anbieter ist etwas schiefgegangen. Versuch es noch einmal.';
|
||||
|
||||
@override
|
||||
String get assistantProblemNotAllowed =>
|
||||
'Das können nur die, für die er freigegeben ist.';
|
||||
|
||||
@override
|
||||
String get assistantProblemFailed =>
|
||||
'Das hat nicht geklappt. Versuch es noch einmal.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1283,6 +1283,314 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get operatorFailed => 'That didn\'t work. Try again.';
|
||||
|
||||
@override
|
||||
String get operatorAssistantNotSet =>
|
||||
'That assistant isn\'t set up here any more.';
|
||||
|
||||
@override
|
||||
String get operatorInvalidLimit => 'A limit can\'t be below zero.';
|
||||
|
||||
@override
|
||||
String get assistantGrantsTitle => 'Assistants';
|
||||
|
||||
@override
|
||||
String get assistantGrantsNone => 'Nobody has an assistant yet.';
|
||||
|
||||
@override
|
||||
String get assistantGrant => 'Grant an assistant';
|
||||
|
||||
@override
|
||||
String get assistantGrantSave => 'Grant';
|
||||
|
||||
@override
|
||||
String get assistantGrantChange => 'Change limits';
|
||||
|
||||
@override
|
||||
String get assistantGrantAssistant => 'Assistant';
|
||||
|
||||
@override
|
||||
String get assistantGrantDaily => 'Limit a day';
|
||||
|
||||
@override
|
||||
String get assistantGrantMonthly => 'Limit a month';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitHelper =>
|
||||
'Empty for none. Days and months are UTC\'s.';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitInvalid => 'Not a number this takes';
|
||||
|
||||
@override
|
||||
String get assistantGrantNoHandle => 'No account has this handle.';
|
||||
|
||||
@override
|
||||
String get assistantGrantTokens => 'tokens';
|
||||
|
||||
@override
|
||||
String get assistantGrantNote =>
|
||||
'They agree once to what goes where before they ask it. You see how much they use, never what they ask.';
|
||||
|
||||
@override
|
||||
String assistantPerDay(String amount) {
|
||||
return '$amount a day';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantPerMonth(String amount) {
|
||||
return '$amount a month';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantNoLimits => 'No limits';
|
||||
|
||||
@override
|
||||
String get assistantGrantAgreed => 'Agreed';
|
||||
|
||||
@override
|
||||
String get assistantGrantNotAgreed => 'Not agreed yet';
|
||||
|
||||
@override
|
||||
String get assistantGrantOutdated =>
|
||||
'Its prices changed: set its limits again';
|
||||
|
||||
@override
|
||||
String get assistantGrantUnset => 'No longer set up';
|
||||
|
||||
@override
|
||||
String get assistantRevoke => 'Take back';
|
||||
|
||||
@override
|
||||
String assistantRevokeTitle(String assistant) {
|
||||
return 'Take back $assistant?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantRevokeMessage =>
|
||||
'They can no longer ask it. What they used stays counted.';
|
||||
|
||||
@override
|
||||
String get assistantUseByMonth => 'Use by month';
|
||||
|
||||
@override
|
||||
String get assistantUseTitle => 'Use of assistants';
|
||||
|
||||
@override
|
||||
String get assistantUseNone => 'Nothing used in this month.';
|
||||
|
||||
@override
|
||||
String get assistantUseKept =>
|
||||
'Counted per UTC day, kept for this month and the two before. Never what was asked.';
|
||||
|
||||
@override
|
||||
String assistantRequests(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count requests',
|
||||
one: '1 request',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantTokens(String amount) {
|
||||
return '$amount tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantsTitle => 'Assistants';
|
||||
|
||||
@override
|
||||
String assistantModelAt(String model, String host) {
|
||||
return '$model at $host';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantLeftToday(String left, String limit) {
|
||||
return '$left of $limit left today';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantLeftThisMonth(String left, String limit) {
|
||||
return '$left of $limit left this month';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantAgree => 'Read and agree';
|
||||
|
||||
@override
|
||||
String get assistantWaitsForLimits => 'Waits for its limits to be set again';
|
||||
|
||||
@override
|
||||
String get assistantYourUse => 'Your use';
|
||||
|
||||
@override
|
||||
String assistantPrivacyPage(String instance) {
|
||||
return 'What goes where on $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantOwnTitle => 'With your own key';
|
||||
|
||||
@override
|
||||
String get assistantOwnAdd => 'Add an assistant with your own key';
|
||||
|
||||
@override
|
||||
String get assistantOwnNote =>
|
||||
'The app asks your provider directly: this instance sees neither your key nor your questions, and counts nothing. The key is kept in your settings, encrypted on your devices.';
|
||||
|
||||
@override
|
||||
String get assistantOwnEdit => 'Your own key';
|
||||
|
||||
@override
|
||||
String get assistantOwnName => 'Name';
|
||||
|
||||
@override
|
||||
String get assistantOwnProtocol => 'API';
|
||||
|
||||
@override
|
||||
String get assistantOwnBaseUrl => 'Address';
|
||||
|
||||
@override
|
||||
String get assistantOwnBaseUrlHelper =>
|
||||
'Empty for the provider\'s own. Mistral, OpenRouter or a model of your own: its address with /v1.';
|
||||
|
||||
@override
|
||||
String get assistantOwnBaseUrlInvalid =>
|
||||
'Only https, or http on your own network';
|
||||
|
||||
@override
|
||||
String get assistantOwnModel => 'Model';
|
||||
|
||||
@override
|
||||
String get assistantOwnKey => 'Key';
|
||||
|
||||
@override
|
||||
String get assistantOwnKeyHelper =>
|
||||
'Empty for a model of your own that takes none.';
|
||||
|
||||
@override
|
||||
String get assistantOwnImages => 'Reads images';
|
||||
|
||||
@override
|
||||
String get assistantOwnSave => 'Save';
|
||||
|
||||
@override
|
||||
String get assistantOwnRemove => 'Remove';
|
||||
|
||||
@override
|
||||
String assistantOwnRemoveTitle(String name) {
|
||||
return 'Remove $name?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantOwnRemoveMessage =>
|
||||
'Its key goes from your settings on every device.';
|
||||
|
||||
@override
|
||||
String get assistantOwnRefused =>
|
||||
'The key wasn\'t kept: this device can\'t encrypt your settings yet. Open your account\'s keys here first.';
|
||||
|
||||
@override
|
||||
String assistantOwnNotAllowed(String instance) {
|
||||
return 'Whoever runs $instance doesn\'t allow own keys here, so the app doesn\'t ask these.';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentTitle(String name) {
|
||||
return 'Before you ask $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentModel(String model, String host) {
|
||||
return 'It answers with the model $model at $host.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantConsentWhatGoes =>
|
||||
'Only what you send goes there: your question and what you pick of what it is about. The provider\'s own terms apply to it.';
|
||||
|
||||
@override
|
||||
String get assistantConsentImages => 'It reads the images you add.';
|
||||
|
||||
@override
|
||||
String assistantConsentInstance(String instance) {
|
||||
return 'It passes through $instance on its way, which keeps none of it.';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentOperator(String instance) {
|
||||
return 'Whoever runs $instance sees how much you use it (requests, tokens, cost), never what you ask.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantConsentConversations =>
|
||||
'Your conversations are kept on your devices, encrypted.';
|
||||
|
||||
@override
|
||||
String get assistantConsentAgree => 'Agree';
|
||||
|
||||
@override
|
||||
String assistantConsentAgreedOn(String date) {
|
||||
return 'You agreed on $date.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantProblemNotGranted =>
|
||||
'This assistant isn\'t granted to you any more.';
|
||||
|
||||
@override
|
||||
String get assistantProblemNotAgreed => 'Agree to what goes where first.';
|
||||
|
||||
@override
|
||||
String assistantProblemLimitReached(String time) {
|
||||
return 'Its limit is reached until $time.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantProblemLimitReachedNow => 'Its limit is reached for now.';
|
||||
|
||||
@override
|
||||
String get assistantProblemLimitOutdated =>
|
||||
'It waits for its limits to be set again.';
|
||||
|
||||
@override
|
||||
String get assistantProblemImagesNotRead => 'It reads no images.';
|
||||
|
||||
@override
|
||||
String get assistantProblemMalformed => 'This question can\'t be sent.';
|
||||
|
||||
@override
|
||||
String get assistantProblemTooLarge =>
|
||||
'This question is too long, or holds too many or too large images.';
|
||||
|
||||
@override
|
||||
String get assistantProblemTooManyAtOnce =>
|
||||
'Wait for the answers on their way first.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderKey => 'The provider refused the key.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderBusy =>
|
||||
'The provider is busy. Try again in a moment.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderRefused =>
|
||||
'The provider refused this question.';
|
||||
|
||||
@override
|
||||
String get assistantProblemProviderFailed =>
|
||||
'The provider failed. Try again.';
|
||||
|
||||
@override
|
||||
String get assistantProblemNotAllowed =>
|
||||
'Only people it is granted to can do this.';
|
||||
|
||||
@override
|
||||
String get assistantProblemFailed => 'That didn\'t work. Try again.';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_repository.dart';
|
||||
import 'package:halkyon_app/src/auth/data/auth_repository.dart';
|
||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/files/file_saver.dart';
|
||||
|
|
@ -39,6 +40,16 @@ abstract interface class AppDependencies<P extends Disposable>()
|
|||
/// suspended, and who operates; refused to anyone else.
|
||||
InstanceOperatorRepository get instanceOperators;
|
||||
|
||||
/// The assistants the operator granted the signed-in person, agreeing to
|
||||
/// them, their use, and asking them or the person's own (Halkyon Learn's
|
||||
/// ADR 0021, step 2).
|
||||
AssistantRepository get assistants;
|
||||
|
||||
/// Granting assistants, with their limits and everyone's use, as the
|
||||
/// instance's operators do (ADR 0021, points 2, 6 and 8); refused to
|
||||
/// anyone else.
|
||||
AssistantOperatorRepository get assistantOperators;
|
||||
|
||||
/// Keeps a file the app made where the person chooses (a recovery phrase,
|
||||
/// recovery codes, an app's exports).
|
||||
FileSaver get fileSaver;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_repository.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/operator/model/operated_account.dart';
|
||||
|
||||
/// What the operator's screen shows of the assistants.
|
||||
final class const AssistantGrantsState({
|
||||
/// Reading the assistants set and the grants.
|
||||
final bool loading = true,
|
||||
|
||||
/// Why they couldn't be read; null while they could.
|
||||
final OperatorFailed? unreadable,
|
||||
|
||||
/// The assistants set in `deploy/assistants.env`, without their keys.
|
||||
final AssistantsSetUp setUp = const AssistantsSetUp(),
|
||||
|
||||
/// Every grant, by handle.
|
||||
final List<AssistantGrant> grants = const [],
|
||||
|
||||
/// What is under way: a grant's key ([keyOf]), or `grant` for a new one;
|
||||
/// null for nothing.
|
||||
final String? busy,
|
||||
|
||||
/// Why the last action failed, nothing changed; null for none.
|
||||
final OperatorFailed? problem,
|
||||
}) with Equatable {
|
||||
/// Whether there is anything to show: an assistant set, or a grant left
|
||||
/// of one no longer set.
|
||||
bool get showsAnything => setUp.assistants.isNotEmpty || grants.isNotEmpty;
|
||||
|
||||
/// What names [grant] while it is acted on.
|
||||
static String keyOf(AssistantGrant grant) =>
|
||||
'${grant.account}/${grant.assistant}';
|
||||
|
||||
AssistantGrantsState copyWith({
|
||||
bool? loading,
|
||||
OperatorFailed? unreadable,
|
||||
bool readable = false,
|
||||
AssistantsSetUp? setUp,
|
||||
List<AssistantGrant>? grants,
|
||||
String? busy,
|
||||
bool clearBusy = false,
|
||||
OperatorFailed? problem,
|
||||
bool clearProblem = false,
|
||||
}) => AssistantGrantsState(
|
||||
loading: loading ?? this.loading,
|
||||
unreadable: readable ? null : unreadable ?? this.unreadable,
|
||||
setUp: setUp ?? this.setUp,
|
||||
grants: grants ?? this.grants,
|
||||
busy: clearBusy ? null : busy ?? this.busy,
|
||||
problem: clearProblem ? null : problem ?? this.problem,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
loading,
|
||||
unreadable,
|
||||
setUp,
|
||||
grants,
|
||||
busy,
|
||||
problem,
|
||||
];
|
||||
}
|
||||
|
||||
/// The assistants the operator grants (Halkyon Learn's ADR 0021, points 2,
|
||||
/// 6 and 8): granted by handle with their limits, the limits changed, taken
|
||||
/// back. One action at a time, the grants read anew after each.
|
||||
class AssistantGrantsCubit(final AssistantOperatorRepository _operators)
|
||||
extends Cubit<AssistantGrantsState> {
|
||||
this : super(const AssistantGrantsState());
|
||||
|
||||
/// Reads the assistants set and the grants.
|
||||
Future<void> load() async {
|
||||
try {
|
||||
final setUp = await _operators.setUp();
|
||||
final grants = await _operators.grants();
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loading: false,
|
||||
readable: true,
|
||||
setUp: setUp,
|
||||
grants: grants,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('Reading the grants failed: ${error.runtimeType}', name: 'AI');
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(loading: false, unreadable: _failure(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Grants [assistant] to the account with [handle], with [dailyLimit] and
|
||||
/// [monthlyLimit] in its pricing's unit; why it failed, or null once
|
||||
/// granted. The sheet asking says it, so it stays out of `problem`.
|
||||
Future<OperatorFailed?> grant({
|
||||
required String handle,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) => _act(
|
||||
'grant',
|
||||
() => _operators.grant(
|
||||
handle: handle,
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
);
|
||||
|
||||
/// Sets [grant]'s limits; why it failed, or null once set.
|
||||
Future<OperatorFailed?> changeLimits(
|
||||
AssistantGrant grant, {
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) => _act(
|
||||
AssistantGrantsState.keyOf(grant),
|
||||
() => _operators.changeLimits(
|
||||
account: grant.account,
|
||||
assistant: grant.assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
);
|
||||
|
||||
/// Takes [grant] back; a failure is the state's `problem`.
|
||||
Future<void> revoke(AssistantGrant grant) async {
|
||||
final failed = await _act(
|
||||
AssistantGrantsState.keyOf(grant),
|
||||
() => _operators.revoke(
|
||||
account: grant.account,
|
||||
assistant: grant.assistant,
|
||||
),
|
||||
);
|
||||
if (failed != null && !isClosed) {
|
||||
emit(state.copyWith(problem: failed));
|
||||
}
|
||||
}
|
||||
|
||||
Future<OperatorFailed?> _act(
|
||||
String what,
|
||||
Future<void> Function() action,
|
||||
) async {
|
||||
if (state.busy != null) {
|
||||
return const OperatorFailed(OperatorProblem.failed);
|
||||
}
|
||||
emit(state.copyWith(busy: what, clearProblem: true));
|
||||
OperatorFailed? failed;
|
||||
try {
|
||||
await action();
|
||||
} on Object catch (error) {
|
||||
log('An assistant grant failed: ${error.runtimeType}', name: 'AI');
|
||||
failed = _failure(error);
|
||||
}
|
||||
await load();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearBusy: true));
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
static OperatorFailed _failure(Object error) => error is OperatorFailed
|
||||
? error
|
||||
: const OperatorFailed(OperatorProblem.failed);
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
|
||||
/// What a list of use shows: one UTC month's.
|
||||
final class const AssistantUseState({
|
||||
/// The month shown, its first day in UTC.
|
||||
required final DateTime month,
|
||||
|
||||
/// The months the instance keeps, the latest first: this one and the
|
||||
/// two before (Halkyon Learn's ADR 0021, D41).
|
||||
required final List<DateTime> months,
|
||||
|
||||
/// Reading it.
|
||||
final bool loading = true,
|
||||
|
||||
/// Whether it couldn't be read.
|
||||
final bool failed = false,
|
||||
|
||||
/// What was used, by person and assistant.
|
||||
final List<AssistantUse> use = const [],
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [month, months, loading, failed, use];
|
||||
}
|
||||
|
||||
/// One UTC month's use of assistants, as [_read] gives it: the person's
|
||||
/// own, or everyone's for the operator (Halkyon Learn's ADR 0021, D3).
|
||||
class AssistantUseCubit(
|
||||
final Future<List<AssistantUse>> Function(DateTime month) _read,
|
||||
) extends Cubit<AssistantUseState> {
|
||||
this : super(_first(clock.now()));
|
||||
|
||||
static AssistantUseState _first(DateTime now) {
|
||||
final month = utcMonthOf(now);
|
||||
return AssistantUseState(
|
||||
month: month,
|
||||
months: [
|
||||
for (var back = 0; back < 3; back++)
|
||||
DateTime.utc(month.year, month.month - back),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads [month]'s use, or the month shown's.
|
||||
Future<void> show([DateTime? month]) async {
|
||||
final shown = month ?? state.month;
|
||||
emit(
|
||||
AssistantUseState(
|
||||
month: shown,
|
||||
months: state.months,
|
||||
use: shown == state.month ? state.use : const [],
|
||||
),
|
||||
);
|
||||
try {
|
||||
final use = await _read(shown);
|
||||
if (!isClosed && state.month == shown) {
|
||||
emit(
|
||||
AssistantUseState(
|
||||
month: shown,
|
||||
months: state.months,
|
||||
loading: false,
|
||||
use: use,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('Reading the use failed: ${error.runtimeType}', name: 'AI');
|
||||
if (!isClosed && state.month == shown) {
|
||||
emit(
|
||||
AssistantUseState(
|
||||
month: shown,
|
||||
months: state.months,
|
||||
loading: false,
|
||||
failed: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_repository.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_failed.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/own_assistant.dart';
|
||||
|
||||
/// What an app's settings show of the person's assistants.
|
||||
final class const AssistantsState({
|
||||
/// Reading what the instance offers.
|
||||
final bool loading = true,
|
||||
|
||||
/// Why it couldn't be read; null while it could.
|
||||
final AssistantFailed? unreadable,
|
||||
|
||||
/// What the instance offers; nothing while it couldn't be read.
|
||||
final AssistantsOffer offer = AssistantsOffer.none,
|
||||
|
||||
/// The person's own assistants, in the order added.
|
||||
final List<OwnAssistant> own = const [],
|
||||
|
||||
/// The ID of the assistant being agreed to; null for none.
|
||||
final String? agreeing,
|
||||
|
||||
/// Why the last agreement failed; null for none.
|
||||
final AssistantFailed? problem,
|
||||
}) with Equatable {
|
||||
/// Whether there is anything to show: an assistant granted, own keys
|
||||
/// allowed, or own assistants kept.
|
||||
bool get showsAnything =>
|
||||
offer.assistants.isNotEmpty || offer.ownKeys || own.isNotEmpty;
|
||||
|
||||
AssistantsState copyWith({
|
||||
bool? loading,
|
||||
AssistantFailed? unreadable,
|
||||
bool readable = false,
|
||||
AssistantsOffer? offer,
|
||||
List<OwnAssistant>? own,
|
||||
String? agreeing,
|
||||
bool clearAgreeing = false,
|
||||
AssistantFailed? problem,
|
||||
bool clearProblem = false,
|
||||
}) => AssistantsState(
|
||||
loading: loading ?? this.loading,
|
||||
unreadable: readable ? null : unreadable ?? this.unreadable,
|
||||
offer: offer ?? this.offer,
|
||||
own: own ?? this.own,
|
||||
agreeing: clearAgreeing ? null : agreeing ?? this.agreeing,
|
||||
problem: clearProblem ? null : problem ?? this.problem,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
loading,
|
||||
unreadable,
|
||||
offer,
|
||||
own,
|
||||
agreeing,
|
||||
problem,
|
||||
];
|
||||
}
|
||||
|
||||
/// How saving an own assistant ended.
|
||||
enum OwnAssistantSaved() {
|
||||
/// It is kept.
|
||||
kept,
|
||||
|
||||
/// This device can't seal the settings, so its key wasn't kept.
|
||||
refused,
|
||||
|
||||
/// Keeping it failed.
|
||||
failed,
|
||||
}
|
||||
|
||||
/// The person's assistants (Halkyon Learn's ADR 0021, points 6, 9 and 10):
|
||||
/// those the operator granted, read anew after every agreement, and those
|
||||
/// with their own key from [ownAssistants], where the app keeps them.
|
||||
class AssistantsCubit(
|
||||
final AssistantRepository _assistants, {
|
||||
|
||||
/// Where the app keeps the person's own assistants; none where it keeps
|
||||
/// none.
|
||||
final OwnAssistantStore? ownAssistants,
|
||||
}) extends Cubit<AssistantsState> {
|
||||
this : super(const AssistantsState()) {
|
||||
_changes = _assistants.changes.listen((_) => unawaited(load()));
|
||||
_own = ownAssistants?.watch().listen((own) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(own: own));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
late final StreamSubscription<void> _changes;
|
||||
late final StreamSubscription<List<OwnAssistant>>? _own;
|
||||
|
||||
/// Reads what the instance offers.
|
||||
Future<void> load() async {
|
||||
try {
|
||||
final offer = await _assistants.offered();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(loading: false, readable: true, offer: offer));
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('Reading the assistants failed: ${error.runtimeType}', name: 'AI');
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loading: false,
|
||||
offer: AssistantsOffer.none,
|
||||
unreadable: error is AssistantFailed
|
||||
? error
|
||||
: const AssistantFailed(AssistantProblem.failed),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Agrees to what goes where with [assistant]; the offer is read anew.
|
||||
Future<void> agree(String assistant) async {
|
||||
if (state.agreeing != null) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(agreeing: assistant, clearProblem: true));
|
||||
AssistantFailed? failed;
|
||||
try {
|
||||
await _assistants.agree(assistant);
|
||||
} on Object catch (error) {
|
||||
log('Agreeing failed: ${error.runtimeType}', name: 'AI');
|
||||
failed = error is AssistantFailed
|
||||
? error
|
||||
: const AssistantFailed(AssistantProblem.failed);
|
||||
}
|
||||
await load();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearAgreeing: true, problem: failed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps [assistant] among the person's own, in place of the one with
|
||||
/// its ID.
|
||||
Future<OwnAssistantSaved> saveOwn(OwnAssistant assistant) async {
|
||||
final store = ownAssistants;
|
||||
if (store == null) {
|
||||
return OwnAssistantSaved.failed;
|
||||
}
|
||||
try {
|
||||
await store.save(assistant);
|
||||
return OwnAssistantSaved.kept;
|
||||
} on OwnAssistantRefused {
|
||||
return OwnAssistantSaved.refused;
|
||||
} on Object catch (error) {
|
||||
log('Keeping an own assistant failed: ${error.runtimeType}', name: 'AI');
|
||||
return OwnAssistantSaved.failed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the person's own assistant [id] names.
|
||||
Future<void> removeOwn(String id) async {
|
||||
try {
|
||||
await ownAssistants?.remove(id);
|
||||
} on Object catch (error) {
|
||||
log('Removing an own assistant failed: ${error.runtimeType}', name: 'AI');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _changes.cancel();
|
||||
await _own?.cancel();
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// The boundary to the assistants the instance's operator grants (Halkyon
|
||||
/// Learn's ADR 0021, points 6 to 8), on the signed-in person's session:
|
||||
/// failures are `AssistantFailed`s. Tests replace it with a fake.
|
||||
abstract interface class AssistantGateway() {
|
||||
/// The assistants granted to the person, with what is left of their
|
||||
/// limits; none on a member of another's register (D43).
|
||||
Future<AssistantsOffer> offered();
|
||||
|
||||
/// Records that the person agrees to what goes where with [assistant],
|
||||
/// its provider as it is now (D40).
|
||||
Future<void> agree(String assistant);
|
||||
|
||||
/// Sends [request] to [assistant] through the instance, the answer
|
||||
/// streamed as halkyon_llm streams a provider's: the text in pieces, then
|
||||
/// the whole turn. A refusal or failure ends it as an `AssistantFailed`;
|
||||
/// cancelling stops the answer coming, which the instance still counts.
|
||||
Stream<LlmEvent> ask(String assistant, LlmRequest request);
|
||||
|
||||
/// What the person used, month by month, the latest first.
|
||||
Future<List<AssistantUse>> figures();
|
||||
}
|
||||
|
||||
/// The boundary to granting assistants (Halkyon Learn's ADR 0021, points 2,
|
||||
/// 6 and 8), on the operator's session: refusals are `OperatorFailed`s.
|
||||
abstract interface class AssistantOperatorGateway() {
|
||||
/// The assistants set in `deploy/assistants.env`, without their keys.
|
||||
Future<AssistantsSetUp> setUp();
|
||||
|
||||
/// Every grant, by handle.
|
||||
Future<List<AssistantGrant>> grants();
|
||||
|
||||
/// Grants [assistant] to [account] with [dailyLimit] and [monthlyLimit]
|
||||
/// (null: none) in its pricing's unit; granted already, the limits change
|
||||
/// and the agreement stays.
|
||||
Future<void> grant({
|
||||
required String account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
});
|
||||
|
||||
/// Takes [assistant] back from [account].
|
||||
Future<void> revoke({required String account, required String assistant});
|
||||
|
||||
/// Everyone's use in the UTC month [month] is in, per person and
|
||||
/// assistant.
|
||||
Future<List<AssistantUse>> figures(DateTime month);
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_failed.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
import 'package:serverpod_client/serverpod_client.dart';
|
||||
|
||||
// A conversation as halkyon_llm holds it, and as the module's assistant
|
||||
// endpoint carries it (Halkyon Learn's ADR 0021, point 7): the app's
|
||||
// features speak halkyon_llm alone, whether the operator's assistant
|
||||
// answers through the instance or a person's own key answers directly, so
|
||||
// a feature never knows which. The module turns them back on its side
|
||||
// (`requestOf`, `messageOf`), each part as it was.
|
||||
|
||||
/// [request] to [assistant] as the module's endpoint takes it; the person's
|
||||
/// ID is the instance's to give (D30), so [LlmRequest.personId] stays here.
|
||||
protocol.AssistantQuestion questionOf(String assistant, LlmRequest request) =>
|
||||
protocol.AssistantQuestion(
|
||||
assistant: assistant,
|
||||
system: request.system,
|
||||
messages: [for (final message in request.messages) messageOf(message)],
|
||||
tools: [
|
||||
for (final tool in request.tools)
|
||||
protocol.AssistantTool(
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: jsonEncode(tool.inputSchema),
|
||||
),
|
||||
],
|
||||
maxOutputTokens: request.maxOutputTokens,
|
||||
);
|
||||
|
||||
/// [message] as the module's endpoint carries it.
|
||||
protocol.AssistantMessage messageOf(LlmMessage message) =>
|
||||
protocol.AssistantMessage(
|
||||
role: switch (message.role) {
|
||||
LlmRole.user => protocol.AssistantRole.user,
|
||||
LlmRole.assistant => protocol.AssistantRole.assistant,
|
||||
},
|
||||
parts: [
|
||||
for (final part in message.parts)
|
||||
switch (part) {
|
||||
LlmText(:final text, :final extra) => protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.text,
|
||||
text: text,
|
||||
extra: extra.isEmpty ? null : jsonEncode(extra),
|
||||
),
|
||||
LlmImage(:final mediaType, :final bytes) => protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.image,
|
||||
mediaType: mediaType,
|
||||
bytes: ByteData.sublistView(bytes),
|
||||
),
|
||||
LlmToolCall(:final id, :final name, :final input, :final extra) =>
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.toolCall,
|
||||
callId: id,
|
||||
name: name,
|
||||
json: jsonEncode(input),
|
||||
extra: extra.isEmpty ? null : jsonEncode(extra),
|
||||
),
|
||||
LlmToolResult(:final callId, :final content, :final isError) =>
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.toolResult,
|
||||
callId: callId,
|
||||
text: content,
|
||||
isError: isError,
|
||||
),
|
||||
LlmProviderBlock(protocol: final of, :final block) =>
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.providerBlock,
|
||||
protocol: of.key,
|
||||
json: jsonEncode(block),
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
/// The model's turn [message], as the module's endpoint answers it, in
|
||||
/// halkyon_llm's terms; a [FormatException] for a turn without parts or a
|
||||
/// part that isn't one, or out of its turn.
|
||||
LlmMessage llmMessageOf(protocol.AssistantMessage message) {
|
||||
final parts = [for (final part in message.parts) _partOf(part)];
|
||||
final byPerson = message.role == protocol.AssistantRole.user;
|
||||
if (parts.isEmpty ||
|
||||
parts.any(
|
||||
(part) => byPerson
|
||||
? part is LlmToolCall || part is LlmProviderBlock
|
||||
: part is LlmImage || part is LlmToolResult,
|
||||
)) {
|
||||
throw const FormatException('Not a turn of its role');
|
||||
}
|
||||
return byPerson ? LlmMessage.user(parts) : LlmMessage.assistant(parts);
|
||||
}
|
||||
|
||||
LlmPart _partOf(protocol.AssistantPart part) => switch (part) {
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.text,
|
||||
:final text?,
|
||||
) =>
|
||||
LlmText(text, extra: _objectOrNone(part.extra)),
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.image,
|
||||
:final mediaType?,
|
||||
:final bytes?,
|
||||
) =>
|
||||
LlmImage(mediaType: mediaType, bytes: Uint8List.sublistView(bytes)),
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.toolCall,
|
||||
:final callId?,
|
||||
:final name?,
|
||||
:final json?,
|
||||
) =>
|
||||
LlmToolCall(
|
||||
id: callId,
|
||||
name: name,
|
||||
input: _object(json),
|
||||
extra: _objectOrNone(part.extra),
|
||||
),
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.toolResult,
|
||||
:final callId?,
|
||||
:final text?,
|
||||
) =>
|
||||
LlmToolResult(
|
||||
callId: callId,
|
||||
content: text,
|
||||
isError: part.isError ?? false,
|
||||
),
|
||||
protocol.AssistantPart(
|
||||
kind: protocol.AssistantPartKind.providerBlock,
|
||||
protocol: final key?,
|
||||
:final json?,
|
||||
) =>
|
||||
LlmProviderBlock(
|
||||
protocol:
|
||||
LlmProtocol.named(key) ??
|
||||
(throw FormatException('No protocol of this name', key)),
|
||||
block: _object(json),
|
||||
),
|
||||
_ => throw FormatException('Not a part of its kind', part.kind.name),
|
||||
};
|
||||
|
||||
Map<String, Object?> _object(String json) => switch (jsonDecode(json)) {
|
||||
final Map<String, Object?> object => object,
|
||||
_ => throw FormatException('Not a JSON object', json),
|
||||
};
|
||||
|
||||
Map<String, Object?> _objectOrNone(String? json) =>
|
||||
json == null ? const {} : _object(json);
|
||||
|
||||
/// Why the model stopped, in halkyon_llm's terms.
|
||||
LlmStop stopOf(protocol.AssistantStop? stop) => switch (stop) {
|
||||
protocol.AssistantStop.end => LlmStop.end,
|
||||
protocol.AssistantStop.maxTokens => LlmStop.maxTokens,
|
||||
protocol.AssistantStop.toolCalls => LlmStop.toolCalls,
|
||||
protocol.AssistantStop.refused => LlmStop.refused,
|
||||
protocol.AssistantStop.other || null => LlmStop.other,
|
||||
};
|
||||
|
||||
/// The module's [answers] as halkyon_llm's events: each piece of text, then
|
||||
/// the whole turn; its refusals, and failing to reach the instance, as
|
||||
/// [AssistantFailed]. An answer that ends without its turn fails too, and
|
||||
/// nothing comes after the first failure.
|
||||
Stream<LlmEvent> eventsOf(Stream<protocol.AssistantAnswer> answers) {
|
||||
var ended = false;
|
||||
return answers.transform(
|
||||
StreamTransformer<protocol.AssistantAnswer, LlmEvent>.fromHandlers(
|
||||
handleData: (answer, sink) {
|
||||
if (ended) {
|
||||
return;
|
||||
}
|
||||
if (answer.delta case final delta? when delta.isNotEmpty) {
|
||||
sink.add(LlmTextDelta(delta));
|
||||
}
|
||||
if (answer.message case final message?) {
|
||||
ended = true;
|
||||
try {
|
||||
sink.add(
|
||||
LlmFinished(
|
||||
message: llmMessageOf(message),
|
||||
stop: stopOf(answer.stop),
|
||||
usage: LlmUsage(
|
||||
inputTokens: answer.inputTokens ?? 0,
|
||||
outputTokens: answer.outputTokens ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
} on FormatException {
|
||||
sink.addError(
|
||||
const AssistantFailed(AssistantProblem.providerFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleError: (error, st, sink) {
|
||||
if (!ended) {
|
||||
ended = true;
|
||||
sink.addError(failureOf(error), st);
|
||||
}
|
||||
},
|
||||
handleDone: (sink) {
|
||||
if (!ended) {
|
||||
sink.addError(const AssistantFailed(AssistantProblem.failed));
|
||||
}
|
||||
sink.close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// [error] from the instance as an [AssistantFailed].
|
||||
AssistantFailed failureOf(Object error) => switch (error) {
|
||||
final AssistantFailed failed => failed,
|
||||
protocol.AssistantRefusedException(
|
||||
:final refusal,
|
||||
:final resetsAt,
|
||||
:final retryAfterSeconds,
|
||||
) =>
|
||||
AssistantFailed(
|
||||
switch (refusal) {
|
||||
protocol.AssistantRefusal.notGranted => AssistantProblem.notGranted,
|
||||
protocol.AssistantRefusal.notAgreed => AssistantProblem.notAgreed,
|
||||
protocol.AssistantRefusal.limitReached => AssistantProblem.limitReached,
|
||||
protocol.AssistantRefusal.limitOutdated =>
|
||||
AssistantProblem.limitOutdated,
|
||||
protocol.AssistantRefusal.imagesNotRead =>
|
||||
AssistantProblem.imagesNotRead,
|
||||
protocol.AssistantRefusal.malformed => AssistantProblem.malformed,
|
||||
protocol.AssistantRefusal.tooLarge => AssistantProblem.tooLarge,
|
||||
protocol.AssistantRefusal.tooManyAtOnce =>
|
||||
AssistantProblem.tooManyAtOnce,
|
||||
protocol.AssistantRefusal.providerKey => AssistantProblem.providerKey,
|
||||
protocol.AssistantRefusal.providerBusy => AssistantProblem.providerBusy,
|
||||
protocol.AssistantRefusal.providerRefused =>
|
||||
AssistantProblem.providerRefused,
|
||||
protocol.AssistantRefusal.providerFailed =>
|
||||
AssistantProblem.providerFailed,
|
||||
},
|
||||
resetsAt: resetsAt,
|
||||
retryAfter: switch (retryAfterSeconds) {
|
||||
final seconds? => Duration(seconds: seconds),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
// A session without the scope, or none.
|
||||
ServerpodClientUnauthorized() || ServerpodClientForbidden() =>
|
||||
const AssistantFailed(AssistantProblem.notAllowed),
|
||||
_ => const AssistantFailed(AssistantProblem.failed),
|
||||
};
|
||||
|
||||
/// A provider's [failure], answering a person's own key, as an
|
||||
/// [AssistantFailed]: the key's, busy, the question refused, or failed.
|
||||
AssistantFailed ownKeyFailureOf(LlmFailure failure) => AssistantFailed(
|
||||
switch (failure.kind) {
|
||||
LlmFailureKind.key => AssistantProblem.providerKey,
|
||||
LlmFailureKind.rateLimited ||
|
||||
LlmFailureKind.unavailable => AssistantProblem.providerBusy,
|
||||
LlmFailureKind.request => AssistantProblem.providerRefused,
|
||||
LlmFailureKind.network => AssistantProblem.failed,
|
||||
LlmFailureKind.provider ||
|
||||
LlmFailureKind.malformed => AssistantProblem.providerFailed,
|
||||
},
|
||||
retryAfter: failure.retryAfter,
|
||||
);
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_gateway.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_messages.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_failed.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/own_assistant.dart';
|
||||
import 'package:halkyon_app/src/operator/data/instance_operator_gateway.dart';
|
||||
import 'package:halkyon_app/src/operator/model/operated_account.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// Who answers a question (Halkyon Learn's ADR 0021, point 7): an assistant
|
||||
/// the operator granted, asked through the instance, or one with the
|
||||
/// person's own key, asked directly (point 10). Features ask either alike.
|
||||
sealed class const AssistantChoice() {
|
||||
/// The name people see.
|
||||
String get name;
|
||||
|
||||
/// The host that receives what is asked.
|
||||
String get host;
|
||||
|
||||
/// Whether it reads images.
|
||||
bool get images;
|
||||
}
|
||||
|
||||
/// An assistant the operator granted, asked through the instance.
|
||||
final class const GrantedAssistant(final OfferedAssistant assistant)
|
||||
extends AssistantChoice {
|
||||
@override
|
||||
String get name => assistant.name;
|
||||
|
||||
@override
|
||||
String get host => assistant.host;
|
||||
|
||||
@override
|
||||
bool get images => assistant.images;
|
||||
}
|
||||
|
||||
/// An assistant with the person's own key, asked directly: the instance
|
||||
/// sees neither the key nor what is asked, and counts nothing.
|
||||
final class const PersonalAssistant(final OwnAssistant assistant)
|
||||
extends AssistantChoice {
|
||||
@override
|
||||
String get name => assistant.name;
|
||||
|
||||
@override
|
||||
String get host => assistant.host;
|
||||
|
||||
@override
|
||||
bool get images => assistant.images;
|
||||
}
|
||||
|
||||
/// The assistants the signed-in person may ask (Halkyon Learn's ADR 0021):
|
||||
/// those the operator granted, agreed to once, their use, and asking
|
||||
/// either kind. Failures are [AssistantFailed]s.
|
||||
class AssistantRepository(
|
||||
final AssistantGateway _gateway, {
|
||||
|
||||
/// What asks a personal key's provider; one over HTTP unless given.
|
||||
LlmClient? llm,
|
||||
}) {
|
||||
final LlmClient _llm = llm ?? LlmClient();
|
||||
final _changes = StreamController<void>.broadcast();
|
||||
|
||||
/// Every agreement made through this repository, for what shows the
|
||||
/// assistants to read them anew.
|
||||
Stream<void> get changes => _changes.stream;
|
||||
|
||||
/// The assistants granted to the person, with what is left of their
|
||||
/// limits.
|
||||
Future<AssistantsOffer> offered() => _gateway.offered();
|
||||
|
||||
/// Records that the person agrees to what goes where with [assistant].
|
||||
Future<void> agree(String assistant) async {
|
||||
await _gateway.agree(assistant);
|
||||
if (!_changes.isClosed) {
|
||||
_changes.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// What the person used, month by month, the latest first.
|
||||
Future<List<AssistantUse>> figures() => _gateway.figures();
|
||||
|
||||
/// Asks [choice] [request], the answer streamed: the text in pieces, then
|
||||
/// the whole turn; a failure ends it as an [AssistantFailed]. A personal
|
||||
/// key's provider gets no person's ID: it knows its key's owner.
|
||||
Stream<LlmEvent> ask(AssistantChoice choice, LlmRequest request) =>
|
||||
switch (choice) {
|
||||
GrantedAssistant(:final assistant) => _gateway.ask(
|
||||
assistant.id,
|
||||
request,
|
||||
),
|
||||
PersonalAssistant(:final assistant) => _askDirectly(
|
||||
assistant,
|
||||
request,
|
||||
),
|
||||
};
|
||||
|
||||
Stream<LlmEvent> _askDirectly(OwnAssistant assistant, LlmRequest request) {
|
||||
final LlmEndpoint endpoint;
|
||||
try {
|
||||
endpoint = assistant.endpoint;
|
||||
} on FormatException {
|
||||
return Stream.error(
|
||||
const AssistantFailed(AssistantProblem.providerFailed),
|
||||
);
|
||||
}
|
||||
return _llm
|
||||
.send(
|
||||
endpoint,
|
||||
LlmRequest(
|
||||
messages: request.messages,
|
||||
maxOutputTokens: request.maxOutputTokens,
|
||||
system: request.system,
|
||||
tools: request.tools,
|
||||
),
|
||||
)
|
||||
.handleError(
|
||||
(Object error) => throw switch (error) {
|
||||
final LlmFailure failure => ownKeyFailureOf(failure),
|
||||
_ => const AssistantFailed(AssistantProblem.failed),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Closes [changes] and the HTTP client; the owner calls it as it closes.
|
||||
Future<void> dispose() async {
|
||||
// Not awaited: a broadcast stream's close completes in the root zone,
|
||||
// which a widget test's fake clock never reaches.
|
||||
unawaited(_changes.close());
|
||||
_llm.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Granting assistants (Halkyon Learn's ADR 0021, points 2, 6 and 8), as
|
||||
/// the operator's screen uses it. Refusals are [OperatorFailed]s.
|
||||
class AssistantOperatorRepository(
|
||||
final AssistantOperatorGateway _gateway,
|
||||
|
||||
/// Where accounts are found by their handles.
|
||||
final InstanceOperatorGateway _accounts,
|
||||
) {
|
||||
final _changes = StreamController<void>.broadcast();
|
||||
|
||||
/// Every change made through this repository.
|
||||
Stream<void> get changes => _changes.stream;
|
||||
|
||||
/// The assistants set, without their keys.
|
||||
Future<AssistantsSetUp> setUp() => _gateway.setUp();
|
||||
|
||||
/// Every grant, by handle.
|
||||
Future<List<AssistantGrant>> grants() => _gateway.grants();
|
||||
|
||||
/// Grants [assistant] to the account with [handle] (an `@` before it
|
||||
/// left out, whatever its case), with [dailyLimit] and [monthlyLimit] in
|
||||
/// its pricing's unit (null: none); refused as `notFound` for a handle
|
||||
/// nobody has.
|
||||
Future<void> grant({
|
||||
required String handle,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) async {
|
||||
final typed = handle.trim().replaceFirst(RegExp('^@'), '');
|
||||
final account = typed.isEmpty
|
||||
? null
|
||||
: (await _accounts.accountsStartingWith(typed))
|
||||
.where(
|
||||
(account) =>
|
||||
account.handle?.toLowerCase() == typed.toLowerCase(),
|
||||
)
|
||||
.firstOrNull;
|
||||
if (account == null) {
|
||||
throw const OperatorFailed(OperatorProblem.notFound);
|
||||
}
|
||||
await changeLimits(
|
||||
account: account.id,
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets the limits of [assistant]'s grant to [account], granting it where
|
||||
/// it isn't; the person's agreement stays.
|
||||
Future<void> changeLimits({
|
||||
required String account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) => _changing(
|
||||
_gateway.grant(
|
||||
account: account,
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
);
|
||||
|
||||
/// Takes [assistant] back from [account].
|
||||
Future<void> revoke({required String account, required String assistant}) =>
|
||||
_changing(_gateway.revoke(account: account, assistant: assistant));
|
||||
|
||||
/// Everyone's use in the UTC month [month] is in.
|
||||
Future<List<AssistantUse>> figures(DateTime month) =>
|
||||
_gateway.figures(utcMonthOf(month));
|
||||
|
||||
Future<void> _changing(Future<void> change) async {
|
||||
await change;
|
||||
if (!_changes.isClosed) {
|
||||
_changes.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes [changes]; the owner calls it as it closes.
|
||||
Future<void> dispose() async {
|
||||
unawaited(_changes.close());
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a person's own assistants are kept (Halkyon Learn's ADR 0021,
|
||||
/// point 10): an app's settings, sealed with the person's key, which each
|
||||
/// app keeps in its own way.
|
||||
abstract interface class OwnAssistantStore() {
|
||||
/// The person's own assistants now and after every change, in the order
|
||||
/// added.
|
||||
Stream<List<OwnAssistant>> watch();
|
||||
|
||||
/// Keeps [assistant], in place of the one with its ID where there is one;
|
||||
/// refused ([OwnAssistantRefused]) where the settings wouldn't be sealed.
|
||||
Future<void> save(OwnAssistant assistant);
|
||||
|
||||
/// Removes the assistant [id] names.
|
||||
Future<void> remove(String id);
|
||||
}
|
||||
|
||||
/// A key wasn't kept: this device can't seal the person's settings, so the
|
||||
/// instance could read it.
|
||||
final class const OwnAssistantRefused() implements Exception {
|
||||
@override
|
||||
String toString() => 'OwnAssistantRefused()';
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import 'package:halkyon_app/src/assistant/data/assistant_gateway.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_messages.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_app/src/operator/data/serverpod_instance_operator_gateway.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
import 'package:serverpod_client/serverpod_client.dart';
|
||||
|
||||
/// [AssistantGateway] on the module's `assistant` endpoint of an app's
|
||||
/// client ([_base], `client.modules.base`), as the device's person.
|
||||
class ServerpodAssistantGateway(final protocol.Caller _base)
|
||||
implements AssistantGateway {
|
||||
@override
|
||||
Future<AssistantsOffer> offered() => _failing(() async {
|
||||
final offered = await _base.assistant.offered();
|
||||
return AssistantsOffer(
|
||||
assistants: [
|
||||
for (final offer in offered.assistants)
|
||||
OfferedAssistant(
|
||||
id: offer.id,
|
||||
name: offer.name,
|
||||
protocol: offer.protocol,
|
||||
host: offer.host,
|
||||
model: offer.model,
|
||||
images: offer.images,
|
||||
agreedAt: offer.agreedAt,
|
||||
unit: unitOf(offer.limitUnit),
|
||||
dailyLimit: offer.dailyLimit,
|
||||
monthlyLimit: offer.monthlyLimit,
|
||||
usedToday: offer.usedToday,
|
||||
usedThisMonth: offer.usedThisMonth,
|
||||
outdated: offer.outdated,
|
||||
),
|
||||
],
|
||||
ownKeys: offered.ownKeys,
|
||||
currency: offered.currency,
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> agree(String assistant) =>
|
||||
_failing(() => _base.assistant.agree(assistant: assistant));
|
||||
|
||||
@override
|
||||
Stream<LlmEvent> ask(String assistant, LlmRequest request) => eventsOf(
|
||||
_base.assistant.ask(question: questionOf(assistant, request)),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<AssistantUse>> figures() => _failing(
|
||||
() async => [...(await _base.assistant.figures()).map(useOf)],
|
||||
);
|
||||
}
|
||||
|
||||
/// [AssistantOperatorGateway] on the module's `assistantOperator` endpoint
|
||||
/// of an app's client ([_base]), as the operator; refusals as
|
||||
/// `OperatorFailed`, as the operator's other calls.
|
||||
class ServerpodAssistantOperatorGateway(final protocol.Caller _base)
|
||||
implements AssistantOperatorGateway {
|
||||
@override
|
||||
Future<AssistantsSetUp> setUp() => operatorCall(() async {
|
||||
final setUp = await _base.assistantOperator.setUp();
|
||||
return AssistantsSetUp(
|
||||
assistants: [
|
||||
for (final info in setUp.assistants)
|
||||
SetUpAssistant(
|
||||
id: info.id,
|
||||
name: info.name,
|
||||
protocol: info.protocol,
|
||||
host: info.host,
|
||||
model: info.model,
|
||||
images: info.images,
|
||||
priceInput: info.priceInput,
|
||||
priceOutput: info.priceOutput,
|
||||
),
|
||||
],
|
||||
currency: setUp.currency,
|
||||
ownKeys: setUp.ownKeys,
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<List<AssistantGrant>> grants() => operatorCall(
|
||||
() async => [
|
||||
for (final grant in await _base.assistantOperator.grants())
|
||||
AssistantGrant(
|
||||
account: grant.account.uuid,
|
||||
handle: grant.handle,
|
||||
assistant: grant.assistant,
|
||||
assistantSet: grant.assistantSet,
|
||||
grantedAt: grant.grantedAt,
|
||||
unit: unitOf(grant.limitUnit),
|
||||
dailyLimit: grant.dailyLimit,
|
||||
monthlyLimit: grant.monthlyLimit,
|
||||
agreedAt: grant.agreedAt,
|
||||
outdated: grant.outdated,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> grant({
|
||||
required String account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) => operatorCall(
|
||||
() => _base.assistantOperator.grant(
|
||||
account: UuidValue.withValidation(account),
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> revoke({required String account, required String assistant}) =>
|
||||
operatorCall(
|
||||
() => _base.assistantOperator.revoke(
|
||||
account: UuidValue.withValidation(account),
|
||||
assistant: assistant,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<AssistantUse>> figures(DateTime month) => operatorCall(
|
||||
() async => [
|
||||
...(await _base.assistantOperator.figures(month: month.toUtc())).map(
|
||||
useOf,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// [unit] in the app's terms.
|
||||
AssistantLimitUnit unitOf(protocol.AssistantLimitUnit unit) => switch (unit) {
|
||||
protocol.AssistantLimitUnit.costMicros => AssistantLimitUnit.money,
|
||||
protocol.AssistantLimitUnit.tokens => AssistantLimitUnit.tokens,
|
||||
};
|
||||
|
||||
/// A month's [figures] in the app's terms.
|
||||
AssistantUse useOf(protocol.AssistantMonthFigures figures) => AssistantUse(
|
||||
assistant: figures.assistant,
|
||||
month: figures.month,
|
||||
requests: figures.requests,
|
||||
inputTokens: figures.inputTokens,
|
||||
outputTokens: figures.outputTokens,
|
||||
costMicros: figures.costMicros,
|
||||
account: figures.account.uuid,
|
||||
handle: figures.handle,
|
||||
);
|
||||
|
||||
/// What [call] gives; the instance's refusals, and failing to reach it, as
|
||||
/// `AssistantFailed`.
|
||||
Future<T> _failing<T>(Future<T> Function() call) async {
|
||||
try {
|
||||
return await call();
|
||||
} on Exception catch (error) {
|
||||
throw failureOf(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
|
||||
/// A limit as the operator types it, in [unit]: an amount of money with up
|
||||
/// to six decimals (a point or a comma before them), as millionths; tokens
|
||||
/// as a whole number, its thousands grouped by points or commas, or not
|
||||
/// (`1000000`, `1,000,000`); spaces and underscores left out. Empty means
|
||||
/// no limit (null); anything else that isn't a number of at most 12 digits
|
||||
/// (before the decimals) is a [FormatException].
|
||||
int? limitFromText(String text, AssistantLimitUnit unit) {
|
||||
final typed = text.trim().replaceAll(RegExp(r'[\s_]'), '');
|
||||
if (typed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
switch (unit) {
|
||||
case AssistantLimitUnit.tokens:
|
||||
final grouped = RegExp(r'^\d{1,3}([,.])\d{3}(?:\1\d{3})*$');
|
||||
final digits = typed.replaceAll(RegExp('[,.]'), '');
|
||||
if ((!RegExp(r'^\d+$').hasMatch(typed) && !grouped.hasMatch(typed)) ||
|
||||
digits.length > 12) {
|
||||
throw FormatException('Not a number of tokens', text);
|
||||
}
|
||||
return int.parse(digits);
|
||||
case AssistantLimitUnit.money:
|
||||
final match = RegExp(r'^(\d{1,12})(?:[.,](\d{1,6}))?$').firstMatch(typed);
|
||||
if (match == null) {
|
||||
throw FormatException('Not an amount', text);
|
||||
}
|
||||
final whole = int.parse(match[1]!);
|
||||
final fraction = (match[2] ?? '').padRight(6, '0');
|
||||
return whole * 1000000 + int.parse(fraction);
|
||||
}
|
||||
}
|
||||
|
||||
/// [limit], in [unit], as [limitFromText] reads it back: money with a
|
||||
/// point and no more decimals than it has, tokens as digits; empty for no
|
||||
/// limit.
|
||||
String limitText(int? limit, AssistantLimitUnit unit) {
|
||||
if (limit == null) {
|
||||
return '';
|
||||
}
|
||||
return switch (unit) {
|
||||
AssistantLimitUnit.tokens => '$limit',
|
||||
AssistantLimitUnit.money => _decimal(limit),
|
||||
};
|
||||
}
|
||||
|
||||
String _decimal(int micros) {
|
||||
final whole = micros ~/ 1000000;
|
||||
final fraction = (micros % 1000000)
|
||||
.toString()
|
||||
.padLeft(6, '0')
|
||||
.replaceFirst(RegExp(r'0+$'), '');
|
||||
return fraction.isEmpty ? '$whole' : '$whole.$fraction';
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Why an assistant didn't answer, or why agreeing or reading figures
|
||||
/// failed (Halkyon Learn's ADR 0021): the instance's refusals, its
|
||||
/// provider's failures, and a personal key's provider's.
|
||||
enum AssistantProblem() {
|
||||
/// The operator granted it to nobody of this name (any more).
|
||||
notGranted,
|
||||
|
||||
/// The person hasn't agreed to what goes where, as it is now.
|
||||
notAgreed,
|
||||
|
||||
/// The day's or the month's limit is used up (`AssistantFailed.resetsAt`).
|
||||
limitReached,
|
||||
|
||||
/// Its limits wait for the operator to set them again.
|
||||
limitOutdated,
|
||||
|
||||
/// It reads no images.
|
||||
imagesNotRead,
|
||||
|
||||
/// The question isn't one it can be sent.
|
||||
malformed,
|
||||
|
||||
/// The question is too long, or holds too many or too large images.
|
||||
tooLarge,
|
||||
|
||||
/// Too many questions are on their way at once.
|
||||
tooManyAtOnce,
|
||||
|
||||
/// The provider refused the key: the operator's, or one's own.
|
||||
providerKey,
|
||||
|
||||
/// The provider is busy or down (`AssistantFailed.retryAfter`).
|
||||
providerBusy,
|
||||
|
||||
/// The provider refused the question.
|
||||
providerRefused,
|
||||
|
||||
/// The provider failed otherwise.
|
||||
providerFailed,
|
||||
|
||||
/// The signed-in person may not do this.
|
||||
notAllowed,
|
||||
|
||||
/// Anything else, such as no connection.
|
||||
failed,
|
||||
}
|
||||
|
||||
/// An assistant failed as [problem] says, with when its limit resets or
|
||||
/// when to try again where the instance or provider says so.
|
||||
final class const AssistantFailed(
|
||||
final AssistantProblem problem, {
|
||||
final DateTime? resetsAt,
|
||||
final Duration? retryAfter,
|
||||
}) with Equatable implements Exception {
|
||||
@override
|
||||
List<Object?> get props => [problem, resetsAt, retryAfter];
|
||||
|
||||
@override
|
||||
String toString() => 'AssistantFailed(${problem.name})';
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
|
||||
/// An assistant the operator set in `deploy/assistants.env`, as the
|
||||
/// operator's screen shows it (Halkyon Learn's ADR 0021, point 4): never
|
||||
/// its key.
|
||||
final class const SetUpAssistant({
|
||||
required final String id,
|
||||
required final String name,
|
||||
required final String protocol,
|
||||
required final String host,
|
||||
required final String model,
|
||||
final bool images = false,
|
||||
|
||||
/// What a million of the request's tokens cost, in the instance's
|
||||
/// currency; null without prices.
|
||||
final double? priceInput,
|
||||
|
||||
/// What a million of the answer's tokens cost; null without prices.
|
||||
final double? priceOutput,
|
||||
}) with Equatable {
|
||||
/// What its limits are counted in: money with prices, else tokens (D38).
|
||||
AssistantLimitUnit get unit => priceInput != null && priceOutput != null
|
||||
? AssistantLimitUnit.money
|
||||
: AssistantLimitUnit.tokens;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
protocol,
|
||||
host,
|
||||
model,
|
||||
images,
|
||||
priceInput,
|
||||
priceOutput,
|
||||
];
|
||||
}
|
||||
|
||||
/// The assistants the operator set, for their screen.
|
||||
final class const AssistantsSetUp({
|
||||
/// Each, in the operator's order; none where none is set.
|
||||
final List<SetUpAssistant> assistants = const [],
|
||||
|
||||
/// The currency of the prices (ISO 4217).
|
||||
final String currency = 'USD',
|
||||
|
||||
/// Whether people may add their own keys.
|
||||
final bool ownKeys = false,
|
||||
}) with Equatable {
|
||||
/// The assistant [id] names; null for one not set (any more).
|
||||
SetUpAssistant? named(String id) {
|
||||
for (final assistant in assistants) {
|
||||
if (assistant.id == id) {
|
||||
return assistant;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [assistants, currency, ownKeys];
|
||||
}
|
||||
|
||||
/// An assistant granted to an account, as the operator sees it (D2, D4).
|
||||
final class const AssistantGrant({
|
||||
/// The account's ID.
|
||||
required final String account,
|
||||
|
||||
/// Its handle; null for one without.
|
||||
final String? handle,
|
||||
|
||||
/// The assistant's ID.
|
||||
required final String assistant,
|
||||
|
||||
/// Whether the assistant is still set; a grant of one taken out of the
|
||||
/// file stays, unused.
|
||||
final bool assistantSet = true,
|
||||
required final DateTime grantedAt,
|
||||
|
||||
/// What [dailyLimit] and [monthlyLimit] are counted in.
|
||||
final AssistantLimitUnit unit = AssistantLimitUnit.tokens,
|
||||
final int? dailyLimit,
|
||||
final int? monthlyLimit,
|
||||
|
||||
/// When the person agreed to what goes where; null until they do.
|
||||
final DateTime? agreedAt,
|
||||
|
||||
/// Whether its limits are in another unit than the pricing's now, to be
|
||||
/// set again.
|
||||
final bool outdated = false,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
account,
|
||||
handle,
|
||||
assistant,
|
||||
assistantSet,
|
||||
grantedAt,
|
||||
unit,
|
||||
dailyLimit,
|
||||
monthlyLimit,
|
||||
agreedAt,
|
||||
outdated,
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// What an assistant's limits and figures are counted in (Halkyon Learn's
|
||||
/// ADR 0021, D38): money where the operator set its prices, else tokens.
|
||||
enum AssistantLimitUnit() {
|
||||
/// Millionths of the instance's currency (`AssistantsOffer.currency`).
|
||||
money,
|
||||
|
||||
/// Tokens, the request's and the answer's together.
|
||||
tokens,
|
||||
}
|
||||
|
||||
/// An assistant the operator granted to the signed-in person (Halkyon
|
||||
/// Learn's ADR 0021, points 6 and 9): who answers, what is left of its
|
||||
/// limits, and whether the person agreed to what goes where.
|
||||
final class const OfferedAssistant({
|
||||
/// The ID the operator gave it, which questions name.
|
||||
required final String id,
|
||||
|
||||
/// The name people see.
|
||||
required final String name,
|
||||
|
||||
/// The protocol it is asked in (`anthropic`, `openai`,
|
||||
/// `openai-responses`).
|
||||
required final String protocol,
|
||||
|
||||
/// The provider's host, which receives what is asked.
|
||||
required final String host,
|
||||
|
||||
/// The model that answers.
|
||||
required final String model,
|
||||
|
||||
/// Whether it reads images.
|
||||
final bool images = false,
|
||||
|
||||
/// When the person agreed to what goes where, as it is now (D40); null
|
||||
/// until they do, or once it moved to another provider.
|
||||
final DateTime? agreedAt,
|
||||
|
||||
/// What its limits and use are counted in.
|
||||
final AssistantLimitUnit unit = AssistantLimitUnit.tokens,
|
||||
|
||||
/// The most a UTC day may use, in [unit]; null for no daily limit.
|
||||
final int? dailyLimit,
|
||||
|
||||
/// The most a UTC month may use, in [unit]; null for no monthly limit.
|
||||
final int? monthlyLimit,
|
||||
|
||||
/// What today used, in [unit].
|
||||
final int usedToday = 0,
|
||||
|
||||
/// What this month used, in [unit].
|
||||
final int usedThisMonth = 0,
|
||||
|
||||
/// Whether its limits were set in another unit than its pricing's now:
|
||||
/// it answers nothing until the operator sets them again.
|
||||
final bool outdated = false,
|
||||
}) with Equatable {
|
||||
/// Whether the person agreed to what goes where.
|
||||
bool get agreed => agreedAt != null;
|
||||
|
||||
/// What is left of today's limit; null without one.
|
||||
int? get leftToday => switch (dailyLimit) {
|
||||
final limit? => max(0, limit - usedToday),
|
||||
null => null,
|
||||
};
|
||||
|
||||
/// What is left of this month's limit; null without one.
|
||||
int? get leftThisMonth => switch (monthlyLimit) {
|
||||
final limit? => max(0, limit - usedThisMonth),
|
||||
null => null,
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
protocol,
|
||||
host,
|
||||
model,
|
||||
images,
|
||||
agreedAt,
|
||||
unit,
|
||||
dailyLimit,
|
||||
monthlyLimit,
|
||||
usedToday,
|
||||
usedThisMonth,
|
||||
outdated,
|
||||
];
|
||||
}
|
||||
|
||||
/// The assistants the instance offers the signed-in person.
|
||||
final class const AssistantsOffer({
|
||||
/// Those granted to them, in the operator's order.
|
||||
final List<OfferedAssistant> assistants = const [],
|
||||
|
||||
/// Whether they may add assistants with their own keys (D5, D6).
|
||||
final bool ownKeys = false,
|
||||
|
||||
/// The currency of what is counted in money (ISO 4217).
|
||||
final String currency = 'USD',
|
||||
}) with Equatable {
|
||||
/// Nothing offered: an instance without assistants, or a member (D43).
|
||||
static const none = AssistantsOffer();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [assistants, ownKeys, currency];
|
||||
}
|
||||
|
||||
/// What a person used of one assistant in one UTC month: their own, or,
|
||||
/// for the operator, anyone's (D3).
|
||||
final class const AssistantUse({
|
||||
/// The assistant's ID.
|
||||
required final String assistant,
|
||||
|
||||
/// The month's first day, in UTC.
|
||||
required final DateTime month,
|
||||
final int requests = 0,
|
||||
final int inputTokens = 0,
|
||||
final int outputTokens = 0,
|
||||
|
||||
/// What it cost, in millionths of the instance's currency, at the prices
|
||||
/// of when it was counted; 0 without prices.
|
||||
final int costMicros = 0,
|
||||
|
||||
/// Whose: the account's ID.
|
||||
final String? account,
|
||||
|
||||
/// Their handle; null for one without.
|
||||
final String? handle,
|
||||
}) with Equatable {
|
||||
/// The tokens of the requests and the answers together.
|
||||
int get tokens => inputTokens + outputTokens;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
assistant,
|
||||
month,
|
||||
requests,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
costMicros,
|
||||
account,
|
||||
handle,
|
||||
];
|
||||
}
|
||||
|
||||
/// The first day of [at]'s UTC month, in UTC.
|
||||
DateTime utcMonthOf(DateTime at) {
|
||||
final utc = at.toUtc();
|
||||
return DateTime.utc(utc.year, utc.month);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue