Skip to content

RSPEED-2529: replace string concatenation with Jinja2 template rendering for system prompts#1267

Merged
tisnik merged 1 commit intolightspeed-core:mainfrom
major:RSPEED-2529/jinja2-prompt-templating
Mar 4, 2026
Merged

RSPEED-2529: replace string concatenation with Jinja2 template rendering for system prompts#1267
tisnik merged 1 commit intolightspeed-core:mainfrom
major:RSPEED-2529/jinja2-prompt-templating

Conversation

@major
Copy link
Contributor

@major major commented Mar 4, 2026

Description

Replace the hard-coded string concatenation in _build_instructions with Jinja2 template rendering. Operators can now use {{ date }}, {{ os }}, {{ version }}, {{ arch }} and conditionals ({% if os %}) in their system prompts. Plain prompts without template markers pass through unchanged.

  • New dependency: jinja2>=3.1.0
  • Sandboxing: Uses SandboxedEnvironment for defense-in-depth against template injection
  • Caching: Compiled template cached via lru_cache (prompt doesn't change at runtime)
  • Error handling: TemplateSyntaxError caught and re-raised as ValueError with the exact syntax problem; moved _build_instructions inside the try block so template errors hit existing error mapping
  • Tests: Rewrote prompt-building tests for the new rendering approach; added coverage for conditionals, None-value rendering, plain passthrough, and malformed templates

Type of change

  • New feature
  • Unit tests improvement

Tools used to create PR

  • Assisted-by: Claude (opencode)
  • Generated by: N/A

Related Tickets & Documents

  • Related Issue: RSPEED-2529

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

  1. uv run make verify passes all linters (black, pylint, pyright, ruff, pydocstyle, mypy)
  2. uv run pytest tests/unit/app/endpoints/test_rlsapi_v1.py -v passes all tests
  3. Malformed templates ({{ unclosed, {% if %}, {% endfor %}) raise ValueError with "invalid Jinja2 syntax" message
  4. Plain prompts without Jinja2 syntax pass through unchanged

Summary by CodeRabbit

  • New Features
    • System prompts support Jinja2 templating with sandboxed rendering, variable substitution (date, OS, version, arch), caching, and safe defaults.
  • Bug Fixes
    • Malformed templates now surface clear errors and are handled consistently.
  • Tests
    • Expanded tests for templating behavior, conditionals, None handling, caching, and invalid-template cases.
  • Chores
    • Added jinja2 as a runtime dependency.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Warning

Rate limit exceeded

@major has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fbae369c-fb8b-4173-8537-0f2f94548f96

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7b91d and 7f030f7.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • pyproject.toml
  • src/app/endpoints/rlsapi_v1.py
  • tests/unit/app/endpoints/test_rlsapi_v1.py

Walkthrough

Adds Jinja2-based templating for system prompts, a sandboxed cached _get_prompt_template() used by _build_instructions(), surfaces template syntax errors as ValueError, adds jinja2>=3.1.0 dependency, and expands tests to cover rendering and failure cases.

Changes

Cohort / File(s) Summary
Dependencies
pyproject.toml
Added runtime dependency jinja2>=3.1.0 for system prompt template rendering.
Template implementation & error handling
src/app/endpoints/rlsapi_v1.py
Added _get_prompt_template() (SandboxedEnvironment + LRU cache), refactored _build_instructions() to render system prompts with context (date, OS, version, arch), convert TemplateSyntaxError to ValueError, included ValueError in _INFER_HANDLED_EXCEPTIONS, moved instruction construction to just before LLM call, and updated imports.
Tests
tests/unit/app/endpoints/test_rlsapi_v1.py
Clears template cache between tests, adds fixtures to mock custom prompts, imports _get_prompt_template, and adds tests for default prompt passthrough, Jinja2 rendering (variables, conditionals), None handling, plain prompts, and malformed-template -> ValueError.

Sequence Diagram

sequenceDiagram
    participant Client as API Request
    participant Handler as _build_instructions()
    participant Config as Configuration
    participant Loader as _get_prompt_template()
    participant Jinja2 as Jinja2 Engine

    Client->>Handler: Request inference
    Handler->>Config: Read customization.system_prompt
    Handler->>Loader: Get compiled template (cached)
    Loader->>Jinja2: Compile template (SandboxedEnvironment)
    Jinja2-->>Loader: Template object
    Loader-->>Handler: Cached template
    Handler->>Handler: Build context (date, OS, version, arch)
    Handler->>Jinja2: Render template with context
    Jinja2-->>Handler: Rendered system prompt
    Handler-->>Client: Invoke LLM with rendered prompt
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • tisnik
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and specifically describes the main change: replacing string concatenation with Jinja2 template rendering for system prompts, which is the primary focus across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 94.44% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/endpoints/rlsapi_v1.py (1)

130-131: Clarify cache invalidation requirement for system_prompt refresh scenario.

@functools.lru_cache(maxsize=1) pins the first compiled prompt template for the process lifetime. The function docstring states "the system prompt does not change at runtime," but configuration.customization.system_prompt is a reloadable configuration field. If AppConfig.init_from_dict() is ever called at runtime (currently only used at startup), the cached template would become stale without explicit cache invalidation.

While no current runtime refresh mechanism exists, init_from_dict() clears other caches (conversation cache, quota limiters, token usage history) but omits _get_prompt_template.cache_clear(). Either:

  1. Document that system_prompt is intentionally immutable post-startup and update the docstring to reflect this architectural constraint, or
  2. Add _get_prompt_template.cache_clear() to init_from_dict() to support potential future configuration refresh scenarios.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/endpoints/rlsapi_v1.py` around lines 130 - 131, The lru_cache on
_get_prompt_template() will pin the compiled prompt for process lifetime and can
become stale if configuration.customization.system_prompt is reloaded; update
AppConfig.init_from_dict() to call _get_prompt_template.cache_clear() after
loading new config so the next call recompiles the updated template (refer to
_get_prompt_template and AppConfig.init_from_dict()), or alternatively clarify
the immutability by updating the _get_prompt_template() docstring to state
system_prompt is immutable at runtime—pick one approach and implement it
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/endpoints/rlsapi_v1.py`:
- Around line 160-167: The ValueError raised when rendering the Jinja2 template
(in the env.from_string(...) block inside _build_instructions()) is not being
handled by infer_endpoint’s error pipeline, so malformed prompts bypass
_record_inference_failure and _map_inference_error_to_http_exception; update the
call site so template construction happens inside the try/except block used by
infer_endpoint (or catch ValueError around the _build_instructions() call) and
then rethrow or map the error into the existing pipeline so
_record_inference_failure and _map_inference_error_to_http_exception run for
template syntax errors; reference _build_instructions(), infer_endpoint,
_record_inference_failure, and _map_inference_error_to_http_exception when
making the change.

---

Nitpick comments:
In `@src/app/endpoints/rlsapi_v1.py`:
- Around line 130-131: The lru_cache on _get_prompt_template() will pin the
compiled prompt for process lifetime and can become stale if
configuration.customization.system_prompt is reloaded; update
AppConfig.init_from_dict() to call _get_prompt_template.cache_clear() after
loading new config so the next call recompiles the updated template (refer to
_get_prompt_template and AppConfig.init_from_dict()), or alternatively clarify
the immutability by updating the _get_prompt_template() docstring to state
system_prompt is immutable at runtime—pick one approach and implement it
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7dbc4556-b715-438d-93f7-bd8f912d5ca2

📥 Commits

Reviewing files that changed from the base of the PR and between 4380346 and 7b6f896.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • pyproject.toml
  • src/app/endpoints/rlsapi_v1.py
  • tests/unit/app/endpoints/test_rlsapi_v1.py

@major
Copy link
Contributor Author

major commented Mar 4, 2026

@tisnik I added jinja as an explicit dependency, but it's already being pulled in as a transitive dependency by two or three packages, including starlette.

@major major force-pushed the RSPEED-2529/jinja2-prompt-templating branch from 7b6f896 to 08c44ab Compare March 4, 2026 14:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/unit/app/endpoints/test_rlsapi_v1.py (1)

272-283: Add one endpoint-level test for malformed template handling.

You currently validate ValueError at _build_instructions(), but not the infer_endpoint() mapping path. Adding a test that asserts HTTP 500 + infer_error event on bad template would lock in the new error-pipeline behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/app/endpoints/test_rlsapi_v1.py` around lines 272 - 283, Add an
endpoint-level test that trips the same malformed Jinja2 path through
infer_endpoint so we assert the runtime mapping behavior: arrange a bad template
via the existing mock_custom_prompt(bad_template) and construct a request that
calls infer_endpoint (or the test client wrapper that routes to
RlsapiV1.infer_endpoint) with a payload using RlsapiV1SystemInfo(os="RHEL",
version="9.3", arch="x86_64"), then assert the HTTP response status is 500 and
that an infer_error event/flag was emitted (or logged) as a result; reuse the
existing fixtures and the _build_instructions reference to ensure the test
exercises the same failure path.
src/app/endpoints/rlsapi_v1.py (1)

60-60: Narrow the caught exception type instead of catching all ValueErrors.

Line 60 + Lines 357-363 currently treat any ValueError as “invalid system prompt template.” That can mislabel unrelated failures and reduce error signal quality. A dedicated exception subtype for template syntax keeps mapping precise.

Also applies to: 357-363

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/endpoints/rlsapi_v1.py` at line 60, Replace the broad except
ValueError used to label "invalid system prompt template" with a dedicated
exception type: define a new exception class (e.g.,
InvalidSystemPromptTemplateError or SystemPromptTemplateError), update the
template parsing/validation code to raise that specific exception instead of
ValueError, and change the except ValueError blocks that emit the "invalid
system prompt template" message to except InvalidSystemPromptTemplateError; also
add the new exception to the module exports/imports so callers can catch it and
adjust any tests/mocks accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/endpoints/rlsapi_v1.py`:
- Around line 131-159: The _get_prompt_template function is currently cached
with functools.lru_cache(maxsize=1) but takes no parameters, so it returns a
stale compiled template after config changes; modify _get_prompt_template to
accept the prompt text (e.g., add a prompt_text: str parameter that is the
resolved configuration.customization.system_prompt or
constants.DEFAULT_SYSTEM_PROMPT) and move the Jinja2 compilation inside using
that parameter, then change the decorator to `@functools.lru_cache`(maxsize=128)
(or similar) so the cache key includes the prompt text; update all call sites to
pass the resolved prompt string when invoking _get_prompt_template so
configuration reloads produce a new compiled template for changed prompts.

---

Nitpick comments:
In `@src/app/endpoints/rlsapi_v1.py`:
- Line 60: Replace the broad except ValueError used to label "invalid system
prompt template" with a dedicated exception type: define a new exception class
(e.g., InvalidSystemPromptTemplateError or SystemPromptTemplateError), update
the template parsing/validation code to raise that specific exception instead of
ValueError, and change the except ValueError blocks that emit the "invalid
system prompt template" message to except InvalidSystemPromptTemplateError; also
add the new exception to the module exports/imports so callers can catch it and
adjust any tests/mocks accordingly.

In `@tests/unit/app/endpoints/test_rlsapi_v1.py`:
- Around line 272-283: Add an endpoint-level test that trips the same malformed
Jinja2 path through infer_endpoint so we assert the runtime mapping behavior:
arrange a bad template via the existing mock_custom_prompt(bad_template) and
construct a request that calls infer_endpoint (or the test client wrapper that
routes to RlsapiV1.infer_endpoint) with a payload using
RlsapiV1SystemInfo(os="RHEL", version="9.3", arch="x86_64"), then assert the
HTTP response status is 500 and that an infer_error event/flag was emitted (or
logged) as a result; reuse the existing fixtures and the _build_instructions
reference to ensure the test exercises the same failure path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7aa21370-5164-40ca-8278-f3e98d3c2b77

📥 Commits

Reviewing files that changed from the base of the PR and between 7b6f896 and 08c44ab.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • pyproject.toml
  • src/app/endpoints/rlsapi_v1.py
  • tests/unit/app/endpoints/test_rlsapi_v1.py

@major major force-pushed the RSPEED-2529/jinja2-prompt-templating branch from 08c44ab to 4f7b91d Compare March 4, 2026 14:25
@major major changed the title RSPEED-2529: harden Jinja2 system prompt rendering RSPEED-2529: replace string concatenation with Jinja2 template rendering for system prompts Mar 4, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/app/endpoints/rlsapi_v1.py (1)

149-152: Consider handling SecurityError from the sandbox.

"Templates can still raise errors when compiled or rendered" and "If the template tries to access insecure code a SecurityError is raised." While admin-controlled templates using only simple variables (date, os, version, arch) are unlikely to trigger this, catching jinja2.sandbox.SecurityError in _build_instructions() and mapping it to ValueError would provide consistent error handling if a misconfigured template attempts unsafe operations.

💡 Proposed enhancement in _build_instructions
 def _build_instructions(systeminfo: RlsapiV1SystemInfo) -> str:
     ...
     date_today = datetime.now().strftime("%B %d, %Y")
 
-    return _get_prompt_template().render(
-        date=date_today,
-        os=systeminfo.os or "",
-        version=systeminfo.version or "",
-        arch=systeminfo.arch or "",
-    )
+    try:
+        return _get_prompt_template().render(
+            date=date_today,
+            os=systeminfo.os or "",
+            version=systeminfo.version or "",
+            arch=systeminfo.arch or "",
+        )
+    except jinja2.sandbox.SecurityError as exc:
+        raise ValueError(
+            f"System prompt template attempted unsafe operation: {exc}"
+        ) from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/endpoints/rlsapi_v1.py` around lines 149 - 152, Catch
jinja2.sandbox.SecurityError inside _build_instructions() and re-raise it as a
ValueError with a clear message so template sandbox violations surface
consistently; specifically import jinja2.sandbox.SecurityError, wrap the
existing template compilation/rendering calls in a try/except that catches
SecurityError (in addition to any existing exceptions) and raise
ValueError("invalid template: sandbox violation" or similar) preserving or
appending the original error text for debugging, referencing the
SandboxedEnvironment and the _build_instructions() function where templates are
constructed and rendered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/endpoints/rlsapi_v1.py`:
- Around line 149-152: Catch jinja2.sandbox.SecurityError inside
_build_instructions() and re-raise it as a ValueError with a clear message so
template sandbox violations surface consistently; specifically import
jinja2.sandbox.SecurityError, wrap the existing template compilation/rendering
calls in a try/except that catches SecurityError (in addition to any existing
exceptions) and raise ValueError("invalid template: sandbox violation" or
similar) preserving or appending the original error text for debugging,
referencing the SandboxedEnvironment and the _build_instructions() function
where templates are constructed and rendered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec81fab4-674c-446b-b83e-f1960f82fb50

📥 Commits

Reviewing files that changed from the base of the PR and between 08c44ab and 4f7b91d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • pyproject.toml
  • src/app/endpoints/rlsapi_v1.py
  • tests/unit/app/endpoints/test_rlsapi_v1.py

…ing for system prompts

Operators can now use Jinja2 template variables ({{ date }}, {{ os }},
{{ version }}, {{ arch }}) and conditionals in system prompts. Prompts
without template syntax pass through unchanged.

Uses SandboxedEnvironment for defense-in-depth and catches
TemplateSyntaxError so malformed prompts surface a clear ValueError
instead of an opaque traceback on every request. The compiled template
is cached via lru_cache.

Signed-off-by: Major Hayden <major@redhat.com>
@major major force-pushed the RSPEED-2529/jinja2-prompt-templating branch from 4f7b91d to 7f030f7 Compare March 4, 2026 14:32
Copy link
Contributor

@tisnik tisnik left a comment

Choose a reason for hiding this comment

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

LGTM

@tisnik tisnik merged commit eb4a1be into lightspeed-core:main Mar 4, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants