dirsql-plugin-embeddings 0.1.0__tar.gz
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.
- dirsql_plugin_embeddings-0.1.0/.gitignore +58 -0
- dirsql_plugin_embeddings-0.1.0/PKG-INFO +5 -0
- dirsql_plugin_embeddings-0.1.0/README.md +58 -0
- dirsql_plugin_embeddings-0.1.0/e2e-attestations/claude-open-issues-review-le8hm5-531.json +7 -0
- dirsql_plugin_embeddings-0.1.0/pyproject.toml +66 -0
- dirsql_plugin_embeddings-0.1.0/src/dirsql_plugin_embeddings/__init__.py +0 -0
- dirsql_plugin_embeddings-0.1.0/src/dirsql_plugin_embeddings/dirsql.toml +18 -0
- dirsql_plugin_embeddings-0.1.0/src/dirsql_plugin_embeddings/embedder.py +71 -0
- dirsql_plugin_embeddings-0.1.0/src/dirsql_plugin_embeddings/on_file.py +33 -0
- dirsql_plugin_embeddings-0.1.0/src/dirsql_plugin_embeddings/pre_query.py +42 -0
- dirsql_plugin_embeddings-0.1.0/testing-conventions.toml +7 -0
- dirsql_plugin_embeddings-0.1.0/tests/conftest.py +62 -0
- dirsql_plugin_embeddings-0.1.0/tests/e2e/__init__.py +0 -0
- dirsql_plugin_embeddings-0.1.0/tests/integration/__init__.py +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/target
|
|
2
|
+
packages/*/target
|
|
3
|
+
.beads
|
|
4
|
+
.claude/settings.local.json
|
|
5
|
+
.claude/scheduled_tasks.lock
|
|
6
|
+
.worktrees/
|
|
7
|
+
.claude/worktrees/
|
|
8
|
+
|
|
9
|
+
# Generated by .claude/hooks/select-environment.sh on session start.
|
|
10
|
+
# The environment source files live in agents/environments/ and are checked in.
|
|
11
|
+
agents/build/
|
|
12
|
+
|
|
13
|
+
# Node
|
|
14
|
+
node_modules/
|
|
15
|
+
*.node
|
|
16
|
+
|
|
17
|
+
# TypeScript build output
|
|
18
|
+
packages/ts/dist/
|
|
19
|
+
# TypeScript coverage output
|
|
20
|
+
packages/ts/coverage/
|
|
21
|
+
# napi-rs generated type definitions (authored types live in packages/ts/src/)
|
|
22
|
+
packages/ts/index.d.ts
|
|
23
|
+
|
|
24
|
+
# Python
|
|
25
|
+
__pycache__/
|
|
26
|
+
*.pyc
|
|
27
|
+
*.pyo
|
|
28
|
+
*.so
|
|
29
|
+
.venv/
|
|
30
|
+
.coverage
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
# Rust CLI binary bundled into the wheel by CI (not in source control).
|
|
33
|
+
packages/python/dirsql/_binary/
|
|
34
|
+
|
|
35
|
+
# putitoutthere per-(mode, triple) staging outputs
|
|
36
|
+
packages/ts/build/
|
|
37
|
+
|
|
38
|
+
# wireit cache (per-package)
|
|
39
|
+
packages/*/.wireit/
|
|
40
|
+
|
|
41
|
+
# uv
|
|
42
|
+
uv.lock
|
|
43
|
+
|
|
44
|
+
# Dolt database files (added by bd init)
|
|
45
|
+
.dolt/
|
|
46
|
+
*.db
|
|
47
|
+
|
|
48
|
+
# VitePress / docs
|
|
49
|
+
docs/node_modules/
|
|
50
|
+
docs/.vitepress/cache/
|
|
51
|
+
docs/.vitepress/dist/
|
|
52
|
+
|
|
53
|
+
# Playwright
|
|
54
|
+
docs/test-results/
|
|
55
|
+
docs/playwright-report/
|
|
56
|
+
docs/blob-report/
|
|
57
|
+
docs/.playwright/
|
|
58
|
+
actionlint
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# dirsql-plugin-embeddings
|
|
2
|
+
|
|
3
|
+
A first-party [`dirsql`](https://github.com/thekevinscott/dirsql) plugin that
|
|
4
|
+
adds **semantic search** over a directory of Markdown files. It is the worked
|
|
5
|
+
implementation behind the [Search documents by
|
|
6
|
+
meaning](../../docs/howto/search-by-meaning.md) how-to, swapping that guide's
|
|
7
|
+
local `model2vec` model for any OpenAI-compatible `/v1/embeddings` endpoint.
|
|
8
|
+
|
|
9
|
+
This is an **in-repo, repo-only** package (not published to an index). It exists
|
|
10
|
+
to prove the dirsql plugin conventions (#531, part of #363) and power the
|
|
11
|
+
semantic-search demo. Deliberately minimal (v0.1): one embedding provider shape,
|
|
12
|
+
one table, no chunking, no config surface beyond three environment variables.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
The plugin ships a `dirsql.toml` fragment that dirsql discovers when the package
|
|
17
|
+
is installed alongside it. The fragment declares:
|
|
18
|
+
|
|
19
|
+
- the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) extension, for
|
|
20
|
+
`vec_distance_cosine()`;
|
|
21
|
+
- a `documents` table whose `on-file` hook embeds each `**/*.md` file into a
|
|
22
|
+
TEXT `embedding` column;
|
|
23
|
+
- a `pre-query` hook that embeds the incoming question and emits the
|
|
24
|
+
nearest-neighbor SQL.
|
|
25
|
+
|
|
26
|
+
Both hooks are console scripts that call the same embedder.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
The embedder reads three environment variables (point them at any hosted or
|
|
31
|
+
self-managed OpenAI-compatible inference server):
|
|
32
|
+
|
|
33
|
+
| Variable | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `DIRSQL_EMBEDDINGS_BASE_URL` | Base URL; `/v1/embeddings` is appended. |
|
|
36
|
+
| `DIRSQL_EMBEDDINGS_MODEL` | Model name sent in the request. |
|
|
37
|
+
| `DIRSQL_EMBEDDINGS_API_KEY` | Bearer token for `Authorization`. |
|
|
38
|
+
|
|
39
|
+
## Console scripts
|
|
40
|
+
|
|
41
|
+
| Script | Hook | Input | Output |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `dirsql-embeddings-on-file` | `on-file` | a file's absolute path (`argv[1]`) | one-line JSON row array with `path`, `text`, `embedding` |
|
|
44
|
+
| `dirsql-embeddings-pre-query` | `pre-query` | a raw request body (`argv[1]`) | nearest-neighbor SQL over `documents` |
|
|
45
|
+
|
|
46
|
+
`pre-query` accepts both a verbatim server body (`{"q": ...}`) and the CLI
|
|
47
|
+
`query` subcommand's `{"sql": <arg>}` wrapper, so `dirsql query '{"q": ...}'` and
|
|
48
|
+
a real `POST /query` both work.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
Three tiers, per the dirsql testing conventions:
|
|
53
|
+
|
|
54
|
+
- **unit** (colocated, mocked seams) — `src/dirsql_plugin_embeddings/*_test.py`
|
|
55
|
+
- **integration** (`tests/integration/`) — each console script as a real
|
|
56
|
+
subprocess against a local stub `/v1/embeddings` server
|
|
57
|
+
- **e2e** (`tests/e2e/`) — the full loop through the real launcher + `dirsql`
|
|
58
|
+
binary + `sqlite-vec`, nothing mocked but the embedding endpoint
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"command": "uv run --with sqlite-vec --with-editable . --with-editable ../../packages/python python -m pytest tests/e2e -x -q",
|
|
3
|
+
"ran_at": 1784022840,
|
|
4
|
+
"exit_code": 0,
|
|
5
|
+
"commit": "5f6caffd4996dfdf09cf798c1deb55f6e216fde5",
|
|
6
|
+
"branch": "claude/open-issues-review-le8hm5-531"
|
|
7
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# First-party dirsql plugin, published independently to PyPI: the worked
|
|
7
|
+
# implementation behind docs/howto/search-by-meaning.md, backed by an
|
|
8
|
+
# OpenAI-compatible /v1/embeddings endpoint. Exists to prove the plugin
|
|
9
|
+
# rails (#531, part of #363) and power the semantic-search demo.
|
|
10
|
+
name = "dirsql-plugin-embeddings"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
description = "First-party dirsql plugin: semantic search via an OpenAI-compatible embeddings endpoint."
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.version]
|
|
16
|
+
source = "vcs"
|
|
17
|
+
# putitoutthere tags this package independently as
|
|
18
|
+
# `dirsql-plugin-embeddings-v<version>`, distinct from the workspace
|
|
19
|
+
# `dirsql-{rust,py,npm}` tags sharing the same git history.
|
|
20
|
+
tag-pattern = "^dirsql-plugin-embeddings-v(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+)$"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.version.raw-options]
|
|
23
|
+
version_scheme = "no-guess-dev"
|
|
24
|
+
# This pyproject.toml lives two directories below the repo root (the
|
|
25
|
+
# actual .git location); without an explicit root, setuptools-scm looks
|
|
26
|
+
# for .git right next to pyproject.toml and fails outright rather than
|
|
27
|
+
# walking up.
|
|
28
|
+
root = "../.."
|
|
29
|
+
# tag-pattern above only *extracts* a version from whatever tag `git
|
|
30
|
+
# describe` returns -- it does not restrict which tags are candidates.
|
|
31
|
+
# In this monorepo `git describe` finds the globally-nearest tag (e.g.
|
|
32
|
+
# `dirsql-py-v0.3.110`), the regex correctly rejects it, and
|
|
33
|
+
# setuptools-scm treats that as a hard error rather than falling back.
|
|
34
|
+
# `--match` constrains `git describe` itself to this package's own
|
|
35
|
+
# tag prefix.
|
|
36
|
+
git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "dirsql-plugin-embeddings-v*"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
# The two command hooks the fragment invokes; installing the package puts them
|
|
40
|
+
# on PATH so `dirsql` resolves them when it spawns the hooks.
|
|
41
|
+
dirsql-embeddings-on-file = "dirsql_plugin_embeddings.on_file:main"
|
|
42
|
+
dirsql-embeddings-pre-query = "dirsql_plugin_embeddings.pre_query:main"
|
|
43
|
+
|
|
44
|
+
[project.entry-points.dirsql]
|
|
45
|
+
# How the #529 launcher discovers the plugin: group `dirsql`, value = the
|
|
46
|
+
# top-level module whose root ships `dirsql.toml`.
|
|
47
|
+
embeddings = "dirsql_plugin_embeddings"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = ["pytest>=8", "pytest-cov>=5", "pytest-describe>=2"]
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build]
|
|
53
|
+
exclude = ["**/*_test.py"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
# The whole package ships, including the non-.py `dirsql.toml` fragment
|
|
57
|
+
# (importlib.resources loads it, and it must be present in the built artifact).
|
|
58
|
+
packages = ["src/dirsql_plugin_embeddings"]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
61
|
+
"src/dirsql_plugin_embeddings/dirsql.toml" = "dirsql_plugin_embeddings/dirsql.toml"
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
# `src` layout: put it on the path so modules and their colocated tests import
|
|
65
|
+
# the package normally (`from dirsql_plugin_embeddings import ...`).
|
|
66
|
+
pythonpath = ["src"]
|
|
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,7 @@
|
|
|
1
|
+
# Dedicated testing-conventions config for the in-repo dirsql-plugin-embeddings
|
|
2
|
+
# package. Mirrors internals/checks: python coverage floor 100 + branch, zero
|
|
3
|
+
# exemptions, and deliberately no `[e2e] extra_scope` -- this package compiles
|
|
4
|
+
# none of packages/rust/src, so a core change never stales its attestation.
|
|
5
|
+
[python.coverage]
|
|
6
|
+
fail_under = 100
|
|
7
|
+
branch = true
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared fixtures for the integration and e2e tiers.
|
|
2
|
+
|
|
3
|
+
`stub_server` brings up a real, local, threaded OpenAI-compatible
|
|
4
|
+
`/v1/embeddings` endpoint. Its embedding is a deterministic keyword-count
|
|
5
|
+
vector, so nearest-neighbor search over the fixtures is reproducible without a
|
|
6
|
+
real model or network.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
# One dimension per keyword; the embedding of a text is the per-keyword count.
|
|
16
|
+
KEYWORDS = [
|
|
17
|
+
"pasta",
|
|
18
|
+
"cook",
|
|
19
|
+
"garlic",
|
|
20
|
+
"git",
|
|
21
|
+
"code",
|
|
22
|
+
"review",
|
|
23
|
+
"tomato",
|
|
24
|
+
"plant",
|
|
25
|
+
"seed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def keyword_vector(text):
|
|
30
|
+
lowered = text.lower()
|
|
31
|
+
return [float(lowered.count(keyword)) for keyword in KEYWORDS]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
35
|
+
def do_POST(self):
|
|
36
|
+
length = int(self.headers["Content-Length"])
|
|
37
|
+
payload = json.loads(self.rfile.read(length))
|
|
38
|
+
text = payload["input"][0]
|
|
39
|
+
body = json.dumps(
|
|
40
|
+
{"data": [{"index": 0, "embedding": keyword_vector(text)}]}
|
|
41
|
+
).encode("utf-8")
|
|
42
|
+
self.send_response(200)
|
|
43
|
+
self.send_header("Content-Type", "application/json")
|
|
44
|
+
self.send_header("Content-Length", str(len(body)))
|
|
45
|
+
self.end_headers()
|
|
46
|
+
self.wfile.write(body)
|
|
47
|
+
|
|
48
|
+
def log_message(self, *args): # silence the default stderr access log
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def stub_server():
|
|
54
|
+
server = HTTPServer(("127.0.0.1", 0), _Handler)
|
|
55
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
56
|
+
thread.start()
|
|
57
|
+
host, port = server.server_address
|
|
58
|
+
try:
|
|
59
|
+
yield f"http://{host}:{port}"
|
|
60
|
+
finally:
|
|
61
|
+
server.shutdown()
|
|
62
|
+
thread.join()
|
|
File without changes
|
|
File without changes
|