feat(studio): R3 step_respond、运行页 UX 与 R4 思考流式
新增 worldbook 步骤 LLM 回复(thinking/draft/questions/evaluation), 运行页聊天区与产物/选项联动;评价区标签改为「评价与修改建议」; 流式开关开启时 NDJSON 推送思考内容,完成后拆分更新各字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import shutil
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||
|
||||
from core.config import settings
|
||||
from models.studio_models import (
|
||||
@@ -22,8 +22,13 @@ from models.studio_models import (
|
||||
StudioRunSummary,
|
||||
)
|
||||
from services.character_service import CharacterService
|
||||
from services.studio_context_service import store_context_on_run
|
||||
from services.studio_context_service import assemble_prompt_blocks, store_context_on_run
|
||||
from services.studio_project_service import studio_project_service
|
||||
from services.studio_step_respond import (
|
||||
resolve_api_config,
|
||||
studio_step_respond,
|
||||
studio_step_respond_stream,
|
||||
)
|
||||
from services.worldbook_service import worldbook_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -302,6 +307,181 @@ class StudioRunService:
|
||||
self._save_run(project_id, run_id, updated_run)
|
||||
return updated_run
|
||||
|
||||
async def send_run_message(
|
||||
self,
|
||||
project_id: str,
|
||||
run_id: str,
|
||||
content: str,
|
||||
*,
|
||||
stream: bool = False,
|
||||
profile_id: Optional[str] = None,
|
||||
api_config: Optional[Dict[str, str]] = None,
|
||||
) -> StudioRun:
|
||||
"""Send a user message to the active worldbook step and invoke LLM (R3)."""
|
||||
trimmed = (content or "").strip()
|
||||
if not trimmed:
|
||||
raise ValueError("消息内容不能为空")
|
||||
|
||||
run, current_node, current_state, current_node_id = self._prepare_run_message(
|
||||
project_id, run_id, trimmed
|
||||
)
|
||||
|
||||
resolved_api = resolve_api_config(profile_id, api_config)
|
||||
prompt_blocks = assemble_prompt_blocks(run, current_node_id)
|
||||
step_messages = list(current_state.stepMessages or [])
|
||||
|
||||
last_draft, last_tool_response, user_msg, assistant_msg = (
|
||||
await studio_step_respond(
|
||||
node=current_node,
|
||||
prompt_blocks=prompt_blocks,
|
||||
step_messages=step_messages,
|
||||
user_message=trimmed,
|
||||
existing_draft=current_state.lastDraft,
|
||||
api_config=resolved_api,
|
||||
stream=stream,
|
||||
)
|
||||
)
|
||||
|
||||
return self._persist_run_message_turn(
|
||||
project_id,
|
||||
run_id,
|
||||
run,
|
||||
current_node_id,
|
||||
prompt_blocks,
|
||||
step_messages,
|
||||
last_draft,
|
||||
last_tool_response,
|
||||
user_msg,
|
||||
assistant_msg,
|
||||
)
|
||||
|
||||
async def send_run_message_stream(
|
||||
self,
|
||||
project_id: str,
|
||||
run_id: str,
|
||||
content: str,
|
||||
*,
|
||||
profile_id: Optional[str] = None,
|
||||
api_config: Optional[Dict[str, str]] = None,
|
||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""Stream thinking deltas, then persist and emit complete run (R4)."""
|
||||
trimmed = (content or "").strip()
|
||||
if not trimmed:
|
||||
raise ValueError("消息内容不能为空")
|
||||
|
||||
run, current_node, current_state, current_node_id = self._prepare_run_message(
|
||||
project_id, run_id, trimmed
|
||||
)
|
||||
|
||||
resolved_api = resolve_api_config(profile_id, api_config)
|
||||
prompt_blocks = assemble_prompt_blocks(run, current_node_id)
|
||||
step_messages = list(current_state.stepMessages or [])
|
||||
|
||||
async for event in studio_step_respond_stream(
|
||||
node=current_node,
|
||||
prompt_blocks=prompt_blocks,
|
||||
step_messages=step_messages,
|
||||
user_message=trimmed,
|
||||
existing_draft=current_state.lastDraft,
|
||||
api_config=resolved_api,
|
||||
):
|
||||
if event.get("type") == "thinking_delta":
|
||||
yield event
|
||||
continue
|
||||
|
||||
if event.get("type") == "complete":
|
||||
from models.studio_models import LastToolResponse, StepMessage
|
||||
|
||||
last_draft = event["last_draft"]
|
||||
last_tool_response = LastToolResponse(**event["last_tool_response"])
|
||||
user_msg = StepMessage(**event["user_msg"])
|
||||
assistant_msg = StepMessage(**event["assistant_msg"])
|
||||
|
||||
updated_run = self._persist_run_message_turn(
|
||||
project_id,
|
||||
run_id,
|
||||
run,
|
||||
current_node_id,
|
||||
prompt_blocks,
|
||||
step_messages,
|
||||
last_draft,
|
||||
last_tool_response,
|
||||
user_msg,
|
||||
assistant_msg,
|
||||
)
|
||||
yield {
|
||||
"type": "complete",
|
||||
"run": updated_run.model_dump(mode="json"),
|
||||
}
|
||||
|
||||
def _prepare_run_message(
|
||||
self,
|
||||
project_id: str,
|
||||
run_id: str,
|
||||
trimmed: str,
|
||||
) -> tuple[StudioRun, StudioNode, StudioNodeRunState, str]:
|
||||
run = self.get_run(project_id, run_id)
|
||||
if run.status != StudioRunStatus.RUNNING:
|
||||
raise ValueError("运行未处于进行中,无法发送消息")
|
||||
|
||||
current_node_id = run.currentNodeId
|
||||
if not current_node_id:
|
||||
raise ValueError("当前运行无活动节点")
|
||||
|
||||
current_node = _find_node(run.pipelineSnapshot, current_node_id)
|
||||
if not current_node:
|
||||
raise ValueError(f"节点不存在:{current_node_id}")
|
||||
|
||||
if current_node.skillId != "studio.worldbook_entry":
|
||||
raise ValueError(
|
||||
f"当前步骤「{current_node.displayName}」不支持对话消息"
|
||||
)
|
||||
|
||||
current_state = next(
|
||||
(s for s in run.nodeStates if s.nodeId == current_node_id), None
|
||||
)
|
||||
if not current_state or current_state.status != "active":
|
||||
raise ValueError("当前节点不可执行")
|
||||
|
||||
return run, current_node, current_state, current_node_id
|
||||
|
||||
def _persist_run_message_turn(
|
||||
self,
|
||||
project_id: str,
|
||||
run_id: str,
|
||||
run: StudioRun,
|
||||
current_node_id: str,
|
||||
prompt_blocks: list,
|
||||
step_messages: list,
|
||||
last_draft: Dict[str, Any],
|
||||
last_tool_response,
|
||||
user_msg,
|
||||
assistant_msg,
|
||||
) -> StudioRun:
|
||||
step_messages = list(step_messages)
|
||||
step_messages.extend([user_msg, assistant_msg])
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
new_node_states: list[StudioNodeRunState] = []
|
||||
for state in run.nodeStates:
|
||||
updated = state.model_copy()
|
||||
if state.nodeId == current_node_id:
|
||||
updated.lastDraft = last_draft
|
||||
updated.lastToolResponse = last_tool_response
|
||||
updated.stepMessages = step_messages
|
||||
new_node_states.append(updated)
|
||||
|
||||
updated_run = run.model_copy(
|
||||
update={
|
||||
"nodeStates": new_node_states,
|
||||
"lastPromptBlocks": prompt_blocks,
|
||||
"updatedAt": now,
|
||||
}
|
||||
)
|
||||
updated_run = store_context_on_run(updated_run, current_node_id)
|
||||
self._save_run(project_id, run_id, updated_run)
|
||||
return updated_run
|
||||
|
||||
def delete_run(self, project_id: str, run_id: str) -> None:
|
||||
run_dir = self._run_dir(project_id, run_id)
|
||||
if not run_dir.exists():
|
||||
|
||||
Reference in New Issue
Block a user