Skip to content

Job Management: Preserve llm_call details#823

Open
Prajna1999 wants to merge 11 commits into
mainfrom
feat/add-llm-call-record
Open

Job Management: Preserve llm_call details#823
Prajna1999 wants to merge 11 commits into
mainfrom
feat/add-llm-call-record

Conversation

@Prajna1999
Copy link
Copy Markdown
Collaborator

@Prajna1999 Prajna1999 commented May 11, 2026

Summary

Target issue is #819
Explain the motivation for making this change. What existing problem does the pull request solve?

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Summary by CodeRabbit

  • New Features

    • Support audio input via URL and presigned URIs; improved audio handling and TTS/STT flows
    • Two-phase LLM call lifecycle: create pending records then update resolved details; reuse chain records when available
    • Input serialization now accepts list inputs
  • Bug Fixes

    • Fixed audio metadata handling for non-base64 inputs
  • Chores

    • Database migration: loosened nullability for certain LLM call fields

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

PR introduces a two-phase LLM call lifecycle (pending → resolved), makes LlmCall provider/model/input_type nullable, expands AudioContent to support "url" alongside "base64", and integrates these changes across CRUD helpers and job start/execute flows.

Changes

Audio URL Support and Call Lifecycle

Layer / File(s) Summary
Database schema and migration
backend/app/alembic/versions/058_make_llm_call_fields_nullable.py, backend/app/models/llm/request.py
Alembic migration 058 makes input_type, provider, and model nullable on the llm_call table. LlmCall ORM model updates nullable columns with default=None.
Audio input schema and serialization
backend/app/models/llm/request.py, backend/app/crud/llm.py
AudioContent discriminates between "base64" and "url" formats with optional uri field. serialize_input accepts lists and handles audio URL/base64 serialization.
LLM call database lifecycle helpers
backend/app/crud/llm.py
New CRUD functions support pending→resolved workflow: get_llm_call_by_job_id fetches standalone calls by job; create_llm_call_pending pre-creates minimal records; update_llm_call_resolved_fields fills deferred fields.
LLM chain job lookup
backend/app/crud/llm_chain.py
select import added; get_llm_chain_by_job_id queries and retrieves chain records by job ID for reuse in deferred execution.
Job initialization and record pre-creation
backend/app/services/llm/jobs.py
start_job() and start_chain_job() pre-create pending LlmCall/LlmChain records before scheduling tasks, logging failures as warnings without blocking.
LLM call execution with pending/resolved pattern and audio handling
backend/app/services/llm/jobs.py
execute_llm_call() reuses pending call rows for standalone jobs or creates fresh rows for chain blocks, captures original STT input format, uploads TTS base64 to S3 when needed, and updates resolved fields/config.
Chain execution with record reuse
backend/app/services/llm/jobs.py
execute_chain_job() looks up and reuses existing LlmChain by job_id when present; otherwise creates a new chain.
Tests import update
backend/app/tests/services/llm/test_jobs.py
Test module imports get_llm_call_by_job_id.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ProjectTech4DevAI/kaapi-backend#726: Adds GET /llm/call/{job_id} polling endpoint; depends on the nullable LlmCall fields and pending/resolved persistence pattern introduced in this PR.

Suggested reviewers

  • vprashrex

Poem

🐰 A rabbit hums where bytes and URIs meet,

Base64 or URL — both inputs sweet,
Pending rows planted before jobs take flight,
Resolved fields blossom, stored safe and right,
S3 and DB wink in the dev-night light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Job Management: Preserve llm_call details' accurately summarizes the main objective of the changeset, which introduces a two-phase LLM call lifecycle with pending and resolved states to preserve call details across job creation and execution.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-llm-call-record

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.

@Prajna1999 Prajna1999 changed the title Feat/add llm call record Job Management: Preserve llm_call details May 11, 2026
@Prajna1999 Prajna1999 requested review from kartpop and vprashrex May 11, 2026 13:16
@Prajna1999 Prajna1999 self-assigned this May 12, 2026
@Prajna1999 Prajna1999 added the enhancement New feature or request label May 12, 2026
@Prajna1999 Prajna1999 moved this to In Review in Kaapi-dev May 12, 2026
@Prajna1999 Prajna1999 linked an issue May 12, 2026 that may be closed by this pull request
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
backend/app/core/storage_utils.py (1)

211-222: ⚡ Quick win

Add m4a MIME aliases to extension mapping.

audio/m4a (and common variant audio/x-m4a) currently falls back to wav, which can produce wrong file extensions.

Suggested mapping update
 _MIME_TO_EXT: dict[str, str] = {
@@
     "audio/mp4": "mp4",
+    "audio/m4a": "m4a",
+    "audio/x-m4a": "m4a",
     "audio/aac": "aac",
     "audio/flac": "flac",
 }
🤖 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 `@backend/app/core/storage_utils.py` around lines 211 - 222, The _MIME_TO_EXT
mapping in storage_utils.py is missing aliases for m4a mime types so audio/m4a
and audio/x-m4a fall back incorrectly; update the _MIME_TO_EXT dict to include
entries mapping "audio/m4a" and "audio/x-m4a" to "m4a" (alongside the existing
audio/* entries) so functions that rely on _MIME_TO_EXT return the correct .m4a
extension.
🤖 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 `@backend/app/alembic/versions/058_make_llm_call_fields_nullable.py`:
- Around line 25-28: The downgrade() changes try to set llm_call.model,
llm_call.provider and llm_call.input_type to nullable=False which will fail if
any rows contain NULL; modify downgrade() to first backfill NULL values for
these columns (or abort with a clear error) before calling op.alter_column, e.g.
run UPDATE statements against the "llm_call" table to set sensible defaults for
model, provider and input_type or raise a RuntimeError if NULLs exist, then call
op.alter_column("llm_call", "model", nullable=False),
op.alter_column("llm_call", "provider", nullable=False) and
op.alter_column("llm_call", "input_type", nullable=False).

In `@backend/app/crud/llm.py`:
- Around line 284-291: get_llm_call_by_job_id is non-deterministic because it
uses .first() without filtering or ordering, which can return the wrong
standalone LlmCall when multiple rows exist; update the query in
get_llm_call_by_job_id to restrict to the intended active/pending row (e.g., add
a predicate such as LlmCall.status == "pending" or LlmCall.is_active) and add a
deterministic ORDER BY (for example LlmCall.created_at.desc() or
LlmCall.id.desc()) before taking the first result so updates always target the
correct record.

In `@backend/app/services/llm/chain/executor.py`:
- Line 73: The _resolve_presigned_url function's parameter is untyped; update
the signature to include a concrete type (e.g., change def
_resolve_presigned_url(self, output) -> None: to def
_resolve_presigned_url(self, output: dict[str, Any]) -> None:) and import Any
from typing at the top of the module; if a more specific type exists in your
codebase (e.g., a Response or Output model), use that type instead of dict[str,
Any] and keep the return as -> None.

In `@backend/app/utils.py`:
- Around line 595-607: The download_audio_bytes function is vulnerable to SSRF
and memory exhaustion; harden it by validating the input URL and streaming with
size caps: ensure the scheme is http/https, resolve the hostname to an IP and
reject private/loopback/metadata or other disallowed ranges (re-check after
redirects), enforce and check Content-Length against a configured
MAX_AUDIO_BYTES before allocating, use requests.get(stream=True) with short
timeouts and iterate response.iter_content(chunk_size) accumulating up to the
max and aborting if exceeded, and return clear error messages on validation
failures, redirect-caused host changes, timeouts, or oversized payloads; keep
all checks and limits inside download_audio_bytes and reference it when
implementing these changes.

---

Nitpick comments:
In `@backend/app/core/storage_utils.py`:
- Around line 211-222: The _MIME_TO_EXT mapping in storage_utils.py is missing
aliases for m4a mime types so audio/m4a and audio/x-m4a fall back incorrectly;
update the _MIME_TO_EXT dict to include entries mapping "audio/m4a" and
"audio/x-m4a" to "m4a" (alongside the existing audio/* entries) so functions
that rely on _MIME_TO_EXT return the correct .m4a extension.
🪄 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

Run ID: aca2da0e-ba34-4944-b3f4-9eac2b41990d

📥 Commits

Reviewing files that changed from the base of the PR and between e8d87ff and 486af51.

📒 Files selected for processing (8)
  • backend/app/alembic/versions/058_make_llm_call_fields_nullable.py
  • backend/app/core/storage_utils.py
  • backend/app/crud/llm.py
  • backend/app/crud/llm_chain.py
  • backend/app/models/llm/request.py
  • backend/app/services/llm/chain/executor.py
  • backend/app/services/llm/jobs.py
  • backend/app/utils.py

Comment on lines +25 to +28
def downgrade() -> None:
op.alter_column("llm_call", "model", nullable=False)
op.alter_column("llm_call", "provider", nullable=False)
op.alter_column("llm_call", "input_type", nullable=False)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Downgrade can fail once NULL rows exist.

Setting these columns back to nullable=False without backfilling will fail on rollback if any row has NULL in input_type, provider, or model.

Suggested migration hardening
 def downgrade() -> None:
+    op.execute(
+        """
+        UPDATE llm_call
+        SET input_type = COALESCE(input_type, 'text'),
+            provider = COALESCE(provider, 'unknown'),
+            model = COALESCE(model, 'unknown')
+        """
+    )
     op.alter_column("llm_call", "model", nullable=False)
     op.alter_column("llm_call", "provider", nullable=False)
     op.alter_column("llm_call", "input_type", nullable=False)
🤖 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 `@backend/app/alembic/versions/058_make_llm_call_fields_nullable.py` around
lines 25 - 28, The downgrade() changes try to set llm_call.model,
llm_call.provider and llm_call.input_type to nullable=False which will fail if
any rows contain NULL; modify downgrade() to first backfill NULL values for
these columns (or abort with a clear error) before calling op.alter_column, e.g.
run UPDATE statements against the "llm_call" table to set sensible defaults for
model, provider and input_type or raise a RuntimeError if NULLs exist, then call
op.alter_column("llm_call", "model", nullable=False),
op.alter_column("llm_call", "provider", nullable=False) and
op.alter_column("llm_call", "input_type", nullable=False).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

false alarm

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Prajna1999, understood! If downgrades are only expected to run in controlled environments (e.g., dev/staging with no real NULL rows), the concern wouldn't apply in practice. Thanks for the clarification.

Comment thread backend/app/crud/llm.py
self._context.project_id, self._context.organization_id
)

def _resolve_presigned_url(self, output) -> None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a type hint for _resolve_presigned_url parameter.

output is currently untyped; this breaks the project’s typing rule and weakens static checks.

As per coding guidelines: “Always add type hints to all function parameters and return values in Python code”.

🤖 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 `@backend/app/services/llm/chain/executor.py` at line 73, The
_resolve_presigned_url function's parameter is untyped; update the signature to
include a concrete type (e.g., change def _resolve_presigned_url(self, output)
-> None: to def _resolve_presigned_url(self, output: dict[str, Any]) -> None:)
and import Any from typing at the top of the module; if a more specific type
exists in your codebase (e.g., a Response or Output model), use that type
instead of dict[str, Any] and keep the return as -> None.

Comment thread backend/app/utils.py
@Prajna1999 Prajna1999 requested a review from nishika26 May 12, 2026 11:04
@Prajna1999 Prajna1999 requested a review from vprashrex May 13, 2026 11:04
Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (1)
backend/app/crud/llm.py (1)

22-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Multimodal list input is serialized via str(...), which loses structure and is hard to parse later.

list is now an accepted input type, but this path currently falls into return str(query_input). That stores Python repr text instead of a stable JSON payload, which can break downstream reads and defeats detail preservation for multimodal jobs.

Proposed fix
-def serialize_input(query_input: QueryInput | str | list) -> str:
+def serialize_input(query_input: QueryInput | str | list[QueryInput | str]) -> str:
@@
-    else:
+    elif isinstance(query_input, list):
+        return json.dumps(
+            [
+                item.model_dump(mode="json") if hasattr(item, "model_dump") else item
+                for item in query_input
+            ]
+        )
+    else:
         return str(query_input)
🤖 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 `@backend/app/crud/llm.py` around lines 22 - 53, The serialize_input function
currently turns list inputs into a Python string which loses structure; update
serialize_input (and its handling of QueryInput | str | list) to detect when
query_input is a list and serialize it to stable JSON (e.g., json.dumps)
preserving element structure and multimodal metadata, rather than falling
through to return str(...). Ensure the list branch serializes elements
consistently (reusing TextInput and AudioInput serialization logic used in
serialize_input for individual items), so lists of TextInput/AudioInput produce
a JSON array with the same objects/fields (type, format, mime_type, url,
size_bytes) as single-item serialization.
🤖 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 `@backend/app/services/llm/jobs.py`:
- Around line 648-709: The guardrail short-circuit can return before the LlmCall
row is created/resolved, losing call history; ensure the pending→resolved
persistence happens before any early return from
execute_llm_call/apply_input_guardrails by either (A) moving the existing
create/update logic (get_llm_call_by_job_id, update_llm_call_resolved_fields,
create_llm_call) to run immediately after resolving the config and before
calling apply_input_guardrails(), or (B) if keeping guardrail invocation first,
add a code path that when apply_input_guardrails returns
guardrail_direct_response or input_error it still calls
update_llm_call_resolved_fields (or create_llm_call when chain_id set) to
persist the resolved call; reference get_llm_call_by_job_id,
update_llm_call_resolved_fields, create_llm_call, apply_input_guardrails,
execute_llm_call, and start_job to locate the relevant spots.
- Line 1272: The call to get_llm_chain_by_job_id in jobs.py is missing its
import and will raise NameError; add an import for get_llm_chain_by_job_id (the
function used at the call site) at the top of backend/app/services/llm/jobs.py
from the module where it’s defined so the symbol is available before the line
invoking get_llm_chain_by_job_id(session, job_uuid).
- Around line 230-239: The code creates an LlmChain via create_llm_chain but if
start_llm_chain_job() raises, the chain row remains in its initial state; wrap
the start_llm_chain_job(...) call in a try/except that catches exceptions, load
or reference the created chain (from create_llm_chain call or via
job_id/project_id/organization_id), set its status to FAILED (and record the
error message/traceback in the chain's error field), persist that update, and
then re-raise the exception so existing job-failure handling still runs;
reference create_llm_chain, start_llm_chain_job, and execute_chain_job when
locating the code to change.

---

Outside diff comments:
In `@backend/app/crud/llm.py`:
- Around line 22-53: The serialize_input function currently turns list inputs
into a Python string which loses structure; update serialize_input (and its
handling of QueryInput | str | list) to detect when query_input is a list and
serialize it to stable JSON (e.g., json.dumps) preserving element structure and
multimodal metadata, rather than falling through to return str(...). Ensure the
list branch serializes elements consistently (reusing TextInput and AudioInput
serialization logic used in serialize_input for individual items), so lists of
TextInput/AudioInput produce a JSON array with the same objects/fields (type,
format, mime_type, url, size_bytes) as single-item serialization.
🪄 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

Run ID: d05cdbd3-8bfe-484d-9916-7709342fb653

📥 Commits

Reviewing files that changed from the base of the PR and between 1070121 and f3f3be1.

📒 Files selected for processing (3)
  • backend/app/crud/llm.py
  • backend/app/services/llm/jobs.py
  • backend/app/tests/services/llm/test_jobs.py
✅ Files skipped from review due to trivial changes (1)
  • backend/app/tests/services/llm/test_jobs.py

Comment on lines +230 to +239
try:
create_llm_chain(
db,
job_id=job.id,
project_id=project_id,
organization_id=organization_id,
total_blocks=len(request.blocks),
input=serialize_input(request.query.input),
configs=[block.model_dump(mode="json") for block in request.blocks],
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mark the pre-created chain as failed if task enqueueing dies.

This block can create the LlmChain row successfully, but if start_llm_chain_job() raises a few lines later only the Job is marked failed. The chain row is then left in its initial state forever because execute_chain_job() never runs to reconcile it.

💡 Proposed fix
 def start_chain_job(
     db: Session, request: LLMChainRequest, project_id: int, organization_id: int
 ) -> UUID:
     """Create an LLM Chain job and schedule Celery task."""
     trace_id = correlation_id.get() or "N/A"
     job_crud = JobCrud(session=db)
     job = job_crud.create(
         job_type=JobType.LLM_CHAIN, trace_id=trace_id, project_id=project_id
     )
+    chain_record = None
@@
         try:
-            create_llm_chain(
+            chain_record = create_llm_chain(
                 db,
                 job_id=job.id,
                 project_id=project_id,
                 organization_id=organization_id,
                 total_blocks=len(request.blocks),
@@
         except Exception as e:
             span.record_exception(e)
             span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
+            if chain_record is not None:
+                update_llm_chain_status(
+                    db,
+                    chain_id=chain_record.id,
+                    status=ChainStatus.FAILED,
+                    error=str(e),
+                )
             logger.error(
                 f"[start_chain_job] Error starting Celery task: {str(e)} | job_id={job.id}, project_id={project_id}",
                 exc_info=True,
             )
🤖 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 `@backend/app/services/llm/jobs.py` around lines 230 - 239, The code creates an
LlmChain via create_llm_chain but if start_llm_chain_job() raises, the chain row
remains in its initial state; wrap the start_llm_chain_job(...) call in a
try/except that catches exceptions, load or reference the created chain (from
create_llm_chain call or via job_id/project_id/organization_id), set its status
to FAILED (and record the error message/traceback in the chain's error field),
persist that update, and then re-raise the exception so existing job-failure
handling still runs; reference create_llm_chain, start_llm_chain_job, and
execute_chain_job when locating the code to change.

Comment on lines +648 to 709
# For standalone calls (no chain_id), a pending LlmCall row was
# pre-created in start_job(). Find it and fill in the resolved fields.
# For chain block calls (chain_id set), always create a fresh row.
existing = (
get_llm_call_by_job_id(session, job_id)
if chain_id is None
else None
)
if existing is not None:
# Determine input/output types now that config is resolved
completion_type = resolved_config_blob.completion.type or (
resolved_config_blob.completion.params.get("type", "text")
if isinstance(resolved_config_blob.completion.params, dict)
else getattr(
resolved_config_blob.completion.params, "type", "text"
)
)
if completion_type == "stt":
_input_type, _output_type = "audio", "text"
elif completion_type == "tts":
_input_type, _output_type = "text", "audio"
elif isinstance(query.input, ImageInput):
_input_type, _output_type = "image", "text"
elif isinstance(query.input, PDFInput):
_input_type, _output_type = "pdf", "text"
elif isinstance(query.input, list):
_input_type, _output_type = "multimodal", "text"
else:
_input_type, _output_type = "text", "text"

config_dict: dict[str, Any]
if llm_call_request.config.is_stored_config:
config_dict = {
"config_id": str(llm_call_request.config.id),
"config_version": llm_call_request.config.version,
}
else:
config_dict = {
"config_blob": resolved_config_blob.model_dump()
}

llm_call = update_llm_call_resolved_fields(
session,
llm_call_id=existing.id,
input_type=_input_type,
output_type=_output_type,
provider=original_provider,
model=model_name,
config=config_dict,
)
else:
llm_call = create_llm_call(
session,
request=llm_call_request,
job_id=job_id,
project_id=project_id,
organization_id=organization_id,
resolved_config=resolved_config_blob,
original_provider=original_provider,
chain_id=chain_id,
)
llm_call_id = llm_call.id
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the call record before any input-guardrail early return.

With the new pending→resolved flow, the LlmCall only gets created/resolved in this block. If apply_input_guardrails() returns guardrail_direct_response or input_error earlier, execute_llm_call() exits without updating the pre-created standalone row and without creating a chain-block row at all. That drops successful short-circuit responses from llm_call history.

🤖 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 `@backend/app/services/llm/jobs.py` around lines 648 - 709, The guardrail
short-circuit can return before the LlmCall row is created/resolved, losing call
history; ensure the pending→resolved persistence happens before any early return
from execute_llm_call/apply_input_guardrails by either (A) moving the existing
create/update logic (get_llm_call_by_job_id, update_llm_call_resolved_fields,
create_llm_call) to run immediately after resolving the config and before
calling apply_input_guardrails(), or (B) if keeping guardrail invocation first,
add a code path that when apply_input_guardrails returns
guardrail_direct_response or input_error it still calls
update_llm_call_resolved_fields (or create_llm_call when chain_id set) to
persist the resolved call; reference get_llm_call_by_job_id,
update_llm_call_resolved_fields, create_llm_call, apply_input_guardrails,
execute_llm_call, and start_job to locate the relevant spots.

input=serialize_input(request.query.input),
configs=[block.model_dump(mode="json") for block in request.blocks],
)
chain_record = get_llm_chain_by_job_id(session, job_uuid)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Import get_llm_chain_by_job_id before calling it.

This path will fail with NameError on the first chain execution because the symbol is never imported into backend/app/services/llm/jobs.py.

🐛 Proposed fix
-from app.crud.llm_chain import create_llm_chain, update_llm_chain_status
+from app.crud.llm_chain import (
+    create_llm_chain,
+    get_llm_chain_by_job_id,
+    update_llm_chain_status,
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chain_record = get_llm_chain_by_job_id(session, job_uuid)
from app.crud.llm_chain import (
create_llm_chain,
get_llm_chain_by_job_id,
update_llm_chain_status,
)
🧰 Tools
🪛 Ruff (0.15.12)

[error] 1272-1272: Undefined name get_llm_chain_by_job_id

(F821)

🤖 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 `@backend/app/services/llm/jobs.py` at line 1272, The call to
get_llm_chain_by_job_id in jobs.py is missing its import and will raise
NameError; add an import for get_llm_chain_by_job_id (the function used at the
call site) at the top of backend/app/services/llm/jobs.py from the module where
it’s defined so the symbol is available before the line invoking
get_llm_chain_by_job_id(session, job_uuid).

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

Labels

enhancement New feature or request ready-for-review

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

Job Management: Preserve llm_call details

2 participants