fix: add structured graph tool results
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
"cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
||||||
|
"cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
"cs-agent/internal/pkg/toolx"
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
@@ -64,14 +65,17 @@ func consumeAgentEvents(events *adk.AsyncIterator[*adk.AgentEvent], summary *Run
|
|||||||
summary.InvokedToolCodes = appendIfMissing(summary.InvokedToolCodes, toolCode)
|
summary.InvokedToolCodes = appendIfMissing(summary.InvokedToolCodes, toolCode)
|
||||||
if strings.TrimSpace(summary.ReplyText) == "" && toolx.ResolveToolSourceType(toolCode) == enums.ToolSourceTypeGraph {
|
if strings.TrimSpace(summary.ReplyText) == "" && toolx.ResolveToolSourceType(toolCode) == enums.ToolSourceTypeGraph {
|
||||||
toolReplyText := strings.TrimSpace(messageOutput.Message.Content)
|
toolReplyText := strings.TrimSpace(messageOutput.Message.Content)
|
||||||
if toolReplyText != "" {
|
if result, ok := tooling.ParseToolResult(toolReplyText); ok {
|
||||||
summary.ReplyText = toolReplyText
|
if result.ReplyText != "" && !result.ReplySent {
|
||||||
} else if toolCode == toolx.GraphHandoffConversation.Code {
|
summary.ReplyText = result.ReplyText
|
||||||
|
}
|
||||||
|
if result.Terminal && !result.ShouldRetry {
|
||||||
suppressAssistantReply = true
|
suppressAssistantReply = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if summary.Status == "started" {
|
if summary.Status == "started" {
|
||||||
switch {
|
switch {
|
||||||
case strings.TrimSpace(summary.ErrorMessage) != "":
|
case strings.TrimSpace(summary.ErrorMessage) != "":
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/pkg/toolx"
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConsumeAgentEventsUsesGraphToolTextAsReplyFallback(t *testing.T) {
|
func TestConsumeAgentEventsIgnoresPlainGraphToolText(t *testing.T) {
|
||||||
summary := &RunResult{
|
summary := &RunResult{
|
||||||
Status: "started",
|
Status: "started",
|
||||||
InvokedToolCodes: make([]string, 0),
|
InvokedToolCodes: make([]string, 0),
|
||||||
@@ -32,7 +34,7 @@ func TestConsumeAgentEventsUsesGraphToolTextAsReplyFallback(t *testing.T) {
|
|||||||
toolx.GraphHandoffConversation.Name: toolx.GraphHandoffConversation.Code,
|
toolx.GraphHandoffConversation.Name: toolx.GraphHandoffConversation.Code,
|
||||||
})
|
})
|
||||||
|
|
||||||
if summary.ReplyText != "已为你转接人工客服,请稍候。,请稍候。" {
|
if summary.ReplyText != "" {
|
||||||
t.Fatalf("unexpected reply text: %q", summary.ReplyText)
|
t.Fatalf("unexpected reply text: %q", summary.ReplyText)
|
||||||
}
|
}
|
||||||
if summary.Status != "completed" {
|
if summary.Status != "completed" {
|
||||||
@@ -40,6 +42,109 @@ func TestConsumeAgentEventsUsesGraphToolTextAsReplyFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConsumeAgentEventsUsesGraphToolResultReplyText(t *testing.T) {
|
||||||
|
summary := &RunResult{
|
||||||
|
Status: "started",
|
||||||
|
InvokedToolCodes: make([]string, 0),
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "off_hours_handoff",
|
||||||
|
ReplyText: "当前暂不在人工客服服务时间内,你可以先继续描述问题。",
|
||||||
|
ShouldRetry: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal graph tool result: %v", err)
|
||||||
|
}
|
||||||
|
events, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||||||
|
gen.Send(&adk.AgentEvent{
|
||||||
|
Output: &adk.AgentOutput{
|
||||||
|
MessageOutput: &adk.MessageVariant{
|
||||||
|
Role: schema.Tool,
|
||||||
|
ToolName: toolx.GraphHandoffConversation.Name,
|
||||||
|
Message: &schema.Message{
|
||||||
|
Content: string(payload),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
gen.Send(&adk.AgentEvent{
|
||||||
|
Output: &adk.AgentOutput{
|
||||||
|
MessageOutput: &adk.MessageVariant{
|
||||||
|
Role: schema.Assistant,
|
||||||
|
Message: &schema.Message{
|
||||||
|
Content: "我再试一次转人工。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
gen.Close()
|
||||||
|
|
||||||
|
consumeAgentEvents(events, summary, nil, map[string]string{
|
||||||
|
toolx.GraphHandoffConversation.Name: toolx.GraphHandoffConversation.Code,
|
||||||
|
})
|
||||||
|
|
||||||
|
if summary.ReplyText != "当前暂不在人工客服服务时间内,你可以先继续描述问题。" {
|
||||||
|
t.Fatalf("unexpected reply text: %q", summary.ReplyText)
|
||||||
|
}
|
||||||
|
if summary.Status != "completed" {
|
||||||
|
t.Fatalf("unexpected summary status: %q", summary.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeAgentEventsSuppressesGraphToolResultWhenReplyAlreadySent(t *testing.T) {
|
||||||
|
summary := &RunResult{
|
||||||
|
Status: "started",
|
||||||
|
InvokedToolCodes: make([]string, 0),
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "off_hours_handoff",
|
||||||
|
ReplyText: "当前暂不在人工客服服务时间内,你可以先继续描述问题。",
|
||||||
|
ReplySent: true,
|
||||||
|
ShouldRetry: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal graph tool result: %v", err)
|
||||||
|
}
|
||||||
|
events, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||||||
|
gen.Send(&adk.AgentEvent{
|
||||||
|
Output: &adk.AgentOutput{
|
||||||
|
MessageOutput: &adk.MessageVariant{
|
||||||
|
Role: schema.Tool,
|
||||||
|
ToolName: toolx.GraphHandoffConversation.Name,
|
||||||
|
Message: &schema.Message{
|
||||||
|
Content: string(payload),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
gen.Send(&adk.AgentEvent{
|
||||||
|
Output: &adk.AgentOutput{
|
||||||
|
MessageOutput: &adk.MessageVariant{
|
||||||
|
Role: schema.Assistant,
|
||||||
|
Message: &schema.Message{
|
||||||
|
Content: "我再试一次转人工。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
gen.Close()
|
||||||
|
|
||||||
|
consumeAgentEvents(events, summary, nil, map[string]string{
|
||||||
|
toolx.GraphHandoffConversation.Name: toolx.GraphHandoffConversation.Code,
|
||||||
|
})
|
||||||
|
|
||||||
|
if summary.ReplyText != "" {
|
||||||
|
t.Fatalf("expected no committed reply because graph already sent it, got %q", summary.ReplyText)
|
||||||
|
}
|
||||||
|
if summary.Status != "completed" {
|
||||||
|
t.Fatalf("unexpected summary status: %q", summary.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConsumeAgentEventsCompletesGraphToolWithNoVisibleReply(t *testing.T) {
|
func TestConsumeAgentEventsCompletesGraphToolWithNoVisibleReply(t *testing.T) {
|
||||||
summary := &RunResult{
|
summary := &RunResult{
|
||||||
Status: "started",
|
Status: "started",
|
||||||
@@ -70,44 +175,3 @@ func TestConsumeAgentEventsCompletesGraphToolWithNoVisibleReply(t *testing.T) {
|
|||||||
t.Fatalf("unexpected summary status: %q", summary.Status)
|
t.Fatalf("unexpected summary status: %q", summary.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConsumeAgentEventsSuppressesAssistantReplyAfterSilentHandoffTool(t *testing.T) {
|
|
||||||
summary := &RunResult{
|
|
||||||
Status: "started",
|
|
||||||
InvokedToolCodes: make([]string, 0),
|
|
||||||
}
|
|
||||||
events, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
|
||||||
gen.Send(&adk.AgentEvent{
|
|
||||||
Output: &adk.AgentOutput{
|
|
||||||
MessageOutput: &adk.MessageVariant{
|
|
||||||
Role: schema.Tool,
|
|
||||||
ToolName: toolx.GraphHandoffConversation.Name,
|
|
||||||
Message: &schema.Message{
|
|
||||||
Content: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
gen.Send(&adk.AgentEvent{
|
|
||||||
Output: &adk.AgentOutput{
|
|
||||||
MessageOutput: &adk.MessageVariant{
|
|
||||||
Role: schema.Assistant,
|
|
||||||
Message: &schema.Message{
|
|
||||||
Content: "好的,已为您发起转接人工客服的请求。系统正在为您确认,请稍候。",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
gen.Close()
|
|
||||||
|
|
||||||
consumeAgentEvents(events, summary, nil, map[string]string{
|
|
||||||
toolx.GraphHandoffConversation.Name: toolx.GraphHandoffConversation.Code,
|
|
||||||
})
|
|
||||||
|
|
||||||
if summary.ReplyText != "" {
|
|
||||||
t.Fatalf("expected assistant reply after silent handoff to be suppressed, got %q", summary.ReplyText)
|
|
||||||
}
|
|
||||||
if summary.Status != "completed" {
|
|
||||||
t.Fatalf("unexpected summary status: %q", summary.Status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/dto"
|
"cs-agent/internal/pkg/dto"
|
||||||
"cs-agent/internal/pkg/dto/request"
|
"cs-agent/internal/pkg/dto/request"
|
||||||
@@ -84,9 +85,21 @@ func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("工单已创建,工单号:%s,标题:%s。", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)), nil
|
return tooling.MarshalToolResult(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "ticket_created",
|
||||||
|
ReplyText: fmt.Sprintf("工单已创建,工单号:%s,标题:%s。", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)),
|
||||||
|
ShouldRetry: false,
|
||||||
|
}), nil
|
||||||
case ConfirmationDecisionCancel:
|
case ConfirmationDecisionCancel:
|
||||||
return CancelCreateTicketReply, nil
|
return tooling.MarshalToolResult(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "ticket_cancelled",
|
||||||
|
ReplyText: CancelCreateTicketReply,
|
||||||
|
ShouldRetry: false,
|
||||||
|
}), nil
|
||||||
default:
|
default:
|
||||||
info := CreateTicketGraphInterruptInfo{
|
info := CreateTicketGraphInterruptInfo{
|
||||||
Type: InterruptTypeTicketCreationConfirmation,
|
Type: InterruptTypeTicketCreationConfirmation,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/services"
|
"cs-agent/internal/services"
|
||||||
|
|
||||||
@@ -53,7 +54,14 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string,
|
|||||||
handled, err := services.ConversationService.TryOffHoursHandoffByAI(g.conversation.ID, g.aiAgent, reason)
|
handled, err := services.ConversationService.TryOffHoursHandoffByAI(g.conversation.ID, g.aiAgent, reason)
|
||||||
if err != nil || handled {
|
if err != nil || handled {
|
||||||
if handled && err == nil {
|
if handled && err == nil {
|
||||||
return services.HandoffOffHoursMessage, nil
|
return tooling.MarshalToolResult(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "off_hours_handoff",
|
||||||
|
ReplyText: services.HandoffOffHoursMessage,
|
||||||
|
ReplySent: true,
|
||||||
|
ShouldRetry: false,
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -87,9 +95,21 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string,
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// ConversationService sends the customer-visible handoff notice according to the dispatch decision.
|
// ConversationService sends the customer-visible handoff notice according to the dispatch decision.
|
||||||
return "", nil
|
return tooling.MarshalToolResult(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "handoff_confirmed",
|
||||||
|
ReplySent: true,
|
||||||
|
ShouldRetry: false,
|
||||||
|
}), nil
|
||||||
case ConfirmationDecisionCancel:
|
case ConfirmationDecisionCancel:
|
||||||
return CancelHandoffReply, nil
|
return tooling.MarshalToolResult(tooling.ToolResult{
|
||||||
|
Handled: true,
|
||||||
|
Terminal: true,
|
||||||
|
Action: "handoff_cancelled",
|
||||||
|
ReplyText: CancelHandoffReply,
|
||||||
|
ShouldRetry: false,
|
||||||
|
}), nil
|
||||||
default:
|
default:
|
||||||
info := HandoffGraphInterruptInfo{
|
info := HandoffGraphInterruptInfo{
|
||||||
Type: InterruptTypeHandoffConfirmation,
|
Type: InterruptTypeHandoffConfirmation,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package graphs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/runtime/tooling"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
"cs-agent/internal/services"
|
"cs-agent/internal/services"
|
||||||
@@ -25,8 +27,18 @@ func TestHandoffGraphOffHoursSendsNoticeWithoutConfirmation(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
if reply != services.HandoffOffHoursMessage {
|
var result tooling.ToolResult
|
||||||
t.Fatalf("expected off-hours graph reply, got %q", reply)
|
if err := json.Unmarshal([]byte(reply), &result); err != nil {
|
||||||
|
t.Fatalf("expected graph tool result JSON, got %q: %v", reply, err)
|
||||||
|
}
|
||||||
|
if !result.Handled || !result.Terminal || result.ShouldRetry {
|
||||||
|
t.Fatalf("unexpected graph result flags: %+v", result)
|
||||||
|
}
|
||||||
|
if !result.ReplySent {
|
||||||
|
t.Fatalf("expected graph result to mark replySent, got %+v", result)
|
||||||
|
}
|
||||||
|
if result.Action != "off_hours_handoff" || result.ReplyText != services.HandoffOffHoursMessage {
|
||||||
|
t.Fatalf("unexpected off-hours graph result: %+v", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
message := services.MessageService.FindOne(sqls.NewCnd().Eq("conversation_id", conversation.ID).Desc("id"))
|
message := services.MessageService.FindOne(sqls.NewCnd().Eq("conversation_id", conversation.ID).Desc("id"))
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package tooling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToolResult struct {
|
||||||
|
Handled bool `json:"handled"`
|
||||||
|
Terminal bool `json:"terminal"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ReplyText string `json:"replyText,omitempty"`
|
||||||
|
ReplySent bool `json:"replySent,omitempty"`
|
||||||
|
ShouldRetry bool `json:"shouldRetry"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalToolResult(result ToolResult) string {
|
||||||
|
result.Action = strings.TrimSpace(result.Action)
|
||||||
|
result.ReplyText = strings.TrimSpace(result.ReplyText)
|
||||||
|
buf, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseToolResult(raw string) (ToolResult, bool) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return ToolResult{}, false
|
||||||
|
}
|
||||||
|
var result ToolResult
|
||||||
|
if err := json.Unmarshal([]byte(raw), &result); err != nil {
|
||||||
|
return ToolResult{}, false
|
||||||
|
}
|
||||||
|
result.Action = strings.TrimSpace(result.Action)
|
||||||
|
result.ReplyText = strings.TrimSpace(result.ReplyText)
|
||||||
|
if result.Action == "" && result.ReplyText == "" && !result.Handled && !result.Terminal {
|
||||||
|
return ToolResult{}, false
|
||||||
|
}
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ func (t *HandoffGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error
|
|||||||
func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||||
return &schema.ToolInfo{
|
return &schema.ToolInfo{
|
||||||
Name: toolx.GraphHandoffConversation.Name,
|
Name: toolx.GraphHandoffConversation.Name,
|
||||||
Desc: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。仅在用户明确要求人工客服,或你已确认必须转人工处理时调用。",
|
Desc: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。仅在用户明确要求人工客服,或你已确认必须转人工处理时调用;若结果标记 terminal=true 且 shouldRetry=false,不要重复调用。",
|
||||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||||
Version: einojsonschema.Version,
|
Version: einojsonschema.Version,
|
||||||
Type: "object",
|
Type: "object",
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ var (
|
|||||||
3. 一旦决定转人工,必须调用 handoff_to_human 工具,禁止只在回复里口头说“我帮你转人工了”。
|
3. 一旦决定转人工,必须调用 handoff_to_human 工具,禁止只在回复里口头说“我帮你转人工了”。
|
||||||
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正转人工;用户取消则结束本次转人工流程。
|
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正转人工;用户取消则结束本次转人工流程。
|
||||||
5. 如果问题仍可由当前对话继续解决,优先继续解答,不要过早转人工。
|
5. 如果问题仍可由当前对话继续解决,优先继续解答,不要过早转人工。
|
||||||
|
6. 如果工具返回 terminal=true 且 shouldRetry=false,说明转人工流程已经结束,禁止重复调用该工具。
|
||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
RegisteredToolSpecs = []ToolSpec{
|
RegisteredToolSpecs = []ToolSpec{
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ func (s *conversationHumanDispatchService) TryOffHoursHandoffByAI(conversationID
|
|||||||
if err := s.createEvent(conversationID, enums.IMEventTypeTransfer, enums.IMSenderTypeAI, aiAgent.ID, "转人工失败:非服务时间", strings.TrimSpace(reason)); err != nil {
|
if err := s.createEvent(conversationID, enums.IMEventTypeTransfer, enums.IMSenderTypeAI, aiAgent.ID, "转人工失败:非服务时间", strings.TrimSpace(reason)); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
if s.hasLatestAIText(conversationID, HandoffOffHoursMessage) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if err := s.sendAIText(conversationID, aiAgent.ID, HandoffOffHoursMessage); err != nil {
|
if err := s.sendAIText(conversationID, aiAgent.ID, HandoffOffHoursMessage); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
@@ -291,14 +288,6 @@ func (s *conversationHumanDispatchService) sendAIText(conversationID, aiAgentID
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *conversationHumanDispatchService) hasLatestAIText(conversationID int64, content string) bool {
|
|
||||||
latest, err := MessageService.GetConversationReadTarget(conversationID, 0)
|
|
||||||
if err != nil || latest == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return latest.SenderType == enums.IMSenderTypeAI && strings.TrimSpace(latest.Content) == strings.TrimSpace(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func orderedPositiveIDs(value string) []int64 {
|
func orderedPositiveIDs(value string) []int64 {
|
||||||
return uniquePositiveInt64sFromStrings(strings.Split(value, ","))
|
return uniquePositiveInt64sFromStrings(strings.Split(value, ","))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,32 +47,6 @@ func TestConversationHumanDispatchAIHandoffOffHoursKeepsAIServingAndSendsNotice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConversationHumanDispatchAIHandoffOffHoursDoesNotDuplicateNotice(t *testing.T) {
|
|
||||||
db := setupConversationHumanDispatchTestDB(t)
|
|
||||||
aiAgent := createHumanDispatchAIAgent(t, db, enums.IMConversationServiceModeAIFirst, "1")
|
|
||||||
conversation := createHumanDispatchConversation(t, db, aiAgent.ID, enums.IMConversationStatusAIServing)
|
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
result, err := services.ConversationHumanDispatchService.HandoffByAI(conversation.ID, aiAgent, "用户要求转人工")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("HandoffByAI() round %d error = %v", i+1, err)
|
|
||||||
}
|
|
||||||
if result == nil || result.Decision != services.HandoffDecisionOffHours {
|
|
||||||
t.Fatalf("expected off_hours decision on round %d, got %+v", i+1, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if err := db.Model(&models.Message{}).
|
|
||||||
Where("conversation_id = ? AND sender_type = ? AND content = ?", conversation.ID, enums.IMSenderTypeAI, services.HandoffOffHoursMessage).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
t.Fatalf("count off-hours messages error = %v", err)
|
|
||||||
}
|
|
||||||
if count != 1 {
|
|
||||||
t.Fatalf("expected one off-hours notice, got %d", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConversationHumanDispatchAIHandoffAssignsAvailableAgent(t *testing.T) {
|
func TestConversationHumanDispatchAIHandoffAssignsAvailableAgent(t *testing.T) {
|
||||||
db := setupConversationHumanDispatchTestDB(t)
|
db := setupConversationHumanDispatchTestDB(t)
|
||||||
aiAgent := createHumanDispatchAIAgent(t, db, enums.IMConversationServiceModeAIFirst, "1")
|
aiAgent := createHumanDispatchAIAgent(t, db, enums.IMConversationServiceModeAIFirst, "1")
|
||||||
|
|||||||
Reference in New Issue
Block a user