diff --git a/LLM.py b/LLM.py index 190bad5..7d02422 100644 --- a/LLM.py +++ b/LLM.py @@ -1,16 +1,56 @@ +import os import requests import json import socket import logging -#TODO: Dont hard code these, need to see how sugar as a whole manages API Keys +from sugar3.activity.activity import get_activity_root + API_URL = "https://ai.sugarlabs.org/ask-llm-prompted" -try: - with open("API_KEY.txt", "r") as f: - API_KEY = f.read().strip() -except OSError: - logging.error("Missing API_KEY.txt file.") - API_KEY = None + +# Store and read the API key from the activity's persistent data directory +# instead of a flat file in the bundle. The data/ subdirectory of +# get_activity_root() survives across activity invocations and is the +# Sugar-standard location for per-activity configuration. +_API_KEY_FILENAME = "api_key" + + +def _get_api_key_path(): + """Return the path to the API key file inside activity_root/data/.""" + data_dir = os.path.join(get_activity_root(), "data") + os.makedirs(data_dir, exist_ok=True) + return os.path.join(data_dir, _API_KEY_FILENAME) + + +def _read_api_key(): + """Read the API key from the activity data directory.""" + path = _get_api_key_path() + try: + with open(path, "r") as f: + key = f.read().strip() + if key: + return key + except OSError: + pass + logging.error( + "Missing API key. Save your key to: %s", path + ) + return None + + +def save_api_key(key): + """Persist *key* so it is available on next launch.""" + path = _get_api_key_path() + with open(path, "w") as f: + f.write(key.strip()) + logging.info("API key saved to %s", path) + + +API_KEY = _read_api_key() + +def get_api_key(): + """Return the current API key.""" + return API_KEY DEFAULT_PROMPT = "You are a friendly teacher named Jane who is 28 years old. You teach 10 year old children. Always give helpful, educational responses in simple words that children can understand. Keep your answers between 20-40 words. Be encouraging and enthusiastic but never use emojis(ever). If you notice spelling mistakes, gently correct them. Stay focused on the topic and give relevant answers." @@ -25,7 +65,7 @@ def is_connected(): def ask_llm_prompted(question, custom_prompt = DEFAULT_PROMPT, timeout=120, max_length=200): if API_KEY is None: - logging.error("Missing API key file: API_KEY.txt") + logging.error("API key not set. Please enter your Sugar-AI API key via the activity settings.") return False if not is_connected(): @@ -89,3 +129,4 @@ def ask_llm_prompted(question, custom_prompt = DEFAULT_PROMPT, timeout=120, max_ else: print("Error, LLM did not respond") + diff --git a/README.md b/README.md index 1b74044..d0617db 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,24 @@ The `not-gstreamer1` branch is a backport of features and bug fixes from the `master` branch for ongoing maintenance of the activity on Fedora 18 systems which don't have well-functioning GStreamer 1 packages. + +API Key Configuration +===================== + +Speak-AI uses the `Sugar-AI `_ backend for +LLM-powered chatbot responses. An API key is required for this feature. + +**Getting an API key:** + +1. Visit https://ai.sugarlabs.org/oauth-login +2. Sign in with GitHub or Google. +3. Copy your API key from the dashboard. + +**Setting the key in the activity:** + +When you launch Speak-AI for the first time, a dialog will +automatically prompt you to enter your API key. To change it later, +click the **Set Sugar-AI API Key** button in the activity toolbar. + +If no key is set, the activity will fall back to the on-device SLM +or AIML brain. \ No newline at end of file diff --git a/activity.py b/activity.py index d25f7cb..bf1693b 100644 --- a/activity.py +++ b/activity.py @@ -93,7 +93,7 @@ except ImportError: USING_BRAIN = True -from LLM import is_connected, ask_llm_prompted, DEFAULT_PROMPT +from LLM import is_connected, ask_llm_prompted, DEFAULT_PROMPT, save_api_key from GenAI import is_profane SERVICE = 'org.sugarlabs.Speak' @@ -331,6 +331,14 @@ def __init__(self, handle): separator.set_expand(True) toolbox.toolbar.insert(separator, -1) + self._api_key_button = ToolButton('preferences-system') + self._api_key_button.set_tooltip(_('Set Sugar-AI API Key')) + self._api_key_button.connect('clicked', self._show_api_key_dialog) + toolbox.toolbar.insert(self._api_key_button, -1) + self._api_key_button.show() + + + toolbox.toolbar.insert(StopButton(self), -1) toolbox.show_all() @@ -386,6 +394,9 @@ def _configure_cb(self, event=None): def _new_instance(self): if self._first_time: + from LLM import get_api_key + if get_api_key() is None: + GLib.timeout_add(500, self._show_api_key_dialog) # self.voices.connect('changed', self.__changed_voices_cb) self.pitchadj.connect('value_changed', self._pitch_adjusted_cb) self.rateadj.connect('value_changed', self._rate_adjusted_cb) @@ -420,6 +431,46 @@ def _new_instance(self): self._set_idle_phrase(speak=False) self._first_time = False + + def _show_api_key_dialog(self, button=None): + """Show dialog to enter or update the Sugar-AI API key.""" + from LLM import get_api_key + dialog = Gtk.Dialog( + title=_('Sugar-AI API Key'), + transient_for=self.get_toplevel(), + modal=True, + destroy_with_parent=True, + ) + dialog.add_buttons( + _('Cancel'), Gtk.ResponseType.CANCEL, + _('Save'), Gtk.ResponseType.OK, + ) + content = dialog.get_content_area() + content.set_spacing(8) + content.set_border_width(12) + label = Gtk.Label() + label.set_markup( + _('Enter your Sugar-AI API key.\n' + 'Get one at ai.sugarlabs.org') + ) + label.set_line_wrap(True) + content.pack_start(label, False, False, 0) + entry = Gtk.Entry() + entry.set_placeholder_text(_('Paste your API key here')) + entry.set_visibility(False) + existing = get_api_key() + if existing: + entry.set_text(existing) + content.pack_start(entry, False, False, 0) + dialog.show_all() + response = dialog.run() + if response == Gtk.ResponseType.OK: + key = entry.get_text().strip() + if key: + save_api_key(key) + logging.info('Sugar-AI API key saved.') + dialog.destroy() + def read_file(self, file_path): self._cfg = json.loads(open(file_path, 'r').read())