feat(semantic-search): report search cost before retrieval
CI / CI configuration security checks (push) Successful in 4s
CI / Capabilities and shell quality (push) Successful in 15s
CI / Python tests and compilation (push) Successful in 18s
CI / Build and inspect generated packages (push) Successful in 19s
CI / PowerShell tests on Ubuntu (push) Successful in 1m6s
CI / Installed client configuration smoke tests (push) Successful in 38s
CI / Bash integration and installation guards (push) Successful in 1m10s
CI / Qwen retrieval integration (push) Successful in 14m24s

This commit is contained in:
Julian lechner committed 2026-10-01 15:50:29 +02:00
1 parent 13220affb3
commit a81c67f070
5 files changed
+116 -33

No files matched your search

+81 -28
View File
@@ -368,6 +368,9 @@ def run_daemon(data_dir: Path, model_cache: Path):
response = (True, None)
try:
request = json.loads(connection.recv_bytes(DAEMON_MESSAGE_LIMIT))
if isinstance(request, dict) and request.get("action") == "stop":
connection.send_bytes(json.dumps((True, None)).encode("utf-8"))
os._exit(0)
if not isinstance(request, dict) or request.get("action") not in ("search", "rebuild") or not isinstance(request.get("root"), str):
raise ValueError("Invalid semantic retrieval daemon request.")
index = indexes.get(request["root"])
@@ -395,16 +398,76 @@ def run_daemon(data_dir: Path, model_cache: Path):
listener.close()
def index_database(root: Path, data: Path) -> Path:
identity = json.dumps([os.path.normcase(str(root.resolve())), MODEL, MODEL_REVISION,
EMBEDDING_DIMENSIONS, RETRIEVAL_INSTRUCTION, INDEX_VERSION,
CHUNK_TOKENS, OVERLAP_TOKENS])
return data.resolve() / (hashlib.sha256(identity.encode()).hexdigest() + ".sqlite3")
def daemon_running(data_dir: Path) -> bool:
if not pipes_available():
return False
try:
Client(daemon_address(data_dir), authkey=daemon_authkey(data_dir)).close()
return True
except (OSError, EOFError, AuthenticationError):
return False
def index_status(root: Path, data_dir: Path) -> dict:
"""Report what the next search costs without loading models or starting the daemon."""
database = index_database(root, data_dir)
previous = {}
if database.is_file():
connection = sqlite3.connect(database.as_uri() + "?mode=ro", uri=True)
try:
previous = {path: (size, mtime, ctime) for path, size, mtime, ctime
in connection.execute("select path, size, mtime, ctime from files")}
except sqlite3.OperationalError:
pass
finally:
connection.close()
files = indexable_files(root.resolve(), data_dir.resolve(), read_max_files())
stale = 0
for path in files:
metadata = path.stat()
stale += previous.get(path.relative_to(root.resolve()).as_posix()) != (metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns)
return {"indexable_files": len(files), "indexed_files": len(previous), "stale_files": stale,
"models_loaded": daemon_running(data_dir), "in_process": not pipes_available()}
def indexable_files(root: Path, data: Path, max_files: int):
ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration", "bin", "obj"}
output = subprocess.run(["git", "-C", str(root), "ls-files", "--cached", "--others",
"--exclude-standard", "-z", "--", "."],
check=True, capture_output=True, timeout=30).stdout
candidates = [root / name for name in
output.decode("utf-8", errors="surrogateescape").split("\0") if name
and not any(part in ignored for part in Path(name).parts)]
result = []
for candidate in sorted(candidates):
if candidate.is_symlink():
continue
path = candidate.resolve()
if (not path.is_relative_to(root) or path.is_relative_to(data)
or path.suffix.lower() not in TEXT_EXTENSIONS or not path.is_file() or path.stat().st_size > 524288):
continue
result.append(path)
if len(result) == max_files:
print(f"Semantic retrieval: stopped scanning after {max_files} indexable files; "
f"some files under {root} were not indexed. Set {MAX_FILES_ENV} to raise this limit.",
file=sys.stderr)
return result
return result
class Index:
def __init__(self, root: Path, data: Path):
self.root = root.resolve()
self.data = data.resolve()
self.data.mkdir(parents=True, exist_ok=True)
identity = json.dumps([os.path.normcase(str(self.root)), MODEL, MODEL_REVISION,
EMBEDDING_DIMENSIONS, RETRIEVAL_INSTRUCTION, INDEX_VERSION,
CHUNK_TOKENS, OVERLAP_TOKENS])
database = hashlib.sha256(identity.encode()).hexdigest() + ".sqlite3"
self.db = sqlite3.connect(self.data / database, check_same_thread=False, timeout=30)
self.db = sqlite3.connect(index_database(self.root, self.data), check_same_thread=False, timeout=30)
self.db.execute("create table if not exists chunks (path text, symbol text, text text, vector blob)")
self.db.execute("create index if not exists chunks_path on chunks (path)")
self.db.execute("create virtual table if not exists lexical using fts5(document)")
@@ -420,28 +483,7 @@ class Index:
self.vector_index = None
def files(self):
ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration", "bin", "obj"}
output = subprocess.run(["git", "-C", str(self.root), "ls-files", "--cached", "--others",
"--exclude-standard", "-z", "--", "."],
check=True, capture_output=True, timeout=30).stdout
candidates = [self.root / name for name in
output.decode("utf-8", errors="surrogateescape").split("\0") if name
and not any(part in ignored for part in Path(name).parts)]
result = []
for candidate in sorted(candidates):
if candidate.is_symlink():
continue
path = candidate.resolve()
if (not path.is_relative_to(self.root) or path.is_relative_to(self.data)
or path.suffix.lower() not in TEXT_EXTENSIONS or not path.is_file() or path.stat().st_size > 524288):
continue
result.append(path)
if len(result) == self.max_files:
print(f"Semantic retrieval: stopped scanning after {self.max_files} indexable files; "
f"some files under {self.root} were not indexed. Set {MAX_FILES_ENV} to raise this limit.",
file=sys.stderr)
return result
return result
return indexable_files(self.root, self.data, self.max_files)
def encoder(self):
return shared_encoder()
@@ -609,7 +651,7 @@ def run_direct(action: str, root: Path, data_dir: Path, model_cache: Path, query
def main():
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=("search", "rebuild", "daemon", "preload"))
parser.add_argument("action", choices=("search", "rebuild", "status", "stop", "daemon", "preload"))
parser.add_argument("--query")
parser.add_argument("--top-k", type=int, default=FINAL_RESULTS)
parser.add_argument("--root", default=os.getcwd(), type=Path)
@@ -627,6 +669,17 @@ def main():
data_dir.mkdir(parents=True, exist_ok=True)
run_daemon(data_dir, model_cache)
return
if args.action == "status":
print(json.dumps(index_status(args.root, data_dir)))
return
if args.action == "stop":
try:
request_daemon(daemon_address(data_dir), {"action": "stop"}, data_dir)
stopped = True
except (OSError, EOFError, AuthenticationError):
stopped = False
print(json.dumps({"stopped": stopped}))
return
if args.action == "search" and not args.query:
parser.error("search requires --query")
root = args.root.resolve()