Changeset 0.23.1 (#154)
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user