feat: edit qdrant workspace collections
This commit is contained in:
@@ -3,17 +3,16 @@ import type { CanonicalWorkspace } from "../api/workspaces";
|
||||
import { workspaceDeletionDrafts, workspaceDrafts, workspacePreferences } from "./drafts";
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD Clinical", language: "en" },
|
||||
workspace: { schema_version: 3, id: "psd-clinical", name: "PSD Clinical", language: "en" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "clinical", schema: "datawarehouse",
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "pgvector", database: "vectors", schema: "public", collection: "clinical",
|
||||
dimensions: 768, distance: "cosine", supported_transports: ["pgvector_direct"],
|
||||
engine: "qdrant", collection: "clinical", dimensions: 1024, distance: "cosine",
|
||||
},
|
||||
embedding: { provider: "ollama_compatible", model: "nomic-embed-text", dimensions: 768 },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
@@ -126,3 +125,24 @@ test.each([
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).toBeNull();
|
||||
});
|
||||
|
||||
test("rejects a draft that tries to persist removed external semantic configuration fields", () => {
|
||||
workspaceDrafts.save({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace: {
|
||||
...workspace,
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
...workspace.semantic_index.vector_store,
|
||||
database: "vectors",
|
||||
},
|
||||
embedding: workspace.semantic_index.embedding,
|
||||
},
|
||||
} as CanonicalWorkspace,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -200,7 +200,7 @@ function copyRequest(value: unknown, extraKeys: readonly string[] = []): RestDia
|
||||
}
|
||||
|
||||
function copyDiagnostics(value: unknown): CanonicalDiagnostics | undefined {
|
||||
const source = exactRecord(value, ["dwh_rest", "vector_rest", "embedding"]);
|
||||
const source = exactRecord(value, ["dwh_rest"]);
|
||||
if (!source) return undefined;
|
||||
const diagnostics: CanonicalDiagnostics = {};
|
||||
if (source.dwh_rest !== undefined) {
|
||||
@@ -212,41 +212,6 @@ function copyDiagnostics(value: unknown): CanonicalDiagnostics | undefined {
|
||||
if (!request || !database || !schema) return undefined;
|
||||
diagnostics.dwh_rest = { ...request, response: { database, schema } };
|
||||
}
|
||||
if (source.vector_rest !== undefined) {
|
||||
const vector = exactRecord(source.vector_rest, ["metadata", "reversible_probe"]);
|
||||
const request = copyRequest(vector?.metadata, ["response"]);
|
||||
const rawMetadata = exactRecord(vector?.metadata, ["method", "path", "auth", "response"]);
|
||||
const response = exactRecord(rawMetadata?.response, ["collection", "dimensions", "distance"]);
|
||||
const collection = identifier(response?.collection);
|
||||
const dimensions = identifier(response?.dimensions);
|
||||
const distance = identifier(response?.distance);
|
||||
if (!vector || !request || !collection || !dimensions || !distance) return undefined;
|
||||
const metadata = { ...request, response: { collection, dimensions, distance } };
|
||||
let reversibleProbe: NonNullable<CanonicalDiagnostics["vector_rest"]>["reversible_probe"] | undefined;
|
||||
if (vector.reversible_probe !== undefined) {
|
||||
const probe = copyRequest(vector.reversible_probe, ["response"]);
|
||||
const rawProbe = exactRecord(vector.reversible_probe, ["method", "path", "auth", "response"]);
|
||||
const probeResponse = exactRecord(rawProbe?.response, ["operation"]);
|
||||
const operation = identifier(probeResponse?.operation);
|
||||
if (!probe || probe.method !== "POST" || probe.auth === "none" || !operation) return undefined;
|
||||
reversibleProbe = {
|
||||
method: "POST",
|
||||
path: probe.path,
|
||||
auth: probe.auth as "bearer" | "x-api-key",
|
||||
response: { operation },
|
||||
};
|
||||
}
|
||||
diagnostics.vector_rest = { metadata, ...(reversibleProbe ? { reversible_probe: reversibleProbe } : {}) };
|
||||
}
|
||||
if (source.embedding !== undefined) {
|
||||
const request = copyRequest(source.embedding, ["response"]);
|
||||
const raw = exactRecord(source.embedding, ["method", "path", "auth", "response"]);
|
||||
const response = exactRecord(raw?.response, ["model", "dimensions"]);
|
||||
const model = identifier(response?.model);
|
||||
const dimensions = identifier(response?.dimensions);
|
||||
if (!request || !model || !dimensions) return undefined;
|
||||
diagnostics.embedding = { ...request, response: { model, dimensions } };
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
@@ -255,9 +220,9 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
|
||||
const source = exactRecord(value, ["workspace", "dwh", "semantic_index", "llm_policy", "diagnostics"]);
|
||||
const metadata = exactRecord(source?.workspace, ["schema_version", "id", "name", "description", "language"]);
|
||||
const dwh = exactRecord(source?.dwh, ["engine", "database", "schema", "port", "timeout_ms", "supported_transports"]);
|
||||
const semanticIndex = exactRecord(source?.semantic_index, ["vector_store", "vector_writer", "embedding"]);
|
||||
const vectorStore = exactRecord(semanticIndex?.vector_store, ["engine", "database", "schema", "collection", "dimensions", "distance", "port", "timeout_ms", "supported_transports"]);
|
||||
const embedding = exactRecord(semanticIndex?.embedding, ["provider", "model", "dimensions", "timeout_ms"]);
|
||||
const semanticIndex = exactRecord(source?.semantic_index, ["vector_store", "embedding"]);
|
||||
const vectorStore = exactRecord(semanticIndex?.vector_store, ["engine", "collection", "dimensions", "distance"]);
|
||||
const embedding = exactRecord(semanticIndex?.embedding, ["provider", "model", "dimensions"]);
|
||||
const policy = exactRecord(source?.llm_policy, ["default", "allowed"]);
|
||||
const diagnostics = source?.diagnostics === undefined ? undefined : copyDiagnostics(source.diagnostics);
|
||||
if (!metadata || !dwh || !semanticIndex || !vectorStore || !embedding || !policy) return undefined;
|
||||
@@ -270,34 +235,28 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
|
||||
const dwhPort = dwh.port === undefined ? undefined : positiveInteger(dwh.port, 65_535);
|
||||
const dwhTimeout = dwh.timeout_ms === undefined ? undefined : positiveInteger(dwh.timeout_ms);
|
||||
const dwhTransports = uniqueChoices(dwh.supported_transports, ["postgres_direct", "rest_api", "ssh_tunnel"] as const);
|
||||
const vectorDatabase = identifier(vectorStore.database);
|
||||
const vectorSchema = identifier(vectorStore.schema);
|
||||
const collection = identifier(vectorStore.collection);
|
||||
const vectorDimensions = positiveInteger(vectorStore.dimensions, 32_768);
|
||||
const distance = oneOf(vectorStore.distance, ["cosine", "l2", "inner_product"] as const);
|
||||
const vectorPort = vectorStore.port === undefined ? undefined : positiveInteger(vectorStore.port, 65_535);
|
||||
const vectorTimeout = vectorStore.timeout_ms === undefined ? undefined : positiveInteger(vectorStore.timeout_ms);
|
||||
const vectorTransports = uniqueChoices(vectorStore.supported_transports, ["pgvector_direct", "rest_api", "ssh_tunnel"] as const);
|
||||
const embeddingProvider = oneOf(embedding.provider, ["ollama_compatible", "openai_compatible"] as const);
|
||||
const distance = oneOf(vectorStore.distance, ["cosine"] as const);
|
||||
const embeddingProvider = oneOf(embedding.provider, ["ollama_internal"] as const);
|
||||
const embeddingModel = text(embedding.model);
|
||||
const embeddingDimensions = positiveInteger(embedding.dimensions, 32_768);
|
||||
const embeddingTimeout = embedding.timeout_ms === undefined ? undefined : positiveInteger(embedding.timeout_ms);
|
||||
const allowedModels = uniqueModels(policy.allowed);
|
||||
const defaultModel = policy.default === undefined ? undefined : modelReference(policy.default);
|
||||
if (
|
||||
metadata.schema_version !== 2 || !id || !name || !language || (metadata.description !== undefined && !description)
|
||||
metadata.schema_version !== 3 || !id || !name || !language || (metadata.description !== undefined && !description)
|
||||
|| dwh.engine !== "postgres" || !database || !schema || (dwh.port !== undefined && !dwhPort) || (dwh.timeout_ms !== undefined && !dwhTimeout) || !dwhTransports
|
||||
|| vectorStore.engine !== "pgvector" || !vectorDatabase || !vectorSchema || !collection || !vectorDimensions || !distance || (vectorStore.port !== undefined && !vectorPort) || (vectorStore.timeout_ms !== undefined && !vectorTimeout) || !vectorTransports
|
||||
|| !embeddingProvider || !embeddingModel || !embeddingDimensions || (embedding.timeout_ms !== undefined && !embeddingTimeout) || !allowedModels
|
||||
|| vectorStore.engine !== "qdrant" || !collection || !vectorDimensions || !distance
|
||||
|| !embeddingProvider || !embeddingModel || !embeddingDimensions || !allowedModels
|
||||
|| (defaultModel !== undefined && !allowedModels.includes(defaultModel)) || vectorDimensions !== embeddingDimensions
|
||||
|| (semanticIndex.vector_writer !== undefined && !exactRecord(semanticIndex.vector_writer, []))
|
||||
|| vectorDimensions !== 1024 || embeddingDimensions !== 1024
|
||||
|| embeddingModel !== "qwen3-embedding:0.6b"
|
||||
) return undefined;
|
||||
if (source?.diagnostics !== undefined && !diagnostics) return undefined;
|
||||
if (diagnostics?.dwh_rest && !dwhTransports.includes("rest_api")) return undefined;
|
||||
if (diagnostics?.vector_rest && !vectorTransports.includes("rest_api")) return undefined;
|
||||
return {
|
||||
workspace: {
|
||||
schema_version: 2,
|
||||
schema_version: 3,
|
||||
id,
|
||||
name,
|
||||
...(description ? { description } : {}),
|
||||
@@ -311,15 +270,10 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "pgvector", database: vectorDatabase, schema: vectorSchema, collection, dimensions: vectorDimensions, distance,
|
||||
...(vectorPort ? { port: vectorPort } : {}),
|
||||
...(vectorTimeout ? { timeout_ms: vectorTimeout } : {}),
|
||||
supported_transports: vectorTransports,
|
||||
engine: "qdrant", collection, dimensions: 1024, distance: "cosine",
|
||||
},
|
||||
...(semanticIndex.vector_writer ? { vector_writer: {} } : {}),
|
||||
embedding: {
|
||||
provider: embeddingProvider, model: embeddingModel, dimensions: embeddingDimensions,
|
||||
...(embeddingTimeout ? { timeout_ms: embeddingTimeout } : {}),
|
||||
provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024,
|
||||
},
|
||||
},
|
||||
llm_policy: {
|
||||
|
||||
Reference in New Issue
Block a user