Source code for pipeworks_mud_mapper.layout.ollama_panel

"""Ollama LLM Assistant panel component.

The Ollama panel provides an interface for generating room descriptions
using local LLM models via Ollama. It includes template selection for
pre-configured system prompts, model management, seed control for
reproducible generation, and configurable model parameters.

Component Structure
-------------------
::

    ┌────────────────────────────────────────────────────────────────────┐
    │ 🤖 LLM Assistant (Ollama)                                          │
    ├────────────────────────────────────────────────────────────────────┤
    │ Server: [http://...] [↻]  Model: [dropdown]  Template: [dropdown]  │
    │ ○ Connected                                                        │
    │                                                                    │
    │ ▶ System Prompt              ▶ Parameters                          │
    │                                                                    │
    │ User Prompt: [Preset ▾]                         [Use Description]  │
    │ [________________________]                                         │
    │                                                                    │
    │ [Generate ✨]                                                       │
    │                                                                    │
    │ Response:                                                          │
    │ [________________________]                                         │
    │ [Send to Description] [Copy]                                       │
    └────────────────────────────────────────────────────────────────────┘

Component IDs
-------------
**Server and Model Selection:**

- ``ollama-server-url``: Input for Ollama server URL
- ``ollama-model-dropdown``: Dropdown to select Ollama model
- ``ollama-refresh-models-btn``: Button to refresh model list
- ``ollama-refresh-icon``: Icon inside refresh button (for spinning animation)
- ``ollama-connection-status``: Connection status indicator
- ``ollama-template-dropdown``: Dropdown to select prompt template

**System Prompt Section (collapsible, hidden by default):**

- ``ollama-system-prompt-toggle``: Button to toggle system prompt visibility
- ``ollama-system-prompt-chevron``: Chevron icon for collapse state
- ``ollama-system-prompt-collapse``: Collapsible wrapper for system prompt
- ``ollama-system-prompt``: Textarea for system prompt (read-only when template selected)
- ``ollama-copy-system-prompt-btn``: Clipboard component to copy system prompt

**Parameters Section (collapsible, hidden by default):**

- ``ollama-params-toggle``: Button to toggle parameters visibility
- ``ollama-params-chevron``: Chevron icon for params collapse state
- ``ollama-params-collapse``: Collapsible wrapper for parameters

**Seed Controls:**

- ``ollama-seed-value``: Input for seed value (-1 for random, 0+ for fixed)
- ``ollama-seed-decrease``: Button to decrement seed
- ``ollama-seed-increase``: Button to increment seed
- ``ollama-seed-random-check``: Checkbox for random mode (sets seed to -1)

**Model Parameters:**

- ``ollama-temperature``: Input for temperature (creativity/randomness, 0.0-2.0)
- ``ollama-top-k``: Input for top_k (vocabulary filtering, 1-100)
- ``ollama-top-p``: Input for top_p (nucleus sampling threshold, 0.0-1.0)
- ``ollama-num-ctx``: Input for num_ctx (context window in tokens, 512-8192)
- ``ollama-num-predict``: Input for num_predict (max output tokens, 30-2048)

**User Prompt and Generation:**

- ``ollama-user-prompt``: Textarea for user prompt
- ``ollama-populate-prompt-btn``: Button to populate prompt from room description
- ``ollama-generate-btn``: Button to generate description
- ``ollama-generate-icon``: Icon inside generate button (for loading state)
- ``ollama-generate-text``: Text inside generate button (changes during generation)

**Response and Actions:**

- ``ollama-response``: Textarea for LLM response
- ``ollama-send-to-description-btn``: Button to send response to room description
- ``ollama-clipboard``: Clipboard component for copying response
- ``ollama-clipboard-feedback``: Clipboard copy feedback message
- ``ollama-status``: Status message area

Parameter Defaults
------------------
The following default values are used for model parameters:

+---------------+---------+-----------+----------------------------------------------+
| Parameter     | Default | Range     | Purpose                                      |
+===============+=========+===========+==============================================+
| seed          | -1      | -1 or 0+  | -1 = random seed, 0+ = reproducible          |
+---------------+---------+-----------+----------------------------------------------+
| temperature   | 0.7     | 0.0-2.0   | Controls creativity/randomness of output     |
+---------------+---------+-----------+----------------------------------------------+
| top_k         | 40      | 1-100     | Limits vocabulary to top K probable tokens   |
+---------------+---------+-----------+----------------------------------------------+
| top_p         | 0.9     | 0.0-1.0   | Nucleus sampling threshold (cumulative prob) |
+---------------+---------+-----------+----------------------------------------------+
| num_ctx       | 4096    | 512-8192  | Context window size in tokens                |
+---------------+---------+-----------+----------------------------------------------+
| num_predict   | 512     | 30-2048   | Maximum number of tokens to generate         |
+---------------+---------+-----------+----------------------------------------------+

See Also
--------
- ``callbacks/ollama_callbacks.py``: Callbacks for Ollama LLM integration
- ``services/template_service.py``: Template loading and compilation
- ``models/template.py``: Pydantic models for template validation
"""

# ruff: noqa: E501

import dash_bootstrap_components as dbc
from dash import dcc, html

# Defaults are defined in a service module so UI and callbacks share a single
# source of truth without coupling business logic to layout code.
from pipeworks_mud_mapper.services.ollama_config import (
    DEFAULT_NUM_CTX,
    DEFAULT_NUM_PREDICT,
    DEFAULT_SEED,
    DEFAULT_TARGET_WORDS,
    DEFAULT_TEMPERATURE,
    DEFAULT_TOP_K,
    DEFAULT_TOP_P,
)

# =============================================================================
# Default Parameter Values
# =============================================================================
# Defaults are imported above from the shared service module.


[docs] def create_ollama_panel() -> dbc.Card: """Create the Ollama LLM Assistant panel component. The Ollama panel provides: - Server connection management (URL input, refresh button, status indicator) - Model selection dropdown (populated from connected Ollama server) - Template selection for pre-configured system prompts - Collapsible system prompt viewer (read-only when using templates) - Collapsible parameters section with seed control and model parameters - User prompt input with "Use Description" button - Generate button with loading state feedback - Response display with copy and send-to-description actions Returns ------- dbc.Card Bootstrap Card containing the complete Ollama LLM interface. Notes ----- - System prompt and parameters sections are hidden by default for a cleaner UI - System prompt is read-only when a template is selected (enforces template integrity) - Templates are loaded from data/ollama/templates/ directory - Uses /api/chat endpoint for proper system/user message separation - Seed of -1 means random (uses isolated RNG to avoid poisoning global state) - Callback changes button state during generation for user feedback Examples -------- The Ollama panel is typically used within the main layout:: >>> from pipeworks_mud_mapper.layout.ollama_panel import create_ollama_panel >>> panel = create_ollama_panel() >>> # Panel contains all ollama-* component IDs """ return dbc.Card( [ # ================================================================= # Card Header with LLM Assistant title # ================================================================= dbc.CardHeader( [ html.I(className="bi bi-robot me-2"), html.Strong("LLM Assistant"), html.Small(" (Ollama)", className="text-muted"), ], ), dbc.CardBody( [ dbc.Row( [ dbc.Col( [ # --------------------------------------------------------- # Server, Model, and Template Row (Compact 3-Column Layout) # --------------------------------------------------------- # All three controls on one row for a more compact display. # Server URL takes 5 columns, Model and Template take 3.5 each. dbc.Row( [ # Server URL column (with refresh button in input group) dbc.Col( [ dbc.Label( "Server", html_for="ollama-server-url", size="sm", ), dbc.InputGroup( [ dbc.Input( id="ollama-server-url", type="text", value="http://localhost:11434", placeholder="http://localhost:11434", size="sm", ), dbc.Button( [ html.I( className="bi bi-arrow-clockwise", id="ollama-refresh-icon", ), ], id="ollama-refresh-models-btn", color="secondary", outline=True, size="sm", title="Connect and refresh models", ), ], size="sm", ), ], width=5, ), # Model dropdown column dbc.Col( [ dbc.Label( "Model", html_for="ollama-model-dropdown", size="sm", ), dcc.Loading( id="ollama-model-loading", type="dot", color="#17a2b8", children=dcc.Dropdown( id="ollama-model-dropdown", options=[], placeholder="Connect first", style={"fontSize": "0.8rem"}, ), ), ], width=4, ), # Template dropdown column dbc.Col( [ dbc.Label( "Template", html_for="ollama-template-dropdown", size="sm", ), dcc.Dropdown( id="ollama-template-dropdown", options=[], # Populated by callback on load placeholder="Select...", style={"fontSize": "0.8rem"}, ), ], width=3, ), ], className="mb-2", ), # --------------------------------------------------------- # Connection Status Indicator # --------------------------------------------------------- # Shows current connection state (connected/not connected) # with appropriate icon color and text. html.Div( id="ollama-connection-status", children=html.Small( [ html.I(className="bi bi-circle text-muted me-1"), "Not connected", ], className="text-muted", ), className="mb-2", ), # --------------------------------------------------------- # Collapsible Section Toggles Row # --------------------------------------------------------- # Two buttons side by side to toggle System Prompt and # Parameters sections. Both are hidden by default. dbc.Row( [ dbc.Col( dbc.Button( [ html.I( className="bi bi-chevron-right me-1", id="ollama-system-prompt-chevron", ), "System Prompt", ], id="ollama-system-prompt-toggle", color="link", size="sm", className="p-0 text-muted", ), width=6, ), dbc.Col( dbc.Button( [ html.I( className="bi bi-chevron-right me-1", id="ollama-params-chevron", ), "Parameters", ], id="ollama-params-toggle", color="link", size="sm", className="p-0 text-muted", ), width=6, ), ], className="mb-2", ), # --------------------------------------------------------- # Collapsible System Prompt Section # --------------------------------------------------------- # Contains the full system prompt (read-only when using # a template) and a copy button for easy extraction. # Hidden by default (is_open=False). dbc.Collapse( [ dbc.Textarea( id="ollama-system-prompt", value=( "You are a creative writer for a MUD (text-based adventure " "game). Write atmospheric, evocative room descriptions. " "Keep descriptions concise (2-3 sentences). Focus on " "sensory details and mood." ), className="mb-1", style={ "height": "150px", "fontSize": "0.75rem", "fontFamily": "monospace", }, ), dcc.Clipboard( id="ollama-copy-system-prompt-btn", target_id="ollama-system-prompt", title="Copy system prompt to clipboard", className="btn btn-outline-secondary btn-sm mb-2", content="Copy System Prompt", ), ], id="ollama-system-prompt-collapse", is_open=False, ), # --------------------------------------------------------- # Collapsible Parameters Section # --------------------------------------------------------- # Contains seed controls and model parameters. Hidden by # default (is_open=False) for a cleaner interface. dbc.Collapse( [ dbc.Card( dbc.CardBody( [ # ============================================= # Seed Controls Row # ============================================= # Seed determines reproducibility. -1 means # random (different output each time), while # a fixed seed produces the same output. dbc.Row( [ dbc.Col( [ dbc.Label( [ "Seed ", html.Small( "(reproducibility)", className="text-muted", ), ], html_for="ollama-seed-value", size="sm", ), dbc.InputGroup( [ # Decrement button dbc.Button( html.I( className="bi bi-dash" ), id="ollama-seed-decrease", color="secondary", outline=True, size="sm", title="Decrease seed by 1", ), # Seed value input dbc.Input( id="ollama-seed-value", type="number", value=DEFAULT_SEED, min=-1, step=1, size="sm", style={ "textAlign": "center" }, ), # Increment button dbc.Button( html.I( className="bi bi-plus" ), id="ollama-seed-increase", color="secondary", outline=True, size="sm", title="Increase seed by 1", ), ], size="sm", ), ], width=6, ), dbc.Col( [ # Spacer to align checkbox with input html.Div( style={"height": "24px"} ), # Checked by default (seed=-1) dbc.Checkbox( id="ollama-seed-random-check", label="Random each time", value=True, className="small", ), ], width=6, className="d-flex align-items-end", ), ], className="mb-3", ), # ============================================= # Model Parameters Row 1: Temperature, Top-K, Top-P # ============================================= dbc.Row( [ # Temperature: Controls randomness # (0=deterministic, 2=creative) dbc.Col( [ dbc.Label( [ "Temperature ", html.Small( "(creativity)", className="text-muted", ), ], html_for="ollama-temperature", size="sm", ), dbc.Input( id="ollama-temperature", type="number", value=DEFAULT_TEMPERATURE, min=0.0, max=2.0, step=0.05, size="sm", ), html.Small( "0.0-2.0: Low=focused, High=creative", className="text-muted", style={ "fontSize": "0.65rem" }, ), ], width=4, ), # Top-K: Limits vocabulary to top K tokens dbc.Col( [ dbc.Label( [ "Top-K ", html.Small( "(vocab filter)", className="text-muted", ), ], html_for="ollama-top-k", size="sm", ), dbc.Input( id="ollama-top-k", type="number", value=DEFAULT_TOP_K, min=1, max=100, step=1, size="sm", ), html.Small( "1-100: Smaller=more focused", className="text-muted", style={ "fontSize": "0.65rem" }, ), ], width=4, ), # Top-P: Nucleus sampling probability threshold dbc.Col( [ dbc.Label( [ "Top-P ", html.Small( "(nucleus)", className="text-muted", ), ], html_for="ollama-top-p", size="sm", ), dbc.Input( id="ollama-top-p", type="number", value=DEFAULT_TOP_P, min=0.0, max=1.0, step=0.05, size="sm", ), html.Small( "0.0-1.0: Cumulative probability", className="text-muted", style={ "fontSize": "0.65rem" }, ), ], width=4, ), ], className="mb-2", ), # ============================================= # Model Parameters Row 2: Context, Max Tokens # ============================================= dbc.Row( [ # Context Window: How many tokens the model can see dbc.Col( [ dbc.Label( [ "Context ", html.Small( "(memory)", className="text-muted", ), ], html_for="ollama-num-ctx", size="sm", ), dbc.Input( id="ollama-num-ctx", type="number", value=DEFAULT_NUM_CTX, min=512, max=8192, step=512, size="sm", ), html.Small( "512-8192 tokens: Larger=more context", className="text-muted", style={ "fontSize": "0.65rem" }, ), ], width=6, ), # Max Output Tokens: Limit on generated text length dbc.Col( [ dbc.Label( [ "num_predict ", html.Small( "(max tokens)", className="text-muted", ), ], html_for="ollama-num-predict", size="sm", ), dbc.Input( id="ollama-num-predict", type="number", value=DEFAULT_NUM_PREDICT, min=30, max=2048, step=1, size="sm", ), html.Small( "30-2048: Max tokens to generate", className="text-muted", style={ "fontSize": "0.65rem" }, ), ], width=6, ), ], ), # ============================================= # Target Words Row (with explanatory note) # ============================================= dbc.Row( [ # Target Words: Guides LLM on desired output length dbc.Col( [ dbc.Label( [ "Target Words ", html.Small( "(for template)", className="text-muted", ), dbc.Button( [ html.I( className=( "bi bi-sliders " "me-1" ) ), "Presets", ], id="ollama-params-help-btn", color="link", size="sm", className=( "p-0 align-baseline" ), title=( "Show parameter examples" ), ), ], html_for="ollama-target-words", size="sm", ), dbc.Input( id="ollama-target-words", type="number", value=DEFAULT_TARGET_WORDS, min=25, max=500, step=5, size="sm", ), html.Small( id="ollama-target-words-hint", className="text-muted", style={ "fontSize": "0.65rem" }, ), # Parameter presets are separate from prompt prefixes: # they update numeric controls in one click to keep # authors from retyping the same tuning values. dbc.Row( [ dbc.Col( dcc.Dropdown( id="ollama-params-preset-dropdown", options=[], placeholder="Parameter preset...", clearable=True, className="mt-1", style={ "fontSize": "0.75rem", }, ), width=8, ), dbc.Col( dbc.Button( "Apply", id="ollama-params-preset-apply", color="secondary", size="sm", className="mt-1 w-100", ), width=4, ), ], className="g-1", ), ], width=6, ), # Explanatory note about token/word relationship dbc.Col( [ html.Div( [ html.I( className=( "bi bi-info-circle " "text-info me-1" ) ), html.Strong( "Tokens vs Words", className="small", ), ], className="mb-1", ), html.Small( [ "1 word ≈ 1.3-1.5 tokens. ", "For 300 words, set ", "Max Tokens to ~450+. ", "If too low, output is ", "truncated mid-sentence.", ], className="text-muted", style={ "fontSize": "0.65rem", "lineHeight": "1.3", }, ), ], width=6, className=( "d-flex flex-column " "justify-content-center" ), ), ], className="mt-2", ), dbc.Popover( [ dbc.PopoverHeader( "Short Output Baselines" ), dbc.PopoverBody( [ html.Div( "Target 30 words:" ), html.Div( "temp=0.4, top_p=0.7, top_k=20, num_predict=70" ), html.Hr(className="my-2"), html.Div( "If truncation happens:" ), html.Div( "raise num_predict to 80 or reduce prompt length" ), html.Hr(className="my-2"), html.Div( "Longer output (60-80 words):" ), html.Div( "temp=0.7, top_p=0.9, top_k=40, num_predict=140+" ), ] ), ], target="ollama-params-help-btn", trigger="click", placement="top", ), ], className="py-2", ), className="mb-2 bg-body-tertiary", ), ], id="ollama-params-collapse", is_open=False, ), # --------------------------------------------------------- # User Prompt Section # --------------------------------------------------------- # Input area for the user's prompt with a button to # automatically populate from the current room description. html.Div( [ dbc.Label( "User Prompt", html_for="ollama-user-prompt", size="sm", className="me-auto", ), dcc.Dropdown( id="ollama-prompt-prefix-dropdown", options=[], placeholder="Prompt preset...", clearable=True, className="ms-2", style={ "minWidth": "220px", "fontSize": "0.75rem", }, ), dbc.Button( [ html.I( className="bi bi-arrow-down-circle me-1" ), "Use Description", ], id="ollama-populate-prompt-btn", color="link", size="sm", className="p-0 text-muted", title="Copy current room description to prompt", ), ], className="d-flex align-items-center mb-1", ), dbc.Textarea( id="ollama-user-prompt", placeholder="Describe a room called 'The Main Hall'...", className="mb-2", style={"height": "60px", "fontSize": "0.8rem"}, ), # --------------------------------------------------------- # Generate Button with Loading State # --------------------------------------------------------- # Button changes appearance during generation (icon spins, # text changes to "Generating...") for user feedback. dbc.Button( [ html.I( className="bi bi-magic me-1", id="ollama-generate-icon", ), html.Span("Generate", id="ollama-generate-text"), ], id="ollama-generate-btn", color="info", size="sm", className="w-100 mb-2", ), # --------------------------------------------------------- # Status Area # --------------------------------------------------------- # Shows feedback messages (success, error, generation status) html.Div( id="ollama-status", className="small mb-2", ), # Internal status stores so a single renderer callback # owns the actual status output. dcc.Store(id="ollama-status-generation"), dcc.Store(id="ollama-status-send"), dcc.Store(id="ollama-status-prompt"), dcc.Store(id="ollama-status-system"), dcc.Store(id="ollama-status-template"), # --------------------------------------------------------- # Response Section # --------------------------------------------------------- # Displays the generated description from Ollama. Read-only # to prevent accidental edits. dbc.Label("Response", html_for="ollama-response", size="sm"), dbc.Textarea( id="ollama-response", placeholder="Generated description will appear here...", className="mb-2", style={"height": "100px", "fontSize": "0.8rem"}, readOnly=True, ), # --------------------------------------------------------- # Action Buttons Row # --------------------------------------------------------- # "Send to Description" copies response to room description # field. Clipboard button copies response to system clipboard. dbc.Row( [ dbc.Col( dbc.Button( [ html.I( className="bi bi-arrow-right-circle me-1" ), "Send to Description", ], id="ollama-send-to-description-btn", color="success", outline=True, size="sm", className="w-100", ), width=8, ), dbc.Col( dcc.Clipboard( id="ollama-clipboard", target_id="ollama-response", title="Copy to clipboard", className="btn btn-outline-secondary btn-sm w-100", style={"height": "31px"}, ), width=4, ), ], className="mb-2", ), # --------------------------------------------------------- # Clipboard Feedback # --------------------------------------------------------- # Shows confirmation when content is copied to clipboard. html.Div( id="ollama-clipboard-feedback", className="small", ), ], width=7, ), dbc.Col( [ dbc.Card( dbc.CardBody( [ html.Div( [ html.I(className="bi bi-shield-check me-2"), html.Strong("Validator"), ], className="mb-2", ), html.Div( id="ollama-validator-status", className="mb-2", ), html.Div( id="ollama-validator-summary", className="small text-muted mb-3", ), html.Div( [ html.Small( "Rule hits", className="text-uppercase text-muted", ), html.Div( id="ollama-validator-hits", className="small mt-1", ), ], className="mb-3", ), html.Div( [ html.Small( "Recent checks", className="text-uppercase text-muted", ), html.Div( id="ollama-validator-history", className="small mt-1", ), ] ), html.Hr(className="my-2"), html.Small( "Validation is advisory. " "Descriptions are not blocked.", className="text-muted", ), ], ), className="bg-body-tertiary", ) ], width=5, ), ], className="g-2", ) ] ), ], className="mt-3", # Margin top to separate from map panel above )