import 'dart:convert'; import 'package:app/components/conversation_thread/conversation.dart'; import 'package:http/http.dart' as http; import 'package:app/components/conversation_thread/chat.dart'; import 'package:app/util/speech_to_text.dart'; import 'model.dart'; // Uses Singleton design pattern to ensure that any server references across the app are using the same instance. class Backend { // SERVER VARIABLES static final Backend _single_backend = Backend._internal(); Backend._internal(); // TODO: One-time process to establish server connection is required for easy setup/maintenance. static final url = "http://10.0.2.2:11434/api/"; // For emulated device in testing //static final url = "http://192.168.1.68:11434/api"; // For local access to home server static String endpoint = ""; static final Map headers = {'Content-Type': 'application/json'}; // AI VARIABLES static final SpeechToText stt = SpeechToText(); static late bool _speechEnabled; // PRIVACY VARIABLES static bool _historyEnabled = true; static late int conversationID; static late int maxConversationID; static List conversations = []; static List loadedChats = []; // RUNTIME & STATUS VARIABLES static late Model model; static String prompt = ""; static String response = ""; static bool initiated = false; // TODO: Verify all different components of STT, TTS, LLM, etc have been initialized. // TODO: Handshake w/ server to validate identity factory Backend() { // Speech-To-Text engine activation. _speechEnabled = stt.isSpeechEnabled(); // loadedChats = conversations[conversationID].chats; return _single_backend; } void init() { initiated = true; // TODO: Dynamically get available models and models that can be pulled. // TODO: Dynamically get the user's default model model = Model("llama3"); // Primarily used llama3, testing gemma2 // TODO: Dynamically obtain conversations. conversations.add( Conversation.completeConversation( 0, "Test Conversation", "Testing Functionality", [ (Chat("Hello there! You are part of a prototype and are running locally on my Macbook Air. Please keep your responses brief as every prompt freezes my computer.", true)), (Chat("Understood! How can I help?", false)), ] ) ); maxConversationID = conversations.length; // conversations.add( // Conversation.completeConversation( // 1, // "Label test", // "Sub-label Test", // [ // (Chat("A different test. Only reply No.", true)), // (Chat("No.", false)), // ] // ) // ); } // PRIMARY INTERFACE @Deprecated("Use STT interface directly and use respondWhenReady().") static Future sttGetResponseWhenReady() async { // TODO: In case on-device STT is unavailable, use server-side STT service. if (!_speechEnabled) { return "Unable to process Speech-To-Text on-device."; } prompt = stt.getTextWhenReady() as String; conversations[conversationID].add(Chat(prompt, true)); httpSendRequest(); conversations[conversationID].add(Chat(response, false)); return response; } static void respondWhenReady(String p) async { prompt = p; conversations[conversationID].add(Chat(prompt, true)); httpSendRequest(); } // CONVERSATION MANAGEMENT static List getConversationsList() { return conversations; } static Conversation getLoadedConversation() { return conversations[conversationID]; } // Load conversation should be called prior to any other server requests. static void loadConversation(id) { conversationID = id; if(maxConversationID == id) { conversations.add(Conversation(conversationID, "Conversation $id", "Another AI conversation")); maxConversationID++; } loadedChats = conversations[conversationID].chats; } static List getChatsByConversationID(id) { return conversations[id].chats; } // SERVER UTILS static List> convertMessages() { // TODO: Integrate with getConversationByID() List> messageList = []; for (Chat c in loadedChats) { messageList.add({ "role": c.role ? "user" : "assistant", "content": c.content, }); } return messageList; } // Formats the prompt in JSON. static String constructPrompt() { // TODO: Allow for additional flags, ie. continuous conversation, images, etc. // TODO: Allow for toggleable states between stream // TODO: Allow for different models to be dynamically selected. Map data; if(_historyEnabled) { loadedChats = [...loadedChats, Chat(prompt, true)]; endpoint = "chat"; List> messageList = convertMessages(); data = { "model": model.name, "messages": messageList, "stream": false, }; } else { endpoint = "generate"; data = { "model": model.name, "prompt": prompt, "stream": false, }; } // Encode the JSON payload return json.encode(data); } // Send the HTTP request to the server. static void httpSendRequest() async { String json = constructPrompt(); // Send the HTTP POST request final serverResponse = await http.post( Uri.parse('$url$endpoint'), headers: headers, body: json, ); if (serverResponse.statusCode == 200) { Map re = jsonDecode(serverResponse.body); if(_historyEnabled) { response = re["message"]["content"]; loadedChats = [...loadedChats, Chat(response, false)]; } else { response = re["response"]; } } conversations[conversationID].add(Chat(response, false)); prompt = ""; response = ""; } }