dirsql-plugin-embeddings 0.1.0__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.
File without changes
@@ -0,0 +1,18 @@
1
+ # Fragment shipped by the dirsql-plugin-embeddings plugin.
2
+ # Discovered via the package's [project.entry-points.dirsql] declaration and
3
+ # injected by the launcher as an ordinary -c flag ("installed = active", #529).
4
+ #
5
+ # The table name here MUST match pre_query.TABLE_NAME (the SQL the pre-query
6
+ # hook prints queries this table).
7
+ [dirsql]
8
+ pre-query = "dirsql-embeddings-pre-query {args}"
9
+ hook-timeout = 300
10
+
11
+ [[dirsql.extension]]
12
+ path = "sqlite_vec"
13
+ entrypoint = "sqlite3_vec_init"
14
+
15
+ [[table]]
16
+ ddl = "CREATE TABLE documents (path TEXT, text TEXT, embedding TEXT)"
17
+ glob = "**/*.md"
18
+ on-file = "dirsql-embeddings-on-file {path}"
@@ -0,0 +1,71 @@
1
+ """Embed text via an OpenAI-compatible ``/v1/embeddings`` endpoint.
2
+
3
+ Configuration comes from three environment variables (base URL, model, API
4
+ key). The HTTP call is behind an injected ``post`` seam so the unit tests drive
5
+ it without a real network; ``_urllib_post`` is the production seam.
6
+
7
+ Annotations are evaluated at runtime (no ``from __future__ import annotations``)
8
+ so a mutated ``X | None`` union in a signature fails at import instead of being
9
+ an inert string -- otherwise every annotation-union mutant would survive.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import urllib.error
15
+ import urllib.request
16
+ from collections.abc import Mapping
17
+
18
+ ENV_BASE_URL = "DIRSQL_EMBEDDINGS_BASE_URL"
19
+ ENV_MODEL = "DIRSQL_EMBEDDINGS_MODEL"
20
+ ENV_API_KEY = "DIRSQL_EMBEDDINGS_API_KEY"
21
+
22
+
23
+ class EmbeddingError(RuntimeError):
24
+ """A configuration, transport, or response error while embedding."""
25
+
26
+
27
+ def _urllib_post(url: str, data: bytes, headers: Mapping[str, str]):
28
+ request = urllib.request.Request(
29
+ url, data=data, headers=dict(headers), method="POST"
30
+ )
31
+ try:
32
+ with urllib.request.urlopen(request) as response:
33
+ return response.status, response.read()
34
+ except urllib.error.HTTPError as error:
35
+ return error.code, error.read()
36
+
37
+
38
+ def _require(env: Mapping[str, str], name: str) -> str:
39
+ value = env.get(name, "")
40
+ if not value:
41
+ raise EmbeddingError(f"missing required environment variable {name}")
42
+ return value
43
+
44
+
45
+ def embed(
46
+ text: str, env: Mapping[str, str] | None = None, post=_urllib_post
47
+ ) -> list[float]:
48
+ if env is None:
49
+ env = os.environ
50
+ base_url = _require(env, ENV_BASE_URL).rstrip("/")
51
+ model = _require(env, ENV_MODEL)
52
+ api_key = _require(env, ENV_API_KEY)
53
+
54
+ data = json.dumps({"model": model, "input": [text]}).encode("utf-8")
55
+ headers = {
56
+ "Content-Type": "application/json",
57
+ "Authorization": f"Bearer {api_key}",
58
+ }
59
+ status, body = post(f"{base_url}/v1/embeddings", data=data, headers=headers)
60
+ if status != 200:
61
+ raise EmbeddingError(f"embeddings endpoint returned status {status}: {body!r}")
62
+
63
+ try:
64
+ entry = json.loads(body)["data"][0]
65
+ except (json.JSONDecodeError, KeyError, IndexError, TypeError) as exc:
66
+ raise EmbeddingError(f"malformed embeddings response: {body!r}") from exc
67
+
68
+ vector = entry.get("embedding")
69
+ if not isinstance(vector, list) or not vector:
70
+ raise EmbeddingError(f"embeddings response carried no vector: {body!r}")
71
+ return [float(component) for component in vector]
@@ -0,0 +1,33 @@
1
+ """``on-file`` console script: embed one matched file into a dirsql row.
2
+
3
+ Reads the file at ``argv[1]``, embeds its text, and prints a one-line JSON row
4
+ array (``path``, ``text``, ``embedding``) -- the embedding stored as JSON text,
5
+ which ``sqlite-vec`` accepts directly.
6
+
7
+ Annotations are evaluated at runtime (no ``from __future__ import annotations``)
8
+ so a mutated ``X | None`` union in a signature fails at import rather than
9
+ surviving as an inert string.
10
+ """
11
+
12
+ import json
13
+ import sys
14
+
15
+ from .embedder import embed
16
+
17
+
18
+ def _read_text(path: str) -> str:
19
+ with open(path, encoding="utf-8") as handle:
20
+ return handle.read()
21
+
22
+
23
+ def build_rows(path: str, text: str, vector: list[float]) -> list[dict]:
24
+ return [{"path": path, "text": text, "embedding": json.dumps(vector)}]
25
+
26
+
27
+ def main(argv: list[str] | None = None) -> int:
28
+ if argv is None:
29
+ argv = sys.argv
30
+ path = argv[1]
31
+ text = _read_text(path)
32
+ print(json.dumps(build_rows(path, text, embed(text))))
33
+ return 0
@@ -0,0 +1,42 @@
1
+ """``pre-query`` console script: turn a ``{"q": ...}`` body into search SQL.
2
+
3
+ Accepts both a verbatim server body (``{"q": ...}``) and the CLI ``query``
4
+ subcommand's ``{"sql": <arg>}`` wrapper, embeds the question, and prints the
5
+ nearest-neighbor SQL over the ``documents`` table (ordered by
6
+ ``vec_distance_cosine``). The hook owns SQL safety: the only interpolated value
7
+ is a numeric vector this script produced.
8
+
9
+ Annotations are evaluated at runtime (no ``from __future__ import annotations``)
10
+ so a mutated ``X | None`` union in a signature fails at import rather than
11
+ surviving as an inert string.
12
+ """
13
+
14
+ import json
15
+ import sys
16
+
17
+ from .embedder import embed
18
+
19
+ TABLE_NAME = "documents"
20
+ RESULT_LIMIT = 3
21
+
22
+
23
+ def question(raw_body: str) -> str:
24
+ body = json.loads(raw_body)
25
+ if "q" in body:
26
+ return body["q"]
27
+ return json.loads(body["sql"])["q"]
28
+
29
+
30
+ def build_sql(vector: list[float]) -> str:
31
+ needle = json.dumps(vector)
32
+ return (
33
+ f"SELECT path, ROUND(vec_distance_cosine(embedding, '{needle}'), 3) "
34
+ f"AS distance FROM {TABLE_NAME} ORDER BY distance LIMIT {RESULT_LIMIT}"
35
+ )
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ if argv is None:
40
+ argv = sys.argv
41
+ print(build_sql(embed(question(argv[1]))))
42
+ return 0
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: dirsql-plugin-embeddings
3
+ Version: 0.1.0
4
+ Summary: First-party dirsql plugin: semantic search via an OpenAI-compatible embeddings endpoint.
5
+ Requires-Python: >=3.11
@@ -0,0 +1,9 @@
1
+ dirsql_plugin_embeddings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ dirsql_plugin_embeddings/embedder.py,sha256=4nNii0sGhskBwH7ppSMFgVKw2Z4yQT3KsR-CNYFNui8,2540
3
+ dirsql_plugin_embeddings/on_file.py,sha256=OojSpkUSLeU4j2CbAdy97k_WcfKQ9pS3EzxEe7UbD-4,994
4
+ dirsql_plugin_embeddings/pre_query.py,sha256=x4fOxHKmGlxBp3ojcPOjsIcM2kiQOC5veAFQgUjXcqQ,1265
5
+ dirsql_plugin_embeddings/dirsql.toml,sha256=G3vdR9fpLt53IoPCkmd0KX9RqUCn3jPTXsphBk5W4FA,624
6
+ dirsql_plugin_embeddings-0.1.0.dist-info/METADATA,sha256=nnaduB4QSPOi8CpJeCv65wIP4dOYDQ6FnTq8NzJau5I,190
7
+ dirsql_plugin_embeddings-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ dirsql_plugin_embeddings-0.1.0.dist-info/entry_points.txt,sha256=JhSvJnpAdXe7FoQ1vtbBgYmLiVPBr1AHPAYla-x93_s,202
9
+ dirsql_plugin_embeddings-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ dirsql-embeddings-on-file = dirsql_plugin_embeddings.on_file:main
3
+ dirsql-embeddings-pre-query = dirsql_plugin_embeddings.pre_query:main
4
+
5
+ [dirsql]
6
+ embeddings = dirsql_plugin_embeddings