source-kb 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +50 -0
- cli/__main__.py +5 -0
- cli/commands/__init__.py +1 -0
- cli/commands/anchor_fix.py +47 -0
- cli/commands/diff_doc.py +52 -0
- cli/commands/dispatch.py +77 -0
- cli/commands/extract.py +72 -0
- cli/commands/file_list.py +74 -0
- cli/commands/index.py +84 -0
- cli/commands/lock.py +89 -0
- cli/commands/merge.py +60 -0
- cli/commands/merge_delta.py +19 -0
- cli/commands/metadata.py +24 -0
- cli/commands/pipeline.py +45 -0
- cli/commands/post_merge.py +43 -0
- cli/commands/query.py +52 -0
- cli/commands/render.py +101 -0
- cli/commands/scan_repos.py +46 -0
- cli/commands/setup.py +94 -0
- cli/commands/split.py +196 -0
- cli/commands/stale_files.py +98 -0
- cli/commands/validate.py +191 -0
- core/__init__.py +32 -0
- core/config.py +261 -0
- core/docs/__init__.py +7 -0
- core/docs/section_updater.py +286 -0
- core/docs/shared.py +149 -0
- core/git.py +294 -0
- core/interfaces.py +249 -0
- core/monitor/__init__.py +5 -0
- core/monitor/progress.py +83 -0
- core/monitor/prompt_store.py +49 -0
- core/paths.py +141 -0
- core/preset.py +237 -0
- core/preset_accessors.py +202 -0
- core/preset_classify.py +132 -0
- core/preset_hooks.py +129 -0
- core/preset_profile.py +89 -0
- core/prompt/__init__.py +7 -0
- core/prompt/__main__.py +147 -0
- core/prompt/content.py +320 -0
- core/prompt/context_manager.py +164 -0
- core/prompt/renderer.py +236 -0
- core/prompt/response_parser.py +274 -0
- core/prompt/templates.py +357 -0
- core/prompt/validate_parity.py +162 -0
- core/prompt/variables.py +339 -0
- core/rag/__init__.py +22 -0
- core/rag/__main__.py +136 -0
- core/rag/bm25_index.py +268 -0
- core/rag/chunker.py +273 -0
- core/rag/embedder.py +151 -0
- core/rag/indexer.py +292 -0
- core/rag/loader.py +89 -0
- core/rag/retriever.py +82 -0
- core/skeleton/__init__.py +11 -0
- core/skeleton/__main__.py +934 -0
- core/skeleton/anchor_fix.py +250 -0
- core/skeleton/classify.py +331 -0
- core/skeleton/cmd_anchor_fix.py +43 -0
- core/skeleton/cmd_diff_doc.py +44 -0
- core/skeleton/cmd_lock.py +87 -0
- core/skeleton/cmd_merge_delta.py +41 -0
- core/skeleton/community.py +233 -0
- core/skeleton/dependency_graph.py +306 -0
- core/skeleton/diff_doc.py +248 -0
- core/skeleton/dispatch.py +273 -0
- core/skeleton/dispatch_render.py +319 -0
- core/skeleton/dispatch_source.py +111 -0
- core/skeleton/extract.py +218 -0
- core/skeleton/extract_methods.py +298 -0
- core/skeleton/file_list.py +239 -0
- core/skeleton/impact.py +278 -0
- core/skeleton/jar_download.py +177 -0
- core/skeleton/jar_resolver.py +186 -0
- core/skeleton/loader.py +162 -0
- core/skeleton/merge.py +278 -0
- core/skeleton/merge_delta.py +229 -0
- core/skeleton/metadata.py +96 -0
- core/skeleton/metadata_builders.py +264 -0
- core/skeleton/module_dag.py +330 -0
- core/skeleton/parsers/__init__.py +71 -0
- core/skeleton/parsers/jqassistant.py +300 -0
- core/skeleton/parsers/jqassistant_cypher.py +225 -0
- core/skeleton/parsers/regex.py +171 -0
- core/skeleton/parsers/treesitter.py +324 -0
- core/skeleton/parsers/treesitter_java.py +284 -0
- core/skeleton/parsers/treesitter_multi.py +289 -0
- core/skeleton/pom_parser.py +299 -0
- core/skeleton/post_merge.py +295 -0
- core/skeleton/post_merge_llm.py +82 -0
- core/skeleton/query.py +195 -0
- core/skeleton/shard_context.py +177 -0
- core/skeleton/split.py +180 -0
- core/skeleton/split_cache.py +107 -0
- core/skeleton/split_feedback.py +174 -0
- core/skeleton/split_plan.py +219 -0
- core/skeleton/split_plan_helpers.py +305 -0
- core/skeleton/split_plan_llm.py +274 -0
- core/utils.py +135 -0
- core/validators/__init__.py +65 -0
- core/validators/__main__.py +215 -0
- core/validators/consistency.py +203 -0
- core/validators/coverage.py +171 -0
- core/validators/duplicates.py +76 -0
- core/validators/engine.py +224 -0
- core/validators/links.py +76 -0
- core/validators/sampling.py +169 -0
- core/validators/structure.py +144 -0
- engine/__init__.py +7 -0
- engine/assembler.py +231 -0
- engine/confirm.py +65 -0
- engine/dedup.py +106 -0
- engine/main.py +211 -0
- engine/pipeline/__init__.py +163 -0
- engine/pipeline/recovery.py +250 -0
- engine/pipeline/steps/__init__.py +23 -0
- engine/pipeline/steps/audit.py +220 -0
- engine/pipeline/steps/audit_apply.py +195 -0
- engine/pipeline/steps/audit_helpers.py +155 -0
- engine/pipeline/steps/classify_llm.py +236 -0
- engine/pipeline/steps/classify_prompt.py +223 -0
- engine/pipeline/steps/finalize.py +160 -0
- engine/pipeline/steps/generate.py +169 -0
- engine/pipeline/steps/generate_batch.py +197 -0
- engine/pipeline/steps/generate_recovery.py +170 -0
- engine/pipeline/steps/llm_plan_split.py +253 -0
- engine/pipeline/steps/lock.py +64 -0
- engine/pipeline/steps/preflight.py +237 -0
- engine/pipeline/steps/preflight_adjust.py +147 -0
- engine/pipeline/steps/pregenerate.py +130 -0
- engine/pipeline/steps/quality.py +81 -0
- engine/pipeline/steps/skeleton.py +149 -0
- engine/pipeline/steps/source.py +163 -0
- engine/pipeline/steps/sync.py +117 -0
- engine/pipeline/steps/sync_finalize.py +237 -0
- engine/pipeline/steps/sync_update.py +341 -0
- engine/pipelines.py +91 -0
- engine/runner.py +335 -0
- engine/strategies/__init__.py +86 -0
- engine/strategies/api.py +128 -0
- engine/strategies/delegated.py +50 -0
- engine/strategies/dryrun.py +25 -0
- engine/two_phase.py +143 -0
- mcp_server/__init__.py +73 -0
- mcp_server/__main__.py +5 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/config.py +63 -0
- mcp_server/tools/discovery.py +276 -0
- mcp_server/tools/generation.py +184 -0
- mcp_server/tools/planning.py +144 -0
- mcp_server/tools/source.py +175 -0
- mcp_server/tools/validation.py +140 -0
- mcp_server/tools/workflow.py +166 -0
- mcp_server/workflow_loader.py +204 -0
- presets/generic/audit_dimensions.md +132 -0
- presets/generic/doc_types.yaml +152 -0
- presets/generic/preset.yaml +115 -0
- presets/java-spring/audit_dimensions.md +228 -0
- presets/java-spring/audit_dimensions.yaml +203 -0
- presets/java-spring/doc_types.yaml +269 -0
- presets/java-spring/hooks.py +122 -0
- presets/java-spring/preset.yaml +341 -0
- presets/java-spring/templates/README.md +34 -0
- presets/java-spring/templates/audit-system.md +15 -0
- presets/java-spring/templates/subagent-aop.md +105 -0
- presets/java-spring/templates/subagent-api.md +63 -0
- presets/java-spring/templates/subagent-architecture.md +111 -0
- presets/java-spring/templates/subagent-async-events.md +107 -0
- presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
- presets/java-spring/templates/subagent-audit-architecture.md +38 -0
- presets/java-spring/templates/subagent-audit-business.md +40 -0
- presets/java-spring/templates/subagent-audit-data-models.md +40 -0
- presets/java-spring/templates/subagent-business.md +129 -0
- presets/java-spring/templates/subagent-caching.md +75 -0
- presets/java-spring/templates/subagent-database-access.md +114 -0
- presets/java-spring/templates/subagent-enum.md +75 -0
- presets/java-spring/templates/subagent-error-handling.md +91 -0
- presets/java-spring/templates/subagent-external-integrations.md +80 -0
- presets/java-spring/templates/subagent-index.md +122 -0
- presets/java-spring/templates/subagent-messaging.md +97 -0
- presets/java-spring/templates/subagent-model.md +88 -0
- presets/java-spring/templates/subagent-observability.md +91 -0
- presets/java-spring/templates/subagent-scheduled.md +81 -0
- presets/java-spring/templates/subagent-security.md +102 -0
- presets/java-spring/templates/subagent-structure.md +101 -0
- presets/java-spring/templates/subagent-sync-section.md +34 -0
- presets/java-spring/templates/subagent-utils.md +73 -0
- presets/java-spring/templates/sync-system.md +8 -0
- presets/java-spring/workflow-extensions.md +112 -0
- skills/__init__.py +1 -0
- skills/_shared/README.md +30 -0
- skills/_shared/doc-coverage-shared.md +134 -0
- skills/_shared/doc-quality-standard.md +1058 -0
- skills/_shared/doc-subagent-rules.md +762 -0
- skills/_shared/windows-compat.md +89 -0
- skills/kb-audit/SKILL.md +52 -0
- skills/kb-audit/rules.md +88 -0
- skills/kb-audit/steps/step-01-prepare.md +75 -0
- skills/kb-audit/steps/step-02-audit.md +96 -0
- skills/kb-audit/steps/step-03-verify.md +65 -0
- skills/kb-audit/steps/step-04-report.md +64 -0
- skills/kb-init/SKILL.md +146 -0
- skills/kb-init/rules.md +187 -0
- skills/kb-init/steps/step-01-scope.md +62 -0
- skills/kb-init/steps/step-02-source.md +410 -0
- skills/kb-init/steps/step-03-generate.md +307 -0
- skills/kb-init/steps/step-04-quality.md +92 -0
- skills/kb-init/steps/step-05-finalize.md +132 -0
- skills/kb-init/templates/core/execution-modes.md +29 -0
- skills/kb-init/templates/core/output-only.md +4 -0
- skills/kb-init/templates/core/readwrite.md +33 -0
- skills/kb-search/SKILL.md +138 -0
- skills/kb-search/rules.md +64 -0
- skills/kb-sync/SKILL.md +43 -0
- skills/kb-sync/rules.md +70 -0
- skills/kb-sync/scripts/rebuild_module.py +91 -0
- skills/kb-sync/scripts/scan_repos.py +687 -0
- skills/kb-sync/steps/step-01-detect.md +72 -0
- skills/kb-sync/steps/step-02-update.md +71 -0
- skills/kb-sync/steps/step-03-verify.md +47 -0
- skills/kb-sync/steps/step-04-finalize.md +52 -0
- source_kb-0.2.2.dist-info/METADATA +194 -0
- source_kb-0.2.2.dist-info/RECORD +228 -0
- source_kb-0.2.2.dist-info/WHEEL +5 -0
- source_kb-0.2.2.dist-info/entry_points.txt +3 -0
- source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
- source_kb-0.2.2.dist-info/top_level.txt +6 -0
core/rag/indexer.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Index builder — writes chunks to ChromaDB vector database.
|
|
4
|
+
|
|
5
|
+
Migrated from engine/rag/indexer.py. Removes print() calls and global config;
|
|
6
|
+
accepts Config object explicitly. Supports checkpoint resume, chunk change
|
|
7
|
+
detection, batch embedding, and incremental rebuild.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import gc
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import random
|
|
15
|
+
import time
|
|
16
|
+
import traceback
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import chromadb
|
|
21
|
+
from filelock import FileLock
|
|
22
|
+
|
|
23
|
+
from core.rag.chunker import Chunk
|
|
24
|
+
from core.rag.embedder import get_embeddings
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from core.config import Config
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
BATCH_SIZE = 100
|
|
32
|
+
_PROGRESS_VERSION = 3
|
|
33
|
+
_DELETE_PAGE_SIZE = 5000
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_client(config: "Config", kb_name: str | None = None):
|
|
37
|
+
"""Get ChromaDB persistent client."""
|
|
38
|
+
chroma_path = config.chroma_dir(kb_name)
|
|
39
|
+
chroma_path.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
return chromadb.PersistentClient(path=str(chroma_path))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _progress_file(config: "Config", kb_name: str) -> Path:
|
|
44
|
+
"""Progress file path for checkpoint resume."""
|
|
45
|
+
return config.chroma_dir(kb_name) / "index_progress.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def embed_with_retry(
|
|
49
|
+
texts: list[str], config: "Config", max_retries: int = 3
|
|
50
|
+
) -> list[list[float]]:
|
|
51
|
+
"""Batch embedding with retry and jitter for transient failures."""
|
|
52
|
+
for attempt in range(max_retries):
|
|
53
|
+
try:
|
|
54
|
+
return get_embeddings(texts, config)
|
|
55
|
+
except RuntimeError:
|
|
56
|
+
raise
|
|
57
|
+
except Exception as e:
|
|
58
|
+
if attempt < max_retries - 1:
|
|
59
|
+
wait = 2 ** attempt + random.uniform(0, 1)
|
|
60
|
+
logger.warning("Embedding retry %d/%d: %s", attempt + 1, max_retries, e)
|
|
61
|
+
time.sleep(wait)
|
|
62
|
+
else:
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _embed_config_key(config: "Config") -> str:
|
|
67
|
+
"""Return a string identifying the current embedding configuration."""
|
|
68
|
+
return f"{config.embed_backend}:{config.embed_model}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _chunk_id(source: str, index: int) -> str:
|
|
72
|
+
"""Unified chunk ID: {source}_{index}."""
|
|
73
|
+
return f"{source}_{index}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _compute_chunks_hash(chunks: list[Chunk], config: "Config") -> str:
|
|
77
|
+
"""Compute content fingerprint for chunks."""
|
|
78
|
+
h = hashlib.sha256()
|
|
79
|
+
h.update(_embed_config_key(config).encode())
|
|
80
|
+
h.update(str(len(chunks)).encode())
|
|
81
|
+
for c in chunks:
|
|
82
|
+
h.update(c.text.encode("utf-8", errors="replace"))
|
|
83
|
+
return h.hexdigest()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _save_progress(config: "Config", kb_name: str, count: int, chunks_hash: str):
|
|
87
|
+
"""Save indexing progress checkpoint."""
|
|
88
|
+
pf = _progress_file(config, kb_name)
|
|
89
|
+
lock = FileLock(str(pf) + ".lock")
|
|
90
|
+
with lock:
|
|
91
|
+
pf.write_text(json.dumps({
|
|
92
|
+
"version": _PROGRESS_VERSION,
|
|
93
|
+
"kb": kb_name, "count": count,
|
|
94
|
+
"chunks_hash": chunks_hash, "ts": time.time(),
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _load_progress(config: "Config", kb_name: str, chunks_hash: str) -> int:
|
|
99
|
+
"""Load progress; returns checkpoint count if hash and version match."""
|
|
100
|
+
try:
|
|
101
|
+
pf = _progress_file(config, kb_name)
|
|
102
|
+
lock = FileLock(str(pf) + ".lock")
|
|
103
|
+
with lock:
|
|
104
|
+
data = json.loads(pf.read_text())
|
|
105
|
+
if (data.get("version") == _PROGRESS_VERSION
|
|
106
|
+
and data.get("kb") == kb_name
|
|
107
|
+
and data.get("chunks_hash") == chunks_hash):
|
|
108
|
+
return data.get("count", 0)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.debug("Failed to load progress for %s: %s", kb_name, e)
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _delete_chunks_by_source(collection, source: str) -> int:
|
|
115
|
+
"""Delete all chunks matching a specific source value (paginated)."""
|
|
116
|
+
deleted = 0
|
|
117
|
+
while True:
|
|
118
|
+
try:
|
|
119
|
+
results = collection.get(where={"source": source}, limit=_DELETE_PAGE_SIZE)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.debug("delete_chunks_by_source query failed for %s: %s", source, e)
|
|
122
|
+
break
|
|
123
|
+
if not results["ids"]:
|
|
124
|
+
break
|
|
125
|
+
collection.delete(ids=results["ids"])
|
|
126
|
+
deleted += len(results["ids"])
|
|
127
|
+
return deleted
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _delete_chunks_by_category(collection, category: str) -> int:
|
|
131
|
+
"""Delete all chunks matching a category (paginated), with prefix fallback."""
|
|
132
|
+
deleted = 0
|
|
133
|
+
try:
|
|
134
|
+
while True:
|
|
135
|
+
results = collection.get(where={"category": category}, limit=_DELETE_PAGE_SIZE)
|
|
136
|
+
if not results["ids"]:
|
|
137
|
+
break
|
|
138
|
+
collection.delete(ids=results["ids"])
|
|
139
|
+
deleted += len(results["ids"])
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.debug("delete_chunks_by_category where-query failed for %s, falling back to prefix scan: %s", category, e)
|
|
142
|
+
while True:
|
|
143
|
+
try:
|
|
144
|
+
all_results = collection.get(limit=_DELETE_PAGE_SIZE, include=["metadatas"])
|
|
145
|
+
except Exception as e2:
|
|
146
|
+
logger.debug("prefix scan query failed: %s", e2)
|
|
147
|
+
break
|
|
148
|
+
if not all_results["ids"]:
|
|
149
|
+
break
|
|
150
|
+
ids_to_delete = [
|
|
151
|
+
all_results["ids"][i]
|
|
152
|
+
for i, meta in enumerate(all_results["metadatas"])
|
|
153
|
+
if meta.get("source", "").startswith(f"{category}/")
|
|
154
|
+
or meta.get("source", "").startswith(f"{category}\\")
|
|
155
|
+
]
|
|
156
|
+
if not ids_to_delete:
|
|
157
|
+
break
|
|
158
|
+
collection.delete(ids=ids_to_delete)
|
|
159
|
+
deleted += len(ids_to_delete)
|
|
160
|
+
if len(all_results["ids"]) < _DELETE_PAGE_SIZE:
|
|
161
|
+
break
|
|
162
|
+
return deleted
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _insert_chunks(
|
|
166
|
+
collection, chunks: list[Chunk], config: "Config",
|
|
167
|
+
kb_name: str, start: int, chunks_hash: str,
|
|
168
|
+
) -> bool:
|
|
169
|
+
"""Embed and insert chunks into collection with checkpoint resume."""
|
|
170
|
+
total = len(chunks)
|
|
171
|
+
for i in range(start, total, BATCH_SIZE):
|
|
172
|
+
batch = chunks[i:i + BATCH_SIZE]
|
|
173
|
+
batch_end = min(i + BATCH_SIZE, total)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
texts = [c.text for c in batch]
|
|
177
|
+
embeddings = embed_with_retry(texts, config)
|
|
178
|
+
collection.add(
|
|
179
|
+
ids=[_chunk_id(c.metadata.get("source", "unknown"), j)
|
|
180
|
+
for j, c in enumerate(batch, start=i)],
|
|
181
|
+
embeddings=embeddings,
|
|
182
|
+
documents=texts,
|
|
183
|
+
metadatas=[c.metadata for c in batch],
|
|
184
|
+
)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error("[%s] batch %d-%d failed: %s", kb_name, i, batch_end, e)
|
|
187
|
+
logger.debug("Traceback:\n%s", traceback.format_exc())
|
|
188
|
+
_save_progress(config, kb_name, i, chunks_hash)
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
_save_progress(config, kb_name, batch_end, chunks_hash)
|
|
192
|
+
gc.collect()
|
|
193
|
+
logger.info("[%s] %d/%d", kb_name, batch_end, total)
|
|
194
|
+
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_index(
|
|
199
|
+
chunks: list[Chunk],
|
|
200
|
+
collection_name: str,
|
|
201
|
+
config: "Config",
|
|
202
|
+
kb_name: str = "unknown",
|
|
203
|
+
module: str | None = None,
|
|
204
|
+
files: list[str] | None = None,
|
|
205
|
+
):
|
|
206
|
+
"""Build ChromaDB index with unified chunk IDs.
|
|
207
|
+
|
|
208
|
+
Modes:
|
|
209
|
+
- module=None, files=None: full rebuild (delete entire collection)
|
|
210
|
+
- module="x": module-level rebuild (delete by category, then insert)
|
|
211
|
+
- module="x", files=["a.md"]: file-level rebuild (delete by source)
|
|
212
|
+
"""
|
|
213
|
+
total = len(chunks)
|
|
214
|
+
if total == 0:
|
|
215
|
+
logger.info("[%s] no chunks to index", kb_name)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
client = _get_client(config, kb_name)
|
|
219
|
+
|
|
220
|
+
if module:
|
|
221
|
+
_build_incremental(client, collection_name, chunks, config, kb_name, module, files)
|
|
222
|
+
else:
|
|
223
|
+
_build_full(client, collection_name, chunks, config, kb_name)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _build_incremental(client, collection_name, chunks, config, kb_name, module, files):
|
|
227
|
+
"""Incremental mode: get or create collection, delete targeted chunks."""
|
|
228
|
+
total = len(chunks)
|
|
229
|
+
try:
|
|
230
|
+
collection = client.get_collection(collection_name)
|
|
231
|
+
except Exception:
|
|
232
|
+
collection = client.get_or_create_collection(
|
|
233
|
+
name=collection_name, metadata={"hnsw:space": "cosine"})
|
|
234
|
+
|
|
235
|
+
if files:
|
|
236
|
+
deleted = 0
|
|
237
|
+
for fname in files:
|
|
238
|
+
source_posix = f"{module}/{fname}"
|
|
239
|
+
deleted += _delete_chunks_by_source(collection, source_posix)
|
|
240
|
+
logger.info("[%s] deleted %d old chunks for %d file(s)", kb_name, deleted, len(files))
|
|
241
|
+
else:
|
|
242
|
+
deleted = _delete_chunks_by_category(collection, module)
|
|
243
|
+
logger.info("[%s] deleted %d old chunks for module %s", kb_name, deleted, module)
|
|
244
|
+
|
|
245
|
+
chunks_hash = _compute_chunks_hash(chunks, config)
|
|
246
|
+
_insert_chunks(collection, chunks, config, kb_name, 0, chunks_hash)
|
|
247
|
+
logger.info("[%s] done! inserted %d chunks for %s", kb_name, total, module)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_full(client, collection_name, chunks, config, kb_name):
|
|
251
|
+
"""Full rebuild mode with checkpoint resume."""
|
|
252
|
+
total = len(chunks)
|
|
253
|
+
chunks_hash = _compute_chunks_hash(chunks, config)
|
|
254
|
+
saved = _load_progress(config, kb_name, chunks_hash)
|
|
255
|
+
|
|
256
|
+
if saved > 0:
|
|
257
|
+
logger.info("[%s] resuming from %d/%d", kb_name, saved, total)
|
|
258
|
+
collection = client.get_collection(collection_name)
|
|
259
|
+
start = saved
|
|
260
|
+
else:
|
|
261
|
+
logger.info("[%s] full rebuild...", kb_name)
|
|
262
|
+
try:
|
|
263
|
+
client.delete_collection(collection_name)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.debug("delete_collection(%s) skipped: %s", collection_name, e)
|
|
266
|
+
collection = client.create_collection(
|
|
267
|
+
name=collection_name, metadata={"hnsw:space": "cosine"})
|
|
268
|
+
start = 0
|
|
269
|
+
|
|
270
|
+
if start >= total:
|
|
271
|
+
logger.info("[%s] already complete: %d/%d", kb_name, start, total)
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
ok = _insert_chunks(collection, chunks, config, kb_name, start, chunks_hash)
|
|
275
|
+
if ok:
|
|
276
|
+
logger.info("[%s] done! %d/%d", kb_name, collection.count(), total)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_collection(collection_name: str, config: "Config", kb_name: str | None = None):
|
|
280
|
+
"""Get an existing collection.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
RuntimeError: If the collection does not exist.
|
|
284
|
+
"""
|
|
285
|
+
client = _get_client(config, kb_name)
|
|
286
|
+
try:
|
|
287
|
+
return client.get_collection(collection_name)
|
|
288
|
+
except Exception:
|
|
289
|
+
raise RuntimeError(
|
|
290
|
+
f"Collection '{collection_name}' not found. "
|
|
291
|
+
f"Run index build first."
|
|
292
|
+
)
|
core/rag/loader.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Document loader — loads markdown and source files from knowledge base directory.
|
|
4
|
+
|
|
5
|
+
Migrated from engine/rag/loader.py. Removes print() calls (no CLI I/O in core/).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Document:
|
|
17
|
+
"""A loaded document with content and metadata."""
|
|
18
|
+
content: str
|
|
19
|
+
metadata: dict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_documents(
|
|
23
|
+
knowledge_dir: str | Path,
|
|
24
|
+
module: str | None = None,
|
|
25
|
+
files: list[str] | None = None,
|
|
26
|
+
) -> list[Document]:
|
|
27
|
+
"""Recursively load .md files from a directory.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
knowledge_dir: Root knowledge directory.
|
|
31
|
+
module: If set, only load from knowledge_dir/module/.
|
|
32
|
+
files: If set (requires module), only load these filenames.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of Document objects with content and metadata.
|
|
36
|
+
"""
|
|
37
|
+
knowledge_dir = Path(knowledge_dir)
|
|
38
|
+
if not knowledge_dir.exists():
|
|
39
|
+
logger.warning("Directory does not exist: %s", knowledge_dir)
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
if module:
|
|
43
|
+
scan_dir = knowledge_dir / module
|
|
44
|
+
if not scan_dir.exists():
|
|
45
|
+
logger.warning("Module directory does not exist: %s", scan_dir)
|
|
46
|
+
return []
|
|
47
|
+
else:
|
|
48
|
+
scan_dir = knowledge_dir
|
|
49
|
+
|
|
50
|
+
docs: list[Document] = []
|
|
51
|
+
for path in sorted(scan_dir.rglob("*.md")):
|
|
52
|
+
if path.is_symlink():
|
|
53
|
+
continue
|
|
54
|
+
if ".meta" in path.parts:
|
|
55
|
+
continue
|
|
56
|
+
if files and path.name not in files:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
content = _read_file(path)
|
|
60
|
+
if not content:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
rel = path.relative_to(knowledge_dir)
|
|
64
|
+
category = rel.parts[0] if len(rel.parts) > 1 else "general"
|
|
65
|
+
|
|
66
|
+
docs.append(Document(
|
|
67
|
+
content=content,
|
|
68
|
+
metadata={
|
|
69
|
+
"source": rel.as_posix(),
|
|
70
|
+
"category": category,
|
|
71
|
+
},
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
logger.info("Loaded %d documents from %s", len(docs), scan_dir)
|
|
75
|
+
return docs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_file(path: Path) -> str | None:
|
|
79
|
+
"""Read a file, handling encoding automatically."""
|
|
80
|
+
for enc in ("utf-8", "gbk"):
|
|
81
|
+
try:
|
|
82
|
+
text = path.read_text(encoding=enc).strip()
|
|
83
|
+
if enc != "utf-8":
|
|
84
|
+
logger.warning("Used %s fallback encoding for: %s", enc, path.name)
|
|
85
|
+
return text if text else None
|
|
86
|
+
except (UnicodeDecodeError, UnicodeError):
|
|
87
|
+
continue
|
|
88
|
+
logger.error("Skipping undecodable file: %s", path.name)
|
|
89
|
+
return None
|
core/rag/retriever.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Retriever — semantic search from ChromaDB vector index.
|
|
4
|
+
|
|
5
|
+
Migrated from engine/rag/retriever.py. Removes global config dependency;
|
|
6
|
+
accepts Config object explicitly. Returns ranked results above similarity threshold.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from core.rag.embedder import get_embeddings
|
|
13
|
+
from core.rag.indexer import get_collection
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from core.config import Config
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def retrieve(
|
|
22
|
+
query: str,
|
|
23
|
+
config: "Config",
|
|
24
|
+
top_k: int | None = None,
|
|
25
|
+
collection_name: str | None = None,
|
|
26
|
+
kb_name: str | None = None,
|
|
27
|
+
min_score: float | None = None,
|
|
28
|
+
) -> list[dict]:
|
|
29
|
+
"""Retrieve chunks most relevant to the query.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
query: Query text for semantic search.
|
|
33
|
+
config: Config object providing retrieval settings.
|
|
34
|
+
top_k: Maximum number of results to return.
|
|
35
|
+
collection_name: ChromaDB collection name.
|
|
36
|
+
kb_name: Knowledge base name (used to locate ChromaDB).
|
|
37
|
+
min_score: Minimum similarity threshold (cosine similarity).
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of dicts with keys: text, metadata, score.
|
|
41
|
+
Sorted by score descending.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If collection_name cannot be determined.
|
|
45
|
+
RuntimeError: If the collection does not exist.
|
|
46
|
+
"""
|
|
47
|
+
if top_k is None:
|
|
48
|
+
top_k = config.top_k
|
|
49
|
+
if min_score is None:
|
|
50
|
+
min_score = config.similarity_threshold
|
|
51
|
+
|
|
52
|
+
if not collection_name:
|
|
53
|
+
kb_cfg = config.get_kb(kb_name) if kb_name else {}
|
|
54
|
+
collection_name = kb_cfg.get("collection") if isinstance(kb_cfg, dict) else None
|
|
55
|
+
if not collection_name:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"collection_name is required — either pass it directly or "
|
|
58
|
+
"configure 'collection' in the knowledge_base config"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
collection = get_collection(collection_name, config, kb_name=kb_name)
|
|
62
|
+
query_embedding = get_embeddings([query], config)[0]
|
|
63
|
+
|
|
64
|
+
results = collection.query(
|
|
65
|
+
query_embeddings=[query_embedding],
|
|
66
|
+
n_results=top_k,
|
|
67
|
+
include=["documents", "metadatas", "distances"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
chunks: list[dict] = []
|
|
71
|
+
for i in range(len(results["ids"][0])):
|
|
72
|
+
score = 1 - results["distances"][0][i] # cosine similarity
|
|
73
|
+
if score < min_score:
|
|
74
|
+
continue
|
|
75
|
+
chunks.append({
|
|
76
|
+
"text": results["documents"][0][i],
|
|
77
|
+
"metadata": results["metadatas"][0][i],
|
|
78
|
+
"score": score,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
logger.debug("Retrieved %d chunks (query=%r, top_k=%d)", len(chunks), query[:50], top_k)
|
|
82
|
+
return chunks
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""core.skeleton — skeleton extraction, file classification, split planning, and merge.
|
|
2
|
+
|
|
3
|
+
Sub-modules:
|
|
4
|
+
parsers/ — Language-specific skeleton parsers (registry pattern)
|
|
5
|
+
extract — Skeleton extraction orchestrator
|
|
6
|
+
file_list — File list extraction + coverage check
|
|
7
|
+
classify — Rule-based + LLM-assisted file classification
|
|
8
|
+
split — SplitConfig + compute_splits + plan_splits
|
|
9
|
+
metadata — Global metadata pre-generation
|
|
10
|
+
merge — Shard merge (fast) + post-merge refinement
|
|
11
|
+
"""
|