优化全能创作
This commit is contained in:
+205
-42
@@ -22,7 +22,17 @@ from apps.products.models import Product
|
||||
|
||||
from .generation_errors import classify_generation_error, public_error_for_task
|
||||
from .creation import append_message, sync_generating_messages
|
||||
from .creation_agent import apply_confirm_params, apply_session_params, stream_creation_agent, submit_confirmed_image, submit_confirmed_video, _sse, _message_payload
|
||||
from .creation_agent import (
|
||||
_ASSET_CARD_LABELS,
|
||||
_message_payload,
|
||||
_sse,
|
||||
apply_confirm_params,
|
||||
apply_session_params,
|
||||
is_greeting,
|
||||
stream_creation_agent,
|
||||
submit_confirmed_image,
|
||||
submit_confirmed_video,
|
||||
)
|
||||
from .mentions import TYPE_LABELS, VALID_TYPES, refs_from_elicit_answers, search_mentions
|
||||
from .models import AITask, CreationConversation, CreationMessage, ImageConversation, ModelConfig
|
||||
from .serializers import (
|
||||
@@ -105,6 +115,14 @@ def _chat_answer_for_field(field: dict, text: str, team=None):
|
||||
return raw
|
||||
|
||||
|
||||
_WANTS_CARD_RE = re.compile(
|
||||
r"(发|打开|展示|看看|看下|给我|发我|发来).{0,10}(卡片|列表|商品库|素材库|库|选项)?"
|
||||
r"|(卡片|列表).{0,10}(选|选择|看看|看下)?"
|
||||
r"|^(需要|要|需要的|要的|可以|行|好|好的|发|发我|发来|发给我|发出来|打开|打开列表|看下|看下列表|发列表|选商品|选素材|你发吧|好的发吧|发吧|发给我选)[吧呀呢啊!!.。 ]*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _pending_chat_question(conversation: CreationConversation) -> CreationMessage | None:
|
||||
"""找最近一条未回答的新式追问;卡片既可点选,也兼容直接输入。"""
|
||||
candidates = conversation.messages.filter(
|
||||
@@ -112,7 +130,7 @@ def _pending_chat_question(conversation: CreationConversation) -> CreationMessag
|
||||
).order_by("-seq")[:20]
|
||||
for message in candidates:
|
||||
payload = message.payload or {}
|
||||
if payload.get("interaction") in {"chat", "asset_picker"} and not payload.get("submitted"):
|
||||
if not payload.get("submitted"):
|
||||
return message
|
||||
return None
|
||||
|
||||
@@ -1465,41 +1483,159 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
if not isinstance(refs, list):
|
||||
return JsonResponse({"detail": "refs 必须是数组"}, status=400)
|
||||
|
||||
if kind == "text" and text:
|
||||
# 打招呼不应被误当成上一条素材追问的答案;让 agent 立刻简短回应即可。
|
||||
if kind == "text" and text and not is_greeting(text):
|
||||
pending = _pending_chat_question(conversation)
|
||||
if pending is not None:
|
||||
payload = dict(pending.payload or {})
|
||||
fields = [item for item in (payload.get("fields") or []) if isinstance(item, dict)]
|
||||
if fields:
|
||||
field = fields[0]
|
||||
answers = {
|
||||
str(field.get("key") or "answer"):
|
||||
_chat_answer_for_field(field, text, conversation.team)
|
||||
}
|
||||
payload["answers"] = answers
|
||||
payload["submitted"] = True
|
||||
payload["answered_via"] = "chat"
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
if payload.get("phase") == "gate":
|
||||
pending_fields = [item for item in (payload.get("pending_fields") or []) if isinstance(item, dict)]
|
||||
primary_field = pending_fields[0] if pending_fields else {}
|
||||
asset_types = [t for t in (primary_field.get("asset_types") or []) if t in TYPE_LABELS] or ["product"]
|
||||
|
||||
params_changed = apply_session_params(conversation, fields, answers)
|
||||
existing = {
|
||||
(item.get("type"), str(item.get("id")))
|
||||
for item in refs if isinstance(item, dict)
|
||||
}
|
||||
for extra in refs_from_elicit_answers(conversation.team, fields, answers):
|
||||
mark = (extra.get("type"), str(extra.get("id")))
|
||||
if mark not in existing:
|
||||
refs.append(extra)
|
||||
existing.add(mark)
|
||||
# 1. 用户明确在打字说「发列表/发给我/发卡片/打开列表」
|
||||
if _WANTS_CARD_RE.search(text):
|
||||
payload["answers"] = {"_asset_gate": "send"}
|
||||
payload["submitted"] = True
|
||||
payload["answered_via"] = "chat"
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
|
||||
force_creative_turn = True
|
||||
continuation_instruction = (
|
||||
"用户刚用输入框回答了你上一句问题。直接基于这条真实回答继续原任务;"
|
||||
"不要复述答案,不要回复收到,也不要问要不要继续或要不要生成。"
|
||||
)
|
||||
if params_changed:
|
||||
continuation_instruction += " 会话参数已更新,旧方案作废,按新参数重新产出。"
|
||||
user_msg = append_message(conversation, role="user", text=text)
|
||||
pick_field = dict(primary_field)
|
||||
pick_field["label"] = _ASSET_CARD_LABELS.get(
|
||||
asset_types[0], _ASSET_CARD_LABELS.get("asset", "请选择素材")
|
||||
)
|
||||
pick = append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.ELICIT,
|
||||
text=pick_field["label"],
|
||||
payload={
|
||||
"interaction": "asset_picker",
|
||||
"phase": "pick",
|
||||
"fields": [pick_field],
|
||||
"submitted": False,
|
||||
"answers": {},
|
||||
},
|
||||
)
|
||||
|
||||
def _gate_pick_stream():
|
||||
yield _sse({"type": "message", "message": _message_payload(user_msg)})
|
||||
yield _sse({"type": "message", "message": _message_payload(pick)})
|
||||
yield _sse({"type": "done"})
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
_gate_pick_stream(), content_type="text/event-stream"
|
||||
)
|
||||
response["Cache-Control"] = "no-cache"
|
||||
response["X-Accel-Buffering"] = "no"
|
||||
return response
|
||||
|
||||
# 2. 用户说「你帮我选/你定/随便」
|
||||
elif re.search(r"(你来定|你定|你帮我定|你帮我选|帮我挑|没想好|随便|都行|你挑)", text):
|
||||
hits = search_mentions(conversation.team, q="", types=asset_types, limit=1)
|
||||
chosen_name = hits[0]["name"] if hits else "推荐商品"
|
||||
if hits:
|
||||
mark = (hits[0].get("type"), str(hits[0].get("id")))
|
||||
existing = {(item.get("type"), str(item.get("id"))) for item in refs if isinstance(item, dict)}
|
||||
if mark not in existing:
|
||||
refs.append(hits[0])
|
||||
payload["answers"] = {"_asset_gate": "auto", str(primary_field.get("key") or "product"): chosen_name}
|
||||
payload["submitted"] = True
|
||||
payload["answered_via"] = "chat"
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
|
||||
force_creative_turn = True
|
||||
if hits:
|
||||
continuation_instruction = (
|
||||
f"用户让你帮忙挑选,系统已为你选定【{hits[0]['name']}】。直接基于该素材推进创作方案,不要复述选项,不要重复追问。"
|
||||
)
|
||||
else:
|
||||
continuation_instruction = (
|
||||
"用户让你帮忙定素材,目前素材库暂无已上传素材。请结合所选预设与合理电商默认设想一款匹配的商品继续推进创作方案,不要重复追问。"
|
||||
)
|
||||
|
||||
# 3. 用户说「先不用/不选了/跳过」
|
||||
elif re.search(r"(先不用|不用|不选|先不选|跳过|暂不|没有商品|不需要)", text):
|
||||
payload["answers"] = {"_asset_gate": "skip"}
|
||||
payload["submitted"] = True
|
||||
payload["answered_via"] = "chat"
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
|
||||
force_creative_turn = True
|
||||
continuation_instruction = (
|
||||
"用户刚选择暂不添加这项素材。接受这个选择,按会话里已有的需求和合理默认继续原任务。"
|
||||
"先用一句自然的话承接,随后直接推进创作;不要再次追问同一素材,不要只说收到或有需要再说。"
|
||||
)
|
||||
|
||||
# 4. 用户直接输入了商品名或其它内容
|
||||
else:
|
||||
hits = search_mentions(conversation.team, q="", types=asset_types, limit=50)
|
||||
text_norm = _normalise_chat_choice(text)
|
||||
matched_hit = None
|
||||
for hit in hits:
|
||||
hname = _normalise_chat_choice(hit.get("name") or "")
|
||||
if hname and (hname in text_norm or text_norm in hname):
|
||||
matched_hit = hit
|
||||
break
|
||||
|
||||
if matched_hit:
|
||||
mark = (matched_hit.get("type"), str(matched_hit.get("id")))
|
||||
existing = {(item.get("type"), str(item.get("id"))) for item in refs if isinstance(item, dict)}
|
||||
if mark not in existing:
|
||||
refs.append(matched_hit)
|
||||
chosen_name = matched_hit["name"]
|
||||
continuation_instruction = (
|
||||
f"用户已指定使用素材【{chosen_name}】。直接基于该素材推进创作方案,不要复述选项,不要重复追问。"
|
||||
)
|
||||
else:
|
||||
chosen_name = text
|
||||
continuation_instruction = (
|
||||
f"用户已指定推广内容为「{text}」。直接围绕该内容推进创作方案,不要重复追问。"
|
||||
)
|
||||
|
||||
payload["answers"] = {"_asset_gate": chosen_name, str(primary_field.get("key") or "product"): chosen_name}
|
||||
payload["submitted"] = True
|
||||
payload["answered_via"] = "chat"
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
force_creative_turn = True
|
||||
|
||||
else:
|
||||
fields = [item for item in (payload.get("fields") or []) if isinstance(item, dict)]
|
||||
if fields:
|
||||
field = fields[0]
|
||||
answers = {
|
||||
str(field.get("key") or "answer"):
|
||||
_chat_answer_for_field(field, text, conversation.team)
|
||||
}
|
||||
payload["answers"] = answers
|
||||
payload["submitted"] = True
|
||||
payload["answered_via"] = "chat"
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
|
||||
params_changed = apply_session_params(conversation, fields, answers)
|
||||
existing = {
|
||||
(item.get("type"), str(item.get("id")))
|
||||
for item in refs if isinstance(item, dict)
|
||||
}
|
||||
for extra in refs_from_elicit_answers(conversation.team, fields, answers):
|
||||
mark = (extra.get("type"), str(extra.get("id")))
|
||||
if mark not in existing:
|
||||
refs.append(extra)
|
||||
existing.add(mark)
|
||||
|
||||
force_creative_turn = True
|
||||
continuation_instruction = (
|
||||
"用户刚用输入框回答了你上一句问题。直接基于这条真实回答继续原任务;"
|
||||
"不要复述答案,不要回复收到,也不要问要不要继续或要不要生成。"
|
||||
)
|
||||
if params_changed:
|
||||
continuation_instruction += " 会话参数已更新,旧方案作废,按新参数重新产出。"
|
||||
|
||||
if kind == "confirm":
|
||||
reply_to = str(request.data.get("reply_to") or "").strip()
|
||||
@@ -1575,13 +1711,21 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
choice = str(answers.get("_asset_gate") or "").strip()
|
||||
if choice == "send":
|
||||
pending = payload.get("pending_fields") or []
|
||||
primary_field = pending[0] if pending else {}
|
||||
types = [t for t in (primary_field.get("asset_types") or []) if t in TYPE_LABELS] or ["product"]
|
||||
pick_field = dict(primary_field)
|
||||
pick_field["label"] = _ASSET_CARD_LABELS.get(
|
||||
types[0], _ASSET_CARD_LABELS.get("asset", "请选择素材")
|
||||
)
|
||||
pick = append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.ELICIT,
|
||||
text=pick_field["label"],
|
||||
payload={
|
||||
"interaction": "asset_picker",
|
||||
"phase": "pick",
|
||||
"fields": pending,
|
||||
"fields": [pick_field],
|
||||
"submitted": False,
|
||||
"answers": {},
|
||||
},
|
||||
@@ -1597,15 +1741,34 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
response["Cache-Control"] = "no-cache"
|
||||
response["X-Accel-Buffering"] = "no"
|
||||
return response
|
||||
# 卡片本身已经记录了用户的选择。不要再伪造一条黑色用户气泡;
|
||||
# 继续原任务,并明确告诉模型不要再次追问同一项素材。
|
||||
text = ""
|
||||
record_user_message = False
|
||||
force_creative_turn = True
|
||||
continuation_instruction = (
|
||||
"用户刚选择暂不添加这项素材。接受这个选择,按会话里已有的需求和合理默认继续原任务。"
|
||||
"先用一句自然的话承接,随后直接推进创作;不要再次追问同一素材,不要只说收到或有需要再说。"
|
||||
)
|
||||
elif choice == "auto":
|
||||
pending = payload.get("pending_fields") or []
|
||||
primary_field = pending[0] if pending else {}
|
||||
types = [item for item in (primary_field.get("asset_types") or []) if item in TYPE_LABELS] or ["product"]
|
||||
hits = search_mentions(conversation.team, q="", types=types, limit=1)
|
||||
if hits:
|
||||
refs = list(refs)
|
||||
refs.append(hits[0])
|
||||
continuation_instruction = (
|
||||
f"用户希望你帮忙挑选素材,已为你选定【{hits[0]['name']}】。直接基于该素材推进创作方案,不要复述选项,不要重复追问。"
|
||||
)
|
||||
else:
|
||||
continuation_instruction = (
|
||||
"用户希望你帮忙定素材,目前素材库暂无已上传素材。请结合所选预设与合理电商默认设想一款匹配的商品继续推进创作方案,不要重复追问。"
|
||||
)
|
||||
text = ""
|
||||
record_user_message = False
|
||||
force_creative_turn = True
|
||||
else:
|
||||
# 卡片本身已经记录了用户的选择。不要再伪造一条黑色用户气泡;
|
||||
# 继续原任务,并明确告诉模型不要再次追问同一项素材。
|
||||
text = ""
|
||||
record_user_message = False
|
||||
force_creative_turn = True
|
||||
continuation_instruction = (
|
||||
"用户刚选择暂不添加这项素材。接受这个选择,按会话里已有的需求和合理默认继续原任务。"
|
||||
"先用一句自然的话承接,随后直接推进创作;不要再次追问同一素材,不要只说收到或有需要再说。"
|
||||
)
|
||||
else:
|
||||
params_changed = apply_session_params(conversation, payload.get("fields") or [], answers)
|
||||
# 点选商品/角色必须钉成 Ref:模型常把选项做成单选文字,前端只回 answers。
|
||||
|
||||
Reference in New Issue
Block a user