Source code for klea_utils.ui.web.nicegui.client

#!/usr/bin/env python3
"""
Server API client for the NiceGUI frontend.

All functions accept the *server_url* as their first argument so the
caller can point them at any running Klea backend without shared state.

File: klea_utils/ui/web/nicegui/client.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import logging
from datetime import datetime

import httpx

from .state import chats, ensure_chat

logger = logging.getLogger(__name__)


[docs] async def hydrate_chats(server_url: str, user_id: str) -> None: """Fetch all chats and their messages from the server into the local state. Populates the in-memory ``chats`` dict with every conversation belonging to *user_id*, including the full message history for each chat. After this call the frontend can switch between any chat without additional server round-trips. If the server has no data for this user yet the local store stays empty. """ try: async with httpx.AsyncClient(timeout=10) as client: logger.debug("GET /chat/%s", user_id) resp = await client.get(f"{server_url}/chat/{user_id}") logger.debug("status=%d", resp.status_code) if resp.status_code == 200: chats_from_server = resp.json() logger.debug( "server returned %d chat(s): %s", len(chats_from_server), [c.get("chat_id") for c in chats_from_server], ) for chat_data in chats_from_server: chat_id = chat_data["chat_id"] key = f"{user_id}:{chat_id}" ensure_chat(user_id, chat_id) chats[key]["name"] = chat_data.get("title", chat_id) chats[key]["created"] = chat_data.get("created_at", 0) # Fetch messages for every chat returned by the server. for chat_data in chats_from_server: chat_id = chat_data["chat_id"] logger.debug("GET /chat/%s/%s/messages", user_id, chat_id) msg_resp = await client.get( f"{server_url}/chat/{user_id}/{chat_id}/messages" ) if msg_resp.status_code == 200: key = f"{user_id}:{chat_id}" current_chat = chats.get(key) if current_chat: current_chat["messages"] = [ ( msg["content"], datetime.fromtimestamp(msg["created_at"]) .astimezone() .strftime("%X"), msg["role"] == "user", ) for msg in msg_resp.json() ] except Exception as e: logger.warning("Failed to hydrate chats from server: %s", e)
[docs] async def create_chat_on_server(server_url: str, user_id: str, chat_id: str) -> None: """POST a new chat to the server so it persists.""" try: async with httpx.AsyncClient(timeout=10) as client: title = chats.get(f"{user_id}:{chat_id}", {}).get("name", chat_id) resp = await client.post( f"{server_url}/chat/{user_id}", json={"chat_id": chat_id, "title": title}, ) if resp.status_code == 200: chat_data = resp.json() ensure_chat(user_id, chat_id) chats[f"{user_id}:{chat_id}"]["name"] = chat_data.get("title", chat_id) except Exception as e: logger.warning("Failed to create chat on server: %s", e)
[docs] async def delete_chat_on_server(server_url: str, user_id: str, chat_id: str) -> None: """DELETE the chat on the server.""" logger.debug("DELETE /chat/%s/%s", user_id, chat_id) try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.delete(f"{server_url}/chat/{user_id}/{chat_id}") logger.debug("status=%d", resp.status_code) except Exception as e: logger.warning("Failed to delete chat on server: %s", e)
[docs] async def rename_chat_on_server( server_url: str, user_id: str, chat_id: str, title: str ) -> None: """PATCH the chat title on the server.""" try: async with httpx.AsyncClient(timeout=10) as client: await client.patch( f"{server_url}/chat/{user_id}/{chat_id}", json={"title": title}, ) except Exception as e: logger.warning("Failed to rename chat on server: %s", e)
[docs] async def set_model_override( server_url: str, user_id: str, chat_id: str, role: str, payload: dict ) -> bool: """POST a model override for a chat role.""" try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.post( f"{server_url}/chat/{user_id}/{chat_id}/models/overrides/{role}", json=payload, ) return resp.status_code == 200 except Exception as e: logger.warning("Failed to set model override: %s", e) return False
[docs] async def clear_model_override( server_url: str, user_id: str, chat_id: str, role: str ) -> bool: """DELETE the model override for a chat role.""" try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.delete( f"{server_url}/chat/{user_id}/{chat_id}/models/overrides/{role}", ) return resp.status_code == 200 except Exception as e: logger.warning("Failed to clear model override: %s", e) return False