Conversation
Adds the shared embedding + index building blocks for the Lookup Service topic dimension (LLMO-7445, M2 v1): - spacecat-shared-gpt-client: AzureEmbeddingClient (createFrom(context), createEmbeddings) behind an EmbeddingProvider typedef so the model/provider stays swappable. Exported from the package root. - spacecat-shared-data-access: semantic-index.utils.js mirroring url-index.utils.js - syncOpportunitySemantic (full-replace per-source vectors), copyEntityVectors (snapshot copy, no re-embed), lookupOpportunitiesByVector (ANN reader), getQueryEmbedding / upsertQueryEmbedding / touchQueryEmbedding cache helpers, serializeVector, SEMANTIC_INDEX_TABLES allow-list. Both sides embed via the same AzureEmbeddingClient / text-embedding-3-small (1536 dims, cosine), so write and read share one vector space by construction. Ref: LLMO-7445
There was a problem hiding this comment.
Hey @tb-adbe,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Request changes - four must-fix issues around input validation, data integrity, and credential hygiene.
Complexity: HIGH - large diff (1431 lines, 8 files).
Changes: Adds an Azure OpenAI embedding client and semantic vector storage/retrieval utilities (sync, copy, ANN lookup, query-embedding cache) with tests for both (8 files).
Must fix before merge
- [Important]
dimsnot validated in query-embedding cache functions - silent cache-key corruption -semantic-index.utils.js:370(details inline) - [Important]
parseVectorsilently returns NaN on malformed vectors - data integrity gap -semantic-index.utils.js:102(details inline) - [Important]
config(with API key) exposed as public instance property -azure-embedding-client.js:100(details inline) - [Important] No retry/backoff for transient Azure API failures (429/5xx) -
azure-embedding-client.js:142(details inline)
Non-blocking (3): minor issues and suggestions
- nit:
normalizeTextsilently coerces non-string input to''instead of throwing, creating a latent collision surface for future callers that skip the pre-check -semantic-index.utils.js:73 - nit:
syncOpportunitySemantichardcodes the table name whilecopyEntityVectorsaccepts it as a parameter; asymmetry will confuse whenSEMANTIC_INDEX_TABLESgrows -semantic-index.utils.js:239 - suggestion:
copyEntityVectorsmaterializes all source rows into JS memory rather than using a server-side RPC; works at current cardinalities but worth a follow-up for anrpc_copy_entity_vectorsfunction -semantic-index.utils.js:277
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 8m 25s | Cost: $7.63 | Commit: 94c48fcc0adee9c773dc95ea5af3965828c241df
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| * Point-read the cached embedding for a query string (keyed by normalized text + model + dims). | ||
| * @returns {Promise<{vector: number[], textHash: string}|null>} the cached vector, or null on miss | ||
| */ | ||
| export async function getQueryEmbedding(postgrestClient, { text, model, dims } = {}) { |
There was a problem hiding this comment.
issue (blocking): dims is not validated in getQueryEmbedding, upsertQueryEmbedding, or touchQueryEmbedding. The cache key is (text_hash, model, dims), but while model gets assertId validation, dims is passed to .eq('dims', dims) unchecked. Passing undefined or a non-number produces a silent cache miss (redundant embedding API calls) or upserts a row with null dims that can never be found again.
Fix: Add if (typeof dims !== 'number' || !Number.isFinite(dims)) throw new ValidationError('dims is required') at the top of each function, or create an assertDims helper matching the assertId pattern.
| if (inner === '') { | ||
| return []; | ||
| } | ||
| return inner.split(',').map(Number); |
There was a problem hiding this comment.
issue (blocking): inner.split(',').map(Number) returns NaN elements for non-numeric segments (e.g., corrupted DB value [0.1,abc,0.3] produces [0.1, NaN, 0.3]). The downstream serializeVector rejects NaN via Number.isFinite, but the error surfaces far from the root cause with a misleading message.
Fix: Validate after parsing:
const nums = inner.split(',').map(Number);
return nums.some(Number.isNaN) ? null : nums;This returns null (cache-miss semantics) for corrupt data, matching the function's existing null-return convention.
| * @param {object} log - Logger. | ||
| */ | ||
| constructor(config, log) { | ||
| this.config = config; |
There was a problem hiding this comment.
issue (blocking): this.config (containing apiKey) is a public property. Any code with a reference to the client can read client.config.apiKey. The class already uses private methods (#post, #logDuration), so #config is consistent with the existing pattern.
Fix: Store as #config private field. Adjust the test to verify behavior (successful embedding call with expected inputs) rather than inspecting client.config.apiKey directly.
| * omit to use the model's native dimension. Must match the stored index's dimension. | ||
| * @returns {Promise<number[][]>} One embedding vector per input, aligned to input order. | ||
| */ | ||
| async createEmbeddings(inputs, options = {}) { |
There was a problem hiding this comment.
issue (blocking): No retry/backoff for transient failures. Azure OpenAI returns 429 (rate limit) with a Retry-After header under normal operation, and 5xx responses are transient. A single failure here fails the upstream sync operation entirely. Both the audit-worker write path and api-service read path will depend on this client.
Fix: Add exponential backoff with jitter for 429/5xx (respecting Retry-After), or accept a maxRetries option so callers can opt in. If intentionally deferred to step 3, document that in a code comment.
…MO-7445) - validate dims in the query-embedding cache helpers (silent cache-key corruption) - parseVector returns null on a corrupt vector; getQueryEmbedding treats it as a miss - make AzureEmbeddingClient config a private #config field (no API-key exposure) - add bounded retry/backoff (429/5xx, honors Retry-After) to the embedding client - back copyEntityVectors with the wrpc_copy_opportunity_semantic_vectors RPC (no client-side row materialization) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
|
Thanks for the review @MysticatBot — addressed in 0bca592. Summary of each point: Must-fix
Non-blocking Also fixed a stale doc reference (vectors are produced by the audit-worker, not Mystique). Coverage stays 100% lines/statements; branches above the 97% floor. |
There was a problem hiding this comment.
Hey @tb-adbe,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Request changes - one must-fix item remains around retry delay bounds.
Complexity: HIGH - large diff (1572 lines, 8 files).
Changes: Adds an Azure OpenAI embedding client with retry/backoff and semantic vector storage/retrieval utilities (sync, copy via server-side RPC, ANN lookup, query-embedding cache) with tests for both (8 files).
Note: CI checks are currently pending - verify before merge.
Must fix before merge
- [Important] Unbounded
Retry-Afterdelay can stall caller indefinitely in time-bounded execution -azure-embedding-client.js:51(details inline)
Non-blocking (3): minor issues and suggestions
- nit:
AZURE_EMBEDDING_MAX_RETRIESaccepts negative integers (e.g."-5"), which silently behaves asmaxRetries=0- clamp toMath.max(0, ...)for clarity -azure-embedding-client.js:105 - suggestion:
touchQueryEmbeddingre-derives normalized text + SHA-256 hash that the caller already has from the precedinggetQueryEmbeddinghit - acceptingtextHashdirectly would save redundant work on the hot read path -semantic-index.utils.js:451 - nit:
COPY_VECTORS_RPCis exported inindex.jsbut never asserted in the test file unlike the other constants -semantic-index.utils.test.js
Previously flagged, now resolved
- dims validation:
assertDimsguard added to all three cache functions, rejects non-positive-integer values - parseVector NaN: returns
nullfor corrupt segments,getQueryEmbeddingtreats null parse as cache miss - config exposure: API key moved to private
#configfield, tests verifyclient.config === undefined - No retry/backoff: bounded retry with
Retry-Afterheader support, exponential backoff with jitter, configurablemaxRetries
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 4m 12s | Cost: $6.98 | Commit: 0bca59265febde9c4adcf20ca09e944201ee1109
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| function retryDelayMs(response, attempt, baseDelayMs) { | ||
| const retryAfter = Number(response.headers.get('retry-after')); | ||
| if (Number.isFinite(retryAfter) && retryAfter > 0) { | ||
| return retryAfter * 1000; |
There was a problem hiding this comment.
issue (blocking): The Retry-After header value is trusted without an upper bound. A misbehaving upstream, CDN, or Azure outage returning Retry-After: 3600 would block the caller for an hour. In a Lambda/worker with a finite execution budget, this wastes the invocation slot and delays the timeout signal to the orchestrator. The exponential backoff path is naturally bounded by maxRetries * (2^attempt * base), but the Retry-After path is not.
Fix: Cap the honored value, e.g.:
const MAX_RETRY_DELAY_MS = 30_000;
return Math.min(retryAfter * 1000, MAX_RETRY_DELAY_MS);Alternatively, make the cap configurable alongside retryBaseDelayMs. If the server asks for more than the cap, use the cap - a bounded retry is always preferable to an unbounded sleep in a time-constrained execution context.
Address the follow-up PR review: - cap any single retry sleep at retryMaxDelayMs (default 30s) so a hostile/unbounded Retry-After can't stall the invocation - clamp a negative maxRetries to 0 - touchQueryEmbedding accepts an optional textHash to skip re-hashing on a cache hit - assert COPY_VECTORS_RPC in the constants test Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the follow-up @MysticatBot — addressed in 7d3b022. Must-fix
Nits
Coverage stays 100% lines/statements; branches above the 97% floor. |
There was a problem hiding this comment.
Hey @tb-adbe,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - all prior blocking findings addressed, no new issues.
Complexity: HIGH - large diff (1572 lines, 8 files).
Changes: Adds an Azure OpenAI embedding client with bounded retry/backoff and semantic vector storage/retrieval utilities (sync, copy via server-side RPC, ANN lookup, query-embedding cache) with tests for both (8 files).
Note: CI checks are currently pending - verify before merge.
Non-blocking (2): minor issues and suggestions
- suggestion:
retryMaxDelayMsis not clamped unlikemaxRetries(which usesMath.max(0, ...)); a zero or negative value silently disables backoff. AddingMath.max(0, ...)or a minimum floor would make the two config paths consistent -azure-embedding-client.js:165 - suggestion:
touchQueryEmbeddingaccepts any non-empty string astextHashwithout validating the expected SHA-256 hex format (/^[0-9a-f]{64}$/); PostgREST parameterizes the query so there is no injection risk, but format validation would make the contract explicit -semantic-index.utils.js:460
Previously flagged, now resolved
- Unbounded Retry-After delay: every retry sleep now capped at
retryMaxDelayMs(default 30s), applied to bothRetry-Afterand backoff paths - Negative
maxRetries: clamped withMath.max(0, ...)at point-of-use touchQueryEmbeddingredundant re-hashing: accepts optionaltextHashparam to skip re-normalize + re-hash on the hot read pathCOPY_VECTORS_RPCuntested: added to the constants assertion
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 17s | Cost: $4.24 | Commit: 7d3b02219cace8cb9b0e5e45785ac4828a27717c
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…LMO-7445) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract a pure, dimension-agnostic per-topic hygiene helper into
spacecat-shared-data-access so the audit-worker write path (sanitizeTopics)
and the api-service by-topics read path reject the same junk and de-duplicate
on the same normalized key the writer hashes on.
- cleanTopicText(title, { maxLength }) -> { text, key } | null
- MAX_SOURCE_TEXT_LENGTH = 2048 (matches the source_text DB CHECK)
- exported from the util barrel; unit tests added.
Ref: LLMO-7445
What
Shared building blocks (step 2 of 4) for the Lookup Service topic dimension — semantic opportunity-by-topic lookup (LLMO-7445, M2 v1). Two packages:
spacecat-shared-gpt-clientAzureEmbeddingClient—createFrom(context)(readsAZURE_EMBEDDING_*, falling back toAZURE_OPENAI_*for endpoint/key/api-version) andcreateEmbeddings(inputs, { dimensions? }) → number[][]. Separate from the chatAzureOpenAIClient.EmbeddingProvidertypedef so consumers depend on the interface, not the concrete client — the model/provider is swappable later via a coordinated re-embed with no code-shape change.index.js+.d.ts).spacecat-shared-data-accesssemantic-index.utils.jsmirroringurl-index.utils.js:syncOpportunitySemantic— full-replace an opportunity's per-source vectors scoped to(entity_id, source_type)(upsert + prune), best-effort.copyEntityVectors—INSERT … SELECTcopy of vector rows to a new entity id (history snapshot, no re-embed).lookupOpportunitiesByVector— the ANN reader (site-scoped, over-fetch + dedup to distinct opportunities).getQueryEmbedding/upsertQueryEmbedding/touchQueryEmbedding—semantic_query_embeddingcache helpers.serializeVector,SEMANTIC_INDEX_TABLESallow-list.Contract
Both the audit-worker write path and the api-service read path embed via the same
AzureEmbeddingClient/text-embedding-3-small(1536 dims, cosine), so the two share one vector space by construction.Notes
feat:minor bump); consumers (audit-worker, api-service) bump the dependency version after the release lands.Related Issues