* refactor(memory): rename package second-brain to memory * refactor(memory): use OpenAI env naming and fix trailing slash * feat(memory): add chunking with env-configurable size and overlap * feat(memory): dedup search results by source in top-K * test(memory): add chunking, batching, dedup, live tests * docs(memory): update README and project map after rename * docs(handoff): add handoff and ADR-032 for memory refactor * docs(handoff): set PR number * fix(ci): reduce index.py complexity to rank A --------- Co-authored-by: opencode-agent <agent@opencode.local>
9.5 KiB
9.5 KiB
| pr | title |
|---|---|
| 75 | Rename to memory, OpenAI env, chunking, batching, dedup |
Что сделано
pyproject.toml:name = "second-brain"→name = "memory", добавлена секция[project.scripts] rag = "src.memory.cli:main".uv.lockрегенерирован (uv lock).src/memory/cli.py:prog="rag"→prog="memory".src/memory/embedder.py: env неймингAI_PROVIDER_API_URL/AI_PROVIDER_API_KEY→OPENAI_BASE_URL/OPENAI_API_KEY(OpenAI-совместимый).API_URL.rstrip("/")фиксит trailing-slash баг (/v1//embeddings→ 404).EMBEDDING_MODELиз envOPENAI_EMBEDDING_MODEL(defaultgemini-embedding-2-preview).BATCH_SIZE = int(os.environ.get("OPENAI_EMBEDDING_BATCH_SIZE", "2048"))—embed_textsрежет texts > BATCH_SIZE на батчи, конкатенирует результаты.src/memory/index.py: добавлена_chunk_text(text, size, overlap) -> list[tuple[str, int]](graceful при size<=overlap → 1 chunk).run_indexиспользует чанки (MEMORY_CHUNK_SIZEdefault 512,MEMORY_CHUNK_OVERLAPdefault 64 из env).file_mapполя:source,chunk_idx,offset,text. Пустые файлы → 1 запись с пустым чанком (не дропаются).src/memory/search.py: дедуп поsourceв top-K — после sort по score, итерация сseenset, оставляет highest score per source, останавливается на K уникальных..env.example: добавлен блок "OpenAI Embeddings (Memory CLI)" с 6 переменными (OPENAI_BASE_URL,OPENAI_API_KEY,OPENAI_EMBEDDING_MODEL,OPENAI_EMBEDDING_BATCH_SIZE,MEMORY_CHUNK_SIZE,MEMORY_CHUNK_OVERLAP).AI_PROVIDER_BASE_URL/AI_PROVIDER_API_KEYоставлены (LLM провайдер в opencode.json, отдельный concern).tests/test_embedder.py: env нейминг (OPENAI_BASE_URL), + 4 теста:test_embed_texts_default_model,test_embed_texts_custom_model(reload с env override),test_embed_texts_trailing_slash(URL без//),test_embed_texts_batches(3000 texts → 2 вызова [2048, 952]).tests/test_index.py: + 3 теста:test_chunking_long_file(1500 chars → 3+ records, chunk_idx/offset),test_chunking_size_env_override(MEMORY_CHUNK_SIZE=256),test_chunking_skip_rag_dir. Существующийtest_index_creates_jsonобновлён (проверкаchunk_idx/offset).tests/test_search.py: +test_dedup_by_source(2 чанка одного source → 1 в top-K, highest score).tests/test_chunking.py(новый): 7 edge cases_chunk_text— empty, 1 char, ровно size, size+1, unicode emoji, size<overlap, size==overlap.tests/test_embedder_live.py(новый): 2 live-теста (@pytest.mark.skipif(not RUN_LIVE)) — single + batch embed.README.md:40,docs/project-map/README.md:69:second-brain→memory. Project-map обновлён:embedder.py→OPENAI_BASE_URL,index.py→ chunking,search.py→ dedup,cli.py→prog="memory", + entries дляtest_embedder_live.py,test_chunking.py.- ADR-032 + этот handoff
Почему
7 проблем в src/memory/ (RAG CLI для opencode-memory plugin), чинятся этим PR:
- Нейминг-путаница:
pyproject.tomlname=second-brain, модульsrc/memory/, CLIprog="rag"— три разных имени. Унифицировано подmemory(пакет) +ragscript entry point (совместимость с setup-memory.sh wrapper). - Env-нейминг нестандартен: код читал
AI_PROVIDER_API_URL, но.env.example/opencode.jsonиспользуютAI_PROVIDER_BASE_URL— код не работал "из коробки". Перешли на OpenAI-стандартOPENAI_BASE_URL/OPENAI_API_KEY(подтверждено openai-python docs). - Trailing-slash бага:
f"{API_URL}/embeddings"без.rstrip("/")→ приOPENAI_BASE_URL=https://api.openai.com/v1/URL становился/v1//embeddings→ HTTP 404 (воспроизведено smoke-test'ом). - Индекс 1-эмбеддинг-на-файл: 1 embedding на весь файл — нет чанков. Для 74 файлов (1.2 MB) грубо, не находит фрагменты внутри длинных файлов. Добавлено чанкирование (default 512 chars, overlap 64).
- Нет batching: все texts одним POST — при 3396 чанках риск упереться в OpenAI лимит (2048 input для text-embedding-3-small). Добавлен batching (default 2048).
- Нет дедупа в search: 2 чанка одного файла в top-K засоряли выдачу одним файлом. Добавлен дедуп по source (highest score per source).
- Модель захардкожена:
EMBEDDING_MODEL = "gemini-embedding-2-preview"— нельзя сменить без правки кода. Вынесена в envOPENAI_EMBEDDING_MODEL.
PR #1 в серии из 2 (PR #2 — интеграция в плагин через setup-memory.sh wrapper).
Pending
- PR #2: интеграция
memoryCLI в плагин@mathew-cf/opencode-memoryчерезsetup-memory.sh(замена Rust rag-cli на Pythonmemory index/memory search). Требует: OPENAI_BASE_URL/OPENAI_API_KEY в.env, обновление wrapper в setup-memory.sh. opencode.json:28всё ещё используетAI_PROVIDER_API_KEYдля LLM провайдера — отдельный concern (LLM, не embeddings), НЕ трогался в этом PR. Возможный future PR для OpenAI-стандартизации LLM env.- Smoke-test (3396 чанков, 1.2 MB index.json) — требует реальный API key, не запускался в CI (только unit-тесты с моками). Запустить вручную перед merge:
OPENAI_BASE_URL=$AI_PROVIDER_BASE_URL OPENAI_API_KEY=$AI_PROVIDER_API_KEY uv run python -m src.memory index /root/.local/share/opencode/opencode-memory -o /tmp/.rag-test && ls /tmp/.rag-test/index.json
Watch out
EMBEDDING_MODEL/BATCH_SIZE— module-level, читаются при импорте. Тесты с env override (custom model, trailing slash) делаютimportlib.reload(embedder_mod)и восстанавливают env после теста. Если未来的 код сделаетEMBEDDING_MODELmutable через функцию — паттерн reload сломается. Альтернатива (читать env вembed_textsкаждый вызов) отклонена: лишний overhead на каждый вызов + несовместимость с существующимAPI_URLmodule-level паттерном.- Reload-тесты меняют module global —
test_embed_texts_custom_model/test_embed_texts_trailing_slashиспользуютmonkeypatch.setenv+importlib.reload.monkeypatchauto-restore env, но reload модуля НЕ auto-restore. Явныйreloadв конце каждого теста восстанавливает default state. Если тест упадёт ДО финального reload — следующий тест может получить stale module state. Mitigation:monkeypatch.setenvвосстанавливает env, а финальныйreloadвfinally-стиле (после assert) — нет. Если тест упал на assert — reload не выполнится. Риск минимальный (assert в конце), но для robustness можно обернуть в try/finally — НЕ сделано для простоты. - Chunking
size<=overlap→ 1 chunk (не infinite loop)._chunk_textвозвращает[(text, 0)]еслиsize <= overlap(step = size-overlap <= 0 →range(0, len, 0)= infinite). Это graceful degradation, не ошибка. Тестtest_size_less_than_overlap/test_size_equals_overlapпокрывают. file_mapтипdict[str, str | int]—chunk_idx/offsetэтоint,source/textэтоstr. mypy strict требует union. Существующийdict[str, str]расширен доdict[str, str | int]. JSON serialization сохраняет int как int (не string).- Empty files → 1 запись с пустым чанком.
_chunk_text("")возвращает[], ноrun_indexзаменяет на[("", 0)]чтобы не дропать файл из индекса (поиск по empty embedding даст score 0, но файл остаётся в index для completeness). Альтернатива (дропать empty files) отклонена — меняет существующее поведение (раньше empty files попадали в индекс с 1 embedding). uv lockобязателен после renamepyproject.toml—uv.lock:716содержалname = "second-brain". Безuv locklockfile рассинхронизирован с pyproject. CIuv syncиспользует lockfile → несоответствие.uv lockрегенерирован (confirmed:Removed second-brain v0.1.0,Added memory v0.1.0).- ADR number = sequential (032), НЕ PR number. Проверить ADR naming в handoff до push (эволюция паттерна PR#26 docs-reviewer typo).