Files
CyberStrikeAI/internal/multiagent/eino_summarize_model_guard.go
T

215 lines
6.9 KiB
Go

package multiagent
import (
"context"
"errors"
"fmt"
"strings"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
)
type einoSummarizationModelError struct {
err error
}
func newEinoSummarizationModelError(err error) error {
if err == nil {
return nil
}
var existing *einoSummarizationModelError
if errors.As(err, &existing) {
return err
}
return &einoSummarizationModelError{err: err}
}
func (e *einoSummarizationModelError) Error() string {
if e == nil || e.err == nil {
return "summarization model error"
}
return "summarization model error: " + e.err.Error()
}
func (e *einoSummarizationModelError) Unwrap() error {
if e == nil {
return nil
}
return e.err
}
func einoSummarizationModelRawErrorText(err error) (string, bool) {
var summaryErr *einoSummarizationModelError
if !errors.As(err, &summaryErr) || summaryErr == nil || summaryErr.err == nil {
return "", false
}
return strings.TrimSpace(summaryErr.err.Error()), true
}
type nonEmptySummaryChatModel struct {
base model.BaseChatModel
}
func newNonEmptySummaryChatModel(base model.BaseChatModel) model.BaseChatModel {
return &nonEmptySummaryChatModel{base: base}
}
func (m *nonEmptySummaryChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
out, err := m.base.Generate(ctx, input, opts...)
if err != nil {
return out, newEinoSummarizationModelError(err)
}
if strings.TrimSpace(classicAssistantTextContent(out)) == "" {
return out, newEinoSummarizationModelError(fmt.Errorf("summary content is empty: %s", classicSummaryEmptyDiagnostics(out)))
}
return out, nil
}
func (m *nonEmptySummaryChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
return m.base.Stream(ctx, input, opts...)
}
type nonEmptyAgenticSummaryModel struct {
base model.BaseModel[*schema.AgenticMessage]
}
func newNonEmptyAgenticSummaryModel(base model.BaseModel[*schema.AgenticMessage]) model.BaseModel[*schema.AgenticMessage] {
return &nonEmptyAgenticSummaryModel{base: base}
}
func (m *nonEmptyAgenticSummaryModel) Generate(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
out, err := m.base.Generate(ctx, input, opts...)
if err != nil {
return out, newEinoSummarizationModelError(err)
}
if strings.TrimSpace(agenticAssistantTextContent(out)) == "" {
return out, newEinoSummarizationModelError(fmt.Errorf("summary content is empty: %s", agenticSummaryEmptyDiagnostics(out)))
}
return out, nil
}
func (m *nonEmptyAgenticSummaryModel) Stream(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) {
return m.base.Stream(ctx, input, opts...)
}
func classicAssistantTextContent(msg *schema.Message) string {
if msg == nil || msg.Role != schema.Assistant {
return ""
}
parts := make([]string, 0, len(msg.AssistantGenMultiContent))
for _, part := range msg.AssistantGenMultiContent {
if part.Type == schema.ChatMessagePartTypeText && part.Text != "" {
parts = append(parts, part.Text)
}
}
if len(parts) > 0 {
return strings.Join(parts, "\n")
}
return msg.Content
}
func agenticAssistantTextContent(msg *schema.AgenticMessage) string {
if msg == nil || msg.Role != schema.AgenticRoleTypeAssistant {
return ""
}
parts := make([]string, 0, len(msg.ContentBlocks))
for _, block := range msg.ContentBlocks {
if block != nil && block.AssistantGenText != nil {
parts = append(parts, block.AssistantGenText.Text)
}
}
return strings.Join(parts, "\n")
}
func classicSummaryEmptyDiagnostics(msg *schema.Message) string {
if msg == nil {
return "model returned nil message"
}
var textParts, reasoningParts, otherParts int
var multiTextRunes, multiReasoningRunes int
for _, part := range msg.AssistantGenMultiContent {
switch part.Type {
case schema.ChatMessagePartTypeText:
textParts++
multiTextRunes += len([]rune(strings.TrimSpace(part.Text)))
case schema.ChatMessagePartTypeReasoning:
reasoningParts++
if part.Reasoning != nil {
multiReasoningRunes += len([]rune(strings.TrimSpace(part.Reasoning.Text)))
}
default:
otherParts++
}
}
reasoningRunes := len([]rune(strings.TrimSpace(msg.ReasoningContent))) + multiReasoningRunes
fields := []string{
fmt.Sprintf("role=%s", msg.Role),
fmt.Sprintf("content_runes=%d", len([]rune(strings.TrimSpace(msg.Content)))+multiTextRunes),
fmt.Sprintf("reasoning_runes=%d", reasoningRunes),
fmt.Sprintf("text_parts=%d", textParts),
fmt.Sprintf("reasoning_parts=%d", reasoningParts),
fmt.Sprintf("other_parts=%d", otherParts),
fmt.Sprintf("tool_calls=%d", len(msg.ToolCalls)),
}
if msg.ResponseMeta != nil {
fields = append(fields, fmt.Sprintf("finish_reason=%q", msg.ResponseMeta.FinishReason))
if usage := msg.ResponseMeta.Usage; usage != nil {
fields = append(fields,
fmt.Sprintf("prompt_tokens=%d", usage.PromptTokens),
fmt.Sprintf("completion_tokens=%d", usage.CompletionTokens),
fmt.Sprintf("total_tokens=%d", usage.TotalTokens),
fmt.Sprintf("reasoning_tokens=%d", usage.CompletionTokensDetails.ReasoningTokens),
)
}
}
if reasoningRunes > 0 {
fields = append(fields, "hint=模型返回了 reasoning_content 但没有返回可作为摘要正文的 content;请检查 DeepSeek thinking 是否已在摘要请求中关闭")
}
return strings.Join(fields, " ")
}
func agenticSummaryEmptyDiagnostics(msg *schema.AgenticMessage) string {
if msg == nil {
return "model returned nil agentic message"
}
var textBlocks, reasoningBlocks, otherBlocks int
var textRunes, reasoningRunes int
for _, block := range msg.ContentBlocks {
if block == nil {
continue
}
switch {
case block.AssistantGenText != nil:
textBlocks++
textRunes += len([]rune(strings.TrimSpace(block.AssistantGenText.Text)))
case block.Reasoning != nil:
reasoningBlocks++
reasoningRunes += len([]rune(strings.TrimSpace(block.Reasoning.Text)))
default:
otherBlocks++
}
}
fields := []string{
fmt.Sprintf("role=%s", msg.Role),
fmt.Sprintf("content_runes=%d", textRunes),
fmt.Sprintf("reasoning_runes=%d", reasoningRunes),
fmt.Sprintf("text_blocks=%d", textBlocks),
fmt.Sprintf("reasoning_blocks=%d", reasoningBlocks),
fmt.Sprintf("other_blocks=%d", otherBlocks),
}
if msg.ResponseMeta != nil && msg.ResponseMeta.TokenUsage != nil {
usage := msg.ResponseMeta.TokenUsage
fields = append(fields,
fmt.Sprintf("prompt_tokens=%d", usage.PromptTokens),
fmt.Sprintf("completion_tokens=%d", usage.CompletionTokens),
fmt.Sprintf("total_tokens=%d", usage.TotalTokens),
fmt.Sprintf("reasoning_tokens=%d", usage.CompletionTokensDetails.ReasoningTokens),
)
}
if reasoningRunes > 0 {
fields = append(fields, "hint=模型返回了 reasoning block 但没有返回可作为摘要正文的 text block;请检查 DeepSeek thinking 是否已在摘要请求中关闭")
}
return strings.Join(fields, " ")
}