Skip to content

AI prompts Phase B: client-side loading via injectable fs.FS#1850

Open
OrriMandarin wants to merge 1 commit into
mainfrom
feat/ai-prompts-client-loading
Open

AI prompts Phase B: client-side loading via injectable fs.FS#1850
OrriMandarin wants to merge 1 commit into
mainfrom
feat/ai-prompts-client-loading

Conversation

@OrriMandarin

@OrriMandarin OrriMandarin commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase B of self-hosted AI prompt distribution (Phase A: the SaaS-side download-serving endpoint, PR #1841). This makes the reading side of usecases/ai_agent/* use an injected fs.FS instead of hardcoded os.Open("prompts/...") calls relative to the process working directory.

infra.InitAiPromptsFS resolves the prompts filesystem once at startup, in priority order:

  1. AI_PROMPTS_EXECUTION_DIR env var, if set — used as-is, no resolution, no network (staging's mechanism, pointed at a mounted bucket's stable main/ folder).
  2. AI_PROMPTS_SERVING_DIR (the existing serving-dir config, reused) — if this instance already has the private prompts bucket mounted locally (production's case), the right vX.Y folder is resolved in-process against its own detected version, with no network call. This avoids production calling its own download endpoint on a cold start, which would deadlock.
  3. Network download — a genuine external self-hosted client with no local mount downloads the bundle from the SaaS endpoint, license-gated, with retries (avast/retry-go, matching the existing convention) on network errors and 5xx responses.

cmd/server.go and cmd/worker.go only run this resolution (and the startup validation below) when license.CaseAiAssist is true — a deployment without the AI entitlement never attempts a local lookup or a network call, and never logs a "prompts unavailable" warning for a feature it isn't licensed for.

A deterministic manifest (models.AiAgentExpectedFiles, single source of truth for every prompt path the code reads) drives two verification steps:

  • At download time, inside the retry loop: the zip is filled against the manifest, with two failure modes deliberately told apart — a corrupted/truncated archive (fails to parse, or a file's content fails its checksum) is retried, since that looks like a bad transfer; an expected file simply absent from an otherwise well-formed archive fails immediately (retry.Unrecoverable), since that's a content mismatch (wrong version, stale manifest) no retry will fix.
  • At server/worker startup: ai_agent.ValidatePromptsFS checks the resolved fs.FS (whichever tier produced it) against the same manifest, logging a warning immediately if something's missing, instead of surfacing it later as a per-job failure.

The downloaded bundle is held entirely in memory via a small map-backed fs.FS (models.AiAgentPromptsMapFS) — never written to disk.

If no prompts filesystem is available, worker jobs (async_case_review.go, async_screening_suggestion.go) return the error and let River's normal retry policy handle it, rather than cancelling the job outright — a worker restarted with prompts correctly resolved can still make an already-queued job succeed on a later attempt.

Test plan

  • go build ./..., go vet ./..., golangci-lint run ./... all clean
  • go test ./... passes across the whole repo
  • Manual verification on a deployed instance (staging/self-hosted)
    • AI_PROMPTS_EXECUTION_DIR like on staging
    • AI_PROMPTS_SERVING_DIR like on prod
    • Network download: need to launch a server on local and, change the prod URL to check to check the flow

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • AI prompts can now be loaded from a validated filesystem source, with support for local overrides and version-matched prompt bundles.
    • Prompt version selection now automatically picks the closest compatible version from available prompt folders.
    • AI agent configuration now loads from bundled prompt resources, improving portability.
  • Bug Fixes

    • Startup now warns when prompt resources are incomplete instead of failing unexpectedly.
    • Missing AI prompts are handled more safely, with clearer fallback behavior for async jobs.

@OrriMandarin OrriMandarin self-assigned this Jul 6, 2026
@OrriMandarin OrriMandarin marked this pull request as draft July 6, 2026 16:01
@coderabbitai coderabbitai Bot added enhancement New feature or request go Pull requests that update Go code L Large PR, long review labels Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@OrriMandarin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 973ec08a-ab89-4b3b-a5ce-592e38c3dceb

📥 Commits

Reviewing files that changed from the base of the PR and between 6d649d0 and cf380e4.

📒 Files selected for processing (20)
  • cmd/server.go
  • cmd/worker.go
  • infra/ai_prompts.go
  • infra/ai_prompts_test.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_prompts_test.go
  • pure_utils/prompt_version.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/usecases_with_creds.go
📝 Walkthrough

Walkthrough

This PR introduces a tiered AI prompts filesystem abstraction (fs.FS) resolved from a local execution directory override, a versioned serving directory, or a network download from Marble SaaS. It wires the resolved filesystem through server/worker startup, Usecases options, and the ai_agent usecase's prompt-reading methods, plus a shared prompt path manifest and semver-based version resolver.

Changes

AI Prompts FS Feature

Layer / File(s) Summary
Prompt path manifest, in-memory FS, and model config
models/ai_agent_prompts.go, models/ai_agent_prompts_test.go, models/ai_agent_config.go
Adds AiAgentExpectedFiles manifest and root-relative prompt path constants, an in-memory AiAgentPromptsMapFS implementing fs.FS, and switches LoadAiAgentModelConfig to read from an injected fs.FS instead of a file path.
Semver prompt version resolution
pure_utils/prompt_version.go, pure_utils/prompt_version_test.go, usecases/prompt_serving_usecase.go
Adds ResolveBestPromptVersion to pick the best-matching version directory <= a requested semver, and rewires PromptServingUsecase.resolvePromptsBase to delegate to it.
Tiered prompts FS initialization
infra/ai_prompts.go, infra/ai_prompts_test.go
Adds InitAiPromptsFS with local execution dir override, local serving-dir version match, and network zip download (with retry, auth header, 5xx retry, strict manifest validation via zipToPromptsFS).
Startup wiring and manifest validation
cmd/server.go, cmd/worker.go, usecases/usecases.go, usecases/usecases_with_creds.go, usecases/ai_agent/prompts_manifest.go, usecases/ai_agent/prompts_manifest_test.go
Reads AI_PROMPTS_SERVING_DIR, builds the prompts fs.FS via infra.InitAiPromptsFS, validates it with ValidatePromptsFS (warn-only on failure), and adds WithAIPromptsServingDir/WithAIPromptsFS options replacing WithPromptsDir.
AiAgentUsecase prompt reading via fs.FS
usecases/ai_agent/ai_agent_usecase.go, ai_case_review.go, ai_enrichment.go, ai_rule_assist.go, ai_screening_suggestion.go, ai_agent_usecase_test.go
Adds promptsFS field to AiAgentUsecase, converts readPrompt/preparePrompt to receiver methods, and swaps hard-coded prompt path strings for shared models constants across all prompt consumers.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server as cmd/server.go
  participant InfraInit as infra.InitAiPromptsFS
  participant Downloader as downloadAiPromptsFS
  participant Validator as ai_agent.ValidatePromptsFS
  participant Usecases as usecases.Usecases

  Server->>InfraInit: InitAiPromptsFS(servingDir, version, licenseKey)
  InfraInit->>InfraInit: check AI_PROMPTS_EXECUTION_DIR
  alt execution dir readable
    InfraInit-->>Server: os.DirFS(executionDir)
  else serving dir resolves version
    InfraInit-->>Server: os.DirFS(servingDir/vX.Y)
  else fallback to network
    InfraInit->>Downloader: GET prompts zip (Authorization, prompts_version)
    Downloader-->>InfraInit: zip bytes or nil
    InfraInit->>InfraInit: zipToPromptsFS(bytes)
    InfraInit-->>Server: AiAgentPromptsMapFS or nil
  end
  Server->>Validator: ValidatePromptsFS(aiPromptsFS)
  Validator-->>Server: nil or error (warning logged)
  Server->>Usecases: WithAIPromptsFS(aiPromptsFS), WithAIPromptsServingDir(dir)
Loading

Possibly related PRs

  • checkmarble/marble-backend#1840: Both PRs touch CreateCaseReviewSync in usecases/ai_agent/ai_case_review.go, with this PR changing its prompt-loading wiring.
  • checkmarble/marble-backend#1841: Both PRs modify usecases/prompt_serving_usecase.go version resolution and the AI_PROMPTS_SERVING_DIR/PROMPTS_DIR configuration used by prompt serving.

Suggested labels: enhancement, go, M

Suggested reviewers: Pascal-Delange, apognu

Poem

A rabbit dug three burrows deep for prompts to hide and stay:
One local, one in serving lands, one fetched from far away.
"Speak, filesystem, what dost thou hold?" the validator cried,
And if the manifest came up short, a warning — not denied.
Now fs.FS doth flow through code where hardcoded strings once lay,
🥕 Bundled, versioned, zipped and blessed — the prompts have found their way.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: moving AI prompt loading to an injectable fs.FS on the client side.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-prompts-client-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
pure_utils/prompt_version.go (1)

13-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap the parse error with context.

semver.NewVersion(requestedVersion)'s error is returned bare here — callers only get the underlying semver message with no indication it came from resolving a prompt version. A little context now saves a little confusion anon.

As per coding guidelines, **/*.go: "Wrap errors with context using github.com/cockroachdb/errors package."

♻️ Proposed fix
+	"github.com/cockroachdb/errors"
 	"github.com/Masterminds/semver"
 )
...
 	reqV, err := semver.NewVersion(requestedVersion)
 	if err != nil {
-		return "", err
+		return "", errors.Wrapf(err, "invalid requested prompt version %q", requestedVersion)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pure_utils/prompt_version.go` around lines 13 - 17, Wrap the parse failure in
ResolveBestPromptVersion with context instead of returning the bare
semver.NewVersion(requestedVersion) error; use the cockroachdb/errors package to
annotate the error so callers can tell it happened while resolving a prompt
version. Keep the change localized to ResolveBestPromptVersion and preserve the
original error as the cause while adding a clear message mentioning
requestedVersion.

Source: Coding guidelines

models/ai_agent_config.go (1)

35-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use cockroachdb/errors for wrapping, not fmt.Errorf.

These two error paths wrap with fmt.Errorf("...%w", ...) rather than the repo's mandated github.com/cockroachdb/errors. A small nit, yet as the bard might say: consistency in errors, like consistency in verse, keeps the reader's trust unswerved.

As per coding guidelines, **/*.go: "Wrap errors with context using github.com/cockroachdb/errors package."

♻️ Proposed fix
 	file, err := promptsFS.Open(AiAgentModelConfigFileName)
 	if err != nil {
-		return nil, fmt.Errorf("could not open AI agent config file %s: %w", AiAgentModelConfigFileName, err)
+		return nil, errors.Wrapf(err, "could not open AI agent config file %s", AiAgentModelConfigFileName)
 	}
 	defer file.Close()

 	var config AiAgentModelConfig
 	if err := json.NewDecoder(file).Decode(&config); err != nil {
-		return nil, fmt.Errorf("could not decode AI agent config file %s: %w", AiAgentModelConfigFileName, err)
+		return nil, errors.Wrapf(err, "could not decode AI agent config file %s", AiAgentModelConfigFileName)
 	}

Requires adding "github.com/cockroachdb/errors" to the import block (and dropping fmt if it becomes unused elsewhere in the file).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/ai_agent_config.go` around lines 35 - 44, The error wrapping in
AiAgentModelConfig loading uses fmt.Errorf instead of the repo-standard
github.com/cockroachdb/errors. Update the error paths in the AI agent config
loader logic around promptsFS.Open and json.NewDecoder(file).Decode to wrap with
cockroachdb/errors while preserving the same context and underlying error, and
add the cockroachdb/errors import (removing fmt if it is no longer used in
models/ai_agent_config.go).

Source: Coding guidelines

usecases/ai_agent/async_case_review.go (1)

169-180: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Sound cancellation logic.

Checking errors.Is(err, ErrPromptsUnavailable) on the original err (not handledErr) is the right call — no risk of the sentinel getting lost in the join/wrap performed by handleCreateCaseReviewSyncError. Persisting the Failed status before cancelling is a good touch; the job dies, but the case review record doesn't dangle.

One small thought, worth a line if you're passing this way again: unlike its sibling in async_screening_suggestion.go, this branch doesn't emit an explicit log before cancelling — future debugging of "why did this job vanish" would benefit from a breadcrumb here, much as a herald announces the King's decree before the curtain falls.

📝 Optional: add a log line before cancelling
 		if errors.Is(err, ErrPromptsUnavailable) {
+			logger.WarnContext(ctx, "AI prompts filesystem unavailable, cancelling case review job", "error", err)
 			// No prompts filesystem available for this instance - retrying won't help, only
 			// this job fails, not the process. The Failed status was already persisted above.
 			return river.JobCancel(handledErr)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usecases/ai_agent/async_case_review.go` around lines 169 - 180, Add an
explicit log breadcrumb in the ErrPromptsUnavailable cancellation branch of
async_case_review.go before returning river.JobCancel(handledErr). Keep the
existing errors.Is(err, ErrPromptsUnavailable) check on the original err, but
emit a clear log from the same path in asyncCaseReview handling so it’s obvious
why the job was cancelled, similar to the sibling flow in
async_screening_suggestion.go.
cmd/server.go (1)

388-393: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider deduplicating the init+validate+warn block with cmd/worker.go.

The infra.InitAiPromptsFSai_agent.ValidatePromptsFS → warn-log sequence here is copy-pasted verbatim in cmd/worker.go (lines 366-375 there). Since both files appear to share the same package/import surface, a small shared helper would keep the two entry points from drifting when this logic changes (e.g., adding metrics, changing the log level).

'Tis but a modest split, yet one line changed twice is a bug waiting to be born.

♻️ Proposed extraction
+func initAndValidateAiPromptsFS(ctx context.Context, promptsServingDir, version, licenseKey string) fs.FS {
+	aiPromptsFS := infra.InitAiPromptsFS(ctx, promptsServingDir, version, licenseKey)
+	if err := ai_agent.ValidatePromptsFS(aiPromptsFS); err != nil {
+		utils.LoggerFromContext(ctx).WarnContext(ctx, "ai prompts filesystem failed validation, some ai features may fail",
+			"error", err.Error())
+	}
+	return aiPromptsFS
+}

Then in RunServer:

-	aiPromptsFS := infra.InitAiPromptsFS(ctx, aiPromptsServingDir, config.Version, licenseConfig.LicenseKey)
-	if err := ai_agent.ValidatePromptsFS(aiPromptsFS); err != nil {
-		utils.LoggerFromContext(ctx).WarnContext(ctx, "ai prompts filesystem failed validation, some ai features may fail",
-			"error", err.Error())
-	}
+	aiPromptsFS := initAndValidateAiPromptsFS(ctx, aiPromptsServingDir, config.Version, licenseConfig.LicenseKey)

Also applies to: 417-418

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/server.go` around lines 388 - 393, The ai prompts filesystem
init/validate/warn flow is duplicated between RunServer and the worker
entrypoint, so extract it into a shared helper to keep both paths consistent.
Move the `infra.InitAiPromptsFS` → `ai_agent.ValidatePromptsFS` →
`utils.LoggerFromContext(ctx).WarnContext(...)` sequence into a common function,
then have `RunServer` call that helper instead of inlining the block. Use the
existing symbols `InitAiPromptsFS`, `ValidatePromptsFS`, and `WarnContext` to
locate the logic and keep behavior identical in both callers.
usecases/ai_agent/ai_agent_usecase_test.go (1)

19-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test updates line up with the new receiver-based API.

All six tests correctly swap the package-level preparePrompt call for AiAgentUsecase{promptsFS: os.DirFS(tmpDir)}.preparePrompt(...).

One gap: none of these tests exercise the new nil-promptsFS path (ErrPromptsUnavailable), which is the one behavioral change actually introduced in this PR. A quick added case would lock in that contract.

🧪 Suggested additional test
func TestPreparePrompt_NoPromptsFS(t *testing.T) {
	uc := AiAgentUsecase{}
	_, err := uc.preparePrompt("test_prompt.txt", map[string]any{})
	if !errors.Is(err, ErrPromptsUnavailable) {
		t.Fatalf("expected ErrPromptsUnavailable, got: %v", err)
	}
}

Also applies to: 50-79, 81-105, 107-136, 138-163, 165-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usecases/ai_agent/ai_agent_usecase_test.go` around lines 19 - 48, Add a test
covering the new nil-promptsFS behavior in AiAgentUsecase.preparePrompt: create
an AiAgentUsecase without setting promptsFS, call preparePrompt with any prompt
name and empty data, and assert the error matches ErrPromptsUnavailable. Keep
the existing receiver-based test updates intact and add this case alongside the
other preparePrompt tests so the new contract is locked in.
usecases/ai_agent/ai_agent_usecase.go (2)

201-201: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Constructor keeps growing via positional args.

promptsFS becomes yet another positional parameter tacked onto an already very long NewAiAgentUsecase argument list (20+ params). Easy to introduce an ordering mistake on a future edit (as seen in the sibling call-site file). Worth considering the functional-options pattern here, as is already done for the top-level Usecases struct (WithAIPromptsFS, etc.).

As per coding guidelines, **/*.go: "Use the functional options pattern for dependency injection when initializing repositories, usecases, and feature-specific components."

Also applies to: 232-233, 258-258

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usecases/ai_agent/ai_agent_usecase.go` at line 201, The NewAiAgentUsecase
constructor is growing with another positional dependency, making the long
argument list error-prone. Update NewAiAgentUsecase to accept promptsFS via the
functional-options pattern instead of adding another positional parameter, and
thread it through the AiAgentUsecase initialization using an option similar to
the existing Usecases builders (for example, the same style as WithAIPromptsFS).
Adjust any call sites accordingly so the dependency is configured by option
rather than argument ordering.

Source: Coding guidelines


171-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Sentinel error package inconsistent with cockroachdb/errors guideline.

ErrPromptsUnavailable is declared via errors.New from github.com/pkg/errors (per the library context), while the coding guidelines call for github.com/cockroachdb/errors for error wrapping/context, and a sibling file in this very package (llm_errors.go) already uses cockroachdb/errors (errors.Mark) for sentinel-style errors. Mixing the two error libraries within usecases/ai_agent risks confusion over which Is/As/Mark semantics apply, even though pkg/errors 0.9.1's Unwrap() support keeps errors.Is working here.

As per coding guidelines, **/*.go: "Wrap errors with context using github.com/cockroachdb/errors package."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usecases/ai_agent/ai_agent_usecase.go` around lines 171 - 176,
ErrPromptsUnavailable in ai_agent_usecase.go is using the pkg/errors sentinel
style instead of the package’s cockroachdb/errors convention; update this
sentinel and any related error creation in readPrompt/InitAiPromptsFS call paths
to use github.com/cockroachdb/errors consistently, matching the approach already
used in llm_errors.go with errors.Mark. Keep the symbol name
ErrPromptsUnavailable so callers and non-retryable checks remain unchanged, but
ensure the declaration and any wrapping/context in this package follow
cockroachdb/errors semantics.

Source: Coding guidelines

infra/ai_prompts.go (1)

89-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap resolver errors with CockroachDB error context.

resolveLocalPromptsVersion returns raw os.ReadDir / resolver errors and uses fmt.Errorf; use errors.Wrapf / errors.Newf so startup logs retain actionable context. As per coding guidelines, **/*.go: "Wrap errors with context using github.com/cockroachdb/errors package".

Proposed fix
 	entries, err := os.ReadDir(promptsServingDir)
 	if err != nil {
-		return "", err
+		return "", errors.Wrapf(err, "read ai prompts serving directory %s", promptsServingDir)
 	}
@@
 	best, err := pure_utils.ResolveBestPromptVersion(names, version)
 	if err != nil {
-		return "", err
+		return "", errors.Wrapf(err, "resolve ai prompts version %s", version)
 	}
 	if best == "" {
-		return "", fmt.Errorf("no ai prompts version available for %s in %s", version, promptsServingDir)
+		return "", errors.Newf("no ai prompts version available for %s in %s", version, promptsServingDir)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@infra/ai_prompts.go` around lines 89 - 106, The resolveLocalPromptsVersion
logic in ai_prompts.go is returning plain os.ReadDir and
ResolveBestPromptVersion errors and also uses fmt.Errorf for the empty-version
case, so update those returns to use CockroachDB error helpers instead. Keep the
same flow in resolveLocalPromptsVersion, but wrap the directory read and
resolver failures with errors.Wrapf and create the missing-version error with
errors.Newf so startup logs retain the promptsServingDir and version context.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/ai_prompts_test.go`:
- Around line 189-222: Add a regression test in ai_prompts_test.go for the
InitAiPromptsFS retry path that specifically covers HTTP 429 Too Many Requests.
Mirror the existing retryable 5xx case by using an httptest.Server that returns
429 on the first requests and then succeeds with a ZIP payload, and assert that
InitAiPromptsFS eventually returns a populated FS and that the server was called
multiple times. Keep the existing non-retryable 4xx test separate so the retry
policy distinction remains explicit.

In `@infra/ai_prompts.go`:
- Around line 155-159: The status handling in the response check currently makes
every non-200/non-5xx response unrecoverable, which incorrectly blocks retries
for 429. Update the retry logic in the code path that checks resp.StatusCode so
that 429 Too Many Requests is treated as retryable like the other tier-3
transient failures, while still wrapping truly non-retryable statuses with
retry.Unrecoverable; use the existing status-code branch near the
http.StatusInternalServerError check to locate it.
- Around line 149-163: Add a bounded HTTP timeout and cap archive/entry reads in
the prompts bundle fetch path. Update the request logic around
`http.DefaultClient.Do` to use a client or context with a timeout so startup
can’t hang indefinitely, and replace the unrestricted
`io.ReadAll(resp.Body)`/`io.ReadAll(f)` behavior with size-limited reads in the
bundle extraction flow. Keep the changes localized to the prompts bundle loading
code so the fetch, unzip, and file parsing paths all enforce the same maximum
byte limit.

---

Nitpick comments:
In `@cmd/server.go`:
- Around line 388-393: The ai prompts filesystem init/validate/warn flow is
duplicated between RunServer and the worker entrypoint, so extract it into a
shared helper to keep both paths consistent. Move the `infra.InitAiPromptsFS` →
`ai_agent.ValidatePromptsFS` → `utils.LoggerFromContext(ctx).WarnContext(...)`
sequence into a common function, then have `RunServer` call that helper instead
of inlining the block. Use the existing symbols `InitAiPromptsFS`,
`ValidatePromptsFS`, and `WarnContext` to locate the logic and keep behavior
identical in both callers.

In `@infra/ai_prompts.go`:
- Around line 89-106: The resolveLocalPromptsVersion logic in ai_prompts.go is
returning plain os.ReadDir and ResolveBestPromptVersion errors and also uses
fmt.Errorf for the empty-version case, so update those returns to use
CockroachDB error helpers instead. Keep the same flow in
resolveLocalPromptsVersion, but wrap the directory read and resolver failures
with errors.Wrapf and create the missing-version error with errors.Newf so
startup logs retain the promptsServingDir and version context.

In `@models/ai_agent_config.go`:
- Around line 35-44: The error wrapping in AiAgentModelConfig loading uses
fmt.Errorf instead of the repo-standard github.com/cockroachdb/errors. Update
the error paths in the AI agent config loader logic around promptsFS.Open and
json.NewDecoder(file).Decode to wrap with cockroachdb/errors while preserving
the same context and underlying error, and add the cockroachdb/errors import
(removing fmt if it is no longer used in models/ai_agent_config.go).

In `@pure_utils/prompt_version.go`:
- Around line 13-17: Wrap the parse failure in ResolveBestPromptVersion with
context instead of returning the bare semver.NewVersion(requestedVersion) error;
use the cockroachdb/errors package to annotate the error so callers can tell it
happened while resolving a prompt version. Keep the change localized to
ResolveBestPromptVersion and preserve the original error as the cause while
adding a clear message mentioning requestedVersion.

In `@usecases/ai_agent/ai_agent_usecase_test.go`:
- Around line 19-48: Add a test covering the new nil-promptsFS behavior in
AiAgentUsecase.preparePrompt: create an AiAgentUsecase without setting
promptsFS, call preparePrompt with any prompt name and empty data, and assert
the error matches ErrPromptsUnavailable. Keep the existing receiver-based test
updates intact and add this case alongside the other preparePrompt tests so the
new contract is locked in.

In `@usecases/ai_agent/ai_agent_usecase.go`:
- Line 201: The NewAiAgentUsecase constructor is growing with another positional
dependency, making the long argument list error-prone. Update NewAiAgentUsecase
to accept promptsFS via the functional-options pattern instead of adding another
positional parameter, and thread it through the AiAgentUsecase initialization
using an option similar to the existing Usecases builders (for example, the same
style as WithAIPromptsFS). Adjust any call sites accordingly so the dependency
is configured by option rather than argument ordering.
- Around line 171-176: ErrPromptsUnavailable in ai_agent_usecase.go is using the
pkg/errors sentinel style instead of the package’s cockroachdb/errors
convention; update this sentinel and any related error creation in
readPrompt/InitAiPromptsFS call paths to use github.com/cockroachdb/errors
consistently, matching the approach already used in llm_errors.go with
errors.Mark. Keep the symbol name ErrPromptsUnavailable so callers and
non-retryable checks remain unchanged, but ensure the declaration and any
wrapping/context in this package follow cockroachdb/errors semantics.

In `@usecases/ai_agent/async_case_review.go`:
- Around line 169-180: Add an explicit log breadcrumb in the
ErrPromptsUnavailable cancellation branch of async_case_review.go before
returning river.JobCancel(handledErr). Keep the existing errors.Is(err,
ErrPromptsUnavailable) check on the original err, but emit a clear log from the
same path in asyncCaseReview handling so it’s obvious why the job was cancelled,
similar to the sibling flow in async_screening_suggestion.go.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9d55ec8-9b76-4cd1-8952-693145658b74

📥 Commits

Reviewing files that changed from the base of the PR and between e65ed80 and 63d381a.

📒 Files selected for processing (24)
  • cmd/server.go
  • cmd/worker.go
  • infra/ai_prompts.go
  • infra/ai_prompts_test.go
  • models/ai_agent_config.go
  • models/ai_agent_config_test.go
  • models/ai_agent_prompts.go
  • models/ai_agent_prompts_fs.go
  • models/ai_agent_prompts_fs_test.go
  • pure_utils/prompt_version.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/usecases_with_creds.go
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: test_backend / Test back-end
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: Wrap errors with context using github.com/cockroachdb/errors package
Use custom error types from models/errors.go (BadParameterError, NotFoundError, ForbiddenError, ConflictError, etc.) for domain-specific errors
Use log/slog structured logging with context, providing relevant key-value pairs for debugging
Use the functional options pattern for dependency injection when initializing repositories, usecases, and feature-specific components

Files:

  • usecases/ai_agent/ai_rule_assist.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • cmd/server.go
  • cmd/worker.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • infra/ai_prompts.go
  • models/ai_agent_prompts_fs_test.go
  • usecases/prompt_serving_usecase.go
  • infra/ai_prompts_test.go
  • models/ai_agent_prompts_fs.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
usecases/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

usecases/**/*.go: Use credentials.Get(ctx) to extract organization context and user permissions from the request context in usecases
Feature-specific usecases should be created via factory methods like NewCaseUsecase(), NewDecisionUsecase() for improved modularity

Files:

  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use testify/assert and testify/require for assertions in unit tests

Files:

  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • models/ai_agent_config_test.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • models/ai_agent_prompts_fs_test.go
  • infra/ai_prompts_test.go
{cmd/worker.go,models/river_job.go,jobs/**/*.go}

📄 CodeRabbit inference engine (CLAUDE.md)

Register new River job worker implementations in cmd/worker.go after defining the job args struct in models/river_job.go

Files:

  • cmd/worker.go
usecases/*_usecase.go

📄 CodeRabbit inference engine (CLAUDE.md)

usecases/*_usecase.go: In usecases, validate that all resource IDs from user input (request body, query params, URL params) belong to the caller's organization using enforceSecurity checks or direct org comparison
Fetch referenced resources by ID in usecases and verify they belong to the caller's organization before using them; do not rely on repository Get*ById methods to filter by organization

Files:

  • usecases/prompt_serving_usecase.go
🧠 Learnings (8)
📚 Learning: 2026-02-27T16:10:37.065Z
Learnt from: apognu
Repo: checkmarble/marble-backend PR: 1558
File: usecases/decision_usecase.go:419-430
Timestamp: 2026-02-27T16:10:37.065Z
Learning: In usecases/decision_usecase.go, payload.Data["object_id"] is asserted to string with the assumption that earlier payload validation guarantees a string. This is actionable: review the file for all such type assertions on payload.Data fields and ensure there is prior validation confirming the type. Prefer 1) keeping the constraint explicit in the validation layer, 2) using a typed struct if possible to avoid map[string]interface{} assertions, and 3) using a type assertion with a safe, checked path (e.g., v, ok := payload.Data["object_id"].(string)) where appropriate. Apply this guidance to all similar files in the usecases directory (and nested, if present) to ensure consistency and avoid runtime panics due to unexpected types.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-04-02T09:23:22.291Z
Learnt from: OrriMandarin
Repo: checkmarble/marble-backend PR: 1656
File: usecases/org_export_usecase.go:7-7
Timestamp: 2026-04-02T09:23:22.291Z
Learning: In checkmarble/marble-backend, treat `Priority` as unique only within a scenario: `Priority` must be unique per `(ScenarioId, Priority)` pair. When ordering workflows for a scenario, sort deterministically by `(ScenarioId, Priority)` (not by `Priority` alone). Because this uniqueness invariant makes `(ScenarioId, Priority)` comparisons unambiguous, the resulting ordering will remain deterministic even if the underlying sort implementation is unstable.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-04-13T15:48:04.390Z
Learnt from: OrriMandarin
Repo: checkmarble/marble-backend PR: 1678
File: usecases/org_import_usecase.go:426-433
Timestamp: 2026-04-13T15:48:04.390Z
Learning: In checkmarble/marble-backend, `models.LinkToSingle.Name` values are guaranteed to be unique within an organization. When building lookups of created links in Go (for example, `createdLinksByName`), it’s safe to use `link.Name` as the sole map key and you should not flag the code as a potential key-collision risk based on `Name` uniqueness.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-04-02T08:17:36.762Z
Learnt from: OrriMandarin
Repo: checkmarble/marble-backend PR: 1644
File: models/data_model.go:13-19
Timestamp: 2026-04-02T08:17:36.762Z
Learning: When creating tables via `CreateTableInput` in the marble-backend, ensure `object_id` (String, non-nullable) and `updated_at` (Timestamp, non-nullable) are treated as required, mandatory fields that the frontend must explicitly include in `CreateTableInput.Fields`. The backend should not auto-create these fields during table creation. Do not add `object_id`/`updated_at` to `DataModelReservedFieldNames` (since that blocks creation). Instead, require pre-request validation to confirm these fields are present in the incoming request; post-creation correctness is covered by `commonFieldValidation` in `usecases/data_model_semantic_validation.go`.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • models/ai_agent_prompts_fs_test.go
  • usecases/prompt_serving_usecase.go
  • models/ai_agent_prompts_fs.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-05-11T11:36:03.794Z
Learnt from: Pascal-Delange
Repo: checkmarble/marble-backend PR: 1737
File: usecases/ai_agent/llm_errors.go:32-37
Timestamp: 2026-05-11T11:36:03.794Z
Learning: In checkmarble/marble-backend Go code, when you need to attach a sentinel error (used for downstream `errors.Is`) onto an existing error while preserving the original error chain, use `errors.Mark(originalErr, sentinelErr)` from `cockroachdb/errors` (e.g., `errors.Mark(err, models.LLMRateLimitedError)`). This keeps the original wrapped error(s) intact while making the sentinel detectable. Do not use `errors.Wrap(sentinelErr, err.Error())`, since it discards the original error chain and will prevent `errors.Is` from working as intended.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • cmd/server.go
  • cmd/worker.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • infra/ai_prompts.go
  • models/ai_agent_prompts_fs_test.go
  • usecases/prompt_serving_usecase.go
  • infra/ai_prompts_test.go
  • models/ai_agent_prompts_fs.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-05-20T15:29:22.314Z
Learnt from: apognu
Repo: checkmarble/marble-backend PR: 1719
File: usecases/screening_config_usecase.go:165-166
Timestamp: 2026-05-20T15:29:22.314Z
Learning: In Go 1.26+, the `new()` builtin accepts an expression (not just a type). In reviews, treat `new(expr)` as valid when the repo/tooling is using Go 1.26 or newer: it allocates a `*T` where `T` is the expression’s type, initializes the allocated value to `expr`, and returns the non-nil pointer to that initialized value. Therefore, patterns like `new(models.ScreeningProviderLexisNexis)` (where `ScreeningProviderLexisNexis` is a typed constant) should not be flagged as incorrect. If the codebase targets Go < 1.26, flag `new(expr)` as an error instead.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • cmd/server.go
  • cmd/worker.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • infra/ai_prompts.go
  • models/ai_agent_prompts_fs_test.go
  • usecases/prompt_serving_usecase.go
  • infra/ai_prompts_test.go
  • models/ai_agent_prompts_fs.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-05-20T15:29:22.314Z
Learnt from: apognu
Repo: checkmarble/marble-backend PR: 1719
File: usecases/screening_config_usecase.go:165-166
Timestamp: 2026-05-20T15:29:22.314Z
Learning: In Go 1.26+ (e.g., when the module is building with Go >= 1.26), recognize that `new(expr)` accepts an expression (not just a type). `new(expr)` allocates a new variable whose type is `T` (the expression’s type), initializes it to the value of `expr`, and returns `*T`—so constructs like `new(models.ScreeningProviderLexisNexis)` where `ScreeningProviderLexisNexis` is a typed constant will correctly return a `*models.ScreeningProvider` pointing to that constant’s value, not a zero-value pointer. Do not flag this as incorrect in Go 1.26+ code.

Applied to files:

  • usecases/ai_agent/ai_rule_assist.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/async_case_review.go
  • usecases/ai_agent/async_screening_suggestion.go
  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • cmd/server.go
  • cmd/worker.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • infra/ai_prompts.go
  • models/ai_agent_prompts_fs_test.go
  • usecases/prompt_serving_usecase.go
  • infra/ai_prompts_test.go
  • models/ai_agent_prompts_fs.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_case_review.go
📚 Learning: 2026-03-03T17:21:35.986Z
Learnt from: Pascal-Delange
Repo: checkmarble/marble-backend PR: 1582
File: models/async_decision_execution.go:49-76
Timestamp: 2026-03-03T17:21:35.986Z
Learning: In Go projects, avoid adding MarshalJSON methods on model-level enums. Route all external serialization through a DTO layer that handles type conversions explicitly (e.g., using .String() for enums). This keeps JSON serialization centralized and consistent. Do not create or rely on model-level MarshalJSON for enums; implement or use the DTO conversions instead.

Applied to files:

  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config_test.go
  • models/ai_agent_prompts_fs_test.go
  • models/ai_agent_prompts_fs.go
🔇 Additional comments (22)
models/ai_agent_prompts.go (1)

1-51: LGTM!

models/ai_agent_prompts_fs.go (1)

1-65: LGTM!

models/ai_agent_prompts_fs_test.go (1)

1-60: LGTM!

models/ai_agent_config_test.go (1)

1-58: LGTM!

pure_utils/prompt_version.go (1)

18-33: LGTM!

pure_utils/prompt_version_test.go (1)

1-52: LGTM!

usecases/prompt_serving_usecase.go (1)

18-18: LGTM!

Also applies to: 122-141

usecases/ai_agent/async_screening_suggestion.go (1)

10-10: LGTM!

Also applies to: 34-54

cmd/worker.go (2)

366-376: 📐 Maintainability & Code Quality | ⚡ Quick win

Same init/validate/warn block duplicated from cmd/server.go.

See the deduplication suggestion left on cmd/server.go (lines 388-393) — same logic, same fix applies here.


398-398: LGTM!

usecases/ai_agent/prompts_manifest.go (1)

1-42: LGTM!

usecases/ai_agent/prompts_manifest_test.go (1)

1-63: LGTM! Solid coverage of the new validation branches — no notes, prithee proceed.

cmd/server.go (1)

201-201: LGTM!

usecases/usecases.go (1)

59-60: LGTM! Fits the existing functional-options pattern like a glove — well wired.

Also applies to: 242-252, 281-282, 322-323, 607-607

usecases/ai_agent/ai_screening_suggestion.go (1)

19-24: LGTM!

Also applies to: 100-100

usecases/usecases_with_creds.go (1)

815-841: LGTM!

usecases/ai_agent/ai_agent_usecase.go (2)

445-458: Clean receiver-based rewrite — the FS injection reads true, like a well-forged key to a well-locked door.

readPrompt correctly short-circuits on nil promptsFS with ErrPromptsUnavailable, and preparePrompt now sources content through the receiver. No issues spotted here.


528-545: LGTM!

usecases/ai_agent/ai_case_review.go (2)

36-48: LGTM!


454-458: Sound fallback behavior preserved for non-critical prompts.

Both SYSTEM_PROMPT_PATH and INSTRUCTION_CUSTOM_REPORT_PATH degrade gracefully to hardcoded defaults on read failure rather than aborting the whole case review — consistent with treating these as optional/soft prompts while PROMPT_CASE_REVIEW_PATH (via preparePromptWithModel) remains hard-required.

Also applies to: 846-850

usecases/ai_agent/ai_enrichment.go (1)

19-23: LGTM!

Also applies to: 147-151

usecases/ai_agent/ai_rule_assist.go (1)

18-23: LGTM!

Comment thread infra/ai_prompts_test.go
Comment thread infra/ai_prompts.go Outdated
Comment thread infra/ai_prompts.go
@OrriMandarin OrriMandarin force-pushed the feat/ai-prompts-client-loading branch 2 times, most recently from e5a4c44 to e08b3c1 Compare July 7, 2026 14:50
Comment thread infra/ai_prompts.go
Comment on lines +125 to +131
v, err := semver.NewVersion(version)
if err != nil {
logger.InfoContext(ctx, "non-semver version, skipping ai prompts download", "version", version, "error", err.Error())
return nil
}
majorMinor := fmt.Sprintf("%d.%d", v.Major(), v.Minor())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a question about the dev version. For the dev version, we won’t download any prompts. In this case, the dev version needs to have the prompt locally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the question? 🤓

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hihi,
The question was about the best approach to handle the case where the backend version is dev. Should we enforce the developer to use the AI_PROMPTS_EXECUTION_DIR variable to use the local prompt? or, should the dev backend be able to fetch prompts from the Marble Saas server? If so, which version of the server should it fetch prompts from?

Discussed with @Pascal-Delange, dev backend must use the AI_PROMPTS_EXECUTION_DIR variable, so it can't fetch prompt from our server

@OrriMandarin OrriMandarin force-pushed the feat/ai-prompts-client-loading branch 2 times, most recently from 020a016 to 6d649d0 Compare July 7, 2026 16:10
@OrriMandarin

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot added M Medium PR, 5-10min review max and removed L Large PR, long review labels Jul 7, 2026
@OrriMandarin OrriMandarin marked this pull request as ready for review July 7, 2026 16:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
usecases/ai_agent/ai_agent_usecase_test.go (1)

19-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM — tests now exercise the injected fs.FS path.

Swapping preparePrompt(promptPath, data) for uc.preparePrompt("test_prompt.txt", data) with promptsFS: os.DirFS(tmpDir) correctly mirrors the new receiver-based contract across all six test functions.

One gap worth a mention, though light on urgency: no test yet exercises readPrompt's nil-promptsFS error branch (Line 443 in ai_agent_usecase.go). A quick AiAgentUsecase{}.preparePrompt(...) case asserting the error message would round out coverage of that guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usecases/ai_agent/ai_agent_usecase_test.go` around lines 19 - 205, Add a test
for the nil-promptsFS guard that is still uncovered. Create a case using
AiAgentUsecase with promptsFS unset and call preparePrompt, then assert it
returns the expected error from readPrompt; this will exercise the nil-fs branch
that the current test suite misses. Use the existing preparePrompt and
readPrompt symbols so the new test matches the receiver-based contract.
pure_utils/prompt_version.go (1)

13-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Wrap the semver parse error with context.

return "", err on a bad requestedVersion loses the value that failed to parse — the raw semver error alone ("Invalid Semantic Version") won't tell a caller (or log) which input string was rejected. As per coding guidelines, **/*.go: "Wrap errors with context using github.com/cockroachdb/errors package".

♻️ Proposed fix
+import (
+	"github.com/cockroachdb/errors"
+	"github.com/Masterminds/semver"
+)
+
 func ResolveBestPromptVersion(availableDirNames []string, requestedVersion string) (string, error) {
 	reqV, err := semver.NewVersion(requestedVersion)
 	if err != nil {
-		return "", err
+		return "", errors.Wrapf(err, "invalid requested version %q", requestedVersion)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pure_utils/prompt_version.go` around lines 13 - 17, The semver parse failure
in ResolveBestPromptVersion currently returns the raw error without saying which
requestedVersion was invalid. Update the error handling around semver.NewVersion
so the returned error is wrapped with contextual information using
github.com/cockroachdb/errors, including the requestedVersion value, so callers
can identify the rejected input from the error message.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/ai_prompts.go`:
- Around line 51-58: The AI_PROMPTS_EXECUTION_DIR override check in aiPromptsFS
only verifies that the path exists, so a regular file can still pass and later
break os.DirFS usage. Update the existing os.Stat validation to also reject
non-directory paths before returning os.DirFS(dir), and keep the warning/logging
path in aiPromptsFS so invalid overrides are treated as unavailable.
- Line 106: The new error construction in ai_prompts.go still uses fmt.Errorf
instead of cockroachdb/errors, so update the return in the prompts version
lookup path to use errors.Newf with the same context and remove the now-unused
fmt import. Make this change in the code path that returns the “no ai prompts
version available” error so the file consistently uses the errors package for
wrapped/contextual errors.

---

Nitpick comments:
In `@pure_utils/prompt_version.go`:
- Around line 13-17: The semver parse failure in ResolveBestPromptVersion
currently returns the raw error without saying which requestedVersion was
invalid. Update the error handling around semver.NewVersion so the returned
error is wrapped with contextual information using
github.com/cockroachdb/errors, including the requestedVersion value, so callers
can identify the rejected input from the error message.

In `@usecases/ai_agent/ai_agent_usecase_test.go`:
- Around line 19-205: Add a test for the nil-promptsFS guard that is still
uncovered. Create a case using AiAgentUsecase with promptsFS unset and call
preparePrompt, then assert it returns the expected error from readPrompt; this
will exercise the nil-fs branch that the current test suite misses. Use the
existing preparePrompt and readPrompt symbols so the new test matches the
receiver-based contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5ff5138-2bff-4ed7-ac21-eafa5ca0d214

📥 Commits

Reviewing files that changed from the base of the PR and between e65ed80 and 6d649d0.

📒 Files selected for processing (20)
  • cmd/server.go
  • cmd/worker.go
  • infra/ai_prompts.go
  • infra/ai_prompts_test.go
  • models/ai_agent_config.go
  • models/ai_agent_prompts.go
  • models/ai_agent_prompts_test.go
  • pure_utils/prompt_version.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_agent_usecase.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/prompt_serving_usecase.go
  • usecases/usecases.go
  • usecases/usecases_with_creds.go
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: test_backend / Test back-end
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: Wrap errors with context using github.com/cockroachdb/errors package
Use custom error types from models/errors.go (BadParameterError, NotFoundError, ForbiddenError, ConflictError, etc.) for domain-specific errors
Use log/slog structured logging with context, providing relevant key-value pairs for debugging
Use the functional options pattern for dependency injection when initializing repositories, usecases, and feature-specific components

Files:

  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/prompt_serving_usecase.go
  • models/ai_agent_prompts_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • cmd/worker.go
  • models/ai_agent_prompts.go
  • usecases/usecases.go
  • infra/ai_prompts.go
  • usecases/ai_agent/ai_agent_usecase.go
  • models/ai_agent_config.go
  • infra/ai_prompts_test.go
  • cmd/server.go
usecases/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

usecases/**/*.go: Use credentials.Get(ctx) to extract organization context and user permissions from the request context in usecases
Feature-specific usecases should be created via factory methods like NewCaseUsecase(), NewDecisionUsecase() for improved modularity

Files:

  • usecases/usecases_with_creds.go
  • usecases/prompt_serving_usecase.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
usecases/*_usecase.go

📄 CodeRabbit inference engine (CLAUDE.md)

usecases/*_usecase.go: In usecases, validate that all resource IDs from user input (request body, query params, URL params) belong to the caller's organization using enforceSecurity checks or direct org comparison
Fetch referenced resources by ID in usecases and verify they belong to the caller's organization before using them; do not rely on repository Get*ById methods to filter by organization

Files:

  • usecases/prompt_serving_usecase.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use testify/assert and testify/require for assertions in unit tests

Files:

  • models/ai_agent_prompts_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • infra/ai_prompts_test.go
{cmd/worker.go,models/river_job.go,jobs/**/*.go}

📄 CodeRabbit inference engine (CLAUDE.md)

Register new River job worker implementations in cmd/worker.go after defining the job args struct in models/river_job.go

Files:

  • cmd/worker.go
🧠 Learnings (8)
📚 Learning: 2026-02-27T16:10:37.065Z
Learnt from: apognu
Repo: checkmarble/marble-backend PR: 1558
File: usecases/decision_usecase.go:419-430
Timestamp: 2026-02-27T16:10:37.065Z
Learning: In usecases/decision_usecase.go, payload.Data["object_id"] is asserted to string with the assumption that earlier payload validation guarantees a string. This is actionable: review the file for all such type assertions on payload.Data fields and ensure there is prior validation confirming the type. Prefer 1) keeping the constraint explicit in the validation layer, 2) using a typed struct if possible to avoid map[string]interface{} assertions, and 3) using a type assertion with a safe, checked path (e.g., v, ok := payload.Data["object_id"].(string)) where appropriate. Apply this guidance to all similar files in the usecases directory (and nested, if present) to ensure consistency and avoid runtime panics due to unexpected types.

Applied to files:

  • usecases/usecases_with_creds.go
  • usecases/prompt_serving_usecase.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
📚 Learning: 2026-04-02T09:23:22.291Z
Learnt from: OrriMandarin
Repo: checkmarble/marble-backend PR: 1656
File: usecases/org_export_usecase.go:7-7
Timestamp: 2026-04-02T09:23:22.291Z
Learning: In checkmarble/marble-backend, treat `Priority` as unique only within a scenario: `Priority` must be unique per `(ScenarioId, Priority)` pair. When ordering workflows for a scenario, sort deterministically by `(ScenarioId, Priority)` (not by `Priority` alone). Because this uniqueness invariant makes `(ScenarioId, Priority)` comparisons unambiguous, the resulting ordering will remain deterministic even if the underlying sort implementation is unstable.

Applied to files:

  • usecases/usecases_with_creds.go
  • usecases/prompt_serving_usecase.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
📚 Learning: 2026-04-13T15:48:04.390Z
Learnt from: OrriMandarin
Repo: checkmarble/marble-backend PR: 1678
File: usecases/org_import_usecase.go:426-433
Timestamp: 2026-04-13T15:48:04.390Z
Learning: In checkmarble/marble-backend, `models.LinkToSingle.Name` values are guaranteed to be unique within an organization. When building lookups of created links in Go (for example, `createdLinksByName`), it’s safe to use `link.Name` as the sole map key and you should not flag the code as a potential key-collision risk based on `Name` uniqueness.

Applied to files:

  • usecases/usecases_with_creds.go
  • usecases/prompt_serving_usecase.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
📚 Learning: 2026-04-02T08:17:36.762Z
Learnt from: OrriMandarin
Repo: checkmarble/marble-backend PR: 1644
File: models/data_model.go:13-19
Timestamp: 2026-04-02T08:17:36.762Z
Learning: When creating tables via `CreateTableInput` in the marble-backend, ensure `object_id` (String, non-nullable) and `updated_at` (Timestamp, non-nullable) are treated as required, mandatory fields that the frontend must explicitly include in `CreateTableInput.Fields`. The backend should not auto-create these fields during table creation. Do not add `object_id`/`updated_at` to `DataModelReservedFieldNames` (since that blocks creation). Instead, require pre-request validation to confirm these fields are present in the incoming request; post-creation correctness is covered by `commonFieldValidation` in `usecases/data_model_semantic_validation.go`.

Applied to files:

  • usecases/usecases_with_creds.go
  • usecases/prompt_serving_usecase.go
  • models/ai_agent_prompts_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • models/ai_agent_prompts.go
  • usecases/usecases.go
  • usecases/ai_agent/ai_agent_usecase.go
  • models/ai_agent_config.go
📚 Learning: 2026-05-11T11:36:03.794Z
Learnt from: Pascal-Delange
Repo: checkmarble/marble-backend PR: 1737
File: usecases/ai_agent/llm_errors.go:32-37
Timestamp: 2026-05-11T11:36:03.794Z
Learning: In checkmarble/marble-backend Go code, when you need to attach a sentinel error (used for downstream `errors.Is`) onto an existing error while preserving the original error chain, use `errors.Mark(originalErr, sentinelErr)` from `cockroachdb/errors` (e.g., `errors.Mark(err, models.LLMRateLimitedError)`). This keeps the original wrapped error(s) intact while making the sentinel detectable. Do not use `errors.Wrap(sentinelErr, err.Error())`, since it discards the original error chain and will prevent `errors.Is` from working as intended.

Applied to files:

  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/prompt_serving_usecase.go
  • models/ai_agent_prompts_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • cmd/worker.go
  • models/ai_agent_prompts.go
  • usecases/usecases.go
  • infra/ai_prompts.go
  • usecases/ai_agent/ai_agent_usecase.go
  • models/ai_agent_config.go
  • infra/ai_prompts_test.go
  • cmd/server.go
📚 Learning: 2026-05-20T15:29:22.314Z
Learnt from: apognu
Repo: checkmarble/marble-backend PR: 1719
File: usecases/screening_config_usecase.go:165-166
Timestamp: 2026-05-20T15:29:22.314Z
Learning: In Go 1.26+, the `new()` builtin accepts an expression (not just a type). In reviews, treat `new(expr)` as valid when the repo/tooling is using Go 1.26 or newer: it allocates a `*T` where `T` is the expression’s type, initializes the allocated value to `expr`, and returns the non-nil pointer to that initialized value. Therefore, patterns like `new(models.ScreeningProviderLexisNexis)` (where `ScreeningProviderLexisNexis` is a typed constant) should not be flagged as incorrect. If the codebase targets Go < 1.26, flag `new(expr)` as an error instead.

Applied to files:

  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/prompt_serving_usecase.go
  • models/ai_agent_prompts_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • cmd/worker.go
  • models/ai_agent_prompts.go
  • usecases/usecases.go
  • infra/ai_prompts.go
  • usecases/ai_agent/ai_agent_usecase.go
  • models/ai_agent_config.go
  • infra/ai_prompts_test.go
  • cmd/server.go
📚 Learning: 2026-05-20T15:29:22.314Z
Learnt from: apognu
Repo: checkmarble/marble-backend PR: 1719
File: usecases/screening_config_usecase.go:165-166
Timestamp: 2026-05-20T15:29:22.314Z
Learning: In Go 1.26+ (e.g., when the module is building with Go >= 1.26), recognize that `new(expr)` accepts an expression (not just a type). `new(expr)` allocates a new variable whose type is `T` (the expression’s type), initializes it to the value of `expr`, and returns `*T`—so constructs like `new(models.ScreeningProviderLexisNexis)` where `ScreeningProviderLexisNexis` is a typed constant will correctly return a `*models.ScreeningProvider` pointing to that constant’s value, not a zero-value pointer. Do not flag this as incorrect in Go 1.26+ code.

Applied to files:

  • usecases/usecases_with_creds.go
  • pure_utils/prompt_version.go
  • usecases/prompt_serving_usecase.go
  • models/ai_agent_prompts_test.go
  • usecases/ai_agent/prompts_manifest_test.go
  • usecases/ai_agent/prompts_manifest.go
  • pure_utils/prompt_version_test.go
  • usecases/ai_agent/ai_screening_suggestion.go
  • usecases/ai_agent/ai_rule_assist.go
  • usecases/ai_agent/ai_case_review.go
  • usecases/ai_agent/ai_agent_usecase_test.go
  • usecases/ai_agent/ai_enrichment.go
  • cmd/worker.go
  • models/ai_agent_prompts.go
  • usecases/usecases.go
  • infra/ai_prompts.go
  • usecases/ai_agent/ai_agent_usecase.go
  • models/ai_agent_config.go
  • infra/ai_prompts_test.go
  • cmd/server.go
📚 Learning: 2026-03-03T17:21:35.986Z
Learnt from: Pascal-Delange
Repo: checkmarble/marble-backend PR: 1582
File: models/async_decision_execution.go:49-76
Timestamp: 2026-03-03T17:21:35.986Z
Learning: In Go projects, avoid adding MarshalJSON methods on model-level enums. Route all external serialization through a DTO layer that handles type conversions explicitly (e.g., using .String() for enums). This keeps JSON serialization centralized and consistent. Do not create or rely on model-level MarshalJSON for enums; implement or use the DTO conversions instead.

Applied to files:

  • models/ai_agent_prompts_test.go
  • models/ai_agent_prompts.go
  • models/ai_agent_config.go
🔇 Additional comments (31)
usecases/ai_agent/ai_agent_usecase.go (3)

439-452: LGTM on the fs.FS migration.

readPrompt cleanly guards against a nil promptsFS and wraps fs.ReadFile errors with context. One small thought worth a nod: were the play to lose its script (promptsFS nil), every prompt read fails hard here — by design for template content, unlike the softer fallback used for system instructions in callers. That asymmetry is intentional based on the call sites reviewed, so no action needed.


522-533: LGTM.

preparePromptWithModel correctly threads uc.promptsFS into models.LoadAiAgentModelConfig, which already handles the nil-fs fallback per its documented contract.


171-254: LGTM on struct field and constructor wiring.

promptsFS fs.FS addition to the struct and constructor is straightforward and consistent with the rest of the dependency list.

usecases/ai_agent/ai_case_review.go (2)

37-46: LGTM.

Constants correctly re-point to the shared models.AiAgentPrompt*Path manifest rather than hardcoded literals.


452-452: LGTM — mechanical switch to receiver-based prompt reads.

Both uc.readPrompt(SYSTEM_PROMPT_PATH) and uc.readPrompt(INSTRUCTION_CUSTOM_REPORT_PATH) preserve the existing debug-log-and-fallback pattern, so behavior on missing/unavailable prompts remains unchanged aside from the new fs.FS source.

Also applies to: 844-844

usecases/ai_agent/ai_enrichment.go (2)

19-21: LGTM.

Constants correctly reference models.AiAgentKYCInstructionPath / models.AiAgentKYCPromptEnrichPath.


145-149: 🎯 Functional Correctness

LGTM, though note the fallback asymmetry.

Unlike system-prompt reads elsewhere that fall back to a hardcoded string on error, uc.preparePrompt(INSTRUCTION_PATH, ...) here returns a hard error via errors.Wrap(err, "failed to read instruction") if the prompt is missing — no soft fallback text like the case-review/screening-suggestion system prompts have. Worth confirming this fail-hard behavior for the KYC instruction is intentional, since promptsFS unavailability (e.g., before infra.InitAiPromptsFS resolves, or on network-download failure) would now abort enrichment entirely rather than degrade gracefully.

usecases/ai_agent/ai_rule_assist.go (1)

19-21: LGTM.

Constants correctly re-point to models.AiAgentRuleDescriptionPromptPath, models.AiAgentRuleGenerationStep1PromptPath, and models.AiAgentRuleGenerationStep2PromptPath.

usecases/ai_agent/ai_screening_suggestion.go (2)

20-21: LGTM.

Constants correctly re-point to the shared manifest paths.


97-101: LGTM.

uc.readPrompt(SCREENING_HIT_SYSTEM_PROMPT_PATH) preserves the same debug-log-and-fallback behavior as before, just sourced from the injected fs. All's well that reads well.

infra/ai_prompts.go (5)

162-166: Duplicate: 429 remains unrecoverable.

Already flagged on the previous commit: Line 162 retries only 5xx, so 429 Too Many Requests still falls into retry.Unrecoverable on Line 166, despite the stated 5xx/429 retry contract.


25-47: LGTM!


61-83: LGTM!


111-160: LGTM!

Also applies to: 169-193


195-227: LGTM!

infra/ai_prompts_test.go (3)

174-207: Duplicate: add the 429 retry regression.

Already flagged on the previous commit: the suite proves 5xx retries and 403 does not, but still lacks the required 429 Too Many Requests retry case. A small test here would keep that rate-limit dragon from returning.


22-172: LGTM!


209-312: LGTM!

models/ai_agent_config.go (1)

22-40: 🗄️ Data Integrity & Integration

Confirm hard failure is intended when a prompt FS is present but the model config file is missing.

promptsFS == nil gracefully returns defaults, but once a non-nil FS is injected, a missing AiAgentModelConfigFileName (which is part of AiAgentExpectedFiles, per models/ai_agent_prompts.go) now hard-fails instead of falling back. Since ai_agent.ValidatePromptsFS only warns (doesn't block) on missing manifest files at startup, a resolved-but-incomplete prompts FS could sail through startup validation only to have this call error out at request time. Worth confirming this fail-fast-per-call behavior (vs. falling back to defaults like the nil case) is the intended contract between validation and loading.

models/ai_agent_prompts.go (1)

67-116: LGTM!

models/ai_agent_prompts_test.go (1)

11-60: LGTM!

pure_utils/prompt_version.go (1)

19-33: LGTM on the backward-search selection logic itself — verified against Masterminds/semver's documented "v" prefix and partial-version coercion, and it matches the test expectations exactly.

pure_utils/prompt_version_test.go (1)

10-51: LGTM!

usecases/prompt_serving_usecase.go (1)

122-140: Good delegation to the shared resolver — removes duplicate semver-walking logic, and the error-mapping to models.NotFoundError/models.BadParameterError lines up with Test_PromptServingUsecase_DownloadPrompts_VersionResolution. As the comment on lines 129-131 rightly notes (in fittingly self-aware prose), the err path here is effectively unreachable but kept as a harmless defensive check — no notes.

usecases/ai_agent/prompts_manifest.go (1)

11-26: LGTM!

usecases/ai_agent/prompts_manifest_test.go (1)

22-53: LGTM! Good coverage of the validation contract — fair tests, well wrought, like a sonnet with no missing feet.

cmd/server.go (1)

202-202: LGTM! The license + entitlement guard before touching the network/serving dir is the right call here.

Also applies to: 389-397, 421-422

usecases/usecases.go (1)

59-60: LGTM! Field/option refactor is consistent end-to-end within this file.

Also applies to: 242-252, 281-283, 322-323, 607-607

usecases/usecases_with_creds.go (1)

815-841: LGTM!

cmd/worker.go (2)

367-368: 🗄️ Data Integrity & Integration

No WithAIPromptsServingDir wiring needed in cmd/worker.go. The worker path does not reach NewPromptServingUsecase(), so the empty aiPromptsServingDir here has no effect.

			> Likely an incorrect or invalid review comment.

367-376: 🔒 Security & Privacy

Drop this: cmd/worker.go doesn’t need WithAIPromptsServingDir, and the AI-prompts download path already gates on license validation before serving anything.

			> Likely an incorrect or invalid review comment.

Comment thread infra/ai_prompts.go
Comment thread infra/ai_prompts.go Outdated
@OrriMandarin OrriMandarin force-pushed the feat/ai-prompts-client-loading branch 3 times, most recently from bd75a2d to 0898552 Compare July 8, 2026 14:28
feat: ai_agent reads prompts from an injectable fs.FS (self-hosted download support)

Replaces hardcoded os.Open("prompts/...") reads in usecases/ai_agent with a single
injected fs.FS, resolved at process startup from one of three tiers: an explicit local
execution-dir override, in-process resolution against a locally-mounted prompts bucket
(no network, avoids a cold-start self-call deadlock on Marble SaaS), or a license-gated
network download from the Phase A serving endpoint (PR #1841). A downloaded bundle is
verified against a deterministic manifest of every prompt path the code actually reads
before it's accepted, and the same manifest is checked against the resolved fs.FS once
at server/worker startup so a missing or corrupt prompt surfaces immediately rather than
mid-job.

chore: add timeout when requesting the prompt

fix: linting

avoid download the prompt if the license doesn't allow Case Ai assist

better retry management

If validation failed, remove prompts FS
@OrriMandarin OrriMandarin force-pushed the feat/ai-prompts-client-loading branch from 0898552 to cf380e4 Compare July 8, 2026 14:28
@OrriMandarin

Copy link
Copy Markdown
Contributor Author

For staging, need to push the AI_PROMPTS_EXECUTION_DIR env to point to main folder which contains the latest prompts

@OrriMandarin

Copy link
Copy Markdown
Contributor Author

Wait for https://github.com/checkmarble/terraform/pull/164 deployment on production and staging

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go Pull requests that update Go code M Medium PR, 5-10min review max

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants