Studio: do not bind a research run to a populated assistant reply
create_run adopted any assistant message under the user turn whose researchRunId was unset, including a prior answer reused by a retry. On completion _update_assistant drops the untagged text and source parts, so that answer was silently overwritten. Only bind to an empty placeholder or this run's own message, and reject a reply that already carries content.
This commit is contained in:
parent
4bc8e0bbe4
commit
048460a3f0
2 changed files with 51 additions and 0 deletions
|
|
@ -192,11 +192,25 @@ def create_run(
|
|||
if isinstance(existing_metadata, dict)
|
||||
else None
|
||||
)
|
||||
# Only bind to an empty placeholder or this run's own message. An
|
||||
# untagged reply carries text/source parts that _update_assistant
|
||||
# drops on completion, so binding one silently overwrites an
|
||||
# existing answer (for example a retry reusing a prior answer id).
|
||||
existing_answer = any(
|
||||
isinstance(part, dict)
|
||||
and (
|
||||
(part.get("type") == "text" and (part.get("text") or "").strip())
|
||||
or part.get("type") == "source"
|
||||
)
|
||||
and part.get("researchRunId") is None
|
||||
for part in _loads(message["content_json"], [])
|
||||
)
|
||||
if (
|
||||
message["thread_id"] != thread_id
|
||||
or message["role"] != "assistant"
|
||||
or message["parent_id"] != user_message_id
|
||||
or existing_run_id not in (None, run_id)
|
||||
or (existing_run_id is None and existing_answer)
|
||||
):
|
||||
raise ResearchConflictError(
|
||||
"Assistant message does not match this research run"
|
||||
|
|
|
|||
|
|
@ -2208,6 +2208,43 @@ def test_create_run_conflict_rolls_back_placeholder_and_run(research_home):
|
|||
assert studio_db.get_chat_message("thread-1", "conflict")["parentId"] is None
|
||||
|
||||
|
||||
def test_create_run_rejects_binding_to_populated_reply(research_home):
|
||||
# A prior answer under the same user turn (untagged, no researchRunId) must
|
||||
# not be adopted as the placeholder: _update_assistant would drop its
|
||||
# text/source parts on completion and silently overwrite that answer.
|
||||
studio_db.upsert_chat_message(
|
||||
{
|
||||
"id": "prior-answer",
|
||||
"threadId": "thread-1",
|
||||
"parentId": "user-1",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": "existing answer"},
|
||||
{"type": "source", "sourceType": "url", "url": "https://kept.example"},
|
||||
],
|
||||
"createdAt": 4,
|
||||
}
|
||||
)
|
||||
with pytest.raises(research_db.ResearchConflictError):
|
||||
_create(assistant_message_id = "prior-answer")
|
||||
assert research_db.get_run("run-1") is None
|
||||
preserved = studio_db.get_chat_message("thread-1", "prior-answer")
|
||||
assert preserved["content"][0]["text"] == "existing answer"
|
||||
# An empty placeholder under the same turn is still accepted.
|
||||
studio_db.upsert_chat_message(
|
||||
{
|
||||
"id": "empty-placeholder",
|
||||
"threadId": "thread-1",
|
||||
"parentId": "user-1",
|
||||
"role": "assistant",
|
||||
"content": [],
|
||||
"createdAt": 5,
|
||||
}
|
||||
)
|
||||
run = _create(assistant_message_id = "empty-placeholder")
|
||||
assert run["assistantMessageId"] == "empty-placeholder"
|
||||
|
||||
|
||||
def test_update_assistant_replaces_report_parts_without_duplication(research_home):
|
||||
from core.research_runs import _update_assistant
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue