> ## Documentation Index
> Fetch the complete documentation index at: https://amd-gaia.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent UI SDK

> Python backend SDK for the GAIA Agent UI - FastAPI server, SQLite database, and Pydantic models

<Info>
  **Source Code:** [`src/gaia/ui/`](https://github.com/amd/gaia/blob/main/src/gaia/ui/)
</Info>

<Note>
  **Import:**

  ```python theme={null}
  from gaia.ui.server import create_app, DEFAULT_PORT
  from gaia.ui.database import ChatDatabase, DEFAULT_DB_PATH
  from gaia.ui.models import SystemStatus, ChatRequest, SessionResponse, DocumentResponse
  ```
</Note>

**See also:** [User Guide](/guides/agent-ui) | [Agent SDK](/sdk/sdks/chat) | [API Specification](/spec/agent-ui-server)

<Warning>
  **Tested Configuration:** The Agent UI has been tested on **AMD Ryzen AI MAX+ 395** with **Qwen3.5-35B-A3B-GGUF**. Other configurations are not officially verified. See the [User Guide](/guides/agent-ui) for full details and how to report issues on other hardware.
</Warning>

***

## Overview

The Agent UI SDK is the Python backend that powers the GAIA Agent UI. It provides:

* **FastAPI REST server** with session, chat, document, and memory endpoints
* **SQLite database** for persistent sessions, messages, and document metadata
* **SSE streaming** for real-time chat responses
* **RAG integration** for document Q\&A
* **Memory dashboard** with knowledge browser, tool stats, and observability ([Memory SDK](/sdk/sdks/memory))
* **Pydantic models** for request/response validation

The backend runs on port `4200` by default and serves both the Electron desktop app and browser-based clients.

<Tip>
  End users don't need to interact with this SDK directly — prebuilt `.exe` /
  `.deb` desktop installers are on the
  [GitHub Releases page](https://github.com/amd/gaia/releases), and
  `npm install -g @amd-gaia/agent-ui` gives the same app on any Node-capable
  platform. This page is for developers embedding or extending the backend.
  See the [Agent UI guide](/guides/agent-ui#install-and-launch) for install
  options.
</Tip>

***

## Quick Start

### Start the Server

```python theme={null}
from gaia.ui.server import create_app

# Create with default database (~/.gaia/chat/gaia_chat.db)
app = create_app()

# Create with custom database path
app = create_app(db_path="/path/to/my/chat.db")

# Create with in-memory database (for testing)
app = create_app(db_path=":memory:")
```

Run with uvicorn:

```python theme={null}
import uvicorn
from gaia.ui.server import create_app

app = create_app()
uvicorn.run(app, host="localhost", port=4200)
```

Or from the command line:

```bash theme={null}
python -m gaia.ui.server --port 4200
```

### Use the Database Directly

```python theme={null}
from gaia.ui.database import ChatDatabase

db = ChatDatabase()  # Uses default path ~/.gaia/chat/gaia_chat.db

# Create a session
session = db.create_session(title="My Chat", model="Qwen3.5-35B-A3B-GGUF")
print(f"Session ID: {session['id']}")

# Add messages
db.add_message(session["id"], "user", "Hello!")
db.add_message(session["id"], "assistant", "Hi there! How can I help?")

# Retrieve messages
messages = db.get_messages(session["id"])
for msg in messages:
    print(f"{msg['role']}: {msg['content']}")

# Clean up
db.close()
```

***

## Core Classes

### ChatDatabase

The persistence layer for all Agent UI data. Uses SQLite with WAL mode for concurrent read access.

```python theme={null}
from gaia.ui.database import ChatDatabase, DEFAULT_DB_PATH
```

**Constructor:**

```python theme={null}
class ChatDatabase:
    def __init__(self, db_path: str = None):
        """Initialize database connection.

        Args:
            db_path: Path to SQLite database file.
                     Defaults to ~/.gaia/chat/gaia_chat.db.
                     Use ":memory:" for in-memory database (testing).
        """
```

#### Session Methods

| Method           | Signature                                                 | Description                                           |
| ---------------- | --------------------------------------------------------- | ----------------------------------------------------- |
| `create_session` | `(title?, model?, system_prompt?, document_ids?) -> Dict` | Create a new chat session                             |
| `get_session`    | `(session_id) -> Optional[Dict]`                          | Get session by ID with message count and document IDs |
| `list_sessions`  | `(limit=50, offset=0) -> List[Dict]`                      | List sessions ordered by most recently updated        |
| `count_sessions` | `() -> int`                                               | Count total sessions                                  |
| `update_session` | `(session_id, title?, system_prompt?) -> Optional[Dict]`  | Update session title and/or system prompt             |
| `delete_session` | `(session_id) -> bool`                                    | Delete a session and its messages (cascading)         |
| `touch_session`  | `(session_id) -> None`                                    | Update the session's `updated_at` timestamp           |

**Example:**

```python theme={null}
db = ChatDatabase()

# Create session with attached documents
session = db.create_session(
    title="Project Review",
    model="Qwen3.5-35B-A3B-GGUF",
    system_prompt="You are a code review assistant.",
    document_ids=["doc-abc123"],
)

# Update title
db.update_session(session["id"], title="Sprint 42 Review")

# List recent sessions
for s in db.list_sessions(limit=10):
    print(f"{s['title']} ({s['message_count']} messages)")
```

#### Message Methods

| Method           | Signature                                                                              | Description                       |
| ---------------- | -------------------------------------------------------------------------------------- | --------------------------------- |
| `add_message`    | `(session_id, role, content, rag_sources?, tokens_prompt?, tokens_completion?) -> int` | Add a message, returns message ID |
| `get_messages`   | `(session_id, limit=100, offset=0) -> List[Dict]`                                      | Get messages oldest-first         |
| `count_messages` | `(session_id) -> int`                                                                  | Count messages in a session       |

**Example:**

```python theme={null}
# Add a user message
db.add_message(session_id, "user", "What does this function do?")

# Add an assistant message with RAG sources
db.add_message(
    session_id,
    "assistant",
    "This function initializes the database connection...",
    rag_sources=[
        {"document_id": "doc-123", "filename": "main.py", "chunk": "def init_db()...", "score": 0.92}
    ],
    tokens_prompt=150,
    tokens_completion=87,
)

# Retrieve conversation
messages = db.get_messages(session_id)
for msg in messages:
    print(f"[{msg['role']}] {msg['content'][:80]}...")
```

#### Document Methods

| Method                  | Signature                                                           | Description                                    |
| ----------------------- | ------------------------------------------------------------------- | ---------------------------------------------- |
| `add_document`          | `(filename, filepath, file_hash, file_size?, chunk_count?) -> Dict` | Add document to library (deduplicates by hash) |
| `get_document`          | `(doc_id) -> Optional[Dict]`                                        | Get document by ID                             |
| `list_documents`        | `() -> List[Dict]`                                                  | List all documents                             |
| `delete_document`       | `(doc_id) -> bool`                                                  | Delete a document                              |
| `attach_document`       | `(session_id, document_id) -> bool`                                 | Attach document to session                     |
| `detach_document`       | `(session_id, document_id) -> bool`                                 | Detach document from session                   |
| `get_session_documents` | `(session_id) -> List[Dict]`                                        | Get all documents for a session                |

**Example:**

```python theme={null}
import hashlib

# Add a document
doc = db.add_document(
    filename="manual.pdf",
    filepath="/home/user/docs/manual.pdf",
    file_hash=hashlib.sha256(open("manual.pdf", "rb").read()).hexdigest(),
    file_size=1_234_567,
    chunk_count=45,
)

# Attach to a session
db.attach_document(session_id, doc["id"])

# Check which documents are in a session
docs = db.get_session_documents(session_id)
for d in docs:
    print(f"{d['filename']} ({d['chunk_count']} chunks)")
```

#### Statistics

```python theme={null}
stats = db.get_stats()
print(f"Sessions: {stats['sessions']}")
print(f"Messages: {stats['messages']}")
print(f"Documents: {stats['documents']}")
print(f"Total chunks: {stats['total_chunks']}")
print(f"Total size: {stats['total_size_bytes']} bytes")
```

***

### create\_app()

Factory function that creates and configures the FastAPI application with all endpoints.

```python theme={null}
from gaia.ui.server import create_app

def create_app(db_path: str = None, webui_dist: str = None) -> FastAPI:
    """Create and configure the FastAPI application.

    Args:
        db_path: Path to SQLite database.
                 None for default (~/.gaia/chat/gaia_chat.db).
                 ":memory:" for testing.
        webui_dist: Path to a prebuilt Agent UI bundle to serve as static
                 files. None falls back to the bundle that ships with the
                 installed package (used by `gaia --ui-dist`).

    Returns:
        Configured FastAPI application with all routers (agents, chat,
        documents, files, mcp, sessions, system, tunnel) registered.
    """
```

Shared state is stored on `app.state` and is accessible in tests:

```python theme={null}
app = create_app(db_path=":memory:")
db        = app.state.db               # ChatDatabase
registry  = app.state.agent_registry   # AgentRegistry (see /spec/plugin-registry)
dispatch  = app.state.dispatch_queue   # background job queue
monitor   = app.state.document_monitor # watches the library for new files
```

***

## Pydantic Models

All request and response bodies use Pydantic models from `gaia.ui.models`.

### System

```python theme={null}
class SystemStatus(BaseModel):
    """System readiness status returned by GET /api/system/status."""
    lemonade_running: bool = False
    model_loaded: Optional[str] = None
    embedding_model_loaded: bool = False
    disk_space_gb: float = 0.0
    memory_available_gb: float = 0.0
    initialized: bool = False
    version: str = "0.1.0"
```

### Sessions

```python theme={null}
class CreateSessionRequest(BaseModel):
    """POST /api/sessions"""
    title: Optional[str] = None
    model: Optional[str] = None
    system_prompt: Optional[str] = None
    document_ids: List[str] = []

class UpdateSessionRequest(BaseModel):
    """PUT /api/sessions/{session_id}"""
    title: Optional[str] = None
    system_prompt: Optional[str] = None

class SessionResponse(BaseModel):
    """Returned by session endpoints."""
    id: str
    title: str
    created_at: str
    updated_at: str
    model: str
    system_prompt: Optional[str] = None
    message_count: int = 0
    document_ids: List[str] = []

class SessionListResponse(BaseModel):
    sessions: List[SessionResponse]
    total: int
```

### Chat

```python theme={null}
class ChatRequest(BaseModel):
    """POST /api/chat/send"""
    session_id: str
    message: str
    document_ids: Optional[List[str]] = None
    stream: bool = True   # SSE streaming by default

class ChatResponse(BaseModel):
    """Non-streaming response from POST /api/chat/send."""
    message_id: int
    content: str
    sources: List[SourceInfo] = []
    tokens: Optional[Dict[str, int]] = None

class SourceInfo(BaseModel):
    """RAG source citation."""
    document_id: str
    filename: str
    chunk: str
    score: float
    page: Optional[int] = None
```

### Messages

```python theme={null}
class MessageResponse(BaseModel):
    """Individual message in a session."""
    id: int
    session_id: str
    role: str             # "user", "assistant", or "system"
    content: str
    created_at: str
    rag_sources: Optional[List[SourceInfo]] = None
    agent_steps: Optional[List[AgentStepResponse]] = None

class MessageListResponse(BaseModel):
    messages: List[MessageResponse]
    total: int
```

### Documents

```python theme={null}
class DocumentResponse(BaseModel):
    """Document in the global library."""
    id: str
    filename: str
    filepath: str
    file_size: int
    chunk_count: int
    indexed_at: str
    last_accessed_at: Optional[str] = None
    sessions_using: int = 0
    indexing_status: str = "complete"  # pending | indexing | complete | failed | cancelled | missing

class DocumentListResponse(BaseModel):
    documents: List[DocumentResponse]
    total: int
    total_size_bytes: int
    total_chunks: int

class DocumentUploadRequest(BaseModel):
    """POST /api/documents/upload-path"""
    filepath: str

class AttachDocumentRequest(BaseModel):
    """POST /api/sessions/{session_id}/documents"""
    document_id: str
```

***

## REST API Endpoints

### System

<AccordionGroup>
  <Accordion title="GET /api/system/status">
    Check system readiness for the agent UI.

    **Response:**

    ```json theme={null}
    {
      "lemonade_running": true,
      "model_loaded": "Qwen3.5-35B-A3B-GGUF",
      "embedding_model_loaded": false,
      "disk_space_gb": 128.5,
      "memory_available_gb": 12.3,
      "initialized": true,
      "version": "0.1.0"
    }
    ```
  </Accordion>

  <Accordion title="GET /api/health">
    Health check with database statistics.

    **Response:**

    ```json theme={null}
    {
      "status": "ok",
      "service": "gaia-agent-ui",
      "stats": {
        "sessions": 12,
        "messages": 245,
        "documents": 5,
        "total_chunks": 320,
        "total_size_bytes": 15234567
      }
    }
    ```
  </Accordion>

  <Accordion title="POST /api/system/load-model">
    Trigger loading a model on the Lemonade server. Returns `202` immediately; loading proceeds in the background. Poll `GET /api/system/status` to detect when loading completes.

    **Request:**

    ```json theme={null}
    {
      "model_name": "Qwen3.5-35B-A3B-GGUF",
      "ctx_size": 32768
    }
    ```

    | Field        | Type   | Required | Description                                         |
    | ------------ | ------ | -------- | --------------------------------------------------- |
    | `model_name` | string | Yes      | Name of the model to load.                          |
    | `ctx_size`   | number | No       | Context window size in tokens. Defaults to `32768`. |

    **Response (202):**

    ```json theme={null}
    {
      "status": "loading",
      "model": "Qwen3.5-35B-A3B-GGUF",
      "ctx_size": 32768
    }
    ```
  </Accordion>

  <Accordion title="POST /api/system/download-model">
    Trigger downloading a model via the Lemonade server. Returns `202` immediately; the download proceeds in the background. Poll `GET /api/system/status` to detect when the model becomes available. Set `force` to `true` to re-download even if the file already exists (repairs corrupted or incomplete downloads).

    **Request:**

    ```json theme={null}
    {
      "model_name": "Qwen3.5-35B-A3B-GGUF",
      "force": false
    }
    ```

    | Field        | Type    | Required | Description                                                        |
    | ------------ | ------- | -------- | ------------------------------------------------------------------ |
    | `model_name` | string  | Yes      | Name of the model to download.                                     |
    | `force`      | boolean | No       | Re-download even if the model already exists. Defaults to `false`. |

    **Response (202):**

    ```json theme={null}
    {
      "status": "downloading",
      "model": "Qwen3.5-35B-A3B-GGUF"
    }
    ```
  </Accordion>

  <Accordion title="GET /api/settings">
    Get current user settings including the custom model override and its status on the Lemonade server.

    **Response:**

    ```json theme={null}
    {
      "custom_model": "huihui-ai/Huihui-Qwen3.5-35B-A3B-abliterated",
      "model_status": {
        "found": true,
        "downloaded": true,
        "loaded": false
      }
    }
    ```

    | Field          | Type           | Description                                                                                                                                                         |
    | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `custom_model` | string or null | HuggingFace model ID overriding the default, or `null` if using the default model.                                                                                  |
    | `model_status` | object or null | Status of the custom model on Lemonade. `null` when no custom model is set. Contains `found` (in catalog), `downloaded` (on disk), and `loaded` (currently active). |
  </Accordion>

  <Accordion title="PUT /api/settings">
    Update user settings. Set `custom_model` to a model ID to override the default, or to an empty string / `null` to clear the override and revert to the default model.

    **Request:**

    ```json theme={null}
    {
      "custom_model": "huihui-ai/Huihui-Qwen3.5-35B-A3B-abliterated"
    }
    ```

    | Field          | Type           | Required | Description                                                                                     |
    | -------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------- |
    | `custom_model` | string or null | No       | HuggingFace model ID to use instead of the default. Empty string or `null` clears the override. |

    **Response:** Same shape as `GET /api/settings`.
  </Accordion>
</AccordionGroup>

### Sessions

<AccordionGroup>
  <Accordion title="POST /api/sessions">
    Create a new chat session.

    **Request:**

    ```json theme={null}
    {
      "title": "Code Review",
      "model": "Qwen3.5-35B-A3B-GGUF",
      "system_prompt": "You are a code reviewer.",
      "document_ids": ["doc-abc123"]
    }
    ```

    **Response:** `SessionResponse`
  </Accordion>

  <Accordion title="GET /api/sessions">
    List all sessions, ordered by most recently updated.

    **Query params:** `limit` (default 50), `offset` (default 0)

    **Response:** `SessionListResponse`
  </Accordion>

  <Accordion title="GET /api/sessions/{session_id}">
    Get session details including message count and attached document IDs.

    **Response:** `SessionResponse`
  </Accordion>

  <Accordion title="PUT /api/sessions/{session_id}">
    Update session title or system prompt.

    **Request:**

    ```json theme={null}
    {
      "title": "Sprint 42 Review"
    }
    ```
  </Accordion>

  <Accordion title="DELETE /api/sessions/{session_id}">
    Delete a session and all its messages (cascading delete).
  </Accordion>

  <Accordion title="GET /api/sessions/{session_id}/messages">
    Get messages for a session, ordered oldest first.

    **Query params:** `limit` (default 100), `offset` (default 0)

    **Response:** `MessageListResponse`
  </Accordion>

  <Accordion title="GET /api/sessions/{session_id}/export">
    Export a session to Markdown or JSON.

    **Query params:** `format` ("markdown" or "json", default "markdown")
  </Accordion>
</AccordionGroup>

### Chat

<AccordionGroup>
  <Accordion title="POST /api/chat/send">
    Send a message and receive a response. Supports both streaming (SSE) and non-streaming modes.

    **Request:**

    ```json theme={null}
    {
      "session_id": "abc-123",
      "message": "What does this code do?",
      "document_ids": ["doc-456"],
      "stream": true
    }
    ```

    **Streaming response** (SSE events):

    When `stream: true`, the server returns a `text/event-stream` response. Each line follows the SSE format `data: <JSON>`. The `SSEOutputHandler` ([`src/gaia/ui/sse_handler.py`](https://github.com/amd/gaia/blob/main/src/gaia/ui/sse_handler.py)) bridges agent console events to the following typed events:

    **Thinking and Progress**

    | Event type | Fields                                            | Description                                                                                                                                                                         |
    | ---------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `thinking` | `content` (string)                                | Agent reasoning or progress message. Emitted when the agent starts processing, thinks through a problem, or begins a long-running operation.                                        |
    | `step`     | `step` (int), `total` (int), `status` (string)    | Agent step progress. `step` is the current step number, `total` is the step limit, and `status` is `"started"`.                                                                     |
    | `status`   | `status` (string), `message` (string)             | General status update. `status` is one of `"working"`, `"complete"`, `"warning"`, or `"info"`. May also include `steps` (int) and `elapsed` (number) when `status` is `"complete"`. |
    | `plan`     | `steps` (string\[]), `current_step` (int or null) | Agent execution plan. Each entry in `steps` is a human-readable description of a planned action.                                                                                    |

    **Tool Execution**

    | Event type     | Fields                                                                                                                                         | Description                                                                                                                                                                           |
    | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `tool_start`   | `tool` (string), `detail` (string)                                                                                                             | Tool invocation started. `tool` is the tool function name (e.g., `"query_documents"`, `"search_file"`). `detail` is a human-readable description of the operation.                    |
    | `tool_args`    | `tool` (string), `args` (object), `detail` (string)                                                                                            | Tool arguments. `args` is the raw arguments dict passed to the tool. `detail` is a formatted human-readable summary of the arguments.                                                 |
    | `tool_end`     | `success` (boolean)                                                                                                                            | Tool invocation completed.                                                                                                                                                            |
    | `tool_result`  | `title` (string or null), `summary` (string), `success` (boolean), `result_data` (object or null), `command_output` (object or null)           | Tool result with structured data. `summary` is a human-readable result. `result_data` contains typed results (see below). `command_output` contains shell command output (see below). |
    | `policy_alert` | `tool` (string), `decision` (`"BLOCK"`), `reason` (string), `rule_ids` (string\[]), `policy_version` (string), `receipt_id` (string, optional) | Governance policy blocked a tool before execution. No user action is required; use this to show a visible policy refusal instead of treating the denial as a generic tool failure.    |

    `result_data` variants in `tool_result`:

    * **File list:** `{"type": "file_list", "files": [...], "total": int}` -- up to 20 file entries
    * **Search results:** `{"type": "search_results", "count": int, "scores": float[], "previews": string[]}` -- top 5 chunk previews (200 chars each)

    `command_output` shape in `tool_result`:

    ```json theme={null}
    {
      "command": "ls -la",
      "stdout": "total 42\n...",
      "stderr": "",
      "return_code": 0,
      "cwd": "/home/user/project",
      "duration_seconds": 0.15,
      "truncated": false
    }
    ```

    **Response Content**

    | Event type    | Fields                                                                    | Description                                                                                                                                                                                     |
    | ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `chunk`       | `content` (string)                                                        | Incremental text fragment of the response, streamed as the LLM generates tokens. Raw tool-call JSON is automatically filtered out.                                                              |
    | `answer`      | `content` (string), `elapsed` (number), `steps` (int), `tools_used` (int) | Final complete answer from the agent. `elapsed` is wall-clock seconds. `steps` and `tools_used` are execution totals. Double-escaped newlines/tabs from LLM output are automatically corrected. |
    | `agent_error` | `content` (string)                                                        | Error message from the agent.                                                                                                                                                                   |

    **Stream Termination**

    | Event type | Fields                                 | Description                                                                                                                         |
    | ---------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
    | `done`     | `message_id` (int), `content` (string) | Signals the end of the stream. `message_id` is the database ID of the saved assistant message. `content` is the full response text. |

    **Example stream** showing a typical multi-step interaction:

    ```
    data: {"type": "thinking", "content": "Sending to Qwen3.5-35B..."}
    data: {"type": "step", "step": 1, "total": 10, "status": "started"}
    data: {"type": "tool_start", "tool": "query_documents", "detail": "Searching indexed documents for relevant content"}
    data: {"type": "tool_args", "tool": "query_documents", "args": {"query": "database init"}, "detail": "query: database init"}
    data: {"type": "tool_result", "title": "Result", "summary": "Found 3 relevant chunk(s) (best score: 0.87)", "success": true, "result_data": {"type": "search_results", "count": 3, "scores": [0.87, 0.72, 0.65], "previews": ["def init_db()..."]}}
    data: {"type": "tool_end", "success": true}
    data: {"type": "chunk", "content": "This function"}
    data: {"type": "chunk", "content": " initializes"}
    data: {"type": "chunk", "content": " the database..."}
    data: {"type": "answer", "content": "This function initializes the database...", "elapsed": 3.45, "steps": 1, "tools_used": 1}
    data: {"type": "status", "status": "complete", "message": "Completed in 1 steps", "steps": 1, "elapsed": 3.45}
    data: {"type": "done", "message_id": 42, "content": "This function initializes the database..."}
    ```

    **Non-streaming response:**

    ```json theme={null}
    {
      "message_id": 42,
      "content": "This function initializes the database...",
      "sources": [],
      "tokens": null
    }
    ```
  </Accordion>
</AccordionGroup>

### Documents

<AccordionGroup>
  <Accordion title="GET /api/documents">
    List all documents in the global library.

    **Response:**

    ```json theme={null}
    {
      "documents": [
        {
          "id": "doc-abc123",
          "filename": "manual.pdf",
          "filepath": "/home/user/docs/manual.pdf",
          "file_size": 1234567,
          "chunk_count": 45,
          "indexed_at": "2026-03-05T10:00:00Z",
          "last_accessed_at": "2026-03-05T14:30:00Z",
          "sessions_using": 3
        }
      ],
      "total": 1,
      "total_size_bytes": 1234567,
      "total_chunks": 45
    }
    ```
  </Accordion>

  <Accordion title="POST /api/documents/upload-path">
    Index a document by file path. The file is hashed for deduplication -- if the same file was already indexed, the existing document is returned.

    **Request:**

    ```json theme={null}
    {
      "filepath": "/home/user/docs/report.pdf"
    }
    ```

    **Response:** `DocumentResponse`
  </Accordion>

  <Accordion title="DELETE /api/documents/{doc_id}">
    Remove a document from the library and all session attachments.
  </Accordion>

  <Accordion title="POST /api/sessions/{session_id}/documents">
    Attach a document from the library to a session.

    **Request:**

    ```json theme={null}
    {
      "document_id": "doc-abc123"
    }
    ```
  </Accordion>

  <Accordion title="DELETE /api/sessions/{session_id}/documents/{doc_id}">
    Detach a document from a session (does not delete the document).
  </Accordion>
</AccordionGroup>

### Files

<AccordionGroup>
  <Accordion title="POST /api/files/open">
    Open a file or folder in the system file explorer. On Windows this launches Explorer, on macOS it uses `open`, and on Linux it uses `xdg-open`. Symbolic links are rejected for security.

    **Request:**

    ```json theme={null}
    {
      "path": "/home/user/docs/report.pdf",
      "reveal": true
    }
    ```

    | Field    | Type    | Description                                                                                                                                              |
    | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `path`   | string  | Absolute path to the file or folder to open.                                                                                                             |
    | `reveal` | boolean | If `true` (default), reveal the file selected in its parent folder. If `false`, open the containing folder directly. Ignored when `path` is a directory. |

    **Response:**

    ```json theme={null}
    {
      "status": "ok",
      "path": "/home/user/docs/report.pdf"
    }
    ```

    **Error responses:**

    | Status | Condition                                                               |
    | ------ | ----------------------------------------------------------------------- |
    | `400`  | Invalid path (empty or contains null bytes), or path is a symbolic link |
    | `404`  | Path does not exist                                                     |
    | `500`  | Failed to launch the system file explorer                               |
  </Accordion>

  <Accordion title="POST /api/files/upload">
    Upload an arbitrary file for use as a chat attachment (not added to the
    RAG library). Use `POST /api/documents/upload` instead if you want the
    file indexed for Q\&A.
  </Accordion>

  <Accordion title="GET /api/files/browse">
    Browse filesystem contents. Returns entries under a directory with size,
    mtime, and type metadata.
  </Accordion>

  <Accordion title="GET /api/files/search">
    Search files by name/pattern across watched or allowed locations.
  </Accordion>

  <Accordion title="GET /api/files/preview">
    Return a preview (first N bytes / first N lines) of a text file.
  </Accordion>

  <Accordion title="GET /api/files/image">
    Stream an image file from the filesystem for display in the UI.
  </Accordion>
</AccordionGroup>

### Agents

<AccordionGroup>
  <Accordion title="GET /api/agents">
    List all agents registered via `AgentRegistry` — built-in agents plus any
    custom agents discovered under `~/.gaia/agents/`. See
    [`plugin-registry`](/spec/plugin-registry) for the registration format.
  </Accordion>

  <Accordion title="GET /api/agents/{agent_id}">
    Return details for a single registered agent (description, models,
    conversation starters, `source` = `builtin` or `custom_python`).
  </Accordion>
</AccordionGroup>

### MCP

The MCP router manages external Model Context Protocol servers the agent can
connect to.

<AccordionGroup>
  <Accordion title="GET /api/mcp/servers">
    List configured MCP servers and their enabled state.
  </Accordion>

  <Accordion title="POST /api/mcp/servers">
    Register a new MCP server configuration.
  </Accordion>

  <Accordion title="DELETE /api/mcp/servers/{name}">
    Remove a registered MCP server configuration.
  </Accordion>

  <Accordion title="POST /api/mcp/servers/{name}/enable">
    Enable a previously-registered MCP server.
  </Accordion>

  <Accordion title="POST /api/mcp/servers/{name}/disable">
    Disable a server without removing its configuration.
  </Accordion>

  <Accordion title="GET /api/mcp/servers/{name}/tools">
    List tools exposed by a specific MCP server.
  </Accordion>

  <Accordion title="GET /api/mcp/catalog">
    Return the curated catalog of known-good MCP servers (Context7, etc.).
  </Accordion>

  <Accordion title="GET /api/mcp/status">
    Overall MCP client status — number of connected servers, aggregate tool
    count, last-error details.
  </Accordion>
</AccordionGroup>

### Tunnel

Expose the local Agent UI over the internet via an ngrok/Cloudflare tunnel.
Used to reach the UI from a phone.

<AccordionGroup>
  <Accordion title="POST /api/tunnel/start">
    Start a tunnel. Returns the public URL.
  </Accordion>

  <Accordion title="POST /api/tunnel/stop">
    Stop the active tunnel.
  </Accordion>

  <Accordion title="GET /api/tunnel/status">
    Current tunnel status — active/inactive, public URL, provider, uptime.
  </Accordion>
</AccordionGroup>

### Chat (advanced)

<AccordionGroup>
  <Accordion title="POST /api/chat/confirm-tool">
    Tool-confirmation handshake. When a tool in the agent's
    `confirmation_required_tools()` set (the generic base set merged with the
    agent's own `CONFIRMATION_REQUIRED_TOOLS`) triggers the agent to pause,
    the UI prompts the user and posts the outcome (`approve` / `deny`) back on
    this endpoint.
  </Accordion>
</AccordionGroup>

### Documents (advanced)

<AccordionGroup>
  <Accordion title="POST /api/documents/upload">
    Multipart drag-and-drop upload. Stores the file in the library and
    enqueues it for indexing.
  </Accordion>

  <Accordion title="GET /api/documents/monitor/status">
    Status of the background `document_monitor.py` file-watcher.
  </Accordion>

  <Accordion title="GET /api/documents/{doc_id}/status">
    Indexing progress for a single document.
  </Accordion>

  <Accordion title="POST /api/documents/{doc_id}/cancel">
    Cancel an in-flight indexing job.
  </Accordion>

  <Accordion title="POST /api/documents/index-folder">
    Recursively index all supported files under a folder.
  </Accordion>
</AccordionGroup>

### Sessions (advanced)

<AccordionGroup>
  <Accordion title="DELETE /api/sessions/{session_id}/messages/{message_id}">
    Delete a single message from a session.
  </Accordion>

  <Accordion title="DELETE /api/sessions/{session_id}/messages/{message_id}/and-below">
    Delete a message and every message after it (the "resend from here"
    pattern).
  </Accordion>
</AccordionGroup>

### System (advanced)

<AccordionGroup>
  <Accordion title="GET /api/system/tasks">
    Inspect the background dispatch queue (model downloads, indexing jobs,
    etc.) used by `src/gaia/ui/dispatch.py`.
  </Accordion>
</AccordionGroup>

### Memory

The Memory endpoints expose the agent's persistent knowledge system. See the [Memory SDK Reference](/sdk/sdks/memory) for the underlying `MemoryStore` and `MemoryMixin` APIs.

<AccordionGroup>
  <Accordion title="GET /api/memory/stats">
    Aggregate statistics across all memory tables.

    **Response:**

    ```json theme={null}
    {
      "knowledge": {
        "total": 142,
        "by_category": {"fact": 68, "preference": 12, "error": 35, "skill": 17, "note": 7, "reminder": 3},
        "by_context": {"global": 15, "work": 95, "personal": 32},
        "sensitive_count": 8,
        "avg_confidence": 0.64
      },
      "conversations": {
        "total_turns": 1847,
        "total_sessions": 93
      },
      "tools": {
        "total_calls": 523,
        "unique_tools": 18,
        "overall_success_rate": 0.91
      },
      "temporal": {
        "upcoming_count": 3,
        "overdue_count": 1
      },
      "embedding": {
        "total": 142,
        "with_embedding": 135,
        "without_embedding": 7,
        "coverage_pct": 95.1
      },
      "db_size_bytes": 2457600
    }
    ```
  </Accordion>

  <Accordion title="GET /api/memory/knowledge">
    Paginated, filterable knowledge browser.

    **Query params:**

    | Param                | Type    | Default      | Description                                                         |
    | -------------------- | ------- | ------------ | ------------------------------------------------------------------- |
    | `category`           | string  | null         | Filter by category (fact, preference, error, skill, note, reminder) |
    | `context`            | string  | null         | Filter by context (global, work, personal, etc.)                    |
    | `entity`             | string  | null         | Filter by entity (e.g., `person:sarah_chen`)                        |
    | `sensitive`          | boolean | null         | Filter by sensitivity flag                                          |
    | `search`             | string  | null         | FTS5 full-text search query                                         |
    | `sort_by`            | string  | `updated_at` | Sort field (updated\_at, confidence, created\_at, category)         |
    | `order`              | string  | `desc`       | Sort order (asc, desc)                                              |
    | `offset`             | integer | 0            | Pagination offset                                                   |
    | `limit`              | integer | 50           | Page size                                                           |
    | `include_superseded` | boolean | false        | Include items superseded by newer facts                             |
    | `time_from`          | string  | null         | ISO 8601 lower bound for created\_at                                |
    | `time_to`            | string  | null         | ISO 8601 upper bound for created\_at                                |

    **Response:**

    ```json theme={null}
    {
      "items": [
        {
          "id": "abc-123",
          "category": "fact",
          "content": "Project uses React 19 with app router",
          "domain": null,
          "source": "tool",
          "confidence": 0.82,
          "context": "work",
          "entity": null,
          "sensitive": false,
          "created_at": "2026-03-15T10:00:00-07:00",
          "updated_at": "2026-03-18T14:30:00-07:00",
          "due_at": null,
          "superseded_by": null
        }
      ],
      "total": 142,
      "offset": 0,
      "limit": 50
    }
    ```
  </Accordion>

  <Accordion title="POST /api/memory/knowledge">
    Create a knowledge entry from the dashboard. Items created via the UI use `source='user'` and `confidence=0.8`.

    **Request:**

    ```json theme={null}
    {
      "category": "fact",
      "content": "Project deadline is April 15",
      "domain": "project",
      "context": "work",
      "entity": "project:q2-report",
      "due_at": "2026-04-15T09:00:00-07:00",
      "sensitive": false
    }
    ```
  </Accordion>

  <Accordion title="PUT /api/memory/knowledge/{knowledge_id}">
    Edit a knowledge entry. Only provided fields are updated.

    **Request:**

    ```json theme={null}
    {
      "content": "Project deadline moved to April 20",
      "due_at": "2026-04-20T09:00:00-07:00"
    }
    ```
  </Accordion>

  <Accordion title="DELETE /api/memory/knowledge/{knowledge_id}">
    Delete a knowledge entry.
  </Accordion>

  <Accordion title="GET /api/memory/upcoming">
    Time-sensitive items due within N days, including overdue.

    **Query params:** `days` (default 7)

    **Response:**

    ```json theme={null}
    [
      {
        "id": "abc-123",
        "category": "reminder",
        "content": "Submit quarterly report",
        "due_at": "2026-04-15T09:00:00-07:00",
        "context": "work"
      }
    ]
    ```
  </Accordion>

  <Accordion title="GET /api/memory/entities">
    List all unique entities with knowledge counts.

    **Response:**

    ```json theme={null}
    [
      {"entity": "person:sarah_chen", "count": 5, "last_updated": "2026-04-01T14:00:00-07:00"},
      {"entity": "project:gaia", "count": 12, "last_updated": "2026-04-01T10:00:00-07:00"}
    ]
    ```
  </Accordion>

  <Accordion title="GET /api/memory/tools">
    Per-tool performance statistics.

    **Response:**

    ```json theme={null}
    [
      {
        "tool_name": "execute_code",
        "total_calls": 87,
        "success_count": 79,
        "failure_count": 8,
        "success_rate": 0.91,
        "avg_duration_ms": 1230,
        "last_error": "SyntaxError: unexpected indent"
      }
    ]
    ```
  </Accordion>

  <Accordion title="GET /api/memory/conversations">
    List conversation sessions with turn counts and first message preview.

    **Query params:** `limit` (default 20)
  </Accordion>

  <Accordion title="GET /api/memory/conversations/{session_id}">
    Get all turns for a specific conversation session.
  </Accordion>

  <Accordion title="GET /api/memory/activity">
    Daily activity counts for the activity timeline chart.

    **Query params:** `days` (default 30)

    **Response:**

    ```json theme={null}
    [
      {
        "date": "2026-04-01",
        "conversations": 12,
        "tool_calls": 8,
        "knowledge_added": 3,
        "errors": 1
      }
    ]
    ```
  </Accordion>

  <Accordion title="GET /api/memory/embedding-coverage">
    Embedding status for the knowledge base.

    **Response:**

    ```json theme={null}
    {
      "total_items": 142,
      "with_embedding": 135,
      "without_embedding": 7,
      "coverage_pct": 95.1
    }
    ```
  </Accordion>

  <Accordion title="POST /api/memory/consolidate">
    Manually trigger conversation consolidation. Distills old sessions (>14 days, >=5 turns) into durable knowledge items.

    **Response:**

    ```json theme={null}
    {
      "consolidated": 3,
      "extracted_items": 8
    }
    ```
  </Accordion>

  <Accordion title="POST /api/memory/reconcile">
    Manually trigger background memory reconciliation. Checks for contradictory facts across sessions.

    **Response:**

    ```json theme={null}
    {
      "pairs_checked": 15,
      "reinforced": 3,
      "contradicted": 1,
      "weakened": 2,
      "neutral": 9
    }
    ```
  </Accordion>

  <Accordion title="POST /api/memory/rebuild-embeddings">
    Trigger embedding backfill for items missing embeddings.

    **Response:**

    ```json theme={null}
    {
      "backfilled": 7,
      "total_without": 0
    }
    ```
  </Accordion>

  <Accordion title="POST /api/memory/rebuild-fts">
    Rebuild FTS5 indexes if search results seem wrong.

    **Response:**

    ```json theme={null}
    {
      "status": "rebuilt"
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Database Schema

The Agent UI uses SQLite with four tables:

```sql theme={null}
-- Global document library
CREATE TABLE documents (
    id TEXT PRIMARY KEY,
    filename TEXT NOT NULL,
    filepath TEXT NOT NULL,
    file_hash TEXT UNIQUE NOT NULL,
    file_size INTEGER DEFAULT 0,
    chunk_count INTEGER DEFAULT 0,
    indexed_at TEXT,
    last_accessed_at TEXT,
    indexing_status TEXT DEFAULT 'complete',  -- pending | indexing | complete | failed | cancelled
    file_mtime REAL                          -- file modification time (Unix epoch)
);

-- Sessions (conversations)
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL DEFAULT 'New Chat',
    created_at TEXT,
    updated_at TEXT,
    model TEXT NOT NULL DEFAULT 'Qwen3.5-35B-A3B-GGUF',
    system_prompt TEXT
);

-- Many-to-many: documents attached to sessions
CREATE TABLE session_documents (
    session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
    document_id TEXT REFERENCES documents(id) ON DELETE CASCADE,
    attached_at TEXT,
    PRIMARY KEY (session_id, document_id)
);

-- Messages
CREATE TABLE messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
    role TEXT CHECK(role IN ('user', 'assistant', 'system')) NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT,
    rag_sources TEXT,          -- JSON array of source citations
    agent_steps TEXT,          -- JSON array of agent execution steps
    inference_stats TEXT,      -- JSON blob with timing/token stats
    tokens_prompt INTEGER,
    tokens_completion INTEGER
);

-- Key/value user settings (e.g. custom_model override)
CREATE TABLE settings (
    key TEXT PRIMARY KEY,
    value TEXT
);
```

**SQLite settings:** Foreign keys enabled, WAL journal mode for concurrent reads.

<Note>
  The `indexing_status`, `file_mtime`, `agent_steps`, and `inference_stats`
  columns (plus the `settings` table) are added via migrations for databases
  created before these existed. New databases include them in the initial schema.
</Note>

***

## Testing

### Unit Testing with In-Memory Database

```python theme={null}
import pytest
from fastapi.testclient import TestClient
from gaia.ui.server import create_app

@pytest.fixture
def client():
    """Create test client with in-memory database."""
    app = create_app(db_path=":memory:")
    return TestClient(app)

def test_create_session(client):
    resp = client.post("/api/sessions", json={"title": "Test Chat"})
    assert resp.status_code == 200
    data = resp.json()
    assert data["title"] == "Test Chat"
    assert data["message_count"] == 0

def test_send_message(client):
    # Create session first
    session = client.post("/api/sessions", json={}).json()
    session_id = session["id"]

    # Send a non-streaming message
    resp = client.post("/api/chat/send", json={
        "session_id": session_id,
        "message": "Hello",
        "stream": False,
    })
    assert resp.status_code == 200

def test_document_lifecycle(client):
    # This test requires a real file on disk
    pass

def test_health_check(client):
    resp = client.get("/api/health")
    assert resp.status_code == 200
    assert resp.json()["status"] == "ok"
```

### Database Testing

```python theme={null}
from gaia.ui.database import ChatDatabase

def test_session_crud():
    db = ChatDatabase(":memory:")

    # Create
    session = db.create_session(title="Test")
    assert session["title"] == "Test"

    # Read
    fetched = db.get_session(session["id"])
    assert fetched is not None

    # Update
    updated = db.update_session(session["id"], title="Updated")
    assert updated["title"] == "Updated"

    # Delete
    assert db.delete_session(session["id"]) is True
    assert db.get_session(session["id"]) is None

    db.close()

def test_message_ordering():
    db = ChatDatabase(":memory:")
    session = db.create_session()

    db.add_message(session["id"], "user", "First")
    db.add_message(session["id"], "assistant", "Second")
    db.add_message(session["id"], "user", "Third")

    messages = db.get_messages(session["id"])
    assert len(messages) == 3
    assert messages[0]["content"] == "First"
    assert messages[2]["content"] == "Third"

    db.close()
```

***

## Integration with the Agent

The Agent UI server delegates to the GAIA [Agent](/sdk/core/agent-system) for LLM communication and tool execution:

```python theme={null}
from gaia_agent_chat.agent import ChatAgent, ChatAgentConfig

# The server creates a ChatAgent instance per request. ChatAgent derives its
# system prompt from `prompt_profile` ("chat" | "doc" | "file" | "data" |
# "web" | "full") — there is no `system_prompt` config field.
config = ChatAgentConfig(
    model_id=session.get("model", "Qwen3.5-35B-A3B-GGUF"),
    prompt_profile="doc",
    allowed_paths=["/path/to/files"],
)
agent = ChatAgent(config)

# Process a query (agent reasons, uses tools, returns response)
result = agent.process_query(message)
```

Document indexing uses the [RAG SDK](/sdk/sdks/rag):

```python theme={null}
from gaia.rag.sdk import RAGSDK, RAGConfig

config = RAGConfig()
rag = RAGSDK(config)
result = rag.index_document(filepath)
chunk_count = result.get("num_chunks", 0)
```

***

## npm Package

GAIA Agent UI is also available as an npm package for quick installation:

```bash theme={null}
npm install -g @amd-gaia/agent-ui
```

This provides the `gaia-ui` CLI command:

```bash theme={null}
gaia-ui              # Start Python backend + open browser
gaia-ui --serve      # Serve frontend only (Node.js static server)
gaia-ui --port 8080  # Custom port
gaia-ui --version    # Show version
```

On first run, `gaia-ui` automatically installs the Python backend (uv, Python 3.12, amd-gaia) if not already present. On subsequent runs, it auto-updates if the version doesn't match.

### Package Contents

The npm package includes:

| Path              | Description                    |
| ----------------- | ------------------------------ |
| `bin/gaia-ui.cjs` | CLI entry point (Node.js)      |
| `dist/`           | Pre-built frontend (React SPA) |

### Release Management

The package version is sourced from `src/gaia/version.py` (single source of truth for all of GAIA):

```bash theme={null}
# Check version consistency (package.json matches version.py)
node installer/version/bump-ui-version.mjs --check

# Sync package.json to version.py
node installer/version/bump-ui-version.mjs

# Full release (sync, commit, tag, push)
node installer/version/release-ui.mjs
```

Tags matching `v*` trigger the automated npm publish workflow.

***

## Related

* **[User Guide](/guides/agent-ui)** -- Desktop app usage, features, and troubleshooting
* **[Agent SDK](/sdk/sdks/chat)** -- Core Agent SDK with memory and RAG
* **[RAG SDK](/sdk/sdks/rag)** -- Document indexing and retrieval
* **[Desktop App Installation](/deployment/ui)** -- Download and install instructions

***

<small style="color: #666;">
  Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved.

  SPDX-License-Identifier: MIT
</small>
