lorme 0.1.1__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.
- lorme/__init__.py +10 -0
- lorme/_version.py +24 -0
- lorme/bootstrap.yaml +89 -0
- lorme/build.py +334 -0
- lorme/build_config.py +58 -0
- lorme/checkpoint.py +228 -0
- lorme/collections.py +12 -0
- lorme/config.py +257 -0
- lorme/embedder.py +434 -0
- lorme/eval.py +1085 -0
- lorme/format_registry.py +206 -0
- lorme/ingest.py +307 -0
- lorme/lint.py +213 -0
- lorme/metadata.py +140 -0
- lorme/preprocess/__init__.py +1503 -0
- lorme/preprocess/clean.py +43 -0
- lorme/preprocess/dedup.py +120 -0
- lorme/preprocess/diarize.py +373 -0
- lorme/preprocess/enrich.py +338 -0
- lorme/preprocess/llm.py +122 -0
- lorme/preprocess/narrate.py +507 -0
- lorme/preprocess/parse.py +1367 -0
- lorme/preprocess/pii.py +47 -0
- lorme/preprocess/service.py +204 -0
- lorme/preprocess/validate.py +15 -0
- lorme/progress.py +281 -0
- lorme/prompts.yaml +15 -0
- lorme/recipe.py +246 -0
- lorme/server.py +1933 -0
- lorme/store.py +945 -0
- lorme/task_manager.py +237 -0
- lorme-0.1.1.dist-info/METADATA +314 -0
- lorme-0.1.1.dist-info/RECORD +36 -0
- lorme-0.1.1.dist-info/WHEEL +4 -0
- lorme-0.1.1.dist-info/entry_points.txt +2 -0
- lorme-0.1.1.dist-info/licenses/LICENSE +661 -0
lorme/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""lorme — Local Offline Retrieval & MCP Engine.
|
|
2
|
+
|
|
3
|
+
See docs/architecture.md for system design and docs/configuration.md
|
|
4
|
+
for environment variables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from lorme._version import __version__
|
|
9
|
+
except ImportError:
|
|
10
|
+
__version__ = "0.0.0.dev0+unknown"
|
lorme/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
lorme/bootstrap.yaml
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# lorme configuration
|
|
2
|
+
# Generated by: lorme init
|
|
3
|
+
# All values shown are defaults. Uncomment to override.
|
|
4
|
+
# Without this file, lorme works with XDG defaults.
|
|
5
|
+
# Docs: docs/configuration.md
|
|
6
|
+
|
|
7
|
+
# === Database ===
|
|
8
|
+
|
|
9
|
+
# Common: base directory for all collections
|
|
10
|
+
# database:
|
|
11
|
+
# dir: ~/.local/share/lorme/
|
|
12
|
+
# default_collection: default
|
|
13
|
+
|
|
14
|
+
# === Embedding ===
|
|
15
|
+
|
|
16
|
+
# Common: model name (references llm registry entry below)
|
|
17
|
+
# embedding:
|
|
18
|
+
# model: nomic-embed
|
|
19
|
+
|
|
20
|
+
# Advanced
|
|
21
|
+
# mode: api # api | builtin:gpu | builtin:cpu
|
|
22
|
+
|
|
23
|
+
# === Chunking ===
|
|
24
|
+
|
|
25
|
+
# Common
|
|
26
|
+
# chunking:
|
|
27
|
+
# size: 1024
|
|
28
|
+
|
|
29
|
+
# Advanced
|
|
30
|
+
# overlap: 128
|
|
31
|
+
|
|
32
|
+
# === Search ===
|
|
33
|
+
|
|
34
|
+
# Common
|
|
35
|
+
# search:
|
|
36
|
+
# top_k: 5
|
|
37
|
+
|
|
38
|
+
# Advanced
|
|
39
|
+
# reranking_model: "" # llm registry entry name, empty = no reranking
|
|
40
|
+
# mmr: false # Maximal Marginal Relevance for diversity
|
|
41
|
+
# window_size: 0 # adjacent chunk expansion, 0 = disabled
|
|
42
|
+
# max_per_source: 0 # max chunks per source, 0 = no limit
|
|
43
|
+
|
|
44
|
+
# === add_source defaults ===
|
|
45
|
+
|
|
46
|
+
# Common
|
|
47
|
+
# add_source:
|
|
48
|
+
# preprocess: true
|
|
49
|
+
|
|
50
|
+
# Advanced
|
|
51
|
+
# enrich: [] # [context, qa, meta]
|
|
52
|
+
# optimize: false
|
|
53
|
+
|
|
54
|
+
# === Enrichment ===
|
|
55
|
+
|
|
56
|
+
# Advanced
|
|
57
|
+
# enrich:
|
|
58
|
+
# techniques: [] # [context, qa, meta, stt_correction]
|
|
59
|
+
# models: [] # llm registry entry names
|
|
60
|
+
# prompts_file: "" # custom prompts.yaml path
|
|
61
|
+
|
|
62
|
+
# === Parse ===
|
|
63
|
+
|
|
64
|
+
# Advanced
|
|
65
|
+
# parse:
|
|
66
|
+
# ocr_engine: "" # tesseract | ""
|
|
67
|
+
# ocr_lang: [] # [eng, fra]
|
|
68
|
+
# stt_model: "" # llm registry entry name
|
|
69
|
+
# video_frame_strategy: scene # scene | fixed | hybrid | ocr
|
|
70
|
+
# frame_interval: 30 # seconds between frames (fixed/hybrid)
|
|
71
|
+
# scene_threshold: 0.3 # scene change sensitivity
|
|
72
|
+
|
|
73
|
+
# === LLM Registry ===
|
|
74
|
+
|
|
75
|
+
# Define inference services. Referenced by name in other sections.
|
|
76
|
+
# llm:
|
|
77
|
+
# - name: nomic-embed
|
|
78
|
+
# model: nomic-ai/nomic-embed-text-v2-moe
|
|
79
|
+
# api_url: http://127.0.0.1:8082/v1/embeddings
|
|
80
|
+
# resource: local_gpu # local_gpu | local_cpu | remote
|
|
81
|
+
#
|
|
82
|
+
# # Advanced service lifecycle (must be self-contained)
|
|
83
|
+
# # start: mkdir -p /path/to/logs; podman rm -f svc 2>/dev/null; podman run -d --name svc ...
|
|
84
|
+
# # stop: podman stop svc; podman rm -f svc
|
|
85
|
+
# # start_timeout: 120
|
|
86
|
+
# # timeout: 180
|
|
87
|
+
# # concurrency: 1
|
|
88
|
+
# # batch_size: 32
|
|
89
|
+
# # verify_ssl: true
|
lorme/build.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Build workflow: manifest + models → optimized .db + metadata. See docs/architecture.md."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from lorme.embedder import Embedder
|
|
8
|
+
from lorme.eval import run_optimize
|
|
9
|
+
from lorme.ingest import ingest_with_manifest
|
|
10
|
+
from lorme.recipe import parse_recipe
|
|
11
|
+
from lorme.metadata import generate_all
|
|
12
|
+
from lorme.preprocess import preprocess_sources
|
|
13
|
+
from lorme.store import ChunkStore
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
QUESTIONS_FILE = "questions.json"
|
|
18
|
+
SCORES_FILE = "scores.jsonl"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_models(
|
|
22
|
+
configs: list[dict],
|
|
23
|
+
embedders: dict | None = None,
|
|
24
|
+
) -> list[str]:
|
|
25
|
+
"""Validate all model configs before starting. Returns list of errors."""
|
|
26
|
+
from lorme.embedder import _probe_api
|
|
27
|
+
|
|
28
|
+
errors = []
|
|
29
|
+
for cfg in configs:
|
|
30
|
+
name = cfg["name"]
|
|
31
|
+
mode = cfg.get("mode", "builtin")
|
|
32
|
+
|
|
33
|
+
if mode == "api":
|
|
34
|
+
url = cfg.get("api_url")
|
|
35
|
+
if not url:
|
|
36
|
+
errors.append(f"{name}: mode=api but no api_url")
|
|
37
|
+
continue
|
|
38
|
+
if not _probe_api(url, cfg.get("api_model", name), verify=False):
|
|
39
|
+
errors.append(f"{name}: API endpoint unreachable ({url})")
|
|
40
|
+
elif embedders and name in embedders:
|
|
41
|
+
pass
|
|
42
|
+
else:
|
|
43
|
+
cache_path = Path.home() / ".cache" / "huggingface" / "hub" / f"models--{name.replace('/', '--')}"
|
|
44
|
+
if not cache_path.exists():
|
|
45
|
+
errors.append(f"{name}: not in HuggingFace cache, use --allow-download")
|
|
46
|
+
|
|
47
|
+
return errors
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _start_embedders(config) -> dict:
|
|
51
|
+
"""Create embedders from config, starting services if needed. See E12.68."""
|
|
52
|
+
from lorme.embedder import create_embedder
|
|
53
|
+
|
|
54
|
+
if config.embedding_models:
|
|
55
|
+
return {
|
|
56
|
+
emb_cfg["name"]: create_embedder(config, emb_cfg)
|
|
57
|
+
for emb_cfg in config.embedding_models
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
entry = config.get_embedding_entry()
|
|
61
|
+
if entry:
|
|
62
|
+
from lorme.preprocess.service import start_service
|
|
63
|
+
start_service(entry)
|
|
64
|
+
|
|
65
|
+
emb = create_embedder(config, entry)
|
|
66
|
+
return {emb.model_name: emb}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run_build(
|
|
70
|
+
recipe_path: str,
|
|
71
|
+
docs_dir: str,
|
|
72
|
+
output_dir: str,
|
|
73
|
+
config,
|
|
74
|
+
embedders: dict | None = None,
|
|
75
|
+
embedder: Embedder | None = None,
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""Full build pipeline: [preprocess →] validate → optimize → index → metadata.
|
|
78
|
+
|
|
79
|
+
All params resolved from config. embedders passed as instantiated objects.
|
|
80
|
+
"""
|
|
81
|
+
force = config.force
|
|
82
|
+
output_level = config.output_level
|
|
83
|
+
skip_optimize = config.skip_optimize
|
|
84
|
+
report_path = config.report_path or None
|
|
85
|
+
chunk_sizes = config.optimize_chunk_sizes
|
|
86
|
+
chunk_overlaps = config.optimize_chunk_overlaps
|
|
87
|
+
top_ks = config.optimize_top_ks
|
|
88
|
+
num_questions = config.optimize_num_questions
|
|
89
|
+
|
|
90
|
+
_build_dir = config.build_dir or output_dir
|
|
91
|
+
|
|
92
|
+
from lorme.task_manager import report_progress, check_cancelled
|
|
93
|
+
|
|
94
|
+
if config.preprocess:
|
|
95
|
+
prep_recipe_path = Path(_build_dir) / (
|
|
96
|
+
Path(recipe_path).stem + "-prep" + Path(recipe_path).suffix
|
|
97
|
+
)
|
|
98
|
+
config.preprocess_recipe_out = str(prep_recipe_path)
|
|
99
|
+
check_cancelled()
|
|
100
|
+
report_progress("Preprocessing sources")
|
|
101
|
+
preprocess_sources(recipe_path, docs_dir, config)
|
|
102
|
+
recipe_path = str(prep_recipe_path)
|
|
103
|
+
docs_dir = str(Path(_build_dir) / "prep")
|
|
104
|
+
|
|
105
|
+
# Use prep dir as docs_dir if it exists (previous preprocess run)
|
|
106
|
+
if not config.preprocess and _build_dir:
|
|
107
|
+
prep_dir = Path(_build_dir) / "prep"
|
|
108
|
+
prep_manifest = Path(_build_dir) / (
|
|
109
|
+
Path(recipe_path).stem + "-prep" + Path(recipe_path).suffix
|
|
110
|
+
)
|
|
111
|
+
if prep_dir.exists() and any(prep_dir.iterdir()):
|
|
112
|
+
docs_dir = str(prep_dir)
|
|
113
|
+
if prep_manifest.exists():
|
|
114
|
+
recipe_path = str(prep_manifest)
|
|
115
|
+
|
|
116
|
+
recipe = parse_recipe(recipe_path)
|
|
117
|
+
collection = getattr(config, "collection_override", "") or recipe["collection"]
|
|
118
|
+
output_path = Path(_build_dir)
|
|
119
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
if embedders is None and embedder is not None:
|
|
122
|
+
embedders = {embedder.model_name: embedder}
|
|
123
|
+
if not embedders:
|
|
124
|
+
embedders = _start_embedders(config)
|
|
125
|
+
|
|
126
|
+
work_path = output_path / ".work"
|
|
127
|
+
work_path.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
from lorme.progress import ProgressReporter
|
|
130
|
+
import time as _time
|
|
131
|
+
|
|
132
|
+
build_reporter = ProgressReporter(
|
|
133
|
+
collection=collection,
|
|
134
|
+
models=list(embedders.keys()),
|
|
135
|
+
total_configs=0,
|
|
136
|
+
level=output_level,
|
|
137
|
+
)
|
|
138
|
+
build_start = _time.time()
|
|
139
|
+
|
|
140
|
+
resumed = False
|
|
141
|
+
optimization = None
|
|
142
|
+
winning_model = next(iter(embedders))
|
|
143
|
+
if skip_optimize:
|
|
144
|
+
winning_chunk_size = config.chunk_size
|
|
145
|
+
winning_chunk_overlap = config.chunk_overlap
|
|
146
|
+
else:
|
|
147
|
+
winning_chunk_size = chunk_sizes[0] if chunk_sizes else 1024
|
|
148
|
+
winning_chunk_overlap = chunk_overlaps[0] if chunk_overlaps else 128
|
|
149
|
+
|
|
150
|
+
if not skip_optimize:
|
|
151
|
+
check_cancelled()
|
|
152
|
+
report_progress("Optimizing chunking parameters")
|
|
153
|
+
optimization = _run_optimization(
|
|
154
|
+
recipe_path=recipe_path,
|
|
155
|
+
docs_dir=docs_dir,
|
|
156
|
+
embedders=embedders,
|
|
157
|
+
work_dir=str(work_path),
|
|
158
|
+
force=force,
|
|
159
|
+
output_level=output_level,
|
|
160
|
+
config=config,
|
|
161
|
+
)
|
|
162
|
+
resumed = optimization.get("resumed", False)
|
|
163
|
+
best = optimization.get("best", {})
|
|
164
|
+
if best:
|
|
165
|
+
winning_model = best.get("model_name", winning_model)
|
|
166
|
+
winning_chunk_size = best.get("chunk_size", winning_chunk_size)
|
|
167
|
+
winning_chunk_overlap = best.get("chunk_overlap", winning_chunk_overlap)
|
|
168
|
+
|
|
169
|
+
for emb in embedders.values():
|
|
170
|
+
emb.unload()
|
|
171
|
+
|
|
172
|
+
build_reporter.print_section(
|
|
173
|
+
f"Final build — {winning_model} chunk={winning_chunk_size}/{winning_chunk_overlap}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
final_db = str(output_path / f"{collection}.db")
|
|
177
|
+
final_emb = embedders[winning_model]
|
|
178
|
+
|
|
179
|
+
t0 = _time.time()
|
|
180
|
+
cache_valid = False
|
|
181
|
+
if not force and Path(final_db).exists():
|
|
182
|
+
try:
|
|
183
|
+
_store = ChunkStore(final_db)
|
|
184
|
+
_meta = _store.get_meta()
|
|
185
|
+
_src = _store.list_sources()
|
|
186
|
+
_store.close()
|
|
187
|
+
cache_valid = (
|
|
188
|
+
len(_src) > 0
|
|
189
|
+
and _meta.get("chunk_size") == str(winning_chunk_size)
|
|
190
|
+
and _meta.get("chunk_overlap") == str(winning_chunk_overlap)
|
|
191
|
+
and _meta.get("model_name") == winning_model
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
cache_valid = False
|
|
195
|
+
|
|
196
|
+
if cache_valid:
|
|
197
|
+
build_reporter.print_step("Skipped (cached)")
|
|
198
|
+
report_progress("Indexing skipped (cached)")
|
|
199
|
+
else:
|
|
200
|
+
if Path(final_db).exists():
|
|
201
|
+
Path(final_db).unlink()
|
|
202
|
+
check_cancelled()
|
|
203
|
+
report_progress(f"Indexing {collection} ({winning_model})")
|
|
204
|
+
ingest_with_manifest(
|
|
205
|
+
recipe_path, docs_dir, str(output_path), final_emb,
|
|
206
|
+
chunk_size=winning_chunk_size, chunk_overlap=winning_chunk_overlap,
|
|
207
|
+
collection=collection,
|
|
208
|
+
)
|
|
209
|
+
build_reporter.print_step("Indexing", elapsed=_time.time() - t0)
|
|
210
|
+
|
|
211
|
+
final_emb.unload()
|
|
212
|
+
|
|
213
|
+
t0 = _time.time()
|
|
214
|
+
check_cancelled()
|
|
215
|
+
report_progress("Generating metadata")
|
|
216
|
+
generate_all(final_db)
|
|
217
|
+
build_reporter.print_step("Metadata", elapsed=_time.time() - t0)
|
|
218
|
+
|
|
219
|
+
_final_store = ChunkStore(final_db)
|
|
220
|
+
sources = _final_store.list_sources()
|
|
221
|
+
_final_store.close()
|
|
222
|
+
|
|
223
|
+
report = {
|
|
224
|
+
"collection": collection,
|
|
225
|
+
"model_name": winning_model,
|
|
226
|
+
"chunk_size": winning_chunk_size,
|
|
227
|
+
"chunk_overlap": winning_chunk_overlap,
|
|
228
|
+
"file_count": len(sources),
|
|
229
|
+
"chunk_count": sum(s["count"] for s in sources),
|
|
230
|
+
"optimization": optimization,
|
|
231
|
+
"resumed": resumed,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
json_report_path = output_path / "build-report.json"
|
|
235
|
+
json_report_path.write_text(
|
|
236
|
+
json.dumps(report, indent=2, ensure_ascii=False),
|
|
237
|
+
encoding="utf-8",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
build_reporter.print_summary(
|
|
241
|
+
files=len(sources),
|
|
242
|
+
chunks=sum(s["count"] for s in sources),
|
|
243
|
+
configs_tested=len(optimization.get("all", [])) if optimization else 0,
|
|
244
|
+
elapsed=_time.time() - build_start,
|
|
245
|
+
report_path=str(json_report_path),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not config.keep_intermediates:
|
|
249
|
+
import shutil
|
|
250
|
+
_prep = output_path / "prep"
|
|
251
|
+
_work = output_path / ".work"
|
|
252
|
+
if _prep.exists():
|
|
253
|
+
shutil.rmtree(_prep)
|
|
254
|
+
if _work.exists():
|
|
255
|
+
shutil.rmtree(_work)
|
|
256
|
+
|
|
257
|
+
return report
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _run_optimization(
|
|
261
|
+
recipe_path: str,
|
|
262
|
+
docs_dir: str,
|
|
263
|
+
embedders: dict,
|
|
264
|
+
work_dir: str,
|
|
265
|
+
chunk_sizes: list[int] | None = None,
|
|
266
|
+
chunk_overlaps: list[int] | None = None,
|
|
267
|
+
top_ks: list[int] | None = None,
|
|
268
|
+
num_questions: int = 50,
|
|
269
|
+
force: bool = False,
|
|
270
|
+
output_level: str = "default",
|
|
271
|
+
metrics: list[str] | None = None,
|
|
272
|
+
judge_url: str = "",
|
|
273
|
+
judge_model: str = "",
|
|
274
|
+
judge_verify_ssl: bool = True,
|
|
275
|
+
report_path: str | None = None,
|
|
276
|
+
config=None,
|
|
277
|
+
) -> dict:
|
|
278
|
+
"""Run optimization with resumability."""
|
|
279
|
+
if config is not None:
|
|
280
|
+
if chunk_sizes is None:
|
|
281
|
+
chunk_sizes = config.optimize_chunk_sizes
|
|
282
|
+
if chunk_overlaps is None:
|
|
283
|
+
chunk_overlaps = config.optimize_chunk_overlaps
|
|
284
|
+
if top_ks is None:
|
|
285
|
+
top_ks = config.optimize_top_ks
|
|
286
|
+
if not num_questions or num_questions == 50:
|
|
287
|
+
num_questions = config.optimize_num_questions or 50
|
|
288
|
+
if metrics is None:
|
|
289
|
+
metrics = config.optimize_metrics or None
|
|
290
|
+
if not judge_url:
|
|
291
|
+
judge_url = config.llm_api_url
|
|
292
|
+
if not judge_model:
|
|
293
|
+
judge_model = config.llm_model
|
|
294
|
+
judge_verify_ssl = config.llm_verify_ssl
|
|
295
|
+
|
|
296
|
+
work_path = Path(work_dir)
|
|
297
|
+
scores_path = work_path / SCORES_FILE
|
|
298
|
+
questions_path = work_path / QUESTIONS_FILE
|
|
299
|
+
|
|
300
|
+
existing_scores = []
|
|
301
|
+
resumed = False
|
|
302
|
+
|
|
303
|
+
if not force and scores_path.exists():
|
|
304
|
+
existing_scores = [
|
|
305
|
+
json.loads(line)
|
|
306
|
+
for line in scores_path.read_text().strip().split("\n")
|
|
307
|
+
if line.strip()
|
|
308
|
+
]
|
|
309
|
+
resumed = True
|
|
310
|
+
logger.info("Resuming: %d existing scores", len(existing_scores))
|
|
311
|
+
|
|
312
|
+
result = run_optimize(
|
|
313
|
+
embedders=embedders,
|
|
314
|
+
db_dir=work_dir,
|
|
315
|
+
recipe_path=recipe_path,
|
|
316
|
+
docs_dir=docs_dir,
|
|
317
|
+
chunk_sizes=chunk_sizes,
|
|
318
|
+
chunk_overlaps=chunk_overlaps,
|
|
319
|
+
top_ks=top_ks,
|
|
320
|
+
num_questions=num_questions,
|
|
321
|
+
output_level=output_level,
|
|
322
|
+
metrics=metrics,
|
|
323
|
+
judge_url=judge_url,
|
|
324
|
+
judge_model=judge_model,
|
|
325
|
+
judge_verify_ssl=judge_verify_ssl,
|
|
326
|
+
report_path=report_path,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
with open(scores_path, "w", encoding="utf-8") as f:
|
|
330
|
+
for entry in result.get("all", []):
|
|
331
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
332
|
+
|
|
333
|
+
result["resumed"] = resumed
|
|
334
|
+
return result
|
lorme/build_config.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Unified build configuration. See docs/architecture.md."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _read_embedding_key(data: dict, path: str) -> list:
|
|
9
|
+
"""Read embedding models, reject old key names."""
|
|
10
|
+
if "models" in data or "embedding_models" in data:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
f"Use 'embedding:' key (not 'models:' or 'embedding_models:') "
|
|
13
|
+
f"in {path}"
|
|
14
|
+
)
|
|
15
|
+
return data.get("embedding", [])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class BuildConfig:
|
|
20
|
+
"""Unified configuration for lorme build."""
|
|
21
|
+
embedding_models: list[dict] = field(default_factory=list)
|
|
22
|
+
judge_model: str = ""
|
|
23
|
+
judge_api_url: str = ""
|
|
24
|
+
judge_verify_ssl: bool = True
|
|
25
|
+
metrics: list[str] = field(default_factory=lambda: ["score_spread", "source_diversity", "result_diversity"])
|
|
26
|
+
chunk_sizes: list[int] = field(default_factory=lambda: [512, 1024, 2048])
|
|
27
|
+
chunk_overlaps: list[int] = field(default_factory=lambda: [64, 128])
|
|
28
|
+
top_ks: list[int] = field(default_factory=lambda: [3, 5, 10])
|
|
29
|
+
num_questions: int = 50
|
|
30
|
+
default_model: str = ""
|
|
31
|
+
default_chunk_size: int = 1024
|
|
32
|
+
default_chunk_overlap: int = 128
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_file(cls, path: str) -> "BuildConfig":
|
|
36
|
+
"""Load build config from a YAML file."""
|
|
37
|
+
with open(path, encoding="utf-8") as f:
|
|
38
|
+
data = yaml.safe_load(f) or {}
|
|
39
|
+
|
|
40
|
+
judge = data.get("judge", {})
|
|
41
|
+
optimize = data.get("optimize", {})
|
|
42
|
+
defaults = data.get("defaults", {})
|
|
43
|
+
|
|
44
|
+
return cls(
|
|
45
|
+
embedding_models=_read_embedding_key(data, path),
|
|
46
|
+
judge_model=judge.get("model", ""),
|
|
47
|
+
judge_api_url=judge.get("api_url", ""),
|
|
48
|
+
judge_verify_ssl=judge.get("verify_ssl", True),
|
|
49
|
+
metrics=data.get("metrics", ["score_spread", "source_diversity", "result_diversity"]),
|
|
50
|
+
chunk_sizes=optimize.get("chunk_sizes", [512, 1024, 2048]),
|
|
51
|
+
chunk_overlaps=optimize.get("chunk_overlaps", [64, 128]),
|
|
52
|
+
top_ks=optimize.get("top_ks", [3, 5, 10]),
|
|
53
|
+
num_questions=optimize.get("num_questions", 50),
|
|
54
|
+
default_model=defaults.get("model", ""),
|
|
55
|
+
default_chunk_size=defaults.get("chunk_size", 1024),
|
|
56
|
+
default_chunk_overlap=defaults.get("chunk_overlap", 128),
|
|
57
|
+
)
|
|
58
|
+
|