masel_ai_chat 2.1.3
masel_ai_chat: ^2.1.3 copied to clipboard
Ai chat.
example/lib/main.dart
// ignore_for_file: invalid_use_of_visible_for_testing_member, implementation_imports, avoid_print
import 'dart:async';
import 'package:cartesian_product/cartesian_product.dart';
import 'package:extended_logger/extended_logger.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart';
import 'package:masel_ai_chat/ai_chat.dart';
import 'package:masel_ai_completions/ai_completions.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'key.dart';
import 'story.dart';
import 'summary_completions.dart';
typedef _TestCase = ({
CompactionSetup compactionSetup,
String aiModel,
String summaryModel,
});
final List<CompactionSetup> _compactionSetups = [
// Balanced (current default)
(
recentCharsToKeep: 80000,
minCharsToCompact: 80000,
compactionTargetChars: 5000,
),
// Aggressive: compact early & hard
(
recentCharsToKeep: 80000,
minCharsToCompact: 40000,
compactionTargetChars: 2000,
),
// Memory-heavy: prioritize recent verbatim context
(
recentCharsToKeep: 120000,
minCharsToCompact: 40000,
compactionTargetChars: 10000,
),
// Minimal-recent: more gets summarized
(
recentCharsToKeep: 40000,
minCharsToCompact: 80000,
compactionTargetChars: 5000,
),
];
final _aiModels = [
'google/gemini-3.1-flash-lite-preview', // Fast/cheap
'google/gemini-3-flash-preview', // Fast/balanced
'anthropic/claude-sonnet-4.6', // Balanced
'anthropic/claude-opus-4.6', // Premium
];
final _summaryModels = [
'google/gemini-3.1-flash-lite-preview', // Cheapest, fastest, 1M context
'google/gemini-3-flash-preview', // Slightly smarter summaries
];
final List<_TestCase> _cases =
cartesianProduct<dynamic>([_compactionSetups, _aiModels, _summaryModels])
.map(
(e) => (
compactionSetup: e[0] as CompactionSetup,
aiModel: e[1] as String,
summaryModel: e[2] as String,
),
)
.toList();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
print('START ALL');
for (final cs in _cases) {
await _evalCase(cs);
}
print('ALL DONE');
}
Future<void> _evalCase(_TestCase testCase) async {
print('## Start: $testCase');
final (summaryCompletions, aiChat) = await _setup(
aiModel: testCase.aiModel,
summaryModel: testCase.summaryModel,
compactionSetup: testCase.compactionSetup,
);
final atLimitCompleter = Completer();
await aiChat.measureResponseTime('Empty history');
aiChat.fillPastCompaction(compactionSetup: testCase.compactionSetup);
aiChat
.measureResponseTime('At limit')
.then((value) => atLimitCompleter.complete());
await summaryCompletions.stream.firstWhere((e) => e.isStart);
print('Compaction started...');
final t0 = DateTime.now();
await summaryCompletions.stream.firstWhere((e) => e.isDone);
print('Compaction: ${DateTime.now().difference(t0)}');
await aiChat.measureResponseTime('After compaction');
await aiChat.measureResponseTime('Again with prompt cache');
// log(aiChat.state.items.map((e) => e.item).join('\n'));
await atLimitCompleter.future;
print('Done: $testCase');
}
Future<(ExampleSummaryCompletions, AiChat)> _setup({
required String aiModel,
required String summaryModel,
required CompactionSetup compactionSetup,
}) async {
final httpClient = Client();
final aiCompletions = OpenRouterCompletions(
httpClient: httpClient,
openRouterApiKey: openRouterApiKey,
modelId: aiModel,
);
final summaryCompletions = ExampleSummaryCompletions(
httpClient: httpClient,
openRouterApiKey: openRouterApiKey,
modelId: summaryModel,
);
SharedPreferences.setMockInitialValues({});
final sharedPreferences = await SharedPreferences.getInstance();
final logger = ExtendedLogger(
logLocally: (level, message, error, stackTrace) =>
print('$level, $message, $error, $stackTrace'),
);
return (
summaryCompletions,
AiChat(
aiCompletions: aiCompletions,
summaryCompletions: summaryCompletions,
sharedPreferences: sharedPreferences,
chatId: 'chatId',
userId: 'userId',
remoteItems: Stream.empty(),
syncRemoteItems: (inserts, deletes) async {},
logger: logger,
instructions: () => 'You are a helpful assistant',
compactionSetup: compactionSetup,
),
);
}
extension on AiChat {
Future<void> measureResponseTime(String description) async {
print('started: $description');
var t0 = DateTime.now();
final prompt = 'Reply with exactly one word: OK';
final nChars = createAiRequest(prompt: prompt).nCharsInRequest();
await getResponse(prompt: prompt);
print(
'completed: $description, nCharsInReq:$nChars, t:${DateTime.now().difference(t0)}',
);
}
void fillPastCompaction({required CompactionSetup compactionSetup}) {
final limit =
compactionSetup.recentCharsToKeep + compactionSetup.minCharsToCompact;
for (int i = 0; i < (limit / 1000).floor(); i++) {
addTurnManually(
prompt: story.randomSubstring(n: 400),
answer: story.randomSubstring(n: 400),
);
addContext(text: story.randomSubstring(n: 200));
}
}
}
extension on AiRequest {
int nCharsInRequest() => messages
.map(
(e) => switch (e) {
SystemMessage() => e.content.length,
UserMessage() => e.content.content.length,
AssistantMessage() => e.content.length,
},
)
.fold(0, (previousValue, element) => previousValue + element);
}