sam / app.py
notmartin's picture
Position Sam as health companion
e78cdf7 unverified
Raw
History Blame Contribute Delete
7.57 kB
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")),
)