Changeset 0.23.1 (#154)

This commit is contained in:
2026-03-06 13:26:25 +00:00
parent 2fc620e1ac
commit 4c6555cb06
38 changed files with 1536 additions and 4031 deletions

View File

@@ -209,6 +209,27 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
}
// ── ai_mode guard (v0.23.1) ────────────────────────────────────────────────
// Check channel ai_mode before doing any work. For DM channels with
// mention_only mode and no @mention in the content, persist the message
// but skip the completion entirely.
{
var aiMode string
_ = database.DB.QueryRowContext(c.Request.Context(), database.Q(`
SELECT COALESCE(ai_mode, 'auto') FROM channels WHERE id = $1
`), channelID).Scan(&aiMode)
if aiMode == "off" {
c.JSON(http.StatusForbidden, gin.H{"error": "AI responses are disabled for this channel"})
return
}
if aiMode == "mention_only" && extractFirstMention(req.Content) == "" {
// Deliver message without triggering completion
c.JSON(http.StatusOK, gin.H{"status": "delivered", "ai_skipped": true})
return
}
}
// ── Persona unwrap: persona overrides defaults, explicit request fields win ──
var personaSystemPrompt string
var personaID string // tracks active persona for KB scoping
@@ -386,10 +407,26 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
// Resolve effective workspace: channel.workspace_id > project.workspace_id
workspaceID, _ := h.stores.Channels.ResolveWorkspaceID(c.Request.Context(), channelID)
// ── @mention routing (v0.23.0) ──────────────
// Resolve @model-id or @persona-handle directly against the enabled
// model catalog and personas table. Works in ANY chat — no roster needed.
if mentionModel, mentionConfig, mentionPersona := h.resolveMention(c.Request.Context(), userID, req.Content); mentionModel != "" {
// ── @mention routing (v0.23.0 / v0.23.1) ─────────────────────────────────
// Resolve @persona-handle, @model-id, or @username.
// - User mention → skip completion, notify recipient (v0.23.1)
// - AI mention → override model/persona for this turn (v0.23.0)
mentionModel, mentionConfig, mentionPersona, mentionedUserID := h.resolveMention(c.Request.Context(), userID, req.Content)
if mentionedUserID != "" {
// Human @mention — message already persisted; notify, skip AI.
if hub, ok := c.Get("events_hub"); ok {
if h2, ok2 := hub.(interface {
NotifyUserMention(toUser, channel, fromUser string)
}); ok2 {
h2.NotifyUserMention(mentionedUserID, channelID, userID)
}
}
c.JSON(http.StatusOK, gin.H{"status": "delivered", "mentioned_user": mentionedUserID})
return
}
if mentionModel != "" {
req.Model = mentionModel
if mentionConfig != "" {
req.ProviderConfigID = mentionConfig
@@ -1127,15 +1164,20 @@ func (h *CompletionHandler) buildParticipantHint(channelID, userID, currentPerso
//
// Resolution order:
// 1. Persona handle (exact match, then prefix)
// 2. Model ID in enabled catalog (exact match, then prefix)
// 2. User handle/username (exact, then prefix) — v0.23.1
// When a user is @mentioned in a DM/channel, skip completion and deliver
// a notification instead. Returns non-empty mentionedUserID in that case.
// 3. Model ID in enabled catalog (exact match, then prefix)
//
// Returns (modelID, providerConfigID, *Persona) — all empty if no @mention found.
// Returns (modelID, providerConfigID, *Persona, mentionedUserID).
// Exactly one of (modelID, mentionedUserID) will be non-empty on a successful
// match, or all empty if no @mention found/resolved.
func (h *CompletionHandler) resolveMention(ctx context.Context, userID, content string) (string, string, *models.Persona) {
func (h *CompletionHandler) resolveMention(ctx context.Context, userID, content string) (string, string, *models.Persona, string) {
// Extract first @token
token := extractFirstMention(content)
if token == "" {
return "", "", nil
return "", "", nil, ""
}
// Normalize: lowercase, hyphens
@@ -1155,7 +1197,7 @@ func (h *CompletionHandler) resolveMention(ctx context.Context, userID, content
cfgID = *p.ProviderConfigID
}
log.Printf("[mention] @%s → persona %s (%s)", token, p.Name, p.BaseModelID)
return p.BaseModelID, cfgID, p
return p.BaseModelID, cfgID, p, ""
}
}
@@ -1177,13 +1219,42 @@ func (h *CompletionHandler) resolveMention(ctx context.Context, userID, content
cfgID = *p.ProviderConfigID
}
log.Printf("[mention] @%s → persona prefix %s (%s)", token, p.Name, p.BaseModelID)
return p.BaseModelID, cfgID, p
return p.BaseModelID, cfgID, p, ""
}
}
}
}
// 3. Try model_id in enabled catalog (exact)
// 3. Try username (exact match) — v0.23.1
// Only resolves to a user if the caller is not the same user (no self-mention)
var mentionedUserID string
err = database.DB.QueryRowContext(ctx, database.Q(`
SELECT id FROM users
WHERE LOWER(username) = $1 AND id != $2 AND is_approved = true
LIMIT 1
`), normalized, userID).Scan(&mentionedUserID)
if err == nil && mentionedUserID != "" {
log.Printf("[mention] @%s → user %s", token, mentionedUserID)
return "", "", nil, mentionedUserID
}
// 4. Try username (prefix — unambiguous only) — v0.23.1
var userCount int
database.DB.QueryRowContext(ctx, database.Q(`
SELECT COUNT(*) FROM users
WHERE LOWER(username) LIKE $1 AND id != $2 AND is_approved = true
`), normalized+"%", userID).Scan(&userCount)
if userCount == 1 {
database.DB.QueryRowContext(ctx, database.Q(`
SELECT id FROM users
WHERE LOWER(username) LIKE $1 AND id != $2 AND is_approved = true
LIMIT 1
`), normalized+"%", userID).Scan(&mentionedUserID)
if mentionedUserID != "" {
log.Printf("[mention] @%s → user prefix %s", token, mentionedUserID)
return "", "", nil, mentionedUserID
}
}
var modelID, provConfigID string
err = database.DB.QueryRowContext(ctx, database.Q(`
SELECT mc.model_id, mc.provider_config_id
@@ -1198,10 +1269,10 @@ func (h *CompletionHandler) resolveMention(ctx context.Context, userID, content
`), normalized).Scan(&modelID, &provConfigID)
if err == nil && modelID != "" {
log.Printf("[mention] @%s → model %s (config %s)", token, modelID, provConfigID)
return modelID, provConfigID, nil
return modelID, provConfigID, nil, ""
}
// 4. Try model_id prefix (unambiguous)
// 5. Try model_id prefix (unambiguous)
var prefixCount int
database.DB.QueryRowContext(ctx, database.Q(`
SELECT COUNT(DISTINCT mc.model_id)
@@ -1224,11 +1295,11 @@ func (h *CompletionHandler) resolveMention(ctx context.Context, userID, content
`), normalized+"%").Scan(&modelID, &provConfigID)
if modelID != "" {
log.Printf("[mention] @%s → model prefix %s (config %s)", token, modelID, provConfigID)
return modelID, provConfigID, nil
return modelID, provConfigID, nil, ""
}
}
return "", "", nil
return "", "", nil, ""
}
// extractFirstMention finds the first @token in content.