feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Modified in the independent AgentDesk fork: localized knowledge workflows and safe display errors.
|
||||
package rag
|
||||
|
||||
import (
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/repositories"
|
||||
)
|
||||
|
||||
@@ -24,6 +26,9 @@ type answer struct {
|
||||
var Answer = &answer{}
|
||||
|
||||
func (s *answer) DebugSearch(ctx context.Context, req request.KnowledgeSearchRequest) (*response.KnowledgeSearchResponse, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(req.Question) == "" {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0340")
|
||||
}
|
||||
@@ -59,6 +64,9 @@ func (s *answer) DebugSearch(ctx context.Context, req request.KnowledgeSearchReq
|
||||
}
|
||||
|
||||
func (s *answer) DebugAnswer(ctx context.Context, req request.KnowledgeAnswerRequest, operator *dto.AuthPrincipal) (*response.KnowledgeAnswerResponse, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(req.Question) == "" {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0340")
|
||||
}
|
||||
@@ -77,7 +85,7 @@ func (s *answer) DebugAnswer(ctx context.Context, req request.KnowledgeAnswerReq
|
||||
}
|
||||
retrieveMs := time.Since(retrieveStartedAt).Milliseconds()
|
||||
knowledgeBase := s.resolveAnswerKnowledgeBase(req.KnowledgeBaseIDs, results)
|
||||
contextResults := buildContextHits(Retrieve.SelectContextResults(results, 4000))
|
||||
contextResults := buildContextHits(Retrieve.SelectContextResultsWithContext(ctx, results, 4000))
|
||||
|
||||
hits := make([]response.KnowledgeSearchResult, 0, len(results))
|
||||
topScore := 0.0
|
||||
@@ -114,36 +122,18 @@ func (s *answer) DebugAnswer(ctx context.Context, req request.KnowledgeAnswerReq
|
||||
|
||||
fallbackMode := enums.AIAgentFallbackModeNoAnswer
|
||||
|
||||
answerStatus := enums.KnowledgeAnswerStatusNormal
|
||||
answer := ""
|
||||
modelName := ""
|
||||
promptTokens := 0
|
||||
completionTokens := 0
|
||||
generateStartedAt := time.Now()
|
||||
|
||||
if len(hits) == 0 {
|
||||
answerStatus = enums.KnowledgeAnswerStatusNoAnswer
|
||||
answer = buildFallbackAnswer(fallbackMode)
|
||||
} else {
|
||||
contextText := Retrieve.BuildContext(ctx, results, 4000)
|
||||
systemPrompt := buildAnswerSystemPrompt(answerMode)
|
||||
userPrompt := fmt.Sprintf("用户问题:%s\n\n参考资料:\n%s", req.Question, contextText)
|
||||
llmResult, llmErr := ai.LLM.Chat(ctx, systemPrompt, userPrompt)
|
||||
if llmErr != nil {
|
||||
answerStatus = enums.KnowledgeAnswerStatusFallback
|
||||
answer = buildFallbackAnswer(fallbackMode)
|
||||
} else {
|
||||
answer = llmResult.Content
|
||||
modelName = llmResult.ModelName
|
||||
promptTokens = llmResult.PromptTokens
|
||||
completionTokens = llmResult.CompletionTokens
|
||||
if strings.TrimSpace(answer) == "" {
|
||||
answerStatus = enums.KnowledgeAnswerStatusFallback
|
||||
answer = buildFallbackAnswer(fallbackMode)
|
||||
}
|
||||
}
|
||||
generated, err := generateKnowledgeAnswer(ctx, req.Question, results, answerMode, fallbackMode, ai.LLM.Chat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
answerStatus := generated.Status
|
||||
answer := generated.Content
|
||||
modelName := generated.ModelName
|
||||
promptTokens := generated.PromptTokens
|
||||
completionTokens := generated.CompletionTokens
|
||||
generateMs := time.Since(generateStartedAt).Milliseconds()
|
||||
|
||||
rerankLimit := 0
|
||||
chunkProvider := ""
|
||||
chunkTargetTokens := 0
|
||||
@@ -191,7 +181,7 @@ func (s *answer) DebugAnswer(ctx context.Context, req request.KnowledgeAnswerReq
|
||||
Question: req.Question,
|
||||
Answer: answer,
|
||||
AnswerStatus: int(answerStatus),
|
||||
AnswerStatusName: getAnswerStatusName(answerStatus),
|
||||
AnswerStatusName: getAnswerStatusName(ctx, answerStatus),
|
||||
Citations: citations,
|
||||
Hits: hits,
|
||||
HitCount: len(hits),
|
||||
@@ -376,20 +366,20 @@ func resolveDefaultRerankLimit(items []models.KnowledgeBase) int {
|
||||
return limit
|
||||
}
|
||||
|
||||
func buildAnswerSystemPrompt(answerMode enums.KnowledgeAnswerMode) string {
|
||||
func buildAnswerSystemPrompt(ctx context.Context, answerMode enums.KnowledgeAnswerMode) string {
|
||||
key := "knowledge.rag.system.strict"
|
||||
if answerMode == enums.KnowledgeAnswerModeAssist {
|
||||
return "你是客服知识库助手。请优先依据提供的知识片段回答,可以做轻度归纳,但不要编造未提供的事实。"
|
||||
key = "knowledge.rag.system.assist"
|
||||
}
|
||||
return "你是严格的客服知识库助手。只能依据提供的知识片段回答;如果资料不足,请明确说明知识库暂无明确信息。"
|
||||
return i18nx.Getf(i18nx.LocaleFromContext(ctx), key)
|
||||
}
|
||||
|
||||
func buildFallbackAnswer(fallbackMode enums.AIAgentFallbackMode) string {
|
||||
switch fallbackMode {
|
||||
case enums.AIAgentFallbackModeSuggestRetry:
|
||||
return "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。"
|
||||
default:
|
||||
return "当前知识库暂无明确信息。"
|
||||
func buildFallbackAnswer(ctx context.Context, fallbackMode enums.AIAgentFallbackMode) string {
|
||||
key := "knowledge.rag.fallback.noAnswer"
|
||||
if fallbackMode == enums.AIAgentFallbackModeSuggestRetry {
|
||||
key = "knowledge.rag.fallback.suggestRetry"
|
||||
}
|
||||
return i18nx.Getf(i18nx.LocaleFromContext(ctx), key)
|
||||
}
|
||||
|
||||
func defaultRetrieveChannel(channel string) string {
|
||||
@@ -406,17 +396,52 @@ func defaultRetrieveScene(scene string) string {
|
||||
return scene
|
||||
}
|
||||
|
||||
func getAnswerStatusName(status enums.KnowledgeAnswerStatus) string {
|
||||
func getAnswerStatusName(ctx context.Context, status enums.KnowledgeAnswerStatus) string {
|
||||
key := "knowledge.labels.unknown"
|
||||
switch status {
|
||||
case enums.KnowledgeAnswerStatusNormal:
|
||||
return "正常"
|
||||
case enums.KnowledgeAnswerStatusNoAnswer:
|
||||
return "无答案"
|
||||
case enums.KnowledgeAnswerStatusFallback:
|
||||
return "兜底"
|
||||
case enums.KnowledgeAnswerStatusBlocked:
|
||||
return "风控拦截"
|
||||
default:
|
||||
return "未知"
|
||||
case enums.KnowledgeAnswerStatusNormal, enums.KnowledgeAnswerStatusNoAnswer, enums.KnowledgeAnswerStatusFallback, enums.KnowledgeAnswerStatusBlocked:
|
||||
key = fmt.Sprintf("knowledge.labels.answer.%d", status)
|
||||
}
|
||||
return i18nx.Getf(i18nx.LocaleFromContext(ctx), key)
|
||||
}
|
||||
|
||||
type generatedKnowledgeAnswer struct {
|
||||
ai.ChatCompletionResult
|
||||
Status enums.KnowledgeAnswerStatus
|
||||
}
|
||||
|
||||
type knowledgeChatFunc func(context.Context, string, string) (*ai.ChatCompletionResult, error)
|
||||
|
||||
// Keep generation independent of persistence so fallback and cancellation paths
|
||||
// can be tested without a configured model, external service, or database.
|
||||
func generateKnowledgeAnswer(ctx context.Context, question string, results []RetrieveResult, mode enums.KnowledgeAnswerMode, fallbackMode enums.AIAgentFallbackMode, chat knowledgeChatFunc) (*generatedKnowledgeAnswer, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &generatedKnowledgeAnswer{Status: enums.KnowledgeAnswerStatusNoAnswer}
|
||||
result.Content = buildFallbackAnswer(ctx, fallbackMode)
|
||||
if len(results) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
contextText := Retrieve.BuildContext(ctx, results, 4000)
|
||||
systemPrompt := buildAnswerSystemPrompt(ctx, mode)
|
||||
userPrompt := i18nx.Getf(i18nx.LocaleFromContext(ctx), "knowledge.rag.userPrompt", question, contextText)
|
||||
llmResult, err := chat(ctx, systemPrompt, userPrompt)
|
||||
// An interrupted request is not a model failure and must not be logged as a
|
||||
// completed fallback answer after the client has gone away.
|
||||
if contextErr := ctx.Err(); contextErr != nil {
|
||||
return nil, contextErr
|
||||
}
|
||||
if err != nil || llmResult == nil {
|
||||
result.Status = enums.KnowledgeAnswerStatusFallback
|
||||
return result, nil
|
||||
}
|
||||
result.ChatCompletionResult = *llmResult
|
||||
if strings.TrimSpace(llmResult.Content) == "" {
|
||||
result.Status = enums.KnowledgeAnswerStatusFallback
|
||||
result.Content = buildFallbackAnswer(ctx, fallbackMode)
|
||||
return result, nil
|
||||
}
|
||||
result.Status = enums.KnowledgeAnswerStatusNormal
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user