Skip to main content
You are viewing: API Specification - Complete technical referenceSee also: User Guide | SDK Reference
  • Component: Agent UI Server - FastAPI backend for the desktop chat application
  • Module: gaia.ui
  • Import: from gaia.ui.server import create_app
  • Source: src/gaia/ui/

Overview

The Agent UI Server is a FastAPI-based backend that powers the GAIA Chat desktop application. It provides REST API endpoints for session management, real-time chat with SSE streaming, document library management, and system health monitoring. Key Design Decisions:
  • SQLite for zero-configuration persistence (WAL mode for concurrent reads)
  • FastAPI for automatic OpenAPI docs and Pydantic validation
  • SSE (Server-Sent Events) for streaming chat responses
  • Hybrid document model โ€” global library with per-session attachment
  • Shared database between CLI and desktop app

Module Structure

src/gaia/ui/
  __init__.py          # Package init
  server.py            # FastAPI app factory and endpoints
  database.py          # ChatDatabase class (SQLite)
  models.py            # Pydantic request/response models
  sse_handler.py       # SSE output handler for streaming agent events
  tunnel.py            # Tunnel manager for mobile access via ngrok

Server (server.py)

App Factory

def create_app(db_path: str = None) -> FastAPI:
Creates a configured FastAPI application with:
  • CORS middleware (allows all origins for local use)
  • Database initialization
  • All API endpoint registration
  • Static file serving (for web frontend, when dist/ build exists)
Default port: 4200

Endpoint Registration

All endpoints are registered as closures within create_app(), sharing the db instance via closure scope. The database is also stored on app.state.db for test access.

Streaming Implementation

Chat streaming uses a thread-based producer/consumer pattern:
  1. A background thread calls AgentSDK.send_stream() synchronously
  2. Chunks are placed into a queue.Queue
  3. The async generator polls the queue and yields SSE events
  4. On completion, the full response is saved to the database
# SSE event format
data: {"type": "chunk", "content": "partial text"}
data: {"type": "done", "message_id": 42, "content": "full response"}
data: {"type": "error", "content": "error description"}

Helper Functions

FunctionDescription
_session_to_response(session)Convert DB dict to SessionResponse
_message_to_response(msg)Convert DB dict to MessageResponse (parses JSON rag_sources)
_doc_to_response(doc)Convert DB dict to DocumentResponse
_sanitize_document_path(user_path)Validate and sanitize user-provided file paths (null bytes, extension allowlist, path resolution)
_sanitize_static_path(base_dir, user_path)Validate URL paths for static file serving stay within base directory
_validate_file_path(filepath)Check path is absolute, no null bytes, and has allowed extension
_compute_file_hash(filepath)SHA-256 hash for document deduplication
_index_document(filepath)Index file via RAG SDK, returns chunk count
_get_chat_response(db, session, request)Non-streaming LLM response
_stream_chat_response(db, session, request)SSE streaming generator

Database (database.py)

Schema

Four tables with foreign key relationships:
documents          -- Global document library
sessions           -- Chat conversations
session_documents  -- Many-to-many attachment relationship
messages           -- Conversation messages with optional RAG metadata

Configuration

  • Foreign keys: Enabled (PRAGMA foreign_keys = ON)
  • Journal mode: WAL (PRAGMA journal_mode = WAL)
  • Thread safety: check_same_thread=False (FastAPI runs across threads)
  • Row factory: sqlite3.Row for dict-like access

Transaction Management

@contextmanager
def _transaction(self):
    """Execute operations atomically."""
    try:
        yield
        self._conn.commit()
    except Exception:
        self._conn.rollback()
        raise

Document Deduplication

Documents are deduplicated by SHA-256 file hash. If a document with the same hash already exists, the existing record is returned with an updated last_accessed_at timestamp.

Document Upload Paths

The server exposes two document upload endpoints, each for a different source of truth:
EndpointInputUsed ByStorage
POST /api/documents/upload-pathJSON {filepath} โ€” absolute server-side pathFile browser (user picks an existing file)Indexes in place; server does NOT own the file
POST /api/documents/uploadmultipart/form-data file blobDrag-and-drop in browser mode / modern ElectronFile is persisted under ~/.gaia/documents/<uuid>.<ext> and owned by the server
The blob endpoint exists because browser File objects never expose a filesystem path, and since Electron 32 File.path has been removed as well (see issue #728). It streams the upload with a running size check (capped at 20 MB), computes a SHA-256 hash, short-circuits on dedup by hash, and only then promotes the file into the managed directory via an atomic os.replace. On failure, partial and orphan files are cleaned up. When a server-owned document is deleted (DELETE /api/documents/{id}), the on-disk file under ~/.gaia/documents/ is also unlinked as a best-effort cleanup. User-owned files from upload-path are left untouched.

Cascade Deletes

  • Deleting a session cascades to its messages and session_documents entries
  • Deleting a document cascades to session_documents entries

Models (models.py)

All 14 Pydantic models:
ModelTypeUsed By
SystemStatusResponseGET /api/system/status
CreateSessionRequestRequestPOST /api/sessions
UpdateSessionRequestRequestPUT /api/sessions/{session_id}
SessionResponseResponseSession endpoints
SessionListResponseResponseGET /api/sessions
ChatRequestRequestPOST /api/chat/send
ChatResponseResponsePOST /api/chat/send (non-streaming)
SourceInfoNestedRAG source citations
MessageResponseResponseGET /api/sessions/{session_id}/messages
MessageListResponseResponseGET /api/sessions/{session_id}/messages
DocumentResponseResponseDocument endpoints
DocumentListResponseResponseGET /api/documents
DocumentUploadRequestRequestPOST /api/documents/upload-path
UploadFile (multipart)RequestPOST /api/documents/upload (blob)
AttachDocumentRequestRequestPOST /api/sessions/{session_id}/documents

Dependencies

Required

PackagePurpose
fastapiWeb framework
uvicornASGI server
pydanticRequest/response validation
httpxAsync HTTP client (Lemonade health checks)

Optional

PackagePurpose
psutilMemory availability check in system status
gaia.chat.sdkLLM communication (AgentSDK)
gaia.rag.sdkDocument indexing (RAGSDK)

Error Handling

ErrorHTTP CodeBehavior
Session not found404"Session not found"
Document not found404"Document not found" or "File not found: {path}"
Path is not a file400"Path is not a file"
Unsupported export format400"Unsupported format: {format}"
LLM error (non-streaming)200Error message in response text
LLM error (streaming)200SSE error event: {"type": "error", "content": "..."}
Database locked500SQLite WAL mode minimizes this

Security Considerations

  • CORS: Uses an allowlist of localhost origins plus ngrok regex. The standalone server (python -m gaia.ui.server) binds to localhost by default. Note: gaia chat --ui binds to 0.0.0.0 for Electron/browser access
  • No authentication: Designed for single-user local use. Do not expose to untrusted networks
  • File path validation: The upload-path endpoint sanitizes all user-provided paths:
    • Null byte rejection (prevents path injection)
    • Path resolution to absolute canonical paths (eliminates .. traversal)
    • File extension allowlist (only document/code file types accepted)
    • See _sanitize_document_path() in server.py
  • Static file serving: URL paths are validated to stay within the dist/ directory via _sanitize_static_path(), preventing directory traversal
  • SQL injection: Prevented by parameterized queries throughout
  • No telemetry: No data is sent externally