diff --git a/cmd/gateway.go b/cmd/gateway.go index f69b144f..cb72601b 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -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)) } }) diff --git a/cmd/gateway_system_config_sync.go b/cmd/gateway_system_config_sync.go index 9e7b4774..87068d3c 100644 --- a/cmd/gateway_system_config_sync.go +++ b/cmd/gateway_system_config_sync.go @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index f8f6fb32..47811969 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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) diff --git a/internal/config/config_system.go b/internal/config/config_system.go index 17048e04..8027a9ea 100644 --- a/internal/config/config_system.go +++ b/internal/config/config_system.go @@ -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) diff --git a/internal/memory/embeddings.go b/internal/memory/embeddings.go index 5ce0f3a5..d98e47b0 100644 --- a/internal/memory/embeddings.go +++ b/internal/memory/embeddings.go @@ -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 { diff --git a/internal/store/pg/memory_docs.go b/internal/store/pg/memory_docs.go index 0626537e..7394376b 100644 --- a/internal/store/pg/memory_docs.go +++ b/internal/store/pg/memory_docs.go @@ -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) { diff --git a/ui/web/src/components/layout/system-settings-modal.tsx b/ui/web/src/components/layout/system-settings-modal.tsx index c8085b53..f9700817 100644 --- a/ui/web/src/components/layout/system-settings-modal.tsx +++ b/ui/web/src/components/layout/system-settings-modal.tsx @@ -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 = {}; 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 )} + +
+
+ + setEmbMaxChunkLen(e.target.value)} className="text-base md:text-sm" /> +

{t("embedding.maxChunkLenHint")}

+
+
+ + setEmbChunkOverlap(e.target.value)} className="text-base md:text-sm" /> +

{t("embedding.chunkOverlapHint")}

+
+
diff --git a/ui/web/src/i18n/locales/en/agents.json b/ui/web/src/i18n/locales/en/agents.json index 563f2692..4730d9f9 100644 --- a/ui/web/src/i18n/locales/en/agents.json +++ b/ui/web/src/i18n/locales/en/agents.json @@ -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", diff --git a/ui/web/src/i18n/locales/en/config.json b/ui/web/src/i18n/locales/en/config.json index 589c818d..fb510c0c 100644 --- a/ui/web/src/i18n/locales/en/config.json +++ b/ui/web/src/i18n/locales/en/config.json @@ -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.", diff --git a/ui/web/src/i18n/locales/en/system-settings.json b/ui/web/src/i18n/locales/en/system-settings.json index 1c67de95..0b56c5bd 100644 --- a/ui/web/src/i18n/locales/en/system-settings.json +++ b/ui/web/src/i18n/locales/en/system-settings.json @@ -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", diff --git a/ui/web/src/i18n/locales/vi/agents.json b/ui/web/src/i18n/locales/vi/agents.json index a297b6de..f66de0cf 100644 --- a/ui/web/src/i18n/locales/vi/agents.json +++ b/ui/web/src/i18n/locales/vi/agents.json @@ -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", diff --git a/ui/web/src/i18n/locales/vi/config.json b/ui/web/src/i18n/locales/vi/config.json index 084b2d3d..3a4b82e8 100644 --- a/ui/web/src/i18n/locales/vi/config.json +++ b/ui/web/src/i18n/locales/vi/config.json @@ -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ả.", diff --git a/ui/web/src/i18n/locales/vi/system-settings.json b/ui/web/src/i18n/locales/vi/system-settings.json index 98963901..078b015f 100644 --- a/ui/web/src/i18n/locales/vi/system-settings.json +++ b/ui/web/src/i18n/locales/vi/system-settings.json @@ -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", diff --git a/ui/web/src/i18n/locales/zh/agents.json b/ui/web/src/i18n/locales/zh/agents.json index 9940dfea..9d62826b 100644 --- a/ui/web/src/i18n/locales/zh/agents.json +++ b/ui/web/src/i18n/locales/zh/agents.json @@ -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": "扩展思考", diff --git a/ui/web/src/i18n/locales/zh/config.json b/ui/web/src/i18n/locales/zh/config.json index 442778b1..fb8da047 100644 --- a/ui/web/src/i18n/locales/zh/config.json +++ b/ui/web/src/i18n/locales/zh/config.json @@ -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)。", diff --git a/ui/web/src/i18n/locales/zh/system-settings.json b/ui/web/src/i18n/locales/zh/system-settings.json index b077e75a..398ad2a1 100644 --- a/ui/web/src/i18n/locales/zh/system-settings.json +++ b/ui/web/src/i18n/locales/zh/system-settings.json @@ -21,7 +21,11 @@ "verifyRequired": "保存前请先验证Embedding模型。", "verifyFailed": "验证失败", "dimensions": "{{count}}维", - "dimensionsMismatch": "{{count}}维 — 不符合要求的1536" + "dimensionsMismatch": "{{count}}维 — 不符合要求的1536", + "maxChunkLen": "最大块长度", + "maxChunkLenHint": "每个记忆块分割前的最大字符数。", + "chunkOverlap": "块重叠", + "chunkOverlapHint": "连续块之间的重叠字符数,用于保持上下文连续性。" }, "ux": { "title": "UX行为", diff --git a/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx index 041a3856..0b4acf42 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx @@ -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(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) */} -
- - { setMemEnabled(v); if (!v) setMem({}); }} - onChange={setMem} - /> -
+ {/* Memory — always visible, per-agent overrides */} + diff --git a/ui/web/src/pages/agents/agent-detail/config-sections/memory-section.tsx b/ui/web/src/pages/agents/agent-detail/config-sections/memory-section.tsx index 9979252a..c8487f72 100644 --- a/ui/web/src/pages/agents/agent-detail/config-sections/memory-section.tsx +++ b/ui/web/src/pages/agents/agent-detail/config-sections/memory-section.tsx @@ -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 ( - -
- onChange({ ...value, enabled: v })} - /> - {t("configSections.contextPruning.enabled")} +
+
+

{t(`${s}.title`)}

+

{t(`${s}.description`)}

- 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 - /> -
-
- {t(`${s}.maxResults`)} - onChange({ ...value, max_results: numOrUndef(e.target.value) })} +
+
+ onChange({ ...value, enabled: v })} /> + {t(`${s}.enabled`)}
-
- {t(`${s}.maxChunkLen`)} - onChange({ ...value, max_chunk_len: numOrUndef(e.target.value) })} - /> +
+
+ {t(`${s}.maxChunkLen`)} + onChange({ ...value, max_chunk_len: numOrUndef(e.target.value) })} + className="text-base md:text-sm" + /> +
+
+ {t(`${s}.chunkOverlap`)} + onChange({ ...value, chunk_overlap: numOrUndef(e.target.value) })} + className="text-base md:text-sm" + /> +
+
+
+
+ {t(`${s}.maxResults`)} + onChange({ ...value, max_results: numOrUndef(e.target.value) })} + className="text-base md:text-sm" + /> +
+
+ {t(`${s}.minScore`)} + onChange({ ...value, min_score: numOrUndef(e.target.value) })} + className="text-base md:text-sm" + /> +
+
+
+
+ {t(`${s}.vectorWeight`)} + onChange({ ...value, vector_weight: numOrUndef(e.target.value) })} + className="text-base md:text-sm" + /> +
+
+ {t(`${s}.textWeight`)} + onChange({ ...value, text_weight: numOrUndef(e.target.value) })} + className="text-base md:text-sm" + /> +
-
-
- {t(`${s}.vectorWeight`)} - onChange({ ...value, vector_weight: numOrUndef(e.target.value) })} - /> -
-
- {t(`${s}.textWeight`)} - onChange({ ...value, text_weight: numOrUndef(e.target.value) })} - /> -
-
- +
); } diff --git a/ui/web/src/pages/config/sections/ai-defaults-section.tsx b/ui/web/src/pages/config/sections/ai-defaults-section.tsx index 0ab80eeb..e60e80d9 100644 --- a/ui/web/src/pages/config/sections/ai-defaults-section.tsx +++ b/ui/web/src/pages/config/sections/ai-defaults-section.tsx @@ -180,6 +180,10 @@ export function AiDefaultsSection({ data, onSave, saving }: Props) { updateNested("memory", { max_results: Number(v) })} placeholder="6" /> updateNested("memory", { min_score: Number(v) })} placeholder="0.35" />
+
+ updateNested("memory", { max_chunk_len: Number(v) })} placeholder="1000" /> + updateNested("memory", { chunk_overlap: Number(v) })} placeholder="200" /> +