Skip to content

feat(prompts): DB-backed prompt library with per-preset and per-partition selection - #835

Merged
andyne13 merged 28 commits into
developfrom
feat/pm-backend
Jul 30, 2026
Merged

feat(prompts): DB-backed prompt library with per-preset and per-partition selection#835
andyne13 merged 28 commits into
developfrom
feat/pm-backend

Conversation

@andyne13

@andyne13 andyne13 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Adds DB-backed prompt management: a global prompt library with a default per
type, selected per preset (indexation/retrieval) and per partition
(generation), and wired into every stage that sends a prompt to a model.

Closes #772.

What it does

Seven prompt types (sys_prompt, query_contextualizer, chunk_contextualizer,
image_captioning, hyde, multi_query, topic_tagger) become editable rows
instead of read-only files on disk. Resolution is one seam:

resolve_prompt(prompt_type, names=[...]) -> str

first resolvable candidate name → the type's global default → the bundled disk
seed. Callers pass precedence-ordered names, so per-user personalisation can
later prepend a name without changing the signature.

Selection follows where the setting belongs:

Prompt Selected on
sys_prompt partition generation_prompt_names
query_contextualizer, hyde, multi_query retrieval preset
chunk_contextualizer, image_captioning, topic_tagger indexation preset

Preset fields serialise into the existing preset JSONB (no preset migration);
partitions get one JSONB column.

Notable choices

  • Resolution timing. Query/generation prompts resolve per request, so an
    edit takes effect with no restart. Ingest prompts resolve once per file, not
    per chunk — at 2K-document batches a per-chunk lookup would be a hot-path DB
    read, and per-actor caching would need cross-Ray cache coherence.
  • Templates validated at write time. Types rendered with str.format accept
    only their own placeholders, so a bad template is a 422 on save instead of a
    500 on every later chat. The other three are sent verbatim and may contain
    braces.
  • One default per type is a partial unique index, promoted by clear-then-set
    inside a locked transaction, not application-level bookkeeping.
  • References are soft. Presets and partitions name prompts by string; a
    deleted or renamed prompt falls back to the default rather than breaking
    retrieval. Deleting a type's default is refused.
  • spoken_style_answer is removed — it had no consumer; sys_prompt is the
    only answer prompt.

Verification

All seven prompts were proven end-to-end on a live deployment: each was given a
uniquely-worded library prompt, selected through its own config surface, and the
logs confirmed both that it resolved (prompt.resolve … <- named:…) and that
its text reached the model (llm.call … system[…]: …). sys_prompt was also
confirmed behaviourally — the answer came back in the style the prompt asked
for, with no restart. Image captioning logs <image_url> in place of the
base64 payload.

Both log lines sit at DEBUG (LOG_LEVEL=DEBUG), so production stays quiet.

Unit suite 2258 green; repo-integration green against Postgres (two pre-existing
failures in test_partition_repo.py also fail on develop, unrelated).

Follow-ups (deliberately not in scope)

  • Preset *_prompt_name references are not validated at write time, while
    partition ones are — a preset typo silently falls back to the default.
  • used_by counts effective resolution, so a type's default shows every
    partition that would fall back to it, including partitions whose retriever
    never invokes that type.
  • Seeding races if several replicas boot into an empty database at once
    (a pre-existing pattern shared with the other seeded tables).

Summary by CodeRabbit

  • New Features
    • Added an admin prompt library for creating, listing, updating, deleting, and promoting prompts as defaults.
    • Added support for the spoken_style_answer prompt type.
    • Enabled partition-level generation prompt overrides (with prompt resolution and automatic prompt seeding).
    • Added configurable per-call system prompt overrides for contextualization and topic tagging.
    • Added bounded debug logging for LLM calls across supported inference backends.
  • Bug Fixes
    • Improved validation for generation prompt overrides, including missing-prompt detection and conflict-safe default handling.
    • Made prompt resolution resilient by falling back to bundled prompt templates when repository lookups fail.

andyne13 added 17 commits July 29, 2026 15:30
The prompts library table (named prompt templates, one is_default per type via
a partial unique index) plus partitions.generation_prompt_names (JSONB) for
per-partition generation-prompt selection. PgPromptRepository replaces the stub
with CRUD, get_by_name, and atomic set_default; idempotent Alembic migration.
Integration tests against real Postgres.

Refs #772
Unified resolver resolve_prompt(prompt_type, names=[...]): first resolvable
candidate name -> global default -> on-disk seed. The ordered name list is the
extension point for a future per-user tier (prepend the user's name). Seeds the
library from bundled templates on boot; library CRUD with a delete-default
guard and atomic default promotion. Lazy prompt_service in the container +
get_prompt_service provider + a "seeding prompts" startup step.

Refs #772
/prompts router (admin-only): list, get, create, patch, set-default, delete,
with prompt_type as a Literal (edge 422s) and a non-empty-content guard.
Registered in main.py under the Prompts tag. Transport tests cover forwarding,
response shaping, 422 validation, and domain-error status mapping.

Refs #772
…(generation)

Wire the resolution seams to the unified resolver:

- Indexation preset gains contextualization/image_captioning/topic_tagging_prompt_name;
  the Ray indexer resolves them once per file (actor already holds the catalog),
  threading the resolved text into the contextualize/topic-tag stages (which gain
  an optional per-call system_prompt override) and the existing caption_prompt row key.
- Retrieval preset gains hyde/multi_query_prompt_name (retrieval-service wiring
  is a follow-up; those still load from disk, no behaviour change).
- Partition gains generation_prompt_names (JSONB) for sys_prompt/spoken_style_answer/
  query_contextualizer, editable via UpdatePartitionRequest and threaded through
  PartitionRow/PartitionConfig/partition_repo; QueryService resolves them for the
  single owning partition (multi-partition/"all" -> global default), matching chat_llm.

Refs #772
- F1: unique index on (prompt_type, name) — the selection key must be unique
  for get_by_name to be deterministic. Require non-empty names, and reject
  duplicate/rename collisions with a 409 in PromptService.
- F2: expose generation_prompt_names in PartitionDetailResponse / _partition_detail
  (was write-only — the UI could set but not read it back).
- F3: validate assigned generation_prompt_names against the library at
  assignment time (symmetry with chat_llm), so a typo'd name is a 422 instead
  of a silent fallback to the default.
- Fix a stale delete() comment that referenced the removed partition_prompts FK.

Refs #772
list_prompts now annotates each prompt with used_by — the number of partitions
(generation_prompt_names) and presets (*_prompt_name config) that reference it
by name, from one bulk aggregate (PromptRepository.reference_counts, JSONB
jsonb_each_text over partitions + pipeline_presets). Surfaced as PromptResponse.used_by
for the admin-UI badge.

Refs #772
Move query_contextualizer off the partition's generation prompts onto the
retrieval preset (query_contextualizer_prompt_name), and wire the retrieval
prompt seam: hyde/multi_query/query_contextualizer now resolve named->default
->disk via PromptService instead of loading disk-only. type=single touches no
prompt. Generation prompts are now purely answer-side (sys_prompt,
spoken_style_answer).
…ly answer prompt

Drop the spoken-style answer capability entirely: the query-service branch and
metadata flag, the Chainlit SpokenStyleAnswer command (i18n), the PromptType
enum member, PromptsConfig field, disk template, the prompts CHECK constraint,
and the seed set (8 -> 7 managed types). The partition now selects only
sys_prompt (the final-answer prompt).
Admin-editable prompts for the 4 str.format-rendered types (sys_prompt,
query_contextualizer, hyde, multi_query) are validated on create/update: unknown
placeholders or malformed/unbalanced braces are rejected with 422 instead of
crashing the chat/retrieval hot path at .format() time (globally for a default
edit). Verbatim types (contextualizer/captioning/tagger) are unrestricted.
Remove the dead prompts.spoken_style_answer config key, the API metadata doc
row + example, the env-vars template row, and correct a stale comment.
reference_counts() counted only explicit namings, so a seeded default (used by
every non-overriding partition via fallback) showed 0/Unused. Now count
effective resolution: each partition resolves each type to a named prompt (when
its partition/preset config names an existing one) or the type's default, and
the default absorbs the non-overriders. Per type the counts sum to the
partition total.
resolve_prompt now emits one INFO line per call — prompt type, offered
candidate names, how it resolved (named/default/disk-seed), the resolved name,
length, and an 80-char preview — so operators can confirm in the logs exactly
which library prompt every indexation/retrieval/chat stage used.
Line-reflow only, left behind by the last two prompt commits — the format
gate was red on these three files.
prompt.resolve proves which library prompt a stage resolved; it does not
prove the resolved text reached the model. Add a companion llm.call DEBUG
line in the vLLM and Ollama clients (chat, stream_chat, generate, and VLM
image captioning) previewing every outbound message by role and length.

Previews are built lazily, so nothing is paid above DEBUG, and multimodal
image parts are reduced to a <image_url> marker so base64 payloads never
reach the log.
prompt.resolve fired at INFO on every chat request and every indexing job,
carrying prompt text; demote it to DEBUG so it pairs with llm.call and a
production deployment stays quiet. Build its preview lazily too.

Also stop llm.call printing its payload twice: the lazy preview was passed
as a kwarg, and loguru copies kwargs into record["extra"], which the
terminal formatter appends. Pass it positionally instead.
create_prompt and the rename path check for a name clash before writing,
but the check and the write are not atomic: two concurrent admins both pass
it and the loser reaches uix_prompts_type_name, where an unhandled
UniqueViolationError became a 500 from the generic exception handler.

Translate it in the repository into the same 409 the sequential path
returns, distinguishing a raced default promotion, and mirroring how
PgPartitionRepository already handles partition-name collisions.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a DB-backed prompt library with admin CRUD routes, global defaults, partition-specific prompt selection, runtime resolution across query, retrieval, and indexing flows, and bounded DEBUG logging for Ollama and vLLM calls.

Changes

DB-backed prompt library

Layer / File(s) Summary
Prompt contracts and persistence
openrag/core/ports/prompt_repo.py, openrag/services/persistence/..., openrag/core/models/preset.py
Adds prompt repository operations, the prompts table, default constraints, partition prompt mappings, migrations, and async PostgreSQL CRUD/default-resolution logic.
Prompt service and admin API
openrag/services/orchestrators/prompt_service.py, openrag/api/routers/admin/prompts.py, openrag/api/schemas/admin/*, openrag/di/*
Adds prompt seeding, validation, resolution, admin CRUD endpoints, OpenAPI wiring, dependency providers, and container initialization.
Partition-aware prompt execution
openrag/services/orchestrators/{query_service,retrieval_service,partition_service}.py, openrag/services/workers/*, openrag/core/{config,indexing}/*
Resolves configured prompts for query generation, retrieval expansion, contextualization, topic tagging, and indexing jobs, with disk fallback where applicable.
Prompt behavior validation
tests/unit/**, tests/integration/repos/*
Covers repository CRUD and defaults, prompt seeding and resolution, API validation, container wiring, retrieval resolution, and partition prompt round trips.

LLM call logging

Layer / File(s) Summary
Bounded inference call logging
openrag/services/inference/_call_log.py, openrag/services/inference/{ollama_client,vllm_client}.py
Adds lazy, bounded DEBUG logging for prompt, message, model, endpoint, and streaming metadata before inference requests.
Logging behavior validation
tests/unit/services/inference/test_call_log.py, tests/unit/services/orchestrators/test_prompt_service.py
Tests clipping budgets, brace-safe formatting, repository-failure fallback, and log output behavior.

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

Possibly related PRs

Suggested labels: enhancement, admin-ui

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The backend library is added, but the linked issue also requires admin UI and partition assignment routes, which aren't shown in the change set. Add the missing admin prompt-management UI and explicit per-partition assignment/unassignment routes, and ensure the partition override model matches the issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely summarizes the main change: a DB-backed prompt library with preset and partition selection.
Out of Scope Changes check ✅ Passed The visible changes are tied to prompt management, and no clearly unrelated code paths stand out from the provided summary.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pm-backend

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: 7

🧹 Nitpick comments (1)
openrag/services/orchestrators/retrieval_service.py (1)

262-266: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential per-partition prompt resolution — consider bounding/parallelizing.

_pipeline_for_partition is awaited one partition at a time. For hyde/multiQuery presets this triggers a DB round-trip per partition; with the "all" sentinel expanded to every partition (the SUPER_ADMIN_MODE case this file explicitly calls out for _gather_partition_groups, #708), this loop serializes N round-trips instead of running them concurrently (optionally bounded by max_partition_concurrency, as already done for the retrieval fan-out itself).

♻️ Suggested direction
-        groups: list[tuple[list[str], RetrieverPipeline, int | None]] = []
-        for partition in partitions:
-            pipeline, default_top_k = await self._pipeline_for_partition(partition)
-            groups.append(([partition], pipeline, default_top_k))
-        return groups
+        results = await self._gather_partition_groups(
+            [self._pipeline_for_partition(partition) for partition in partitions]
+        )
+        return [([partition], pipeline, default_top_k) for partition, (pipeline, default_top_k) in zip(partitions, results)]
🤖 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 `@openrag/services/orchestrators/retrieval_service.py` around lines 262 - 266,
Update the partition loop in _gather_partition_groups to resolve
_pipeline_for_partition concurrently rather than awaiting each partition
serially. Reuse the existing max_partition_concurrency pattern or apply an
equivalent bounded concurrency mechanism, while preserving each partition’s
pipeline/default_top_k grouping and output behavior.
🤖 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 `@openrag/services/inference/_call_log.py`:
- Around line 25-27: Update _preview to pseudonymize email addresses in the
normalized text before returning the preview, ensuring chat messages, retrieval
context, and vision prompts never expose raw email addresses at DEBUG while
preserving the existing whitespace normalization and length truncation behavior.
- Around line 78-82: Update the _detail function to cap the number of messages
and multimodal parts included in the rendered llm.call detail, then enforce a
maximum length on the complete joined result. Preserve the existing prompt
formatting and previews while truncating or summarizing excess content so DEBUG
logs remain bounded.
- Around line 88-90: Normalize and length-bound the user-controlled caller,
model, and endpoint values in the logging path before using them. Update the
logger.bind call and the formatted debug message in the surrounding call-logging
function so both consume the same sanitized values, preventing newlines or
oversized metadata from creating forged log entries.

In `@openrag/services/orchestrators/prompt_service.py`:
- Around line 149-169: Update PromptService.resolve_prompt so its documented
always-string contract holds when _disk_seed raises FileNotFoundError or
ValueError: catch those failures, log the resolution error, and return an
appropriate string fallback instead of propagating the exception. Preserve the
existing named, database-default, and successful disk-seed resolution order.
- Around line 115-138: Update seed_defaults to validate disk-seeded content
through the existing template-validation path before persisting it, such as by
reusing create_prompt or _validate_template. If validation fails for a templated
prompt type, catch the validation error, log a warning, and skip that seed;
preserve idempotency and continue seeding other prompt types.

In `@openrag/services/orchestrators/query_service.py`:
- Around line 340-343: Guard all three request-path prompt resolutions against
exceptions and use the existing disk-seed fallbacks: update generate_query’s
query_contextualizer call and _prepare_chat’s sys_prompt call in
openrag/services/orchestrators/query_service.py (lines 340-343 and 499-501), and
extend _resolve_query_template in
openrag/services/orchestrators/retrieval_service.py (lines 159-169) so
resolve_prompt failures reach its existing load_template_by_key fallback;
alternatively centralize the handling at PromptService.resolve_prompt while
preserving these fallback behaviors.

In `@tests/unit/services/inference/test_call_log.py`:
- Around line 65-69: Update the _Exploding test sentinel used by _preview() to
override split() and raise the assertion, rather than overriding __len__. This
ensures the test detects preview construction before the length check and still
fails whenever lazy evaluation regresses.

---

Nitpick comments:
In `@openrag/services/orchestrators/retrieval_service.py`:
- Around line 262-266: Update the partition loop in _gather_partition_groups to
resolve _pipeline_for_partition concurrently rather than awaiting each partition
serially. Reuse the existing max_partition_concurrency pattern or apply an
equivalent bounded concurrency mechanism, while preserving each partition’s
pipeline/default_top_k grouping and output behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d6175b3b-2be1-4495-8277-29b3ca0a23c0

📥 Commits

Reviewing files that changed from the base of the PR and between ddac1fc and 75dc014.

📒 Files selected for processing (50)
  • conf/config.yaml
  • docs/content/docs/documentation/API.mdx
  • docs/content/docs/documentation/env_vars.md
  • i8n/en-US.json
  • i8n/fr.json
  • openrag/api/main.py
  • openrag/api/routers/admin/prompts.py
  • openrag/api/schemas/admin/partition_schemas.py
  • openrag/api/schemas/admin/prompt_schemas.py
  • openrag/api/schemas/user/chat.py
  • openrag/app_front.py
  • openrag/core/config/indexation_pipeline.py
  • openrag/core/config/infrastructure.py
  • openrag/core/config/retrieval_pipeline.py
  • openrag/core/indexing/contextualize.py
  • openrag/core/indexing/topic_tags.py
  • openrag/core/models/preset.py
  • openrag/core/models/prompt.py
  • openrag/core/ports/prompt_repo.py
  • openrag/di/container.py
  • openrag/di/providers.py
  • openrag/prompts/templates/spoken_style_answer_tmpl.txt
  • openrag/services/inference/_call_log.py
  • openrag/services/inference/ollama_client.py
  • openrag/services/inference/vllm_client.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/orchestrators/prompt_service.py
  • openrag/services/orchestrators/query_service.py
  • openrag/services/orchestrators/retrieval_service.py
  • openrag/services/persistence/migrations/alembic/versions/e8f9a0b1c2d3_add_prompts_library.py
  • openrag/services/persistence/partition_repo.py
  • openrag/services/persistence/prompt_repo.py
  • openrag/services/persistence/schema.py
  • openrag/services/workers/indexer_actor.py
  • openrag/services/workers/indexer_pool.py
  • openrag/services/workers/stages/contextualize.py
  • openrag/services/workers/stages/topic_tag.py
  • tests/integration/repos/conftest.py
  • tests/integration/repos/test_partition_repo.py
  • tests/integration/repos/test_prompt_repo.py
  • tests/integration/repos/test_prompt_service_integration.py
  • tests/unit/api/routers/admin/test_prompt_routes.py
  • tests/unit/di/test_container.py
  • tests/unit/services/inference/test_call_log.py
  • tests/unit/services/orchestrators/test_prompt_service.py
  • tests/unit/services/orchestrators/test_query_service.py
  • tests/unit/services/orchestrators/test_retrieval_service.py
  • tests/unit/services/persistence/test_partition_repo.py
  • tests/unit/services/workers/stages/test_pipeline_stages.py
  • tests/unit/services/workers/test_pipeline_builder.py
💤 Files with no reviewable changes (10)
  • openrag/prompts/templates/spoken_style_answer_tmpl.txt
  • docs/content/docs/documentation/API.mdx
  • i8n/fr.json
  • openrag/app_front.py
  • openrag/api/schemas/user/chat.py
  • openrag/core/models/prompt.py
  • openrag/core/config/infrastructure.py
  • docs/content/docs/documentation/env_vars.md
  • i8n/en-US.json
  • conf/config.yaml

Comment thread openrag/services/inference/_call_log.py Outdated
Comment thread openrag/services/inference/_call_log.py Outdated
Comment thread openrag/services/inference/_call_log.py Outdated
Comment thread openrag/services/orchestrators/prompt_service.py
Comment thread openrag/services/orchestrators/prompt_service.py
Comment thread openrag/services/orchestrators/query_service.py
Comment thread tests/unit/services/inference/test_call_log.py
andyne13 added 3 commits July 29, 2026 17:28
Addresses the review on #835:

- Pseudonymize email addresses in previewed prompt text. Prompts carry user
  questions and retrieved context; the diagnostic value is the prompt shape,
  not the personal data in it.
- Bound the whole llm.call record, not just each fragment: cap the number of
  messages and multimodal parts rendered and clamp the joined result, so a
  long conversation cannot turn one call into an unbounded line.
- Flatten and bound the interpolated identifiers. 'model' comes from the
  client via metadata.llm_override, so a newline in it could forge extra log
  lines.
- Validate bundled templates in seed_defaults. It writes straight to the repo,
  so it was the one path able to store content the CRUD API would reject — a
  malformed bundled template would become a type's default and raise inside
  .format() on every request that fell back to it.
- resolve_prompt no longer claims it always returns a string: an unreadable
  bundled template now raises a typed ConfigError naming the type instead of a
  bare FileNotFoundError surfacing as an opaque 500.
…ndex

e5f6a7b8c9d0 landed on develop while this branch was open and took the same
parent as e8f9a0b1c2d3, leaving two alembic heads once the branch is merged.
'alembic upgrade head' then aborts, ServiceContainer.initialize fails, and the
app serves every request with a null container — which is why the API suite
saw a 500 on every endpoint while the unit suite stayed green.
@andyne13 andyne13 added this to the v2.0.2 milestone Jul 29, 2026
@andyne13

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the breaking-change Change of behavior after upgrade label Jul 29, 2026

@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: 1

🤖 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 `@openrag/services/inference/_call_log.py`:
- Around line 46-47: Update _clip so its output never exceeds the requested
limit, including when truncation adds the ellipsis; reserve one character of the
limit for "…" before slicing the original text. Preserve unchanged text when its
length is already within the limit, and ensure all callers such as
PREVIEW_CHARS, MAX_META_CHARS, and MAX_DETAIL_CHARS receive strictly capped
output.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e4408a10-8e04-4bad-a9fa-f74b251b5107

📥 Commits

Reviewing files that changed from the base of the PR and between 75dc014 and 2cd22fb.

📒 Files selected for processing (7)
  • docs/content/docs/documentation/API.mdx
  • openrag/services/inference/_call_log.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/orchestrators/prompt_service.py
  • openrag/services/persistence/migrations/alembic/versions/e8f9a0b1c2d3_add_prompts_library.py
  • openrag/services/persistence/schema.py
  • tests/unit/services/inference/test_call_log.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/content/docs/documentation/API.mdx
  • openrag/services/persistence/migrations/alembic/versions/e8f9a0b1c2d3_add_prompts_library.py
  • openrag/services/persistence/schema.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/orchestrators/prompt_service.py

Comment thread openrag/services/inference/_call_log.py Outdated
andyne13 added 2 commits July 30, 2026 00:00
_clip appended the ellipsis after slicing to the limit, so every clipped span
emitted limit + 1 characters and PREVIEW_CHARS / MAX_META_CHARS /
MAX_DETAIL_CHARS were each one over their stated ceiling. Reserve the ellipsis
inside the budget and guard a non-positive limit.

The tests asserted the caps loosely (one carried an explicit '+1 for the
ellipsis'); they now assert the exact ceiling, which is what made the off-by-one
invisible.
Validation reduced a field expression to its root name, so a template using a
conversion, format spec or attribute/index access passed the check and then
raised when the prompt was rendered: {context!x} raises ValueError and
{context.missing} raises AttributeError. Saved as a type's global default,
either fails every request that falls back to it — the exact failure this
write-time validation exists to prevent.

Accept only plain placeholders. These prompts are prose with a few injected
values, so nothing legitimate is lost, and a test asserts every bundled
template still passes so the seed path cannot silently skip a type.
@coderabbitai coderabbitai Bot removed the breaking-change Change of behavior after upgrade label Jul 30, 2026
Comment thread openrag/services/inference/_call_log.py Outdated
Comment thread openrag/services/orchestrators/prompt_service.py Outdated
Comment thread openrag/services/workers/indexer_pool.py
Comment thread openrag/services/persistence/schema.py
@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator

Review notes (three items that aren't inline-anchorable)

1. The /prompts API isn't documented — fixing in this branch

docs/content/docs/documentation/API.mdx is already touched here (to drop spoken_style_answer), and it documents every sibling admin resource — /presets at L473, /model-endpoints at L520 — so the seven new prompt-library routes plus the partition-level generation_prompt_names field are the one part of this feature an operator can't discover from the docs. I'll add a Prompts section in the same shape as the two above.

2. ui/src/pages/admin/presets.tsx still writes the old preset key — not fixing here

IndexationPipelineConfig.vlm_caption_prompt_name was renamed to image_captioning_prompt_name, but the preset editor still reads and writes the old name:

// ui/src/pages/admin/presets.tsx:390-391
promptValue={configGet(config, "vlm_caption_prompt_name", "")}
onPromptChange={(v) => set("vlm_caption_prompt_name", v || null)}

IndexationPipelineConfig is extra="ignore", so this doesn't break anything — the key is just stored and never read, and reference_counts won't count it either. Not a regression (vlm_caption_prompt_name had no consumer on develop either), but the "Caption prompt" selector is now inert while a working backend field sits next to it. Left alone deliberately: this is the backend PR, and the prompt-library UI (which will need real promptsByType data anyway) is where that field should be rewired.

3. Seeding race — respecting your stated scope, with one correction

Follow-up #3 in the description calls the concurrent-boot seeding race "a pre-existing pattern shared with the other seeded tables". Half true: preset_service.seed_defaults goes through PgPresetRepository.upsert, which is INSERT … ON CONFLICT DO UPDATE, so it's race-safe. model_endpoint_service.seed_defaults does share the check-then-insert shape.

What's different here is the blast radius: PgPromptRepository.create maps a unique violation to a ValidationError, _initialize_step re-raises, and ServiceContainer.initialize aborts — so the replica that loses the race doesn't lose prompt seeding, it fails to boot. With N replicas starting against an empty database that's a crash-loop until one wins. A except ValidationError: continue around the create in seed_defaults (another replica already seeded this type — the idempotency check at the top of the loop just lost a race) would close it in three lines.

Not touching it since you've marked it out of scope; flagging it so the decision is explicit rather than inherited from the "pre-existing" framing.

@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator

Review summary

Review-only pass — nothing pushed to this branch. Correcting my two earlier "I'll fix this" lines: the docs section and the code fixes are all yours. CI is green on 8c207a9 (api-tests, milvus-integration, lint, layer-import-guard, tests) and the local unit suite runs 2289 passed.

The design holds up well: one resolution seam, selection filed where the setting belongs, soft name references with a default fallback, and the one invariant that needs SQL atomicity pushed into a partial unique index rather than application bookkeeping. The migration chain is a single head (…d4e5f6a7b8c9 → e5f6a7b8c9d0 → e8f9a0b1c2d3), every op is inspector-guarded, the JSONB round-trip is covered by the pool-level codec, and the removed spoken_style_answer leaves no dangling reference anywhere in the repo (extra="ignore" on IndexationPipelineConfig also keeps stored presets carrying the old vlm_caption_prompt_name loadable).

Findings, most serious first:

# Where What
1 _call_log.py:141 + prompt_service.py:247 Braces in interpolated log metadata raise KeyError out of the request path. Confirmed on this branch. Client-controlled model, and admin-chosen prompt name, both land in loguru's format string. At DEBUG this 500s the request.
2 prompt_service.py:185-221 (CodeRabbit thread) Chat and /search have taken on a new per-request Postgres dependency; a pool error can't reach the disk-seed fallback, so it surfaces as a 500 where the pre-PR code used an in-memory template.
3 indexer_pool.py:239-243 indexation_config.get(flag) drops the model default, and captioning defaults to on — a sparse config would caption while silently ignoring the selected caption prompt. Latent today (dispatch sends a full model_dump).
4 API.mdx The seven /prompts routes and partition generation_prompt_names are undocumented, while /presets (L473) and /model-endpoints (L520) are.
5 schema.py:95, ports/prompt_repo.py:53, prompt_repo.py:32 Stale count and misfiled/duplicated section comments.

Plus the two I flagged as deliberately-not-fixed above: the inert vlm_caption_prompt_name field in the preset UI, and the boot-crash blast radius of the seeding race (preset_service is race-safe via upsert, so only the model_endpoint_service half of the "pre-existing pattern" framing holds).

Nothing here touches the feature's core; #1 is the only one I'd consider a merge blocker, and it's a two-line change plus a test.

Removing it was justified as having no consumer. That was wrong: it is a
public API flag (metadata.spoken_style_answer, documented in the chat request
schema) and a persistent Chainlit command ("Get a conversational text answer
suitable for voice assistants"), wired end to end on develop. Deleting it was
an undeclared breaking change for any client using either.

Restore it and bring it into the library like every other type: an eighth
PromptType, seeded from its bundled template, resolvable per partition through
generation_prompt_names, and validated against the same {context}/{current_date}
placeholders as sys_prompt since the same call site renders both.

Add the regression test that was missing — nothing asserted the metadata flag
swapped the prompt, which is exactly why the removal passed a green suite.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
openrag/api/schemas/admin/partition_schemas.py (1)

79-82: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Trim prompt names before returning the mapping.

The validator checks v.strip() but returns the original value, so names such as " my_prompt " are accepted and persisted. Downstream lookup uses the exact name, causing the override to miss and silently fall back. Store the normalized value instead.

Proposed fix
+        normalized: dict[str, str] = {}
         for k, v in value.items():
             if not isinstance(v, str) or not v.strip():
                 raise ValueError(f"generation_prompt_names['{k}'] must be a non-empty prompt name")
-        return value
+            normalized[k] = v.strip()
+        return normalized
🤖 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 `@openrag/api/schemas/admin/partition_schemas.py` around lines 79 - 82, Update
the generation_prompt_names validator to trim each string value before returning
the mapping. Preserve the existing validation for non-string and blank values,
then store the normalized prompt name so downstream lookups use the trimmed
value.
openrag/services/orchestrators/query_service.py (2)

429-431: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve workspace scope before selecting the contextualizer prompt.

generate_query() runs before the workspace block narrows partition to scope.partition. For workspace requests using "all" or multiple partitions, _retrieval_prompt_name() therefore returns None, selecting the global contextualizer even when the workspace’s owning partition has an override. Resolve the workspace first, or pass the effective partition into generate_query().

Based on the supplied partition-aware prompt-resolution contract.

🤖 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 `@openrag/services/orchestrators/query_service.py` around lines 429 - 431,
Update _prepare_chat so the workspace scope is resolved before generate_query
selects the contextualizer prompt, and pass the effective scope.partition when
applicable. Ensure workspace requests using "all" or multiple partitions use the
owning partition’s override while preserving the existing partition behavior for
non-workspace requests.

340-343: ⚠️ Potential issue | 🟠 Major

Duplicate: request-time prompt-store failures can still become 500s.

Both calls await resolve_prompt() directly. A database/pool exception can escape before the bundled disk seed is tried, so chat and query generation still depend on PostgreSQL availability. Fix this at PromptService.resolve_prompt() by catching only repository access failures and keeping _log_resolution() outside that boundary.

Based on learnings, prompt resolution must fall back to the bundled seed on transient repository failures.

Also applies to: 500-503

🤖 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 `@openrag/services/orchestrators/query_service.py` around lines 340 - 343,
Update PromptService.resolve_prompt() to catch only transient
repository/database access failures, then continue resolving from the bundled
disk seed instead of propagating the exception. Keep _log_resolution() outside
the repository-error handling boundary, and preserve normal propagation for
non-repository failures so chat and query generation remain available when
PostgreSQL is unavailable.

Source: Learnings

🧹 Nitpick comments (1)
tests/unit/services/orchestrators/test_query_service.py (1)

491-526: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the rendered spoken-style system message.

This test verifies only that "spoken_style_answer" was requested. Retain the returned payload and assert that the inserted system message contains the "SPOKEN::" result, so a regression that resolves the correct type but inserts the wrong template cannot pass.

Based on the added branch-specific regression requirement.

🤖 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 `@tests/unit/services/orchestrators/test_query_service.py` around lines 491 -
526, Update test_spoken_style_metadata_swaps_the_answer_prompt to retain the
result of _prepare_chat when spoken_style_answer is enabled, then assert the
prepared messages include a system message containing the rendered “SPOKEN::”
prompt payload. Keep the existing prompt-resolution call assertions and ordinary
sys_prompt fallback checks unchanged.
🤖 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.

Outside diff comments:
In `@openrag/api/schemas/admin/partition_schemas.py`:
- Around line 79-82: Update the generation_prompt_names validator to trim each
string value before returning the mapping. Preserve the existing validation for
non-string and blank values, then store the normalized prompt name so downstream
lookups use the trimmed value.

In `@openrag/services/orchestrators/query_service.py`:
- Around line 429-431: Update _prepare_chat so the workspace scope is resolved
before generate_query selects the contextualizer prompt, and pass the effective
scope.partition when applicable. Ensure workspace requests using "all" or
multiple partitions use the owning partition’s override while preserving the
existing partition behavior for non-workspace requests.
- Around line 340-343: Update PromptService.resolve_prompt() to catch only
transient repository/database access failures, then continue resolving from the
bundled disk seed instead of propagating the exception. Keep _log_resolution()
outside the repository-error handling boundary, and preserve normal propagation
for non-repository failures so chat and query generation remain available when
PostgreSQL is unavailable.

---

Nitpick comments:
In `@tests/unit/services/orchestrators/test_query_service.py`:
- Around line 491-526: Update test_spoken_style_metadata_swaps_the_answer_prompt
to retain the result of _prepare_chat when spoken_style_answer is enabled, then
assert the prepared messages include a system message containing the rendered
“SPOKEN::” prompt payload. Keep the existing prompt-resolution call assertions
and ordinary sys_prompt fallback checks unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e0b31ce5-b9de-41ba-b983-230f5e00ca54

📥 Commits

Reviewing files that changed from the base of the PR and between 8c207a9 and 60aac50.

📒 Files selected for processing (9)
  • openrag/api/schemas/admin/partition_schemas.py
  • openrag/api/schemas/admin/prompt_schemas.py
  • openrag/core/models/preset.py
  • openrag/services/orchestrators/prompt_service.py
  • openrag/services/orchestrators/query_service.py
  • openrag/services/persistence/migrations/alembic/versions/e8f9a0b1c2d3_add_prompts_library.py
  • openrag/services/persistence/schema.py
  • tests/unit/services/orchestrators/test_prompt_service.py
  • tests/unit/services/orchestrators/test_query_service.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • openrag/core/models/preset.py
  • tests/unit/services/orchestrators/test_prompt_service.py

… flags

Addresses the review on #835.

- A brace in an interpolated identifier raised KeyError out of the request
  path. Both log lines built the format string by interpolation and passed the
  payload positionally, so loguru's message.format() parsed any surviving
  brace as a field. 'model' is client-controlled (metadata.llm_override), and
  llm.call is emitted outside the client's try/except, so a request naming a
  model 'gpt{x}' failed with LOG_LEVEL=DEBUG; a prompt named 'my{tmpl}' broke
  every request resolving it. Both now use a single literal placeholder with
  the whole line built inside the lazy callable, so nothing is rescanned.

- Resolution moved onto the request path, giving chat and search a per-request
  Postgres dependency they did not have when prompts were read once at boot. A
  repository failure now degrades to the bundled template with one warning,
  swallowed at the single choke point rather than at each caller. ConfigError
  still covers the genuinely unrecoverable case.

- Enrichment flags were read with a bare .get(), so a config omitting
  enable_image_captioning (default True) captioned during ingest while never
  resolving its prompt — silently ignoring the preset's selection and the
  library default. Defaults now come from IndexationPipelineConfig itself.

Also tidies comments the reviewer flagged.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openrag/services/orchestrators/prompt_service.py (1)

46-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not re-add spoken_style_answer to the managed prompt library.

The PR objective removes this unused prompt, but these mappings make it managed again. Because seed_defaults() iterates _TYPE_TO_CONFIG_KEY, every startup will validate and seed a spoken_style_answer global default and expose an obsolete record through the admin CRUD surface. Remove both entries unless retaining this prompt is intentional.

Also applies to: 117-132

🤖 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 `@openrag/services/orchestrators/prompt_service.py` around lines 46 - 55,
Remove the PromptType.SPOKEN_STYLE_ANSWER entry from _PROMPT_FORMAT_FIELDS and
its corresponding entry from _TYPE_TO_CONFIG_KEY. Keep spoken_style_answer
unmanaged so seed_defaults() does not validate or seed it and the admin CRUD
surface does not expose an obsolete default.
🧹 Nitpick comments (1)
tests/unit/services/orchestrators/test_prompt_service.py (1)

288-304: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the get_default failure path separately.

ExplodingRepo.get_by_name() always raises, so the get_default() override at Line 299-300 is never reached. Add a repository fake that returns None from get_by_name() and raises only from get_default() to cover that fallback branch.

🤖 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 `@tests/unit/services/orchestrators/test_prompt_service.py` around lines 288 -
304, The test currently exercises only the get_by_name failure path because that
method raises before get_default is called. Add a separate repository fake or
test using the existing service setup where get_by_name returns None and
get_default raises, then assert resolve_prompt still returns the bundled
sys_prompt template.
🤖 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.

Outside diff comments:
In `@openrag/services/orchestrators/prompt_service.py`:
- Around line 46-55: Remove the PromptType.SPOKEN_STYLE_ANSWER entry from
_PROMPT_FORMAT_FIELDS and its corresponding entry from _TYPE_TO_CONFIG_KEY. Keep
spoken_style_answer unmanaged so seed_defaults() does not validate or seed it
and the admin CRUD surface does not expose an obsolete default.

---

Nitpick comments:
In `@tests/unit/services/orchestrators/test_prompt_service.py`:
- Around line 288-304: The test currently exercises only the get_by_name failure
path because that method raises before get_default is called. Add a separate
repository fake or test using the existing service setup where get_by_name
returns None and get_default raises, then assert resolve_prompt still returns
the bundled sys_prompt template.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 22f21d1a-33ef-4571-a536-4418fe763aae

📥 Commits

Reviewing files that changed from the base of the PR and between 60aac50 and 309f614.

📒 Files selected for processing (9)
  • openrag/core/ports/prompt_repo.py
  • openrag/services/inference/_call_log.py
  • openrag/services/orchestrators/prompt_service.py
  • openrag/services/persistence/prompt_repo.py
  • openrag/services/workers/indexer_pool.py
  • tests/unit/services/inference/test_call_log.py
  • tests/unit/services/orchestrators/test_prompt_service.py
  • tests/unit/services/workers/test_indexer_pool.py
  • tests/unit/services/workers/test_pipeline_builder.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/services/workers/test_pipeline_builder.py
  • openrag/services/persistence/prompt_repo.py
  • openrag/core/ports/prompt_repo.py

@andyne13

Copy link
Copy Markdown
Contributor Author

@Ahmath-Gadji all five addressed in 309f6140 — replies inline on each thread. Both brace bugs reproduced exactly as you described before I touched anything, and the resolve_prompt resilience point was the most valuable one in the review: per-request resolution had quietly given chat and /search a Postgres dependency they didn't have when prompts were read once at boot, and a pool error bypassed the disk-seed fallback entirely. That's now handled at the single choke point.

Also restored spoken_style_answer (your feature — I'd removed it claiming no consumer, which was wrong: it's a public API flag and a Chainlit command). It's back as an eighth library type, with the regression test that was missing.

2294 unit tests, ruff, layer guard and the prompt integration suite are green.

A concurrent boot against an empty database was worse than losing the seed:
PgPromptRepository.create maps the unique violation to a ValidationError,
_initialize_step re-raises it and ServiceContainer.initialize aborts, so the
replica that loses the race fails to boot — N replicas crash-loop until one
wins. Losing that race is a no-op; skip the type and carry on.

This is not the pre-existing pattern the PR description claimed: preset seeding
goes through an INSERT ... ON CONFLICT DO UPDATE and is race-safe. Only the
model-endpoint seeding shares the check-then-insert shape.

Also document the prompt library in API.mdx, which covered every sibling admin
resource but not the seven /prompts routes or the partition-level
generation_prompt_names field.
@andyne13

Copy link
Copy Markdown
Contributor Author

@Ahmath-Gadji thanks — I'd missed your two non-inline comments when I replied earlier. Both now addressed in ea730de8, and your correction on the seeding race was right and worth more than the framing I gave it.

Seeding race — you're right that "pre-existing pattern" was half true: preset_service goes through INSERT … ON CONFLICT DO UPDATE and is race-safe, so only the model-endpoint half shares the shape. And the blast radius is as you describe — worse still because my own 409 mapping is what turns the unique violation into a ValidationError that _initialize_step re-raises, so the losing replica fails to boot, not just to seed. Closed with the except ValidationError: continue you suggested, plus a test driving a repo whose create always raises.

API docs — added a Prompt Library section in the same shape as /presets and /model-endpoints: the seven routes, the resolution order, a table of where each type is selected, the plain-placeholder rule with its 422, and the partition generation_prompt_names PATCH.

vlm_caption_prompt_name — already fixed in the UI PR (#836): presets.tsx there reads and writes image_captioning_prompt_name, no occurrences of the old key remain. You were reviewing this branch, where ui/ is still at develop's state, so the inert selector you saw is gone by the time the two land.

CI green on ea730de8; 2295 unit tests locally.

@andyne13
andyne13 requested a review from Ahmath-Gadji July 30, 2026 11:19
Comment thread openrag/services/orchestrators/prompt_service.py
@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator

Verification of ea730de8

Re-checked every finding against the branch, not just the diff — each repro from the original review re-run, plus the local suite (2295 passed, up 6) and ruff check / ruff format --check clean. CI green on ea730de8.

# Finding Status
1 Braces in log metadata → KeyError out of the request path ✅ fixed 309f6140 — both sites, re-ran both repros; braces pass through verbatim, laziness intact
2 Per-request Postgres dependency with no fallback ✅ fixed 309f6140 — repo raising RuntimeError now degrades to the bundled template with one warning
3 .get(flag) dropping the model default ✅ fixed 309f6140 — defaults read from model_fields, so they can't drift; a sparse config resolves ['image_captioning']
4 /prompts undocumented ✅ fixed ea730de8 — accurate against the code, including the used_by semantics and the write-time template rules
5 Stale count / misfiled comments ✅ fixed 309f6140, count correctly back to 8 via 60aac502
Seeding race crash-loop (I'd flagged, not asked you to fix) ✅ fixed ea730de8

Each fix has a regression test that fails without it, which is the part that matters — three of these were invisible to a green suite before.

60aac502 is a good catch of your own and I'd have missed it: metadata.spoken_style_answer is a documented public flag and a Chainlit command, so dropping it was a breaking change, and the reason nothing caught it is that nothing asserted the flag swapped the prompt. Restoring it as a library type is consistent everywhere I checked — PromptType, _PROMPT_TYPE_VALUES, the migration CHECK, PromptTypeName, _TYPE_TO_CONFIG_KEY, _PROMPT_FORMAT_FIELDS (same {context}/{current_date} pair as sys_prompt, correct since the same call site renders both), the partition key whitelist, PromptsConfig, the bundled template, docs and i18n. Migration chain is still a single head.

One new finding, which the fixes brought within reach rather than caused — prompt_service.py:243-247: the raise ConfigError(..., code="PROMPT_UNAVAILABLE") throws TypeError instead of the typed error, because ConfigError hard-codes code and forwards **kwargs. Reachable via a PROMPTS_DIR missing one template — seed_defaults skips that type, so it has no default and every resolution walks to the failing disk seed. Details and repro in the inline thread.

Two things left open by design, unchanged: the inert vlm_caption_prompt_name field in the preset UI (frontend PR), and preset *_prompt_name references still not validated at write time (your follow-up #1).

… paths

The unavailable-prompt guard could never raise what it intended: ConfigError
hard-coded its own code and forwarded **kwargs, so ConfigError(msg,
code='PROMPT_UNAVAILABLE') collided and the raise statement itself threw
TypeError. Accept an overridable code, matching ValidationError in the same
module.

It survived because the raise was reasoned about, never executed. Add a test
that drives it (no library default, no bundled template) and cover the rest of
the service's error branches — malformed braces, unknown ids on
get/update/set_default/delete, refusing to delete a type's default, and seeding
skipping a type whose template is missing. Branch coverage of prompt_service
goes 91% -> 96%, with every raise this PR adds now executed by a test.
Ahmath-Gadji
Ahmath-Gadji previously approved these changes Jul 30, 2026

@Ahmath-Gadji Ahmath-Gadji left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving on 764a79e1. Every finding from the review is addressed, each with a regression test that fails without its fix.

The ConfigError correction went the way I'd argued against, and it's the better call: making code keyword-only is safer than the variant I sketched (it can't be passed positionally, so it can't shadow anything), the four existing ConfigError sites are untouched, and the coverage added with it addresses the actual root cause rather than the symptom — a raise that was reasoned about but never executed. 91% → 96% branch coverage on prompt_service, with every raise this PR adds now driven by a test. That's worth more than the one-line deletion I proposed.

Verified on this head:

  • The original repro now yields the intended error: ConfigError | code=PROMPT_UNAVAILABLE | status=500, message naming the type.
  • Re-swept every exception construction site in openrag/ (42 sites passing code=/status_code=, checked against what each class accepts): 0 raise TypeError, down from 1.
  • Local unit suite 2303 passed, ruff check and ruff format --check clean.

Nothing outstanding on my side. The two items left open are deliberate and stated: the inert vlm_caption_prompt_name field in the preset UI (frontend PR), and preset *_prompt_name references not validated at write time (follow-up #1 in the description).

# Conflicts:
#	openrag/services/orchestrators/query_service.py
#	openrag/services/orchestrators/retrieval_service.py
The no-retrieval reply came from #807 and read an __init__-time snapshot this
branch removes. Git auto-merged that reference without raising a conflict, so
the resolution I wrote during the merge had nothing asserting it — the same
shape of gap that let two stale attribute references through in the first
place.

@Ahmath-Gadji Ahmath-Gadji left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving on 6cbc40dd after the develop merge. Reviewed the conflict resolution rather than just the result — both sides survive in both files:

query_service.pydevelop's prepend_system_prompt, force_retrieval, the requires_retrieval conversational skip, the web_source_numbers filtering and the reshaped _prepare_completions are all intact, and each of the three answer-prompt sites now resolves through the library instead of the __init__-time snapshots this branch deletes.

retrieval_service.pydevelop's _resolve_reranker / _default_reranker_name land intact, with the reranker line correctly switched to the new helper, alongside this branch's async _pipeline_for_partition and hyde/multi_query template resolution.

6cbc40dd is the part worth calling out: the no-retrieval conversational reply from #807 read self._sys_prompt_tmplt, and git auto-merged that reference without reporting a conflict — so the resolution was live but unasserted. Adding the test that proves the conversational path resolves through PromptService (and honours the partition's selection) is exactly the right response to a silent auto-merge, and the commit message says so plainly.

Verified on this head:

  • No leftover references to the three removed __init__ prompt attributes anywhere in openrag/ or tests/.
  • Migration graph still a single head (e8f9a0b1c2d3, 15 revisions) — the merge brought no new revisions, so the multi-head hazard this branch already hit once didn't recur.
  • All five original findings still hold: braces inert in both log lines, a repo failure degrading to the bundled seed, the captioning flag defaulting to True, and ConfigError carrying PROMPT_UNAVAILABLE.
  • 2429 unit tests pass (2303 → 2429 with develop's), ruff check + ruff format --check clean, check_layer_imports.py OK.

Nothing outstanding. The two open items remain deliberate: the inert vlm_caption_prompt_name field in the preset UI (frontend PR), and preset *_prompt_name references not validated at write time (follow-up #1).

@andyne13
andyne13 merged commit 5370176 into develop Jul 30, 2026
6 checks passed
@andyne13
andyne13 deleted the feat/pm-backend branch July 30, 2026 13:19
@andyne13 andyne13 mentioned this pull request Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

admin-ui Admin UI enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prompt management: DB-backed prompt library with global defaults + per-partition overrides + admin UI

2 participants