feat: jc 引擎主线搬入 app/engine(315 测试基线全绿:320 去掉 admin 5 用例)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
|
||||
from app.engine.providers.base import AIProvider
|
||||
from app.engine.providers.glm import GLMProvider
|
||||
from app.engine.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),
|
||||
}
|
||||
Reference in New Issue
Block a user