Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/feature_mind/lib/feature_mind.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ export 'src/assistant/presentation/screens/prompt_lab_screen.dart';

// Agent chat + model management
export 'src/agent_chat/application/assistant_model_preferences.dart';
export 'src/agent_chat/application/chat_model_config_preferences.dart';
export 'src/agent_chat/domain/models/chat_model_config.dart';
export 'src/agent_chat/application/assistant_runtime_readiness.dart';
export 'src/agent_chat/data/services/agent_notification_scheduler.dart';
export 'src/agent_chat/data/services/assistant_runtime_service.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:flutter_riverpod/legacy.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../domain/models/chat_model_config.dart';

const String chatModelConfigMaxTokensKey = 'chat_model_config.max_tokens';
const String chatModelConfigTopKKey = 'chat_model_config.top_k';
const String chatModelConfigTopPKey = 'chat_model_config.top_p';
const String chatModelConfigTemperatureKey = 'chat_model_config.temperature';
const String chatModelConfigAcceleratorKey = 'chat_model_config.accelerator';
const String chatModelConfigSystemPromptKey = 'chat_model_config.system_prompt';

final chatModelConfigProvider =
StateNotifierProvider<ChatModelConfigNotifier, ChatModelConfig>((ref) {
return ChatModelConfigNotifier();
});

class ChatModelConfigNotifier extends StateNotifier<ChatModelConfig> {
ChatModelConfigNotifier() : super(ChatModelConfig.defaults) {
_load();
}

Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
state = ChatModelConfig(
maxTokens:
prefs.getInt(chatModelConfigMaxTokensKey) ??
ChatModelConfig.defaults.maxTokens,
topK:
prefs.getInt(chatModelConfigTopKKey) ?? ChatModelConfig.defaults.topK,
topP:
prefs.getDouble(chatModelConfigTopPKey) ??
ChatModelConfig.defaults.topP,
temperature:
prefs.getDouble(chatModelConfigTemperatureKey) ??
ChatModelConfig.defaults.temperature,
accelerator: _acceleratorFromName(
prefs.getString(chatModelConfigAcceleratorKey),
),
systemPrompt:
prefs.getString(chatModelConfigSystemPromptKey) ??
ChatModelConfig.defaults.systemPrompt,
).normalized();
}

Future<void> save(ChatModelConfig config) async {
final next = config.normalized();
state = next;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(chatModelConfigMaxTokensKey, next.maxTokens);
await prefs.setInt(chatModelConfigTopKKey, next.topK);
await prefs.setDouble(chatModelConfigTopPKey, next.topP);
await prefs.setDouble(chatModelConfigTemperatureKey, next.temperature);
await prefs.setString(chatModelConfigAcceleratorKey, next.accelerator.name);
await prefs.setString(chatModelConfigSystemPromptKey, next.systemPrompt);
}

static ChatAccelerator _acceleratorFromName(String? raw) {
return ChatAccelerator.values.firstWhere(
(value) => value.name == raw,
orElse: () => ChatModelConfig.defaults.accelerator,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,11 @@ class AssistantRuntimeService {
required String prompt,
String? systemPrompt,
String? grammar,
int? maxOutputTokens,
double? temperature,
double? topP,
int? topK,
bool preferGpu = true,
}) async {
await _ensureCheckpointsHydrated();
lastGenerationStats = null;
Expand Down Expand Up @@ -794,6 +799,11 @@ class AssistantRuntimeService {
package: package,
prompt: prompt,
systemPrompt: systemPrompt,
maxOutputTokens: maxOutputTokens,
temperature: temperature,
topP: topP,
topK: topK,
preferGpu: preferGpu,
emitRequestTrace: false,
grammar: constrained ? grammar : null,
)) {
Expand Down Expand Up @@ -835,6 +845,10 @@ class AssistantRuntimeService {
required String prompt,
String? systemPrompt,
int? maxOutputTokens,
double? temperature,
double? topP,
int? topK,
bool preferGpu = true,
GenerationConstraint? constraint,
}) async* {
await _ensureCheckpointsHydrated();
Expand All @@ -854,6 +868,10 @@ class AssistantRuntimeService {
prompt: prompt,
systemPrompt: systemPrompt,
maxOutputTokens: maxOutputTokens,
temperature: temperature,
topP: topP,
topK: topK,
preferGpu: preferGpu,
assistantPrefill: constraint?.forcedPrefix,
);
return;
Expand All @@ -863,6 +881,11 @@ class AssistantRuntimeService {
selectedModelId: runtimeId,
prompt: constrainedPrompt,
systemPrompt: systemPrompt,
maxOutputTokens: maxOutputTokens,
temperature: temperature,
topP: topP,
topK: topK,
preferGpu: preferGpu,
);
return;
}
Expand Down Expand Up @@ -914,6 +937,10 @@ class AssistantRuntimeService {
required String prompt,
String? systemPrompt,
int? maxOutputTokens,
double? temperature,
double? topP,
int? topK,
bool preferGpu = true,
bool emitRequestTrace = true,
String? grammar,
String? assistantPrefill,
Expand All @@ -930,7 +957,7 @@ class AssistantRuntimeService {
),
);
}
await _ensureGgufReady(runtimeId, package);
await _ensureGgufReady(runtimeId, package, preferGpu: preferGpu);
final instructPrompt = formatGgufInstructPrompt(
prompt: prompt,
systemPrompt: systemPrompt,
Expand All @@ -950,6 +977,9 @@ class AssistantRuntimeService {
await for (final token in _llamaGguf.generate(
prompt: instructPrompt,
maxTokens: maxOutputTokens ?? ggufMaxOutputTokens(package),
temperature: temperature ?? 0.7,
topP: topP ?? 0.9,
topK: topK ?? 40,
grammar: grammar,
)) {
accumulated += token;
Expand Down Expand Up @@ -997,8 +1027,9 @@ class AssistantRuntimeService {

Future<void> _ensureGgufReady(
String runtimeId,
OfflineModelInfo package,
) async {
OfflineModelInfo package, {
bool preferGpu = true,
}) async {
if (!await _llamaGguf.isAvailable()) {
throw AssistantRuntimeUnavailableException(
runtimeId,
Expand All @@ -1008,6 +1039,7 @@ class AssistantRuntimeService {
final loaded = await _llamaGguf.loadModelOutcome(
package,
contextSize: _effectiveContextLength(package),
preferGpu: preferGpu,
);
if (loaded.succeeded) return;
final copy = GgufLoadDiagnostics.describe(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:equatable/equatable.dart';

/// CPU vs GPU preference for on-device chat generation.
enum ChatAccelerator { cpu, gpu }

/// Per-conversation sampling and prompt overrides for the chat screen.
class ChatModelConfig extends Equatable {
const ChatModelConfig({
required this.maxTokens,
required this.topK,
required this.topP,
required this.temperature,
required this.accelerator,
required this.systemPrompt,
});

static const int minMaxTokens = 64;
static const int maxMaxTokens = 8192;
static const int minTopK = 1;
static const int maxTopK = 128;
static const double minTopP = 0;
static const double maxTopP = 1;
static const double minTemperature = 0;
static const double maxTemperature = 2;

/// Defaults match the on-device Gemma gallery config the chat UI copies.
static const ChatModelConfig defaults = ChatModelConfig(
maxTokens: 4000,
topK: 1,
topP: 0.95,
temperature: 1,
accelerator: ChatAccelerator.gpu,
systemPrompt: '',
);

final int maxTokens;
final int topK;
final double topP;
final double temperature;
final ChatAccelerator accelerator;
final String systemPrompt;

bool get preferGpu => accelerator == ChatAccelerator.gpu;

ChatModelConfig copyWith({
int? maxTokens,
int? topK,
double? topP,
double? temperature,
ChatAccelerator? accelerator,
String? systemPrompt,
}) {
return ChatModelConfig(
maxTokens: maxTokens ?? this.maxTokens,
topK: topK ?? this.topK,
topP: topP ?? this.topP,
temperature: temperature ?? this.temperature,
accelerator: accelerator ?? this.accelerator,
systemPrompt: systemPrompt ?? this.systemPrompt,
).normalized();
}

ChatModelConfig normalized() {
return ChatModelConfig(
maxTokens: maxTokens.clamp(minMaxTokens, maxMaxTokens).toInt(),
topK: topK.clamp(minTopK, maxTopK).toInt(),
topP: topP.clamp(minTopP, maxTopP).toDouble(),
temperature: temperature.clamp(minTemperature, maxTemperature).toDouble(),
accelerator: accelerator,
systemPrompt: systemPrompt,
);
}

/// Prepends a user-authored system prompt without replacing assembled context.
String mergeSystemPrompt(String assembled) {
final custom = systemPrompt.trim();
final existing = assembled.trim();
if (custom.isEmpty) return assembled;
if (existing.isEmpty) return custom;
return '$custom\n\n$assembled';
}

@override
List<Object?> get props => [
maxTokens,
topK,
topP,
temperature,
accelerator,
systemPrompt,
];
}
Loading
Loading