@learncard/network-brain-service 4.0.3 → 4.1.0
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/.env.example +14 -0
- package/CHANGELOG.md +76 -0
- package/config/config.dev.json +5 -0
- package/config/config.example.json +5 -0
- package/config/config.production.json +5 -0
- package/config/config.scouts.dev.json +5 -0
- package/config/config.scouts.production.json +5 -0
- package/dist/accesslayer/campaign/audience.d.ts +7 -0
- package/dist/accesslayer/campaign/audience.d.ts.map +1 -0
- package/dist/accesslayer/campaign/create.d.ts +3 -0
- package/dist/accesslayer/campaign/create.d.ts.map +1 -0
- package/dist/accesslayer/campaign/delete.d.ts +2 -0
- package/dist/accesslayer/campaign/delete.d.ts.map +1 -0
- package/dist/accesslayer/campaign/read.d.ts +6 -0
- package/dist/accesslayer/campaign/read.d.ts.map +1 -0
- package/dist/accesslayer/campaign/shared.d.ts +16 -0
- package/dist/accesslayer/campaign/shared.d.ts.map +1 -0
- package/dist/accesslayer/campaign/state.d.ts +4 -0
- package/dist/accesslayer/campaign/state.d.ts.map +1 -0
- package/dist/accesslayer/campaign/update.d.ts +3 -0
- package/dist/accesslayer/campaign/update.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential-share-previews.d.ts +5 -0
- package/dist/accesslayer/credential-share-previews.d.ts.map +1 -0
- package/dist/accesslayer/inbox-batch/schema.d.ts +4 -0
- package/dist/accesslayer/inbox-batch/schema.d.ts.map +1 -0
- package/dist/accesslayer/inbox-batch/schema.test.d.ts +2 -0
- package/dist/accesslayer/inbox-batch/schema.test.d.ts.map +1 -0
- package/dist/accesslayer/inbox-batch/store.d.ts +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -1
- package/dist/accesslayer/profile/delete.d.ts.map +1 -1
- package/dist/accesslayer/skill/read.d.ts +12 -2
- package/dist/accesslayer/skill/read.d.ts.map +1 -1
- package/dist/accesslayer/skill/update.d.ts +11 -1
- package/dist/accesslayer/skill/update.d.ts.map +1 -1
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +2 -1
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/environment.d.ts +268 -178
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/skillEmbedding.d.ts +70 -0
- package/dist/config/skillEmbedding.d.ts.map +1 -0
- package/dist/config/skillEmbedding.neo4j.test.d.ts +2 -0
- package/dist/config/skillEmbedding.neo4j.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +20 -0
- package/dist/config/stageConfig.d.ts.map +1 -1
- package/dist/constants/campaigns.d.ts +3 -0
- package/dist/constants/campaigns.d.ts.map +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/helpers/bedrock-embedding.helpers.d.ts +40 -0
- package/dist/helpers/bedrock-embedding.helpers.d.ts.map +1 -0
- package/dist/helpers/bedrock-embedding.helpers.test.d.ts +2 -0
- package/dist/helpers/bedrock-embedding.helpers.test.d.ts.map +1 -0
- package/dist/helpers/bedrock-embedding.smoke.test.d.ts +2 -0
- package/dist/helpers/bedrock-embedding.smoke.test.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts +1 -0
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/skill-embedding.helpers.d.ts +65 -7
- package/dist/helpers/skill-embedding.helpers.d.ts.map +1 -1
- package/dist/helpers/skill-embedding.helpers.test.d.ts +2 -0
- package/dist/helpers/skill-embedding.helpers.test.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/Campaign.d.ts +10 -0
- package/dist/models/Campaign.d.ts.map +1 -0
- package/dist/models/CredentialSharePreview.d.ts +14 -0
- package/dist/models/CredentialSharePreview.d.ts.map +1 -0
- package/dist/models/Integration.d.ts +1 -1
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/campaign-constraints.d.ts +2 -0
- package/dist/models/campaign-constraints.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/routes/app-store.d.ts +1 -1
- package/dist/routes/boosts.d.ts +24 -4
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/campaigns.d.ts +1411 -0
- package/dist/routes/campaigns.d.ts.map +1 -0
- package/dist/routes/contracts.d.ts +19 -6
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-share-previews.d.ts +39 -0
- package/dist/routes/credential-share-previews.d.ts.map +1 -0
- package/dist/routes/credentials.d.ts +4 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +5 -5
- package/dist/routes/index.d.ts +4 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/integrations.d.ts +3 -3
- package/dist/routes/profile-manager.d.ts +4 -0
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts +44 -0
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/skill-frameworks.d.ts +1 -0
- package/dist/routes/skill-frameworks.d.ts.map +1 -1
- package/dist/routes/skills-semantic-search.test.d.ts +2 -0
- package/dist/routes/skills-semantic-search.test.d.ts.map +1 -0
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/test.d.ts +3 -1
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/seed/seedSkillFrameworks.d.ts +1 -0
- package/dist/seed/seedSkillFrameworks.d.ts.map +1 -1
- package/dist/seed/seedSkillFrameworks.embeddings.test.d.ts +2 -0
- package/dist/seed/seedSkillFrameworks.embeddings.test.d.ts.map +1 -0
- package/dist/types/app-manifest.d.ts +2 -2
- package/dist/types/campaign.d.ts +19 -0
- package/dist/types/campaign.d.ts.map +1 -0
- package/dist/types/consentflowcontract.d.ts +1 -1
- package/dist/types/integration.d.ts +3 -3
- package/dist/types/profile.d.ts +9 -0
- package/dist/types/profile.d.ts.map +1 -1
- package/dist/types/skill.d.ts.map +1 -1
- package/esbuildPlugins.cjs +9 -6
- package/package.json +20 -17
- package/project.json +14 -0
- package/scripts/skill-embeddings-backfill.ts +161 -0
- package/serverless-local.yml +4 -0
- package/serverless.function-env.cjs +3 -0
- package/serverless.local.yml +4 -0
- package/serverless.yml +12 -1
- package/src/accesslayer/campaign/README.md +174 -0
- package/src/accesslayer/campaign/audience.ts +152 -0
- package/src/accesslayer/campaign/create.ts +47 -0
- package/src/accesslayer/campaign/delete.ts +18 -0
- package/src/accesslayer/campaign/read.ts +125 -0
- package/src/accesslayer/campaign/shared.ts +92 -0
- package/src/accesslayer/campaign/state.ts +80 -0
- package/src/accesslayer/campaign/update.ts +50 -0
- package/src/accesslayer/consentflowcontract/relationships/create.ts +3 -1
- package/src/accesslayer/consentflowcontract/relationships/update.ts +5 -1
- package/src/accesslayer/credential-share-previews.ts +76 -0
- package/src/accesslayer/inbox-batch/schema.test.ts +91 -0
- package/src/accesslayer/inbox-batch/schema.ts +54 -0
- package/src/accesslayer/inbox-batch/store.ts +2 -38
- package/src/accesslayer/profile/delete.ts +6 -0
- package/src/accesslayer/skill/read.ts +56 -16
- package/src/accesslayer/skill/update.ts +43 -1
- package/src/app.ts +9 -0
- package/src/config/environment.test.ts +208 -0
- package/src/config/environment.ts +166 -11
- package/src/config/skillEmbedding.neo4j.test.ts +138 -0
- package/src/config/skillEmbedding.ts +202 -0
- package/src/constants/campaigns.ts +9 -0
- package/src/helpers/bedrock-embedding.helpers.test.ts +208 -0
- package/src/helpers/bedrock-embedding.helpers.ts +243 -0
- package/src/helpers/bedrock-embedding.smoke.test.ts +53 -0
- package/src/helpers/boost-storage.helpers.test.ts +155 -1
- package/src/helpers/boost.helpers.ts +52 -10
- package/src/helpers/skill-embedding.helpers.test.ts +1044 -0
- package/src/helpers/skill-embedding.helpers.ts +611 -123
- package/src/models/Campaign.ts +32 -0
- package/src/models/CredentialSharePreview.ts +30 -0
- package/src/models/Profile.ts +1 -0
- package/src/models/campaign-constraints.ts +22 -0
- package/src/models/index.ts +29 -1
- package/src/routes/campaigns.ts +179 -0
- package/src/routes/credential-share-previews.ts +74 -0
- package/src/routes/profiles.ts +7 -0
- package/src/routes/skills-semantic-search.test.ts +168 -0
- package/src/routes/skills.ts +42 -6
- package/src/routes/workflows.ts +8 -1
- package/src/seed/seedSkillFrameworks.embeddings.test.ts +82 -0
- package/src/seed/seedSkillFrameworks.test.ts +6 -0
- package/src/seed/seedSkillFrameworks.ts +83 -18
- package/src/types/campaign.ts +17 -0
- package/src/types/skill.ts +6 -0
- package/test/campaigns.spec.ts +789 -0
- package/test/consentflow.spec.ts +101 -0
- package/test/credential-share-previews.spec.ts +97 -0
- package/test/inbox-batch.spec.ts +189 -1
- package/test/verification/campaigns.ts +49 -0
- package/vitest.campaigns.config.ts +21 -0
- package/vitest.config.ts +11 -0
- package/vitest.integration.config.ts +1 -0
package/src/routes/skills.ts
CHANGED
|
@@ -22,10 +22,14 @@ import {
|
|
|
22
22
|
import { createSkill } from '@accesslayer/skill/create';
|
|
23
23
|
import { updateSkill, deleteSkill } from '@accesslayer/skill/update';
|
|
24
24
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
generateQueryEmbedding,
|
|
26
|
+
getCachedQueryEmbedding,
|
|
27
|
+
getSkillEmbeddingSearchIndex,
|
|
28
|
+
isSkillEmbeddingNovaReadEnabled,
|
|
29
|
+
isSkillEmbeddingError,
|
|
27
30
|
upsertSkillEmbeddings,
|
|
28
31
|
} from '@helpers/skill-embedding.helpers';
|
|
32
|
+
import { NOVA2_MAX_TEXT_CHARS } from '../config/skillEmbedding';
|
|
29
33
|
import { listTagsForSkill, addTagToSkill, removeTagFromSkill } from '@accesslayer/skill/tags';
|
|
30
34
|
import {
|
|
31
35
|
TagValidator,
|
|
@@ -226,7 +230,16 @@ export const skillsRouter = t.router({
|
|
|
226
230
|
.query(async ({ ctx, input }) => {
|
|
227
231
|
const profileId = ctx.user.profile.profileId;
|
|
228
232
|
const { text, frameworkId, limit } = input;
|
|
229
|
-
|
|
233
|
+
|
|
234
|
+
// Nova's synchronous input limit; the legacy Google read path keeps main's behaviour.
|
|
235
|
+
if (isSkillEmbeddingNovaReadEnabled() && text.trim().length > NOVA2_MAX_TEXT_CHARS) {
|
|
236
|
+
throw new TRPCError({
|
|
237
|
+
code: 'BAD_REQUEST',
|
|
238
|
+
message: `Semantic search text must be at most ${NOVA2_MAX_TEXT_CHARS} characters`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const cachedEmbedding = await getCachedQueryEmbedding(text);
|
|
230
243
|
|
|
231
244
|
if (!cachedEmbedding) {
|
|
232
245
|
const rateLimitKey = `skill-semantic-search-rate:${profileId}`;
|
|
@@ -249,16 +262,39 @@ export const skillsRouter = t.router({
|
|
|
249
262
|
|
|
250
263
|
let embedding: number[];
|
|
251
264
|
try {
|
|
252
|
-
embedding = cachedEmbedding ?? (await
|
|
265
|
+
embedding = cachedEmbedding ?? (await generateQueryEmbedding(text));
|
|
253
266
|
} catch (error) {
|
|
254
|
-
|
|
267
|
+
const code = isSkillEmbeddingError(error) ? error.code : 'failed';
|
|
268
|
+
console.error(`Failed to generate embedding for skill search: ${code}`);
|
|
269
|
+
|
|
270
|
+
if (code === 'throttled') {
|
|
271
|
+
throw new TRPCError({
|
|
272
|
+
code: 'TOO_MANY_REQUESTS',
|
|
273
|
+
message: 'Skill search is rate limited. Please try again shortly.',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (code === 'unavailable' || code === 'timeout' || code === 'disabled') {
|
|
278
|
+
throw new TRPCError({
|
|
279
|
+
code: 'SERVICE_UNAVAILABLE',
|
|
280
|
+
message: 'Skill search is temporarily unavailable',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
255
284
|
throw new TRPCError({
|
|
256
285
|
code: 'INTERNAL_SERVER_ERROR',
|
|
257
286
|
message: 'Failed to generate embedding for skill search',
|
|
258
287
|
});
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
|
|
290
|
+
// Legacy Google index unless SKILL_EMBEDDING_BEDROCK_READ_ENABLED (LC-2206); the query
|
|
291
|
+
// embedding above was produced for the same index.
|
|
292
|
+
const results = await searchSkillsByEmbedding(
|
|
293
|
+
embedding,
|
|
294
|
+
limit,
|
|
295
|
+
frameworkId,
|
|
296
|
+
getSkillEmbeddingSearchIndex()
|
|
297
|
+
);
|
|
262
298
|
|
|
263
299
|
return {
|
|
264
300
|
records: results.map(({ skill, score }) => ({
|
package/src/routes/workflows.ts
CHANGED
|
@@ -37,7 +37,12 @@ import { createClaimedRelationship } from '@accesslayer/inbox-credential/relatio
|
|
|
37
37
|
import { getContactMethodById, getProfileByContactMethod } from '@accesslayer/contact-method/read';
|
|
38
38
|
import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
getBoostUri,
|
|
42
|
+
isBoostViewableByClaimLink,
|
|
43
|
+
isDraftBoost,
|
|
44
|
+
stampIssuanceDate,
|
|
45
|
+
} from '@helpers/boost.helpers';
|
|
41
46
|
import { getBoostOwnerProfile } from 'types/boost';
|
|
42
47
|
import { getEmptyLearnCard, getLearnCard } from '@helpers/learnCard.helpers';
|
|
43
48
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
@@ -392,6 +397,8 @@ async function handlePresentationForClaim(
|
|
|
392
397
|
|
|
393
398
|
boostCredential.issuer = signingAuthorityForUser.relationship.did;
|
|
394
399
|
|
|
400
|
+
stampIssuanceDate(boostCredential);
|
|
401
|
+
|
|
395
402
|
if (Array.isArray(boostCredential.credentialSubject)) {
|
|
396
403
|
boostCredential.credentialSubject = boostCredential.credentialSubject.map(subject => ({
|
|
397
404
|
...subject,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_SKILL_FRAMEWORKS } from './skill-frameworks.fixtures';
|
|
4
|
+
|
|
5
|
+
// Review r4339891853: a failing Nova pass must not suppress the legacy Google pass (and the
|
|
6
|
+
// reverse), and each failure must still be reported. Bedrock and Google are mocked here.
|
|
7
|
+
const embeddings = vi.hoisted(() => ({
|
|
8
|
+
upsertNova: vi.fn(),
|
|
9
|
+
upsertLegacy: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
class FakeSkillEmbeddingError extends Error {
|
|
13
|
+
constructor(readonly code: string) {
|
|
14
|
+
super(code);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
vi.mock('@helpers/skill-embedding.helpers', () => ({
|
|
19
|
+
upsertNovaSkillEmbeddings: embeddings.upsertNova,
|
|
20
|
+
upsertLegacySkillEmbeddings: embeddings.upsertLegacy,
|
|
21
|
+
isSkillEmbeddingEnabled: () => true,
|
|
22
|
+
isLegacySkillEmbeddingEnabled: () => true,
|
|
23
|
+
isSkillEmbeddingError: (error: unknown) => error instanceof FakeSkillEmbeddingError,
|
|
24
|
+
getSkillEmbeddingGeneration: () => ({ vectorProperty: 'embedding_nova2_v1_0_d1024' }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
/** Query runner double: every framework has one skill missing both vectors. */
|
|
28
|
+
const run = vi.fn(async (query: string, params?: Record<string, unknown>) => {
|
|
29
|
+
if (query.includes('IS NULL') && query.includes('[:CONTAINS]->(s:Skill)')) {
|
|
30
|
+
const frameworkId = String(params?.frameworkId);
|
|
31
|
+
return {
|
|
32
|
+
records: [
|
|
33
|
+
{
|
|
34
|
+
get: (key: string) =>
|
|
35
|
+
key === 's'
|
|
36
|
+
? { properties: { id: `${frameworkId}-skill`, statement: 'Python' } }
|
|
37
|
+
: null,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { records: [] };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('seedSkillFrameworkFixtures embedding passes', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
run.mockClear();
|
|
48
|
+
embeddings.upsertNova.mockReset();
|
|
49
|
+
embeddings.upsertLegacy.mockReset().mockResolvedValue(undefined);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('still writes legacy embeddings for every framework when Nova is denied, and reports it', async () => {
|
|
53
|
+
embeddings.upsertNova.mockRejectedValue(new FakeSkillEmbeddingError('access_denied'));
|
|
54
|
+
const log = { log: vi.fn(), warn: vi.fn() };
|
|
55
|
+
const { seedSkillFrameworkFixtures } = await import('./seedSkillFrameworks');
|
|
56
|
+
|
|
57
|
+
await seedSkillFrameworkFixtures(run, { log });
|
|
58
|
+
|
|
59
|
+
expect(embeddings.upsertLegacy).toHaveBeenCalledTimes(DEFAULT_SKILL_FRAMEWORKS.length);
|
|
60
|
+
// Nova stops after its first failure instead of retrying every framework.
|
|
61
|
+
expect(embeddings.upsertNova).toHaveBeenCalledTimes(1);
|
|
62
|
+
const warnings = log.warn.mock.calls.map(call => String(call[0])).join('\n');
|
|
63
|
+
expect(warnings).toContain('Nova');
|
|
64
|
+
expect(warnings).toContain('Bedrock denied the embedding request');
|
|
65
|
+
expect(warnings).not.toMatch(/legacy google embedding backfill was skipped/i);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('still writes Nova embeddings when the legacy Google pass fails, and reports it', async () => {
|
|
69
|
+
embeddings.upsertNova.mockResolvedValue(undefined);
|
|
70
|
+
embeddings.upsertLegacy.mockRejectedValue(new Error('google down'));
|
|
71
|
+
const log = { log: vi.fn(), warn: vi.fn() };
|
|
72
|
+
const { seedSkillFrameworkFixtures } = await import('./seedSkillFrameworks');
|
|
73
|
+
|
|
74
|
+
await seedSkillFrameworkFixtures(run, { log });
|
|
75
|
+
|
|
76
|
+
expect(embeddings.upsertNova).toHaveBeenCalledTimes(DEFAULT_SKILL_FRAMEWORKS.length);
|
|
77
|
+
expect(embeddings.upsertLegacy).toHaveBeenCalledTimes(1);
|
|
78
|
+
const warnings = log.warn.mock.calls.map(call => String(call[0])).join('\n');
|
|
79
|
+
expect(warnings).toMatch(/legacy google embedding backfill was skipped/i);
|
|
80
|
+
expect(warnings).not.toMatch(/nova embedding backfill was skipped/i);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -24,6 +24,12 @@ vi.mock('neogma', () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
vi.mock('@helpers/skill-embedding.helpers', () => ({
|
|
26
26
|
upsertSkillEmbeddings: mockUpsertSkillEmbeddings,
|
|
27
|
+
upsertNovaSkillEmbeddings: mockUpsertSkillEmbeddings,
|
|
28
|
+
upsertLegacySkillEmbeddings: mockUpsertSkillEmbeddings,
|
|
29
|
+
isSkillEmbeddingEnabled: () => false,
|
|
30
|
+
isLegacySkillEmbeddingEnabled: () => false,
|
|
31
|
+
isSkillEmbeddingError: () => false,
|
|
32
|
+
getSkillEmbeddingGeneration: vi.fn(),
|
|
27
33
|
}));
|
|
28
34
|
|
|
29
35
|
describe('seedSkillFrameworks staging env resolution', () => {
|
|
@@ -10,9 +10,20 @@ import {
|
|
|
10
10
|
type SeedSkillNode,
|
|
11
11
|
} from './skill-frameworks.fixtures';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
getSkillEmbeddingGeneration,
|
|
14
|
+
isLegacySkillEmbeddingEnabled,
|
|
15
|
+
isSkillEmbeddingEnabled,
|
|
16
|
+
isSkillEmbeddingError,
|
|
17
|
+
upsertLegacySkillEmbeddings,
|
|
18
|
+
upsertNovaSkillEmbeddings,
|
|
19
|
+
type UpdateLegacySkillEmbeddingFn,
|
|
14
20
|
type UpdateSkillEmbeddingFn,
|
|
15
21
|
} from '@helpers/skill-embedding.helpers';
|
|
22
|
+
import {
|
|
23
|
+
LEGACY_GOOGLE_SKILL_EMBEDDING_PROPERTY,
|
|
24
|
+
buildSkillEmbeddingUpdateParams,
|
|
25
|
+
buildSkillEmbeddingUpdateQuery,
|
|
26
|
+
} from '../config/skillEmbedding';
|
|
16
27
|
import { syncProfileDisplayNameLower } from '../accesslayer/profile/search-fields';
|
|
17
28
|
|
|
18
29
|
// Inlined from @helpers/profile.helpers to avoid importing the access/model layer
|
|
@@ -365,6 +376,20 @@ export const addSkillFrameworkAdmin = async (
|
|
|
365
376
|
};
|
|
366
377
|
|
|
367
378
|
const buildSeedSkillEmbeddingUpdater = (run: QueryRunnerLike['run']): UpdateSkillEmbeddingFn => {
|
|
379
|
+
return async update => {
|
|
380
|
+
const result = await run(
|
|
381
|
+
buildSkillEmbeddingUpdateQuery(update.generation, Boolean(update.frameworkId)),
|
|
382
|
+
buildSkillEmbeddingUpdateParams(update)
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
return Number(result.records[0]?.get('count') ?? 0) > 0;
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/** LC-2206 legacy write (main's Cypher), kept while search can still read the legacy index. */
|
|
390
|
+
const buildSeedLegacySkillEmbeddingUpdater = (
|
|
391
|
+
run: QueryRunnerLike['run']
|
|
392
|
+
): UpdateLegacySkillEmbeddingFn => {
|
|
368
393
|
return async (skillId, embedding, frameworkId) => {
|
|
369
394
|
if (frameworkId) {
|
|
370
395
|
const result = await run(
|
|
@@ -390,7 +415,8 @@ const buildSeedSkillEmbeddingUpdater = (run: QueryRunnerLike['run']): UpdateSkil
|
|
|
390
415
|
|
|
391
416
|
const getSeedSkillsMissingEmbedding = async (
|
|
392
417
|
run: QueryRunnerLike['run'],
|
|
393
|
-
frameworkId: string
|
|
418
|
+
frameworkId: string,
|
|
419
|
+
vectorProperty: string
|
|
394
420
|
): Promise<
|
|
395
421
|
Array<{
|
|
396
422
|
id: string;
|
|
@@ -400,9 +426,10 @@ const getSeedSkillsMissingEmbedding = async (
|
|
|
400
426
|
code?: string;
|
|
401
427
|
}>
|
|
402
428
|
> => {
|
|
429
|
+
// Property names come from resolveSkillEmbeddingGeneration or the legacy constant ([a-z0-9_] only).
|
|
403
430
|
const result = await run(
|
|
404
431
|
`MATCH (f:SkillFramework {id: $frameworkId})-[:CONTAINS]->(s:Skill)
|
|
405
|
-
WHERE s
|
|
432
|
+
WHERE s.${vectorProperty} IS NULL
|
|
406
433
|
OPTIONAL MATCH (s)-[:IS_CHILD_OF]->(parent:Skill)
|
|
407
434
|
RETURN s AS s, parent.id AS parentId
|
|
408
435
|
ORDER BY s.id`,
|
|
@@ -423,19 +450,18 @@ const getSeedSkillsMissingEmbedding = async (
|
|
|
423
450
|
});
|
|
424
451
|
};
|
|
425
452
|
|
|
426
|
-
const describeEmbeddingBackfillFailure = (error: unknown): string => {
|
|
427
|
-
const
|
|
428
|
-
const normalized = message.toLowerCase();
|
|
453
|
+
export const describeEmbeddingBackfillFailure = (error: unknown): string => {
|
|
454
|
+
const code = isSkillEmbeddingError(error) ? error.code : undefined;
|
|
429
455
|
|
|
430
|
-
if (
|
|
431
|
-
return '
|
|
456
|
+
if (code === 'throttled') {
|
|
457
|
+
return 'Bedrock embedding requests were throttled. The frameworks were seeded, but some semantic search embeddings were skipped for now; the startup backfill will fill them in.';
|
|
432
458
|
}
|
|
433
459
|
|
|
434
|
-
if (
|
|
435
|
-
return '
|
|
460
|
+
if (code === 'access_denied') {
|
|
461
|
+
return 'Bedrock denied the embedding request. Check model access for Nova 2 Multimodal Embeddings and the bedrock:InvokeModel grant for your AWS credentials.';
|
|
436
462
|
}
|
|
437
463
|
|
|
438
|
-
return
|
|
464
|
+
return `Skill embedding failed (${code ?? 'unknown'}).\nUse \`bun run skill-frameworks add-admin\` to add admins.`;
|
|
439
465
|
};
|
|
440
466
|
|
|
441
467
|
const upsertSkillNode = async (
|
|
@@ -547,7 +573,12 @@ export const seedSkillFrameworkFixtures = async (
|
|
|
547
573
|
await ensureSeedProfile(run, ownerProfileId);
|
|
548
574
|
|
|
549
575
|
const seededFrameworkIds: string[] = [];
|
|
550
|
-
|
|
576
|
+
// Each provider tracks its own failure, so a Nova failure never skips the legacy Google
|
|
577
|
+
// pass that semantic search may still be reading (and vice versa).
|
|
578
|
+
const embeddingFailures: { nova: string | null; legacy: string | null } = {
|
|
579
|
+
nova: null,
|
|
580
|
+
legacy: null,
|
|
581
|
+
};
|
|
551
582
|
|
|
552
583
|
for (const framework of fixtures) {
|
|
553
584
|
const now = new Date().toISOString();
|
|
@@ -592,17 +623,42 @@ export const seedSkillFrameworkFixtures = async (
|
|
|
592
623
|
await upsertSkillNode(run, framework, rootSkill);
|
|
593
624
|
}
|
|
594
625
|
|
|
595
|
-
|
|
596
|
-
|
|
626
|
+
// LC-2206: Nova vectors whenever Nova is enabled (independent of the read flag), plus
|
|
627
|
+
// the legacy Google vectors main writes while a Google key is configured.
|
|
628
|
+
if (!embeddingFailures.nova && isSkillEmbeddingEnabled()) {
|
|
629
|
+
const missingEmbeddingSkills = await getSeedSkillsMissingEmbedding(
|
|
630
|
+
run,
|
|
631
|
+
framework.id,
|
|
632
|
+
getSkillEmbeddingGeneration().vectorProperty
|
|
633
|
+
);
|
|
597
634
|
|
|
598
635
|
if (missingEmbeddingSkills.length > 0) {
|
|
599
636
|
try {
|
|
600
|
-
await
|
|
637
|
+
await upsertNovaSkillEmbeddings(
|
|
601
638
|
missingEmbeddingSkills,
|
|
602
639
|
buildSeedSkillEmbeddingUpdater(run)
|
|
603
640
|
);
|
|
604
641
|
} catch (error) {
|
|
605
|
-
|
|
642
|
+
embeddingFailures.nova = describeEmbeddingBackfillFailure(error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!embeddingFailures.legacy && isLegacySkillEmbeddingEnabled()) {
|
|
648
|
+
const missingLegacySkills = await getSeedSkillsMissingEmbedding(
|
|
649
|
+
run,
|
|
650
|
+
framework.id,
|
|
651
|
+
LEGACY_GOOGLE_SKILL_EMBEDDING_PROPERTY
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
if (missingLegacySkills.length > 0) {
|
|
655
|
+
try {
|
|
656
|
+
await upsertLegacySkillEmbeddings(
|
|
657
|
+
missingLegacySkills,
|
|
658
|
+
buildSeedLegacySkillEmbeddingUpdater(run)
|
|
659
|
+
);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
embeddingFailures.legacy = describeEmbeddingBackfillFailure(error);
|
|
606
662
|
}
|
|
607
663
|
}
|
|
608
664
|
}
|
|
@@ -610,10 +666,19 @@ export const seedSkillFrameworkFixtures = async (
|
|
|
610
666
|
seededFrameworkIds.push(framework.id);
|
|
611
667
|
}
|
|
612
668
|
|
|
613
|
-
if (
|
|
669
|
+
if (embeddingFailures.nova) {
|
|
614
670
|
options.log?.warn?.(
|
|
615
|
-
`
|
|
671
|
+
`Nova embedding backfill was skipped for the remaining frameworks: ${embeddingFailures.nova}`
|
|
616
672
|
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (embeddingFailures.legacy) {
|
|
676
|
+
options.log?.warn?.(
|
|
677
|
+
`Legacy Google embedding backfill was skipped for the remaining frameworks: ${embeddingFailures.legacy}`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (embeddingFailures.nova || embeddingFailures.legacy) {
|
|
617
682
|
options.log?.warn?.(
|
|
618
683
|
'The frameworks were still seeded. Use `bun run skill-frameworks add-admin` to add admins. Semantic search will keep using whichever embeddings already exist.'
|
|
619
684
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CampaignStatus } from '@learncard/types';
|
|
2
|
+
|
|
3
|
+
export type CampaignRecord = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: CampaignStatus;
|
|
7
|
+
definition: string;
|
|
8
|
+
startsAt?: string;
|
|
9
|
+
endsAt?: string;
|
|
10
|
+
excludeMinors: boolean;
|
|
11
|
+
selfEnroll: boolean;
|
|
12
|
+
createdByProfileId: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CampaignAudienceQuery = { match: string; params: Record<string, unknown> };
|
package/src/types/skill.ts
CHANGED
|
@@ -12,5 +12,11 @@ export const FlatSkillValidator = SkillValidator.extend({
|
|
|
12
12
|
export type FlatSkillType = z.infer<typeof FlatSkillValidator>;
|
|
13
13
|
|
|
14
14
|
export type SkillDbType = FlatSkillType & {
|
|
15
|
+
/**
|
|
16
|
+
* Legacy gemini-embedding-001 (768-d) vector. Still written on skill changes and read by
|
|
17
|
+
* semantic search until SKILL_EMBEDDING_BEDROCK_READ_ENABLED is on (LC-2206); dropped by the
|
|
18
|
+
* post-rollback-window cleanup. Nova 2 vectors live in generation-specific properties
|
|
19
|
+
* (see config/skillEmbedding.ts).
|
|
20
|
+
*/
|
|
15
21
|
embedding?: number[];
|
|
16
22
|
};
|