优化全能创作

This commit is contained in:
Azmat@qq.com
2026-09-11 10:47:19 +08:00
parent 43f80a4d90
commit dbcac8a44b
6 changed files with 560 additions and 143 deletions
+127 -30
View File
@@ -184,6 +184,22 @@ _CHITCHAT_RE = re.compile(
)
_GREETING_RE = re.compile(
r"^\s*("
r"hi|hello|hey|yo|hola|"
r"你好呀?|您好|嗨|哈喽|嘿|"
r"在吗|在不在|有人吗|"
r"早+|早安|早上好|午安|晚安"
r")[\s!.。~~??…]*$",
re.IGNORECASE,
)
def is_greeting(user_text: str) -> bool:
"""单纯打招呼要立刻回应,不能为了分析引用素材去等模型。"""
return bool(_GREETING_RE.match((user_text or "").strip()))
def is_pure_chitchat(user_text: str) -> bool:
"""纯打招呼 / 应答,没有创作意图。这类消息绝不能触发 write_strategy / write_plan。"""
text = (user_text or "").strip()
@@ -268,7 +284,7 @@ def session_has_creative_context(conversation: CreationConversation) -> bool:
def wanted_asset_pick(user_text: str, refs: list | None) -> str | None:
"""用户说「改商品」却没点名时,直接发对应素材选择卡"""
"""用户说「改商品」却没点名时,启动对应素材的自然确认"""
if refs:
return None
text = user_text or ""
@@ -464,8 +480,7 @@ def tool_schemas(context: AgentContext, *, allow_plan: bool = True) -> list[dict
"缺少必要信息时向用户追问。"
"只在信息**确实缺失且无法合理推断**时用;能自己定的就自己定,别把用户当填表机器。"
"用户要选/改/换商品、角色、模特、场景时**必须**调这个工具,"
"type 用 asset 并填 asset_types,系统会直接显示可视化素材卡让用户点选"
"不要先问用户是否需要卡片或列表。其他类型才显示为普通聊天问题。"
"type 用 asset 并填 asset_types系统会以类Agent自然对话轻量询问是否需要发送商品列表(同时支持直接输入名字或由你推荐),避免一上来粗暴弹出大卡片打断交流"
"一次只问 1 项,禁止问「要不要继续」「要不要生成」「是否开始创作」这类流程问题。"
),
"parameters": {
@@ -1044,7 +1059,9 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
"- 不要逐字复述用户刚说过的话。需要承接时,只说你的判断或下一步,例如「这个方向能做,我先把反转落在商品登场上。」",
"- 一次只推进一步,但不要把能直接做的事停在寒暄、确认或客套话上。",
"- 缺信息时一次只问一个真正影响结果的问题,不要把对话做成问卷。",
"- 商品、模特、角色、场景这类有库内素材可选的内容,直接发可视化选择卡让用户点;不要先问「要不要发列表」",
"- 缺少商品、模特、角色、场景等素材时:**绝不要直接弹出大块素材选择卡**打断对话。像懂创作、懂电商的专业伙伴一样先自然询问用户想推什么商品/用哪个角色,询问是否需要把商品列表发给他选,同时说明也可以直接输入商品名或由你推荐",
"- 只有当用户明确说要发列表(如「发给我」「发列表」「给我看看」「我来选」)时,才展示可视化卡片。",
"- 用户直接打字输入商品名时,直接采纳该商品并继续推进创作方案,不要强迫用户去卡片里点选。",
"- 调性、受众、文案、时长等其他信息才用聊天追问,问题末尾顺手告诉用户下一句怎么回。",
"- 用户刚回答过你的问题时,直接沿着答案继续;不要复述答案,也不要额外回一句「收到」。",
"- 用户选择暂不提供某项素材时,把它当成明确授权:按已有信息和合理默认继续。除非任务客观上无法完成,否则不要再次追问同一素材。",
@@ -1058,10 +1075,10 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
"",
"【什么时候反问】",
"- 只有信息**确实缺失且无法合理推断**时才调用 ask_user;能自己定的就自己定。",
"- ask_user 一次只问一件事。type=asset 会显示素材选择卡;其他类型显示普通聊天问题,让用户直接输入。",
"- ask_user 一次只问一件事。type=asset 会以类Agent自然追问轻量询问是否发送列表或由你推荐;其他类型显示普通聊天问题,让用户直接输入。",
"- 商品是谁、给谁看、什么调性 —— 这些缺了会直接影响成片,值得问。",
"- 让用户选商品/角色/模特/场景时,ask_user 必须用 type=asset 并填 asset_types。",
"- 用户说「改商品」「换角色」却没点名是哪个:立刻 ask_user 发对应素材卡,不要再用文字追问名称",
"- 用户说「改商品」「换角色」却没点名是哪个:立刻 ask_user 启动素材确认,不要强行出大卡片",
"- 用户说改时长/模型/比例/分辨率但没给新值:立刻 ask_user,type=single 提供可识别的候选值;聊天里只显示问题,用户直接输入。回答后旧方案作废,必须按新参数重新 write_plan。",
"- 光线、构图、镜头这些专业判断是你的活,不要反过来问用户。",
]
@@ -1181,8 +1198,12 @@ def build_messages(context: AgentContext, *, allow_plan: bool = True, has_contex
choice = str(answers.get("_asset_gate") or "")
if choice == "skip":
answer_text = "(用户选择暂不添加这项素材,要求按现有信息继续原任务)"
else:
elif choice == "auto":
answer_text = "(用户希望你帮忙挑选/决定素材,按合理推荐继续推进)"
elif choice == "send":
answer_text = "(用户希望打开素材列表继续选择)"
else:
answer_text = f"(用户指定了素材:{choice})"
else:
joined = "".join(
f"{field_by_key.get(str(key), {}).get('label') or key}"
@@ -1252,12 +1273,15 @@ def stream_creation_agent(
) -> Iterator[str]:
"""一条用户消息 → SSE 流。生成器,由 StreamingHttpResponse 逐帧下发。"""
refs = refs or []
model_config = get_creation_chat_model(model_config)
if model_config is None:
yield _sse({"type": "error", "detail": "没有可用的文本模型,请先在模型库配置"})
return
fast_greeting = record_user_message and is_greeting(text)
# 打招呼不依赖模型,模型未配置也能即时回应;其他消息保持原有的先校验模型行为。
if not fast_greeting:
model_config = get_creation_chat_model(model_config)
if model_config is None:
yield _sse({"type": "error", "detail": "没有可用的文本模型,请先在模型库配置"})
return
context = AgentContext(conversation=conversation, user=user, model_config=model_config)
context = AgentContext(conversation=conversation, user=user, model_config=model_config)
try:
user_message = None
with transaction.atomic():
@@ -1266,8 +1290,23 @@ def stream_creation_agent(
user_message = append_message(conversation, role="user", text=text, refs=refs)
if user_message is not None:
yield _sse({"type": "message", "message": _message_payload(user_message)})
# 空会话里的纯打招呼:短回一句就够。已有商品/方向时走模型,
# 针对用户实际说的话自然回应,但不给方案工具
# 单纯打招呼无论有没有参考素材、有没有既有上下文,都不值得等模型
# 素材先收下,等用户说清用途再分析,避免「你好」卡住还冒出一大段建议
if fast_greeting:
reply = append_message(
conversation,
role="assistant",
text=(
"你好,素材我收到了。想拿它做什么,直接说就行。"
if refs else "你好。想做什么,直接说就行。"
),
)
yield _sse({"type": "message", "message": _message_payload(reply)})
yield _sse({"type": "done"})
return
# 空会话里的简短应答:短回一句就够。已有商品/方向时走模型,
# 针对用户实际说的话自然回应,但不给方案工具。
has_context = session_has_creative_context(conversation)
if record_user_message and is_pure_chitchat(text) and not refs and not has_context:
reply = append_message(
@@ -1330,22 +1369,38 @@ def stream_creation_agent(
said = "".join(text_buffer).strip()
calls = [tool_buffer[i] for i in sorted(tool_buffer) if tool_buffer[i].get("name")]
fallback_fields = None
allow_pick = False
if not calls:
pick = (
wanted_asset_pick(text, refs)
or requested_asset_card_from_context(conversation, text)
)
requested_card = requested_asset_card_from_context(conversation, text)
wanted_pick = wanted_asset_pick(text, refs)
pick = requested_card or wanted_pick
param_keys = wanted_param_keys(text, is_video=context.is_video)
if pick:
fallback_fields = [{
"key": pick,
"label": _ASSET_PICK_LABEL[pick],
"label": _ASSET_PICK_LABEL.get(pick, _ASSET_CARD_LABELS.get(pick, "请选择素材")),
"type": "asset",
"required": True,
"asset_types": [pick],
}]
allow_pick = bool(requested_card)
elif param_keys:
fallback_fields = session_param_fields(param_keys, context.is_video)
elif allow_plan and not said:
# 部分推理模型会只给 reasoning_content 后结束本轮。不能让用户
# 只看到自己那条消息;没有商品上下文时,收敛成一条自然追问。
has_product = any(
isinstance(ref, dict) and ref.get("type") == "product"
for ref in (conversation.pinned_refs or [])
)
if not has_product:
fallback_fields = [{
"key": "product",
"label": "这条想推哪款商品?",
"type": "asset",
"required": True,
"asset_types": ["product"],
}]
# ask_user 自己会落一条可追踪的聊天问题。模型同时吐出的过渡文案不再
# 另存一条,否则界面会连续出现两遍几乎相同的问题。
@@ -1356,7 +1411,9 @@ def stream_creation_agent(
if not calls:
if fallback_fields:
result, _stop = _dispatch_tool(context, "ask_user", {"fields": fallback_fields})
result, _stop = _dispatch_tool(
context, "ask_user", {"fields": fallback_fields}, allow_pick=allow_pick
)
for event in result.get("_events", []):
yield _sse(event)
break
@@ -1422,11 +1479,19 @@ _ASSET_CARD_LABELS = {
"asset": "这一步要使用哪项素材?",
}
_GATE_LABELS = {
"product": "这条想推哪款商品?可以直接告诉我商品名,或者需要我把商品库列表发给你选吗?",
"character": "这条想用哪个角色?可以直接告诉我角色名,或者需要我发角色列表给你选吗?",
"model": "这条想用哪位模特?可以直接告诉我,或者需要我把模特库发给你选吗?",
"scene": "这条想放在哪个场景里?可以直接告诉我,或者需要我把场景库发给你选吗?",
"asset": "这一步想用哪项素材?可以直接告诉我,或者需要我把素材列表发给你选吗?",
}
def _guided_elicit_text(field: dict) -> str:
"""追问既要问清一件事,也要让用户知道下一句怎么回。"""
label = str(field.get("label") or "这项你想怎么定?").strip()
if field.get("type") == "asset":
if field.get("type") == "asset" or field.get("key") == "_asset_gate":
return label
if str(field.get("key") or "") in SESSION_PARAM_KEYS:
return label
@@ -1434,16 +1499,46 @@ def _guided_elicit_text(field: dict) -> str:
def _elicit_payload_for_fields(fields: list[dict]) -> dict:
"""素材问题直接出选择卡;其他问题继续走普通聊天。"""
def _elicit_payload_for_fields(
fields: list[dict], *, allow_pick: bool = False, is_video: bool = True
) -> dict:
"""素材问题默认出轻量对话闸门;用户明确要求发列表时才出 pick 选择卡。"""
field = dict(fields[0])
if field.get("type") == "asset":
asset_types = [item for item in (field.get("asset_types") or []) if item in _ASSET_CARD_LABELS]
field["label"] = _ASSET_CARD_LABELS[asset_types[0]] if len(asset_types) == 1 else _ASSET_CARD_LABELS["asset"]
asset_types = [item for item in (field.get("asset_types") or []) if item in _GATE_LABELS]
primary_type = asset_types[0] if asset_types else "product"
if allow_pick:
field["label"] = _ASSET_CARD_LABELS.get(primary_type, _ASSET_CARD_LABELS["asset"])
return {
"interaction": "asset_picker",
"phase": "pick",
"fields": [field],
"submitted": False,
"answers": {},
}
if primary_type == "product":
gate_label = (
"这条视频想推哪款商品?可以直接告诉我商品名,或者需要我把商品库列表发给你选吗?"
if is_video else
"这次想做哪款商品?可以直接告诉我商品名,或者需要我把商品库列表发给你选吗?"
)
else:
gate_label = _GATE_LABELS.get(primary_type, _GATE_LABELS["asset"])
gate_field = {
"key": "_asset_gate",
"label": gate_label,
"type": "text",
"required": False,
}
return {
"interaction": "asset_picker",
"phase": "pick",
"fields": [field],
# 这是 Agent 的一句自然追问,不是让用户点选流程的卡片。
# 只有用户明确要求发列表后,才会生成上面 allow_pick 分支的商品卡。
"interaction": "chat",
"phase": "gate",
"fields": [gate_field],
"pending_fields": [field],
"submitted": False,
"answers": {},
}
@@ -1455,7 +1550,9 @@ def _elicit_payload_for_fields(fields: list[dict]) -> dict:
}
def _dispatch_tool(context: AgentContext, name: str, args: dict) -> tuple[dict, bool]:
def _dispatch_tool(
context: AgentContext, name: str, args: dict, *, allow_pick: bool = False
) -> tuple[dict, bool]:
"""执行一个工具。返回 (结果, 是否中断循环)。
结果里的 `_events` 会原样转发给前端,`payload` 回喂给模型。
@@ -1464,7 +1561,7 @@ def _dispatch_tool(context: AgentContext, name: str, args: dict) -> tuple[dict,
fields = _coerce_fields(args.get("fields"))
if not fields:
return {"payload": {"error": "fields 不合法,请重新组织问题"}}, False
payload = _elicit_payload_for_fields(fields)
payload = _elicit_payload_for_fields(fields, allow_pick=allow_pick, is_video=context.is_video)
display_field = (payload.get("fields") or fields)[0]
message = append_message(
context.conversation, role="assistant",