Rewrite DESIGN.md to reflect current v0.1.16 state
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
# Design: Mattermore — Mattermost AI Chat Agent for Ollama
|
# Design: Matty — forkless' annoying Mattermost sidekick
|
||||||
|
|
||||||
> **Status:** Draft / Pre-implementation
|
> **Status:** v0.1.16 — Implemented and deployed
|
||||||
> **Last updated:** 2026-06-16
|
> **Last updated:** 2026-06-16
|
||||||
> **Assumed direction:** AI chat agent plugin connecting to a remote Ollama instance.
|
> **Bot username:** @matty
|
||||||
> **Open questions are marked** `TODO(design):` throughout. This is a living template — change
|
> **Default model:** dolphin-llama3:latest (configurable in System Console)
|
||||||
> assumptions as the product direction firms up.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,12 +11,16 @@
|
|||||||
|
|
||||||
### 1.1 What It Is
|
### 1.1 What It Is
|
||||||
|
|
||||||
Mattermore is a Mattermost (Community Edition) plugin that lets users chat
|
Matty is a Mattermost (Community Edition) plugin that lets users chat
|
||||||
with remote LLMs through an Ollama backend. Users address the bot by
|
with remote LLMs through an Ollama backend. Users address the bot by
|
||||||
**@-mentioning** it in any channel (e.g. `@mattermore write a poem`). The
|
**@-mentioning** it in any channel (e.g. `@matty write a poem`). The
|
||||||
bot responds **only when addressed** and disengages naturally — it never
|
bot responds **only when addressed** and disengages naturally — it never
|
||||||
reads every message in the room. A slash command (`/ai`) remains available
|
reads every message in the room. Once addressed, the user enters an
|
||||||
as an alternative entry point.
|
**active session** where follow-up messages continue the conversation
|
||||||
|
without needing to re-@mention. Sessions end on stop words ("thanks",
|
||||||
|
"bye"), explicit `@matty stop`, or 5 minutes of inactivity.
|
||||||
|
|
||||||
|
A slash command (`/ai`) remains available as an alternative entry point.
|
||||||
|
|
||||||
### 1.2 What It Is Not (Explicit Out-of-Scope)
|
### 1.2 What It Is Not (Explicit Out-of-Scope)
|
||||||
|
|
||||||
@@ -26,7 +29,9 @@ as an alternative entry point.
|
|||||||
| Hosting its own LLM inference | Ollama is the sole backend; the plugin is a thin proxy |
|
| 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 |
|
| Multi-backend abstraction (OpenAI, Anthropic, etc.) | If needed later, abstract behind an `LLMProvider` interface — not now |
|
||||||
| Training / fine-tuning | Ollama handles that |
|
| Training / fine-tuning | Ollama handles that |
|
||||||
| **Channel whisper mode** — replying to every message in a channel | The agent only activates on @-mention or thread reply. It never reads unprompted messages. |
|
| **Channel whisper mode** (replying to every message) | The agent only activates on @-mention or active session. It never reads unprompted messages. |
|
||||||
|
| Bot account management | Plugin creates the bot account automatically via `API.CreateBot()` |
|
||||||
|
| Webapp UI | No frontend — @-mention + `/ai` covers all interaction |
|
||||||
|
|
||||||
### 1.3 Target Audience
|
### 1.3 Target Audience
|
||||||
|
|
||||||
@@ -46,7 +51,7 @@ as an alternative entry point.
|
|||||||
│ Mattermost Server (Community Edition) │
|
│ Mattermost Server (Community Edition) │
|
||||||
│ │
|
│ │
|
||||||
│ ┌───────────────────────────────────────────────────────┐ │
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
│ │ Mattermore Plugin │ │
|
│ │ Matty Plugin │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ ┌──────────────────┐ ┌────────────────────────┐ │ │
|
│ │ ┌──────────────────┐ ┌────────────────────────┐ │ │
|
||||||
│ │ │ MessageHasBeen │──▶│ Plugin (Go) │ │ │
|
│ │ │ MessageHasBeen │──▶│ Plugin (Go) │ │ │
|
||||||
@@ -58,85 +63,93 @@ as an alternative entry point.
|
|||||||
│ │ └──────────────────┘ │ │ ConversationStore│ │ │ │
|
│ │ └──────────────────┘ │ │ ConversationStore│ │ │ │
|
||||||
│ │ ┌──────────────────┐ │ └──────────────────┘ │ │ │
|
│ │ ┌──────────────────┐ │ └──────────────────┘ │ │ │
|
||||||
│ │ │ EngagementEngine │ │ ┌──────────────────┐ │ │ │
|
│ │ │ EngagementEngine │ │ ┌──────────────────┐ │ │ │
|
||||||
│ │ │ wake / sleep │──▶│ │ RateLimiter │ │ │ │
|
│ │ │ sleep / active │──▶│ │ RateLimiter │ │ │ │
|
||||||
│ │ └──────────────────┘ │ └──────────────────┘ │ │ │
|
│ │ └──────────────────┘ │ └──────────────────┘ │ │ │
|
||||||
│ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │
|
│ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │
|
||||||
│ │ │ Bot Account │ │ │ Config (KV) │ │ │ │
|
│ │ │ PersonaLibrary │ │ │ Config (KV) │ │ │ │
|
||||||
│ │ │ (auto-created) │ │ └──────────────────┘ │ │ │
|
│ │ │ resolvePersona() │ │ └──────────────────┘ │ │ │
|
||||||
|
│ │ └──────────────────┘ │ │ │ │
|
||||||
|
│ │ ┌──────────────────┐ │ │ │ │
|
||||||
|
│ │ │ Bot Account │ │ │ │ │
|
||||||
|
│ │ │ (auto-created) │ │ │ │ │
|
||||||
│ │ └──────────────────┘ │ │ │ │
|
│ │ └──────────────────┘ │ │ │ │
|
||||||
│ │ ┌──────────────────┐ │ │ │ │
|
│ │ ┌──────────────────┐ │ │ │ │
|
||||||
│ │ │ Webapp (none) │ │ │ │ │
|
│ │ │ Webapp (none) │ │ │ │ │
|
||||||
│ │ └──────────────────┘ │ │ │ │
|
│ │ └──────────────────┘ │ │ │ │
|
||||||
│ └──────────────────────────────────────────────────────┘ │
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ KV Store (per-plugin) │ │
|
||||||
|
│ │ • ctx_{user}_{channel} — conversation history │ │
|
||||||
|
│ │ • persona_{user}_{ch} — persona override │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 Request Flow (happy path)
|
### 2.2 Request Flow (happy path — @-mention)
|
||||||
|
|
||||||
```
|
```
|
||||||
User types "@mattermore write a release note for v2.1"
|
User types "@matty write a release note for v2.1"
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Mattermore MessageHasBeenPosted fires
|
MessageHasBeenPosted fires
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
EngagementEngine.Wake(userId, channelId) → mark session Active
|
EngagementEngine.Wake(userId, channelId) → mark session Active
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@-mention detected → strip "@mattermore " prefix from prompt
|
@-mention detected → strip "@matty " prefix → "write a release note for v2.1"
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
RateLimiter.Allow(userId) → 200 or 429
|
RateLimiter.Allow(userId) → 200
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
ConversationStore.GetSession(userId, channelId, threadId) → previous messages for context
|
Check stop words → no match
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
OllamaClient.ChatCompletion(messages, stream=true, model=defaultModel)
|
Check persona override (KV store) → resolvePersona() → full description
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Ollama returns ndjson stream → plugin creates root post with live-updating thread
|
ConversationStore.BuildMessages(userId, channelId, prompt) → up to 10 turns
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Post created in channel → user sees response in thread
|
Trim context based on Brevity (e.g. brevity=1 → keep only current message)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
ConversationStore.Append(userId, channelId, threadId, userMsg, assistantMsg)
|
Prepend system prompt: "You are {persona}. Respond in character. ... Reply in {N} sentences."
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Calculate token cap: maxTokens = auto from brevity, or manual override
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
OllamaClient.ChatCompletionStream(model, messages, options)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Ollama returns ndjson stream → accumulate all tokens
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Create single post with full response (not a thread reply, not live-updated)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Thread Reply Flow (continues the conversation)
|
### 2.3 Engagement Session Flow (follow-up without @-mention)
|
||||||
|
|
||||||
```
|
```
|
||||||
User replies in thread → "Can you make it more formal?"
|
User: "@matty hello" → bot responds, session starts
|
||||||
│
|
User: "tell me a joke" (no @) → IsActive() → true → bot responds
|
||||||
▼
|
User: "what about weather?" (no @) → IsActive() → true → bot responds
|
||||||
Mattermore MessageHasBeenPosted fires (same thread as bot post)
|
User: "thanks" → stopWordMatch → Sleep() → bot signs off
|
||||||
│
|
User: "hello again" (no @, 10 min later)→ IsActive() → false → ignored
|
||||||
▼
|
User: "@matty hello again" → Wake() → bot responds, new session
|
||||||
EngagementEngine.IsActive(userId, threadId) → true
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Not an @-mention, but active thread → treat as continuation
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
ConversationStore.GetSession(userId, channelId, threadId) → full context so far
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
OllamaClient.ChatCompletion(...) → stream response into same thread
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
ConversationStore.Append(userId, channelId, userMsg, assistantMsg)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 Error Flow
|
### 2.4 Error Flow
|
||||||
|
|
||||||
| Failure point | Behaviour |
|
| Failure point | Behaviour |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Ollama unreachable | Ephemeral post: "Ollama at <url> unreachable. Check configuration." |
|
| Ollama unreachable | Post: "Matty is asleep. @matty when you need me." |
|
||||||
| Rate limit hit | Ephemeral post: "Rate limit exceeded. Try again in N seconds." |
|
| Rate limit hit | Post: "Rate limit exceeded. Please wait before sending another request." |
|
||||||
| Invalid model name | Ephemeral post: "Model 'xyz' not found on Ollama server." |
|
| Invalid persona | Ephemeral: "Unknown persona. Available: ..." |
|
||||||
| Timeout (>30 s) | Abort stream, post partial response + "Response truncated (timeout)." |
|
| Timeout (>30 s) | Post truncated with "(Matty is asleep...)" |
|
||||||
| Input too long | Reject with context window limit message; suggest `@mattermore new` to reset. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,89 +157,27 @@ ConversationStore.Append(userId, channelId, userMsg, assistantMsg)
|
|||||||
|
|
||||||
### 3.1 `plugin.json`
|
### 3.1 `plugin.json`
|
||||||
|
|
||||||
```json
|
Current settings (v0.1.16):
|
||||||
{
|
|
||||||
"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.",
|
|
||||||
"placeholder": "llama3:latest",
|
|
||||||
"default": "llama3:latest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "StopWords",
|
|
||||||
"display_name": "Stop Words (comma-separated)",
|
|
||||||
"type": "text",
|
|
||||||
"help_text": "When the user says one of these, the bot responds briefly and disengages.",
|
|
||||||
"placeholder": "thanks, thank you, bye, goodbye, that's all, done",
|
|
||||||
"default": "thanks, thank you, bye, goodbye, see you, that's all, that's it, done, all done, stop, quit, end"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`TODO(design):` Should `AllowedUserIDs` be a team/channel whitelist instead?
|
| Key | Type | Default | Purpose |
|
||||||
For now, keep user-level granularity — easy to widen later.
|
|---|---|---|---|
|
||||||
|
| `OllamaURL` | text | `http://localhost:11434` | Ollama server endpoint |
|
||||||
|
| `DefaultModel` | dropdown | `dolphin-llama3:latest` | Model for chat |
|
||||||
|
| `Persona` | dropdown | `assistant` | Character for the bot |
|
||||||
|
| `RepeatPenalty` | number | `11` (1.1) | Penalise repeated tokens |
|
||||||
|
| `FrequencyPenalty` | number | `1` (0.1) | Penalise frequent phrases |
|
||||||
|
| `Brevity` | dropdown | `3` | Max sentences |
|
||||||
|
| `DebugLogging` | bool | `false` | Enable verbose logging |
|
||||||
|
| `CustomPersona` | text | `""` | Override dropdown persona |
|
||||||
|
| `MaxTokens` | number | `0` | Manual token cap (0 = auto from Brevity) |
|
||||||
|
| `RateLimitPerMinute` | number | `0` | Requests/min/user (0 = unlimited) |
|
||||||
|
| `AllowedUserIDs` | text | `""` | Restrict to specific users |
|
||||||
|
| `StopWords` | text | `thanks, bye, goodbye, done, stop, quit, end` | Disengagement triggers |
|
||||||
|
|
||||||
### 3.2 Versioning
|
### 3.2 Versioning
|
||||||
|
|
||||||
Follow `release-workflow` skill: `v0.1.0`, `v0.2.0`, etc. Bump PATCH for
|
Follow `release-workflow` skill: `v0.1.x` for small iterations. Bump PATCH for
|
||||||
bugs, MINOR for features. MAJOR after 1.0 for breaking config schema changes.
|
bug fixes, MINOR for features. No MAJOR bump until stable release.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -236,171 +187,149 @@ bugs, MINOR for features. MAJOR after 1.0 for breaking config schema changes.
|
|||||||
|
|
||||||
| Hook | Purpose |
|
| Hook | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `OnActivate()` | Register slash command, create bot account via `API.CreateBot()`, initialise `EngagementEngine` |
|
| `OnActivate()` | Register `/ai` command, create bot account via `ensureBot()`, initialise `OllamaClient`, `ConversationStore`, `RateLimiter`, `EngagementEngine` |
|
||||||
| `OnConfigurationChange()` | Re-read System Console settings, validate OllamaURL, rebuild clients, rebuild stop-word trie |
|
| `OnConfigurationChange()` | Re-read System Console settings, validate OllamaURL, sync debug logging flag to OllamaClient |
|
||||||
| `MessageHasBeenPosted()` | **Primary entry point** — detect @-mentions and thread continuations, delegate to `EngagementEngine` |
|
| `MessageHasBeenPosted()` | **Primary entry point** — detect @-mentions and active sessions, delegate to EngagementEngine |
|
||||||
| `ExecuteCommand()` | **Alternative entry point** — intercept `/ai` and subcommands |
|
| `ExecuteCommand()` | **Alternative entry point** — intercept `/ai` and subcommands (`/ai persona`, `/ai new`, `/ai help`) |
|
||||||
| `OnDeactivate()` | Clean up goroutines, deactivate bot account, close idle HTTP connections |
|
| `Implemented()` | Declares handled hooks: `OnActivate`, `OnDeactivate`, `OnConfigurationChange`, `ExecuteCommand`, `MessageHasBeenPosted` |
|
||||||
|
|
||||||
### 4.2 Interaction Model
|
### 4.2 Interaction Model
|
||||||
|
|
||||||
The plugin has two entry points, but **@-mention is the primary UX**.
|
|
||||||
|
|
||||||
#### 4.2.1 @-mention (primary)
|
#### 4.2.1 @-mention (primary)
|
||||||
|
|
||||||
User types `@mattermore <prompt>` in any channel. The plugin:
|
User types `@matty <prompt>` in any channel. The plugin:
|
||||||
|
|
||||||
1. Detects the @-mention via `MessageHasBeenPosted` → `model.Post.Mentions`
|
1. Detects the @-mention via `MessageHasBeenPosted` → string match on `@matty`
|
||||||
2. Strips the bot username from the prompt
|
2. Strips the bot username from the prompt
|
||||||
3. Wakes the `EngagementEngine` for this user+thread
|
3. Checks stop words, rate limit, authorization
|
||||||
4. Creates a **thread root post** with the streamed response
|
4. Looks up persona override from KV store (set via `/ai persona <name>`)
|
||||||
5. Monitors thread replies for continuation — as long as the user keeps
|
5. Builds conversation context from `ConversationStore` (up to 10 turns)
|
||||||
replying in the thread, the bot auto-responds (no @-mention needed for
|
6. Trims context based on Brevity setting (e.g. brevity 1 = only current message)
|
||||||
follow-ups in the same thread)
|
7. Prepends persona system prompt with general instructions + brevity
|
||||||
|
8. Calculates token cap (auto from Brevity or manual MaxTokens override)
|
||||||
|
9. Wakes the `EngagementEngine` for this user+channel
|
||||||
|
10. Sends to Ollama via `ChatCompletionStream`
|
||||||
|
11. Accumulates all tokens, creates a single channel post with the full response
|
||||||
|
|
||||||
**When the bot does NOT respond:**
|
**When the bot does NOT respond:**
|
||||||
- Messages without `@mattermore` while the EngagementEngine is Sleeping
|
- Messages without `@matty` while the EngagementEngine is Sleeping
|
||||||
- Messages in threads the bot didn't start
|
- Messages during the Disengaging → Sleeping transition
|
||||||
- Messages from other bot accounts (infinite-loop guard)
|
- Messages from other bot accounts (infinite-loop guard)
|
||||||
|
|
||||||
#### 4.2.2 Slash command (alternative)
|
#### 4.2.2 Engagement Session (continuation)
|
||||||
|
|
||||||
`/ai <prompt>` works as a fallback for users who prefer it or when
|
Once a user is in an Active session (via @-mention or `/ai`), their subsequent
|
||||||
@-mention is inconvenient. Behaviour is identical — creates a thread,
|
non-@-mention messages in the same channel are treated as continuations:
|
||||||
activates the EngagementEngine, continues on thread replies.
|
|
||||||
|
```
|
||||||
|
User enters Active → subsequent messages auto-continue → stop word → Sleep
|
||||||
|
```
|
||||||
|
|
||||||
|
The session expires after 5 minutes of inactivity.
|
||||||
|
|
||||||
|
#### 4.2.3 Slash command (alternative)
|
||||||
|
|
||||||
|
`/ai <prompt>` works as a fallback. Behaviour is identical.
|
||||||
|
|
||||||
**Full syntax:**
|
**Full syntax:**
|
||||||
|
|
||||||
```
|
```
|
||||||
/ai <prompt>
|
/ai <prompt> — Chat with default model
|
||||||
— Send a prompt using the default model (current session context).
|
/ai persona — Show current active persona
|
||||||
/ai model <name> <prompt>
|
/ai persona list — List all available personas with descriptions
|
||||||
— Send a prompt using a specific model.
|
/ai persona <name> — Switch persona (persisted)
|
||||||
/ai new
|
/ai new — Reset conversation context + end session
|
||||||
— Reset conversation context for the current user+channel.
|
/ai help — Show help
|
||||||
/ai model list
|
|
||||||
— List available models from the Ollama server.
|
|
||||||
/ai help
|
|
||||||
— Show usage help.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.3 OllamaClient
|
### 4.3 OllamaClient
|
||||||
|
|
||||||
Interface (in `server/ollama/client.go`):
|
Located in `server/ollama.go` (package main, flat file).
|
||||||
|
|
||||||
```go
|
Key methods:
|
||||||
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 {
|
| Method | Purpose |
|
||||||
Model string `json:"model"`
|
|---|---|
|
||||||
Messages []ChatMessage `json:"messages"`
|
| `ChatCompletion(req)` | Non-streaming call (not used by main flow) |
|
||||||
Stream bool `json:"stream"`
|
| `ChatCompletionStream(req)` | **Primary** — returns `< -chan StreamEvent`, reads ndjson |
|
||||||
Options map[string]any `json:"options,omitempty"`
|
| `ListModels()` | Returns available model names |
|
||||||
}
|
| `Ping()` | Health check |
|
||||||
|
|
||||||
type ChatStreamEvent struct {
|
The client accepts a `logFunc` callback for debug logging (hooked to
|
||||||
Token string // delta content
|
`p.API.LogInfo` when `DebugLogging` is enabled).
|
||||||
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
|
### 4.4 ConversationStore
|
||||||
|
|
||||||
Store session context in Mattermost's KV store (plugin's `API.KVSet` /
|
Stored in KV store per `ctx_{userID}_{channelID}`. Contains the last 10
|
||||||
`KVGet`).
|
messages (5 user + 5 assistant). Context is trimmed further based on
|
||||||
|
Brevity setting before sending to Ollama.
|
||||||
|
|
||||||
| Key pattern | Value | TTL |
|
| Brevity | Messages kept |
|
||||||
|---|---|---|
|
|---|---|
|
||||||
| `ctx_{userID}_{channelID}` | `[]Message` (last N turns) | 1 hour |
|
| 1 | 1 (current only) |
|
||||||
| `ctx_{userID}_{channelID}_model` | Model name override | 1 hour |
|
| 2 | 2 (last exchange) |
|
||||||
|
| 3 | 4 (last 2 exchanges) |
|
||||||
`TODO(design):`
|
| 5 | 6 |
|
||||||
- How many turns to keep? **Proposed:** last 10 messages (5 user + 5 assistant).
|
| 10 | 10 |
|
||||||
- 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
|
### 4.5 RateLimiter
|
||||||
|
|
||||||
Token-bucket per user. Configurable requests/minute from System Console.
|
Token-bucket per user. Configurable from System Console.
|
||||||
Resets on config change. Stored in memory, not KV (ephemeral state).
|
Default: 0 (unlimited). Resets on config change. Stored in memory.
|
||||||
|
|
||||||
### 4.6 Engagement Lifecycle
|
### 4.6 EngagementEngine
|
||||||
|
|
||||||
The **EngagementEngine** is the core mechanism that prevents the bot from
|
Tracks active conversations per user+channel. Two states:
|
||||||
replying to everything in the channel. It manages whether the bot should
|
|
||||||
respond to a given message.
|
|
||||||
|
|
||||||
#### States
|
|
||||||
|
|
||||||
| State | Behaviour | Entered when |
|
| State | Behaviour | Entered when |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Sleeping** | Ignores all messages | Agent started, disengage triggered, timeout expired |
|
| **Sleeping** | Ignores all messages | Plugin start, stop word, `/ai new`, 5 min timeout |
|
||||||
| **Active** | Responds to @-mentions and thread replies in the active session | User @-mentions bot or sends `/ai` |
|
| **Active** | Responds to @-mentions and follow-ups | @-mention or `/ai` |
|
||||||
| **Disengaging** | Responds to current message with a closing note, then sleeps | Stop word detected or user sends stop command |
|
|
||||||
|
The timeout is 5 minutes of inactivity. On expiry, the session is silently
|
||||||
|
removed — next @-mention starts fresh.
|
||||||
|
|
||||||
|
### 4.7 Persona System
|
||||||
|
|
||||||
|
Located in `server/persona.go`. A library of 19 personas mapped by stable
|
||||||
|
short IDs:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐
|
assistant a helpful assistant. Be concise, 2-3 sentences max...
|
||||||
│ Sleeping │
|
canadian an overly polite Canadian who apologises excessively...
|
||||||
└────────┬────────┘
|
carlin George Carlin. Acerbic, observational, profane comedy...
|
||||||
│ @-mention or /ai
|
dnd a Dungeons & Dragons dungeon master...
|
||||||
▼
|
infomercial a 1980s TV infomercial host. Over-enthusiastic, loud...
|
||||||
┌─────────────────┐
|
jester a medieval court jester who mocks everyone...
|
||||||
┌──────│ Active │◄──── thread reply continues
|
mime a French mime. Respond with dramatic pauses...
|
||||||
│ └────────┬────────┘
|
motivational a motivational speaker who turns everything into a life lesson...
|
||||||
│ │ stop word / stop command / timeout
|
noir a noir detective who answers every question with another...
|
||||||
│ ▼
|
philosopher a philosopher who overcomplicates everything...
|
||||||
│ ┌─────────────────┐
|
pirate a pirate captain. Use pirate slang...
|
||||||
│ │ Disengaging │── responds once, then...
|
pirro a drunk Jeanine Pirro. Slur your words...
|
||||||
│ └────────┬────────┘
|
ramsay Gordon Ramsay but for code reviews. Aggressive chef...
|
||||||
│ │
|
scammer a tech support scammer from 2008. Alarmist, pushy...
|
||||||
└───────────────┘
|
shakespeare William Shakespeare himself. Respond in iambic pentameter...
|
||||||
|
surfer a surfer dude explaining complex topics badly...
|
||||||
|
sysadmin a grumpy sysadmin who thinks every question is stupid...
|
||||||
|
tiredparent a tired parent who's given up on life...
|
||||||
|
trump Donald Trump. Boastful, hyperbolic, third-person...
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Disengagement Triggers
|
Priority: **KV override** (`/ai persona <id>`) → **CustomPersona** (System Console text field) → **Persona** dropdown.
|
||||||
|
|
||||||
| Trigger | Example | Behaviour |
|
`resolvePersona(id)` looks up the short ID in the library. Returns empty
|
||||||
|
for unknown IDs (so `/ai persona shakespear` is properly rejected).
|
||||||
|
|
||||||
|
### 4.8 Debug Logging
|
||||||
|
|
||||||
|
When `DebugLogging` is enabled in System Console, the plugin logs:
|
||||||
|
|
||||||
|
| Log message | Source | What it contains |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Stop word | "thanks", "thank you", "bye", "goodbye", "that's all", "that's it" | Bot responds with a brief closing message, then sleeps |
|
| `"Config loaded"` | `configuration.go` | All settings on save |
|
||||||
| Stop command | `@mattermore stop` | Bot sleeps immediately, no closing message |
|
| `"Chat request"` | `hooks.go` | 13 fields: model, url, persona, brevity, tokens, repeat, freq, rate limit, allowed users, stop words, full system prompt |
|
||||||
| Thread timeout | No reply in thread for 15 minutes | Bot sleeps silently — next @-mention starts a fresh session |
|
| `"Ollama API call"` | `ollama.go` | Full request body sent to Ollama (model, messages, options) |
|
||||||
| Model EOS signal | Model outputs `[EOS]` naturally | Same as stop word — clean disengagement from the AI's side |
|
|
||||||
|
|
||||||
#### Stop word matching
|
|
||||||
|
|
||||||
Stop words are configurable via System Console (`StopWords`). The plugin
|
|
||||||
builds a case-insensitive trie at config load. Matching is done on the
|
|
||||||
**user's raw prompt** (after stripping the @-mention), before sending to
|
|
||||||
Ollama — this avoids the latency and cost of a round-trip just to detect
|
|
||||||
a goodbye.
|
|
||||||
|
|
||||||
The system prompt also instructs the model to append `[EOS]` when it
|
|
||||||
believes the conversation is naturally complete. The EngagementEngine
|
|
||||||
watches the response stream for this token as an additional trigger.
|
|
||||||
|
|
||||||
#### Thread timeout
|
|
||||||
|
|
||||||
15 minutes of inactivity in the thread → bot sleeps. The `ConversationStore`
|
|
||||||
TTL naturally handles cleanup. On wake, a new thread is created rather than
|
|
||||||
resuming the old one.
|
|
||||||
|
|
||||||
`TODO(design):` Should the timeout be configurable via System Console?
|
|
||||||
**Proposed:** yes — `EngagementTimeoutMinutes` setting, default 15.
|
|
||||||
|
|
||||||
#### Infinite-loop guard
|
|
||||||
|
|
||||||
The plugin never processes messages from:
|
|
||||||
- Its own bot account (checked via `post.UserId == botUserID`)
|
|
||||||
- Any other bot account (`post.UserId` cross-referenced against `model.Bot`)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -408,157 +337,116 @@ The plugin never processes messages from:
|
|||||||
|
|
||||||
### 5.1 API Mapping
|
### 5.1 API Mapping
|
||||||
|
|
||||||
| Plugin Action | Ollama Endpoint | Method |
|
| Plugin Action | Ollama Endpoint | Method | Stream |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| Chat completion (streaming) | `/api/chat` | POST |
|
| Chat completion | `/api/chat` | POST | Yes |
|
||||||
| List models | `/api/tags` | GET |
|
| List models | `/api/tags` | GET | No |
|
||||||
| Health check | `/api/tags` (or HEAD `/`) | GET |
|
| Health check | `/api/tags` | GET | No |
|
||||||
| 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
|
### 5.2 Streaming Strategy
|
||||||
|
|
||||||
1. Send POST to `/api/chat` with `"stream": true`.
|
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}`).
|
2. Read ndjson response body line by line.
|
||||||
3. On each `done:false` event, update the thread root post via
|
3. Accumulate all tokens in a buffer (no live post updates — avoids "Edited" spam).
|
||||||
`API.UpdatePost()` with accumulated content.
|
4. On `done:true`, create a single channel post with the full content.
|
||||||
4. On `done:true`, finalise the post with full content, usage stats
|
|
||||||
(token count, duration).
|
|
||||||
|
|
||||||
`TODO(design):` Post-update on every token may be too chatty. **Proposed:**
|
### 5.3 Token Auto-Calculation
|
||||||
batch updates every ~200 ms or every 3 tokens, whichever comes first.
|
|
||||||
|
|
||||||
### 5.3 Context Window Management
|
Brevity auto-calculates the token cap. Formula: `100 + brevity × 75`.
|
||||||
|
|
||||||
- Token counting: use Ollama's `num_ctx` parameter or a Go tokenizer
|
| Brevity | Auto tokens | Override with |
|
||||||
(tiktoken-go).
|
|---|---|---|
|
||||||
- If input exceeds configured limit (default: 4096 tokens), trim oldest
|
| 1 | 175 | MaxTokens > 175 |
|
||||||
messages until within limit, or reject if a single message overflows.
|
| 2 | 250 | MaxTokens > 250 |
|
||||||
- Display a warning: "Context window trimmed (oldest messages removed)."
|
| 3 | 325 | MaxTokens > 325 |
|
||||||
|
| 5 | 475 | MaxTokens > 475 |
|
||||||
|
| 10 | 850 | MaxTokens > 850 |
|
||||||
|
| Unlimited (0) | Falls back to MaxTokens (0 = Ollama default) | MaxTokens > 0 |
|
||||||
|
|
||||||
`TODO(design):` Should the user be able to set `num_ctx` per-request?
|
### 5.4 Penalties
|
||||||
`@mattermore --context 8192 ...`
|
|
||||||
|
|
||||||
### 5.4 Model Selection Precedence
|
RepeatPenalty and FrequencyPenalty use whole numbers divided by 10:
|
||||||
|
|
||||||
1. Per-request: `@mattermore model gemma3:12b write a poem`
|
| UI value | Sent to Ollama |
|
||||||
2. Session override: `/ai model gemma3:12b` (stored in KV)
|
|---|---|
|
||||||
3. Default from System Console: `DefaultModel`
|
| 10 | 1.0 (none) |
|
||||||
|
| 11 | 1.1 (mild) |
|
||||||
|
| 12 | 1.2 (strong) |
|
||||||
|
|
||||||
|
| UI value | Sent to Ollama |
|
||||||
|
|---|---|
|
||||||
|
| 0 | 0.0 (none) |
|
||||||
|
| 1 | 0.1 (mild) |
|
||||||
|
| 5 | 0.5 (strong) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Configuration
|
## 6. Configuration
|
||||||
|
|
||||||
### 6.1 System Console Settings
|
All configuration is done via **System Console → Plugins → Matty**.
|
||||||
|
The 12 settings are described in §3.1.
|
||||||
|
|
||||||
Defined in `plugin.json` `settings_schema` (see §3.1). The System Console
|
`OnConfigurationChange()` reloads all settings when saved. Plugins do not
|
||||||
provides the UI; the plugin reads via `OnConfigurationChange()`.
|
have a `SavePluginConfiguration` API in Mattermost v11.x, so `/ai persona`
|
||||||
|
uses the KV store to persist overrides instead.
|
||||||
### 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. If `StopWords` changed, rebuild the stop-word trie.
|
|
||||||
6. Log the change at debug level.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Data Flow — Detailed
|
## 7. Data Flow — Detailed
|
||||||
|
|
||||||
### 7.1 Full Lifecycle
|
### 7.1 MessageHasBeenPosted (full lifecycle)
|
||||||
|
|
||||||
```
|
```
|
||||||
Step User / System Component
|
Step User / System Component
|
||||||
──── ────────────────────────────── ──────────────────
|
──── ────────────────────────────── ──────────────────
|
||||||
1 User types "@mattermore explain TCP" Mattermost webapp
|
1 User types "@matty explain TCP"
|
||||||
2 MessageHasBeenPosted fires Mattermore plugin
|
|
||||||
3 EngagementEngine.Wake() EngagementEngine
|
|
||||||
3a Check not a bot post (loop guard)
|
|
||||||
3b Check @-mention or active thread
|
|
||||||
3c Transition to Active
|
|
||||||
4 Strip "@mattermore " → "explain TCP"
|
|
||||||
5 Check stop-word trie (no match)
|
|
||||||
6 RateLimiter.Allow(userId) RateLimiter
|
|
||||||
7 ConversationStore.GetSession(...) ConversationStore
|
|
||||||
8 Build []ChatMessage (system + history + new prompt)
|
|
||||||
9 OllamaClient.ChatCompletion(...) OllamaClient
|
|
||||||
10 Ollama streams ndjson tokens Ollama (remote)
|
|
||||||
11 Plugin accumulates tokens, updates Mattermore plugin
|
|
||||||
thread post every ~200ms
|
|
||||||
12 On done:true → finalise post Mattermore plugin
|
|
||||||
13 Check response for [EOS] signal EngagementEngine
|
|
||||||
14 ConversationStore.Append(...) ConversationStore
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Disengagement Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Step User / System Component
|
|
||||||
──── ────────────────────────────── ──────────────────
|
|
||||||
1 User replies "thanks that's all"
|
|
||||||
2 MessageHasBeenPosted fires
|
2 MessageHasBeenPosted fires
|
||||||
3 EngagementEngine.IsActive → true
|
3 Check: not bot's own post Loop guard
|
||||||
4 Strip @-mention if present
|
4 Check: not from OAuth bot Loop guard
|
||||||
5 Stop-word trie matches "that's all" StopWordTrie
|
5 Check: has @matty? yes Mention detection
|
||||||
6 EngagementEngine.Transition(Disengaging)
|
6 Check: EngagementEngine.IsActive? EngagementEngine
|
||||||
7 OllamaClient.ChatCompletion(lastMsg) (brief closing response)
|
7 Strip @matty → "explain TCP"
|
||||||
8 Post closing message in thread
|
8 Check stop words StopWordSet
|
||||||
9 EngagementEngine.Transition(Sleeping)
|
9 Check rate limit RateLimiter
|
||||||
|
10 Check authorization AllowedUserIDs
|
||||||
|
11 EngagementEngine.Wake() EngagementEngine
|
||||||
|
12 GetPersonaOverride → resolvePersona KV Store / personaLibrary
|
||||||
|
13 BuildMessages → conversation history ConversationStore
|
||||||
|
14 Trim context by Brevity hooks.go
|
||||||
|
15 Build system prompt + brevity hooks.go
|
||||||
|
16 Calculate token cap hooks.go
|
||||||
|
17 Build Ollama options (penalties) hooks.go
|
||||||
|
18 Log Chat request (if debug) hooks.go
|
||||||
|
19 ChatCompletionStream() OllamaClient
|
||||||
|
20 Accumulate tokens hooks.go
|
||||||
|
21 CreatePost with full response postReply()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.3 Thread Participation
|
|
||||||
|
|
||||||
Thread replies continue the conversation without requiring a new @-mention.
|
|
||||||
The EngagementEngine tracks which threads it owns (via `ownerThread_{threadID}`
|
|
||||||
in KV). For posts in owned threads:
|
|
||||||
- If the EngagementEngine is Active for this user+thread → respond
|
|
||||||
- If Sleeping → respond once to wake, then continue
|
|
||||||
- If Disengaging → respond with closing note, then sleep
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Security Model
|
## 8. Security Model
|
||||||
|
|
||||||
### 8.1 Input Sanitization
|
### 8.1 Input Sanitization
|
||||||
|
|
||||||
- Strip control characters / zero-width Unicode from prompts before sending
|
- Stop words checked before sending to Ollama (saves round-trip)
|
||||||
to Ollama.
|
- Rate limiting per user (configurable, default: unlimited)
|
||||||
- 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
|
### 8.2 Network Security
|
||||||
|
|
||||||
- Cap response tokens in the Ollama request (`options.num_predict`).
|
- `OllamaURL` validated as http/https at config load
|
||||||
- `TODO(design):` Add a "Report this response" feedback mechanism? Defer.
|
- TLS enabled by default
|
||||||
|
|
||||||
### 8.3 Network Security
|
### 8.3 Authorization
|
||||||
|
|
||||||
- `OllamaURL` must start with `http://` or `https://` — validate at config
|
- `AllowedUserIDs` restricts by Mattermost user ID
|
||||||
load.
|
- Empty = all authenticated users
|
||||||
- Reject internal IP ranges unless explicitly enabled by a
|
- Bot account created by plugin, managed automatically
|
||||||
`AllowPrivateNetworks` toggle (default: off for production).
|
|
||||||
- TLS verify enabled by default; optional `SkipTLSVerify` toggle (logged as
|
|
||||||
a warning).
|
|
||||||
|
|
||||||
### 8.4 Authorization
|
### 8.4 Infinite-Loop Guard
|
||||||
|
|
||||||
- If `AllowedUserIDs` is non-empty, reject requests from other users with a
|
The plugin never processes messages from:
|
||||||
"not authorized" ephemeral message.
|
- Its own bot account (`post.UserId == botUserID`)
|
||||||
- All plugin API calls are already scoped to authenticated Mattermost users.
|
- Other OAuth bots (`post.IsFromOAuthBot()`)
|
||||||
|
|
||||||
### 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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -566,49 +454,46 @@ in KV). For posts in owned threads:
|
|||||||
|
|
||||||
### 9.1 Logging
|
### 9.1 Logging
|
||||||
|
|
||||||
|
When Debug Logging is enabled:
|
||||||
|
|
||||||
| Level | When |
|
| Level | When |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `INFO` | Chat request (all 13 settings), Config loaded, Ollama API call body |
|
||||||
| `ERROR` | Ollama unreachable, config validation failure, KV store error |
|
| `ERROR` | Ollama unreachable, config validation failure, KV store error |
|
||||||
| `WARN` | Rate limit hit, context window trimmed, TLS skipped |
|
| `WARN` | Rate limit hit (with debug on) |
|
||||||
| `INFO` | Plugin activated/deactivated, config changed, first request |
|
|
||||||
| `DEBUG` | Every request/response round-trip (with userID, model, token count, latency, engagement state) |
|
|
||||||
|
|
||||||
### 9.2 Metrics (future)
|
### 9.2 Error Reporting
|
||||||
|
|
||||||
`TODO(design):` Expose Prometheus-style counters via a `/metrics` endpoint
|
- Ollama unreachable → "Matty is asleep. @matty when you need me."
|
||||||
if the Mattermost server has plugin metrics support:
|
- Rate limit → "Rate limit exceeded..."
|
||||||
- `mattermore_requests_total{user,model,status}`
|
- Unknown persona → "Unknown persona. Available: ..."
|
||||||
- `mattermore_latency_seconds`
|
- Stream error → appended as "(Matty is asleep...)" in the response
|
||||||
- `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
|
## 10. Resolved & Open Design Decisions
|
||||||
|
|
||||||
These are the unresolved questions that need answers before v0.1.0 ships.
|
### Resolved
|
||||||
|
|
||||||
| # | Question | Proposed | Alternatives | Decided? |
|
| # | Question | Decision |
|
||||||
|---|---|---|---|---|
|
|---|---|---|
|
||||||
| D1 | Reply style | **Thread posts** (not ephemeral) — community sees the interaction | Ephemeral, channel-wide | ✅ → DECISIONS.md |
|
| D1 | Reply style | **Direct channel posts** (not ephemeral, not threads) |
|
||||||
| D2 | Context scope | Per-channel per-user — each channel gets its own conversation | Per-user global, per-channel only, no context | ❌ |
|
| D4 | Webapp UI | **None** — @-mention + `/ai` + `/ai persona` covers everything |
|
||||||
| D3 | Context turns | Last 5 exchanges (10 messages) | Last N tokens, everything in TTL, sliding window | ❌ |
|
| D6 | Thread replies | **EngagementEngine** — active sessions track follow-ups without threads |
|
||||||
| D4 | Webapp UI | **None** — @-mention + thread replies cover all interaction | Channel header button, RHS panel | ✅ → DECISIONS.md |
|
| D11 | Engagement scope | **Per-user+channel** — separate sessions per channel |
|
||||||
| D5 | Model governance | All models available | Admin-restricted model list, least-privilege Ollama API key | ❌ |
|
| D12 | Thread timeout | **5 minutes** (not configurable yet) |
|
||||||
| D6 | Thread replies | **Core feature** — the EngagementEngine auto-follows thread replies | Slash-command-only, ephemeral | ✅ → DECISIONS.md |
|
|
||||||
| 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 | ❌ |
|
|
||||||
| D11 | Engagement scope | Per-user+channel | Per-user global, per-channel only | ❌ |
|
|
||||||
| D12 | Thread timeout | 15 minutes, configurable | Fixed 30min, no timeout | ❌ |
|
|
||||||
|
|
||||||
Each decision should be recorded in `DECISIONS.md` once resolved.
|
### Still Open
|
||||||
|
|
||||||
|
| # | Question | Proposed |
|
||||||
|
|---|---|---|
|
||||||
|
| D2 | Context scope | Per-channel per-user (implemented) |
|
||||||
|
| D3 | Context turns | Currently 10 messages; Brevity trims further |
|
||||||
|
| D5 | Model governance | All models available via System Console dropdown |
|
||||||
|
| D7 | `num_ctx` per request | Not implemented; Brevity controls token cap |
|
||||||
|
| D8 | Auto-pull models | No (returns error with list) |
|
||||||
|
| D9 | Prompt injection guard | Deferred — document risk |
|
||||||
|
| D10 | Metrics endpoint | Deferred |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -620,42 +505,67 @@ Each decision should be recorded in `DECISIONS.md` once resolved.
|
|||||||
make dist
|
make dist
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces `dist/com.forkless.mattermore-0.1.0.tar.gz` containing the server
|
Produces `dist/com.forkless.mattermore-{version}.tar.gz` containing the plugin
|
||||||
binaries and `plugin.json`.
|
manifest and platform binaries.
|
||||||
|
|
||||||
### 11.2 Upload
|
### 11.2 Upload
|
||||||
|
|
||||||
Via Mattermost System Console → Plugins → Upload Plugin, or via REST API:
|
Via Mattermost System Console → Plugins → Upload Plugin, or via 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
|
### 11.3 Upgrade
|
||||||
|
|
||||||
1. Upload new `.tar.gz` — Mattermost replaces the plugin binaries.
|
Upload new `.tar.gz` — Mattermost replaces the plugin binary.
|
||||||
2. Plugin's `OnConfigurationChange()` fires (config preserved).
|
Settings are preserved via KV store. Bot account is re-fetched (not re-created).
|
||||||
3. Bot account is re-fetched (not re-created) — idempotent.
|
|
||||||
4. If the KV schema changed, a migration in `OnActivate()` handles it.
|
|
||||||
|
|
||||||
### 11.4 Rollback
|
### 11.4 Local Testing
|
||||||
|
|
||||||
Re-upload the previous version's `.tar.gz`. No data migration needed if the
|
```bash
|
||||||
KV key schema hasn't changed. Bot account survives rollback. If it has,
|
docker compose -f dev/docker-compose.yml up -d
|
||||||
restore from backup or `DECISIONS.md` records the migration path.
|
```
|
||||||
|
|
||||||
|
Spins up Mattermost 11.6.1 + PostgreSQL 16 at http://localhost:8065.
|
||||||
|
Auto-creates admin user (admin/password).
|
||||||
|
Test rig is disposable: `docker compose down -v` wipes everything.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Future Considerations (v0.2+)
|
## 12. File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
mattermore/
|
||||||
|
├── plugin.json — Plugin manifest
|
||||||
|
├── Makefile — Build, dist, clean
|
||||||
|
├── .golangci.yml — Linter config
|
||||||
|
├── DESIGN.md — This document
|
||||||
|
├── PROJECT.md
|
||||||
|
├── CHANGES.md
|
||||||
|
├── DECISIONS.md
|
||||||
|
├── README.md
|
||||||
|
├── dev/
|
||||||
|
│ ├── docker-compose.yml — Local Mattermost + PostgreSQL
|
||||||
|
│ └── init.sh — Admin auto-creation script
|
||||||
|
├── server/
|
||||||
|
│ ├── main.go — plugin.ClientMain
|
||||||
|
│ ├── plugin.go — Plugin struct
|
||||||
|
│ ├── hooks.go — OnActivate, MessageHasBeenPosted, streaming
|
||||||
|
│ ├── command.go — /ai and subcommands, persona override
|
||||||
|
│ ├── configuration.go — Config struct, validation, OnConfigurationChange
|
||||||
|
│ ├── ollama.go — OllamaClient (streaming + blocking)
|
||||||
|
│ ├── store.go — ConversationStore (KV-backed)
|
||||||
|
│ ├── rate.go — RateLimiter (token bucket)
|
||||||
|
│ ├── engagement.go — EngagementEngine (sleep/active)
|
||||||
|
│ └── persona.go — PersonaLibrary with 19 personas
|
||||||
|
└── dist/
|
||||||
|
└── com.forkless.mattermore-*.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Future Considerations (v0.2+)
|
||||||
|
|
||||||
- **Multi-modal inputs** — Ollama supports images via `/api/chat` with
|
- **Multi-modal inputs** — Ollama supports images via `/api/chat` with
|
||||||
`images` field. Could allow image attachment in thread.
|
`images` field.
|
||||||
- **Multi-model routing** — Different models for different channel categories
|
- **Tool calling** — Ollama supports tool definitions for external APIs.
|
||||||
(e.g. code-review channel uses codellama).
|
- **Conversation export** — Allow users to export AI threads as markdown.
|
||||||
- **Tool calling** — Ollama supports tool definitions. Could let the
|
|
||||||
plugin fetch data from external APIs (weather, Jira, etc.).
|
|
||||||
- **Streaming into message attachments** — Render structured data (tables,
|
- **Streaming into message attachments** — Render structured data (tables,
|
||||||
JSON, code blocks) with better formatting.
|
JSON, code blocks) with better formatting.
|
||||||
- **Conversation export** — Allow users to export an AI thread as markdown.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user