From 9d48a30fd6ef1ef2fbccdb6a3b0fd0e88bb4f32e Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Sat, 16 May 2026 22:52:07 +0800 Subject: [PATCH] up --- docs4/todo.md | 7 +++- reme4/components/base_component.py | 7 ++++ .../embedding/base_embedding_model.py | 2 +- .../components/file_graph/base_file_graph.py | 2 +- .../components/file_store/base_file_store.py | 2 +- .../file_watcher/base_file_watcher.py | 2 +- .../file_watcher/lite_file_watcher.py | 4 +-- .../keyword_index/base_keyword_index.py | 2 +- reme4/config/config_parser.py | 8 +++++ reme4/config/default.yaml | 15 ++++---- reme4/reme.py | 3 ++ reme4/schema/application_config.py | 1 + reme4/utils/__init__.py | 2 ++ reme4/utils/env_utils.py | 36 +++++++++++++++++++ 14 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 reme4/utils/env_utils.py diff --git a/docs4/todo.md b/docs4/todo.md index ef3a474b..eca1816c 100644 --- a/docs4/todo.md +++ b/docs4/todo.md @@ -1,2 +1,7 @@ 1. 完善mcp_servers config -2. 完善mcp/http的服务测试 \ No newline at end of file +2. 完善mcp/http的服务测试 +3. [PosixPath('.reme')] +4. error +5. meta信息存在一个地方 +6. 测试一个完整的Service client的框架,测试各种命令 +7. config 默认改成default \ No newline at end of file diff --git a/reme4/components/base_component.py b/reme4/components/base_component.py index afbcb86f..06e289e3 100644 --- a/reme4/components/base_component.py +++ b/reme4/components/base_component.py @@ -127,6 +127,13 @@ class BaseComponent(ABC): return Path.cwd() return Path(self.app_context.app_config.working_dir) + @property + def working_metadata_path(self) -> Path: + """Resolved metadata directory: working_path / metadata_dir, or absolute metadata_dir.""" + if self.app_context is None: + return Path.cwd() / "metadata" + return self.working_path / self.app_context.app_config.metadata_dir + # ----- Lifecycle ----------------------------------------------------- async def _start(self) -> None: diff --git a/reme4/components/embedding/base_embedding_model.py b/reme4/components/embedding/base_embedding_model.py index 2c4efb2c..48aca850 100644 --- a/reme4/components/embedding/base_embedding_model.py +++ b/reme4/components/embedding/base_embedding_model.py @@ -49,7 +49,7 @@ class BaseEmbeddingModel(BaseComponent): @property def cache_path(self) -> Path: """Disk path for the embedding cache file.""" - return self.working_path / "embedding_cache" / f"{self.name}.npz" + return self.working_metadata_path / "embedding_cache" / f"{self.name}.npz" async def _start(self) -> None: """Load cache from disk on startup.""" diff --git a/reme4/components/file_graph/base_file_graph.py b/reme4/components/file_graph/base_file_graph.py index 6d04404f..6844c7f5 100644 --- a/reme4/components/file_graph/base_file_graph.py +++ b/reme4/components/file_graph/base_file_graph.py @@ -16,7 +16,7 @@ class BaseFileGraph(BaseComponent): def __init__(self, graph_name: str = "default", **kwargs): super().__init__(**kwargs) self.graph_name: str = graph_name or self.name - self.graph_path: Path = self.working_path / self.component_type.value + self.graph_path: Path = self.working_metadata_path / self.component_type.value self.graph_path.mkdir(parents=True, exist_ok=True) # -- Node CRUD --------------------------------------------------------- diff --git a/reme4/components/file_store/base_file_store.py b/reme4/components/file_store/base_file_store.py index 7e140535..768e26bb 100644 --- a/reme4/components/file_store/base_file_store.py +++ b/reme4/components/file_store/base_file_store.py @@ -35,7 +35,7 @@ class BaseFileStore(BaseComponent): self.embedding_model = self.bind(embedding_model, BaseEmbeddingModel, default_factory=OpenAIEmbeddingModel) self.keyword_index = self.bind(keyword_index, BaseKeywordIndex, default_factory=BM25Index) self.file_graph = self.bind(file_graph, BaseFileGraph, default_factory=LocalFileGraph) - self.store_path = self.working_path / self.component_type.value / store_name + self.store_path = self.working_metadata_path / self.component_type.value / store_name self.store_path.mkdir(parents=True, exist_ok=True) async def upsert_file( diff --git a/reme4/components/file_watcher/base_file_watcher.py b/reme4/components/file_watcher/base_file_watcher.py index dfee23d7..b246537c 100644 --- a/reme4/components/file_watcher/base_file_watcher.py +++ b/reme4/components/file_watcher/base_file_watcher.py @@ -50,7 +50,7 @@ class BaseFileWatcher(BaseComponent): async def _start(self): self._stop_event = asyncio.Event() self._background_task = asyncio.create_task(self._background_run()) - self.logger.info(f"Started watching: {self.watch_paths}") + self.logger.info(f"Started watching: {[str(p) for p in self.watch_paths]}") async def _background_run(self): """Sync store then enter watch loop.""" diff --git a/reme4/components/file_watcher/lite_file_watcher.py b/reme4/components/file_watcher/lite_file_watcher.py index 681ec0ce..e1fea327 100644 --- a/reme4/components/file_watcher/lite_file_watcher.py +++ b/reme4/components/file_watcher/lite_file_watcher.py @@ -34,10 +34,10 @@ class LiteFileWatcher(BaseFileWatcher): invalid = set(self.watch_paths) - set(valid_paths) if invalid: - self.logger.warning(f"Skipping invalid paths: {invalid}") + self.logger.warning(f"Skipping invalid paths: {[str(p) for p in invalid]}") try: - self.logger.info(f"Watching: {valid_paths}") + self.logger.info(f"Watching: {[str(p) for p in valid_paths]}") async for changes in awatch( *valid_paths, watch_filter=self.watch_filter, diff --git a/reme4/components/keyword_index/base_keyword_index.py b/reme4/components/keyword_index/base_keyword_index.py index 05b63231..2791bf83 100644 --- a/reme4/components/keyword_index/base_keyword_index.py +++ b/reme4/components/keyword_index/base_keyword_index.py @@ -18,7 +18,7 @@ class BaseKeywordIndex(BaseComponent): from ..tokenizer import RegexTokenizer self.tokenizer = self.bind(tokenizer, BaseTokenizer, default_factory=RegexTokenizer) - self.index_path = self.working_path / self.component_type.value + self.index_path = self.working_metadata_path / self.component_type.value self.index_path.mkdir(parents=True, exist_ok=True) async def _start(self) -> None: diff --git a/reme4/config/config_parser.py b/reme4/config/config_parser.py index 3410608c..128ffe80 100644 --- a/reme4/config/config_parser.py +++ b/reme4/config/config_parser.py @@ -182,6 +182,7 @@ def parse_args(*args, **kwargs) -> tuple[str, dict]: action = first configs: list[dict] = [] + explicit_config = False for raw in args[1:]: arg = _strip_arg_dashes(raw) @@ -189,9 +190,16 @@ def parse_args(*args, **kwargs) -> tuple[str, dict]: path = arg.split("=", 1)[1].strip() if path: configs.append(_load_config(path)) + explicit_config = True elif "=" in arg: configs.append(parse_dot_notation([arg])) + if not explicit_config and "default" in _CONFIG_REGISTRY: + from ..utils import get_logger + + get_logger().info("No config specified, using 'default'") + configs.insert(0, _load_config("default")) + configs.append(kwargs) merged: dict = {} diff --git a/reme4/config/default.yaml b/reme4/config/default.yaml index e511c4bb..59a938ee 100644 --- a/reme4/config/default.yaml +++ b/reme4/config/default.yaml @@ -23,44 +23,45 @@ jobs: - backend: demo_echo_step2 components: - # 1. tokenizer — 无依赖 + # 1. tokenizer — no dependencies tokenizer: default: backend: regex - # 2. embedding_model — 无依赖 + # 2. embedding_model — no dependencies embedding_model: default: backend: openai model_name: text-embedding-3-small dimensions: 1536 - # 3. file_graph — 无依赖 + # 3. file_graph — no dependencies file_graph: default: backend: local - # 4. file_parser — 无依赖 + # 4. file_parser — no dependencies file_parser: default: backend: default - # 5. keyword_index — 依赖 tokenizer + # 5. keyword_index — depends on tokenizer keyword_index: default: backend: bm25 tokenizer: default - # 6. file_store — 依赖 embedding_model / keyword_index / file_graph + # 6. file_store — depends on embedding_model / keyword_index / file_graph file_store: default: backend: local store_name: default embedding_model: default +# embedding_model: "" keyword_index: default file_graph: default - # 7. file_watcher — 依赖 file_store / file_parser + # 7. file_watcher — depends on file_store / file_parser file_watcher: default: backend: lite diff --git a/reme4/reme.py b/reme4/reme.py index 9dc5b3f7..cd23f0c5 100644 --- a/reme4/reme.py +++ b/reme4/reme.py @@ -7,6 +7,9 @@ from .application import Application from .components import R from .config import parse_args from .enumeration import ComponentEnum +from .utils import load_env + +load_env() class ReMe(Application): diff --git a/reme4/schema/application_config.py b/reme4/schema/application_config.py index a6cdf018..c296a83b 100644 --- a/reme4/schema/application_config.py +++ b/reme4/schema/application_config.py @@ -29,6 +29,7 @@ class ApplicationConfig(BaseModel): app_name: str = Field(default=os.getenv("APP_NAME", "ReMe"), description="Application display name") working_dir: str = Field(default=".reme", description="Working directory for runtime files") + metadata_dir: str = Field(default="reme_metadata",description="Subdirectory for ReMe persistent state") enable_logo: bool = Field(default=True, description="Show ASCII logo on startup") language: str = Field(default="", description="Default language for LLM interactions") log_to_console: bool = Field(default=True, description="Log to console") diff --git a/reme4/utils/__init__.py b/reme4/utils/__init__.py index cc43fe4e..c9a0e460 100644 --- a/reme4/utils/__init__.py +++ b/reme4/utils/__init__.py @@ -1,6 +1,7 @@ """Utility modules.""" from .common_utils import hash_text, execute_stream_task +from .env_utils import load_env from .logger_utils import get_logger from .logo_utils import print_logo from .similarity_utils import cosine_similarity, batch_cosine_similarity @@ -8,6 +9,7 @@ from .similarity_utils import cosine_similarity, batch_cosine_similarity __all__ = [ "hash_text", "execute_stream_task", + "load_env", "get_logger", "print_logo", "cosine_similarity", diff --git a/reme4/utils/env_utils.py b/reme4/utils/env_utils.py new file mode 100644 index 00000000..7c0aecbc --- /dev/null +++ b/reme4/utils/env_utils.py @@ -0,0 +1,36 @@ +"""Load .env files into os.environ (idempotent).""" + +import os +from pathlib import Path + +_LOADED = False + + +def _parse(path: Path) -> None: + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + os.environ[key.strip()] = value.strip().strip("'\"") + + +def load_env(path: str | Path | None = None) -> None: + """Load .env from given path, or search cwd and up to 5 parents.""" + global _LOADED + if _LOADED: + return + + if path: + path = Path(path) + if path.exists(): + _parse(path) + _LOADED = True + return + + for directory in [Path.cwd(), *Path.cwd().parents[:5]]: + env_path = directory / ".env" + if env_path.exists(): + _parse(env_path) + _LOADED = True + return