This commit is contained in:
HugeFrog24
2026-09-03 21:55:36 +02:00
parent c36e1846f5
commit 89458480f3
17 changed files with 1593 additions and 127 deletions
+36 -15
View File
@@ -27,7 +27,7 @@ var mcpUnsupportedCount atomic.Uint64
type mcpCall struct{ server, name, input string }
func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages []anthropic.BetaMessageParam, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int, onSegment func(string) error) (string, error) {
func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages []anthropic.BetaMessageParam, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int, onSegment func(string) error, onProgress func(string)) (string, error) {
staticPrompt := strings.TrimSpace(b.config.SystemPrompts["custom_instructions"])
InfoLogger.Printf("Sending %d messages to Anthropic", len(messages))
@@ -107,11 +107,13 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
params.Tools = tools
}
var fullText strings.Builder
var full string
var lastMsg anthropic.BetaMessage
fileRetries, pauseContinuations := 0, 0
for {
joined, msg, streamErr := b.streamMessages(ctx, params, onSegment)
// full is passed as the progress prefix so that, across pause_turn
// continuations, the user watches the whole reply so far.
joined, msg, streamErr := b.streamMessages(ctx, params, onSegment, onProgress, full)
if streamErr != nil {
var apiErr *anthropic.Error
if !errors.As(streamErr, &apiErr) || apiErr.StatusCode != http.StatusNotFound {
@@ -136,12 +138,7 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
}
lastMsg = msg
if joined != "" {
if fullText.Len() > 0 {
fullText.WriteString("\n\n")
}
fullText.WriteString(joined)
}
full = joinNonEmpty(full, joined)
if msg.StopReason == anthropic.BetaStopReasonPauseTurn {
pauseContinuations++
@@ -156,11 +153,11 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
break
}
if fullText.Len() == 0 {
if full == "" {
return "", emptyStreamError(string(lastMsg.StopReason),
lastMsg.Usage.OutputTokensDetails.ThinkingTokens, params.MaxTokens)
}
return fullText.String(), nil
return full, nil
}
func webSearchTools(cfg *WebSearchConfig) []anthropic.BetaToolUnionParam {
@@ -276,7 +273,12 @@ func thinkingParamFromConfig(mode, display string) (anthropic.BetaThinkingConfig
}
}
func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNewParams, onSegment func(string) error) (string, anthropic.BetaMessage, error) {
// streamMessages runs one streaming request and returns its text blocks joined.
// onSegment fires with each completed text block; onProgress fires on every
// text delta with the whole reply so far, prefix included, for sinks that
// redraw in place rather than append. prefix is what earlier requests of the
// same turn already produced and is not part of the returned text.
func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNewParams, onSegment func(string) error, onProgress func(string), prefix string) (string, anthropic.BetaMessage, error) {
stream := b.anthropicClient.Beta.Messages.NewStreaming(ctx, params)
defer func() {
if err := stream.Close(); err != nil {
@@ -286,7 +288,8 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
var (
message anthropic.BetaMessage
allSegments []string
done string // this request's completed text blocks, joined
doneAll = prefix // same, with the turn's earlier requests in front
currentKind string
currentText strings.Builder
currentThinking strings.Builder
@@ -336,6 +339,9 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
case "text_delta":
if currentKind == "text" {
currentText.WriteString(cbd.Delta.Text)
if onProgress != nil {
onProgress(joinNonEmpty(doneAll, currentText.String()))
}
}
case "thinking_delta":
if currentKind == "thinking" {
@@ -352,7 +358,8 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
case "text":
seg := strings.TrimSpace(currentText.String())
if seg != "" {
allSegments = append(allSegments, seg)
done = joinNonEmpty(done, seg)
doneAll = joinNonEmpty(doneAll, seg)
if onSegment != nil {
if cbErr := onSegment(seg); cbErr != nil {
ErrorLogger.Printf("[stream] onSegment failed: %v", cbErr)
@@ -428,7 +435,21 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
}
}
return strings.Join(allSegments, "\n\n"), message, nil
return done, message, nil
}
// joinNonEmpty is the one rule for assembling reply text from parts: a blank
// line between them, empty parts skipped. The live draft, the final message,
// and the stored partial all go through it, so they can never disagree.
func joinNonEmpty(a, b string) string {
switch {
case a == "":
return b
case b == "":
return a
default:
return a + "\n\n" + b
}
}
func emptyStreamError(stopReason string, thinkingTokens, maxTokens int64) error {