added ambiguity node after schema explorer - #26
Conversation
📝 WalkthroughWalkthroughThe agent adds a dedicated ambiguity-detection and HITL clarification path before SQL generation, with retry limits and unanswerable outcomes. API and frontend contracts expose this status, while schema exploration, diagnostic messages, and profiling endpoint handling are updated. ChangesAmbiguity workflow
Profiling endpoint handling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant MCPServer
participant SchemaExplorer
participant DetectAmbiguity
participant AmbiguityResolution
participant QueryBuilder
User->>MCPServer: Submit request
MCPServer->>SchemaExplorer: Execute schema exploration
SchemaExplorer->>DetectAmbiguity: schema_plan and schema context
DetectAmbiguity->>QueryBuilder: Clear result
DetectAmbiguity->>AmbiguityResolution: Ambiguous result and questions
User->>MCPServer: Provide clarification
MCPServer->>AmbiguityResolution: Resume with feedback
AmbiguityResolution->>SchemaExplorer: Retry with updated user_query
DetectAmbiguity-->>MCPServer: Unanswerable result
MCPServer-->>User: is_unanswerable response
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@agent/src/agent/nodes/detect_ambiguity.py`:
- Around line 155-165: Update the parse-failure fallback in detect_ambiguity to
use a generic user-facing clarification in the reason field, preventing raw
exception details from reaching downstream clarifying-question output. Keep the
captured exception available only through logging or internal ambiguity_result
data, while preserving the existing ambiguous classification.
In `@agent/src/agent/nodes/schema_explorer.py`:
- Around line 525-529: Update the exception fallback in the schema explorer
parsing flow to construct SchemaExplorerOutput with an empty string (or the
model’s valid default) for schema_plan instead of None. Preserve the existing
plan = data.schema_plan or "" recovery behavior so parsing failures return
normally without triggering another validation error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 26a079bf-e99e-4260-b056-0974e018258d
📒 Files selected for processing (14)
agent/scripts/upload_all_prompts.pyagent/src/agent/config.pyagent/src/agent/graph.pyagent/src/agent/mcp_server.pyagent/src/agent/nodes/detect_ambiguity.pyagent/src/agent/nodes/refiner.pyagent/src/agent/nodes/satisfaction_check.pyagent/src/agent/nodes/schema_explorer.pyagent/src/agent/state.pyagent/src/agent/utils/flag_bridge.pyagent/src/agent/utils/schema_enrichment.pybackend/app/routers/agent.pyfrontend/src/api/agent.tsfrontend/src/pages/AgentTestingPage.tsx
💤 Files with no reviewable changes (1)
- agent/src/agent/utils/flag_bridge.py
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
agent/src/agent/nodes/detect_ambiguity.py (1)
263-274: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winPass the fallback clarification via
feedbackso it reachesschema_explorer.When the user provides no feedback, the code constructs
user_feedbackwith the previous question but then explicitly omits it fromnew_queryand sets"feedback": None. This discards the fallback context entirely, leavingschema_explorerwith the exact same inputs as the previous run. This will result in a loop of identical outputs until max retries are hit.Pass the fallback context via the
feedbackstate field soschema_explorercan use it. The downstreamsql_static_validations_nodewill safely clear it before it reaches therejection_router.🐛 Proposed fix
new_query = state.get("user_query", "") if user_feedback and not user_feedback.startswith("[Previous"): new_query += f"\n[User Clarification: {user_feedback}]" return { # Update the main query so all subsequent nodes see the unified intent. "user_query": new_query, # Clear ambiguity decision so detect_ambiguity re-evaluates cleanly on retry. "ambiguity_type": None, "ambiguity_result": None, "clarifying_questions": None, - # Clear feedback so it doesn't accidentally trigger the rejection_router later. - "feedback": None, + # Pass the previous question as feedback if no user clarification was provided; + # sql_static_validations_node will safely clear it later. + "feedback": user_feedback if user_feedback.startswith("[Previous") else None, "ambiguity_retry_count": retry_count, "execution_path": ["ambiguity_resolution"], }🤖 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 `@agent/src/agent/nodes/detect_ambiguity.py` around lines 263 - 274, Update the retry return state in the ambiguity-handling flow to preserve the constructed fallback user_feedback through the "feedback" field instead of setting it to None. Keep the existing new_query behavior for explicit feedback, and ensure schema_explorer receives the fallback clarification while downstream validation clears it before rejection routing.
🤖 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 `@agent/src/agent/nodes/detect_ambiguity.py`:
- Around line 157-166: Update the exception fallback result in the ambiguity
detection flow to include only the current AmbiguityResult fields:
ambiguity_type, reason, and clarifying_questions. Remove intent_deconstruction,
active_schema_search, ambiguity_check, logical_sql_plan, and
schema_alignment_check from the parsed fallback.
---
Outside diff comments:
In `@agent/src/agent/nodes/detect_ambiguity.py`:
- Around line 263-274: Update the retry return state in the ambiguity-handling
flow to preserve the constructed fallback user_feedback through the "feedback"
field instead of setting it to None. Keep the existing new_query behavior for
explicit feedback, and ensure schema_explorer receives the fallback
clarification while downstream validation clears it before rejection routing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 43eabab8-5f21-4f33-8c74-0657b5e4be95
📒 Files selected for processing (2)
agent/src/agent/nodes/detect_ambiguity.pyagent/src/agent/nodes/schema_explorer.py
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
agent/src/agent/config.py (1)
46-46: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winKeep ambiguity detection disabled until its Langfuse prompt is guaranteed to exist.
ENABLE_AMBIGUITY_DETECTdefaults toTrue, so upgrades withouttext2sql/detect_ambiguitywill route intodetect_ambiguityand raise at request time. Either leave the default off until rollout is complete or provision/validate the prompt at startup.🤖 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 `@agent/src/agent/config.py` at line 46, Update the ambiguity-detection configuration around ENABLE_AMBIGUITY_DETECT so it defaults to disabled until the Langfuse prompt identified by LANGFUSE_PROMPT_DETECT_AMBIGUITY is guaranteed to exist; alternatively, add startup validation/provisioning that verifies this prompt before enabling the feature.
🤖 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 `@agent/src/agent/config.py`:
- Line 73: Update the MAX_AMBIGUITY_RETRIES configuration field to use
validation with a default of 2 and a minimum value of 0, rejecting negative
environment-configured values while preserving zero as valid. Leave the existing
ambiguity retry handling unchanged.
---
Outside diff comments:
In `@agent/src/agent/config.py`:
- Line 46: Update the ambiguity-detection configuration around
ENABLE_AMBIGUITY_DETECT so it defaults to disabled until the Langfuse prompt
identified by LANGFUSE_PROMPT_DETECT_AMBIGUITY is guaranteed to exist;
alternatively, add startup validation/provisioning that verifies this prompt
before enabling the feature.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9b709a29-07df-45ab-923c-ef166ba17b9c
📒 Files selected for processing (3)
agent/src/agent/config.pyagent/src/agent/graph.pyagent/src/agent/nodes/detect_ambiguity.py
💤 Files with no reviewable changes (1)
- agent/src/agent/nodes/detect_ambiguity.py
There was a problem hiding this comment.
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 (6)
agent/src/agent/nodes/schema_explorer.py (2)
525-533: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftDo not treat schema-explorer failures as an empty successful plan.
The
exceptblock catches every structured-output/LLM failure and returns emptyschema_planandtables_used.route_schema_exploreronly retries or escalates whenhallucinated_tablesis populated, so this path can proceed to SQL generation without schema context. Propagate an explicit failure that routes to retry/HITL instead of returning a valid-looking empty result.🤖 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 `@agent/src/agent/nodes/schema_explorer.py` around lines 525 - 533, Update the structured-output exception handling in the schema explorer flow to propagate an explicit failure instead of constructing an empty SchemaExplorerOutput. Ensure route_schema_explorer receives the failure through its existing retry or HITL escalation path, preventing SQL generation from continuing with an empty schema_plan and tables_used.
309-311: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse consistent nullable
RunnableConfigcontracts across nodes.Each affected node defaults
configtoNoneand handles the missing value, but annotates it as non-nullable. Change these signatures toRunnableConfig | None = None, or requireconfigconsistently.
agent/src/agent/nodes/schema_explorer.py#L309-L311: updateschema_explorer_node.agent/src/agent/nodes/schema_explorer.py#L540-L540: updatesql_static_validations_node.agent/src/agent/nodes/satisfaction_check.py#L40-L47: updatesatisfaction_check_node.🤖 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 `@agent/src/agent/nodes/schema_explorer.py` around lines 309 - 311, Use consistent nullable RunnableConfig contracts by changing the config parameter annotation to RunnableConfig | None = None in schema_explorer_node and sql_static_validations_node in agent/src/agent/nodes/schema_explorer.py (lines 309-311 and 540), and satisfaction_check_node in agent/src/agent/nodes/satisfaction_check.py (lines 40-47). Preserve the existing missing-config handling.backend/app/routers/profiling.py (3)
242-264: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winHonor the
forcerequest parameter.
ProfilingTabsendsforce: truefor “Restart from Start” and “Re-profile”, but this endpoint never readsforce; onlyresume_from_partialis passed to Temporal. These actions cannot implement their advertised cache/restart semantics.🤖 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/routers/profiling.py` around lines 242 - 264, Update run_table_profile to honor the force parameter by propagating it through trigger_temporal_profiling_workflow and the underlying profiling workflow invocation. Ensure forced requests bypass the 24-hour cache and restart from the beginning, while preserving resume_from_partial behavior for non-forced requests.
258-264: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftAvoid duplicate run rows for an already-running workflow.
Every request inserts a new pending
ProfilingRun, whileWorkflowAlreadyStartedErroris treated as success for the same fixed workflow ID. A second request therefore creates a synthetic latest run without starting a new execution. Reuse the existing active run or associate each database run with its workflow execution.🤖 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/routers/profiling.py` around lines 258 - 264, Update the profiling request flow around ProfilingRun creation and trigger_temporal_profiling_workflow so repeated requests for an already-running workflow do not insert a new pending row. Before creating a run, locate and reuse the existing active run for the fixed workflow ID, or associate the new run with a distinct execution ID; ensure WorkflowAlreadyStartedError continues to reference the actual active run rather than a synthetic latest row.
326-338: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not mark profiling cancellation complete until the workflow is terminal.
handle.cancel()only requests cancellation, butTableProfilingWorkflow.run()catchesasyncio.CancelledErrorand then runspersist_profiling_results_activity(), which persists the partial profile and can finalize the run ascompleted. The router currently writesProfilingStatus.canceledimmediately; make the worker’s final terminal update authoritative or poll until Temporal reports a terminal state before updating the run status.🤖 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/routers/profiling.py` around lines 326 - 338, The profiling cancellation flow around handle.cancel() must not immediately set latest_run.status to canceled, because the worker may still persist results and finalize the run. Remove the immediate status update and make TableProfilingWorkflow.run()’s final terminal update authoritative, or poll the workflow handle until Temporal reports a terminal state before committing cancellation; preserve the existing error handling.backend/app/infra_init.py (1)
1194-1194: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winInclude column-level failures in the partial calculation.
run_table_profilingrecords failures inColumnStats.errors, whileresult.successremains true for non-empty tables andresult.errorsmay remain empty. This expression can therefore persistis_partial=Falsefor an incomplete profile.🤖 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/infra_init.py` at line 1194, Update the is_partial assignment in run_table_profiling to also detect column-level failures recorded in ColumnStats.errors, while preserving the existing result.success and result.errors checks. Ensure any non-empty column error collection marks the profile as partial.
♻️ Duplicate comments (1)
agent/src/agent/config.py (1)
78-81: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject negative ambiguity retry limits.
MAX_AMBIGUITY_RETRIESis environment-configurable but accepts negative values. Sincedetect_ambiguity.pychecksretry_count >= settings.MAX_AMBIGUITY_RETRIES, any negative value makes every ambiguous request immediately unanswerable. UseField(default=2, ge=0).🤖 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 `@agent/src/agent/config.py` around lines 78 - 81, Constrain the environment-configurable MAX_AMBIGUITY_RETRIES setting in the configuration model to a nonnegative integer by declaring it with Field(default=2, ge=0), preserving the existing default and preventing negative retry limits.
🤖 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/routers/profiling.py`:
- Around line 330-334: Update the exception handling around the Temporal
workflow cancellation to keep logging the detailed exception server-side, but
replace the HTTPException detail with a stable generic message instead of
interpolating str(e). Preserve the 500 status and the existing cancellation
failure context in the server log.
- Around line 180-197: Align the pending profile sentinel with ProfilingTab’s
frontend checks by updating the row_count condition used for progress and
failure panels to treat both null and undefined as absent. Apply the same
adjustment to the additional check around the profile stub handling, while
preserving existing behavior for populated row counts.
- Around line 180-200: Update both response paths in the profiling handler to
include is_partial=False in the fallback profile stub, while preserving the
persisted is_partial value from profile.model_dump() for real profiles. Remove
the unconditional is_partial=False keyword from both TableProfileRead
constructions so stored values are not overwritten and duplicate-keyword errors
cannot occur.
---
Outside diff comments:
In `@agent/src/agent/nodes/schema_explorer.py`:
- Around line 525-533: Update the structured-output exception handling in the
schema explorer flow to propagate an explicit failure instead of constructing an
empty SchemaExplorerOutput. Ensure route_schema_explorer receives the failure
through its existing retry or HITL escalation path, preventing SQL generation
from continuing with an empty schema_plan and tables_used.
- Around line 309-311: Use consistent nullable RunnableConfig contracts by
changing the config parameter annotation to RunnableConfig | None = None in
schema_explorer_node and sql_static_validations_node in
agent/src/agent/nodes/schema_explorer.py (lines 309-311 and 540), and
satisfaction_check_node in agent/src/agent/nodes/satisfaction_check.py (lines
40-47). Preserve the existing missing-config handling.
In `@backend/app/infra_init.py`:
- Line 1194: Update the is_partial assignment in run_table_profiling to also
detect column-level failures recorded in ColumnStats.errors, while preserving
the existing result.success and result.errors checks. Ensure any non-empty
column error collection marks the profile as partial.
In `@backend/app/routers/profiling.py`:
- Around line 242-264: Update run_table_profile to honor the force parameter by
propagating it through trigger_temporal_profiling_workflow and the underlying
profiling workflow invocation. Ensure forced requests bypass the 24-hour cache
and restart from the beginning, while preserving resume_from_partial behavior
for non-forced requests.
- Around line 258-264: Update the profiling request flow around ProfilingRun
creation and trigger_temporal_profiling_workflow so repeated requests for an
already-running workflow do not insert a new pending row. Before creating a run,
locate and reuse the existing active run for the fixed workflow ID, or associate
the new run with a distinct execution ID; ensure WorkflowAlreadyStartedError
continues to reference the actual active run rather than a synthetic latest row.
- Around line 326-338: The profiling cancellation flow around handle.cancel()
must not immediately set latest_run.status to canceled, because the worker may
still persist results and finalize the run. Remove the immediate status update
and make TableProfilingWorkflow.run()’s final terminal update authoritative, or
poll the workflow handle until Temporal reports a terminal state before
committing cancellation; preserve the existing error handling.
---
Duplicate comments:
In `@agent/src/agent/config.py`:
- Around line 78-81: Constrain the environment-configurable
MAX_AMBIGUITY_RETRIES setting in the configuration model to a nonnegative
integer by declaring it with Field(default=2, ge=0), preserving the existing
default and preventing negative retry limits.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 25861bea-493f-440d-aab9-7f66330056a1
📒 Files selected for processing (8)
agent/src/agent/config.pyagent/src/agent/nodes/satisfaction_check.pyagent/src/agent/nodes/schema_explorer.pyagent/src/agent/state.pybackend/app/infra_init.pybackend/app/routers/profiling.pyfrontend/src/components/tables/ProfilingTab.tsxfrontend/src/types/index.ts
| profile_dict = ( | ||
| profile.model_dump() | ||
| if profile | ||
| else { | ||
| "id": "pending", | ||
| "table_id": table_id, | ||
| "row_count": None, | ||
| "sample_size": None, | ||
| "column_count": None, | ||
| "size_bytes": None, | ||
| "null_rate_avg": None, | ||
| "duplicate_rate": None, | ||
| "sample_data": None, | ||
| "profile_json": None, | ||
| "cached_until": None, | ||
| "created_at": datetime.now(), | ||
| "updated_at": datetime.now(), | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Align the pending sentinel with the frontend checks.
The stub serializes row_count as null, but ProfilingTab checks profile?.row_count === undefined for its progress and failure panels. Consequently, pending/failed stub responses render neither panel. Use a consistent sentinel, such as row_count == null in the frontend.
Also applies to: 279-296
🤖 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/routers/profiling.py` around lines 180 - 197, Align the pending
profile sentinel with ProfilingTab’s frontend checks by updating the row_count
condition used for progress and failure panels to treat both null and undefined
as absent. Apply the same adjustment to the additional check around the profile
stub handling, while preserving existing behavior for populated row counts.
| profile_dict = ( | ||
| profile.model_dump() | ||
| if profile | ||
| else { | ||
| "id": "pending", | ||
| "table_id": table_id, | ||
| "row_count": None, | ||
| "sample_size": None, | ||
| "column_count": None, | ||
| "size_bytes": None, | ||
| "null_rate_avg": None, | ||
| "duplicate_rate": None, | ||
| "sample_data": None, | ||
| "profile_json": None, | ||
| "cached_until": None, | ||
| "created_at": datetime.now(), | ||
| "updated_at": datetime.now(), | ||
| } | ||
| ) | ||
|
|
||
| return TableProfileRead(**profile_dict, status=status, is_partial=False) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Preserve the persisted partial-profile state.
Both responses force is_partial=False, even though backend/app/infra_init.py now computes and stores that flag and the frontend uses it to expose retry actions. If model_dump() includes is_partial, this also raises a duplicate-keyword error. Default the stub to False, but preserve the stored value for real profiles.
Proposed fix
- return TableProfileRead(**profile_dict, status=status, is_partial=False)
+ profile_dict.setdefault("is_partial", False)
+ return TableProfileRead(**profile_dict, status=status)Apply the same change to both response paths.
Also applies to: 279-301
🤖 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/routers/profiling.py` around lines 180 - 200, Update both
response paths in the profiling handler to include is_partial=False in the
fallback profile stub, while preserving the persisted is_partial value from
profile.model_dump() for real profiles. Remove the unconditional
is_partial=False keyword from both TableProfileRead constructions so stored
values are not overwritten and duplicate-keyword errors cannot occur.
| except Exception as e: | ||
| logger.warning(f"Failed to cancel temporal workflow for {table_id}: {e}") | ||
| raise HTTPException(status_code=500, detail=f"Failed to cancel temporal workflow: {e}") | ||
| raise HTTPException( | ||
| status_code=500, detail=f"Failed to cancel temporal workflow: {e}" | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Do not expose raw Temporal errors to clients.
Returning str(e) in the HTTP detail can leak internal transport, host, or workflow information. Log the exception server-side and return a stable generic error response.
🤖 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/routers/profiling.py` around lines 330 - 334, Update the
exception handling around the Temporal workflow cancellation to keep logging the
detailed exception server-side, but replace the HTTPException detail with a
stable generic message instead of interpolating str(e). Preserve the 500 status
and the existing cancellation failure context in the server log.
Summary by CodeRabbit