Files
mattermore/DESIGN.md
T

19 KiB

Design: Mattermore — Mattermost AI Chat Agent for Ollama

Status: Draft / Pre-implementation
Last updated: 2026-06-16
Assumed direction: AI chat agent plugin connecting to a remote Ollama instance.
Open questions are marked TODO(design): throughout. This is a living template — change assumptions as the product direction firms up.


1. Purpose & Scope

1.1 What It Is

Mattermore is a Mattermost (Community Edition) plugin that lets users chat with remote LLMs through an Ollama backend. Users invoke the plugin via a slash command (e.g. /ai) and receive responses — either ephemeral or as thread replies — streamed directly into the channel.

1.2 What It Is Not (Explicit Out-of-Scope)

Out of scope Rationale
Hosting its own LLM inference Ollama is the sole backend; the plugin is a thin proxy
Multi-backend abstraction (OpenAI, Anthropic, etc.) If needed later, abstract behind an LLMProvider interface — not now
Training / fine-tuning Ollama handles that
Native webapp UI beyond slash commands TODO(design): decide if a channel header button or RHS panel adds value
Bot accounts Pure plugin hooks — no separate bot user to manage

1.3 Target Audience

  • Teams self-hosting Mattermost who want AI assistant access without data leaving their infrastructure
  • Users comfortable with slash commands

2. Architecture Overview

2.1 Component Diagram (text)

┌─────────────────────────────────────────────────────────┐
│  Mattermost Server (Community Edition)                   │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │  Mattermore Plugin                                │   │
│  │                                                   │   │
│  │  ┌─────────────┐   ┌─────────────────────────┐   │   │
│  │  │ SlashCommand │──▶│ Plugin (Go)             │   │   │
│  │  │   /ai        │   │                         │   │   │
│  │  └─────────────┘   │  ┌───────────────────┐  │   │   │
│  │                     │  │ OllamaClient      │──┼───┼───┼──▶ Ollama API
│  │  ┌─────────────┐   │  └───────────────────┘  │   │   │   (remote)
│  │  │ Config (KV) │   │  ┌───────────────────┐  │   │   │
│  │  └─────────────┘   │  │ ConversationStore │  │   │   │
│  │                     │  └───────────────────┘  │   │   │
│  │  ┌─────────────┐   │  ┌───────────────────┐  │   │   │
│  │  │ Webapp (?)  │   │  │ RateLimiter       │  │   │   │
│  │  └─────────────┘   │  └───────────────────┘  │   │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

2.2 Request Flow (happy path)

User types "/ai write a release note for v2.1"
    │
    ▼
Mattermost parses slash command → routes to Mattermore plugin
    │
    ▼
Plugin.OnConfigurationChange() → reads config (Ollama URL, model, system prompt)
    │
    ▼
Plugin.ExecuteCommand() → parses args, strips command prefix
    │
    ▼
RateLimiter.Allow(userId) → 200 or 429
    │
    ▼
ConversationStore.GetSession(userId, channelId) → previous messages for context
    │
    ▼
OllamaClient.ChatCompletion(messages, stream=true, model=defaultModel)
    │
    ▼
Ollama returns ndjson stream → plugin writes ephemeral post with live updates
    │
    ▼
Post created → user sees response in channel (ephemeral or thread reply)
    │
    ▼
ConversationStore.Append(userId, channelId, userMsg, assistantMsg)

2.3 Error Flow

Failure point Behaviour
Ollama unreachable Ephemeral post: "Ollama at unreachable. Check configuration."
Rate limit hit Ephemeral post: "Rate limit exceeded. Try again in N seconds."
Invalid model name Ephemeral post: "Model 'xyz' not found on Ollama server."
Timeout (>30 s) Abort stream, post partial response + "Response truncated (timeout)."
Input too long Reject with context window limit message; suggest /ai new to reset.

3. Plugin Manifest & Identity

3.1 plugin.json

{
    "id": "com.forkless.mattermore",
    "name": "Mattermore",
    "description": "AI chat agent powered by Ollama.",
    "version": "0.1.0",
    "server": {
        "executables": {
            "linux-amd64": "server/dist/plugin-linux-amd64",
            "linux-arm64": "server/dist/plugin-linux-arm64",
            "darwin-amd64": "server/dist/plugin-darwin-amd64"
        }
    },
    "webapp": {},
    "settings_schema": {
        "header": "Configure the connection to your Ollama instance.",
        "footer": "",
        "settings": [
            {
                "key": "OllamaURL",
                "display_name": "Ollama Server URL",
                "type": "text",
                "help_text": "Base URL of the Ollama API, e.g. http://10.0.0.5:11434",
                "placeholder": "http://localhost:11434",
                "default": "http://localhost:11434"
            },
            {
                "key": "DefaultModel",
                "display_name": "Default Model",
                "type": "text",
                "help_text": "Model name to use when none is specified in the command.",
                "placeholder": "llama3.2",
                "default": "llama3.2"
            },
            {
                "key": "SystemPrompt",
                "display_name": "System Prompt",
                "type": "text",
                "help_text": "System prompt prepended to every conversation.",
                "placeholder": "You are a helpful assistant.",
                "default": "You are a helpful assistant."
            },
            {
                "key": "MaxTokens",
                "display_name": "Max Response Tokens",
                "type": "number",
                "help_text": "Maximum tokens per response (0 = model default).",
                "default": 2048
            },
            {
                "key": "RateLimitPerMinute",
                "display_name": "Rate Limit (requests/minute/user)",
                "type": "number",
                "help_text": "Maximum requests per minute per user. 0 = unlimited.",
                "default": 10
            },
            {
                "key": "AllowedUserIDs",
                "display_name": "Allowed User IDs (comma-separated)",
                "type": "text",
                "help_text": "Restrict to specific user IDs. Empty = all users.",
                "default": ""
            }
        ]
    }
}

TODO(design): Should AllowedUserIDs be a team/channel whitelist instead? For now, keep user-level granularity — easy to widen later.

3.2 Versioning

Follow release-workflow skill: v0.1.0, v0.2.0, etc. Bump PATCH for bugs, MINOR for features. MAJOR after 1.0 for breaking config schema changes.


4. Server-Side Components

4.1 Plugin Lifecycle Hooks

Hook Purpose
OnConfigurationChange() Re-read System Console settings, validate OllamaURL, rebuild clients
ExecuteCommand() Intercept /ai and subcommands (/ai new, /ai model ...)
OnActivate() Register slash command in OnConfigurationChange() — idempotent
OnDeactivate() Clean up goroutines, close idle HTTP connections

4.2 Slash Command: /ai

Syntax:

/ai <prompt>
    — Send a prompt using the default model (current session context).
/ai model <name> <prompt>
    — Send a prompt using a specific model.
/ai new
    — Reset conversation context for the current user+channel.
/ai model list
    — List available models from the Ollama server (ephemeral).
/ai help
    — Show usage help.

TODO(design): Should /ai reply ephemerally (only the caller sees it) or as a thread post (visible to the channel)? Proposed: ephemeral for privacy, with an opt-in --public flag to post to thread.

4.3 OllamaClient

Interface (in server/ollama/client.go):

type Client interface {
    ChatCompletion(ctx context.Context, req *ChatRequest) (<-chan ChatStreamEvent, error)
    ListModels(ctx context.Context) ([]Model, error)
    Ping(ctx context.Context) error
}

type ChatRequest struct {
    Model    string          `json:"model"`
    Messages []ChatMessage   `json:"messages"`
    Stream   bool            `json:"stream"`
    Options  map[string]any  `json:"options,omitempty"`
}

type ChatStreamEvent struct {
    Token    string // delta content
    Done     bool
    Error    error
}

TODO(design): Should we use Ollama's /api/chat (chat format) or /api/generate (raw prompt)? /api/chat is preferred — it maps cleanly to the conversational UX and supports the messages array natively.

4.4 ConversationStore

Store session context in Mattermost's KV store (plugin's API.KVSet / KVGet).

Key pattern Value TTL
ctx_{userID}_{channelID} []Message (last N turns) 1 hour
ctx_{userID}_{channelID}_model Model name override 1 hour

TODO(design):

  • How many turns to keep? Proposed: last 10 messages (5 user + 5 assistant).
  • TTL of 1 hour — resets on activity. Configurable?
  • Should context be channel-scoped or user-global? Channel-scoped means a user gets different context in different channels.

4.5 RateLimiter

Token-bucket per user. Configurable requests/minute from System Console. Resets on config change. Stored in memory, not KV (ephemeral state).


5. Ollama Integration

5.1 API Mapping

Plugin Action Ollama Endpoint Method
Chat completion (streaming) /api/chat POST
List models /api/tags GET
Health check /api/tags (or HEAD /) GET
Pull model /api/pull POST

TODO(design): Should the plugin auto-pull a model if it's not present? Risk: user mistypes name → long pull. Proposed: return error with list of available models instead.

5.2 Streaming Strategy

  1. Send POST to /api/chat with "stream": true.
  2. Read ndjson response body line by line (each line is {"message":{"role":"assistant","content":"..."},"done":false}).
  3. On each done:false event, update an ephemeral post via API.UpdateEphemeralPost() with accumulated content.
  4. On done:true, create the final ephemeral (or thread) post with full content, usage stats (token count, duration).

TODO(design): Post-update on every token may be too chatty. Proposed: batch updates every ~200 ms or every 3 tokens, whichever comes first.

5.3 Context Window Management

  • Token counting: use Ollama's num_ctx parameter or a Go tokenizer (tiktoken-go).
  • If input exceeds configured limit (default: 4096 tokens), trim oldest messages until within limit, or reject if a single message overflows.
  • Display a warning: "Context window trimmed (oldest messages removed)."

TODO(design): Should the user be able to set num_ctx per-request? /ai --context 8192 ...

5.4 Model Selection Precedence

  1. Per-request: /ai model gemma3:12b write a poem
  2. Session override: /ai model gemma3:12b (stored in KV)
  3. Default from System Console: DefaultModel

6. Configuration

6.1 System Console Settings

Defined in plugin.json settings_schema (see §3.1). The System Console provides the UI; the plugin reads via OnConfigurationChange().

6.2 Dynamic Config Reload

OnConfigurationChange() is called on every save. The plugin must:

  1. Re-read the config struct from API.GetConfig() / API.GetPluginConfig().
  2. Validate OllamaURL (parse as URL, reject non-HTTP(S) schemes).
  3. If URL changed, create a new http.Client (with configurable timeout).
  4. If RateLimitPerMinute changed, rebuild the token buckets.
  5. Log the change at debug level.

7. Data Flow — Detailed

7.1 Full Lifecycle

Step  User / System                          Component
────  ──────────────────────────────         ──────────────────
  1   User types "/ai explain TCP"           Mattermost webapp
  2   Mattermost routes to plugin            mm-server
  3   Plugin.ExecuteCommand() parses args    Mattermore plugin
  3a    If "/ai help"     → show help text
  3b    If "/ai new"      → KVStore.Delete(ctx)
  3c    If "/ai model …"  → override model  KVStore.Set(model)
  3d    If "/ai model list" → OllamaClient.ListModels()
  4   RateLimiter.Allow(userId)             RateLimiter
  5   ConversationStore.GetSession(...)     ConversationStore
  6   Build []ChatMessage (system + history + new prompt)
  7   OllamaClient.ChatCompletion(...)      OllamaClient
  8   Ollama streams ndjson tokens          Ollama (remote)
  9   Plugin accumulates tokens, updates    Mattermore plugin
      ephemeral post every ~200ms
 10   On done:true → create final post      Mattermore plugin
 11   ConversationStore.Append(...)         ConversationStore

7.2 Thread Participation

TODO(design): If the user replies in a thread where the bot posted, should the plugin pick it up? This requires a MessageHasBeenPosted hook and thread detection — adds complexity. Scope: defer to v0.2+.


8. Security Model

8.1 Input Sanitization

  • Strip control characters / zero-width Unicode from prompts before sending to Ollama.
  • Limit prompt length to 4096 characters (configurable via System Console).
  • TODO(design): Should we block prompts that look like prompt injections? ("Ignore previous instructions...") Hard to do reliably — document risk instead.

8.2 Output Safety

  • Cap response tokens in the Ollama request (options.num_predict).
  • TODO(design): Add a "Report this response" feedback mechanism? Defer.

8.3 Network Security

  • OllamaURL must start with http:// or https:// — validate at config load.
  • Reject internal IP ranges unless explicitly enabled by a AllowPrivateNetworks toggle (default: off for production).
  • TLS verify enabled by default; optional SkipTLSVerify toggle (logged as a warning).

8.4 Authorization

  • If AllowedUserIDs is non-empty, reject requests from other users with a "not authorized" ephemeral message.
  • All plugin API calls are already scoped to authenticated Mattermost users.

8.5 Rate Limiting

  • Per-user token bucket, replenish rate set via System Console.
  • Default: 10 requests/minute. 0 = unlimited (logged as a warning).
  • Burst: 1 (enforce smooth spacing).

9. Observability

9.1 Logging

Level When
ERROR Ollama unreachable, config validation failure, KV store error
WARN Rate limit hit, context window trimmed, TLS skipped
INFO Plugin activated/deactivated, config changed, first request
DEBUG Every request/response round-trip (with userID, model, token count, latency)

9.2 Metrics (future)

TODO(design): Expose Prometheus-style counters via a /metrics endpoint if the Mattermost server has plugin metrics support:

  • mattermore_requests_total{user,model,status}
  • mattermore_latency_seconds
  • mattermore_tokens_total{direction="input|output"}
  • mattermore_rate_limit_hits_total

9.3 Error Reporting

Ephemeral posts with user-facing messages (not raw Go errors). Internal errors logged at ERROR with stack trace.


10. Open Design Decisions

These are the unresolved questions that need answers before v0.1.0 ships.

# Question Proposed Alternatives Decided?
D1 Reply style Ephemeral by default, --public flag for thread Always ephemeral, always thread, always channel
D2 Context scope Per-channel per-user Per-user global, per-channel only, no context
D3 Context turns Last 5 exchanges (10 messages) Last N tokens, everything in TTL, sliding window
D4 Webapp UI None (slash commands only) Channel header button, RHS panel, settings page
D5 Model governance All models available Admin-restricted model list, least-privilege Ollama API key
D6 Thread replies Defer to v0.2 Auto-reply in thread when user replies to bot post
D7 num_ctx per request Via --context N flag System Console default only
D8 Auto-pull models No (error + list available) Yes (convenient but slow)
D9 Prompt injection guard Defer (document risk) Regex blocklist, LLM-as-judge pre-filter
D10 Metrics endpoint Defer /metrics via plugin HTTP handler

Each decision should be recorded in DECISIONS.md once resolved.


11. Deployment & Lifecycle

11.1 Build

make dist

Produces dist/com.forkless.mattermore-0.1.0.tar.gz containing the server binaries and plugin.json.

11.2 Upload

Via Mattermost System Console → Plugins → Upload Plugin, or via REST API:

curl -X POST $MM_URL/api/v4/plugins \
  -H "Authorization: Bearer $MM_TOKEN" \
  -F "plugin=@dist/com.forkless.mattermore-0.1.0.tar.gz"

11.3 Upgrade

  1. Upload new .tar.gz — Mattermost replaces the plugin binaries.
  2. Plugin's OnConfigurationChange() fires (config preserved).
  3. If the KV schema changed, a migration in OnActivate() handles it.

11.4 Rollback

Re-upload the previous version's .tar.gz. No data migration needed if the KV key schema hasn't changed. If it has, restore from backup or DECISIONS.md records the migration path.


12. Future Considerations (v0.2+)

  • Multi-modal inputs — Ollama supports images via /api/chat with images field. Could allow /ai with attached image.
  • Multi-model routing — Different models for different channel categories (e.g. code-review channel uses codellama).
  • Tool calling — Ollama supports tool definitions. Could let the plugin fetch data from external APIs (weather, Jira, etc.).
  • Bot account mode — A separate mattermost-bot skill / mode for always-on presence in channels vs. on-demand slash commands.
  • Streaming into message attachments — Render structured data (tables, JSON, code blocks) with better formatting.