From d9c940fa5be981df2ea3345c438bfbbd277094e7 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sun, 28 Jun 2026 13:50:35 +0800 Subject: [PATCH 1/4] feat(memory): configure semantic embedding schema --- .../templates/configmap-agent-config.yaml | 15 ++++ coding-agent/helm/values.yaml | 11 +++ coding-agent/src/coding_agent/core/app.py | 83 +++++++++++++++++-- .../tests/coding_agent/test_memory_switch.py | 55 +++++++++++- coding-agent/tests/deploy/test_helm_chart.py | 33 ++++++++ 5 files changed, 190 insertions(+), 7 deletions(-) diff --git a/coding-agent/helm/templates/configmap-agent-config.yaml b/coding-agent/helm/templates/configmap-agent-config.yaml index 68dd85ea4..a476cf552 100644 --- a/coding-agent/helm/templates/configmap-agent-config.yaml +++ b/coding-agent/helm/templates/configmap-agent-config.yaml @@ -54,6 +54,21 @@ data: local = {{ printf "%s/local.sqlite3" .Values.persistence.data.mountPath | quote }} docs = {{ printf "%s/docs" .Values.persistence.data.mountPath | quote }} + {{- if .Values.memory.semantic.enabled }} + [memory.semantic] + enabled = true + backend = {{ .Values.memory.semantic.backend | quote }} + db_path = {{ printf "%s/%s" (.Values.persistence.data.mountPath | trimSuffix "/") (.Values.memory.semantic.dbPath | trimPrefix "/") | quote }} + table_name = {{ .Values.memory.semantic.tableName | quote }} + embedding_provider_id = {{ .Values.memory.semantic.embeddingProviderId | quote }} + embedding_model = {{ .Values.memory.semantic.embeddingModel | quote }} + embedding_dim = {{ .Values.memory.semantic.embeddingDim }} + {{- if .Values.memory.semantic.embeddingBaseUrl }} + embedding_base_url = {{ .Values.memory.semantic.embeddingBaseUrl | quote }} + {{- end }} + + {{- end }} + {{- if .Values.kb.enabled }} [kb] db_path = {{ printf "%s/%s" (.Values.persistence.data.mountPath | trimSuffix "/") (.Values.kb.dbPath | trimPrefix "/") | quote }} diff --git a/coding-agent/helm/values.yaml b/coding-agent/helm/values.yaml index 0ed3d8256..c9c553464 100644 --- a/coding-agent/helm/values.yaml +++ b/coding-agent/helm/values.yaml @@ -109,6 +109,17 @@ agent: memoryLimitMb: null dockerImage: python:3.11-slim +memory: + semantic: + enabled: false + backend: lancedb + dbPath: semantic-memory + tableName: semantic_memory + embeddingProviderId: openai + embeddingModel: text-embedding-3-small + embeddingDim: 1536 + embeddingBaseUrl: null + kb: enabled: false dbPath: kb diff --git a/coding-agent/src/coding_agent/core/app.py b/coding-agent/src/coding_agent/core/app.py index 0979ccbd9..0acbc7b3e 100644 --- a/coding-agent/src/coding_agent/core/app.py +++ b/coding-agent/src/coding_agent/core/app.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Mapping from contextlib import contextmanager -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from pathlib import Path from typing import Any @@ -78,6 +78,8 @@ def to_config_dict(self) -> dict[str, object]: payload["table_name"] = self.table_name if self.embedding_base_url is not None: payload["embedding_base_url"] = self.embedding_base_url + if schema_payload := _semantic_schema_config_payload(self.backend, self.schema): + payload.update(schema_payload) return payload @@ -231,20 +233,89 @@ def _parse_semantic_memory_config( embedding_base_url = raw_semantic_cfg.get("embedding_base_url") if embedding_base_url is not None and not isinstance(embedding_base_url, str): raise ValueError("[memory.semantic].embedding_base_url must be a string") + schema = _semantic_schema_from_config(backend, raw_semantic_cfg) return SemanticMemoryConfig( enabled=enabled, backend=backend, - schema=( - _default_semantic_schema(backend) - if backend in semantic_backend_registry.available_semantic_memory_backends() - else FAKE_SEMANTIC_INDEX_SCHEMA - ), + schema=schema, db_path=db_path, table_name=table_name, embedding_base_url=embedding_base_url, ) +def _semantic_schema_from_config( + backend: str, + raw_semantic_cfg: Mapping[str, Any], +) -> SemanticIndexSchema: + embedding_provider_id = _optional_semantic_string( + raw_semantic_cfg, + "embedding_provider_id", + ) + embedding_model = _optional_semantic_string( + raw_semantic_cfg, + "embedding_model", + ) + embedding_dim = _optional_semantic_positive_int( + raw_semantic_cfg, + "embedding_dim", + ) + if backend not in semantic_backend_registry.available_semantic_memory_backends(): + return FAKE_SEMANTIC_INDEX_SCHEMA + default_schema = _default_semantic_schema(backend) + return replace( + default_schema, + embedding_provider_id=embedding_provider_id + or default_schema.embedding_provider_id, + embedding_model=embedding_model or default_schema.embedding_model, + embedding_dim=embedding_dim or default_schema.embedding_dim, + ) + + +def _semantic_schema_config_payload( + backend: str, + schema: SemanticIndexSchema, +) -> dict[str, object]: + if backend not in semantic_backend_registry.available_semantic_memory_backends(): + return {} + default_schema = _default_semantic_schema(backend) + if ( + schema.embedding_provider_id == default_schema.embedding_provider_id + and schema.embedding_model == default_schema.embedding_model + and schema.embedding_dim == default_schema.embedding_dim + ): + return {} + return { + "embedding_provider_id": schema.embedding_provider_id, + "embedding_model": schema.embedding_model, + "embedding_dim": schema.embedding_dim, + } + + +def _optional_semantic_string( + raw_semantic_cfg: Mapping[str, Any], + key: str, +) -> str | None: + value = raw_semantic_cfg.get(key) + if value is None: + return None + if not isinstance(value, str): + raise ValueError(f"[memory.semantic].{key} must be a string") + return value + + +def _optional_semantic_positive_int( + raw_semantic_cfg: Mapping[str, Any], + key: str, +) -> int | None: + value = raw_semantic_cfg.get(key) + if value is None: + return None + if type(value) is not int or value <= 0: + raise ValueError(f"[memory.semantic].{key} must be a positive integer") + return value + + def _default_semantic_schema(backend: str) -> SemanticIndexSchema: if backend == "fake": return FAKE_SEMANTIC_INDEX_SCHEMA diff --git a/coding-agent/tests/coding_agent/test_memory_switch.py b/coding-agent/tests/coding_agent/test_memory_switch.py index 7f285c4f0..33ef8255a 100644 --- a/coding-agent/tests/coding_agent/test_memory_switch.py +++ b/coding-agent/tests/coding_agent/test_memory_switch.py @@ -797,9 +797,18 @@ def create_backend( db_path: object, table_name: object, embedding_base_url: object, + embedding_fn: object | None = None, ) -> FakeSemanticMemoryBackend: calls.append( - (backend, schema, data_dir, db_path, table_name, embedding_base_url) + ( + backend, + schema, + data_dir, + db_path, + table_name, + embedding_base_url, + embedding_fn, + ) ) return RecordingBackend(schema=schema) @@ -823,6 +832,7 @@ def create_backend( None, "semantic_memory", None, + None, ) ] assert isinstance(ctx.config["semantic_memory_backend"], RecordingBackend) @@ -870,6 +880,49 @@ def test_semantic_enabled_lancedb_backend_uses_configured_local_path( assert "semantic_memory" in pipeline._registry.plugin_ids() +def test_semantic_enabled_lancedb_backend_uses_configured_embedding_schema( + tmp_path: Path, +) -> None: + config_path = _default_plugin_config( + tmp_path, + memory=""" + +[memory] +read_enabled = true +write_enabled = true + +[memory.semantic] +enabled = true +backend = "lancedb" +db_path = "semantic-memory" +embedding_provider_id = "siliconflow" +embedding_model = "BAAI/bge-m3" +embedding_dim = 1024 +embedding_base_url = "https://api.siliconflow.cn/v1" +""", + ) + + _pipeline, ctx = create_agent( + config_path=config_path, + data_dir=tmp_path / "data", + api_key="sk-test", + ) + + backend = ctx.config["semantic_memory_backend"] + assert backend.schema.embedding_provider_id == "siliconflow" + assert backend.schema.embedding_model == "BAAI/bge-m3" + assert backend.schema.embedding_dim == 1024 + assert ctx.config["memory"]["semantic"] == { + "enabled": True, + "backend": "lancedb", + "db_path": "semantic-memory", + "embedding_base_url": "https://api.siliconflow.cn/v1", + "embedding_provider_id": "siliconflow", + "embedding_model": "BAAI/bge-m3", + "embedding_dim": 1024, + } + + def test_semantic_enabled_forwards_explicit_topic_dependencies_to_plugin( tmp_path: Path, ) -> None: diff --git a/coding-agent/tests/deploy/test_helm_chart.py b/coding-agent/tests/deploy/test_helm_chart.py index 289895538..087f67ae4 100644 --- a/coding-agent/tests/deploy/test_helm_chart.py +++ b/coding-agent/tests/deploy/test_helm_chart.py @@ -197,6 +197,39 @@ def test_helm_default_config_does_not_render_or_enable_memory_semantic() -> None assert "[memory.semantic]" not in _agent_toml_text(docs) +def test_helm_semantic_memory_enabled_renders_config() -> None: + docs = _render( + "--set", + "memory.semantic.enabled=true", + "--set", + "memory.semantic.backend=lancedb", + "--set", + "memory.semantic.dbPath=semantic-memory", + "--set", + "memory.semantic.tableName=semantic_docs", + "--set", + "memory.semantic.embeddingProviderId=siliconflow", + "--set", + "memory.semantic.embeddingModel=BAAI/bge-m3", + "--set", + "memory.semantic.embeddingDim=1024", + "--set", + "memory.semantic.embeddingBaseUrl=https://api.siliconflow.cn/v1", + ) + config = _agent_toml(docs) + + assert config["memory"]["semantic"] == { + "enabled": True, + "backend": "lancedb", + "db_path": "/var/lib/coding-agent/data/semantic-memory", + "table_name": "semantic_docs", + "embedding_provider_id": "siliconflow", + "embedding_model": "BAAI/bge-m3", + "embedding_dim": 1024, + "embedding_base_url": "https://api.siliconflow.cn/v1", + } + + def test_helm_agent_config_checksum_annotation_changes_with_config( tmp_path: Path, ) -> None: From 8182e51adac451b44d9ac549359d9ea768a07484 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sun, 28 Jun 2026 15:45:53 +0800 Subject: [PATCH 2/4] fix(helm): enable semantic memory plugin --- .../templates/configmap-agent-config.yaml | 3 ++ coding-agent/tests/deploy/test_helm_chart.py | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/coding-agent/helm/templates/configmap-agent-config.yaml b/coding-agent/helm/templates/configmap-agent-config.yaml index a476cf552..9f6e7ffd3 100644 --- a/coding-agent/helm/templates/configmap-agent-config.yaml +++ b/coding-agent/helm/templates/configmap-agent-config.yaml @@ -38,6 +38,9 @@ data: "session_metrics", "shell_session", "skills", + {{- if .Values.memory.semantic.enabled }} + "semantic_memory", + {{- end }} {{- if .Values.kb.enabled }} "kb", {{- end }} diff --git a/coding-agent/tests/deploy/test_helm_chart.py b/coding-agent/tests/deploy/test_helm_chart.py index 087f67ae4..77d160792 100644 --- a/coding-agent/tests/deploy/test_helm_chart.py +++ b/coding-agent/tests/deploy/test_helm_chart.py @@ -218,6 +218,7 @@ def test_helm_semantic_memory_enabled_renders_config() -> None: ) config = _agent_toml(docs) + assert "semantic_memory" in config["agent"]["plugins"]["enabled"] assert config["memory"]["semantic"] == { "enabled": True, "backend": "lancedb", @@ -230,6 +231,44 @@ def test_helm_semantic_memory_enabled_renders_config() -> None: } +def test_helm_semantic_memory_enabled_bootstraps_runtime(tmp_path: Path) -> None: + data_mount = tmp_path / "data" + docs = _render( + "--set", + f"persistence.data.mountPath={data_mount}", + "--set", + "memory.semantic.enabled=true", + "--set", + "memory.semantic.backend=lancedb", + "--set", + "memory.semantic.dbPath=semantic-memory", + "--set", + "memory.semantic.embeddingProviderId=siliconflow", + "--set", + "memory.semantic.embeddingModel=BAAI/bge-m3", + "--set", + "memory.semantic.embeddingDim=1024", + "--set", + "memory.semantic.embeddingBaseUrl=https://api.siliconflow.cn/v1", + ) + config_path = tmp_path / "agent.toml" + config_path.write_text(_agent_toml_text(docs), encoding="utf-8") + + pipeline, ctx = create_agent( + config_path=config_path, + data_dir=tmp_path / "unused-data-dir", + workspace_root=tmp_path / "workspace", + api_key="sk-test", + ) + + backend = ctx.config["semantic_memory_backend"] + assert backend.schema.embedding_provider_id == "siliconflow" + assert backend.schema.embedding_model == "BAAI/bge-m3" + assert backend.schema.embedding_dim == 1024 + assert backend.db_path == data_mount / "semantic-memory" + assert "semantic_memory" in pipeline._registry.plugin_ids() + + def test_helm_agent_config_checksum_annotation_changes_with_config( tmp_path: Path, ) -> None: From 3c249f6a0dcc659bedc42131719541ded40bea72 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sun, 28 Jun 2026 15:49:13 +0800 Subject: [PATCH 3/4] fix(memory): reject blank semantic schema overrides --- coding-agent/src/coding_agent/core/app.py | 19 ++++++++++--- .../tests/coding_agent/test_memory_switch.py | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/coding-agent/src/coding_agent/core/app.py b/coding-agent/src/coding_agent/core/app.py index 0acbc7b3e..9ca495b29 100644 --- a/coding-agent/src/coding_agent/core/app.py +++ b/coding-agent/src/coding_agent/core/app.py @@ -265,10 +265,19 @@ def _semantic_schema_from_config( default_schema = _default_semantic_schema(backend) return replace( default_schema, - embedding_provider_id=embedding_provider_id - or default_schema.embedding_provider_id, - embedding_model=embedding_model or default_schema.embedding_model, - embedding_dim=embedding_dim or default_schema.embedding_dim, + embedding_provider_id=( + embedding_provider_id + if embedding_provider_id is not None + else default_schema.embedding_provider_id + ), + embedding_model=( + embedding_model + if embedding_model is not None + else default_schema.embedding_model + ), + embedding_dim=( + embedding_dim if embedding_dim is not None else default_schema.embedding_dim + ), ) @@ -301,6 +310,8 @@ def _optional_semantic_string( return None if not isinstance(value, str): raise ValueError(f"[memory.semantic].{key} must be a string") + if not value: + raise ValueError(f"[memory.semantic].{key} must be non-empty") return value diff --git a/coding-agent/tests/coding_agent/test_memory_switch.py b/coding-agent/tests/coding_agent/test_memory_switch.py index 33ef8255a..066369a09 100644 --- a/coding-agent/tests/coding_agent/test_memory_switch.py +++ b/coding-agent/tests/coding_agent/test_memory_switch.py @@ -923,6 +923,33 @@ def test_semantic_enabled_lancedb_backend_uses_configured_embedding_schema( } +def test_semantic_enabled_rejects_empty_embedding_schema_override( + tmp_path: Path, +) -> None: + config_path = _default_plugin_config( + tmp_path, + memory=""" + +[memory] + +[memory.semantic] +enabled = true +backend = "lancedb" +embedding_model = "" +""", + ) + + with pytest.raises( + ValueError, + match=r"\[memory\.semantic\]\.embedding_model must be non-empty", + ): + create_agent( + config_path=config_path, + data_dir=tmp_path / "data", + api_key="sk-test", + ) + + def test_semantic_enabled_forwards_explicit_topic_dependencies_to_plugin( tmp_path: Path, ) -> None: From 46f9391ce49eee86426b3d13935341e78c64e7c1 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sun, 28 Jun 2026 16:14:24 +0800 Subject: [PATCH 4/4] fix(memory): tighten semantic schema config output --- coding-agent/src/coding_agent/core/app.py | 21 +++---- .../tests/coding_agent/test_memory_switch.py | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/coding-agent/src/coding_agent/core/app.py b/coding-agent/src/coding_agent/core/app.py index 9ca495b29..d3f1c2f87 100644 --- a/coding-agent/src/coding_agent/core/app.py +++ b/coding-agent/src/coding_agent/core/app.py @@ -288,17 +288,14 @@ def _semantic_schema_config_payload( if backend not in semantic_backend_registry.available_semantic_memory_backends(): return {} default_schema = _default_semantic_schema(backend) - if ( - schema.embedding_provider_id == default_schema.embedding_provider_id - and schema.embedding_model == default_schema.embedding_model - and schema.embedding_dim == default_schema.embedding_dim - ): - return {} - return { - "embedding_provider_id": schema.embedding_provider_id, - "embedding_model": schema.embedding_model, - "embedding_dim": schema.embedding_dim, - } + payload: dict[str, object] = {} + if schema.embedding_provider_id != default_schema.embedding_provider_id: + payload["embedding_provider_id"] = schema.embedding_provider_id + if schema.embedding_model != default_schema.embedding_model: + payload["embedding_model"] = schema.embedding_model + if schema.embedding_dim != default_schema.embedding_dim: + payload["embedding_dim"] = schema.embedding_dim + return payload def _optional_semantic_string( @@ -310,7 +307,7 @@ def _optional_semantic_string( return None if not isinstance(value, str): raise ValueError(f"[memory.semantic].{key} must be a string") - if not value: + if not value.strip(): raise ValueError(f"[memory.semantic].{key} must be non-empty") return value diff --git a/coding-agent/tests/coding_agent/test_memory_switch.py b/coding-agent/tests/coding_agent/test_memory_switch.py index 066369a09..624d9b480 100644 --- a/coding-agent/tests/coding_agent/test_memory_switch.py +++ b/coding-agent/tests/coding_agent/test_memory_switch.py @@ -923,6 +923,35 @@ def test_semantic_enabled_lancedb_backend_uses_configured_embedding_schema( } +def test_semantic_config_dict_emits_only_changed_embedding_schema_overrides( + tmp_path: Path, +) -> None: + config_path = _default_plugin_config( + tmp_path, + memory=""" + +[memory] + +[memory.semantic] +enabled = true +backend = "lancedb" +embedding_dim = 1024 +""", + ) + + _pipeline, ctx = create_agent( + config_path=config_path, + data_dir=tmp_path / "data", + api_key="sk-test", + ) + + assert ctx.config["memory"]["semantic"] == { + "enabled": True, + "backend": "lancedb", + "embedding_dim": 1024, + } + + def test_semantic_enabled_rejects_empty_embedding_schema_override( tmp_path: Path, ) -> None: @@ -950,6 +979,33 @@ def test_semantic_enabled_rejects_empty_embedding_schema_override( ) +def test_semantic_enabled_rejects_whitespace_embedding_schema_override( + tmp_path: Path, +) -> None: + config_path = _default_plugin_config( + tmp_path, + memory=""" + +[memory] + +[memory.semantic] +enabled = true +backend = "lancedb" +embedding_model = " " +""", + ) + + with pytest.raises( + ValueError, + match=r"\[memory\.semantic\]\.embedding_model must be non-empty", + ): + create_agent( + config_path=config_path, + data_dir=tmp_path / "data", + api_key="sk-test", + ) + + def test_semantic_enabled_forwards_explicit_topic_dependencies_to_plugin( tmp_path: Path, ) -> None: