mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 04:51:01 +02:00
Add files via upload
This commit is contained in:
@@ -301,7 +301,7 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
|
||||
// 目标:以主 agent(编排器)视角输出整轮迭代
|
||||
// - 保留:编排器工具调用/结果、对子代理的 task 调度、子代理最终回复(不含推理)
|
||||
// - 丢弃:thinking/planning/progress 等噪声、子代理的工具细节与推理过程
|
||||
if d.EventType == "progress" || d.EventType == "thinking" || d.EventType == "planning" {
|
||||
if d.EventType == "progress" || d.EventType == "thinking" || d.EventType == "reasoning_chain" || d.EventType == "planning" {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
+81
-81
@@ -19,6 +19,7 @@ import (
|
||||
"cyberstrike-ai/internal/agent"
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/reasoning"
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
"cyberstrike-ai/internal/multiagent"
|
||||
@@ -201,6 +202,14 @@ type ChatAttachment struct {
|
||||
ServerPath string `json:"serverPath,omitempty"` // 已保存在 chat_uploads 下的绝对路径(由 POST /api/chat-uploads 返回)
|
||||
}
|
||||
|
||||
// ChatReasoningRequest 对话页「模型推理」意图(仅 Eino 路径消费;原生 agent-loop 忽略)。
|
||||
type ChatReasoningRequest struct {
|
||||
// Mode: default(跟随系统)| off | on | auto
|
||||
Mode string `json:"mode,omitempty"`
|
||||
// Effort: low | medium | high | max;空表示不指定(由系统默认与各 profile 决定)。
|
||||
Effort string `json:"effort,omitempty"`
|
||||
}
|
||||
|
||||
// ChatRequest 聊天请求
|
||||
type ChatRequest struct {
|
||||
Message string `json:"message" binding:"required"`
|
||||
@@ -209,10 +218,18 @@ type ChatRequest struct {
|
||||
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
||||
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
||||
Hitl *HITLRequest `json:"hitl,omitempty"`
|
||||
Reasoning *ChatReasoningRequest `json:"reasoning,omitempty"`
|
||||
// Orchestration 仅对 /api/multi-agent、/api/multi-agent/stream:deep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。/api/eino-agent* 不使用此字段。
|
||||
Orchestration string `json:"orchestration,omitempty"`
|
||||
}
|
||||
|
||||
func chatReasoningToClientIntent(r *ChatReasoningRequest) *reasoning.ClientIntent {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &reasoning.ClientIntent{Mode: r.Mode, Effort: r.Effort}
|
||||
}
|
||||
|
||||
type HITLRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
@@ -567,14 +584,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
||||
agentHistoryMessages = []agent.ChatMessage{}
|
||||
} else {
|
||||
// 将数据库消息转换为Agent消息格式
|
||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
||||
for _, msg := range historyMessages {
|
||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
||||
}
|
||||
} else {
|
||||
@@ -775,6 +785,7 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
||||
progressCallback,
|
||||
h.agentsMarkdownDir,
|
||||
"deep",
|
||||
nil,
|
||||
)
|
||||
if errMA != nil {
|
||||
if shouldPersistEinoAgentTraceAfterRunError(ctx) {
|
||||
@@ -788,17 +799,8 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
||||
return "", conversationID, errMA
|
||||
}
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(resultMA.MCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(resultMA.MCPExecutionIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
_, err = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
resultMA.Response, mcpIDsJSON, time.Now(), assistantMessageID,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(err))
|
||||
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, resultMA.Response, resultMA.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(resultMA.LastAgentTraceInput)); errU != nil {
|
||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(errU))
|
||||
}
|
||||
} else {
|
||||
if _, err = h.db.AddMessage(conversationID, "assistant", resultMA.Response, resultMA.MCPExecutionIDs); err != nil {
|
||||
@@ -823,17 +825,8 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
||||
|
||||
// 更新助手消息内容与 MCP 执行 ID(与 stream 一致)
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(result.MCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
_, err = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
result.Response, mcpIDsJSON, time.Now(), assistantMessageID,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(err))
|
||||
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput)); errU != nil {
|
||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(errU))
|
||||
}
|
||||
} else {
|
||||
if _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs); err != nil {
|
||||
@@ -891,10 +884,12 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
return ""
|
||||
}
|
||||
|
||||
// thinking_stream_*:不逐条落库,按 streamId 聚合,在后续关键事件前补一条可持久化的 thinking
|
||||
// thinking_stream_*(ReAct 等助手正文流)与 reasoning_chain_stream_*(Eino ReasoningContent):
|
||||
// 不逐条落库,按 streamId 聚合,flush 时分别落 thinking / reasoning_chain。
|
||||
type thinkingBuf struct {
|
||||
b strings.Builder
|
||||
meta map[string]interface{}
|
||||
b strings.Builder
|
||||
meta map[string]interface{}
|
||||
persistAs string // "thinking" | "reasoning_chain"
|
||||
}
|
||||
thinkingStreams := make(map[string]*thinkingBuf) // streamId -> buf
|
||||
flushedThinking := make(map[string]bool) // streamId -> flushed
|
||||
@@ -948,8 +943,12 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
}
|
||||
data[k] = v
|
||||
}
|
||||
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, "thinking", content, data); err != nil {
|
||||
h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", "thinking"))
|
||||
persist := tb.persistAs
|
||||
if persist != "reasoning_chain" {
|
||||
persist = "thinking"
|
||||
}
|
||||
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, persist, content, data); err != nil {
|
||||
h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", persist))
|
||||
}
|
||||
flushedThinking[sid] = true
|
||||
}
|
||||
@@ -1177,14 +1176,20 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
return
|
||||
}
|
||||
|
||||
// 聚合 thinking_stream_*(ReasoningContent),不逐条落库
|
||||
if eventType == "thinking_stream_start" {
|
||||
// 聚合 thinking_stream_* / reasoning_chain_stream_*,不逐条落库
|
||||
if eventType == "thinking_stream_start" || eventType == "reasoning_chain_stream_start" {
|
||||
persistAs := "thinking"
|
||||
if eventType == "reasoning_chain_stream_start" {
|
||||
persistAs = "reasoning_chain"
|
||||
}
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||
tb := thinkingStreams[sid]
|
||||
if tb == nil {
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}}
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}, persistAs: persistAs}
|
||||
thinkingStreams[sid] = tb
|
||||
} else {
|
||||
tb.persistAs = persistAs
|
||||
}
|
||||
// 记录元信息(source/einoAgent/einoRole/iteration 等)
|
||||
for k, v := range dataMap {
|
||||
@@ -1194,15 +1199,21 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
}
|
||||
return
|
||||
}
|
||||
if eventType == "thinking_stream_delta" {
|
||||
if eventType == "thinking_stream_delta" || eventType == "reasoning_chain_stream_delta" {
|
||||
persistAs := "thinking"
|
||||
if eventType == "reasoning_chain_stream_delta" {
|
||||
persistAs = "reasoning_chain"
|
||||
}
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||
tb := thinkingStreams[sid]
|
||||
if tb == nil {
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}}
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}, persistAs: persistAs}
|
||||
thinkingStreams[sid] = tb
|
||||
} else if tb.persistAs == "" {
|
||||
tb.persistAs = persistAs
|
||||
}
|
||||
// delta 片段直接拼接;message 本身就是 reasoning content
|
||||
// delta 片段直接拼接
|
||||
tb.b.WriteString(message)
|
||||
// 有时 delta 先到 start 未到,补充元信息
|
||||
for k, v := range dataMap {
|
||||
@@ -1213,10 +1224,9 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
return
|
||||
}
|
||||
|
||||
// 当 Agent 同时发送 thinking_stream_* 和 thinking(带同一 streamId)时,
|
||||
// thinking_stream_* 已经会在 flushThinkingStreams() 聚合落库;
|
||||
// 这里跳过同 streamId 的 thinking,避免 processDetails 双份展示。
|
||||
if eventType == "thinking" {
|
||||
// 当 Agent 同时发送 *_stream_* 与同名 streamId 的 thinking/reasoning_chain 时,
|
||||
// 流式聚合已会在 flushThinkingStreams() 落库;此处跳过逐条重复。
|
||||
if eventType == "thinking" || eventType == "reasoning_chain" {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||
if tb, exists := thinkingStreams[sid]; exists && tb != nil {
|
||||
@@ -1245,7 +1255,7 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
if eventType == "tool_result" {
|
||||
discardPlanningIfEchoesToolResult(&respPlan, data)
|
||||
}
|
||||
// 在关键过程事件落库前,先把「规划中」与 thinking_stream 落库
|
||||
// 在关键过程事件落库前,先把「规划中」与聚合中的 thinking / reasoning_chain 流落库
|
||||
flushResponsePlan()
|
||||
flushThinkingStreams()
|
||||
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, eventType, message, data); err != nil {
|
||||
@@ -1427,14 +1437,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
||||
agentHistoryMessages = []agent.ChatMessage{}
|
||||
} else {
|
||||
// 将数据库消息转换为Agent消息格式
|
||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
||||
for _, msg := range historyMessages {
|
||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
||||
}
|
||||
} else {
|
||||
@@ -1727,20 +1730,8 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
|
||||
// 更新助手消息内容
|
||||
if assistantMsg != nil {
|
||||
_, err = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
result.Response,
|
||||
func() string {
|
||||
if len(result.MCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
||||
return string(jsonData)
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
time.Now(), assistantMessageID,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("更新助手消息失败", zap.Error(err))
|
||||
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput)); errU != nil {
|
||||
h.logger.Error("更新助手消息失败", zap.Error(errU))
|
||||
}
|
||||
} else {
|
||||
// 如果之前创建失败,现在创建
|
||||
@@ -2664,12 +2655,12 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
var runErr error
|
||||
switch {
|
||||
case useBatchMulti:
|
||||
resultMA, runErr = multiagent.RunDeepAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, h.agentsMarkdownDir, batchOrch)
|
||||
resultMA, runErr = multiagent.RunDeepAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, h.agentsMarkdownDir, batchOrch, nil)
|
||||
case useEinoSingle:
|
||||
if h.config == nil {
|
||||
runErr = fmt.Errorf("服务器配置未加载")
|
||||
} else {
|
||||
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback)
|
||||
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, nil)
|
||||
}
|
||||
default:
|
||||
result, runErr = h.agent.AgentLoopWithProgress(taskCtx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
||||
@@ -2768,17 +2759,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
|
||||
// 更新助手消息内容
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(mcpIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(mcpIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
if _, updateErr := h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
resText,
|
||||
mcpIDsJSON,
|
||||
time.Now(), assistantMessageID,
|
||||
); updateErr != nil {
|
||||
if updateErr := h.db.UpdateAssistantMessageFinalize(assistantMessageID, resText, mcpIDs, multiagent.AggregatedReasoningFromTraceJSON(lastIn)); updateErr != nil {
|
||||
h.logger.Warn("更新助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
|
||||
// 如果更新失败,尝试创建新消息
|
||||
_, err = h.db.AddMessage(conversationID, "assistant", resText, mcpIDs)
|
||||
@@ -2870,6 +2851,10 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
||||
if content, ok := msgMap["content"].(string); ok {
|
||||
msg.Content = content
|
||||
}
|
||||
// DeepSeek 思考模式:含工具调用的 assistant 须在后续请求中回传 reasoning_content
|
||||
if rc, ok := msgMap["reasoning_content"].(string); ok && strings.TrimSpace(rc) != "" {
|
||||
msg.ReasoningContent = rc
|
||||
}
|
||||
|
||||
// 解析tool_calls(如果存在)
|
||||
if toolCallsRaw, ok := msgMap["tool_calls"]; ok && toolCallsRaw != nil {
|
||||
@@ -2975,3 +2960,18 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
||||
)
|
||||
return agentMessages, nil
|
||||
}
|
||||
|
||||
// dbMessagesToAgentChatMessages maps DB rows to agent ChatMessage for history fallback
|
||||
// (includes reasoning_content for DeepSeek thinking + tool replay).
|
||||
func dbMessagesToAgentChatMessages(msgs []database.Message) []agent.ChatMessage {
|
||||
out := make([]agent.ChatMessage, 0, len(msgs))
|
||||
for i := range msgs {
|
||||
m := msgs[i]
|
||||
out = append(out, agent.ChatMessage{
|
||||
Role: m.Role,
|
||||
Content: m.Content,
|
||||
ReasoningContent: m.ReasoningContent,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1312,6 +1312,19 @@ func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
|
||||
if cfg.MaxTotalTokens > 0 {
|
||||
setIntInMap(openaiNode, "max_total_tokens", cfg.MaxTotalTokens)
|
||||
}
|
||||
rn := ensureMap(openaiNode, "reasoning")
|
||||
if strings.TrimSpace(cfg.Reasoning.Mode) != "" {
|
||||
setStringInMap(rn, "mode", cfg.Reasoning.Mode)
|
||||
}
|
||||
if strings.TrimSpace(cfg.Reasoning.Effort) != "" {
|
||||
setStringInMap(rn, "effort", cfg.Reasoning.Effort)
|
||||
}
|
||||
if cfg.Reasoning.AllowClientReasoning != nil {
|
||||
setBoolInMap(rn, "allow_client_reasoning", *cfg.Reasoning.AllowClientReasoning)
|
||||
}
|
||||
if strings.TrimSpace(cfg.Reasoning.Profile) != "" {
|
||||
setStringInMap(rn, "profile", cfg.Reasoning.Profile)
|
||||
}
|
||||
}
|
||||
|
||||
func updateFOFAConfig(doc *yaml.Node, cfg config.FofaConfig) {
|
||||
|
||||
@@ -196,6 +196,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
||||
curHistory,
|
||||
roleTools,
|
||||
progressCallback,
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
timeoutCancel()
|
||||
|
||||
@@ -297,18 +298,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(cumulativeMCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(cumulativeMCPExecutionIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
_, _ = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
result.Response,
|
||||
mcpIDsJSON,
|
||||
time.Now(),
|
||||
assistantMessageID,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, cumulativeMCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
@@ -376,6 +366,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||
prep.History,
|
||||
prep.RoleTools,
|
||||
progressCallback,
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
if runErr != nil {
|
||||
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
||||
@@ -386,18 +377,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||
}
|
||||
|
||||
if prep.AssistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(result.MCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
_, _ = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
result.Response,
|
||||
mcpIDsJSON,
|
||||
time.Now(),
|
||||
prep.AssistantMessageID,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(prep.AssistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
_ = h.db.SaveAgentTrace(prep.ConversationID, result.LastAgentTraceInput, result.LastAgentTraceOutput)
|
||||
|
||||
@@ -208,6 +208,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
||||
progressCallback,
|
||||
h.agentsMarkdownDir,
|
||||
orch,
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
timeoutCancel()
|
||||
|
||||
@@ -309,18 +310,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(cumulativeMCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(cumulativeMCPExecutionIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
_, _ = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
result.Response,
|
||||
mcpIDsJSON,
|
||||
time.Now(),
|
||||
assistantMessageID,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, cumulativeMCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
@@ -390,6 +380,7 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
||||
progressCallback,
|
||||
h.agentsMarkdownDir,
|
||||
strings.TrimSpace(req.Orchestration),
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
if runErr != nil {
|
||||
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
||||
@@ -405,18 +396,7 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
||||
}
|
||||
|
||||
if prep.AssistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(result.MCPExecutionIDs) > 0 {
|
||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
||||
mcpIDsJSON = string(jsonData)
|
||||
}
|
||||
_, _ = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
result.Response,
|
||||
mcpIDsJSON,
|
||||
time.Now(),
|
||||
prep.AssistantMessageID,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(prep.AssistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
|
||||
@@ -55,13 +55,7 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
||||
if getErr != nil {
|
||||
agentHistoryMessages = []agent.ChatMessage{}
|
||||
} else {
|
||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
||||
for _, msg := range historyMessages {
|
||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// Package reasoning maps user/config intent to CloudWeGo Eino OpenAI ChatModel fields
|
||||
// (ReasoningEffort, ExtraFields such as thinking / reasoning_effort / output_config).
|
||||
package reasoning
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
|
||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||
)
|
||||
|
||||
// ClientIntent is optional per-request override from ChatRequest.reasoning.
|
||||
type ClientIntent struct {
|
||||
Mode string
|
||||
Effort string
|
||||
}
|
||||
|
||||
type wireProfile int
|
||||
|
||||
const (
|
||||
wireNone wireProfile = iota
|
||||
wireClaude
|
||||
wireDeepseek
|
||||
wireOpenAI
|
||||
wireOutputConfig
|
||||
)
|
||||
|
||||
// ApplyToEinoChatModelConfig merges reasoning-related options into cfg.
|
||||
// Precondition: cfg already has APIKey, BaseURL, Model, HTTPClient set.
|
||||
func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.OpenAIConfig, client *ClientIntent) {
|
||||
if cfg == nil || oa == nil {
|
||||
return
|
||||
}
|
||||
sr := &oa.Reasoning
|
||||
allowClient := sr.AllowClientReasoningEffective()
|
||||
mode := effectiveMode(sr, client, allowClient)
|
||||
|
||||
// Claude (Anthropic): merge admin extras first; optional extended thinking maps to top-level `thinking`
|
||||
// (see internal/openai convertOpenAIToClaude). DeepSeek/OpenAI-style fields are not sent.
|
||||
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") ||
|
||||
strings.EqualFold(strings.TrimSpace(oa.Provider), "anthropic") {
|
||||
if len(sr.ExtraRequestFields) > 0 {
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
for k, v := range sr.ExtraRequestFields {
|
||||
cfg.ExtraFields[k] = v
|
||||
}
|
||||
}
|
||||
if mode == "off" {
|
||||
return
|
||||
}
|
||||
applyClaudeExtendedThinking(cfg, mode, effectiveEffort(sr, client, allowClient), oa.Model)
|
||||
return
|
||||
}
|
||||
|
||||
if mode == "off" {
|
||||
return
|
||||
}
|
||||
effort := effectiveEffort(sr, client, allowClient)
|
||||
prof := resolveWireProfile(oa, sr)
|
||||
|
||||
// Admin-defined extra root fields (merged first; automatic keys may follow).
|
||||
if len(sr.ExtraRequestFields) > 0 {
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
for k, v := range sr.ExtraRequestFields {
|
||||
cfg.ExtraFields[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
switch prof {
|
||||
case wireClaude, wireNone:
|
||||
return
|
||||
case wireDeepseek:
|
||||
applyDeepseek(cfg, mode, effort)
|
||||
case wireOutputConfig:
|
||||
applyOutputConfigEffort(cfg, mode, effort)
|
||||
default: // wireOpenAI
|
||||
applyOpenAICompat(cfg, mode, effort)
|
||||
}
|
||||
}
|
||||
|
||||
// applyClaudeExtendedThinking sets Anthropic Messages API `thinking` when absent from ExtraRequestFields.
|
||||
// Uses adaptive + summarized display by default (per Anthropic guidance for Claude 4.x); Sonnet 3.7 uses enabled+budget.
|
||||
func applyClaudeExtendedThinking(cfg *einoopenai.ChatModelConfig, mode, effort, model string) {
|
||||
if cfg == nil || mode == "off" {
|
||||
return
|
||||
}
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
if _, exists := cfg.ExtraFields["thinking"]; exists {
|
||||
return
|
||||
}
|
||||
m := strings.ToLower(strings.TrimSpace(model))
|
||||
thinking := map[string]any{
|
||||
"type": "adaptive",
|
||||
"display": "summarized",
|
||||
}
|
||||
// Sonnet 3.7: manual extended thinking is the documented path.
|
||||
if strings.Contains(m, "claude-3-7-sonnet") || strings.Contains(m, "3-7-sonnet") || strings.Contains(m, "sonnet-3.7") {
|
||||
thinking = map[string]any{
|
||||
"type": "enabled",
|
||||
"budget_tokens": 10000,
|
||||
"display": "summarized",
|
||||
}
|
||||
}
|
||||
// Opus 4.7+: manual enabled+budget rejected — keep adaptive only.
|
||||
if strings.Contains(m, "opus-4-7") || strings.Contains(m, "opus-4.7") {
|
||||
thinking = map[string]any{
|
||||
"type": "adaptive",
|
||||
"display": "summarized",
|
||||
}
|
||||
}
|
||||
_ = effort // reserved: map to Anthropic effort / output_config when API stabilizes in one place
|
||||
cfg.ExtraFields["thinking"] = thinking
|
||||
}
|
||||
|
||||
func effectiveMode(sr *config.OpenAIReasoningConfig, client *ClientIntent, allowClient bool) string {
|
||||
server := strings.ToLower(strings.TrimSpace(sr.ModeEffective()))
|
||||
if server == "" || server == "default" {
|
||||
server = "auto"
|
||||
}
|
||||
if !allowClient || client == nil {
|
||||
return server
|
||||
}
|
||||
cm := strings.ToLower(strings.TrimSpace(client.Mode))
|
||||
if cm == "" || cm == "default" {
|
||||
return server
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
func effectiveEffort(sr *config.OpenAIReasoningConfig, client *ClientIntent, allowClient bool) string {
|
||||
se := normalizeEffort(sr.Effort)
|
||||
if !allowClient || client == nil {
|
||||
return se
|
||||
}
|
||||
ce := normalizeEffort(client.Effort)
|
||||
if ce != "" {
|
||||
return ce
|
||||
}
|
||||
return se
|
||||
}
|
||||
|
||||
func normalizeEffort(s string) string {
|
||||
e := strings.ToLower(strings.TrimSpace(s))
|
||||
switch e {
|
||||
case "low", "medium", "high", "max":
|
||||
return e
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func resolveWireProfile(oa *config.OpenAIConfig, sr *config.OpenAIReasoningConfig) wireProfile {
|
||||
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") {
|
||||
return wireClaude
|
||||
}
|
||||
p := strings.ToLower(strings.TrimSpace(sr.ProfileEffective()))
|
||||
switch p {
|
||||
case "output_config", "output_config_effort":
|
||||
return wireOutputConfig
|
||||
case "openai", "openai_compat":
|
||||
return wireOpenAI
|
||||
case "deepseek", "deepseek_compat":
|
||||
return wireDeepseek
|
||||
case "auto", "":
|
||||
bu := strings.ToLower(oa.BaseURL)
|
||||
mo := strings.ToLower(oa.Model)
|
||||
if strings.Contains(bu, "deepseek") || strings.Contains(mo, "deepseek") {
|
||||
return wireDeepseek
|
||||
}
|
||||
return wireOpenAI
|
||||
default:
|
||||
return wireOpenAI
|
||||
}
|
||||
}
|
||||
|
||||
func applyDeepseek(cfg *einoopenai.ChatModelConfig, mode, effort string) {
|
||||
// auto: enable thinking for DeepSeek line; on: same; auto without effort still opens thinking.
|
||||
if mode == "off" {
|
||||
return
|
||||
}
|
||||
if mode == "auto" || mode == "on" {
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
cfg.ExtraFields["thinking"] = map[string]any{"type": "enabled"}
|
||||
}
|
||||
if effort != "" {
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
cfg.ExtraFields["reasoning_effort"] = effortStringForAPI(effort)
|
||||
}
|
||||
}
|
||||
|
||||
func applyOpenAICompat(cfg *einoopenai.ChatModelConfig, mode, effort string) {
|
||||
if mode == "auto" && effort == "" {
|
||||
return
|
||||
}
|
||||
e := effort
|
||||
if mode == "on" && e == "" {
|
||||
e = "medium"
|
||||
}
|
||||
if e == "" {
|
||||
return
|
||||
}
|
||||
if e == "max" {
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
cfg.ExtraFields["reasoning_effort"] = "max"
|
||||
return
|
||||
}
|
||||
switch e {
|
||||
case "low":
|
||||
cfg.ReasoningEffort = einoopenai.ReasoningEffortLevelLow
|
||||
case "medium":
|
||||
cfg.ReasoningEffort = einoopenai.ReasoningEffortLevelMedium
|
||||
case "high":
|
||||
cfg.ReasoningEffort = einoopenai.ReasoningEffortLevelHigh
|
||||
}
|
||||
}
|
||||
|
||||
func applyOutputConfigEffort(cfg *einoopenai.ChatModelConfig, mode, effort string) {
|
||||
if mode == "auto" && effort == "" {
|
||||
return
|
||||
}
|
||||
e := effort
|
||||
if mode == "on" && e == "" {
|
||||
e = "high"
|
||||
}
|
||||
if e == "" {
|
||||
return
|
||||
}
|
||||
if cfg.ExtraFields == nil {
|
||||
cfg.ExtraFields = make(map[string]any)
|
||||
}
|
||||
cfg.ExtraFields["output_config"] = map[string]any{"effort": effortStringForAPI(e)}
|
||||
}
|
||||
|
||||
func effortStringForAPI(e string) string {
|
||||
// Gateways expect lowercase strings; "max" kept as max.
|
||||
return strings.ToLower(strings.TrimSpace(e))
|
||||
}
|
||||
Reference in New Issue
Block a user