49c0e5559c
Co-Authored-By: Claude Code <noreply@anthropic.com>
99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
import json
|
|
import logging
|
|
from collections.abc import AsyncGenerator
|
|
from pathlib import Path
|
|
|
|
from app.providers.base import AIProvider
|
|
from app.providers.glm import GLMProvider
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"
|
|
|
|
|
|
def _load_prompt(filename: str) -> str:
|
|
return (PROMPTS_DIR / filename).read_text(encoding="utf-8")
|
|
|
|
|
|
def create_provider() -> AIProvider:
|
|
if settings.AI_PROVIDER == "glm":
|
|
return GLMProvider(
|
|
api_key=settings.ZHIPU_API_KEY,
|
|
model=settings.AI_MODEL,
|
|
base_url=settings.ZHIPU_BASE_URL,
|
|
)
|
|
raise ValueError(f"Unknown AI provider: {settings.AI_PROVIDER}")
|
|
|
|
|
|
SYSTEM_PROMPT = _load_prompt("system.md")
|
|
COMPANY_FACTS = _load_prompt("company_facts.md") # 公司事实卡(知识与行为分离)
|
|
EXTRACTOR_PROMPT = _load_prompt("extractor.md")
|
|
EVALUATOR_PROMPT = _load_prompt("evaluator.md")
|
|
|
|
# 对话基座: 行为 prompt 在前(红线显著性),事实卡居中(参考资料),
|
|
# 当轮 context 提醒最后拼接(在 chat/stream_chat 内,保持最新鲜)
|
|
_BASE_CHAT_PROMPT = SYSTEM_PROMPT + "\n\n" + COMPANY_FACTS
|
|
|
|
|
|
class AIService:
|
|
def __init__(self, provider: AIProvider | None = None):
|
|
self.provider = provider or create_provider()
|
|
|
|
async def chat(self, messages: list[dict], context: str = "") -> str:
|
|
"""Main conversation: get AI reply to user message."""
|
|
prompt = _BASE_CHAT_PROMPT + ("\n\n" + context if context else "")
|
|
return await self.provider.chat(messages, prompt)
|
|
|
|
async def stream_chat(self, messages: list[dict], context: str = "") -> AsyncGenerator[str, None]:
|
|
"""Stream AI reply token by token."""
|
|
prompt = _BASE_CHAT_PROMPT + ("\n\n" + context if context else "")
|
|
async for token in self.provider.stream_chat(messages, prompt):
|
|
yield token
|
|
|
|
async def extract_info(
|
|
self, messages: list[dict], existing_info: dict
|
|
) -> dict:
|
|
"""Extract structured info from conversation history."""
|
|
prompt = EXTRACTOR_PROMPT.replace(
|
|
"$existing_info", json.dumps(existing_info or {}, ensure_ascii=False)
|
|
).replace("$messages", self._format_messages(messages))
|
|
_, extracted = await self.provider.chat_with_extraction(
|
|
messages=[], system_prompt=prompt
|
|
)
|
|
return extracted
|
|
|
|
async def evaluate(
|
|
self, profile: dict, recent_messages: list[dict]
|
|
) -> dict:
|
|
"""Evaluate customer intent level."""
|
|
prompt = EVALUATOR_PROMPT.replace(
|
|
"$profile", json.dumps(profile, ensure_ascii=False)
|
|
).replace("$recent_messages", self._format_messages(recent_messages[-6:]))
|
|
_, result = await self.provider.chat_with_extraction(
|
|
messages=[], system_prompt=prompt
|
|
)
|
|
return self._normalize_evaluation(result)
|
|
|
|
@staticmethod
|
|
def _format_messages(messages: list[dict]) -> str:
|
|
lines = []
|
|
for m in messages:
|
|
role = m.get("role", "unknown")
|
|
content = m.get("content", "")
|
|
label = {"user": "客户", "assistant": "顾问"}.get(role, role)
|
|
lines.append(f"{label}: {content}")
|
|
return "\n".join(lines)
|
|
|
|
@staticmethod
|
|
def _normalize_evaluation(result: dict) -> dict:
|
|
intent = result.get("intent_level", "pending")
|
|
if intent not in ("interested", "not_interested", "pending"):
|
|
intent = "pending"
|
|
return {
|
|
"intent_level": intent,
|
|
"reasoning": result.get("reasoning", ""),
|
|
"contact_info": result.get("contact_info", {}),
|
|
"info_sufficient": result.get("info_sufficient", False),
|
|
}
|