Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a52feae
[FIX] Resolve Prompt Studio default LLM profile and unblock deploy wh…
hari-kuriakose Jul 24, 2026
5a4da0e
[TEST] Pin challenge_llm schema behaviour across the enable_challenge…
hari-kuriakose Jul 24, 2026
83bd7d7
[FIX] Address review: seed challenge_llm instead of dropping adapterType
hari-kuriakose Jul 30, 2026
4c4c540
[FIX] Keep the challenge_llm tests collectable in the unit-backend lane
hari-kuriakose Jul 30, 2026
c5c2289
[FIX] Record the resolved profile in the bulk fetch_response callback…
hari-kuriakose Jul 30, 2026
27dd539
[TEST] Defer every real import in the challenge_llm tests to call time
hari-kuriakose Jul 30, 2026
77c857b
Merge origin/main into fix/prompt-studio-profile-fallback-challenge-llm
hari-kuriakose Jul 30, 2026
74c3aa6
[TEST] Cover the fetch_response profile resolution ladder
hari-kuriakose Jul 30, 2026
1fa6a7a
[TEST] Test the real payload builders, not extracted source text
hari-kuriakose Jul 31, 2026
7d08358
[TEST] Cover the synchronous _fetch_response path too
hari-kuriakose Jul 31, 2026
a517cd9
[FIX] Book synchronous fetch_response output against the profile used
hari-kuriakose Jul 31, 2026
3d59981
[TEST] Harden the profile-resolution tests against three proven gaps
hari-kuriakose Jul 31, 2026
6cc40b3
[REFACTOR] Extract the profile resolution ladder into one helper
hari-kuriakose Jul 31, 2026
bf33f56
[TEST] Close the last two Low findings from the third review pass
hari-kuriakose Jul 31, 2026
34e5e48
Merge branch 'main' into fix/prompt-studio-profile-fallback-challenge…
muhammad-ali-e Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions backend/prompt_studio/prompt_studio_core_v2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ def __init__(self, detail: str | None = None, status_code: int = 500):


class DefaultProfileError(APIException):
status_code = 500
# A missing default profile is a project-configuration problem the user can
# fix, not a server fault - 500 misreports it as an outage.
status_code = 400
default_detail = (
"Default LLM profile is not configured."
"Please set an LLM profile as default to continue."
"No LLM profile could be resolved. Set an LLM profile as the project "
"default, or attach one to the prompt, to continue."
)


Expand Down
90 changes: 62 additions & 28 deletions backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,36 @@ def build_index_payload(

return context, cb_kwargs

@staticmethod
def _resolve_profile_manager(
tool: Any, prompt: Any = None, profile_manager_id: str | None = None
) -> Any:
"""Resolve the profile a run executes under.

The ladder, in order: an explicitly passed ``profile_manager_id``, then
the prompt's own FK, then the project default. A prompt need not carry
its own FK - falling back to the project default matches what
index_document and single-pass extraction already do.

``get_default_llm_profile`` raises ``DefaultProfileError`` when no
project default exists, so this never returns a falsy value.

Args:
tool (CustomTool): Prompt Studio project the prompt belongs to
prompt (ToolStudioPrompt | None): Prompt whose FK to consult, if any
profile_manager_id (str | None): Explicitly requested profile

Returns:
ProfileManager: The resolved profile
"""
if profile_manager_id:
return ProfileManagerHelper.get_profile_manager(
profile_manager_id=profile_manager_id
)
if prompt is not None and prompt.profile_manager:
return prompt.profile_manager
return ProfileManager.get_default_llm_profile(tool)

@staticmethod
def _resolve_llm_ids(tool: Any) -> tuple[str, str]:
"""Resolve monitor_llm and challenge_llm IDs for the tool."""
Expand Down Expand Up @@ -766,14 +796,9 @@ def build_fetch_response_payload(
Returns:
(context, cb_kwargs) or (None, pending_response_dict)
"""
profile_manager = prompt.profile_manager
if profile_manager_id:
profile_manager = ProfileManagerHelper.get_profile_manager(
profile_manager_id=profile_manager_id
)

if not profile_manager:
raise DefaultProfileError()
profile_manager = PromptStudioHelper._resolve_profile_manager(
tool=tool, prompt=prompt, profile_manager_id=profile_manager_id
)

monitor_llm, challenge_llm = PromptStudioHelper._resolve_llm_ids(tool)

Expand Down Expand Up @@ -960,7 +985,11 @@ def build_fetch_response_payload(
"document_id": document_id,
"tool_id": tool_id,
"prompt_ids": [str(prompt.prompt_id)],
"profile_manager_id": profile_manager_id,
# Record the profile actually used, not the (possibly None) argument.
# The callback otherwise re-resolves the project default, so a
# default change mid-run would book output against a different
# profile than the one that produced it.
"profile_manager_id": str(profile_manager.profile_id),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"is_single_pass": False,
}

Expand Down Expand Up @@ -989,15 +1018,10 @@ def build_bulk_fetch_response_payload(
Returns:
(context, cb_kwargs) or (None, pending_response_dict)
"""
profile_manager = (
ProfileManagerHelper.get_profile_manager(profile_manager_id)
if profile_manager_id
else None
)
if not profile_manager:
profile_manager = ProfileManager.get_default_llm_profile(tool)
if not profile_manager:
raise DefaultProfileError()
# No single prompt to consult here, so the FK rung is skipped.
profile_manager = PromptStudioHelper._resolve_profile_manager(
tool=tool, profile_manager_id=profile_manager_id
)

PromptStudioHelper.validate_adapter_status(profile_manager)
PromptStudioHelper.validate_profile_manager_owner_access(
Expand Down Expand Up @@ -1154,7 +1178,9 @@ def build_bulk_fetch_response_payload(
"document_id": document_id,
"tool_id": tool_id,
"prompt_ids": [str(p.prompt_id) for p in prompts],
"profile_manager_id": profile_manager_id,
# Record the profile actually used, not the (possibly None)
# argument - same reason as build_fetch_response_payload above.
"profile_manager_id": str(profile_manager.profile_id),
"is_single_pass": False,
}

Expand Down Expand Up @@ -1701,13 +1727,26 @@ def _execute_single_prompt(
user_id=user_id,
request_user=request_user,
)
# Book the output against the profile the run actually used, the
# same ladder _fetch_response applies. Forwarding the raw (possibly
# None) argument makes _handle_response re-resolve the project
# default, so a prompt carrying its own FK would run under that FK
# but have its output stored under the project default.
resolved_profile_id = str(
PromptStudioHelper._resolve_profile_manager(
tool=tool,
prompt=prompt_instance,
profile_manager_id=profile_manager_id,
).profile_id
)

return PromptStudioHelper._handle_response(
response=response,
run_id=run_id,
prompts=prompts,
document_id=document_id,
is_single_pass=False,
profile_manager_id=profile_manager_id,
profile_manager_id=resolved_profile_id,
)
except APIException:
# Validation responses are user-facing; DRF renders them as-is.
Expand Down Expand Up @@ -1880,14 +1919,9 @@ def _fetch_response(
Any: Output from LLM
"""
# Fetch the ProfileManager instance using the profile_manager_id if provided
profile_manager = prompt.profile_manager
if profile_manager_id:
profile_manager = ProfileManagerHelper.get_profile_manager(
profile_manager_id=profile_manager_id
)

if not profile_manager:
raise DefaultProfileError()
profile_manager = PromptStudioHelper._resolve_profile_manager(
tool=tool, prompt=prompt, profile_manager_id=profile_manager_id
)

monitor_llm_instance: AdapterInstance | None = tool.monitor_llm
monitor_llm: str | None = None
Expand Down
Loading
Loading