# 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` ```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 — Send a prompt using the default model (current session context). /ai model — 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`): ```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 ```bash 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: ```bash 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.