Add /ai persona command, debug logging toggle, context trimming, persona override
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
# make clean — Remove build artifacts
|
# make clean — Remove build artifacts
|
||||||
|
|
||||||
PLUGIN_ID := com.forkless.mattermore
|
PLUGIN_ID := com.forkless.mattermore
|
||||||
PLUGIN_VERSION := 0.1.0
|
PLUGIN_VERSION := 0.1.9
|
||||||
DIST_DIR := dist
|
DIST_DIR := dist
|
||||||
SERVER_DIR := server
|
SERVER_DIR := server
|
||||||
BUNDLE_NAME := $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
|
BUNDLE_NAME := $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
|
||||||
|
|||||||
+28
-21
@@ -2,7 +2,7 @@
|
|||||||
"id": "com.forkless.mattermore",
|
"id": "com.forkless.mattermore",
|
||||||
"name": "Matty",
|
"name": "Matty",
|
||||||
"description": "AI chat agent powered by Ollama.",
|
"description": "AI chat agent powered by Ollama.",
|
||||||
"version": "0.1.0",
|
"version": "0.1.9",
|
||||||
"min_server_version": "11.6.1",
|
"min_server_version": "11.6.1",
|
||||||
"server": {
|
"server": {
|
||||||
"executables": {
|
"executables": {
|
||||||
@@ -44,27 +44,27 @@
|
|||||||
"display_name": "Bot Persona",
|
"display_name": "Bot Persona",
|
||||||
"type": "dropdown",
|
"type": "dropdown",
|
||||||
"help_text": "The bot's character. Pick a persona or set a custom one in the config file.",
|
"help_text": "The bot's character. Pick a persona or set a custom one in the config file.",
|
||||||
"default": "a helpful assistant. Be concise, 2-3 sentences max, never repeat yourself.",
|
"default": "assistant",
|
||||||
"options": [
|
"options": [
|
||||||
{ "display_name": "D&D dungeon master", "value": "a Dungeons & Dragons dungeon master. Epic narrative, roll for initiative." },
|
{ "display_name": "D&D dungeon master", "value": "dnd" },
|
||||||
{ "display_name": "Donald Trump", "value": "Donald Trump. Boastful, hyperbolic, third-person. Think: rally speech, not catchphrase list. Keep it to 2 sentences." },
|
{ "display_name": "Donald Trump", "value": "trump" },
|
||||||
{ "display_name": "Drunken Jeanine Pirro", "value": "a drunk Jeanine Pirro. Slur your words, repeat yourself, be loud and uncensored." },
|
{ "display_name": "Drunken Jeanine Pirro", "value": "pirro" },
|
||||||
{ "display_name": "French mime", "value": "a French mime. Respond with dramatic pauses and gestures described in asterisks. Minimal words." },
|
{ "display_name": "French mime", "value": "mime" },
|
||||||
{ "display_name": "George Carlin", "value": "George Carlin. Acerbic, observational, profane comedy. Rant about the absurdity of everything. Use seven dirty words liberally. Keep it tight, one or two punchlines max." },
|
{ "display_name": "George Carlin", "value": "carlin" },
|
||||||
{ "display_name": "Gordon Ramsay (code review)", "value": "Gordon Ramsay but for code reviews. Aggressive chef. YELL. Brief." },
|
{ "display_name": "Gordon Ramsay (code review)", "value": "ramsay" },
|
||||||
{ "display_name": "Grumpy sysadmin", "value": "a grumpy sysadmin who thinks every question is stupid. Be sarcastic, brief, and annoyed." },
|
{ "display_name": "Grumpy sysadmin", "value": "sysadmin" },
|
||||||
{ "display_name": "Helpful assistant (concise)", "value": "a helpful assistant. Be concise, 2-3 sentences max, never repeat yourself." },
|
{ "display_name": "Helpful assistant (concise)", "value": "assistant" },
|
||||||
{ "display_name": "Infomercial host (1980s)", "value": "a 1980s TV infomercial host. Over-enthusiastic, loud, BUT WAIT THERE'S MORE." },
|
{ "display_name": "Infomercial host (1980s)", "value": "infomercial" },
|
||||||
{ "display_name": "Medieval court jester", "value": "a medieval court jester who mocks everyone. Old-timey insults, brief." },
|
{ "display_name": "Medieval court jester", "value": "jester" },
|
||||||
{ "display_name": "Motivational speaker", "value": "a motivational speaker who turns everything into a life lesson. Over-the-top positive but concise." },
|
{ "display_name": "Motivational speaker", "value": "motivational" },
|
||||||
{ "display_name": "Noir detective", "value": "a noir detective who answers every question with another question. Brooding, cryptic, short." },
|
{ "display_name": "Noir detective", "value": "noir" },
|
||||||
{ "display_name": "Overly polite Canadian", "value": "an overly polite Canadian who apologises excessively. Sorry, eh, keep it short." },
|
{ "display_name": "Overly polite Canadian", "value": "canadian" },
|
||||||
{ "display_name": "Philosophical overthinker", "value": "a philosopher who overcomplicates everything. Pseudo-intellectual, wordy is the point." },
|
{ "display_name": "Philosophical overthinker", "value": "philosopher" },
|
||||||
{ "display_name": "Pirate captain", "value": "a pirate captain. Use pirate slang, be theatrical, keep responses short." },
|
{ "display_name": "Pirate captain", "value": "pirate" },
|
||||||
{ "display_name": "Shakespearean author", "value": "a Shakespearean author. Respond in eloquent old English, but keep it brief." },
|
{ "display_name": "Shakespearean author", "value": "shakespeare" },
|
||||||
{ "display_name": "Surfer dude", "value": "a surfer dude explaining complex topics badly. Chill, inaccurate, gnarly." },
|
{ "display_name": "Surfer dude", "value": "surfer" },
|
||||||
{ "display_name": "Tech support scammer (2008)", "value": "a tech support scammer from 2008. Alarmist, pushy, uses ALL CAPS occasionally." },
|
{ "display_name": "Tech support scammer (2008)", "value": "scammer" },
|
||||||
{ "display_name": "Tired parent", "value": "a tired parent who's given up on life. Exhausted, relatable, monosyllabic when possible." }
|
{ "display_name": "Tired parent", "value": "tiredparent" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,6 +96,13 @@
|
|||||||
{ "display_name": "Unlimited", "value": "0" }
|
{ "display_name": "Unlimited", "value": "0" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "DebugLogging",
|
||||||
|
"display_name": "Debug Logging",
|
||||||
|
"type": "bool",
|
||||||
|
"help_text": "Enable detailed request/response logging in the server logs.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "CustomPersona",
|
"key": "CustomPersona",
|
||||||
"display_name": "Custom Persona (overrides dropdown)",
|
"display_name": "Custom Persona (overrides dropdown)",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
@@ -16,6 +17,7 @@ const helpText = `### Matty -- AI Chat Agent
|
|||||||
|
|
||||||
- @matty <prompt> -- Chat with the default model
|
- @matty <prompt> -- Chat with the default model
|
||||||
- @matty model <name> <prompt> -- Use a specific model
|
- @matty model <name> <prompt> -- Use a specific model
|
||||||
|
- @matty persona <name> -- Switch persona (e.g. "trump", "carlin", "sysadmin")
|
||||||
- @matty new -- Reset conversation context
|
- @matty new -- Reset conversation context
|
||||||
- @matty model list -- List available models
|
- @matty model list -- List available models
|
||||||
- @matty help -- Show this help
|
- @matty help -- Show this help
|
||||||
@@ -52,6 +54,15 @@ func (p *Plugin) ExecuteCommand(_ *plugin.Context, args *model.CommandArgs) (*mo
|
|||||||
p.engagementEngine.Sleep(args.UserId, args.ChannelId)
|
p.engagementEngine.Sleep(args.UserId, args.ChannelId)
|
||||||
return p.response("Conversation reset. @matty to start again."), nil
|
return p.response("Conversation reset. @matty to start again."), nil
|
||||||
|
|
||||||
|
case parts[0] == "persona" && len(parts) == 1:
|
||||||
|
return p.handlePersonaShow(args)
|
||||||
|
|
||||||
|
case parts[0] == "persona" && len(parts) >= 2 && parts[1] == "list":
|
||||||
|
return p.handlePersonaList(args)
|
||||||
|
|
||||||
|
case parts[0] == "persona" && len(parts) >= 2:
|
||||||
|
return p.handlePersonaSwitch(args, parts[1])
|
||||||
|
|
||||||
case parts[0] == "model" && len(parts) >= 2 && parts[1] == "list":
|
case parts[0] == "model" && len(parts) >= 2 && parts[1] == "list":
|
||||||
return p.handleModelList(args)
|
return p.handleModelList(args)
|
||||||
|
|
||||||
@@ -113,6 +124,57 @@ func (p *Plugin) handlePrompt(args *model.CommandArgs, modelName, prompt string)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePersonaShow shows the currently active persona.
|
||||||
|
func (p *Plugin) handlePersonaShow(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||||
|
override := p.GetPersonaOverride(args.UserId, args.ChannelId)
|
||||||
|
if override != "" {
|
||||||
|
return p.response("Active persona: " + override + "."), nil
|
||||||
|
}
|
||||||
|
cfg := p.getConfiguration()
|
||||||
|
if cfg.CustomPersona != "" {
|
||||||
|
return p.response("Active persona: custom."), nil
|
||||||
|
}
|
||||||
|
return p.response("Active persona: " + cfg.Persona + "."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePersonaList lists available personas with descriptions.
|
||||||
|
func (p *Plugin) handlePersonaList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||||
|
return p.response(p.listPersonas()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePersonaSwitch persists a persona override for the user+channel.
|
||||||
|
func (p *Plugin) handlePersonaSwitch(args *model.CommandArgs, personaID string) (*model.CommandResponse, *model.AppError) {
|
||||||
|
resolved := resolvePersona(personaID)
|
||||||
|
if resolved == "" || resolved == personaLibrary["assistant"] {
|
||||||
|
return p.response("Unknown persona. Available: " + p.listPersonas()), nil
|
||||||
|
}
|
||||||
|
if err := p.SavePersonaOverride(args.UserId, args.ChannelId, personaID); err != nil {
|
||||||
|
return p.response("Failed to save persona: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
return p.response("Persona switched to: " + personaID + ". @matty to test."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// listPersonas returns a formatted list of persona IDs with short descriptions.
|
||||||
|
func (p *Plugin) listPersonas() string {
|
||||||
|
ids := make([]string, 0, len(personaLibrary))
|
||||||
|
for id := range personaLibrary {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
sort.Strings(ids)
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("Available personas:\n")
|
||||||
|
for _, id := range ids {
|
||||||
|
desc := personaLibrary[id]
|
||||||
|
// Truncate to ~60 chars for a compact preview.
|
||||||
|
if len(desc) > 60 {
|
||||||
|
desc = desc[:57] + "..."
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(" %-12s %s\n", id, desc))
|
||||||
|
}
|
||||||
|
b.WriteString("Use /ai persona <name> to switch.")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
// handleModelList queries Ollama for available models and returns them.
|
// handleModelList queries Ollama for available models and returns them.
|
||||||
func (p *Plugin) handleModelList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
func (p *Plugin) handleModelList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||||
models, err := p.ollamaClient.ListModels()
|
models, err := p.ollamaClient.ListModels()
|
||||||
@@ -127,6 +189,28 @@ func (p *Plugin) handleModelList(args *model.CommandArgs) (*model.CommandRespons
|
|||||||
return p.response(b.String()), nil
|
return p.response(b.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// personaOverrideKey returns the KV store key for a user+channel persona override.
|
||||||
|
func personaOverrideKey(userID, channelID string) string {
|
||||||
|
return "persona_" + userID + "_" + channelID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePersonaOverride persists a persona override to the KV store.
|
||||||
|
func (p *Plugin) SavePersonaOverride(userID, channelID, personaID string) error {
|
||||||
|
if appErr := p.API.KVSet(personaOverrideKey(userID, channelID), []byte(personaID)); appErr != nil {
|
||||||
|
return fmt.Errorf("kvset: %w", appErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPersonaOverride returns the persisted persona override for a user+channel.
|
||||||
|
func (p *Plugin) GetPersonaOverride(userID, channelID string) string {
|
||||||
|
data, appErr := p.API.KVGet(personaOverrideKey(userID, channelID))
|
||||||
|
if appErr != nil || data == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
// parseArgs splits a command string into tokens.
|
// parseArgs splits a command string into tokens.
|
||||||
func parseArgs(input string) []string {
|
func parseArgs(input string) []string {
|
||||||
return strings.Fields(input)
|
return strings.Fields(input)
|
||||||
|
|||||||
+14
-2
@@ -13,12 +13,13 @@ type configuration struct {
|
|||||||
Persona string
|
Persona string
|
||||||
CustomPersona string
|
CustomPersona string
|
||||||
Brevity string
|
Brevity string
|
||||||
RepeatPenalty float64
|
RepeatPenalty int
|
||||||
FrequencyPenalty float64
|
FrequencyPenalty int
|
||||||
MaxTokens int
|
MaxTokens int
|
||||||
RateLimitPerMinute int
|
RateLimitPerMinute int
|
||||||
AllowedUserIDs string
|
AllowedUserIDs string
|
||||||
StopWords string
|
StopWords string
|
||||||
|
DebugLogging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConfiguration returns the current configuration, safe for concurrent use.
|
// getConfiguration returns the current configuration, safe for concurrent use.
|
||||||
@@ -76,13 +77,24 @@ func (p *Plugin) OnConfigurationChange() error {
|
|||||||
|
|
||||||
// Load from the System Console settings.
|
// Load from the System Console settings.
|
||||||
if err := p.API.LoadPluginConfiguration(&c); err != nil {
|
if err := p.API.LoadPluginConfiguration(&c); err != nil {
|
||||||
|
p.API.LogError("Config load failed", "error", err.Error())
|
||||||
return fmt.Errorf("failed to load plugin configuration: %w", err)
|
return fmt.Errorf("failed to load plugin configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.DebugLogging {
|
||||||
|
p.API.LogInfo("Config loaded", "model", c.DefaultModel, "persona", c.Persona, "brevity", c.Brevity, "repeat", c.RepeatPenalty, "freq", c.FrequencyPenalty, "tokens", c.MaxTokens)
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.setConfiguration(&c); err != nil {
|
if err := p.setConfiguration(&c); err != nil {
|
||||||
|
p.API.LogError("Config validate failed", "error", err.Error())
|
||||||
return fmt.Errorf("failed to set configuration: %w", err)
|
return fmt.Errorf("failed to set configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass the debug logging flag to the Ollama client.
|
||||||
|
if p.ollamaClient != nil {
|
||||||
|
p.ollamaClient.SetLogging(c.DebugLogging)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-17
@@ -55,7 +55,7 @@ func (p *Plugin) OnActivate() error {
|
|||||||
cfg = p.getConfiguration()
|
cfg = p.getConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ollamaClient = NewOllamaClient(cfg.OllamaURL)
|
p.ollamaClient = NewOllamaClient(cfg.OllamaURL, p.API.LogInfo)
|
||||||
p.conversationStore = NewConversationStore(p.API)
|
p.conversationStore = NewConversationStore(p.API)
|
||||||
p.rateLimiter = NewRateLimiter(cfg.RateLimitPerMinute)
|
p.rateLimiter = NewRateLimiter(cfg.RateLimitPerMinute)
|
||||||
p.engagementEngine = NewEngagementEngine()
|
p.engagementEngine = NewEngagementEngine()
|
||||||
@@ -183,30 +183,78 @@ func (p *Plugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend the persona system prompt to set the bot's character.
|
// Limit conversation history based on brevity — fewer sentences = less context
|
||||||
persona := cfg.CustomPersona
|
// to prevent the model re-addressing old messages.
|
||||||
if persona == "" {
|
|
||||||
persona = cfg.Persona
|
|
||||||
}
|
|
||||||
if persona != "" {
|
|
||||||
systemContent := "You are " + persona + ". Respond in character. Only respond to the latest message — do not re-address earlier messages. Vary your openings and phrasing each time."
|
|
||||||
// Append brevity instruction if set.
|
|
||||||
if cfg.Brevity != "" && cfg.Brevity != "0" {
|
if cfg.Brevity != "" && cfg.Brevity != "0" {
|
||||||
systemContent += " Reply in " + cfg.Brevity + " sentences or fewer."
|
brevity := 0
|
||||||
|
fmt.Sscanf(cfg.Brevity, "%d", &brevity)
|
||||||
|
if brevity > 0 && len(messages) > 2 {
|
||||||
|
// Keep: system prompt (added later) + (brevity * 2) lines of history + current prompt
|
||||||
|
keep := brevity * 2
|
||||||
|
if keep < 2 {
|
||||||
|
keep = 2 // always keep at least the current exchange
|
||||||
|
}
|
||||||
|
if keep < len(messages) {
|
||||||
|
messages = messages[len(messages)-keep:]
|
||||||
}
|
}
|
||||||
systemMsg := ChatMessage{
|
|
||||||
Role: "system",
|
|
||||||
Content: systemContent,
|
|
||||||
}
|
}
|
||||||
messages = append([]ChatMessage{systemMsg}, messages...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.API.LogDebug("Chat request", "model", cfg.DefaultModel, "persona", persona, "brevity", cfg.Brevity)
|
// Prepend the persona system prompt to set the bot's character.
|
||||||
|
// Priority: KV override > CustomPersona config > Persona dropdown config
|
||||||
|
persona := p.GetPersonaOverride(post.UserId, post.ChannelId)
|
||||||
|
if persona == "" {
|
||||||
|
persona = cfg.CustomPersona
|
||||||
|
}
|
||||||
|
if persona == "" {
|
||||||
|
persona = resolvePersona(cfg.Persona)
|
||||||
|
}
|
||||||
|
systemPrompt := ""
|
||||||
|
if persona != "" {
|
||||||
|
systemPrompt = "You are " + persona + ". Respond in character. Only respond to the latest message — do not re-address earlier messages. Vary your openings and phrasing each time."
|
||||||
|
if cfg.Brevity != "" && cfg.Brevity != "0" {
|
||||||
|
systemPrompt += " Reply in " + cfg.Brevity + " sentences or fewer."
|
||||||
|
}
|
||||||
|
messages = append([]ChatMessage{{
|
||||||
|
Role: "system",
|
||||||
|
Content: systemPrompt,
|
||||||
|
}}, messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the resolved token cap.
|
||||||
|
maxTokens := cfg.MaxTokens
|
||||||
|
if cfg.Brevity != "" && cfg.Brevity != "0" {
|
||||||
|
brevity := 0
|
||||||
|
fmt.Sscanf(cfg.Brevity, "%d", &brevity)
|
||||||
|
if brevity > 0 {
|
||||||
|
suggested := 100 + brevity*75
|
||||||
|
if maxTokens == 0 || suggested < maxTokens {
|
||||||
|
maxTokens = suggested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.DebugLogging {
|
||||||
|
p.API.LogInfo("Chat request",
|
||||||
|
"model", cfg.DefaultModel,
|
||||||
|
"ollama_url", cfg.OllamaURL,
|
||||||
|
"persona", cfg.Persona,
|
||||||
|
"custom_persona", cfg.CustomPersona,
|
||||||
|
"brevity", cfg.Brevity,
|
||||||
|
"max_tokens_config", cfg.MaxTokens,
|
||||||
|
"max_tokens_resolved", maxTokens,
|
||||||
|
"repeat", cfg.RepeatPenalty,
|
||||||
|
"freq", cfg.FrequencyPenalty,
|
||||||
|
"rate_limit", cfg.RateLimitPerMinute,
|
||||||
|
"allowed_users", cfg.AllowedUserIDs,
|
||||||
|
"stop_words", cfg.StopWords,
|
||||||
|
"system", systemPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
// Stream the full response from Ollama, then post once.
|
// Stream the full response from Ollama, then post once.
|
||||||
opts := map[string]any{}
|
opts := map[string]any{}
|
||||||
if cfg.MaxTokens > 0 {
|
if maxTokens > 0 {
|
||||||
opts["num_predict"] = cfg.MaxTokens
|
opts["num_predict"] = maxTokens
|
||||||
}
|
}
|
||||||
if cfg.RepeatPenalty > 0 {
|
if cfg.RepeatPenalty > 0 {
|
||||||
opts["repeat_penalty"] = float64(cfg.RepeatPenalty) / 10.0
|
opts["repeat_penalty"] = float64(cfg.RepeatPenalty) / 10.0
|
||||||
|
|||||||
+22
-1
@@ -9,19 +9,37 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// logFunc is a callback for logging (hooked to the Mattermost plugin API).
|
||||||
|
type logFunc func(msg string, keyValuePairs ...any)
|
||||||
|
|
||||||
// OllamaClient communicates with the Ollama API.
|
// OllamaClient communicates with the Ollama API.
|
||||||
type OllamaClient struct {
|
type OllamaClient struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
logInfo logFunc
|
||||||
|
logEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogging enables or disables request/response logging.
|
||||||
|
func (c *OllamaClient) SetLogging(enabled bool) {
|
||||||
|
c.logEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOllamaClient creates a new client for the given Ollama server URL.
|
// NewOllamaClient creates a new client for the given Ollama server URL.
|
||||||
func NewOllamaClient(baseURL string) *OllamaClient {
|
func NewOllamaClient(baseURL string, logInfo logFunc) *OllamaClient {
|
||||||
return &OllamaClient{
|
return &OllamaClient{
|
||||||
baseURL: strings.TrimRight(baseURL, "/"),
|
baseURL: strings.TrimRight(baseURL, "/"),
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
|
logInfo: logInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logRequest logs the full Ollama API request for debugging.
|
||||||
|
func (c *OllamaClient) logRequest(model, body string) {
|
||||||
|
if c.logEnabled && c.logInfo != nil {
|
||||||
|
c.logInfo("Ollama API call", "model", model, "url", c.baseURL+"/api/chat", "body", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +131,9 @@ func (c *OllamaClient) ChatCompletionStream(req *ChatRequest) (<-chan StreamEven
|
|||||||
return nil, fmt.Errorf("marshal request: %w", err)
|
return nil, fmt.Errorf("marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the full API request for debugging.
|
||||||
|
c.logRequest(req.Model, string(body))
|
||||||
|
|
||||||
httpReq, err := http.NewRequest("POST", c.baseURL+"/api/chat", strings.NewReader(string(body)))
|
httpReq, err := http.NewRequest("POST", c.baseURL+"/api/chat", strings.NewReader(string(body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create request: %w", err)
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// personaLibrary maps stable IDs to persona descriptions.
|
||||||
|
// The IDs never change — only the descriptions get refined.
|
||||||
|
var personaLibrary = map[string]string{
|
||||||
|
"assistant": "a helpful assistant. Be concise, 2-3 sentences max, never repeat yourself.",
|
||||||
|
"carlin": "George Carlin. Acerbic, observational, profane comedy. Rant about the absurdity of everything. Use seven dirty words liberally. Keep it tight, one or two punchlines max.",
|
||||||
|
"canadian": "an overly polite Canadian who apologises excessively. Sorry, eh, keep it short.",
|
||||||
|
"dnd": "a Dungeons & Dragons dungeon master. Epic narrative, roll for initiative.",
|
||||||
|
"infomercial": "a 1980s TV infomercial host. Over-enthusiastic, loud, ALWAYS acting like you're pitching a product. Be concise — one short pitch per response.",
|
||||||
|
"jester": "a medieval court jester who mocks everyone. Old-timey insults, brief.",
|
||||||
|
"mime": "a French mime. Respond with dramatic pauses and gestures described in asterisks. Minimal words.",
|
||||||
|
"motivational": "a motivational speaker who turns everything into a life lesson. Over-the-top positive but concise.",
|
||||||
|
"noir": "a noir detective who answers every question with another question. Brooding, cryptic, short.",
|
||||||
|
"philosopher": "a philosopher who overcomplicates everything. Pseudo-intellectual, wordy is the point.",
|
||||||
|
"pirate": "a pirate captain. Use pirate slang, be theatrical, keep responses short.",
|
||||||
|
"pirro": "a drunk Jeanine Pirro. Slur your words, repeat yourself, be loud and uncensored.",
|
||||||
|
"ramsay": "Gordon Ramsay but for code reviews. Aggressive chef. YELL. Brief.",
|
||||||
|
"shakespeare": "a Shakespearean author. Respond in eloquent old English, but keep it brief.",
|
||||||
|
"surfer": "a surfer dude explaining complex topics badly. Chill, inaccurate, gnarly.",
|
||||||
|
"sysadmin": "a grumpy sysadmin who thinks every question is stupid. Be sarcastic, brief, and annoyed.",
|
||||||
|
"tiredparent": "a tired parent who's given up on life. Exhausted, relatable, monosyllabic when possible.",
|
||||||
|
"trump": "Donald Trump. Boastful, hyperbolic, third-person, rally speech style, vocabulary of a 12 year old. Use his usual cadence naturally. Keep it to 2 sentences.",
|
||||||
|
"scammer": "a tech support scammer from 2008. Alarmist, pushy, uses ALL CAPS occasionally.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePersona takes a value from the dropdown (stable ID or legacy full text)
|
||||||
|
// and returns the full persona description. Falls back to the default if unknown.
|
||||||
|
func resolvePersona(value string) string {
|
||||||
|
if p, ok := personaLibrary[value]; ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
// If it's already a full description (e.g. old saved config), pass through.
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return personaLibrary["assistant"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user