mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 12:18:59 +00:00
feat(memory): add chunk overlap support and clean up per-agent config
- Add ChunkOverlap to MemoryConfig (default 200) for context continuity at chunk boundaries during semantic search - Implement overlap logic in ChunkText() with safety clamp at maxChunkLen/2 - Wire per-agent chunk_len/chunk_overlap overrides at IndexDocument time via RunContext, falling back to global PGMemoryConfig defaults - Add sync.RWMutex to PGMemoryStore for thread-safe config updates - Persist chunk settings via system_configs (embedding.max_chunk_len, embedding.chunk_overlap) with runtime refresh on config change - Remove dead per-agent embedding_provider/embedding_model fields from agent Memory UI (system uses single global provider) - Show Memory section always-visible in agent config (no toggle needed) - Add chunk_overlap fields to System Settings modal and Config AI Defaults - Update i18n (en/vi/zh) for all new and removed keys
This commit is contained in:
1 parent
da98a54393
commit
258e378593
20 files changed
+275
-106
No files matched your search
@@ -410,6 +410,12 @@ func runGateway() {
|
||||
}
|
||||
if sysConfigs, err := pgStores.SystemConfigs.List(ctx); err == nil && len(sysConfigs) > 0 {
|
||||
cfg.ApplySystemConfigs(sysConfigs)
|
||||
// Update PGMemoryStore chunk config so new documents use updated settings
|
||||
if mem := cfg.Agents.Defaults.Memory; mem != nil {
|
||||
if pgMem, ok := pgStores.Memory.(*pg.PGMemoryStore); ok {
|
||||
pgMem.UpdateChunkConfig(mem.MaxChunkLen, mem.ChunkOverlap)
|
||||
}
|
||||
}
|
||||
slog.Debug("system_configs refreshed to in-memory config", "keys", len(sysConfigs))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -87,6 +87,8 @@ func seedConfigForContext(ctx context.Context, sc store.SystemConfigStore, cfg *
|
||||
if m := cfg.Agents.Defaults.Memory; m != nil {
|
||||
set("embedding.provider", m.EmbeddingProvider)
|
||||
set("embedding.model", m.EmbeddingModel)
|
||||
setInt("embedding.max_chunk_len", m.MaxChunkLen)
|
||||
setInt("embedding.chunk_overlap", m.ChunkOverlap)
|
||||
}
|
||||
|
||||
// Agent defaults
|
||||
|
||||
@@ -181,6 +181,7 @@ type MemoryConfig struct {
|
||||
EmbeddingAPIBase string `json:"embedding_api_base,omitempty"` // custom endpoint URL
|
||||
MaxResults int `json:"max_results,omitempty"` // default 6
|
||||
MaxChunkLen int `json:"max_chunk_len,omitempty"` // default 1000
|
||||
ChunkOverlap int `json:"chunk_overlap,omitempty"` // overlap chars between chunks (default 200)
|
||||
VectorWeight float64 `json:"vector_weight,omitempty"` // hybrid search vector weight (default 0.7)
|
||||
TextWeight float64 `json:"text_weight,omitempty"` // hybrid search FTS weight (default 0.3)
|
||||
MinScore float64 `json:"min_score,omitempty"` // minimum relevance score (default 0.35)
|
||||
|
||||
@@ -34,6 +34,8 @@ func (c *Config) ApplySystemConfigs(configs map[string]string) {
|
||||
}
|
||||
str("embedding.provider", &c.Agents.Defaults.Memory.EmbeddingProvider)
|
||||
str("embedding.model", &c.Agents.Defaults.Memory.EmbeddingModel)
|
||||
integer("embedding.max_chunk_len", &c.Agents.Defaults.Memory.MaxChunkLen)
|
||||
integer("embedding.chunk_overlap", &c.Agents.Defaults.Memory.ChunkOverlap)
|
||||
|
||||
// Agent defaults
|
||||
str("agent.default_provider", &c.Agents.Defaults.Provider)
|
||||
|
||||
@@ -25,18 +25,31 @@ type TextChunk struct {
|
||||
EndLine int
|
||||
}
|
||||
|
||||
// ChunkText splits text into chunks at paragraph boundaries.
|
||||
// ChunkText splits text into chunks at paragraph boundaries with optional overlap.
|
||||
// Each chunk includes its starting line number in the source file.
|
||||
func ChunkText(text string, maxChunkLen int) []TextChunk {
|
||||
// When overlap > 0, trailing lines from the previous chunk are prepended to the next chunk
|
||||
// so that context at chunk boundaries is preserved for semantic search.
|
||||
func ChunkText(text string, maxChunkLen, overlap int) []TextChunk {
|
||||
if maxChunkLen <= 0 {
|
||||
maxChunkLen = 1000
|
||||
}
|
||||
if overlap < 0 {
|
||||
overlap = 0
|
||||
}
|
||||
// Clamp overlap to half the chunk size to prevent infinite loop
|
||||
if overlap >= maxChunkLen/2 {
|
||||
overlap = maxChunkLen / 2
|
||||
}
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
var chunks []TextChunk
|
||||
var current strings.Builder
|
||||
startLine := 1
|
||||
|
||||
// overlapLines holds trailing lines from the previous chunk to prepend to the next.
|
||||
var overlapLines []string
|
||||
overlapStartLine := 0
|
||||
|
||||
flush := func(endLine int) {
|
||||
content := strings.TrimSpace(current.String())
|
||||
if content != "" {
|
||||
@@ -46,8 +59,40 @@ func ChunkText(text string, maxChunkLen int) []TextChunk {
|
||||
EndLine: endLine,
|
||||
})
|
||||
}
|
||||
|
||||
// Compute overlap lines to carry into the next chunk.
|
||||
overlapLines = nil
|
||||
overlapStartLine = 0
|
||||
if overlap > 0 && endLine > 0 {
|
||||
charCount := 0
|
||||
for j := endLine - 1; j >= startLine-1 && j >= 0; j-- {
|
||||
lineLen := len(lines[j])
|
||||
if charCount+lineLen > overlap {
|
||||
break
|
||||
}
|
||||
charCount += lineLen + 1 // +1 for newline
|
||||
overlapLines = append(overlapLines, lines[j])
|
||||
overlapStartLine = j + 1 // 1-based
|
||||
}
|
||||
// Reverse to restore original order
|
||||
for left, right := 0, len(overlapLines)-1; left < right; left, right = left+1, right-1 {
|
||||
overlapLines[left], overlapLines[right] = overlapLines[right], overlapLines[left]
|
||||
}
|
||||
}
|
||||
|
||||
current.Reset()
|
||||
startLine = endLine + 1
|
||||
// Seed next chunk with overlap content
|
||||
if len(overlapLines) > 0 {
|
||||
startLine = overlapStartLine
|
||||
for k, ol := range overlapLines {
|
||||
if k > 0 {
|
||||
current.WriteString("\n")
|
||||
}
|
||||
current.WriteString(ol)
|
||||
}
|
||||
} else {
|
||||
startLine = endLine + 1
|
||||
}
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -17,12 +18,14 @@ import (
|
||||
type PGMemoryStore struct {
|
||||
db *sql.DB
|
||||
provider store.EmbeddingProvider
|
||||
mu sync.RWMutex // protects cfg from concurrent read/write
|
||||
cfg PGMemoryConfig
|
||||
}
|
||||
|
||||
// PGMemoryConfig configures the PG memory store.
|
||||
type PGMemoryConfig struct {
|
||||
MaxChunkLen int
|
||||
ChunkOverlap int
|
||||
MaxResults int
|
||||
VectorWeight float64
|
||||
TextWeight float64
|
||||
@@ -32,6 +35,7 @@ type PGMemoryConfig struct {
|
||||
func DefaultPGMemoryConfig() PGMemoryConfig {
|
||||
return PGMemoryConfig{
|
||||
MaxChunkLen: 1000,
|
||||
ChunkOverlap: 200,
|
||||
MaxResults: 6,
|
||||
VectorWeight: 0.7,
|
||||
TextWeight: 0.3,
|
||||
@@ -207,8 +211,19 @@ func (s *PGMemoryStore) IndexDocument(ctx context.Context, agentID, userID, path
|
||||
// Delete old chunks
|
||||
s.db.ExecContext(ctx, "DELETE FROM memory_chunks WHERE document_id = $1", docID)
|
||||
|
||||
// Resolve chunk parameters: per-agent override → global default
|
||||
chunkLen, chunkOverlap := s.chunkConfig()
|
||||
if rc := store.RunContextFromCtx(ctx); rc != nil && rc.MemoryCfg != nil {
|
||||
if rc.MemoryCfg.MaxChunkLen > 0 {
|
||||
chunkLen = rc.MemoryCfg.MaxChunkLen
|
||||
}
|
||||
if rc.MemoryCfg.ChunkOverlap > 0 {
|
||||
chunkOverlap = rc.MemoryCfg.ChunkOverlap
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk text
|
||||
chunks := memory.ChunkText(content, s.cfg.MaxChunkLen)
|
||||
chunks := memory.ChunkText(content, chunkLen, chunkOverlap)
|
||||
if len(chunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -350,6 +365,25 @@ func (s *PGMemoryStore) SetEmbeddingProvider(provider store.EmbeddingProvider) {
|
||||
s.provider = provider
|
||||
}
|
||||
|
||||
// UpdateChunkConfig updates chunk splitting parameters at runtime (e.g. after system config change).
|
||||
func (s *PGMemoryStore) UpdateChunkConfig(maxChunkLen, chunkOverlap int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if maxChunkLen > 0 {
|
||||
s.cfg.MaxChunkLen = maxChunkLen
|
||||
}
|
||||
if chunkOverlap >= 0 {
|
||||
s.cfg.ChunkOverlap = chunkOverlap
|
||||
}
|
||||
}
|
||||
|
||||
// chunkConfig returns a snapshot of the current chunk parameters (thread-safe).
|
||||
func (s *PGMemoryStore) chunkConfig() (maxLen, overlap int) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cfg.MaxChunkLen, s.cfg.ChunkOverlap
|
||||
}
|
||||
|
||||
// BackfillEmbeddings finds all chunks without embeddings and generates them.
|
||||
// Processes in batches to avoid memory spikes. Safe to call multiple times.
|
||||
func (s *PGMemoryStore) BackfillEmbeddings(ctx context.Context) (int, error) {
|
||||
|
||||
@@ -64,6 +64,8 @@ interface SystemSettingsModalProps {
|
||||
interface InitState {
|
||||
embProvider: string;
|
||||
embModel: string;
|
||||
embMaxChunkLen: string;
|
||||
embChunkOverlap: string;
|
||||
toolStatus: boolean;
|
||||
blockReply: boolean;
|
||||
intentClassify: boolean;
|
||||
@@ -76,6 +78,7 @@ interface InitState {
|
||||
|
||||
const DEFAULTS: InitState = {
|
||||
embProvider: "", embModel: "",
|
||||
embMaxChunkLen: "", embChunkOverlap: "",
|
||||
toolStatus: true, blockReply: false, intentClassify: true,
|
||||
compProvider: "", compModel: "",
|
||||
compThreshold: "", compKeepRecent: "", compMaxTokens: "",
|
||||
@@ -98,6 +101,8 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
// Embedding
|
||||
const [embProvider, setEmbProvider] = useState("");
|
||||
const [embModel, setEmbModel] = useState("");
|
||||
const [embMaxChunkLen, setEmbMaxChunkLen] = useState("");
|
||||
const [embChunkOverlap, setEmbChunkOverlap] = useState("");
|
||||
const { verifyEmbedding, embVerifying, embResult, resetEmb } = useProviderVerify();
|
||||
|
||||
// UX Behavior
|
||||
@@ -116,6 +121,8 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
const s: InitState = {
|
||||
embProvider: configs["embedding.provider"] ?? "",
|
||||
embModel: configs["embedding.model"] ?? "",
|
||||
embMaxChunkLen: configs["embedding.max_chunk_len"] ?? "",
|
||||
embChunkOverlap: configs["embedding.chunk_overlap"] ?? "",
|
||||
toolStatus: parseBool(configs["gateway.tool_status"], true),
|
||||
blockReply: parseBool(configs["gateway.block_reply"], false),
|
||||
intentClassify: parseBool(configs["gateway.intent_classify"], true),
|
||||
@@ -128,6 +135,8 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
setInit(s);
|
||||
setEmbProvider(s.embProvider);
|
||||
setEmbModel(s.embModel);
|
||||
setEmbMaxChunkLen(s.embMaxChunkLen);
|
||||
setEmbChunkOverlap(s.embChunkOverlap);
|
||||
setToolStatus(s.toolStatus);
|
||||
setBlockReply(s.blockReply);
|
||||
setIntentClassify(s.intentClassify);
|
||||
@@ -172,6 +181,8 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
const updates: Record<string, string> = {};
|
||||
if (embProvider !== init.embProvider) updates["embedding.provider"] = embProvider;
|
||||
if (embModel !== init.embModel) updates["embedding.model"] = embModel;
|
||||
if (embMaxChunkLen !== init.embMaxChunkLen) updates["embedding.max_chunk_len"] = embMaxChunkLen;
|
||||
if (embChunkOverlap !== init.embChunkOverlap) updates["embedding.chunk_overlap"] = embChunkOverlap;
|
||||
if (toolStatus !== init.toolStatus) updates["gateway.tool_status"] = String(toolStatus);
|
||||
if (blockReply !== init.blockReply) updates["gateway.block_reply"] = String(blockReply);
|
||||
if (intentClassify !== init.intentClassify) updates["gateway.intent_classify"] = String(intentClassify);
|
||||
@@ -317,6 +328,19 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="embMaxChunkLen" className="text-xs">{t("embedding.maxChunkLen")}</Label>
|
||||
<Input id="embMaxChunkLen" type="number" placeholder="1000" value={embMaxChunkLen} onChange={(e) => setEmbMaxChunkLen(e.target.value)} className="text-base md:text-sm" />
|
||||
<p className="text-xs text-muted-foreground">{t("embedding.maxChunkLenHint")}</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="embChunkOverlap" className="text-xs">{t("embedding.chunkOverlap")}</Label>
|
||||
<Input id="embChunkOverlap" type="number" placeholder="200" value={embChunkOverlap} onChange={(e) => setEmbChunkOverlap(e.target.value)} className="text-base md:text-sm" />
|
||||
<p className="text-xs text-muted-foreground">{t("embedding.chunkOverlapHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -383,12 +383,20 @@
|
||||
"memory": {
|
||||
"title": "Memory",
|
||||
"description": "Semantic memory search and embedding configuration",
|
||||
"embeddingProvider": "Embedding Provider",
|
||||
"embeddingModel": "Embedding Model",
|
||||
"maxResults": "Max Results",
|
||||
"enabled": "Enabled",
|
||||
"enabledTip": "Enable or disable the memory system for this agent.",
|
||||
"maxChunkLen": "Max Chunk Length",
|
||||
"maxChunkLenTip": "Maximum characters per memory chunk. Leave empty for system default.",
|
||||
"chunkOverlap": "Chunk Overlap",
|
||||
"chunkOverlapTip": "Overlap characters between chunks for context continuity. Leave empty for system default.",
|
||||
"maxResults": "Max Results",
|
||||
"maxResultsTip": "Maximum memory entries returned per search query.",
|
||||
"minScore": "Min Score",
|
||||
"minScoreTip": "Minimum relevance score for search results.",
|
||||
"vectorWeight": "Vector Weight",
|
||||
"textWeight": "Text Weight"
|
||||
"vectorWeightTip": "Weight for semantic similarity in hybrid search.",
|
||||
"textWeight": "Text Weight",
|
||||
"textWeightTip": "Weight for keyword similarity in hybrid search."
|
||||
},
|
||||
"thinking": {
|
||||
"title": "Extended Thinking",
|
||||
|
||||
@@ -87,6 +87,10 @@
|
||||
"agents.memory.embeddingModelTip": "Embedding model name. Leave empty to use the provider default.",
|
||||
"agents.memory.maxResults": "Max Results",
|
||||
"agents.memory.maxResultsTip": "Maximum number of memory entries returned per recall query.",
|
||||
"agents.memory.maxChunkLen": "Max Chunk Length",
|
||||
"agents.memory.maxChunkLenTip": "Maximum characters per memory chunk before splitting.",
|
||||
"agents.memory.chunkOverlap": "Chunk Overlap",
|
||||
"agents.memory.chunkOverlapTip": "Overlap characters between chunks for context continuity.",
|
||||
"agents.memory.minScore": "Min Score",
|
||||
"agents.memory.minScoreTip": "Minimum similarity score (0-1) for a memory entry to be included in results.",
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"verifyRequired": "Verify the embedding model before saving.",
|
||||
"verifyFailed": "Verification failed",
|
||||
"dimensions": "{{count}} dimensions",
|
||||
"dimensionsMismatch": "{{count}} dimensions — does not match required 1536"
|
||||
"dimensionsMismatch": "{{count}} dimensions — does not match required 1536",
|
||||
"maxChunkLen": "Max Chunk Length",
|
||||
"maxChunkLenHint": "Maximum characters per memory chunk before splitting.",
|
||||
"chunkOverlap": "Chunk Overlap",
|
||||
"chunkOverlapHint": "Overlap characters between consecutive chunks for context continuity."
|
||||
},
|
||||
"ux": {
|
||||
"title": "UX Behavior",
|
||||
|
||||
@@ -383,12 +383,20 @@
|
||||
"memory": {
|
||||
"title": "Bộ nhớ",
|
||||
"description": "Tìm kiếm bộ nhớ ngữ nghĩa và cấu hình nhúng",
|
||||
"embeddingProvider": "Provider nhúng",
|
||||
"embeddingModel": "Model nhúng",
|
||||
"maxResults": "Kết quả tối đa",
|
||||
"enabled": "Đã bật",
|
||||
"enabledTip": "Bật/tắt hệ thống bộ nhớ cho agent này.",
|
||||
"maxChunkLen": "Độ dài chunk tối đa",
|
||||
"maxChunkLenTip": "Số ký tự tối đa mỗi chunk. Để trống dùng mặc định hệ thống.",
|
||||
"chunkOverlap": "Chunk Overlap",
|
||||
"chunkOverlapTip": "Số ký tự chồng lấp giữa các chunk để giữ ngữ cảnh. Để trống dùng mặc định.",
|
||||
"maxResults": "Kết quả tối đa",
|
||||
"maxResultsTip": "Số kết quả tối đa trả về mỗi truy vấn tìm kiếm.",
|
||||
"minScore": "Điểm tối thiểu",
|
||||
"minScoreTip": "Ngưỡng điểm tối thiểu cho kết quả tìm kiếm.",
|
||||
"vectorWeight": "Trọng số vector",
|
||||
"textWeight": "Trọng số văn bản"
|
||||
"vectorWeightTip": "Trọng số tương đồng ngữ nghĩa trong tìm kiếm hỗn hợp.",
|
||||
"textWeight": "Trọng số văn bản",
|
||||
"textWeightTip": "Trọng số từ khóa trong tìm kiếm hỗn hợp."
|
||||
},
|
||||
"thinking": {
|
||||
"title": "Suy nghĩ mở rộng",
|
||||
|
||||
@@ -87,6 +87,10 @@
|
||||
"agents.memory.embeddingModelTip": "Tên model nhúng. Để trống để dùng mặc định của provider.",
|
||||
"agents.memory.maxResults": "Kết quả tối đa",
|
||||
"agents.memory.maxResultsTip": "Số mục bộ nhớ tối đa trả về cho mỗi truy vấn.",
|
||||
"agents.memory.maxChunkLen": "Độ dài chunk tối đa",
|
||||
"agents.memory.maxChunkLenTip": "Số ký tự tối đa mỗi chunk trước khi chia nhỏ.",
|
||||
"agents.memory.chunkOverlap": "Chunk Overlap",
|
||||
"agents.memory.chunkOverlapTip": "Số ký tự chồng lấp giữa các chunk để giữ ngữ cảnh.",
|
||||
"agents.memory.minScore": "Điểm tối thiểu",
|
||||
"agents.memory.minScoreTip": "Điểm tương đồng tối thiểu (0-1) để đưa mục bộ nhớ vào kết quả.",
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"verifyRequired": "Kiểm tra model embedding trước khi lưu.",
|
||||
"verifyFailed": "Kiểm tra thất bại",
|
||||
"dimensions": "{{count}} chiều",
|
||||
"dimensionsMismatch": "{{count}} chiều — không khớp yêu cầu 1536"
|
||||
"dimensionsMismatch": "{{count}} chiều — không khớp yêu cầu 1536",
|
||||
"maxChunkLen": "Độ dài chunk tối đa",
|
||||
"maxChunkLenHint": "Số ký tự tối đa mỗi chunk trước khi chia nhỏ.",
|
||||
"chunkOverlap": "Chunk Overlap",
|
||||
"chunkOverlapHint": "Số ký tự chồng lấp giữa các chunk liên tiếp để giữ ngữ cảnh."
|
||||
},
|
||||
"ux": {
|
||||
"title": "Hành vi UX",
|
||||
|
||||
@@ -383,12 +383,20 @@
|
||||
"memory": {
|
||||
"title": "记忆",
|
||||
"description": "语义记忆搜索和嵌入配置",
|
||||
"embeddingProvider": "嵌入Provider",
|
||||
"embeddingModel": "嵌入模型",
|
||||
"maxResults": "最大结果数",
|
||||
"enabled": "已启用",
|
||||
"enabledTip": "启用或禁用此Agent的记忆系统。",
|
||||
"maxChunkLen": "最大块长度",
|
||||
"maxChunkLenTip": "每个记忆块的最大字符数。留空使用系统默认值。",
|
||||
"chunkOverlap": "块重叠",
|
||||
"chunkOverlapTip": "连续块之间的重叠字符数。留空使用系统默认值。",
|
||||
"maxResults": "最大结果数",
|
||||
"maxResultsTip": "每次搜索返回的最大记忆条目数。",
|
||||
"minScore": "最低分数",
|
||||
"minScoreTip": "搜索结果的最低相关性分数阈值。",
|
||||
"vectorWeight": "向量权重",
|
||||
"textWeight": "文本权重"
|
||||
"vectorWeightTip": "混合搜索中语义相似度的权重。",
|
||||
"textWeight": "文本权重",
|
||||
"textWeightTip": "混合搜索中关键词相似度的权重。"
|
||||
},
|
||||
"thinking": {
|
||||
"title": "扩展思考",
|
||||
|
||||
@@ -87,6 +87,10 @@
|
||||
"agents.memory.embeddingModelTip": "嵌入模型名称。留空则使用提供商默认值。",
|
||||
"agents.memory.maxResults": "最大结果数",
|
||||
"agents.memory.maxResultsTip": "每次召回查询返回的最大记忆条目数。",
|
||||
"agents.memory.maxChunkLen": "最大块长度",
|
||||
"agents.memory.maxChunkLenTip": "每个记忆块分割前的最大字符数。",
|
||||
"agents.memory.chunkOverlap": "块重叠",
|
||||
"agents.memory.chunkOverlapTip": "连续块之间的重叠字符数,用于保持上下文连续性。",
|
||||
"agents.memory.minScore": "最低分数",
|
||||
"agents.memory.minScoreTip": "记忆条目被纳入结果的最低相似度分数(0-1)。",
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"verifyRequired": "保存前请先验证Embedding模型。",
|
||||
"verifyFailed": "验证失败",
|
||||
"dimensions": "{{count}}维",
|
||||
"dimensionsMismatch": "{{count}}维 — 不符合要求的1536"
|
||||
"dimensionsMismatch": "{{count}}维 — 不符合要求的1536",
|
||||
"maxChunkLen": "最大块长度",
|
||||
"maxChunkLenHint": "每个记忆块分割前的最大字符数。",
|
||||
"chunkOverlap": "块重叠",
|
||||
"chunkOverlapHint": "连续块之间的重叠字符数,用于保持上下文连续性。"
|
||||
},
|
||||
"ux": {
|
||||
"title": "UX行为",
|
||||
|
||||
@@ -11,7 +11,6 @@ import { EvolutionSection } from "./overview-sections/evolution-section";
|
||||
import { CapabilitiesSection } from "./overview-sections/capabilities-section";
|
||||
import { HeartbeatCard } from "./overview-sections/heartbeat-card";
|
||||
import { MemorySection } from "./config-sections";
|
||||
import { ConfigGroupHeader } from "@/components/shared/config-group-header";
|
||||
import type { UseAgentHeartbeatReturn } from "../hooks/use-agent-heartbeat";
|
||||
|
||||
interface AgentOverviewTabProps {
|
||||
@@ -47,8 +46,7 @@ export function AgentOverviewTab({ agent, onUpdate, heartbeat }: AgentOverviewTa
|
||||
typeof otherCfg.skill_nudge_interval === "number" ? otherCfg.skill_nudge_interval : 15,
|
||||
);
|
||||
|
||||
// Memory (now standalone section)
|
||||
const [memEnabled, setMemEnabled] = useState(agent.memory_config != null);
|
||||
// Memory (always shown — per-agent overrides, empty = use system defaults)
|
||||
const [mem, setMem] = useState<MemoryConfig>(agent.memory_config ?? {});
|
||||
|
||||
// Capabilities (subagents + tool policy)
|
||||
@@ -83,7 +81,7 @@ export function AgentOverviewTab({ agent, onUpdate, heartbeat }: AgentOverviewTa
|
||||
is_default: isDefault,
|
||||
other_config: updatedOtherConfig,
|
||||
budget_monthly_cents: budgetCents,
|
||||
memory_config: memEnabled ? mem : null,
|
||||
memory_config: mem,
|
||||
subagents_config: subEnabled ? sub : null,
|
||||
tools_config: toolsEnabled
|
||||
? { profile: tools.profile, allow: tools.allow, deny: tools.deny, alsoAllow: tools.alsoAllow, byProvider: tools.byProvider }
|
||||
@@ -128,19 +126,11 @@ export function AgentOverviewTab({ agent, onUpdate, heartbeat }: AgentOverviewTa
|
||||
onSaveBlockedChange={setLlmSaveBlocked}
|
||||
/>
|
||||
|
||||
{/* Memory — standalone section (was inside Capabilities) */}
|
||||
<section className="space-y-4">
|
||||
<ConfigGroupHeader
|
||||
title={t("configSections.memory.title")}
|
||||
description={t("configSections.memory.description")}
|
||||
/>
|
||||
<MemorySection
|
||||
enabled={memEnabled}
|
||||
value={mem}
|
||||
onToggle={(v) => { setMemEnabled(v); if (!v) setMem({}); }}
|
||||
onChange={setMem}
|
||||
/>
|
||||
</section>
|
||||
{/* Memory — always visible, per-agent overrides */}
|
||||
<MemorySection
|
||||
value={mem}
|
||||
onChange={setMem}
|
||||
/>
|
||||
|
||||
<HeartbeatCard heartbeat={heartbeat} />
|
||||
|
||||
|
||||
@@ -2,89 +2,100 @@ import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { MemoryConfig } from "@/types/agent";
|
||||
import { ConfigSection, InfoLabel, numOrUndef } from "./config-section";
|
||||
import { ProviderModelSelect } from "@/components/shared/provider-model-select";
|
||||
import { InfoLabel, numOrUndef } from "./config-section";
|
||||
|
||||
interface MemorySectionProps {
|
||||
enabled: boolean;
|
||||
value: MemoryConfig;
|
||||
onToggle: (v: boolean) => void;
|
||||
onChange: (v: MemoryConfig) => void;
|
||||
}
|
||||
|
||||
export function MemorySection({ enabled, value, onToggle, onChange }: MemorySectionProps) {
|
||||
export function MemorySection({ value, onChange }: MemorySectionProps) {
|
||||
const { t } = useTranslation("agents");
|
||||
const s = "configSections.memory";
|
||||
return (
|
||||
<ConfigSection
|
||||
title={t(`${s}.title`)}
|
||||
description={t(`${s}.description`)}
|
||||
enabled={enabled}
|
||||
onToggle={onToggle}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={value.enabled ?? true}
|
||||
onCheckedChange={(v) => onChange({ ...value, enabled: v })}
|
||||
/>
|
||||
<InfoLabel tip="Enable or disable the memory system for this agent. When enabled, the agent can store and recall information across sessions.">{t("configSections.contextPruning.enabled")}</InfoLabel>
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">{t(`${s}.title`)}</h3>
|
||||
<p className="text-xs text-muted-foreground">{t(`${s}.description`)}</p>
|
||||
</div>
|
||||
<ProviderModelSelect
|
||||
provider={value.embedding_provider ?? ""}
|
||||
onProviderChange={(v) => onChange({ ...value, embedding_provider: v || undefined })}
|
||||
model={value.embedding_model ?? ""}
|
||||
onModelChange={(v) => onChange({ ...value, embedding_model: v || undefined })}
|
||||
providerLabel={t(`${s}.embeddingProvider`)}
|
||||
modelLabel={t(`${s}.embeddingModel`)}
|
||||
providerTip="LLM provider used for generating text embeddings. Leave empty to auto-detect. Only providers with embedding enabled are shown."
|
||||
modelTip="Embedding model name (e.g. text-embedding-3-small). Must be supported by the provider."
|
||||
providerPlaceholder="(auto)"
|
||||
modelPlaceholder="text-embedding-3-small"
|
||||
allowEmpty
|
||||
filterEmbedding
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip="Maximum number of memory entries returned per search query.">{t(`${s}.maxResults`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="6"
|
||||
value={value.max_results ?? ""}
|
||||
onChange={(e) => onChange({ ...value, max_results: numOrUndef(e.target.value) })}
|
||||
<div className="rounded-lg border p-3 space-y-4 sm:p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={value.enabled ?? true}
|
||||
onCheckedChange={(v) => onChange({ ...value, enabled: v })}
|
||||
/>
|
||||
<InfoLabel tip={t(`${s}.enabledTip`)}>{t(`${s}.enabled`)}</InfoLabel>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip="Maximum character length for each memory chunk. Longer content is split into smaller chunks before storing.">{t(`${s}.maxChunkLen`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
value={value.max_chunk_len ?? ""}
|
||||
onChange={(e) => onChange({ ...value, max_chunk_len: numOrUndef(e.target.value) })}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.maxChunkLenTip`)}>{t(`${s}.maxChunkLen`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
value={value.max_chunk_len ?? ""}
|
||||
onChange={(e) => onChange({ ...value, max_chunk_len: numOrUndef(e.target.value) })}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.chunkOverlapTip`)}>{t(`${s}.chunkOverlap`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="200"
|
||||
value={value.chunk_overlap ?? ""}
|
||||
onChange={(e) => onChange({ ...value, chunk_overlap: numOrUndef(e.target.value) })}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.maxResultsTip`)}>{t(`${s}.maxResults`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="6"
|
||||
value={value.max_results ?? ""}
|
||||
onChange={(e) => onChange({ ...value, max_results: numOrUndef(e.target.value) })}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.minScoreTip`)}>{t(`${s}.minScore`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.35"
|
||||
value={value.min_score ?? ""}
|
||||
onChange={(e) => onChange({ ...value, min_score: numOrUndef(e.target.value) })}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.vectorWeightTip`)}>{t(`${s}.vectorWeight`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.7"
|
||||
value={value.vector_weight ?? ""}
|
||||
onChange={(e) => onChange({ ...value, vector_weight: numOrUndef(e.target.value) })}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.textWeightTip`)}>{t(`${s}.textWeight`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.3"
|
||||
value={value.text_weight ?? ""}
|
||||
onChange={(e) => onChange({ ...value, text_weight: numOrUndef(e.target.value) })}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip="Weight for vector (semantic) similarity in hybrid search scoring. Higher values prioritize meaning over keywords.">{t(`${s}.vectorWeight`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.7"
|
||||
value={value.vector_weight ?? ""}
|
||||
onChange={(e) => onChange({ ...value, vector_weight: numOrUndef(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip="Weight for text (keyword/BM25) similarity in hybrid search scoring. Higher values prioritize exact keyword matches.">{t(`${s}.textWeight`)}</InfoLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.3"
|
||||
value={value.text_weight ?? ""}
|
||||
onChange={(e) => onChange({ ...value, text_weight: numOrUndef(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConfigSection>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -180,6 +180,10 @@ export function AiDefaultsSection({ data, onSave, saving }: Props) {
|
||||
<Field label={t("agents.memory.maxResults")} tip={t("agents.memory.maxResultsTip")} type="number" value={memory.max_results} onChange={(v) => updateNested("memory", { max_results: Number(v) })} placeholder="6" />
|
||||
<Field label={t("agents.memory.minScore")} tip={t("agents.memory.minScoreTip")} type="number" step="0.01" value={memory.min_score} onChange={(v) => updateNested("memory", { min_score: Number(v) })} placeholder="0.35" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<Field label={t("agents.memory.maxChunkLen")} tip={t("agents.memory.maxChunkLenTip")} type="number" value={memory.max_chunk_len} onChange={(v) => updateNested("memory", { max_chunk_len: Number(v) })} placeholder="1000" />
|
||||
<Field label={t("agents.memory.chunkOverlap")} tip={t("agents.memory.chunkOverlapTip")} type="number" value={memory.chunk_overlap} onChange={(v) => updateNested("memory", { chunk_overlap: Number(v) })} placeholder="200" />
|
||||
</div>
|
||||
</SubSection>
|
||||
|
||||
<SubSection
|
||||
|
||||
@@ -64,8 +64,10 @@ export interface MemoryConfig {
|
||||
embedding_model?: string;
|
||||
max_results?: number;
|
||||
max_chunk_len?: number;
|
||||
chunk_overlap?: number;
|
||||
vector_weight?: number;
|
||||
text_weight?: number;
|
||||
min_score?: number;
|
||||
}
|
||||
|
||||
export interface WorkspaceSharingConfig {
|
||||
|
||||
Reference in new issue
Block a user