Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions flocks/config/config_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ def ensure_config_files() -> None:
"error": str(e),
})

ConfigWriter.ensure_memory_config()


class ConfigWriter:
"""Atomic read-modify-write operations on the provider section of flocks.json."""
Expand Down Expand Up @@ -106,9 +108,13 @@ def _read_raw(cls) -> Dict[str, Any]:
return {}

@classmethod
def _write_raw(cls, data: Dict[str, Any]) -> None:
def _write_raw(
cls,
data: Dict[str, Any],
path: Optional[Path] = None,
) -> None:
"""Atomic write: write to tmp file then rename, then clear Config cache."""
path = cls._get_config_path()
path = path or cls._get_config_path()
path.parent.mkdir(parents=True, exist_ok=True)

# Atomic write via temp file in same directory
Expand Down Expand Up @@ -136,6 +142,62 @@ def _write_raw(cls, data: Dict[str, Any]) -> None:

log.debug("config_writer.written", {"path": str(path)})

@classmethod
def ensure_memory_config(cls) -> bool:
"""Persist the editable Memory Search config when absent."""
path = Config.get_config_file()
try:
text = path.read_text(encoding="utf-8") if path.exists() else ""
data = json.loads(text) if text.strip() else {}
except (json.JSONDecodeError, OSError) as exc:
log.error(
"config_writer.memory_config_init_failed",
{"path": str(path), "error": str(exc)},
)
return False

if not isinstance(data, dict):
log.error(
"config_writer.memory_config_init_failed",
{"path": str(path), "error": "top-level config must be an object"},
)
return False
if "memory" in data:
return False

from flocks.memory.config import MemoryConfig

default_config = MemoryConfig()
data["memory"] = {
"search": {
"embedding": default_config.search.embedding.model_dump(
mode="json",
exclude_none=True,
),
},
}
cls._write_raw(data, path=path)
log.info("config_writer.memory_config_initialized", {"path": str(path)})
return True

@classmethod
def enable_memory_source(cls, source: str) -> bool:
"""Persist a Memory source without rewriting unrelated config."""
data = cls._read_raw()
memory = data.get("memory")
if not isinstance(memory, dict):
memory = {}
sources = memory.get("sources")
if not isinstance(sources, list):
sources = ["memory"]
if source in sources:
return False
memory["sources"] = [*sources, source]
data["memory"] = memory
cls._write_raw(data)
log.info("config_writer.memory_source_enabled", {"source": source})
return True

# ------------------------------------------------------------------
# Provider-level CRUD
# ------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion flocks/hooks/builtin/slug_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async def generate_slug_via_llm(
"""

# Get provider configuration
provider_id = getattr(config.memory.embedding, 'provider', 'openai')
provider_id = getattr(config.memory.search.embedding, 'provider', 'openai')
if provider_id == "auto":
provider_id = "openai"

Expand Down
2 changes: 2 additions & 0 deletions flocks/memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from flocks.memory.config import (
MemoryConfig,
MemoryEmbeddingConfig,
MemorySearchConfig,
MemoryChunkingConfig,
MemorySyncConfig,
MemoryQueryConfig,
Expand Down Expand Up @@ -68,6 +69,7 @@
# Config
"MemoryConfig",
"MemoryEmbeddingConfig",
"MemorySearchConfig",
"MemoryChunkingConfig",
"MemorySyncConfig",
"MemoryQueryConfig",
Expand Down
33 changes: 27 additions & 6 deletions flocks/memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

class MemoryEmbeddingConfig(BaseModel):
"""Embedding provider configuration"""
enabled: bool = Field(
False,
description="Enable vector embeddings for Memory search",
)
provider: Literal["auto", "openai", "google", "local"] = Field(
"auto",
description="Embedding provider (auto=try openai then google)"
Expand All @@ -32,6 +36,15 @@ class MemoryEmbeddingConfig(BaseModel):
)


class MemorySearchConfig(BaseModel):
"""Memory search configuration."""

embedding: MemoryEmbeddingConfig = Field(
default_factory=MemoryEmbeddingConfig,
description="Embedding configuration",
)


class MemoryChunkingConfig(BaseModel):
"""Text chunking configuration"""
tokens: int = Field(
Expand All @@ -52,7 +65,7 @@ class MemorySyncSessionConfig(BaseModel):
)
delta_messages: int = Field(
50,
description="Number of new messages to trigger sync"
description="Batch size for session transcript reconciliation"
)


Expand All @@ -64,7 +77,7 @@ class MemorySyncConfig(BaseModel):
)
on_search: bool = Field(
True,
description="Run incremental sync before every search"
description="Run incremental filesystem reconciliation before every search"
)
watch: bool = Field(
True,
Expand Down Expand Up @@ -295,9 +308,9 @@ class MemoryConfig(BaseModel):
)

# Sub-configurations
embedding: MemoryEmbeddingConfig = Field(
default_factory=MemoryEmbeddingConfig,
description="Embedding configuration"
search: MemorySearchConfig = Field(
default_factory=MemorySearchConfig,
description="Memory search configuration",
)
chunking: MemoryChunkingConfig = Field(
default_factory=MemoryChunkingConfig,
Expand Down Expand Up @@ -330,7 +343,15 @@ class MemoryConfig(BaseModel):


def resolve_memory_config(app_config: object) -> MemoryConfig:
"""Resolve runtime Memory config, using defaults when absent."""
"""Resolve runtime Memory config, using defaults when absent.

Args:
app_config: Loaded application configuration.

Returns:
Configured Memory settings, or defaults when the application has no
Memory section.
"""
memory_config = getattr(app_config, "memory", None)
if isinstance(memory_config, MemoryConfig):
return memory_config
Expand Down
Loading