dieya_risk_prediction_backend/app/services/conversation_prompts.py

132 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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