Add ai
This commit is contained in:
@@ -105,7 +105,6 @@ The app can automatically parse SMS messages from banks to create transactions:
|
||||
|
||||
### Performance Considerations
|
||||
- Create small, focused widgets that can be efficiently rebuilt
|
||||
- Use `const` constructors where possible
|
||||
- Implement proper `shouldRebuild` logic in BlocBuilder/BlocListener
|
||||
- Avoid heavy operations in build methods
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:budget_app/models/prefilled_transaction.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
|
||||
import '../../models/ai_rule.dart';
|
||||
import '../../models/ai_settings.dart';
|
||||
import '../../models/app_settings.dart';
|
||||
import '../../models/category.dart';
|
||||
import '../../models/global_settings.dart';
|
||||
@@ -43,6 +44,7 @@ class HiveService {
|
||||
static late Box<SmsMessage> smsMessages;
|
||||
static late Box<AiRule> aiRules;
|
||||
static late Box<PrefilledTransaction> prefilledTransactions;
|
||||
static late Box<AiSettings> aiSettings;
|
||||
|
||||
/// Инициализация Hive Box для конкретного пользователя.
|
||||
/// [userId] - Уникальный идентификатор пользователя.
|
||||
@@ -61,6 +63,7 @@ class HiveService {
|
||||
smsMessages = await Hive.openBox<SmsMessage>('sms_messages_\$userId');
|
||||
aiRules = await Hive.openBox<AiRule>('ai_rules_\$userId');
|
||||
prefilledTransactions = await Hive.openBox<PrefilledTransaction>('prefilled_transactions_\$userId');
|
||||
aiSettings = await Hive.openBox<AiSettings>('ai_settings_\$userId');
|
||||
}
|
||||
|
||||
/// Закрытие пользовательских Hive Box.
|
||||
@@ -74,5 +77,6 @@ class HiveService {
|
||||
await smsMessages.close();
|
||||
await aiRules.close();
|
||||
await prefilledTransactions.close();
|
||||
await aiSettings.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '/models/ai_settings.dart';
|
||||
import 'interfaces/iai_settings_repository.dart';
|
||||
|
||||
class HiveAiSettingsRepository implements IAiSettingsRepository {
|
||||
final Box<AiSettings> _box;
|
||||
|
||||
HiveAiSettingsRepository(this._box);
|
||||
|
||||
static const String _settingsKey = 'ai_settings';
|
||||
|
||||
@override
|
||||
AiSettings? getSettings() {
|
||||
return _box.get(_settingsKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSettings(AiSettings settings) async {
|
||||
await _box.put(_settingsKey, settings);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteSettings() async {
|
||||
await _box.delete(_settingsKey);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hasSettings() {
|
||||
return _box.containsKey(_settingsKey);
|
||||
}
|
||||
|
||||
@override
|
||||
AiSettings getDefaultSettings() {
|
||||
return AiSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import '/models/ai_settings.dart';
|
||||
|
||||
abstract class IAiSettingsRepository {
|
||||
/// Получить настройки ИИ
|
||||
AiSettings? getSettings();
|
||||
|
||||
/// Сохранить настройки ИИ
|
||||
Future<void> saveSettings(AiSettings settings);
|
||||
|
||||
/// Удалить настройки ИИ
|
||||
Future<void> deleteSettings();
|
||||
|
||||
/// Проверить существование настроек
|
||||
bool hasSettings();
|
||||
|
||||
/// Получить настройки по умолчанию
|
||||
AiSettings getDefaultSettings();
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:budget_app/hive/hive_adapters.dart';
|
||||
import 'package:budget_app/models/ai_rule.dart';
|
||||
import 'package:budget_app/models/ai_settings.dart';
|
||||
import 'package:budget_app/models/app_settings.dart';
|
||||
import 'package:budget_app/models/category.dart';
|
||||
import 'package:budget_app/models/global_settings.dart';
|
||||
@@ -19,6 +20,7 @@ extension HiveRegistrar on HiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AiRuleAdapter());
|
||||
registerAdapter(AiRuleTypeAdapter());
|
||||
registerAdapter(AiSettingsAdapter());
|
||||
registerAdapter(AppSettingsAdapter());
|
||||
registerAdapter(CategoryAdapter());
|
||||
registerAdapter(GlobalSettingsAdapter());
|
||||
@@ -40,6 +42,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AiRuleAdapter());
|
||||
registerAdapter(AiRuleTypeAdapter());
|
||||
registerAdapter(AiSettingsAdapter());
|
||||
registerAdapter(AppSettingsAdapter());
|
||||
registerAdapter(CategoryAdapter());
|
||||
registerAdapter(GlobalSettingsAdapter());
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
|
||||
|
||||
import 'data/database/hive_service.dart';
|
||||
import 'data/repositories/hive_ai_rule_repository.dart';
|
||||
import 'data/repositories/hive_ai_settings_repository.dart';
|
||||
import 'data/repositories/hive_category_repository.dart';
|
||||
import 'data/repositories/hive_global_settings_repository.dart';
|
||||
import 'data/repositories/hive_settings_repository.dart';
|
||||
@@ -14,6 +15,7 @@ import 'data/repositories/hive_tag_repository.dart';
|
||||
import 'data/repositories/hive_transaction_repository.dart';
|
||||
import 'data/repositories/hive_user_repository.dart';
|
||||
import 'data/repositories/interfaces/iai_rule_repository.dart';
|
||||
import 'data/repositories/interfaces/iai_settings_repository.dart';
|
||||
import 'data/repositories/interfaces/icategory_repository.dart';
|
||||
import 'data/repositories/interfaces/iglobal_settings_repository.dart';
|
||||
import 'data/repositories/interfaces/isettings_repository.dart';
|
||||
@@ -22,6 +24,7 @@ import 'data/repositories/interfaces/isms_message_repository.dart';
|
||||
import 'data/repositories/interfaces/itag_repository.dart';
|
||||
import 'data/repositories/interfaces/itransaction_repository.dart';
|
||||
import 'data/repositories/interfaces/iuser_repository.dart';
|
||||
import 'logic/ai/ai_cubit.dart';
|
||||
import 'logic/ai_rules/ai_rules_bloc.dart';
|
||||
import 'logic/auth/auth_bloc.dart';
|
||||
import 'logic/category/category_cubit.dart';
|
||||
@@ -30,9 +33,11 @@ import 'logic/sms/sms_settings_cubit.dart';
|
||||
import 'logic/tag/tag_cubit.dart';
|
||||
import 'logic/transaction/transaction_bloc.dart';
|
||||
import 'logic/user/user_cubit.dart';
|
||||
import 'services/openrouter_ai_service.dart';
|
||||
import 'services/sms_service.dart';
|
||||
import 'services/sms_transaction_service.dart';
|
||||
import 'services/user_service.dart';
|
||||
import 'services/interfaces/iai_service.dart';
|
||||
import 'services/interfaces/iuser_service.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
@@ -148,12 +153,30 @@ Future<void> initUserSpecificDependencies(String userId) async {
|
||||
() => HivePrefilledTransactionRepository(HiveService.prefilledTransactions),
|
||||
);
|
||||
|
||||
// Настройки ИИ
|
||||
if (getIt.isRegistered<IAiSettingsRepository>()) {
|
||||
await getIt.unregister<IAiSettingsRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<IAiSettingsRepository>(
|
||||
() => HiveAiSettingsRepository(HiveService.aiSettings),
|
||||
);
|
||||
|
||||
// SMS Service
|
||||
if (getIt.isRegistered<SmsService>()) {
|
||||
await getIt.unregister<SmsService>();
|
||||
}
|
||||
getIt.registerSingleton<SmsService>(SmsService());
|
||||
|
||||
// AI Service
|
||||
if (getIt.isRegistered<IAiService>()) {
|
||||
await getIt.unregister<IAiService>();
|
||||
}
|
||||
getIt.registerLazySingleton<IAiService>(() {
|
||||
final aiSettings = getIt<IAiSettingsRepository>().getSettings() ??
|
||||
getIt<IAiSettingsRepository>().getDefaultSettings();
|
||||
return OpenRouterAiService(aiSettings);
|
||||
});
|
||||
|
||||
// --- Обновление UserCubit новыми репозиториями ---
|
||||
// Мы не пересоздаем UserCubit, а просто обновляем его зависимости.
|
||||
final userCubit = getIt<UserCubit>();
|
||||
@@ -231,6 +254,17 @@ Future<void> initUserSpecificDependencies(String userId) async {
|
||||
getIt<IPrefilledTransactionRepository>(),
|
||||
),
|
||||
);
|
||||
|
||||
// AI Cubit
|
||||
if (getIt.isRegistered<AiCubit>()) {
|
||||
await getIt.unregister<AiCubit>();
|
||||
}
|
||||
getIt.registerFactory<AiCubit>(
|
||||
() => AiCubit(
|
||||
settingsRepository: getIt<IAiSettingsRepository>(),
|
||||
aiService: getIt<IAiService>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Сброс пользовательских зависимостей при выходе из системы.
|
||||
@@ -252,4 +286,8 @@ Future<void> resetUserSpecificDependencies() async {
|
||||
await getIt.unregister<CategoryCubit>();
|
||||
await getIt.unregister<TagCubit>();
|
||||
await getIt.unregister<IAiRuleRepository>();
|
||||
await getIt.unregister<IAiSettingsRepository>();
|
||||
if (getIt.isRegistered<IAiService>()) {
|
||||
await getIt.unregister<IAiService>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import '/models/ai_settings.dart';
|
||||
import '/data/repositories/interfaces/iai_settings_repository.dart';
|
||||
import '/services/interfaces/iai_service.dart';
|
||||
|
||||
part 'ai_state.dart';
|
||||
|
||||
class AiCubit extends Cubit<AiState> {
|
||||
final IAiSettingsRepository _settingsRepository;
|
||||
final IAiService _aiService;
|
||||
final Logger _logger = Logger();
|
||||
|
||||
AiCubit({
|
||||
required IAiSettingsRepository settingsRepository,
|
||||
required IAiService aiService,
|
||||
}) : _settingsRepository = settingsRepository,
|
||||
_aiService = aiService,
|
||||
super(AiInitial()) {
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
try {
|
||||
final settings = _settingsRepository.getSettings();
|
||||
if (settings != null) {
|
||||
_aiService.setApiKey(settings.apiKey ?? '');
|
||||
emit(AiLoaded(settings: settings));
|
||||
} else {
|
||||
final defaultSettings = _settingsRepository.getDefaultSettings();
|
||||
emit(AiLoaded(settings: defaultSettings));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка загрузки настроек ИИ: $e');
|
||||
emit(AiError(message: 'Ошибка загрузки настроек ИИ: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateSettings(AiSettings settings) async {
|
||||
try {
|
||||
emit(AiLoading());
|
||||
await _settingsRepository.saveSettings(settings);
|
||||
_aiService.setApiKey(settings.apiKey ?? '');
|
||||
emit(AiLoaded(settings: settings));
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка сохранения настроек ИИ: $e');
|
||||
emit(AiError(message: 'Ошибка сохранения настроек: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String message) async {
|
||||
final currentState = state;
|
||||
if (currentState is! AiLoaded) {
|
||||
emit(AiError(message: 'Настройки ИИ не загружены'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentState.settings.isEnabled) {
|
||||
emit(AiError(message: 'ИИ сервис отключен в настройках'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_aiService.isConfigured()) {
|
||||
emit(AiError(message: 'API ключ не настроен'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
emit(AiLoading());
|
||||
final response = await _aiService.sendMessage(message);
|
||||
emit(AiLoaded(
|
||||
settings: currentState.settings,
|
||||
lastResponse: response,
|
||||
lastMessage: message,
|
||||
));
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка отправки сообщения ИИ: $e');
|
||||
emit(AiError(message: e.toString()));
|
||||
emit(AiLoaded(settings: currentState.settings));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessageWithParams({
|
||||
required String message,
|
||||
String? model,
|
||||
double? temperature,
|
||||
int? maxTokens,
|
||||
}) async {
|
||||
final currentState = state;
|
||||
if (currentState is! AiLoaded) {
|
||||
emit(AiError(message: 'Настройки ИИ не загружены'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentState.settings.isEnabled) {
|
||||
emit(AiError(message: 'ИИ сервис отключен в настройках'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_aiService.isConfigured()) {
|
||||
emit(AiError(message: 'API ключ не настроен'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
emit(AiLoading());
|
||||
final response = await _aiService.sendMessageWithParams(
|
||||
message: message,
|
||||
model: model,
|
||||
temperature: temperature,
|
||||
maxTokens: maxTokens,
|
||||
);
|
||||
emit(AiLoaded(
|
||||
settings: currentState.settings,
|
||||
lastResponse: response,
|
||||
lastMessage: message,
|
||||
));
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка отправки сообщения ИИ с параметрами: $e');
|
||||
emit(AiError(message: e.toString()));
|
||||
emit(AiLoaded(settings: currentState.settings));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkHealth() async {
|
||||
final currentState = state;
|
||||
if (currentState is! AiLoaded) {
|
||||
emit(AiError(message: 'Настройки ИИ не загружены'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
emit(AiLoading());
|
||||
final isHealthy = await _aiService.checkHealth();
|
||||
emit(AiLoaded(
|
||||
settings: currentState.settings,
|
||||
isHealthy: isHealthy,
|
||||
));
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка проверки здоровья ИИ сервиса: $e');
|
||||
emit(AiError(message: 'Ошибка проверки соединения: $e'));
|
||||
emit(AiLoaded(settings: currentState.settings));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadAvailableModels() async {
|
||||
final currentState = state;
|
||||
if (currentState is! AiLoaded) {
|
||||
emit(AiError(message: 'Настройки ИИ не загружены'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_aiService.isConfigured()) {
|
||||
emit(AiError(message: 'API ключ не настроен'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
emit(AiLoading());
|
||||
final models = await _aiService.getAvailableModels();
|
||||
emit(AiLoaded(
|
||||
settings: currentState.settings,
|
||||
availableModels: models,
|
||||
));
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка загрузки доступных моделей: $e');
|
||||
emit(AiError(message: 'Ошибка загрузки моделей: $e'));
|
||||
emit(AiLoaded(settings: currentState.settings));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetSettings() async {
|
||||
try {
|
||||
emit(AiLoading());
|
||||
await _settingsRepository.deleteSettings();
|
||||
final defaultSettings = _settingsRepository.getDefaultSettings();
|
||||
_aiService.setApiKey('');
|
||||
emit(AiLoaded(settings: defaultSettings));
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка сброса настроек ИИ: $e');
|
||||
emit(AiError(message: 'Ошибка сброса настроек: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
part of 'ai_cubit.dart';
|
||||
|
||||
abstract class AiState extends Equatable {
|
||||
const AiState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class AiInitial extends AiState {}
|
||||
|
||||
class AiLoading extends AiState {}
|
||||
|
||||
class AiLoaded extends AiState {
|
||||
final AiSettings settings;
|
||||
final String? lastResponse;
|
||||
final String? lastMessage;
|
||||
final bool? isHealthy;
|
||||
final List<String>? availableModels;
|
||||
|
||||
const AiLoaded({
|
||||
required this.settings,
|
||||
this.lastResponse,
|
||||
this.lastMessage,
|
||||
this.isHealthy,
|
||||
this.availableModels,
|
||||
});
|
||||
|
||||
AiLoaded copyWith({
|
||||
AiSettings? settings,
|
||||
String? lastResponse,
|
||||
String? lastMessage,
|
||||
bool? isHealthy,
|
||||
List<String>? availableModels,
|
||||
}) {
|
||||
return AiLoaded(
|
||||
settings: settings ?? this.settings,
|
||||
lastResponse: lastResponse ?? this.lastResponse,
|
||||
lastMessage: lastMessage ?? this.lastMessage,
|
||||
isHealthy: isHealthy ?? this.isHealthy,
|
||||
availableModels: availableModels ?? this.availableModels,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
settings,
|
||||
lastResponse,
|
||||
lastMessage,
|
||||
isHealthy,
|
||||
availableModels,
|
||||
];
|
||||
}
|
||||
|
||||
class AiError extends AiState {
|
||||
final String message;
|
||||
|
||||
const AiError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
part 'ai_settings.g.dart';
|
||||
|
||||
@HiveType(typeId: 9)
|
||||
class AiSettings extends HiveObject {
|
||||
@HiveField(0)
|
||||
String? apiKey;
|
||||
|
||||
@HiveField(1)
|
||||
String baseUrl;
|
||||
|
||||
@HiveField(2)
|
||||
String defaultModel;
|
||||
|
||||
@HiveField(3)
|
||||
int timeoutSeconds;
|
||||
|
||||
@HiveField(4)
|
||||
double temperature;
|
||||
|
||||
@HiveField(5)
|
||||
int maxTokens;
|
||||
|
||||
@HiveField(6)
|
||||
bool isEnabled;
|
||||
|
||||
AiSettings({
|
||||
this.apiKey,
|
||||
this.baseUrl = 'https://openrouter.ai/api/v1',
|
||||
this.defaultModel = 'openai/gpt-3.5-turbo',
|
||||
this.timeoutSeconds = 30,
|
||||
this.temperature = 0.7,
|
||||
this.maxTokens = 1000,
|
||||
this.isEnabled = false,
|
||||
});
|
||||
|
||||
AiSettings copyWith({
|
||||
String? apiKey,
|
||||
String? baseUrl,
|
||||
String? defaultModel,
|
||||
int? timeoutSeconds,
|
||||
double? temperature,
|
||||
int? maxTokens,
|
||||
bool? isEnabled,
|
||||
}) {
|
||||
return AiSettings(
|
||||
apiKey: apiKey ?? this.apiKey,
|
||||
baseUrl: baseUrl ?? this.baseUrl,
|
||||
defaultModel: defaultModel ?? this.defaultModel,
|
||||
timeoutSeconds: timeoutSeconds ?? this.timeoutSeconds,
|
||||
temperature: temperature ?? this.temperature,
|
||||
maxTokens: maxTokens ?? this.maxTokens,
|
||||
isEnabled: isEnabled ?? this.isEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AiSettings(baseUrl: $baseUrl, defaultModel: $defaultModel, timeoutSeconds: $timeoutSeconds, temperature: $temperature, maxTokens: $maxTokens, isEnabled: $isEnabled)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ai_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AiSettingsAdapter extends TypeAdapter<AiSettings> {
|
||||
@override
|
||||
final typeId = 9;
|
||||
|
||||
@override
|
||||
AiSettings read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AiSettings(
|
||||
apiKey: fields[0] as String?,
|
||||
baseUrl: fields[1] == null
|
||||
? 'https://openrouter.ai/api/v1'
|
||||
: fields[1] as String,
|
||||
defaultModel: fields[2] == null
|
||||
? 'openai/gpt-3.5-turbo'
|
||||
: fields[2] as String,
|
||||
timeoutSeconds: fields[3] == null ? 30 : (fields[3] as num).toInt(),
|
||||
temperature: fields[4] == null ? 0.7 : (fields[4] as num).toDouble(),
|
||||
maxTokens: fields[5] == null ? 1000 : (fields[5] as num).toInt(),
|
||||
isEnabled: fields[6] == null ? false : fields[6] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AiSettings obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.apiKey)
|
||||
..writeByte(1)
|
||||
..write(obj.baseUrl)
|
||||
..writeByte(2)
|
||||
..write(obj.defaultModel)
|
||||
..writeByte(3)
|
||||
..write(obj.timeoutSeconds)
|
||||
..writeByte(4)
|
||||
..write(obj.temperature)
|
||||
..writeByte(5)
|
||||
..write(obj.maxTokens)
|
||||
..writeByte(6)
|
||||
..write(obj.isEnabled);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AiSettingsAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
abstract class IAiService {
|
||||
/// Отправить текстовый запрос к ИИ и получить ответ
|
||||
Future<String> sendMessage(String message);
|
||||
|
||||
/// Отправить запрос с настраиваемыми параметрами
|
||||
Future<String> sendMessageWithParams({
|
||||
required String message,
|
||||
String? model,
|
||||
double? temperature,
|
||||
int? maxTokens,
|
||||
});
|
||||
|
||||
/// Проверить доступность сервиса
|
||||
Future<bool> checkHealth();
|
||||
|
||||
/// Получить список доступных моделей
|
||||
Future<List<String>> getAvailableModels();
|
||||
|
||||
/// Настроить API ключ
|
||||
void setApiKey(String apiKey);
|
||||
|
||||
/// Проверить настройки соединения
|
||||
bool isConfigured();
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logger/logger.dart';
|
||||
import '/models/ai_settings.dart';
|
||||
import 'interfaces/iai_service.dart';
|
||||
|
||||
class OpenRouterAiService implements IAiService {
|
||||
final Logger _logger = Logger();
|
||||
final AiSettings _settings;
|
||||
String? _apiKey;
|
||||
|
||||
OpenRouterAiService(this._settings) {
|
||||
_apiKey = _settings.apiKey;
|
||||
}
|
||||
|
||||
@override
|
||||
void setApiKey(String apiKey) {
|
||||
_apiKey = apiKey;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConfigured() {
|
||||
return _apiKey != null && _apiKey!.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sendMessage(String message) async {
|
||||
return sendMessageWithParams(
|
||||
message: message,
|
||||
model: _settings.defaultModel,
|
||||
temperature: _settings.temperature,
|
||||
maxTokens: _settings.maxTokens,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sendMessageWithParams({
|
||||
required String message,
|
||||
String? model,
|
||||
double? temperature,
|
||||
int? maxTokens,
|
||||
}) async {
|
||||
if (!isConfigured()) {
|
||||
throw Exception('OpenRouter API ключ не настроен');
|
||||
}
|
||||
|
||||
if (!_settings.isEnabled) {
|
||||
throw Exception('ИИ сервис отключен в настройках');
|
||||
}
|
||||
|
||||
try {
|
||||
final url = Uri.parse('${_settings.baseUrl}/chat/completions');
|
||||
|
||||
final body = {
|
||||
'model': model ?? _settings.defaultModel,
|
||||
'messages': [
|
||||
{
|
||||
'role': 'user',
|
||||
'content': message,
|
||||
}
|
||||
],
|
||||
'temperature': temperature ?? _settings.temperature,
|
||||
'max_tokens': maxTokens ?? _settings.maxTokens,
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $_apiKey',
|
||||
'HTTP-Referer': 'https://github.com/your-repo',
|
||||
'X-Title': 'Budget App',
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
).timeout(Duration(seconds: _settings.timeoutSeconds));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (responseData['choices'] != null &&
|
||||
responseData['choices'].isNotEmpty &&
|
||||
responseData['choices'][0]['message'] != null) {
|
||||
return responseData['choices'][0]['message']['content'] ?? 'Пустой ответ от ИИ';
|
||||
} else {
|
||||
throw Exception('Неверный формат ответа от OpenRouter');
|
||||
}
|
||||
} else {
|
||||
final errorData = jsonDecode(response.body);
|
||||
final errorMessage = errorData['error']?['message'] ?? 'Неизвестная ошибка';
|
||||
throw Exception('Ошибка OpenRouter API (${response.statusCode}): $errorMessage');
|
||||
}
|
||||
} on SocketException {
|
||||
throw Exception('Нет подключения к интернету');
|
||||
} on http.ClientException {
|
||||
throw Exception('Ошибка HTTP клиента');
|
||||
} on FormatException {
|
||||
throw Exception('Ошибка парсинга ответа');
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка при отправке запроса к OpenRouter: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> checkHealth() async {
|
||||
if (!isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final url = Uri.parse('${_settings.baseUrl}/models');
|
||||
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $_apiKey',
|
||||
},
|
||||
).timeout(Duration(seconds: 10));
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
_logger.w('Проверка здоровья OpenRouter API не удалась: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> getAvailableModels() async {
|
||||
if (!isConfigured()) {
|
||||
throw Exception('OpenRouter API ключ не настроен');
|
||||
}
|
||||
|
||||
try {
|
||||
final url = Uri.parse('${_settings.baseUrl}/models');
|
||||
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $_apiKey',
|
||||
},
|
||||
).timeout(Duration(seconds: _settings.timeoutSeconds));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (responseData['data'] != null) {
|
||||
final models = <String>[];
|
||||
for (final model in responseData['data']) {
|
||||
if (model['id'] != null) {
|
||||
models.add(model['id'] as String);
|
||||
}
|
||||
}
|
||||
return models;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
throw Exception('Ошибка получения моделей (${response.statusCode})');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('Ошибка при получении списка моделей: $e');
|
||||
return [
|
||||
'openai/gpt-3.5-turbo',
|
||||
'openai/gpt-4',
|
||||
'anthropic/claude-3-haiku',
|
||||
'anthropic/claude-3-sonnet',
|
||||
'meta-llama/llama-3.1-8b-instruct',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -457,7 +457,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
|
||||
@@ -53,6 +53,7 @@ dependencies:
|
||||
freezed_annotation: ^3.1.0
|
||||
json_annotation: ^4.8.1
|
||||
crypto: ^3.0.3
|
||||
http: ^1.1.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user