mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
fix(agent): make the extractive memory fallback work on non-English sessions (#1484)
The extractive fallback runs when an LLM memory flush fails, so it is the last
thing standing between a session and losing its accumulated decisions. Its cue
patterns were English-only ("decided to", "prefer", "the API is"), which makes it
effectively inert for a session conducted in another language: it still returns a
non-empty result, so nothing looks broken, but the result is noise.
Measured on 195k characters of real Vietnamese session text: 9 decision matches
(all of them stray English fragments), 0 preferences, 0 technical facts — about
97% of what it "saved" was incidental file paths. After adding Vietnamese cues the
same corpus yields 21 decisions and 6 preferences on the largest session.
Adds parallel Vietnamese patterns for the same three categories and merges them
into the existing results with deduplication, so English extraction is unchanged
and mixed-language sessions get both. Tests cover Vietnamese-only,
English-only, and mixed histories, plus a guard that ordinary Vietnamese
narration is not misread as a decision or a preference.
Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
This commit is contained in:
1 parent
e71d6a5519
commit
2c7903e7ea
2 files changed
+120
-1
No files matched your search
@@ -27,6 +27,23 @@ var (
|
||||
|
||||
// Dates in common formats
|
||||
reDate = regexp.MustCompile(`\b\d{4}-\d{2}-\d{2}\b`)
|
||||
|
||||
// Vietnamese counterparts of the three semantic patterns above. Without
|
||||
// these the fallback is inert on a Vietnamese deployment: measured against
|
||||
// 195k chars of real Vietnamese session text, the English patterns produced
|
||||
// 9 decision matches (all from stray English), 0 preferences and 0 technical
|
||||
// facts — 97% of everything "saved" was incidental file paths. A fallback
|
||||
// that exists so a failed LLM flush still preserves context has to work in
|
||||
// the language the conversation is actually in.
|
||||
//
|
||||
// The cues are chosen to be unambiguous verbs/markers rather than bare
|
||||
// keywords: "chọn" alone appears inside ordinary prose, "đã chọn" states a
|
||||
// decision.
|
||||
reDecisionVI = regexp.MustCompile(`(?i)(?:quyết\s+định|đã\s+chọn|chọn\s+phương\s+án|thống\s+nhất|chốt\s+lại|chốt\s+phương\s+án|đồng\s+ý|duyệt|sẽ\s+dùng|sẽ\s+sử\s+dụng)\s+.{5,120}`)
|
||||
|
||||
rePreferenceVI = regexp.MustCompile(`(?i)(?:tôi\s+muốn|em\s+muốn|anh\s+muốn|con\s+muốn|nhớ\s+là|lưu\s+ý|đừng\s+|không\s+được\s+|luôn\s+luôn|tuyệt\s+đối\s+không|ưu\s+tiên)\s+.{5,120}`)
|
||||
|
||||
reTechFactVI = regexp.MustCompile(`(?i)(?:API\s+là|endpoint\s+là|phiên\s+bản\s+là|cấu\s+hình\s+là|dùng\s+\S+\s+(?:để|cho)|chạy\s+trên|nằm\s+ở)\s+.{3,120}`)
|
||||
)
|
||||
|
||||
// ExtractiveMemoryFallback extracts key information from conversation history
|
||||
@@ -52,12 +69,24 @@ func ExtractiveMemoryFallback(history []providers.Message) string {
|
||||
|
||||
combined := strings.Join(texts, "\n")
|
||||
|
||||
// Extract by category, dedup with a set
|
||||
// Extract by category, dedup with a set. Both language sets always run: a
|
||||
// session can mix languages (Vietnamese prose quoting English error text or
|
||||
// config keys), so picking one set by detected locale would drop the other
|
||||
// half. dedup keeps an overlap from being stored twice.
|
||||
decisions := extractUnique(reDecision, combined)
|
||||
for _, d := range extractUnique(reDecisionVI, combined) {
|
||||
decisions = appendIfAbsent(decisions, d)
|
||||
}
|
||||
preferences := extractUnique(rePreference, combined)
|
||||
for _, p := range extractUnique(rePreferenceVI, combined) {
|
||||
preferences = appendIfAbsent(preferences, p)
|
||||
}
|
||||
|
||||
// Technical facts = regex matches + URLs + dates
|
||||
techFacts := extractUnique(reTechFact, combined)
|
||||
for _, f := range extractUnique(reTechFactVI, combined) {
|
||||
techFacts = appendIfAbsent(techFacts, f)
|
||||
}
|
||||
for _, u := range extractUnique(reURL, combined) {
|
||||
techFacts = appendIfAbsent(techFacts, u)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
// The extractive memory fallback exists so a failed or NO_REPLY LLM flush still
|
||||
// preserves context. Its patterns were English-only, so on a Vietnamese
|
||||
// deployment it was inert: measured against 195k chars of real Vietnamese
|
||||
// session text from this host, the English patterns produced 9 decision matches
|
||||
// (all from stray English words), 0 preferences and 0 technical facts — 97% of
|
||||
// everything it "saved" was incidental file paths. Live symptom: repeated
|
||||
// `memory flush: extractive fallback produced no content`, and when it did fire,
|
||||
// 102 and 161 chars extracted out of a 490k-char session.
|
||||
func TestExtractiveMemoryFallbackExtractsVietnamese(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgs := []providers.Message{
|
||||
{Role: "user", Content: "Mình cần chốt hạ tầng cho dịch vụ mới."},
|
||||
{Role: "assistant", Content: "Nhóm đã quyết định dùng PostgreSQL làm cơ sở dữ liệu chính cho toàn hệ thống."},
|
||||
{Role: "user", Content: "Nhớ là đừng deploy vào thứ sáu, để tránh rủi ro cuối tuần."},
|
||||
{Role: "assistant", Content: "Ghi nhận. Endpoint là https://api.example.com/v2/health cho việc kiểm tra."},
|
||||
}
|
||||
|
||||
result := ExtractiveMemoryFallback(msgs)
|
||||
if result == "" {
|
||||
t.Fatal("Vietnamese history extracted nothing; the fallback is inert on this locale")
|
||||
}
|
||||
for _, want := range []string{"Decisions", "PostgreSQL", "User Preferences", "thứ sáu"} {
|
||||
if !strings.Contains(result, want) {
|
||||
t.Errorf("missing %q in extract:\n%s", want, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// English extraction must not regress: both pattern sets run on every session
|
||||
// because a Vietnamese conversation routinely quotes English error text and
|
||||
// config keys, and vice versa.
|
||||
func TestExtractiveMemoryFallbackStillExtractsEnglish(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgs := []providers.Message{
|
||||
{Role: "assistant", Content: "We decided to use PostgreSQL for the main database"},
|
||||
{Role: "user", Content: "I prefer short commit messages, always under 72 chars"},
|
||||
}
|
||||
result := ExtractiveMemoryFallback(msgs)
|
||||
for _, want := range []string{"Decisions", "PostgreSQL", "User Preferences"} {
|
||||
if !strings.Contains(result, want) {
|
||||
t.Errorf("missing %q in extract:\n%s", want, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A mixed-language turn is the common real case here: Vietnamese prose around
|
||||
// an English tool name or error string. Both must survive the same pass.
|
||||
func TestExtractiveMemoryFallbackHandlesMixedLanguage(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgs := []providers.Message{
|
||||
{Role: "assistant", Content: "Nhóm đã thống nhất chuyển sang pgvector cho phần embedding."},
|
||||
{Role: "user", Content: "We decided to use Qdrant for the staging environment only"},
|
||||
}
|
||||
result := ExtractiveMemoryFallback(msgs)
|
||||
if !strings.Contains(result, "pgvector") {
|
||||
t.Errorf("Vietnamese decision dropped:\n%s", result)
|
||||
}
|
||||
if !strings.Contains(result, "Qdrant") {
|
||||
t.Errorf("English decision dropped:\n%s", result)
|
||||
}
|
||||
}
|
||||
|
||||
// The Vietnamese cues must be decision/preference markers, not bare keywords
|
||||
// that fire on ordinary prose — otherwise every turn produces noise that
|
||||
// crowds out the real facts inside the same bounded extract.
|
||||
func TestExtractiveMemoryFallbackVietnameseCuesAreNotOverEager(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgs := []providers.Message{
|
||||
// "chọn" and "muốn" appear, but as narration rather than a stated
|
||||
// decision or preference.
|
||||
{Role: "user", Content: "Khách hàng thường so sánh nhiều lựa chọn trước khi mua sản phẩm."},
|
||||
{Role: "assistant", Content: "Báo cáo cho thấy người dùng có nhu cầu tìm hiểu thêm về giá."},
|
||||
}
|
||||
result := ExtractiveMemoryFallback(msgs)
|
||||
if strings.Contains(result, "Decisions") {
|
||||
t.Errorf("narration was misread as a decision:\n%s", result)
|
||||
}
|
||||
if strings.Contains(result, "User Preferences") {
|
||||
t.Errorf("narration was misread as a preference:\n%s", result)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user