search-as-code 0.0.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.
- search_as_code/__init__.py +89 -0
- search_as_code/_resilience.py +99 -0
- search_as_code/adapters/__init__.py +5 -0
- search_as_code/adapters/base.py +108 -0
- search_as_code/adapters/chroma.py +95 -0
- search_as_code/adapters/faiss_store.py +90 -0
- search_as_code/adapters/memory.py +148 -0
- search_as_code/adapters/milvus_store.py +87 -0
- search_as_code/adapters/nmslib_store.py +102 -0
- search_as_code/adapters/opensearch.py +370 -0
- search_as_code/adapters/pgvector.py +110 -0
- search_as_code/adapters/qdrant.py +133 -0
- search_as_code/adapters/registry.py +98 -0
- search_as_code/adapters/sqlite_store.py +105 -0
- search_as_code/embeddings.py +155 -0
- search_as_code/errors.py +145 -0
- search_as_code/explore/__init__.py +31 -0
- search_as_code/explore/engine.py +160 -0
- search_as_code/explore/pack.py +127 -0
- search_as_code/explore/report.py +72 -0
- search_as_code/explore/stages.py +349 -0
- search_as_code/filters.py +102 -0
- search_as_code/primitives.py +422 -0
- search_as_code/rerankers.py +94 -0
- search_as_code/sandbox.py +116 -0
- search_as_code/session.py +460 -0
- search_as_code/types.py +129 -0
- search_as_code-0.0.1.dist-info/METADATA +234 -0
- search_as_code-0.0.1.dist-info/RECORD +32 -0
- search_as_code-0.0.1.dist-info/WHEEL +5 -0
- search_as_code-0.0.1.dist-info/licenses/LICENSE +201 -0
- search_as_code-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""The exploration engine — a resumable, validate-before-keep stage runner.
|
|
2
|
+
|
|
3
|
+
``explore(session, out=...)`` walks an ordered list of :class:`Stage` objects. Each
|
|
4
|
+
stage reads/writes artifacts on the :class:`ProfilePack`. The engine:
|
|
5
|
+
|
|
6
|
+
- **skips** stages already done (artifacts present) unless ``force`` or the corpus
|
|
7
|
+
fingerprint changed (drift);
|
|
8
|
+
- runs a stage, then calls its ``validate`` gate — a stage whose output does not beat
|
|
9
|
+
baseline is recorded as ``rejected`` and its artifacts are *not* trusted downstream
|
|
10
|
+
(the honesty rule: keep only tunings that help);
|
|
11
|
+
- records status/timing/summary for every stage in the manifest, and never lets one
|
|
12
|
+
stage's failure abort the rest.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import abc
|
|
18
|
+
import hashlib
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
from .pack import ProfilePack
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ExploreContext:
|
|
28
|
+
"""Everything a stage needs: the live Session, the pack to write to, and config."""
|
|
29
|
+
|
|
30
|
+
session: Any # search_as_code.Session
|
|
31
|
+
pack: ProfilePack
|
|
32
|
+
config: dict = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def store(self):
|
|
36
|
+
return self.session.store
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def embedder(self):
|
|
40
|
+
return self.session.embedder
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def generator(self):
|
|
44
|
+
return self.session.generator
|
|
45
|
+
|
|
46
|
+
def cfg(self, key: str, default: Any = None) -> Any:
|
|
47
|
+
return self.config.get(key, default)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Stage(abc.ABC):
|
|
51
|
+
"""One unit of exploration. Subclasses declare what they produce/require."""
|
|
52
|
+
|
|
53
|
+
name: str = "stage"
|
|
54
|
+
produces: list[str] = [] # artifact filenames written on success
|
|
55
|
+
requires: list[str] = [] # names of stages that must be ``ok`` first
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
def run(self, ctx: ExploreContext) -> dict:
|
|
59
|
+
"""Do the work, write artifacts, return a small summary dict for the manifest."""
|
|
60
|
+
|
|
61
|
+
def validate(self, ctx: ExploreContext, summary: dict) -> tuple[bool, str]:
|
|
62
|
+
"""Gate: return (keep, reason). Default keeps everything. Learning stages
|
|
63
|
+
override this to reject output that does not beat baseline."""
|
|
64
|
+
return True, ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def corpus_fingerprint(store, k: int = 12) -> str:
|
|
68
|
+
"""Cheap, stable signature of the corpus so we can detect drift. Combines the
|
|
69
|
+
doc count with a hash of a small deterministic-ish text sample."""
|
|
70
|
+
try:
|
|
71
|
+
count = store.count()
|
|
72
|
+
except Exception:
|
|
73
|
+
count = -1
|
|
74
|
+
texts = []
|
|
75
|
+
try:
|
|
76
|
+
for d in store.sample(k):
|
|
77
|
+
texts.append((d.text or "")[:120])
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
h = hashlib.sha1(("|".join(sorted(texts))).encode("utf-8", "ignore")).hexdigest()[:12]
|
|
81
|
+
return f"n{count}-{h}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def explore(session, out: str, stages: Optional[list[Stage]] = None, *,
|
|
85
|
+
force: bool = False, config: Optional[dict] = None) -> ProfilePack:
|
|
86
|
+
"""Run the exploration pipeline over ``session``'s corpus into a ProfilePack at ``out``.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
session : Session the bound corpus (store + embedder + optional generator).
|
|
91
|
+
out : str directory for the ProfilePack (created if absent).
|
|
92
|
+
stages : list ordered Stage instances; defaults to :func:`default_pipeline`.
|
|
93
|
+
force : bool re-run every stage even if its artifacts exist.
|
|
94
|
+
config : dict knobs passed through to stages (sample sizes, k, etc.).
|
|
95
|
+
"""
|
|
96
|
+
pack = ProfilePack.open(out)
|
|
97
|
+
ctx = ExploreContext(session=session, pack=pack, config=config or {})
|
|
98
|
+
|
|
99
|
+
fp = corpus_fingerprint(session.store)
|
|
100
|
+
drift = pack.fingerprint_changed(fp)
|
|
101
|
+
pack.set_fingerprint(fp)
|
|
102
|
+
if stages is None:
|
|
103
|
+
stages = default_pipeline()
|
|
104
|
+
|
|
105
|
+
done: set[str] = set()
|
|
106
|
+
for stage in stages:
|
|
107
|
+
missing = [r for r in stage.requires if r not in done]
|
|
108
|
+
if missing:
|
|
109
|
+
pack.record_stage(stage.name, "skipped",
|
|
110
|
+
note=f"unmet requires: {', '.join(missing)}")
|
|
111
|
+
continue
|
|
112
|
+
if pack.is_done(stage.name) and not force and not drift:
|
|
113
|
+
done.add(stage.name)
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
t0 = time.time()
|
|
117
|
+
try:
|
|
118
|
+
summary = stage.run(ctx)
|
|
119
|
+
keep, reason = stage.validate(ctx, summary)
|
|
120
|
+
secs = time.time() - t0
|
|
121
|
+
if keep:
|
|
122
|
+
pack.record_stage(stage.name, "ok", seconds=secs, summary=summary,
|
|
123
|
+
artifacts=list(stage.produces), note=reason)
|
|
124
|
+
done.add(stage.name)
|
|
125
|
+
else:
|
|
126
|
+
pack.record_stage(stage.name, "rejected", seconds=secs, summary=summary,
|
|
127
|
+
note=reason or "did not beat baseline")
|
|
128
|
+
except NotImplementedError as e:
|
|
129
|
+
pack.record_stage(stage.name, "planned", seconds=time.time() - t0, note=str(e))
|
|
130
|
+
except Exception as e: # never let one stage abort the pipeline
|
|
131
|
+
pack.record_stage(stage.name, "error", seconds=time.time() - t0,
|
|
132
|
+
note=f"{type(e).__name__}: {e}")
|
|
133
|
+
return pack
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def default_pipeline() -> list[Stage]:
|
|
137
|
+
"""The full seven-stage pipeline. Stages not yet implemented raise
|
|
138
|
+
NotImplementedError and are recorded as ``planned`` — the pipeline still runs
|
|
139
|
+
end-to-end and the pack shows the roadmap."""
|
|
140
|
+
from .stages import (
|
|
141
|
+
CodegenStage,
|
|
142
|
+
CrossDocStage,
|
|
143
|
+
OntologyStage,
|
|
144
|
+
ProfileStage,
|
|
145
|
+
RouterStage,
|
|
146
|
+
SampleStage,
|
|
147
|
+
SynthesizeStage,
|
|
148
|
+
ValidateStage,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return [
|
|
152
|
+
SampleStage(),
|
|
153
|
+
ProfileStage(),
|
|
154
|
+
OntologyStage(),
|
|
155
|
+
CrossDocStage(),
|
|
156
|
+
SynthesizeStage(),
|
|
157
|
+
RouterStage(),
|
|
158
|
+
CodegenStage(),
|
|
159
|
+
ValidateStage(),
|
|
160
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""ProfilePack — the on-disk artifact produced by ``sac.explore``.
|
|
2
|
+
|
|
3
|
+
A ProfilePack is a *versioned directory* holding everything the exploration phase
|
|
4
|
+
learns about a corpus (schema, content profile, ontology, synthetic queries, a
|
|
5
|
+
primitive router, few-shots/templates, prompt overrides). A ``Session`` loads it
|
|
6
|
+
at query time to tune retrieval to the data — and works fine without it.
|
|
7
|
+
|
|
8
|
+
Design goals (robustness):
|
|
9
|
+
- **Everything serializes here** — nothing the explorer learns is implicit.
|
|
10
|
+
- **Resumable** — each stage writes its own artifact; a crash re-runs only what's missing.
|
|
11
|
+
- **Drift-aware** — the manifest stores a corpus *fingerprint*; when the data changes
|
|
12
|
+
enough, stages are re-run.
|
|
13
|
+
- **Self-describing** — the manifest records every stage's status/timestamp/summary so a
|
|
14
|
+
human (or agent) can see exactly what was built and what was rejected.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
|
|
24
|
+
SCHEMA_VERSION = 1
|
|
25
|
+
MANIFEST_NAME = "manifest.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProfilePack:
|
|
29
|
+
"""A directory of exploration artifacts + a manifest tracking stage status."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, root: Path, manifest: dict):
|
|
32
|
+
self.root = root
|
|
33
|
+
self.manifest = manifest
|
|
34
|
+
|
|
35
|
+
# ---- lifecycle -------------------------------------------------------
|
|
36
|
+
@classmethod
|
|
37
|
+
def open(cls, path: str | Path) -> "ProfilePack":
|
|
38
|
+
"""Open an existing pack or initialise a fresh one at ``path``."""
|
|
39
|
+
root = Path(path)
|
|
40
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
mpath = root / MANIFEST_NAME
|
|
42
|
+
if mpath.exists():
|
|
43
|
+
manifest = json.loads(mpath.read_text())
|
|
44
|
+
else:
|
|
45
|
+
manifest = {
|
|
46
|
+
"schema_version": SCHEMA_VERSION,
|
|
47
|
+
"created": time.time(),
|
|
48
|
+
"fingerprint": None,
|
|
49
|
+
"stages": {}, # name -> {status, ts, seconds, summary, artifacts, note}
|
|
50
|
+
}
|
|
51
|
+
pack = cls(root, manifest)
|
|
52
|
+
pack.save_manifest()
|
|
53
|
+
return pack
|
|
54
|
+
|
|
55
|
+
def save_manifest(self) -> None:
|
|
56
|
+
self.manifest["updated"] = time.time()
|
|
57
|
+
(self.root / MANIFEST_NAME).write_text(json.dumps(self.manifest, indent=2, default=str))
|
|
58
|
+
|
|
59
|
+
# ---- artifacts -------------------------------------------------------
|
|
60
|
+
def path(self, name: str) -> Path:
|
|
61
|
+
return self.root / name
|
|
62
|
+
|
|
63
|
+
def has(self, name: str) -> bool:
|
|
64
|
+
return (self.root / name).exists()
|
|
65
|
+
|
|
66
|
+
def write_json(self, name: str, obj: Any) -> None:
|
|
67
|
+
self.path(name).write_text(json.dumps(obj, indent=2, default=str))
|
|
68
|
+
|
|
69
|
+
def read_json(self, name: str, default: Any = None) -> Any:
|
|
70
|
+
p = self.path(name)
|
|
71
|
+
return json.loads(p.read_text()) if p.exists() else default
|
|
72
|
+
|
|
73
|
+
def write_jsonl(self, name: str, rows: list) -> None:
|
|
74
|
+
with self.path(name).open("w") as f:
|
|
75
|
+
for r in rows:
|
|
76
|
+
f.write(json.dumps(r, default=str) + "\n")
|
|
77
|
+
|
|
78
|
+
def read_jsonl(self, name: str) -> list:
|
|
79
|
+
p = self.path(name)
|
|
80
|
+
if not p.exists():
|
|
81
|
+
return []
|
|
82
|
+
return [json.loads(ln) for ln in p.read_text().splitlines() if ln.strip()]
|
|
83
|
+
|
|
84
|
+
# ---- stage bookkeeping ----------------------------------------------
|
|
85
|
+
def stage(self, name: str) -> dict:
|
|
86
|
+
return self.manifest["stages"].get(name, {})
|
|
87
|
+
|
|
88
|
+
def stage_status(self, name: str) -> Optional[str]:
|
|
89
|
+
return self.stage(name).get("status")
|
|
90
|
+
|
|
91
|
+
def is_done(self, name: str) -> bool:
|
|
92
|
+
"""A stage counts as done if it succeeded and its artifacts still exist."""
|
|
93
|
+
st = self.stage(name)
|
|
94
|
+
if st.get("status") != "ok":
|
|
95
|
+
return False
|
|
96
|
+
return all(self.has(a) for a in st.get("artifacts", []))
|
|
97
|
+
|
|
98
|
+
def record_stage(self, name: str, status: str, *, seconds: float = 0.0,
|
|
99
|
+
summary: Optional[dict] = None, artifacts: Optional[list[str]] = None,
|
|
100
|
+
note: str = "") -> None:
|
|
101
|
+
self.manifest["stages"][name] = {
|
|
102
|
+
"status": status, # ok | rejected | error | planned | skipped
|
|
103
|
+
"ts": time.time(),
|
|
104
|
+
"seconds": round(seconds, 3),
|
|
105
|
+
"summary": summary or {},
|
|
106
|
+
"artifacts": artifacts or [],
|
|
107
|
+
"note": note,
|
|
108
|
+
}
|
|
109
|
+
self.save_manifest()
|
|
110
|
+
|
|
111
|
+
# ---- drift -----------------------------------------------------------
|
|
112
|
+
def set_fingerprint(self, fp: str) -> None:
|
|
113
|
+
self.manifest["fingerprint"] = fp
|
|
114
|
+
self.save_manifest()
|
|
115
|
+
|
|
116
|
+
def fingerprint_changed(self, fp: str) -> bool:
|
|
117
|
+
prev = self.manifest.get("fingerprint")
|
|
118
|
+
return prev is not None and prev != fp
|
|
119
|
+
|
|
120
|
+
# ---- summary ---------------------------------------------------------
|
|
121
|
+
def report(self) -> str:
|
|
122
|
+
lines = [f"ProfilePack {self.root} (schema v{self.manifest.get('schema_version')})"]
|
|
123
|
+
for name, st in self.manifest["stages"].items():
|
|
124
|
+
summ = ", ".join(f"{k}={v}" for k, v in (st.get("summary") or {}).items())
|
|
125
|
+
lines.append(f" [{st.get('status','?'):8}] {name:14} {summ}"
|
|
126
|
+
+ (f" # {st['note']}" if st.get("note") else ""))
|
|
127
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""CSV export for a ProfilePack — flat tables for spreadsheets / quick review.
|
|
2
|
+
|
|
3
|
+
Writes up to three CSVs next to the pack (or a chosen dir):
|
|
4
|
+
- ``stages.csv`` one row per pipeline stage (status/timing/summary)
|
|
5
|
+
- ``clusters.csv`` one row per sample cluster (size, content-type mix, LLM profile)
|
|
6
|
+
- ``documents.csv`` one row per sampled document (cluster, content_type, snippet)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import csv
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from ..primitives import content_type
|
|
16
|
+
from .pack import ProfilePack
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def write_csv_report(pack: ProfilePack, out_dir: str | None = None) -> dict[str, str]:
|
|
20
|
+
"""Write the CSV tables; return {name: path}. Missing artifacts are skipped."""
|
|
21
|
+
out = Path(out_dir) if out_dir else pack.root
|
|
22
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
written: dict[str, str] = {}
|
|
24
|
+
|
|
25
|
+
# ---- stages.csv ------------------------------------------------------
|
|
26
|
+
spath = out / "stages.csv"
|
|
27
|
+
with spath.open("w", newline="") as f:
|
|
28
|
+
w = csv.writer(f)
|
|
29
|
+
w.writerow(["stage", "status", "seconds", "summary", "artifacts", "note"])
|
|
30
|
+
for name, st in pack.manifest.get("stages", {}).items():
|
|
31
|
+
summary = "; ".join(f"{k}={v}" for k, v in (st.get("summary") or {}).items())
|
|
32
|
+
w.writerow([name, st.get("status", ""), st.get("seconds", ""),
|
|
33
|
+
summary, " ".join(st.get("artifacts", [])), st.get("note", "")])
|
|
34
|
+
written["stages"] = str(spath)
|
|
35
|
+
|
|
36
|
+
sample = pack.read_jsonl("sample.jsonl")
|
|
37
|
+
profile = pack.read_json("content_profile.json") or {}
|
|
38
|
+
llm_by_cluster = profile.get("llm_by_cluster") or {}
|
|
39
|
+
sizes = (pack.read_json("sample_meta.json") or {}).get("cluster_sizes", {})
|
|
40
|
+
|
|
41
|
+
if sample:
|
|
42
|
+
# ---- documents.csv ----------------------------------------------
|
|
43
|
+
dpath = out / "documents.csv"
|
|
44
|
+
with dpath.open("w", newline="") as f:
|
|
45
|
+
w = csv.writer(f)
|
|
46
|
+
w.writerow(["id", "cluster", "content_type", "chars", "snippet"])
|
|
47
|
+
for r in sample:
|
|
48
|
+
text = r.get("text") or ""
|
|
49
|
+
snippet = " ".join(text.split())[:200]
|
|
50
|
+
w.writerow([r.get("id"), r.get("cluster"),
|
|
51
|
+
content_type(text), len(text), snippet])
|
|
52
|
+
written["documents"] = str(dpath)
|
|
53
|
+
|
|
54
|
+
# ---- clusters.csv -----------------------------------------------
|
|
55
|
+
by_cluster: dict = {}
|
|
56
|
+
for r in sample:
|
|
57
|
+
by_cluster.setdefault(r.get("cluster"), []).append(r)
|
|
58
|
+
cpath = out / "clusters.csv"
|
|
59
|
+
with cpath.open("w", newline="") as f:
|
|
60
|
+
w = csv.writer(f)
|
|
61
|
+
w.writerow(["cluster", "pool_size", "n_sampled", "content_types",
|
|
62
|
+
"llm_profile", "example_snippet"])
|
|
63
|
+
for c in sorted(by_cluster, key=lambda x: (x is None, x)):
|
|
64
|
+
rows = by_cluster[c]
|
|
65
|
+
cts = Counter(content_type(r.get("text") or "") for r in rows)
|
|
66
|
+
ct_str = ", ".join(f"{k}:{v}" for k, v in cts.most_common())
|
|
67
|
+
example = " ".join((rows[0].get("text") or "").split())[:160]
|
|
68
|
+
w.writerow([c, sizes.get(str(c), ""), len(rows), ct_str,
|
|
69
|
+
(llm_by_cluster.get(str(c)) or "").strip(), example])
|
|
70
|
+
written["clusters"] = str(cpath)
|
|
71
|
+
|
|
72
|
+
return written
|