pi-harness-runtime 1.1.411 → 1.1.413
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.
- package/harness/notification-settings.ts +130 -0
- package/harness/skill-remote-source.ts +157 -0
- package/harness/skill-sync.ts +74 -22
- package/harness/skills-startup.ts +229 -0
- package/harness/startup-report.ts +98 -56
- package/harness-startup-status.ts +31 -2
- package/index.ts +125 -31
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/dist/client.d.ts +10 -0
- package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/client.js +41 -9
- package/packages/qdrant-skills/dist/client.js.map +1 -1
- package/packages/qdrant-skills/dist/embedder.d.ts +10 -0
- package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/embedder.js +12 -2
- package/packages/qdrant-skills/dist/embedder.js.map +1 -1
- package/packages/qdrant-skills/dist/index.d.ts +2 -2
- package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/index.js +2 -2
- package/packages/qdrant-skills/dist/index.js.map +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/qdrant-skills/src/client.ts +38 -9
- package/packages/qdrant-skills/src/embedder.ts +14 -2
- package/packages/qdrant-skills/src/index.ts +3 -0
- package/packages/qdrant-skills/test/collection-info.test.ts +165 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/index.d.ts +14 -1
- package/packages/skills/dist/index.d.ts.map +1 -1
- package/packages/skills/dist/index.js +3 -1
- package/packages/skills/dist/index.js.map +1 -1
- package/packages/skills/dist/loader.d.ts +209 -0
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +386 -15
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/dist/remote-source.d.ts +107 -0
- package/packages/skills/dist/remote-source.d.ts.map +1 -0
- package/packages/skills/dist/remote-source.js +79 -0
- package/packages/skills/dist/remote-source.js.map +1 -0
- package/packages/skills/dist/vector-schema.d.ts +58 -0
- package/packages/skills/dist/vector-schema.d.ts.map +1 -0
- package/packages/skills/dist/vector-schema.js +55 -0
- package/packages/skills/dist/vector-schema.js.map +1 -0
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/index.ts +16 -2
- package/packages/skills/src/loader.ts +532 -39
- package/packages/skills/src/remote-source.ts +148 -0
- package/packages/skills/src/vector-schema.ts +72 -0
- package/packages/skills/test/remote-skills.test.ts +292 -0
- package/packages/skills/test/remote-source.test.ts +248 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/sync-to-qdrant.ts +79 -14
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skills that live on the server.
|
|
3
|
+
*
|
|
4
|
+
* The vector store is an index - it says WHICH skill fits a question. The
|
|
5
|
+
* service holds the authoritative files, so a machine without a local copy
|
|
6
|
+
* can still use a skill.
|
|
7
|
+
*
|
|
8
|
+
* The states must stay apart: a service that refused the call needs a
|
|
9
|
+
* different fix than a service that could not be reached, and both differ
|
|
10
|
+
* from a service that holds nothing.
|
|
11
|
+
*
|
|
12
|
+
* The source is injected, so the test neither reaches the network nor
|
|
13
|
+
* replaces a process-global module.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from "bun:test";
|
|
17
|
+
|
|
18
|
+
import { createSkillRegistry } from "../src/loader.js";
|
|
19
|
+
import type { RemoteSkill, RemoteSkillSource } from "../src/remote-source.js";
|
|
20
|
+
|
|
21
|
+
/** A source that holds the given skills. */
|
|
22
|
+
class FakeSource implements RemoteSkillSource {
|
|
23
|
+
/** The skills the source holds. */
|
|
24
|
+
readonly skills: RemoteSkill[];
|
|
25
|
+
/** The ids that were asked for. */
|
|
26
|
+
readonly requested: string[] = [];
|
|
27
|
+
/** The error the next call raises. */
|
|
28
|
+
failure: { message: string; code: "denied" | "unavailable" } | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(skills: RemoteSkill[] = []) {
|
|
31
|
+
this.skills = skills;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async list(): Promise<RemoteSkill[]> {
|
|
35
|
+
if (this.failure) {
|
|
36
|
+
const error = new Error(this.failure.message) as Error & {
|
|
37
|
+
code: string;
|
|
38
|
+
};
|
|
39
|
+
error.code = this.failure.code;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
return this.skills;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get(id: string): Promise<RemoteSkill | null> {
|
|
46
|
+
this.requested.push(id);
|
|
47
|
+
if (this.failure) {
|
|
48
|
+
const error = new Error(this.failure.message) as Error & {
|
|
49
|
+
code: string;
|
|
50
|
+
};
|
|
51
|
+
error.code = this.failure.code;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return this.skills.find((skill) => skill.id === id) ?? null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("the skills of the server", () => {
|
|
59
|
+
it("are listed by the registry", async () => {
|
|
60
|
+
const source = new FakeSource([
|
|
61
|
+
{
|
|
62
|
+
id: "shared-skill",
|
|
63
|
+
name: "shared-skill",
|
|
64
|
+
description: "the shared skill",
|
|
65
|
+
version: "1.2.0",
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
const registry = createSkillRegistry();
|
|
69
|
+
registry.setRemoteSource(source);
|
|
70
|
+
|
|
71
|
+
const stats = await registry.refreshRemoteSkills();
|
|
72
|
+
|
|
73
|
+
expect(stats.state).toBe("populated");
|
|
74
|
+
expect(stats.count).toBe(1);
|
|
75
|
+
expect(registry.listRemote().map((skill) => skill.id)).toEqual(["shared-skill"]);
|
|
76
|
+
// The local count stays local.
|
|
77
|
+
expect(registry.list()).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("are not local skills", async () => {
|
|
81
|
+
const source = new FakeSource([
|
|
82
|
+
{ id: "shared-skill", name: "shared-skill", description: "d", version: "1.0.0" },
|
|
83
|
+
]);
|
|
84
|
+
const registry = createSkillRegistry();
|
|
85
|
+
registry.setRemoteSource(source);
|
|
86
|
+
await registry.refreshRemoteSkills();
|
|
87
|
+
|
|
88
|
+
expect(registry.list()).toHaveLength(0);
|
|
89
|
+
expect(registry.listRemote()).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("replace the cached skill when the version changed", async () => {
|
|
93
|
+
const source = new FakeSource([
|
|
94
|
+
{ id: "shared-skill", name: "shared-skill", description: "old", version: "1.0.0" },
|
|
95
|
+
]);
|
|
96
|
+
const registry = createSkillRegistry();
|
|
97
|
+
registry.setRemoteSource(source);
|
|
98
|
+
await registry.refreshRemoteSkills();
|
|
99
|
+
expect(registry.listRemote()[0].frontmatter.description).toBe("old");
|
|
100
|
+
|
|
101
|
+
// The server publishes a new version.
|
|
102
|
+
source.skills[0] = {
|
|
103
|
+
id: "shared-skill",
|
|
104
|
+
name: "shared-skill",
|
|
105
|
+
description: "new",
|
|
106
|
+
version: "1.1.0",
|
|
107
|
+
};
|
|
108
|
+
await registry.refreshRemoteSkills();
|
|
109
|
+
|
|
110
|
+
expect(registry.listRemote()[0].frontmatter.description).toBe("new");
|
|
111
|
+
expect(registry.listRemote()[0].frontmatter.version).toBe("1.1.0");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("stay when the version is unknown", async () => {
|
|
115
|
+
const source = new FakeSource([
|
|
116
|
+
{ id: "shared-skill", name: "shared-skill", description: "known", version: "1.0.0" },
|
|
117
|
+
]);
|
|
118
|
+
const registry = createSkillRegistry();
|
|
119
|
+
registry.setRemoteSource(source);
|
|
120
|
+
await registry.refreshRemoteSkills();
|
|
121
|
+
|
|
122
|
+
// A record without a version is not evidence of a change.
|
|
123
|
+
source.skills[0] = {
|
|
124
|
+
id: "shared-skill",
|
|
125
|
+
name: "shared-skill",
|
|
126
|
+
description: "changed without a version",
|
|
127
|
+
};
|
|
128
|
+
await registry.refreshRemoteSkills();
|
|
129
|
+
|
|
130
|
+
expect(registry.listRemote()[0].frontmatter.description).toBe("known");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("are loaded completely from the service", async () => {
|
|
134
|
+
const source = new FakeSource([
|
|
135
|
+
{
|
|
136
|
+
id: "shared-skill",
|
|
137
|
+
name: "shared-skill",
|
|
138
|
+
description: "the shared skill",
|
|
139
|
+
version: "1.0.0",
|
|
140
|
+
content: "the complete body of the skill",
|
|
141
|
+
contentComplete: true,
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
const registry = createSkillRegistry();
|
|
145
|
+
registry.setRemoteSource(source);
|
|
146
|
+
|
|
147
|
+
const loaded = await registry.load("shared-skill");
|
|
148
|
+
|
|
149
|
+
expect(loaded).not.toBeNull();
|
|
150
|
+
expect(loaded?.body).toBe("the complete body of the skill");
|
|
151
|
+
expect(source.requested).toContain("shared-skill");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("are not asked for again once the content is complete", async () => {
|
|
155
|
+
const source = new FakeSource([
|
|
156
|
+
{
|
|
157
|
+
id: "shared-skill",
|
|
158
|
+
name: "shared-skill",
|
|
159
|
+
description: "d",
|
|
160
|
+
version: "1.0.0",
|
|
161
|
+
content: "the body",
|
|
162
|
+
contentComplete: true,
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
const registry = createSkillRegistry();
|
|
166
|
+
registry.setRemoteSource(source);
|
|
167
|
+
|
|
168
|
+
await registry.load("shared-skill");
|
|
169
|
+
const afterFirst = source.requested.length;
|
|
170
|
+
await registry.load("shared-skill");
|
|
171
|
+
|
|
172
|
+
expect(source.requested.length).toBe(afterFirst);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("are reported as empty when the service holds nothing", async () => {
|
|
176
|
+
const registry = createSkillRegistry();
|
|
177
|
+
registry.setRemoteSource(new FakeSource([]));
|
|
178
|
+
|
|
179
|
+
const stats = await registry.refreshRemoteSkills();
|
|
180
|
+
|
|
181
|
+
expect(stats.state).toBe("empty");
|
|
182
|
+
expect(stats.count).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("are reported as not configured without a service", async () => {
|
|
186
|
+
const registry = createSkillRegistry();
|
|
187
|
+
|
|
188
|
+
const stats = await registry.getRemoteStats();
|
|
189
|
+
|
|
190
|
+
expect(stats.state).toBe("not-configured");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("a service that does not answer", () => {
|
|
195
|
+
it("reports a refusal as denied", async () => {
|
|
196
|
+
const source = new FakeSource();
|
|
197
|
+
source.failure = { message: "unauthorized", code: "denied" };
|
|
198
|
+
const registry = createSkillRegistry();
|
|
199
|
+
registry.setRemoteSource(source);
|
|
200
|
+
|
|
201
|
+
const stats = await registry.refreshRemoteSkills();
|
|
202
|
+
|
|
203
|
+
expect(stats.state).toBe("denied");
|
|
204
|
+
expect(stats.error).toContain("unauthorized");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("reports an unreachable service as unavailable", async () => {
|
|
208
|
+
const source = new FakeSource();
|
|
209
|
+
source.failure = { message: "connection refused", code: "unavailable" };
|
|
210
|
+
const registry = createSkillRegistry();
|
|
211
|
+
registry.setRemoteSource(source);
|
|
212
|
+
|
|
213
|
+
const stats = await registry.refreshRemoteSkills();
|
|
214
|
+
|
|
215
|
+
expect(stats.state).toBe("unavailable");
|
|
216
|
+
expect(stats.error).toContain("connection refused");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("keeps the cached skills after a failed refresh", async () => {
|
|
220
|
+
const source = new FakeSource([
|
|
221
|
+
{ id: "shared-skill", name: "shared-skill", description: "d", version: "1.0.0" },
|
|
222
|
+
]);
|
|
223
|
+
const registry = createSkillRegistry();
|
|
224
|
+
registry.setRemoteSource(source);
|
|
225
|
+
await registry.refreshRemoteSkills();
|
|
226
|
+
|
|
227
|
+
source.failure = { message: "connection refused", code: "unavailable" };
|
|
228
|
+
const stats = await registry.refreshRemoteSkills();
|
|
229
|
+
|
|
230
|
+
expect(stats.state).toBe("unavailable");
|
|
231
|
+
// A failed refresh must not throw the cached skills away.
|
|
232
|
+
expect(registry.listRemote()).toHaveLength(1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("does not put a credential into the message", async () => {
|
|
236
|
+
const source = new FakeSource();
|
|
237
|
+
source.failure = {
|
|
238
|
+
message: "Bearer sk-abcdefghijklmnopqrstuvwxyz0123456789 refused",
|
|
239
|
+
code: "denied",
|
|
240
|
+
};
|
|
241
|
+
const registry = createSkillRegistry();
|
|
242
|
+
registry.setRemoteSource(source);
|
|
243
|
+
|
|
244
|
+
const stats = await registry.refreshRemoteSkills();
|
|
245
|
+
|
|
246
|
+
expect(stats.error).not.toContain("sk-abcdefghijklmnopqrstuvwxyz0123456789");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/two-phase-review",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.413",
|
|
4
4
|
"description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,6 +14,21 @@
|
|
|
14
14
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
15
15
|
import { join, basename } from "node:path";
|
|
16
16
|
import { QdrantClient } from "@qdrant/js-client-rest";
|
|
17
|
+
// The runtime and this script must write into the SAME collection with the
|
|
18
|
+
// SAME vectors: a collection holds vectors of exactly one size, and a vector
|
|
19
|
+
// of a different size cannot be searched with the vectors of the other one.
|
|
20
|
+
// The embedder and the collection name are therefore imported from the
|
|
21
|
+
// packages that own them - they are not repeated here.
|
|
22
|
+
import {
|
|
23
|
+
EMBEDDING_DIMENSIONS,
|
|
24
|
+
EMBEDDING_MODEL,
|
|
25
|
+
createEmbedding,
|
|
26
|
+
createEmbeddingsBatch,
|
|
27
|
+
} from "../packages/qdrant-skills/dist/index.js";
|
|
28
|
+
import {
|
|
29
|
+
LEGACY_SKILL_COLLECTION,
|
|
30
|
+
SKILL_COLLECTION,
|
|
31
|
+
} from "../packages/skills/dist/vector-schema.js";
|
|
17
32
|
|
|
18
33
|
// =============================================================================
|
|
19
34
|
// SANITIZATION: Replace client-specific names with placeholders
|
|
@@ -81,17 +96,17 @@ interface QdrantConfig {
|
|
|
81
96
|
function getQdrantConfig(): QdrantConfig | null {
|
|
82
97
|
let apiKey = process.env.QDRANT_API_KEY;
|
|
83
98
|
const url = process.env.QDRANT_CLUSTER_ENDPOINT || "https://api.qdrant.tech";
|
|
84
|
-
const collection = process.env.QDRANT_COLLECTION ||
|
|
85
|
-
|
|
99
|
+
const collection = process.env.QDRANT_COLLECTION || SKILL_COLLECTION;
|
|
100
|
+
|
|
86
101
|
if (!apiKey) {
|
|
87
102
|
const keyPath = `${process.env.HOME}/.pi-harness-runtime/keys/qdrant-api-key.txt`;
|
|
88
103
|
if (existsSync(keyPath)) {
|
|
89
104
|
apiKey = readFileSync(keyPath, "utf-8").trim();
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
|
-
|
|
107
|
+
|
|
93
108
|
if (!apiKey) return null;
|
|
94
|
-
|
|
109
|
+
|
|
95
110
|
return { url, apiKey, collection };
|
|
96
111
|
}
|
|
97
112
|
|
|
@@ -180,15 +195,58 @@ function findSkillDirs(dir: string): string[] {
|
|
|
180
195
|
// =============================================================================
|
|
181
196
|
|
|
182
197
|
async function ensureCollection(client: QdrantClient, name: string): Promise<void> {
|
|
198
|
+
const existing = await collectionSchema(client, name);
|
|
199
|
+
if (existing?.dimensions !== undefined && existing.dimensions !== EMBEDDING_DIMENSIONS) {
|
|
200
|
+
// The collection holds vectors of another size. Writing into it would
|
|
201
|
+
// fail, and recreating it would destroy the skills of the other model.
|
|
202
|
+
// The name says so the user can act.
|
|
203
|
+
console.error(
|
|
204
|
+
`[Sync] The collection ${name} holds ${existing.dimensions}-dimensional vectors,` +
|
|
205
|
+
` but the runtime embeds with ${EMBEDDING_DIMENSIONS} (${EMBEDDING_MODEL}).`,
|
|
206
|
+
);
|
|
207
|
+
if (name === LEGACY_SKILL_COLLECTION) {
|
|
208
|
+
console.error(
|
|
209
|
+
`[Sync] ${LEGACY_SKILL_COLLECTION} is the legacy collection of the MiniLM model.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
console.error(
|
|
213
|
+
`[Sync] Use a different collection (QDRANT_COLLECTION) or re-create this one.`,
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
183
217
|
try {
|
|
184
218
|
await client.createCollection(name, {
|
|
185
|
-
vectors: { size:
|
|
219
|
+
vectors: { size: EMBEDDING_DIMENSIONS, distance: "Cosine" }
|
|
186
220
|
});
|
|
187
221
|
} catch {
|
|
188
222
|
// Already exists - OK
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The vector size and the distance of a collection.
|
|
228
|
+
*
|
|
229
|
+
* `null` means the collection does not exist (or cannot be read).
|
|
230
|
+
*/
|
|
231
|
+
async function collectionSchema(
|
|
232
|
+
client: QdrantClient,
|
|
233
|
+
name: string,
|
|
234
|
+
): Promise<{ dimensions?: number; distance?: string } | null> {
|
|
235
|
+
try {
|
|
236
|
+
// SAFETY: the Qdrant client answers with the collection description;
|
|
237
|
+
// the shape below is the one field this function reads. The cast is
|
|
238
|
+
// needed because the generated client types the response as `unknown`.
|
|
239
|
+
const info = (await client.getCollection(name)) as unknown as {
|
|
240
|
+
config?: { params?: { vectors?: { size?: number; distance?: string } } };
|
|
241
|
+
};
|
|
242
|
+
const vectors = info?.config?.params?.vectors;
|
|
243
|
+
if (!vectors) return { };
|
|
244
|
+
return { dimensions: vectors.size, distance: vectors.distance };
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
192
250
|
async function clearCollection(client: QdrantClient, name: string): Promise<void> {
|
|
193
251
|
try {
|
|
194
252
|
await client.deleteCollection(name);
|
|
@@ -203,6 +261,17 @@ async function upsertSkills(
|
|
|
203
261
|
collectionName: string,
|
|
204
262
|
skills: SkillDoc[]
|
|
205
263
|
): Promise<void> {
|
|
264
|
+
// The vectors are produced by the same embedder the runtime uses, so the
|
|
265
|
+
// two can search each other's points. The embedding happens in batches:
|
|
266
|
+
// one request per 64 skills instead of one per skill.
|
|
267
|
+
const { embeddings } = await createEmbeddingsBatch(skills.map((skill) => skill.text));
|
|
268
|
+
if (embeddings.length !== skills.length) {
|
|
269
|
+
console.error(
|
|
270
|
+
`[Sync] The embedder returned ${embeddings.length} vectors for ${skills.length} skills.`,
|
|
271
|
+
);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
206
275
|
const points = skills.map((skill, idx) => ({
|
|
207
276
|
id: idx + 1,
|
|
208
277
|
payload: {
|
|
@@ -210,12 +279,9 @@ async function upsertSkills(
|
|
|
210
279
|
description: skill.description,
|
|
211
280
|
body: skill.body,
|
|
212
281
|
},
|
|
213
|
-
vector:
|
|
214
|
-
text: skill.text,
|
|
215
|
-
model: "sentence-transformers/all-MiniLM-L6-v2"
|
|
216
|
-
}
|
|
282
|
+
vector: embeddings[idx],
|
|
217
283
|
}));
|
|
218
|
-
|
|
284
|
+
|
|
219
285
|
await client.upsert(collectionName, { wait: true, points });
|
|
220
286
|
}
|
|
221
287
|
|
|
@@ -224,11 +290,10 @@ async function searchSkills(
|
|
|
224
290
|
collectionName: string,
|
|
225
291
|
query: string
|
|
226
292
|
): Promise<void> {
|
|
293
|
+
// The query is embedded with the same model as the stored vectors.
|
|
294
|
+
const { embedding } = await createEmbedding(query);
|
|
227
295
|
const results = await client.query(collectionName, {
|
|
228
|
-
query:
|
|
229
|
-
text: query,
|
|
230
|
-
model: "sentence-transformers/all-MiniLM-L6-v2"
|
|
231
|
-
},
|
|
296
|
+
query: embedding,
|
|
232
297
|
limit: 10,
|
|
233
298
|
with_payload: true,
|
|
234
299
|
} as Parameters<typeof client.query>[1]);
|