chore: establish Sovereign AI reference

This commit is contained in:
Hamza Ayed
2026-09-29 22:38:37 +03:00
commit ce1b22f100
147 changed files with 7394 additions and 0 deletions
@@ -0,0 +1,214 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:uuid/uuid.dart';
import '../../features/chat/presentation/cubit/chat_state.dart';
const _uuid = Uuid();
class ApiRepository {
ApiRepository({required String baseUrl}) : _baseUrl = baseUrl;
String _baseUrl;
String? _cachedUserId;
Future<String>? _loadingUserId;
String get baseUrl => _baseUrl;
String newId() => _uuid.v4();
void setBaseUrl(String value) =>
_baseUrl = value.replaceFirst(RegExp(r'/+$'), '');
Future<String> _userId() async {
final cached = _cachedUserId;
if (cached != null) return cached;
return _loadingUserId ??= _loadLocalUserId();
}
Future<String> _loadLocalUserId() async {
final response = await http.get(Uri.parse('$_baseUrl/v1/local-user'));
_checkStatus(response);
final data = jsonDecode(response.body) as Map<String, dynamic>;
_cachedUserId = data['user_id'] as String;
return _cachedUserId!;
}
Future<Map<String, String>> _userHeaders() async => {
'Content-Type': 'application/json',
'X-User-ID': await _userId(),
};
void _checkStatus(http.Response response) {
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception('API ${response.statusCode}: ${response.body}');
}
}
Future<List<SavedConversation>> listConversations() async {
final response = await http.get(
Uri.parse('$_baseUrl/v1/conversations'),
headers: await _userHeaders(),
);
_checkStatus(response);
return (jsonDecode(response.body) as List<dynamic>)
.map(
(item) =>
SavedConversation.fromSummaryJson(item as Map<String, dynamic>),
)
.toList();
}
Future<SavedConversation> getConversation(String id) async {
final response = await http.get(
Uri.parse('$_baseUrl/v1/conversations/$id'),
headers: await _userHeaders(),
);
_checkStatus(response);
final data = jsonDecode(response.body) as Map<String, dynamic>;
return SavedConversation(
id: data['id'] as String,
title: data['title'] as String,
updatedAt: DateTime.parse(data['updated_at'] as String).toLocal(),
messages:
(data['messages'] as List<dynamic>).map((item) {
final message = item as Map<String, dynamic>;
return ChatMessage(
message['content'] as String,
fromUser: message['role'] == 'user',
);
}).toList(),
);
}
Future<void> saveConversation(SavedConversation conversation) async {
final response = await http.put(
Uri.parse('$_baseUrl/v1/conversations/${conversation.id}'),
headers: await _userHeaders(),
body: jsonEncode({
'title': conversation.title,
'messages':
conversation.messages
.map(
(message) => {
'role': message.fromUser ? 'user' : 'assistant',
'content': message.text,
},
)
.toList(),
}),
);
_checkStatus(response);
}
Future<void> deleteConversation(String id) async {
final response = await http.delete(
Uri.parse('$_baseUrl/v1/conversations/$id'),
headers: await _userHeaders(),
);
_checkStatus(response);
}
Future<String> complete(List<ChatMessage> messages) async {
final response = await http
.post(
Uri.parse('$_baseUrl/v1/chat/completions'),
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({
'messages':
messages
.map(
(message) => {
'role': message.fromUser ? 'user' : 'assistant',
'content': message.text,
},
)
.toList(),
}),
)
.timeout(const Duration(minutes: 3));
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception('API ${response.statusCode}: ${response.body}');
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final choices = data['choices'] as List<dynamic>;
final message = choices.first['message'] as Map<String, dynamic>;
return message['content'] as String? ?? 'لم يصل نص في الإجابة.';
}
Stream<String> completeStream(List<ChatMessage> messages) async* {
final client = http.Client();
try {
final request =
http.Request('POST', Uri.parse('$_baseUrl/v1/chat/stream'))
..headers.addAll({
'Content-Type': 'application/json',
'Accept': 'application/x-ndjson',
})
..body = jsonEncode({
'messages':
messages
.map(
(message) => {
'role': message.fromUser ? 'user' : 'assistant',
'content': message.text,
},
)
.toList(),
});
final response = await client
.send(request)
.timeout(const Duration(minutes: 3));
if (response.statusCode < 200 || response.statusCode >= 300) {
final body = await response.stream.bytesToString();
throw Exception('API ${response.statusCode}: $body');
}
var completed = false;
await for (final line in response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.timeout(const Duration(minutes: 3))) {
if (line.isEmpty) continue;
final event = jsonDecode(line) as Map<String, dynamic>;
if (event['error'] != null) {
throw Exception(event['error']);
}
if (event['done'] == true) {
completed = true;
break;
}
final delta = event['delta'] as String?;
if (delta != null && delta.isNotEmpty) yield delta;
}
if (!completed) throw Exception('انقطع بث الإجابة قبل اكتماله.');
} finally {
client.close();
}
}
Future<String> transcribe(Uint8List wavBytes) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$_baseUrl/v1/audio/transcriptions'),
)
..files.add(
http.MultipartFile.fromBytes(
'file',
wavBytes,
filename: 'recording.wav',
contentType: MediaType('audio', 'wav'),
),
);
final streamed = await request.send().timeout(const Duration(minutes: 3));
final response = await http.Response.fromStream(streamed);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception(
'Transcription API ${response.statusCode}: ${response.body}',
);
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
return data['text'] as String? ?? '';
}
}