Changeset 0.23.2 (#155)

This commit is contained in:
2026-03-06 23:17:03 +00:00
parent 4c6555cb06
commit 2dc4514a57
36 changed files with 2784 additions and 192 deletions

View File

@@ -203,6 +203,141 @@ func (h *CompletionHandler) chainIfMentioned(
h.chainIfMentioned(channelID, userID, mentionPersona.ID, resp.Content, depth+1)
}
// chainToPersona triggers a completion for a specific persona by ID.
// Used by @all fan-out. Does NOT recurse (depth-1 only).
func (h *CompletionHandler) chainToPersona(channelID, userID, personaID, triggerContent string, depth int) {
defer func() {
if r := recover(); r != nil {
log.Printf("[chain-all] PANIC: %v", r)
}
}()
persona := ResolvePersona(h.stores, personaID, userID)
if persona == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
if h.hub != nil {
h.emitTyping(userID, channelID, persona.ID, persona.Name, true)
defer h.emitTyping(userID, channelID, persona.ID, persona.Name, false)
}
configID := ""
if persona.ProviderConfigID != nil {
configID = *persona.ProviderConfigID
}
chainReq := completionRequest{
ChannelID: channelID,
Model: persona.BaseModelID,
ProviderConfigID: configID,
}
providerCfg, providerID, model, resolvedConfigID, _, err := h.resolveConfig(userID, channelID, chainReq)
if err != nil {
log.Printf("[chain-all] Config failed for %s: %v", persona.Name, err)
return
}
configID = resolvedConfigID
provider, err := providers.Get(providerID)
if err != nil {
return
}
messages, err := h.loadConversation(channelID, userID, persona.SystemPrompt, persona.ID, model)
if err != nil {
return
}
boundary := fmt.Sprintf(
"[You are now %s. Previous assistant messages may be from different AI personas — ignore their style. Respond only as yourself per your system prompt.]",
persona.Name,
)
messages = append(messages, providers.Message{Role: "system", Content: boundary})
caps := capspkg.ResolveIntrinsic(model, nil, nil)
provReq := providers.CompletionRequest{
Model: model,
Messages: messages,
MaxTokens: capspkg.ResolveMaxOutput(model, caps),
}
if persona.Temperature != nil {
provReq.Temperature = persona.Temperature
}
if persona.ThinkingBudget != nil && *persona.ThinkingBudget > 0 {
if providerCfg.Settings == nil {
providerCfg.Settings = make(map[string]interface{})
}
providerCfg.Settings["extended_thinking"] = true
providerCfg.Settings["thinking_budget"] = *persona.ThinkingBudget
}
if hooks := providers.GetHooks(providerID); hooks != nil {
hooks.PreRequest(providerCfg, &provReq)
}
callStart := time.Now()
resp, err := provider.ChatCompletion(ctx, providerCfg, provReq)
latencyMs := int(time.Since(callStart).Milliseconds())
if err != nil {
if h.health != nil {
h.health.RecordError(configID, latencyMs, err.Error())
}
return
}
if h.health != nil {
h.health.RecordSuccess(configID, latencyMs)
}
if resp.Content == "" {
return
}
msgID, err := h.persistMessage(channelID, userID, "assistant", resp.Content, model,
resp.InputTokens, resp.OutputTokens, nil, nil, configID, persona.ID)
if err != nil {
return
}
if h.hub != nil {
payload, _ := json.Marshal(map[string]interface{}{
"id": msgID,
"channel_id": channelID,
"role": "assistant",
"content": resp.Content,
"model": model,
"participant_type": "persona",
"participant_id": persona.ID,
"display_name": persona.Name,
"avatar": persona.Avatar,
"tokens_used": resp.InputTokens + resp.OutputTokens,
"chain_depth": depth,
})
h.hub.SendToUser(userID, events.Event{
Label: "message.created",
Payload: payload,
Ts: time.Now().UnixMilli(),
})
}
if h.stores.Usage != nil {
entry := &models.UsageEntry{
ChannelID: &channelID,
UserID: userID,
ProviderConfigID: &configID,
ProviderScope: "global",
ModelID: model,
PromptTokens: resp.InputTokens,
CompletionTokens: resp.OutputTokens,
}
_ = h.stores.Usage.Log(ctx, entry)
}
log.Printf("[chain-all] ✅ %s responded (%d tokens) in channel %s",
persona.Name, resp.InputTokens+resp.OutputTokens, channelID[:min(8, len(channelID))])
}
// emitTyping sends a typing indicator via WebSocket.
func (h *CompletionHandler) emitTyping(userID, channelID, participantID, displayName string, start bool) {
eventType := "typing.start"