This commit is contained in:
16
lib/domain/entities/note_chunk.dart
Normal file
16
lib/domain/entities/note_chunk.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'note_chunk.freezed.dart';
|
||||
|
||||
/// A single text chunk produced by splitting a trainer's note.
|
||||
/// [sourceId] groups all chunks that came from the same original note
|
||||
/// (useful for bulk deletion later).
|
||||
@freezed
|
||||
class NoteChunkEntity with _$NoteChunkEntity {
|
||||
const factory NoteChunkEntity({
|
||||
required String id,
|
||||
required String text,
|
||||
required String sourceId,
|
||||
required String createdAt,
|
||||
}) = _NoteChunkEntity;
|
||||
}
|
||||
215
lib/domain/entities/note_chunk.freezed.dart
Normal file
215
lib/domain/entities/note_chunk.freezed.dart
Normal file
@@ -0,0 +1,215 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'note_chunk.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
/// @nodoc
|
||||
mixin _$NoteChunkEntity {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get text => throw _privateConstructorUsedError;
|
||||
String get sourceId => throw _privateConstructorUsedError;
|
||||
String get createdAt => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of NoteChunkEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$NoteChunkEntityCopyWith<NoteChunkEntity> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $NoteChunkEntityCopyWith<$Res> {
|
||||
factory $NoteChunkEntityCopyWith(
|
||||
NoteChunkEntity value,
|
||||
$Res Function(NoteChunkEntity) then,
|
||||
) = _$NoteChunkEntityCopyWithImpl<$Res, NoteChunkEntity>;
|
||||
@useResult
|
||||
$Res call({String id, String text, String sourceId, String createdAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$NoteChunkEntityCopyWithImpl<$Res, $Val extends NoteChunkEntity>
|
||||
implements $NoteChunkEntityCopyWith<$Res> {
|
||||
_$NoteChunkEntityCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of NoteChunkEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? text = null,
|
||||
Object? sourceId = null,
|
||||
Object? createdAt = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
text: null == text
|
||||
? _value.text
|
||||
: text // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
sourceId: null == sourceId
|
||||
? _value.sourceId
|
||||
: sourceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$NoteChunkEntityImplCopyWith<$Res>
|
||||
implements $NoteChunkEntityCopyWith<$Res> {
|
||||
factory _$$NoteChunkEntityImplCopyWith(
|
||||
_$NoteChunkEntityImpl value,
|
||||
$Res Function(_$NoteChunkEntityImpl) then,
|
||||
) = __$$NoteChunkEntityImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String id, String text, String sourceId, String createdAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$NoteChunkEntityImplCopyWithImpl<$Res>
|
||||
extends _$NoteChunkEntityCopyWithImpl<$Res, _$NoteChunkEntityImpl>
|
||||
implements _$$NoteChunkEntityImplCopyWith<$Res> {
|
||||
__$$NoteChunkEntityImplCopyWithImpl(
|
||||
_$NoteChunkEntityImpl _value,
|
||||
$Res Function(_$NoteChunkEntityImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of NoteChunkEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? text = null,
|
||||
Object? sourceId = null,
|
||||
Object? createdAt = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$NoteChunkEntityImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
text: null == text
|
||||
? _value.text
|
||||
: text // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
sourceId: null == sourceId
|
||||
? _value.sourceId
|
||||
: sourceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$NoteChunkEntityImpl implements _NoteChunkEntity {
|
||||
const _$NoteChunkEntityImpl({
|
||||
required this.id,
|
||||
required this.text,
|
||||
required this.sourceId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String text;
|
||||
@override
|
||||
final String sourceId;
|
||||
@override
|
||||
final String createdAt;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NoteChunkEntity(id: $id, text: $text, sourceId: $sourceId, createdAt: $createdAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$NoteChunkEntityImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.text, text) || other.text == text) &&
|
||||
(identical(other.sourceId, sourceId) ||
|
||||
other.sourceId == sourceId) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, text, sourceId, createdAt);
|
||||
|
||||
/// Create a copy of NoteChunkEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$NoteChunkEntityImplCopyWith<_$NoteChunkEntityImpl> get copyWith =>
|
||||
__$$NoteChunkEntityImplCopyWithImpl<_$NoteChunkEntityImpl>(
|
||||
this,
|
||||
_$identity,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class _NoteChunkEntity implements NoteChunkEntity {
|
||||
const factory _NoteChunkEntity({
|
||||
required final String id,
|
||||
required final String text,
|
||||
required final String sourceId,
|
||||
required final String createdAt,
|
||||
}) = _$NoteChunkEntityImpl;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String get text;
|
||||
@override
|
||||
String get sourceId;
|
||||
@override
|
||||
String get createdAt;
|
||||
|
||||
/// Create a copy of NoteChunkEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$NoteChunkEntityImplCopyWith<_$NoteChunkEntityImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -15,5 +15,11 @@ abstract class AnalysisRepository {
|
||||
required double endTime,
|
||||
required String color,
|
||||
});
|
||||
Future<void> updateAnnotation({
|
||||
required String id,
|
||||
required String name,
|
||||
required String description,
|
||||
required String color,
|
||||
});
|
||||
Future<void> deleteAnnotation(String id);
|
||||
}
|
||||
|
||||
21
lib/domain/repositories/note_repository.dart
Normal file
21
lib/domain/repositories/note_repository.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
/// Persistence interface for the trainer's knowledge base.
|
||||
///
|
||||
/// The implementation splits raw text into chunks, generates embeddings
|
||||
/// via the Nomic server, stores them in the local database, and provides
|
||||
/// semantic search for RAG context injection.
|
||||
abstract class NoteRepository {
|
||||
/// Splits [text] into overlapping chunks, generates an embedding for each,
|
||||
/// and persists them under a shared [sourceId].
|
||||
Future<void> addNote(String text);
|
||||
|
||||
/// Returns the [topK] most semantically similar chunk texts for [query].
|
||||
/// Returns an empty list if no chunks are stored or the embedding server
|
||||
/// is unavailable.
|
||||
Future<List<String>> searchSimilar(String query, {int topK = 3});
|
||||
|
||||
/// Returns the total number of stored chunks.
|
||||
Future<int> getChunkCount();
|
||||
|
||||
/// Deletes every stored chunk (full knowledge-base reset).
|
||||
Future<void> clearAll();
|
||||
}
|
||||
88
lib/domain/services/ai_process_manager.dart
Normal file
88
lib/domain/services/ai_process_manager.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// Manages the two llama.cpp server processes that provide AI features.
|
||||
///
|
||||
/// Both processes are kept alive for the lifetime of the app and must be
|
||||
/// killed on shutdown to prevent zombie processes from consuming RAM.
|
||||
///
|
||||
/// - Qwen 2.5 7B → port 8080 (chat / completions)
|
||||
/// - Nomic Embed → port 8081 (embeddings)
|
||||
class AiProcessManager {
|
||||
Process? _qwenProcess;
|
||||
Process? _nomicProcess;
|
||||
bool _running = false;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
bool get isRunning => _running;
|
||||
|
||||
/// Starts both inference servers. No-ops if already running or if the
|
||||
/// llama-server binary is not present on disk.
|
||||
Future<void> startServers() async {
|
||||
if (_running) return;
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final base = dir.path;
|
||||
final serverBin = p.join(
|
||||
base,
|
||||
Platform.isWindows ? 'llama-server.exe' : 'llama-server',
|
||||
);
|
||||
|
||||
if (!File(serverBin).existsSync()) return;
|
||||
|
||||
try {
|
||||
// ── Qwen 2.5 7B chat server ──────────────────────────────────────────
|
||||
_qwenProcess = await Process.start(
|
||||
serverBin,
|
||||
[
|
||||
'-m', p.join(base, 'qwen2.5-7b-instruct-q4_k_m.gguf'),
|
||||
'--port', '8080',
|
||||
'--ctx-size', '4096',
|
||||
'-ngl', '99', // offload all layers to GPU
|
||||
],
|
||||
runInShell: false,
|
||||
);
|
||||
// Drain pipes so the process is never blocked by a full buffer.
|
||||
_qwenProcess!.stdout.drain<List<int>>();
|
||||
_qwenProcess!.stderr.drain<List<int>>();
|
||||
|
||||
// ── Nomic embedding server ───────────────────────────────────────────
|
||||
_nomicProcess = await Process.start(
|
||||
serverBin,
|
||||
[
|
||||
'-m', p.join(base, 'nomic-embed-text-v1.5.Q4_K_M.gguf'),
|
||||
'--port', '8081',
|
||||
'--ctx-size', '8192',
|
||||
'--embedding',
|
||||
],
|
||||
runInShell: false,
|
||||
);
|
||||
_nomicProcess!.stdout.drain<List<int>>();
|
||||
_nomicProcess!.stderr.drain<List<int>>();
|
||||
|
||||
_running = true;
|
||||
} catch (_) {
|
||||
// Clean up any partially-started processes before rethrowing.
|
||||
_qwenProcess?.kill();
|
||||
_nomicProcess?.kill();
|
||||
_qwenProcess = null;
|
||||
_nomicProcess = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Kills both processes and resets the running flag.
|
||||
/// Safe to call even if servers were never started.
|
||||
Future<void> stopServers() async {
|
||||
_qwenProcess?.kill();
|
||||
_nomicProcess?.kill();
|
||||
_qwenProcess = null;
|
||||
_nomicProcess = null;
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
31
lib/domain/services/embedding_service.dart
Normal file
31
lib/domain/services/embedding_service.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// Wraps the Nomic embedding server (llama.cpp, port 8081).
|
||||
/// Returns a 768-dimensional float vector for any input text.
|
||||
class EmbeddingService {
|
||||
final _dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
),
|
||||
);
|
||||
|
||||
static const _url = 'http://localhost:8081/v1/embeddings';
|
||||
|
||||
/// Returns the embedding vector for [text].
|
||||
/// Throws a [DioException] if the Nomic server is unreachable.
|
||||
Future<List<double>> embed(String text) async {
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
_url,
|
||||
data: {
|
||||
'input': text,
|
||||
'model': 'nomic-embed-text-v1.5.Q4_K_M',
|
||||
},
|
||||
);
|
||||
|
||||
final raw =
|
||||
(response.data!['data'] as List<dynamic>)[0]['embedding']
|
||||
as List<dynamic>;
|
||||
return raw.map((e) => (e as num).toDouble()).toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user