package rag import ( "context" "encoding/json" "fmt" "strings" "agent-desk/internal/models" "agent-desk/internal/pkg/i18nx" ) // faqContent keeps application-owned labels separate from source text. Never // translate, trim, or replace substrings in the source question or answer. type faqContent struct { question string similarQuestions []string answer string } func sourceFAQContent(faq models.KnowledgeFAQ) faqContent { content := faqContent{question: faq.Question, answer: faq.Answer} if faq.SimilarQuestions != "" { _ = json.Unmarshal([]byte(faq.SimilarQuestions), &content.similarQuestions) } return content } func (content faqContent) render(locale string) string { parts := []string{i18nx.Getf(locale, "knowledge.rag.faq.question", content.question)} if len(content.similarQuestions) > 0 { similar := joinNonEmptySimilarQuestions(content.similarQuestions, i18nx.Getf(locale, "knowledge.rag.faq.separator")) parts = append(parts, i18nx.Getf(locale, "knowledge.rag.faq.similarQuestions", similar)) } parts = append(parts, i18nx.Getf(locale, "knowledge.rag.faq.answer", content.answer)) return strings.Join(parts, "\n") } // Index generation deliberately uses the configured default, not the locale of // whichever administrator requested a build. A locale read never re-embeds data. func buildFAQChunkContent(faq models.KnowledgeFAQ) string { return sourceFAQContent(faq).render(i18nx.DefaultLocale) } // legacyChinese renders the exact pre-localization serialization. This is a // recognition format, not UI copy. Keep it frozen when catalog wording changes. func (content faqContent) legacyChinese() string { parts := []string{fmt.Sprintf("问题:%s", content.question)} if len(content.similarQuestions) > 0 { parts = append(parts, "相似问:"+joinNonEmptySimilarQuestions(content.similarQuestions, ";")) } parts = append(parts, "回答:"+content.answer) return strings.Join(parts, "\n") } // Only a full byte-for-byte match against a generated representation of the // current source proves which text belongs to us. Stale/manual/malformed chunks // stay as recorded. In particular, do not infer labels by splitting on Chinese // words: the source content may itself contain those words or arbitrary lines. // This function is for live retrieval only, never historical log replay. func renderVerifiedFAQChunkContent(ctx context.Context, stored string, faq models.KnowledgeFAQ) string { content := sourceFAQContent(faq) if stored == content.legacyChinese() { return content.render(i18nx.LocaleFromContext(ctx)) } for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} { if stored == content.render(locale) { return content.render(i18nx.LocaleFromContext(ctx)) } } return stored } func joinNonEmptySimilarQuestions(items []string, separator string) string { nonEmpty := make([]string, 0, len(items)) for _, item := range items { if item != "" { nonEmpty = append(nonEmpty, item) } } return strings.Join(nonEmpty, separator) }