feat: enhance event consumption logic and add success reply handling
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
"cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
||||||
|
"cs-agent/internal/pkg/enums"
|
||||||
|
"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"
|
||||||
@@ -56,6 +58,12 @@ func consumeAgentEvents(events *adk.AsyncIterator[*adk.AgentEvent], summary *Run
|
|||||||
toolCode = strings.TrimSpace(mappedCode)
|
toolCode = strings.TrimSpace(mappedCode)
|
||||||
}
|
}
|
||||||
summary.InvokedToolCodes = appendIfMissing(summary.InvokedToolCodes, toolCode)
|
summary.InvokedToolCodes = appendIfMissing(summary.InvokedToolCodes, toolCode)
|
||||||
|
if strings.TrimSpace(summary.ReplyText) == "" && toolx.ResolveToolSourceType(toolCode) == enums.ToolSourceTypeGraph {
|
||||||
|
toolReplyText := strings.TrimSpace(messageOutput.Message.Content)
|
||||||
|
if toolReplyText != "" {
|
||||||
|
summary.ReplyText = toolReplyText
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if summary.Status == "started" {
|
if summary.Status == "started" {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/adk"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConsumeAgentEventsUsesGraphToolTextAsReplyFallback(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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string,
|
|||||||
if err := services.ConversationService.HandoffByAI(g.conversation.ID, g.aiAgent, state.Reason); err != nil {
|
if err := services.ConversationService.HandoffByAI(g.conversation.ID, g.aiAgent, state.Reason); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return "已为你转接人工客服,请稍候。", nil
|
return g.buildSuccessReply(), nil
|
||||||
case ConfirmationDecisionCancel:
|
case ConfirmationDecisionCancel:
|
||||||
return CancelHandoffReply, nil
|
return CancelHandoffReply, nil
|
||||||
default:
|
default:
|
||||||
@@ -112,6 +112,10 @@ func (g *HandoffGraph) buildConfirmationPrompt(reason string) string {
|
|||||||
return fmt.Sprintf("我准备为你转接人工客服。\n原因:%s\n请直接回复“确认”或“取消”。", strings.TrimSpace(reason))
|
return fmt.Sprintf("我准备为你转接人工客服。\n原因:%s\n请直接回复“确认”或“取消”。", strings.TrimSpace(reason))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *HandoffGraph) buildSuccessReply() string {
|
||||||
|
return "已为你转接人工客服,请稍候。,请稍候。"
|
||||||
|
}
|
||||||
|
|
||||||
func parseHandoffDecision(value string) ConfirmationDecision {
|
func parseHandoffDecision(value string) ConfirmationDecision {
|
||||||
return ParseConfirmationDecision(value)
|
return ParseConfirmationDecision(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,3 +29,13 @@ func TestHandoffGraphBuildReasonFallback(t *testing.T) {
|
|||||||
t.Fatalf("unexpected fallback reason: %q", reason)
|
t.Fatalf("unexpected fallback reason: %q", reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandoffGraphBuildSuccessReply(t *testing.T) {
|
||||||
|
graph := NewHandoffGraph(&models.Conversation{ID: 1}, &models.AIAgent{Name: "AI"})
|
||||||
|
|
||||||
|
got := graph.buildSuccessReply()
|
||||||
|
want := "已为你转接人工客服,请稍候。,请稍候。"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("unexpected success reply: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ func (s *messageService) ValidateConversationSender(conversationID int64, sender
|
|||||||
if operator == nil {
|
if operator == nil {
|
||||||
return nil, errorsx.Unauthorized("未登录或登录已过期")
|
return nil, errorsx.Unauthorized("未登录或登录已过期")
|
||||||
}
|
}
|
||||||
if conversation.Status != enums.IMConversationStatusAIServing {
|
if conversation.Status != enums.IMConversationStatusAIServing && !allowAIMessageOnPendingHandoff(conversation) {
|
||||||
return nil, errorsx.Forbidden("当前会话不处于 AI 接待状态")
|
return nil, errorsx.Forbidden("当前会话不处于 AI 接待状态")
|
||||||
}
|
}
|
||||||
if conversation.CurrentAssigneeID != 0 {
|
if conversation.CurrentAssigneeID != 0 {
|
||||||
@@ -530,6 +530,15 @@ func (s *messageService) ValidateConversationSender(conversationID int64, sender
|
|||||||
return conversation, nil
|
return conversation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allowAIMessageOnPendingHandoff(conversation *models.Conversation) bool {
|
||||||
|
if conversation == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return conversation.Status == enums.IMConversationStatusPending &&
|
||||||
|
conversation.HandoffAt != nil &&
|
||||||
|
conversation.CurrentAssigneeID == 0
|
||||||
|
}
|
||||||
|
|
||||||
func suffixFilenameForSummary(filename string) string {
|
func suffixFilenameForSummary(filename string) string {
|
||||||
filename = strings.TrimSpace(filename)
|
filename = strings.TrimSpace(filename)
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/enums"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAllowAIMessageOnPendingHandoff(t *testing.T) {
|
||||||
|
conversation := &models.Conversation{
|
||||||
|
Status: enums.IMConversationStatusPending,
|
||||||
|
CurrentAssigneeID: 0,
|
||||||
|
HandoffAt: ptrTime(time.Now()),
|
||||||
|
}
|
||||||
|
if !allowAIMessageOnPendingHandoff(conversation) {
|
||||||
|
t.Fatalf("expected pending handoff conversation to allow ai handoff notice")
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.Status = enums.IMConversationStatusAIServing
|
||||||
|
if allowAIMessageOnPendingHandoff(conversation) {
|
||||||
|
t.Fatalf("expected ai serving conversation not to use pending handoff allowance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrTime(v time.Time) *time.Time {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user