132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
"""Select file-backed conversation prompts from semantic routing results."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Sequence
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import TYPE_CHECKING, Any
|
||
|
||
from app.core.exceptions import AppException
|
||
|
||
if TYPE_CHECKING:
|
||
from app.models.entities import ConversationMessage
|
||
|
||
|
||
PROMPT_DIRECTORY = Path(__file__).resolve().parents[1] / "prompts"
|
||
DEFAULT_PROMPT_KEY = "product_manager"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class PromptDefinition:
|
||
name: str
|
||
filename: str
|
||
|
||
|
||
PROMPT_CATALOG = {
|
||
DEFAULT_PROMPT_KEY: PromptDefinition("开品产品经理", "开品产品经理-美妆护肤-角色Prompt.md"),
|
||
"selection_insight": PromptDefinition("选品洞察", "01-选品洞察.md"),
|
||
"project_review": PromptDefinition("立项评审", "02-立项评审.md"),
|
||
"cost_pricing": PromptDefinition("成本定价", "03-成本定价.md"),
|
||
"compliance_risk": PromptDefinition("合规风控", "04-合规风控.md"),
|
||
"listing_selling_points": PromptDefinition("上架卖点", "05-上架卖点.md"),
|
||
}
|
||
SPECIALIST_PROMPT_KEYS = frozenset(PROMPT_CATALOG) - {DEFAULT_PROMPT_KEY}
|
||
|
||
|
||
def _read_prompt(relative_path: str) -> str:
|
||
"""Read only application-owned paths; model output never becomes a path."""
|
||
|
||
try:
|
||
content = (PROMPT_DIRECTORY / relative_path).read_text(encoding="utf-8-sig").strip()
|
||
except (OSError, UnicodeError) as exc:
|
||
raise AppException("会话 Prompt 文件无法读取,请检查部署文件", code=500, status_code=500) from exc
|
||
if not content:
|
||
raise AppException("会话 Prompt 文件不能为空", code=500, status_code=500)
|
||
return content
|
||
|
||
|
||
def semantic_system_prompt() -> str:
|
||
return _read_prompt("semantic_router.md")
|
||
|
||
|
||
def previous_prompt_key(messages: Sequence[ConversationMessage]) -> str:
|
||
"""Recover the last completed turn's route, including after a restart."""
|
||
|
||
for message in reversed(messages):
|
||
if message.role != "assistant":
|
||
continue
|
||
semantic = (message.structured_data or {}).get("semantic")
|
||
if isinstance(semantic, dict):
|
||
key = semantic.get("prompt_key")
|
||
if isinstance(key, str) and key in PROMPT_CATALOG:
|
||
return key
|
||
# A newer legacy turn must not revive a stale specialist from earlier turns.
|
||
return DEFAULT_PROMPT_KEY
|
||
return DEFAULT_PROMPT_KEY
|
||
|
||
|
||
def semantic_history(
|
||
history: Sequence[dict[str, str]],
|
||
messages: Sequence[ConversationMessage],
|
||
) -> list[dict[str, str]]:
|
||
return [
|
||
{
|
||
"role": "system",
|
||
"content": (
|
||
f"上一轮已使用的主题 prompt_key={previous_prompt_key(messages)}。"
|
||
"本条是应用路由元数据。用户补充材料或无新主题地继续追问时可选择 inherit;"
|
||
"用户明确提出新任务时必须重新选择主题。"
|
||
),
|
||
},
|
||
*history,
|
||
]
|
||
|
||
|
||
def resolve_prompt_selection(
|
||
semantics: dict[str, Any],
|
||
messages: Sequence[ConversationMessage],
|
||
) -> dict[str, Any]:
|
||
"""Resolve inheritance and reject unsupported keys before reading any file."""
|
||
|
||
result = dict(semantics)
|
||
requested = result.get("prompt_key")
|
||
if requested == "inherit":
|
||
key = previous_prompt_key(messages)
|
||
source = "inherited"
|
||
elif isinstance(requested, str) and requested in PROMPT_CATALOG:
|
||
key = requested
|
||
source = "semantic"
|
||
else:
|
||
key = DEFAULT_PROMPT_KEY
|
||
source = "fallback"
|
||
definition = PROMPT_CATALOG[key]
|
||
result.update(
|
||
prompt_key=key,
|
||
prompt_name=definition.name,
|
||
prompt_file=definition.filename,
|
||
prompt_route_source=source,
|
||
)
|
||
return result
|
||
|
||
|
||
def reply_system_messages(semantics: dict[str, Any]) -> list[dict[str, str]]:
|
||
requested = semantics.get("prompt_key")
|
||
key = requested if isinstance(requested, str) and requested in PROMPT_CATALOG else DEFAULT_PROMPT_KEY
|
||
base = PROMPT_CATALOG[DEFAULT_PROMPT_KEY]
|
||
messages = [
|
||
{"role": "system", "content": _read_prompt(f"product_development/{base.filename}")},
|
||
]
|
||
if key != DEFAULT_PROMPT_KEY:
|
||
expert = PROMPT_CATALOG[key]
|
||
messages.append(
|
||
{"role": "system", "content": _read_prompt(f"product_development/{expert.filename}")},
|
||
)
|
||
messages.append(
|
||
{
|
||
"role": "system",
|
||
"content": (f"本轮主任务:{PROMPT_CATALOG[key].name}。\n" + _read_prompt("conversation_runtime.md")),
|
||
},
|
||
)
|
||
return messages
|