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