Files
ThothII/scripts/internal-semantic-smoke.sh

405 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Live smoke for the mandatory internal semantic stack: disposable project/volumes, no host
# ports on semantic services, exact cleanup via Task 13 labels only, and offline persistence.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
# shellcheck source=./unified-deployment-smoke.sh
source "$root/scripts/unified-deployment-smoke.sh"
task13_write_offline_semantic_override() {
TASK13_OFFLINE_OVERRIDE="$TASK13_TMP/compose.task13.offline-semantic.yaml"
cat >"$TASK13_OFFLINE_OVERRIDE" <<'EOF'
networks:
thothii:
internal: true
EOF
chmod 0600 "$TASK13_OFFLINE_OVERRIDE"
}
task13_offline_semantic_compose() {
docker compose \
--project-name "$TASK13_PROJECT" \
--project-directory "$TASK13_ROOT" \
--env-file "$TASK13_ENV_FILE" \
-f "$TASK13_ROOT/compose.yaml" \
-f "$TASK13_ROOT/deploy/compose.local.yaml" \
-f "$TASK13_OVERRIDE" \
-f "$TASK13_OFFLINE_OVERRIDE" \
"$@"
}
task13_offline_semantic_compose_logged() {
local label="$1"
shift
task13_run_logged "$label" task13_offline_semantic_compose "$@"
}
task13_wait_internal_embedding_model() {
printf '== Wait for the internal embedding model ==\n'
for _attempt in $(seq 1 30); do
if task13_compose exec -T core /opt/venv/bin/python - <<'PY' >>"$TASK13_LOG" 2>&1
import json
import urllib.request
import urllib.error
with urllib.request.urlopen("http://embedding:11434/api/tags", timeout=10) as response:
payload = json.load(response)
models = [entry.get("name") for entry in payload.get("models", []) if isinstance(entry, dict)]
if "qwen3-embedding:0.6b" not in models:
raise SystemExit(1)
request = urllib.request.Request(
"http://embedding:11434/api/embed",
data=json.dumps({"model": "qwen3-embedding:0.6b", "input": ["warm semantic smoke"]}).encode("utf-8"),
headers={"content-type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=180) as response:
payload = json.load(response)
except urllib.error.URLError:
raise SystemExit(1)
embeddings = payload.get("embeddings")
raise SystemExit(0 if isinstance(embeddings, list) and len(embeddings) == 1 and len(embeddings[0]) == 1024 else 1)
PY
then
return 0
fi
sleep 1
done
task13_log_failure "internal embedding model readiness"
}
task13_wait_internal_embedding_model_offline() {
printf '== Wait for the internal embedding model ==\n'
for _attempt in $(seq 1 30); do
if docker run --rm -i --pull never \
--label "io.thothii.task13.run=$TASK13_RUN_ID" \
--network "$TASK13_NETWORK" \
--entrypoint /opt/venv/bin/python \
"$TASK13_CORE_IMAGE" - <<'PY' >>"$TASK13_LOG" 2>&1
import json
import urllib.request
import urllib.error
with urllib.request.urlopen("http://embedding:11434/api/tags", timeout=10) as response:
payload = json.load(response)
models = [entry.get("name") for entry in payload.get("models", []) if isinstance(entry, dict)]
if "qwen3-embedding:0.6b" not in models:
raise SystemExit(1)
request = urllib.request.Request(
"http://embedding:11434/api/embed",
data=json.dumps({"model": "qwen3-embedding:0.6b", "input": ["warm semantic smoke"]}).encode("utf-8"),
headers={"content-type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=180) as response:
payload = json.load(response)
except urllib.error.URLError:
raise SystemExit(1)
embeddings = payload.get("embeddings")
raise SystemExit(0 if isinstance(embeddings, list) and len(embeddings) == 1 and len(embeddings[0]) == 1024 else 1)
PY
then
return 0
fi
sleep 1
done
task13_log_failure "offline internal embedding model readiness"
}
task13_semantic_python_probe_with() {
local mode="$1"
shift
"$@" "$mode" <<'PY'
from __future__ import annotations
import hashlib
import json
import sys
from types import SimpleNamespace
import requests
from tht.adapters.vector.qdrant import QdrantVectorStore
from tht.ports.vector import VectorWriteRecord
from tht.vectorstore.embeddings import OllamaEmbeddings
from tht.vectorstore.records import VectorRecord
MODE = sys.argv[1]
WORKSPACE_ID = "task13-smoke"
WORKSPACE_REVISION = "b" * 40
COLLECTION = "task13-smoke"
QDRANT = "http://qdrant:6333"
EMBEDDING = "http://embedding:11434"
MODEL = "qwen3-embedding:0.6b"
DIM = 1024
def embedder() -> OllamaEmbeddings:
return OllamaEmbeddings(
SimpleNamespace(
base_url=EMBEDDING,
model=MODEL,
dim=DIM,
connect_timeout=2.0,
timeout=180.0,
batch_size=8,
)
)
def store() -> QdrantVectorStore:
return QdrantVectorStore(
base_url=QDRANT,
collection=COLLECTION,
workspace_id=WORKSPACE_ID,
workspace_revision=WORKSPACE_REVISION,
expected_dimension=DIM,
)
def content_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def query_payload(vector: list[float], must: list[dict]) -> dict:
response = requests.post(
f"{QDRANT}/collections/{COLLECTION}/points/query",
json={
"vector": vector,
"limit": 1,
"with_payload": True,
"filter": {"must": must},
},
timeout=(2.0, 15.0),
)
response.raise_for_status()
payload = response.json()
points = payload.get("result", {}).get("points")
if not isinstance(points, list) or len(points) != 1:
raise RuntimeError(f"expected exactly one semantic point, got {payload!r}")
point = points[0]
result = point.get("payload")
if not isinstance(result, dict):
raise RuntimeError(f"missing payload in query result: {point!r}")
return result
records = {
"schema": {
"collection": "schema_records",
"record": VectorRecord(
id="schema_table:fact_task13",
kind="schema_table",
ref="fact_task13",
title="fact_task13",
content="Tabella fact_task13 con una riga dedicata allo smoke semantico interno.",
metadata={"table_name": "fact_task13"},
),
"query": "fact task13 smoke table",
"must": [
{"key": "workspace_id", "match": {"value": WORKSPACE_ID}},
{"key": "kind", "match": {"value": "schema"}},
{"key": "record_kind", "match": {"value": "schema_table"}},
{"key": "record_key", "match": {"value": "schema_table:fact_task13"}},
],
},
"evidence": {
"collection": "evidence",
"record": VectorRecord(
id="evidence:task13-doc:0",
kind="evidence",
ref="task13-doc",
title="Task 13 Evidence",
content="Evidence dedicata allo smoke semantico interno con filtro esatto per generazione.",
metadata={
"document_id": "task13-doc",
"vector_generation": "gen:11111111111111111111111111111111",
"status": "published",
"tier": "gold",
"tables": ["fact_task13"],
"concepts": ["semantic smoke"],
},
),
"query": "semantic smoke evidence generation",
"must": [
{"key": "workspace_id", "match": {"value": WORKSPACE_ID}},
{"key": "kind", "match": {"value": "evidence"}},
{"key": "document_id", "match": {"value": "task13-doc"}},
{"key": "vector_generation", "match": {"value": "gen:11111111111111111111111111111111"}},
],
},
"memory": {
"collection": "memory",
"record": VectorRecord(
id="mem-9000",
kind="memory",
ref="mem-9000",
title="Task 13 Memory",
content="Memoria riusabile per lo smoke semantico interno persistente.",
metadata={
"session_id": "task13-session",
"decision_seq": 9,
"subject": "semantic smoke memory",
"type": "concept_clarified",
"concepts": ["semantic smoke memory"],
},
),
"query": "semantic smoke memory reusable",
"must": [
{"key": "workspace_id", "match": {"value": WORKSPACE_ID}},
{"key": "kind", "match": {"value": "memory"}},
{"key": "record_kind", "match": {"value": "memory"}},
{"key": "record_key", "match": {"value": "mem-9000"}},
],
},
}
embedding_client = embedder()
vector_store = store()
if MODE == "seed":
for family in records.values():
record = family["record"]
vector_store.upsert(
family["collection"],
[
VectorWriteRecord(
record=record,
embedding=embedding_client.embed_query(record.content),
content_hash=content_hash(record.content),
)
],
)
collection_info = requests.get(
f"{QDRANT}/collections/{COLLECTION}",
timeout=(2.0, 15.0),
)
collection_info.raise_for_status()
payload = collection_info.json()
size = payload.get("result", {}).get("config", {}).get("params", {}).get("vectors", {}).get("size")
distance = payload.get("result", {}).get("config", {}).get("params", {}).get("vectors", {}).get("distance")
if size != DIM or distance != "Cosine":
raise RuntimeError(f"unexpected Qdrant collection shape: size={size!r} distance={distance!r}")
verified: dict[str, dict[str, str]] = {}
for family_name, family in records.items():
payload = query_payload(
embedding_client.embed_query(family["query"]),
family["must"],
)
if payload.get("workspace_id") != WORKSPACE_ID:
raise RuntimeError(f"{family_name} query leaked another workspace")
if payload.get("record_key") != family["record"].id:
raise RuntimeError(f"{family_name} query returned the wrong record key: {payload!r}")
verified[family_name] = {
"record_key": payload["record_key"],
"kind": payload["kind"],
}
tags = requests.get(f"{EMBEDDING}/api/tags", timeout=(2.0, 15.0))
tags.raise_for_status()
models = [entry.get("name") for entry in tags.json().get("models", []) if isinstance(entry, dict)]
if MODEL not in models:
raise RuntimeError(f"missing cached embedding model {MODEL}")
print(json.dumps({
"mode": MODE,
"collection": COLLECTION,
"model": MODEL,
"verified": verified,
}, sort_keys=True))
PY
}
task13_semantic_python_probe() {
local mode="$1"
task13_semantic_python_probe_with "$mode" task13_compose exec -T core /opt/venv/bin/python -
}
task13_semantic_python_probe_offline() {
local mode="$1"
task13_semantic_python_probe_with "$mode" docker run --rm -i --pull never \
--label "io.thothii.task13.run=$TASK13_RUN_ID" \
--network "$TASK13_NETWORK" \
--entrypoint /opt/venv/bin/python \
"$TASK13_CORE_IMAGE" -
}
task13_semantic_seed_and_assert() {
local output
printf '== Ensure the semantic collection and seed schema/evidence/memory ==\n'
output="$(task13_semantic_python_probe seed)"
printf '%s\n' "$output" >>"$TASK13_LOG"
grep -Fq '"schema"' <<<"$output" || task13_fail "schema semantic verification did not run"
grep -Fq '"evidence"' <<<"$output" || task13_fail "evidence semantic verification did not run"
grep -Fq '"memory"' <<<"$output" || task13_fail "memory semantic verification did not run"
}
task13_assert_offline_semantic_isolation() {
local running
[[ "$(docker network inspect --format '{{.Internal}}' "$TASK13_NETWORK")" == true ]] \
|| task13_fail "offline semantic network still allows egress"
running="$(task13_offline_semantic_compose ps --services --status running | sort)"
[[ "$running" == $'embedding\nqdrant' ]] \
|| task13_fail "offline semantic recreation started non-semantic services"
for service in core frontend embedding-model-init; do
if docker ps -a --filter "label=com.docker.compose.project=$TASK13_PROJECT" \
--filter "label=com.docker.compose.service=$service" --format '{{.ID}}' | grep -q .; then
task13_fail "offline semantic recreation invoked bootstrap service $service"
fi
done
}
task13_semantic_verify_persistence() {
local output
printf '== Restart offline and prove semantic points plus model cache persist ==\n'
task13_write_environment /fixtures/offline.git
task13_write_offline_semantic_override
task13_remove_labeled_container "${TASK13_LLM_CONTAINER:-}" >>"$TASK13_LOG" 2>&1 \
|| task13_fail "offline semantic phase could not remove the temporary LLM fixture"
task13_compose_logged "offline semantic stop" down --remove-orphans --timeout 10
task13_offline_semantic_compose_logged "offline semantic recreation" \
up --detach --wait --wait-timeout 120 --pull never qdrant embedding
TASK13_NETWORK="$(docker network ls \
--filter "label=com.docker.compose.project=$TASK13_PROJECT" \
--filter 'label=com.docker.compose.network=thothii' --format '{{.Name}}')"
[[ -n "$TASK13_NETWORK" && "$TASK13_NETWORK" != *$'\n'* ]] || task13_fail "offline semantic network was not resolved"
task13_assert_offline_semantic_isolation
task13_wait_internal_embedding_model_offline
output="$(task13_semantic_python_probe_offline verify)"
printf '%s\n' "$output" >>"$TASK13_LOG"
grep -Fq '"schema"' <<<"$output" || task13_fail "schema semantic persistence did not verify"
grep -Fq '"evidence"' <<<"$output" || task13_fail "evidence semantic persistence did not verify"
grep -Fq '"memory"' <<<"$output" || task13_fail "memory semantic persistence did not verify"
}
task13_internal_semantic_smoke_main() {
local qdrant_before embedding_before
task13_initialize
task13_require_tools
task13_write_fixture_files
task13_write_environment /fixtures/remote.git
task13_seed_registry
task13_start_stack
task13_assert_project_ownership
task13_assert_built_image_ownership
task13_wait_internal_embedding_model
qdrant_before="$(task13_service_mount_fingerprint qdrant)"
embedding_before="$(task13_service_mount_fingerprint embedding)"
task13_semantic_seed_and_assert
task13_semantic_verify_persistence
[[ "$(task13_service_mount_fingerprint qdrant)" == "$qdrant_before" ]] \
|| task13_fail "offline recreation changed qdrant volume identity"
[[ "$(task13_service_mount_fingerprint embedding)" == "$embedding_before" ]] \
|| task13_fail "offline recreation changed embedding model cache volume identity"
printf 'Task 13 internal semantic smoke passed.\n'
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
task13_supervise "$TASK13_SMOKE_TIMEOUT" "internal semantic smoke" task13_internal_semantic_smoke_main
fi