from __future__ import annotations import os from pathlib import Path import gradio as gr from openai import OpenAIError from mr_reachy.config import load_settings from mr_reachy.medication import is_confirmation_intent, is_medication_intent, parse_medication_instruction, plan_summary from mr_reachy.og_client import OGClient from mr_reachy.reminders import confirm_due_dose, medication_status_text, process_due_reminders from mr_reachy.storage import build_medication_store def _build_client() -> OGClient: return OGClient(load_settings()) def _build_store(): return build_medication_store(load_settings().storage) def _status() -> str: settings = load_settings() parts = [] for name, cfg in ( ("Chat", settings.chat), ("Speech-to-text", settings.stt), ("Vision", settings.vision), ): parts.append(f"{name}: {'configured' if cfg.enabled else 'not configured'}") return " | ".join(parts) def _history_for_og(history: list) -> list[dict[str, str]]: turns: list[dict[str, str]] = [] for item in history[-12:]: if isinstance(item, dict): role = item.get("role") content = item.get("content", "") if role in {"user", "assistant"} and content: turns.append({"role": role, "content": content}) elif isinstance(item, (list, tuple)) and len(item) >= 2: user_msg, assistant_msg = item[0], item[1] if user_msg: turns.append({"role": "user", "content": str(user_msg)}) if assistant_msg: turns.append({"role": "assistant", "content": str(assistant_msg)}) return turns def chat(message: str, history: list) -> str: client = _build_client() store = _build_store() if is_confirmation_intent(message): confirmed, response = confirm_due_dose(store) return f"{response}\n\nEmotion: {'happy' if confirmed else 'curious'}" if is_medication_intent(message): result = parse_medication_instruction(message, client) if result.accepted and result.plan is not None: memory = store.load() memory.plans.append(result.plan) store.save(memory) return f"{plan_summary(result.plan)}\n\nEmotion: happy" return f"{result.reason}\n\nEmotion: confused" if not client.chat_enabled: return ( "0G chat is not configured yet. Add OG_CHAT_BASE_URL, OG_CHAT_MODEL, " "OG_CHAT_PROVIDER, and OG_CHAT_API_KEY as Hugging Face Space secrets." ) turns = _history_for_og(history) turns.append({"role": "user", "content": message}) try: reply = client.chat(turns) except OpenAIError as exc: return f"0G chat request failed: {exc}" except Exception as exc: return f"Sam hit an unexpected chat error: {exc}" return f"{reply.speech}\n\nEmotion: {reply.emotion}" def add_medication(instruction: str) -> tuple[str, str]: client = _build_client() store = _build_store() result = parse_medication_instruction(instruction, client) if not result.accepted or result.plan is None: return result.reason, medication_status_text(store.load()) memory = store.load() memory.plans.append(result.plan) store.save(memory) return plan_summary(result.plan), medication_status_text(memory) def confirm_medication() -> tuple[str, str]: store = _build_store() confirmed, response = confirm_due_dose(store) emotion = "happy" if confirmed else "curious" return f"{response}\n\nEmotion: {emotion}", medication_status_text(store.load()) def check_due_reminders() -> tuple[str, str]: store = _build_store() replies = [] changed = process_due_reminders(store=store, notify=replies.append) if replies: response = "\n\n".join(f"{reply.speech}\nEmotion: {reply.emotion}" for reply in replies) elif changed: response = "Medication reminders were updated." else: response = "No medication dose is due right now." return response, medication_status_text(store.load()) def medication_status() -> str: return medication_status_text(_build_store().load()) def transcribe(audio_path: str | None) -> str: if not audio_path: return "Record or upload audio first." client = _build_client() if not client.stt_enabled: return ( "0G speech-to-text is not configured yet. Add the OG_STT_* secrets " "in the Space settings." ) try: return client.transcribe(audio_path) except OpenAIError as exc: return f"0G speech-to-text request failed: {exc}" except Exception as exc: return f"Sam hit an unexpected speech-to-text error: {exc}" def describe(image_path: str | None) -> str: if not image_path: return "Upload an image first." client = _build_client() if not client.vision_enabled: return ( "0G vision is not configured yet. Add/fund the OG_VISION_* secrets " "when you want the camera path enabled." ) image_bytes = Path(image_path).read_bytes() try: return client.describe(image_bytes, prompt="Briefly describe what you see for a friendly robot.") except OpenAIError as exc: return f"0G vision request failed: {exc}" except Exception as exc: return f"Sam hit an unexpected vision error: {exc}" with gr.Blocks(title="Sam") as demo: gr.Markdown( "# Sam\n" "A Reachy Mini AI health companion powered by 0G intelligence, with " "fast local memory synced to 0G Storage." ) gr.Markdown(f"**0G status:** {_status()}") gr.ChatInterface( fn=chat, title="Talk to Sam", description="Chat calls 0G from the Space backend, so API keys stay private.", ) with gr.Tab("Medication Plan"): medication_input = gr.Textbox( label="Pharmacy instruction", placeholder="Example: Take metformin three times a day for five days.", lines=2, ) medication_response = gr.Textbox(label="Sam", lines=4) medication_status_box = gr.Textbox(label="Saved health memory", value=medication_status(), lines=8) gr.Button("Add to Sam's health plan").click( add_medication, inputs=medication_input, outputs=[medication_response, medication_status_box], ) gr.Button("I took it").click( confirm_medication, outputs=[medication_response, medication_status_box], ) gr.Button("Check due health actions").click( check_due_reminders, outputs=[medication_response, medication_status_box], ) gr.Button("Refresh medication plan").click( medication_status, outputs=medication_status_box, ) with gr.Tab("Speech-to-text"): audio = gr.Audio(sources=["microphone", "upload"], type="filepath", label="Audio") transcript = gr.Textbox(label="Transcript", lines=4) gr.Button("Transcribe with 0G Whisper").click(transcribe, inputs=audio, outputs=transcript) with gr.Tab("Vision"): image = gr.Image(type="filepath", label="Image") description = gr.Textbox(label="Description", lines=4) gr.Button("Describe with 0G Vision").click(describe, inputs=image, outputs=description) if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")), )