feat(serenity): server-compute intent:<Value> on every prompt write path#2785
feat(serenity): server-compute intent:<Value> on every prompt write path#2785byteclimber wants to merge 20 commits into
Conversation
Removes the manual Intent picker requirement by computing intent:<Value> server-side via an LLM call on every Serenity/Semrush prompt write (manual create, edit, AI-generation) -- the intent companion to the already-merged type:branded work (#2772). Mirrors that same unified-layer pattern: a shared classifier factory parameterized with a Serenity-specific 5-value taxonomy (existing DRS 6-bucket behavior byte-for-byte unchanged), a request-scoped write-budget with a single budget-gated retry and hard skip-gate, and a terminal Informational default so a write never fails on classification. serenity-docs#32. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…path Adds body.deferPublish to skip publishAffected on the bulk-create path (flat + subworkspace), so elmo-ui's CSV chunking (serenity-docs#32) can create drafts-only per chunk and publish once on the final, non-deferred chunk of an import — no new endpoint needed since a single CSV import always targets one project. Response now echoes published:true/false.
…tion/taxonomy coverage - test/it/shared/tests/serenity.js: the id-based create IT test asserted an exact tagIds length of 3, missing the new computed intent: tag alongside the existing computed type: tag — now expects 4 (this was breaking CI's it-postgres job). - Adds the dedicated unit tests for the two new modules (intent-classification.js, intent-taxonomy.js) called for in the original implementation plan but not yet written: the full fallback ladder (hard skip-gate, Azure-unconfigured prod/non-prod logging, first-pass success, budget-gated retry success/ exhaustion, retry-skipped-on-no-budget), and parseSerenityIntent's validity- floor/value-matching behavior. Mirrors resolveTypeTagInjection's existing test shape for the new resolveIntentTagInjection sibling in tags.test.js. - Closes the codecov patch-coverage gap flagged on PR #2785 (all three files now at 100% line/branch coverage).
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Request changes - two issues to address in the budget model and API surface.
Complexity: MEDIUM - substantial feature across 18 files with API surface changes.
Changes: Adds server-side LLM-based intent classification on every Serenity prompt write path, with budget-gated retries and graceful fallback to Informational (18 files).
Must fix before merge
- [Important]
writeDeadlineclock drift in brand-provisioning/onboarding flow -src/support/serenity/handlers/markets-subworkspace.js:347(details inline) - [Important]
deferPublishbody flag lacks input validation and observability -src/support/serenity/handlers/prompts.js:590(details inline)
Non-blocking (4): minor issues and suggestions
- nit: O(n^2) dedup via
allTexts.includes(p)is redundant sinceclassifyPromptIntentsalready deduplicates internally vianew Set(...)-src/support/serenity/handlers/markets-subworkspace.js:229 - suggestion: Log when
AI_GEN_CLASSIFY_MAXcap is binding (prompts beyond 16 silently default with no observability signal) -src/support/serenity/handlers/markets-subworkspace.js:245 - suggestion: Consider narrowing
ctx.envto only the 6 keys the classifier needs before threading through handler layers, reducing the trust surface -src/controllers/serenity.js:506 - suggestion: The positional parameter lists (8-9 args) are approaching the threshold where a context-bag refactor (
{ classifyPromptType, env, writeDeadline }) would prevent ordering bugs - track as follow-up
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 3m 50s | Cost: $6.31 | Commit: 634896c304423367a415911d208536d1377ce509
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| @@ -314,6 +345,8 @@ export async function handleCreateMarketSubworkspace( | |||
| competitors = [], | |||
| publishMode = 'require', | |||
| dataAccess = null, | |||
There was a problem hiding this comment.
issue (blocking): writeDeadline = computeWriteDeadline() as a default parameter computes the deadline at function-entry time, not at HTTP request entry. In the brand-provisioning/onboarding flow (handleOnboarding line 698, handleRefreshMarketOnboarding line 1225), this function is called after auth + loadBrand + provisioning steps (potentially 3-7s into the request). Combined with the 12s budget, the effective request time can exceed the ~15s Fastly first-byte timeout the budget was designed to protect.
Fix: Thread writeDeadline from the controller through provisionBrandSubworkspace into handleCreateMarketSubworkspace, mirroring the pattern already established for the flat/subworkspace create/update paths. The default-parameter fallback then only fires in tests.
| invalidateTagCacheForProject(semrushWorkspaceId, pid); | ||
| } | ||
|
|
||
| if (body?.deferPublish === true) { |
There was a problem hiding this comment.
issue (blocking): deferPublish is a new request-body flag that changes behavior (skips the publish step entirely) with no input validation at the controller boundary. If a client accidentally sends deferPublish: true (stale state, copy-paste), prompts silently remain unpublished with no signal to the brand owner.
Fix: (1) Validate deferPublish as a boolean at the controller/Joi schema level so non-boolean values are rejected. (2) Add a structured log when deferPublish fires so the behavior is observable in production. The response already returns published: false which is good, but server-side observability helps detect misuse patterns.
…rPublish Address MysticatBot blocking review findings on PR #2785. writeDeadline clock drift: compute the shared write-budget deadline once at the true request entry (createBrandForOrg, createMarket, activate) and thread it through provisionBrandSubworkspace into handleCreateMarketSubworkspace, mirroring the create/update prompt paths. Previously the onboarding/brand- provisioning flow relied on the default-parameter fallback, which computed the deadline function-entry-late (after auth + loadBrand + provisioning), so total elapsed time could exceed the ~15s Fastly first-byte timeout the budget protects. The default now only fires for direct/test callers. deferPublish validation + observability: add validateDeferPublish() to reject a present-but-non-boolean deferPublish at the write boundary (400) instead of silently treating it as "publish", and log a structured line with created/ skipped/failed counts when the flag fires. Applied to both handleCreatePrompts and its subworkspace twin. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - both prior blocking findings are properly resolved; no new issues.
Complexity: HIGH - large diff (19 files); API surface changes.
Changes: Adds server-side LLM-based intent classification on every Serenity prompt write path, with budget-gated retries and graceful fallback to Informational. The new commit threads writeDeadline from controller entry and validates the deferPublish body flag (18 files).
Note: CI build is still pending - verify before merge.
Non-blocking (2): minor issues and suggestions
- suggestion: Default-parameter
writeDeadline = computeWriteDeadline()inprovisionBrandSubworkspace,generateAndAttachPrompts, andhandleCreateMarketSubworkspacesilently re-computes a fresh deadline when omitted. All production callers currently pass the value explicitly, but a future caller that forgets the parameter gets a deadline computed partway through the request rather than at HTTP entry. Consider making it required (callers supplyInfinityor an explicit value in tests) or adding alog.warnwhen the default fires in non-test environments -src/support/serenity/brand-provisioning.js:108 - suggestion: Server-computed tags (
type:*+intent:*) append aftersanitizeTagIdscaps user-supplied tagIds atMAX_TAG_IDS(50), so the final array can reach 52. This is by-design and tested, but a one-line comment nearsanitizeTagIdsdocumenting the effective max after server injection would prevent confusion if the upstream Semrush API ever enforces a hard cap -src/support/serenity/handlers/prompts.js:50
Previously flagged, now resolved
- writeDeadline clock drift: now computed once at controller entry and threaded via parameter to all write paths
- deferPublish validation:
validateDeferPublish()rejects non-boolean with 400, structured logging emits when the flag fires
Note: QA review did not complete and is not included.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 34s | Cost: $9.21 | Commit: 7dc33116929a12a96f1d9fc07d3f8a26a7403917
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…s#32) Instrument the Serenity write-path intent classifier with EMF metrics and structured logs, layered additively on top of the existing classify/retry/ default ladder (no behavior change): - IntentOutcome counters (classified_ok, low_confidence, retry_attempted, retry_succeeded, defaulted, budget_skipped) dimensioned by WritePath + Workspace. - IntentValueDistribution per WritePath (5 values + defaulted bucket) — the primary degradation signal. - Per-call latency p50/p95, batch duration, and a per-call timeout tally. - A real ProdLlmUnavailable counter (plus the existing warn) on both classifier-construction failure and repeated-invoke failure; prod detection now uses resolveEnvironment (ENV/AWS_ENV) instead of AWS_ENV only. - Distinguish low_confidence soft-failures via inspectSerenityIntent and log truncated reasoning (capped, 200 chars) for debuggability. All emission is best-effort and never throws into the classify path. All 5 call sites pass writePath + workspaceId. 100% patch coverage on the two changed source modules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reintegrate the Serenity intent-classification feature (PR #2785) onto the dimension-root tag model introduced by #2788. The feature was built on the old name-prefix tag model (intent:<Value> wire tags, resolveIntentTagInjection in handlers/tags.js). #2788 replaced that with id-based children of fixed dimension roots resolved via tag-tree.js. This merge ports intent onto the new model, mirroring type exactly: - tag-tree.js: add resolveIntentValueInjection, the intent analog of resolveTypeValueInjection (resolve-or-create the value id under the intent root, return every intent id so callers can strip a client-supplied one). - intent-taxonomy.js / intent-classification.js: return/consume BARE intent values (e.g. Task) instead of intent:<Value> wire tags; the injector resolves the value to a child id. Classify/confidence/retry/budget/observability logic is unchanged. - handlers/prompts.js: add makeIntentInjector (id-based, mirrors makeTypeInjector but driven by the per-request batch-classified map), wired as a sibling classify+inject step into create/update; port deferPublish (CSV-chunking) and the published flag. - prompts-subworkspace.js / markets-subworkspace.js: same intent wiring; AI-gen now partitions prompts by their (type, intent) id pair. - handlers/tags.js: drop the now-dead resolveIntentTagInjection (its replacement lives in tag-tree.js). - controller/brand-provisioning: thread env + writeDeadline through the new signatures alongside main's dynamic-allocation options bag. Tests updated to the id model (every write now attaches an intent tag); intent + deferPublish coverage added. type-check + lint clean; serenity/intent suites pass with 100% patch coverage on the changed lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - observability addition is well-isolated and does not affect classification behavior.
Complexity: HIGH - large diff (19 files); API surface changes.
Changes: Adds CloudWatch EMF metrics (outcome counters, latency percentiles, value distribution) and structured soft-failure logging to the intent-classification write path, with a privacy regression test confirming prompt text never leaks into logs (19 files).
Note: CI build is still pending - verify before merge.
Non-blocking (3): minor issues and suggestions
- suggestion: The
Workspacedimension onIntentOutcomecreates 24N unique CloudWatch metric streams (4 write paths x 6 outcomes x N workspaces). At scale this becomes expensive and makes dashboarding unwieldy. The workspace ID is already in the structured log line (safeLog.info('summary', counts)) for ad-hoc drill-down via Logs Insights. Consider droppingWorkspacefrom EMF dimensions and keeping onlyWritePath+Outcome(bounded cardinality) -src/support/serenity/intent-classification.js:202 - suggestion:
emitObservability()is called at the end ofclassifyPromptIntentsafter the result Map is fully populated. If it throws (e.g. an unexpected error in the forEach iteration), the already-computed result would be lost. A top-leveltry { emitObservability(); } catch { /* best-effort */ }at the call site would make the never-throw guarantee structural rather than relying on each internal emit being individually wrapped -src/support/serenity/intent-classification.js:330 - nit:
PerCallTimeoutcounter fires whenr === null && dt >= PER_CALL_MS, which conflates actual LLM timeouts with slow non-timeout errors (e.g. a network failure that happens to take longer than PER_CALL_MS). The metric name implies timeout specifically. Consider renaming toPerCallSlowFailureor checking for a timeout-specific error type -src/support/serenity/intent-classification.js:286
Previously flagged, now resolved
- writeDeadline clock drift: computed once at controller entry and threaded via parameter (addressed in prior commit)
- deferPublish validation:
validateDeferPublish()rejects non-boolean with 400, structured logging emits when the flag fires (addressed in prior commit)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 4m 9s | Cost: $7.77 | Commit: f3f9bd708c285d852268990195dabca544827a51
If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Related: #2825 (origin-dimension rename) touches the same files ( |
…s endpoint (#2858) ## Summary Adds an **opt-in `userIntent` enrichment** to the Serenity brand-presence prompts endpoint (`GET /v2/orgs/:orgId/brands/:brandId/serenity/brand-presence/prompts`). When `?userIntent=true`, each returned row gains its own classified intent (`userIntent`) alongside the existing `prompt` / `prompt_topic` / `primary_intent` / `volume`. ## How Per-prompt intent is **not a column** on the Semrush `PROMPTS` element, so it's derived the same way branded% already is: the base `PROMPTS` call **+ one `tags:['intent__<Value>']`-filtered call per Semrush intent**, run in parallel (`mapWithConcurrency`) and joined back by prompt text. **Non-fatal** — a classification-call failure degrades each row to `userIntent: ''`, never failing the request. Without the flag, response shape **and upstream call count are unchanged** (protects the existing branded% / intent-coverage caller, `useSerenityPromptHealthStats`). ## Why The Topic Intent Alignment tile ([project-elmo-ui#2478](adobe/project-elmo-ui#2478)) compares each topic's expected intent against the distribution of its prompts' **own** intents. It previously joined against the ~1,000-capped prompt vocabulary; this lets it read per-prompt intent off the **full, uncapped** bp rows. Complements Niccolo's server-computed classification ([serenity-docs#40](adobe/serenity-docs#40), [#2785](#2785)) which populates the `intent` tag this reads back. ## Verified - `npm run type-check` clean · `npx eslint` clean · unit tests (`test/support/elements/elements-service.test.js` + `test/controllers/elements.test.js`) — **131 passing**. - `intent__<Value>` tag filter verified **live** against the PROMPTS element (Lovesac: `intent__Informational`=1156, `intent__Commercial`=128 of 1488). ## Notes / follow-ups - **OpenAPI:** this endpoint is currently an undocumented POC (its siblings stats/sentiment are documented; prompts/weeks/cited-domains aren't). Left undocumented, consistent with that — happy to add a full entry if preferred. - **Integration test:** the elements bp endpoints are POC with deferred ITs; the enrichment is unit-covered. Can add a serenity IT (vendor-mock `intent__` tags) if we want IT coverage here. - **Coordinate with @ntoccane** on exposing per-prompt intent as this read-out, to align with the classification design. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: hjen_adobe <hjen@adobe.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix 1: parseUpdatePromptBody accepted empty/whitespace text on PATCH (only rejected `undefined`), so an edit could reach renamePrompt with a blank prompt that then got classified/defaulted. Now trims and rejects empty text with a 400, mirroring the create path (normalizePromptInput) including the `|| ''` falsy coercion. Shared with the subworkspace twin, so both edit paths are covered. Fix 2: no behavior change. The always-reclassify on edit is intentional and unavoidable: the handler is not sent the old text and upstream has no GET-by-id, so it cannot detect a no-op text edit, and classification must run before the rename for failure-safety. Documented the rationale with a code comment; added a regression test that a rename returning is_updated:false still writes tags and publishes. Fix 3: modernized the stale handleUpdatePrompt tests (and the three edit-path twins in prompts-subworkspace.test.js) that still stubbed the retired delete+create flow. They failed at runtime and one referenced an undefined `createErr`. They now exercise the rename + replace-mode tag-write flow (409 collision, non-404 rename error, tag-write failure after rename, 404 promptNotFound plus its generic-Error negative). Added the intent tag to the recompute assertions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The feature branch left the dynamic-allocation headroom guard half-wired,
breaking type-check + build and a metering test:
- markets-subworkspace.js generateAndAttachPrompts: the caller threaded a
headroom guard as a 6th arg and the JSDoc documented it as required, but the
function never declared or used it (TS8024 + TS2554). Add the param and front
headroom.ensure({ prompts: texts.size }, { includeDrafted: true }) before the
metered createPromptsByIds write loop.
- prompts-subworkspace.js handleCreatePromptsSubworkspace: removed the redundant
pre-publish ensure that re-created a second headroom guard (no-shadow) and
issued a second getWorkspaceResources read. LLMO-6190 meters ONCE before the
write (the create is the metered write, not publish); the pre-write ensure
already reserves the whole batch, so the pre-publish read was a leftover.
- prompts-subworkspace.test.js: the dynamic-allocation test passed its options
object in the env positional slot instead of the 8th, so the guard was never
enabled; corrected to match the controller/signature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the LLMO-6190 create-prompts fronting: handleCreatePromptsSubworkspace now passes headroom.retryOnQuota as publishAffected's wrapPublish, so a disguised metered-405 on a project publish gets one bounded top-up+retry per project before being recorded as a failure (the JSDoc on publishAffected already documented this caller contract; the wiring was missing). Reuses the single guard created before the write — no extra guard, no extra read on the success path. Also fixes the stale positional call in the dynamic-allocation-fronting test (options were passed in the env slot instead of the 8th), matching the controller and the sibling test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integrates 38 commits of main (Project-Engine facade + typed errors, source->origin rename + server-side origin injection, brand-presence userIntent, disguised-405 quota classification, retryOnQuota write-enforcement-lag handling) with feat's server-side intent classification. Also folds in the edit-path + LLMO-6190 headroom fixes (was PR #2875). Reconciliation of the shared prompt write path: - Prompts now carry THREE server-computed dimensions - type + origin (main's makePromptTagInjector) chained with intent (feat's makeIntentInjector); the injectors act on disjoint dimensions so they compose cleanly. - Edit path recomputes type + intent, leaves origin untouched (create/update asymmetry, origin-dimension.md section 3). - Create write keeps main's headroom.retryOnQuota wrapper; feat's (type,intent) byTagSet partitioning retained. - Tests updated for the 3-dimension tag order and main's 8th-arg handler signature. type-check, lint, and full unit suite (15507 passing) all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merged Reviewer, please scrutinize these reconciliation decisions on the shared prompt write path (
No behavior of |
…resolveClosedValueInjection
Collapse the duplicated body into a delegation to resolveClosedValueInjection(
DIMENSION.INTENT), mirroring resolveTypeValueInjection exactly. Behavior-
preserving: same param order, same { computedId, intentTagIds } shape, same
fail-closed (502) path. Add the resolveIntentValueInjection unit block as a
twin of the type cases (resolve+strip, on-demand create, 502, transport error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ty guards Add the spec's bolded acceptance test: a customer category named Commercial survives a write that computes intent=Commercial (strip is by intent-root id, never by name). Add a byte-for-byte guard that DRS_CATEGORY_SPEC.systemPrompt is the same SYSTEM_PROMPT reference, locking the native 6-bucket prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict: REQUEST_CHANGES | Complexity: HIGH | Re-review (7 new commits since last APPROVE at f3f9bd7)
Critical
1. Committed node_modules symlink breaks all checkouts
A tracked symlink node_modules -> /Users/ntoccane/Desktop/mysticat-workspace/spacecat-api-service/node_modules was added. The .gitignore has node_modules/ (trailing slash = directory only), so git tracks the symlink (a file). This will:
- Break
npm ci/npm installon every other machine and CI runner (the target path doesn't exist) - Prevent git from creating the
node_modules/directory while a tracked symlink with that name exists - Expose the author's filesystem layout
Fix: Remove the tracked symlink (git rm node_modules) and add a bare node_modules line (no trailing slash) to .gitignore to prevent recurrence.
Important
2. makeIntentInjector caches rejected promises permanently
The intent injector caches the resolveIntentValueInjection Promise keyed by (projectId, intentValue). If the LLM call fails with a transient error (e.g., 502), the rejected promise is cached for the lifetime of the request. Every subsequent prompt with the same key awaits the same rejected promise and fails — a single transient failure poisons the entire batch for that intent value.
Suggestion: Evict the cache entry on rejection, or wrap with a retry-aware cache that only stores resolved values.
3. Budget model can overrun the 6s classification reserve
The cursor = unique.length abort stops new iterations but does not cancel in-flight LLM workers (no AbortController). Under Azure slowness (e.g., 2.5s/call), workers already dispatched continue to completion past budget expiry. The publish step may then start 9s+ into the Lambda, risking Fastly/CDN gateway timeout on the full response.
Suggestion: Wire an AbortController signal into the fetch calls and abort in-flight requests when the budget expires, rather than relying solely on the cursor trick.
4. No circuit breaker for repeated Azure OpenAI failures
When Azure OpenAI is down, every write request still attempts the full classification budget (~6s) before defaulting. With high write volume this adds significant latency across all requests during the outage window.
Suggestion: Track consecutive request-level classification failures; skip classification for 30-60s after N consecutive failures (lightweight circuit breaker). The ProdLlmUnavailable metric already detects the condition.
5. OpenAPI spec not updated for new deferPublish query parameter
The PR adds deferPublish (boolean query param) to the create-prompts endpoints but does not update the OpenAPI spec. Per the repo's CLAUDE.md: "Update OpenAPI spec when adding or modifying API endpoints."
6. computeWriteDeadline() evaluated per call site, not per request
Multiple function signatures use writeDeadline = computeWriteDeadline() as a default parameter. This means any caller that omits the argument gets a fresh deadline computed at their call time rather than the request-entry deadline — defeating the single-clock design. Currently safe (the controller always passes explicitly), but a latent trap for future callers.
Minor
8 minor findings (click to expand)
.gitignoreneeds barenode_modulesentry — currentnode_modules/only matches directories, not symlink files- Magic numbers for budget constants —
AI_GEN_CLASSIFY_MAX = 16,PER_CALL_MS = 3000,CLASSIFY_CONCURRENCY = 8lack explanatory names or comments for the derivation allTexts.slice(0, AI_GEN_CLASSIFY_MAX)silently drops tail prompts — texts beyond index 16 are never classified and silently default toInformationalwith no log/metric noting the cap was hitpublishedresponse field not in OpenAPI spec — new response shape undocumented- No boundary test at exactly
AI_GEN_CLASSIFY_MAX(16/17 items) — the cap behavior is untested at the boundary deferPublishpartial-batch scenario untested — no test verifiespublished: falsealongside mixedcreated/failedarrays- Twin-file duplication continues to grow —
prompts.js/prompts-subworkspace.jsnow wire intent injection identically; no convergence tracker visible - Flat-mode
handleCreatePromptsmissingretryOnQuotaon publish — subworkspace twin passes it but flat-mode does not (pre-existing gap, worth tracking)
Notes
- CI status: build pending at time of review; type-check and validate-pr-title passing.
- Prior findings (last review): writeDeadline clock drift and deferPublish validation issues from earlier reviews appear addressed in the new commits.
- Design assessment: The core architecture (batch-classify once per request, budget-gated retry ladder, terminal default to Informational, three-dimension tag composition on disjoint dimensions) is sound. The fail-open design ensures classification failures never block writes. CloudWatch EMF observability is well-structured.
- Spec divergence detected: OpenAPI spec requires update before merge (see Important #5).
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 3m 58s | Cost: $8.96 | Commit: b8f0ec042abbbcc307842c38e150f30af30dc8b7
If this code review was useful, please react with 👍. Otherwise, react with 👎.
The reconciliation merge (683f7b9) tracked a node_modules symlink pointing at an absolute local path, which breaks npm ci / checkouts on every other machine and CI runner. Untrack it and add a bare node_modules .gitignore entry (the existing node_modules/ only matches directories, not the symlink file) so it can't recur. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sify cap Address MysticatBot review on #2785: - OpenAPI: add the deferPublish request flag and the published response field to the create-prompts schemas (were undocumented — CLAUDE.md requires the spec track endpoint changes). - Observability: log one line when the AI-gen classify cap (AI_GEN_CLASSIFY_MAX) binds, so a silently-defaulted tail is visible rather than invisible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rage Address MysticatBot minor findings on #2785: - boundary test at AI_GEN_CLASSIFY_MAX+1: only the first MAX texts are classified, the overflow defaults to Informational, and the cap-hit logs. - deferPublish partial-batch: published:false alongside a mixed created/failed batch, with no publish triggered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Mysticat review failed: Claude CLI crashed (exit 1): stderr= stdout={"type":"result","subtype":"error_max_budget_usd","duration_ms":3,"duration_api_ms":1260206,"is_error":true,"num_turns":1,"stop_reason":null,"session_id":"e3fef99f-3f60-4b2a-956e-d802192838a5","total_cost_usd":10.153901549999999,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_i |
The OpenAPI contract test's sample response predates the published field and failed AJV validation once published became required on the response schema. The handler always returns published, so add it (true) to the fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - all prior blocking findings are resolved; no new issues.
Complexity: HIGH - large diff (23 files); API surface changes.
Changes: Adds server-side LLM-based intent classification on every Serenity prompt write path, with budget-gated retries and graceful fallback to Informational. The new commits fix the node_modules symlink, document deferPublish/published in the OpenAPI spec, add cap-hit logging, and cover boundary + partial-batch scenarios in tests (23 files).
Note: Recommend a human read before merge - shared contract change (OpenAPI spec modified: docs/openapi/schemas.yaml). The bot review is a complement to, not a replacement for, a human read here.
Note: CI build/integration checks are still pending - verify before merge.
Non-blocking (3): minor issues and suggestions
- suggestion:
Workspacedimension on theIntentOutcomeEMF metric creates unbounded CloudWatch metric streams (one per workspace). Consider droppingWorkspacefrom EMF dimensions and keeping onlyWritePath+Outcome(bounded cardinality); the workspace ID is already in the structured log line for ad-hoc drill-down via Logs Insights -src/support/serenity/intent-classification.js:202 - nit:
.gitignorenow has bothnode_modules/(directories only) and barenode_modules(any path component); the bare entry subsumes the directory match, making the trailing-slash line redundant -.gitignore:3 - suggestion: The AI-gen cap boundary test exercises cap+1 (3 texts with cap=2). Consider also asserting at exactly cap (2 texts, no log fired) to confirm the
>check is not an off-by-one>=-test/support/serenity/handlers/markets-subworkspace.test.js:1586
Previously flagged, now resolved
- node_modules symlink removed (4442c57)
- OpenAPI spec updated with deferPublish request field and published response field (02f4ec6)
- AI-gen classify cap now logs when it binds, making the defaulted tail observable (02f4ec6)
- Boundary test at AI_GEN_CLASSIFY_MAX added (a4c937f)
- deferPublish partial-batch scenario now tested (a4c937f)
- Prior Important findings (rejected-promise cache, budget overrun, circuit breaker) re-evaluated: all mirror established codebase patterns, are bounded by the request lifecycle and fail-open design, and are observable via existing metrics - accepted as reasonable v1 tradeoffs
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 8s | Cost: $6.72 | Commit: ad26ff3ca79776f5b0500a9178a5200e3b610886
If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Correctness: trim mismatch silently defaults classified intent to In both const intentByText = await classifyPromptIntents(
inputs.map((raw) => String(raw?.text || '')), // NOT trimmed
{ env, log, deadline: writeDeadline, writePath: deferPublish ? 'csv' : 'create', workspaceId },
);
const intentValue = intentByText.get(input.text) ?? INTENT_VALUE.INFORMATIONAL;…and Impact: the flagship use case here is chunked CSV import ( Why the tests don't catch it: every create/CSV fixture uses clean text ( Fix: classify the normalized text so the keys line up, e.g. For reference: the edit path ( |
|
Should fix: new env var
function resolveDeploymentName(env = {}) {
return env.PROMPT_INTENT_CLASSIFICATION_DEPLOYMENT_NAME
|| env.AZURE_OPEN_AI_API_DEPLOYMENT_NAME;
}It falls back safely when unset (behavior unchanged until explicitly configured), but the new variable isn't listed anywhere operators would look (env docs / deploy config). Please document it so the override is discoverable. |
|
Should fix (defensive): make the "classify key == lookup key" contract explicit This is a follow-up to the trim-mismatch bug. The bug class is easy to reintroduce because the two injectors normalize differently:
Once the classify input is fixed to use normalized text, a short comment at the |
|
Nit: redundant The diff adds |
Addresses review feedback on #2785: - Correctness: handleCreatePrompts / handleCreatePromptsSubworkspace classified the UN-trimmed raw text, but makeIntentInjector looks up by input.text (which normalizePromptInput trims). A whitespace-padded prompt (common in CSV import) missed the map and silently defaulted to Informational despite a real classification. Classify the trimmed text so the keys align; add a regression test that a padded input keeps its non-default intent. - Defensive: comment the "classify key == lookup key" contract at both call sites so the alignment is not silently broken by a future edit. - Docs: document PROMPT_INTENT_CLASSIFICATION_DEPLOYMENT_NAME (the classifier- scoped Azure deployment override) in the serenity operator guide's env table. - .gitignore: collapse the redundant node_modules entries to a single bare `node_modules` (matches the dir AND a stray symlink; the trailing-slash form would only match directories). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @aliciadriani — all four addressed in
Full unit suite green (15701) + type-check + lint. |
Summary
intent:<Value>server-side via an LLM call on every Serenity/Semrush prompt write path (manual create, edit, AI-generation), mirroring the already-mergedtype:brandedunified layer (feat(serenity): server-compute branded/non-branded prompt type on every write path #2772, serenity-docs#31).createIntentClassifier(src/support/intent-classifier.js) with acategorySpec, defaulting to the native/DRS 6-bucket taxonomy reproduced byte-for-byte, and adds a Serenity-specific 5-value config (src/support/serenity/intent-taxonomy.js) with the validated system prompt + confidence floor.src/support/serenity/intent-classification.js): a single shared write-deadline, one budget-gated retry, a hard skip-gate, and a terminalInformationaldefault — a classification failure never blocks a write.intent:*injector (mirroringmakeTypeInjector/resolveTypeTagInjection) intohandleCreatePrompts(Subworkspace)andhandleUpdatePrompt(Subworkspace)(classify-before-delete), and into AI-generation (generateAndAttachPrompts, capped atAI_GEN_CLASSIFY_MAX=16, replacing the old staticintent:Informationaldefault).Test plan
npm run lint/npm run type-checkcleanmain's baseline (pre-existing/unrelated: missing@adobe/spacecat-shared-akamai-clientpackage, unrelated Suggestions/GeoExperiment tests)(serenity-docs#31)-style test coverage inprompts.test.js,prompts-subworkspace.test.js,markets-subworkspace.test.js,serenity.test.js,brand-provisioning.test.jsto assert the injectedintent:<Value>tag alongside the existingtype:*assertions