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:
viettranx committed 2026-03-25 18:24:36 +07:00
1 parent da98a54393
commit 258e378593
20 files changed
+275 -106

No files matched your search

+6
View File
@@ -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))
}
})
+2
View File
@@ -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
+1
View File
@@ -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)
+2
View File
@@ -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)
+48 -3
View File
@@ -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 {
+35 -1
View File
@@ -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>
+12 -4
View File
@@ -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",
+4
View File
@@ -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",
+12 -4
View File
@@ -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",
+4
View File
@@ -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",
+12 -4
View File
@@ -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": "扩展思考",
+4
View File
@@ -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
+2
View File
@@ -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 {