masel_ai_chat 2.1.0 copy "masel_ai_chat: ^2.1.0" to clipboard
masel_ai_chat: ^2.1.0 copied to clipboard

unlisted

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);
}