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 markedTODO(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
- Send POST to
/api/chatwith"stream": true. - Read ndjson response body line by line (each line is
{"message":{"role":"assistant","content":"..."},"done":false}). - On each
done:falseevent, update an ephemeral post viaAPI.UpdateEphemeralPost()with accumulated content. - 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_ctxparameter 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
- Per-request:
/ai model gemma3:12b write a poem - Session override:
/ai model gemma3:12b(stored in KV) - 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:
- Re-read the config struct from
API.GetConfig()/API.GetPluginConfig(). - Validate
OllamaURL(parse as URL, reject non-HTTP(S) schemes). - If URL changed, create a new
http.Client(with configurable timeout). - If
RateLimitPerMinutechanged, rebuild the token buckets. - 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
OllamaURLmust start withhttp://orhttps://— validate at config load.- Reject internal IP ranges unless explicitly enabled by a
AllowPrivateNetworkstoggle (default: off for production). - TLS verify enabled by default; optional
SkipTLSVerifytoggle (logged as a warning).
8.4 Authorization
- If
AllowedUserIDsis 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_secondsmattermore_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
- Upload new
.tar.gz— Mattermost replaces the plugin binaries. - Plugin's
OnConfigurationChange()fires (config preserved). - 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/chatwithimagesfield. Could allow/aiwith 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-botskill / 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.