| """MiniFam — gradio.Server entry point. |
| |
| This serves a custom React frontend (the Dia-styled design under `frontend/`) |
| and exposes the local agent + Markdown store as queued JSON endpoints the |
| frontend calls with `@gradio/client`. |
| |
| GET / -> the React app (frontend/index.html) |
| /app/* -> static assets (CSS, JSX) |
| api "chat" -> one agent turn (live model + tools), with tool-call cards |
| api "recipes" -> the recipe box as JSON |
| api "notes" -> a member's notes as JSON |
| api "meal_plan" -> the weekly plan + shopping list as JSON |
| |
| Everything runs on this machine: the model is an OpenAI-compatible endpoint |
| (Ollama in dev, llama.cpp later) and all data lives in local Markdown files. |
| |
| The classic Gradio Blocks UI is preserved in app_blocks.py. |
| """ |
|
|
| from pathlib import Path |
|
|
| import gradio as gr |
| from fastapi.responses import HTMLResponse |
| from fastapi.staticfiles import StaticFiles |
|
|
| from minifam import agent, bootstrap, calendar_store, config, llm, serialize |
|
|
| FRONTEND = Path(__file__).resolve().parent / "frontend" |
|
|
| app = gr.Server(title="MiniFam") |
|
|
| |
| |
| |
| |
| bootstrap.ensure_ollama() |
|
|
| |
| |
| |
| if llm.BACKEND == "zerogpu": |
| from minifam.llm_zerogpu import _generate as _gpu_generate |
|
|
|
|
| class NoCacheStaticFiles(StaticFiles): |
| """Serve the frontend with caching off so edits show up on reload.""" |
|
|
| async def get_response(self, path, scope): |
| response = await super().get_response(path, scope) |
| response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" |
| return response |
|
|
|
|
| |
| |
| |
| _TOOL_CARDS = { |
| "add_note": ("notes", "Wrote to notes", "Saved"), |
| "list_notes": ("notes", "Read the notes", "Read"), |
| "add_recipe": ("recipes", "Saved a recipe", "Saved"), |
| "import_recipe_from_url": ("recipes", "Fetched the page", "Read"), |
| "list_recipes": ("recipes", "Checked the recipe box", "Read"), |
| "get_recipe": ("recipes", "Looked up a recipe", "Read"), |
| "search_recipes": ("recipes", "Searched recipes", "Read"), |
| "save_meal_plan": ("meals", "Saved the meal plan", "Saved"), |
| "get_meal_plan": ("meals", "Checked the meal plan", "Read"), |
| "generate_shopping_list": ("cart", "Built the shopping list","Ready"), |
| "add_member": ("user", "Added a family member", "Saved"), |
| "set_food_preferences": ("user", "Updated food preferences", "Saved"), |
| "list_members": ("user", "Checked the family", "Read"), |
| "add_event": ("calendar","Added an event", "Saved"), |
| "add_task": ("check", "Added a task", "Saved"), |
| "list_calendar": ("calendar","Checked the calendar", "Read"), |
| "complete_task": ("check", "Checked off a task", "Done"), |
| } |
|
|
| |
| _GCAL_TOOLS = {"add_event": "Add event to Google Calendar", |
| "add_task": "Add task to Google Calendar"} |
|
|
|
|
| def _gcal_from_args(name: str, args: dict) -> str: |
| """Build the Google Calendar link for a just-created event/task card.""" |
| return calendar_store.gcal_url( |
| { |
| "title": args.get("title"), |
| "date": args.get("date"), |
| "time": args.get("time"), |
| "end": args.get("end_time"), |
| "members": args.get("members"), |
| "details": args.get("details"), |
| "type": "task" if name == "add_task" else "event", |
| } |
| ) |
|
|
|
|
| def _card(event: dict) -> dict: |
| kind, label, ok = _TOOL_CARDS.get( |
| event["name"], ("sparkle", event["name"], "Done") |
| ) |
| rows = [ |
| [str(k).replace("_", " ").title(), str(v)[:80]] |
| for k, v in (event.get("args") or {}).items() |
| ][:3] |
| |
| card = {"name": event["name"], "kind": kind, "label": label, "ok": ok, "rows": rows} |
| if event["name"] in _GCAL_TOOLS: |
| card["gcalUrl"] = _gcal_from_args(event["name"], event.get("args") or {}) |
| card["gcalLabel"] = _GCAL_TOOLS[event["name"]] |
| return card |
|
|
|
|
| |
| @app.api(name="chat") |
| def chat(message: str, member: str = "elena", history: list | None = None) -> dict: |
| """Run one agent turn. Returns the reply, the tool cards, and updated history.""" |
| history = history or [] |
| convo = history + [{"role": "user", "content": message}] |
| events: list[dict] = [] |
| try: |
| reply, convo = agent.run_agent(convo, member, events=events) |
| except Exception as e: |
| |
| |
| reply = bootstrap.warmup_message() or ( |
| f"I couldn't reach the local model ({e}). Is it running?" |
| ) |
| return {"reply": reply, "tools": [], "history": history} |
| if not (reply or "").strip(): |
| |
| reply = "Done." if events else "I'm not sure how to help with that — could you rephrase?" |
| return {"reply": reply, "tools": [_card(e) for e in events], "history": convo} |
|
|
|
|
| @app.api(name="recipes") |
| def recipes() -> list: |
| """The whole recipe box as structured cards.""" |
| return serialize.recipes() |
|
|
|
|
| @app.api(name="notes") |
| def notes() -> list: |
| """The shared family notes, projected as note cards.""" |
| return serialize.notes() |
|
|
|
|
| @app.api(name="meal_plan") |
| def meal_plan() -> dict: |
| """The latest weekly plan plus a shopping list built from its recipes.""" |
| return serialize.meal_plan() |
|
|
|
|
| @app.api(name="members") |
| def members() -> list: |
| """The family roster as avatar-ready cards.""" |
| return serialize.members() |
|
|
|
|
| @app.api(name="add_member") |
| def add_member(name: str, food_preferences: str = "") -> dict: |
| """Add a family member and return the full updated roster.""" |
| config.add_member(name, food_preferences=food_preferences) |
| return {"members": serialize.members()} |
|
|
|
|
| @app.api(name="remove_member") |
| def remove_member(member: str) -> dict: |
| """Remove a family member by id and return the full updated roster.""" |
| config.remove_member(member) |
| return {"members": serialize.members()} |
|
|
|
|
| @app.api(name="calendar") |
| def calendar() -> list: |
| """The family calendar (events + tasks) as UI-ready items.""" |
| return serialize.calendar() |
|
|
|
|
| @app.api(name="set_task_done") |
| def set_task_done(item: str, done: bool = True) -> dict: |
| """Check/uncheck a task by id; return the updated calendar.""" |
| calendar_store.set_done(item, done) |
| return {"calendar": serialize.calendar()} |
|
|
|
|
| @app.api(name="remove_calendar_item") |
| def remove_calendar_item(item: str) -> dict: |
| """Remove an event/task by id; return the updated calendar.""" |
| calendar_store.remove_item(item) |
| return {"calendar": serialize.calendar()} |
|
|
|
|
| |
| app.mount("/app", NoCacheStaticFiles(directory=str(FRONTEND / "app")), name="assets") |
|
|
|
|
| @app.get("/", response_class=HTMLResponse) |
| def index() -> str: |
| return (FRONTEND / "index.html").read_text(encoding="utf-8") |
|
|
|
|
| if __name__ == "__main__": |
| app.launch(server_name="0.0.0.0", server_port=7860, show_error=True) |
|
|