chore: establish Sovereign AI reference
This commit is contained in:
@@ -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? ?? '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user