1
This commit is contained in:
222
apps/backend/app/ai/apps/app1_chat.py
Normal file
222
apps/backend/app/ai/apps/app1_chat.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""应用 1:通用对话(SSE 流式)。
|
||||
|
||||
每次进入 chat 页面新建 ai_conversations 记录(不复用),
|
||||
首条消息注入页面上下文,流式返回 AI 回复。
|
||||
|
||||
app_id = "app1_chat"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import SSEEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app1_chat"
|
||||
|
||||
|
||||
async def chat_stream(
|
||||
*,
|
||||
message: str,
|
||||
user_id: int | str,
|
||||
nickname: str,
|
||||
role: str,
|
||||
site_id: int,
|
||||
source_page: str | None = None,
|
||||
page_context: dict | None = None,
|
||||
screen_content: str | None = None,
|
||||
bailian: BailianClient,
|
||||
conv_svc: ConversationService,
|
||||
) -> AsyncGenerator[SSEEvent, None]:
|
||||
"""流式对话入口,返回 SSEEvent 异步生成器。
|
||||
|
||||
流程:
|
||||
1. 创建 conversation 记录
|
||||
2. 写入 user message
|
||||
3. 构建 system prompt(注入页面上下文)
|
||||
4. 调用 bailian.chat_stream 流式获取回复
|
||||
5. 逐 chunk yield SSEEvent(type="chunk")
|
||||
6. 完成后写入 assistant message,yield SSEEvent(type="done")
|
||||
7. 异常时 yield SSEEvent(type="error")
|
||||
"""
|
||||
conversation_id: int | None = None
|
||||
|
||||
try:
|
||||
# 1. 每次新建 conversation(不复用)
|
||||
source_ctx = _build_source_context(
|
||||
source_page=source_page,
|
||||
page_context=page_context,
|
||||
screen_content=screen_content,
|
||||
)
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_page=source_page,
|
||||
source_context=source_ctx,
|
||||
)
|
||||
logger.info(
|
||||
"App1 新建对话: conversation_id=%s user_id=%s site_id=%s",
|
||||
conversation_id, user_id, site_id,
|
||||
)
|
||||
|
||||
# 2. 立即写入 user message
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=message,
|
||||
)
|
||||
|
||||
# 3. 构建消息列表(system prompt + user message)
|
||||
messages = _build_messages(
|
||||
message=message,
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
role=role,
|
||||
source_page=source_page,
|
||||
page_context=page_context,
|
||||
screen_content=screen_content,
|
||||
)
|
||||
|
||||
# 4-5. 流式调用百炼,逐 chunk yield
|
||||
full_reply_parts: list[str] = []
|
||||
async for chunk in bailian.chat_stream(messages):
|
||||
full_reply_parts.append(chunk)
|
||||
yield SSEEvent(type="chunk", content=chunk)
|
||||
|
||||
# 6. 流式完成,拼接完整回复并写入 assistant message
|
||||
full_reply = "".join(full_reply_parts)
|
||||
# 百炼流式模式不返回 tokens_used,按字符数估算(粗略)
|
||||
estimated_tokens = len(full_reply)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=full_reply,
|
||||
tokens_used=estimated_tokens,
|
||||
)
|
||||
|
||||
yield SSEEvent(
|
||||
type="done",
|
||||
conversation_id=conversation_id,
|
||||
tokens_used=estimated_tokens,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"App1 对话异常: conversation_id=%s error=%s",
|
||||
conversation_id, e,
|
||||
exc_info=True,
|
||||
)
|
||||
yield SSEEvent(type="error", message=str(e))
|
||||
|
||||
|
||||
def _build_messages(
|
||||
*,
|
||||
message: str,
|
||||
user_id: int | str,
|
||||
nickname: str,
|
||||
role: str,
|
||||
source_page: str | None,
|
||||
page_context: dict | None,
|
||||
screen_content: str | None,
|
||||
) -> list[dict]:
|
||||
"""构建发送给百炼的消息列表。
|
||||
|
||||
首条 system 消息注入页面上下文和用户信息。
|
||||
"""
|
||||
system_content = _build_system_prompt(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
role=role,
|
||||
source_page=source_page,
|
||||
page_context=page_context,
|
||||
screen_content=screen_content,
|
||||
)
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": message},
|
||||
]
|
||||
|
||||
|
||||
def _build_system_prompt(
|
||||
*,
|
||||
user_id: int | str,
|
||||
nickname: str,
|
||||
role: str,
|
||||
source_page: str | None,
|
||||
page_context: dict | None,
|
||||
screen_content: str | None,
|
||||
) -> dict:
|
||||
"""构建 system prompt JSON。
|
||||
|
||||
通过 biz_params.user_prompt_params 传入用户信息,
|
||||
注入页面上下文供 AI 理解当前场景。
|
||||
"""
|
||||
prompt: dict = {
|
||||
"task": "你是台球门店的 AI 助手,根据用户的问题和当前页面上下文提供帮助。",
|
||||
"biz_params": {
|
||||
"user_prompt_params": {
|
||||
"User_ID": str(user_id),
|
||||
"Role": role,
|
||||
"Nickname": nickname,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# 注入页面上下文(首条消息)
|
||||
page_ctx = _build_page_context(
|
||||
source_page=source_page,
|
||||
page_context=page_context,
|
||||
screen_content=screen_content,
|
||||
)
|
||||
if page_ctx:
|
||||
prompt["page_context"] = page_ctx
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def _build_page_context(
|
||||
*,
|
||||
source_page: str | None,
|
||||
page_context: dict | None,
|
||||
screen_content: str | None,
|
||||
) -> dict:
|
||||
"""构建页面上下文信息。
|
||||
|
||||
P5-A 阶段:直接透传前端传入的上下文字段。
|
||||
P5-B 阶段:各页面逐步实现文本化工具,丰富 screen_content。
|
||||
"""
|
||||
# TODO: P5-B 各页面文本化工具细化
|
||||
ctx: dict = {}
|
||||
if source_page:
|
||||
ctx["source_page"] = source_page
|
||||
if page_context:
|
||||
ctx["page_context"] = page_context
|
||||
if screen_content:
|
||||
ctx["screen_content"] = screen_content
|
||||
return ctx
|
||||
|
||||
|
||||
def _build_source_context(
|
||||
*,
|
||||
source_page: str | None,
|
||||
page_context: dict | None,
|
||||
screen_content: str | None,
|
||||
) -> dict | None:
|
||||
"""构建存入 ai_conversations.source_context 的 JSON。"""
|
||||
ctx: dict = {}
|
||||
if source_page:
|
||||
ctx["source_page"] = source_page
|
||||
if page_context:
|
||||
ctx["page_context"] = page_context
|
||||
if screen_content:
|
||||
ctx["screen_content"] = screen_content
|
||||
return ctx if ctx else None
|
||||
210
apps/backend/app/ai/apps/app2_finance.py
Normal file
210
apps/backend/app/ai/apps/app2_finance.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""应用 2:财务洞察。
|
||||
|
||||
8 个时间维度独立调用,每次调用结果写入 ai_cache,
|
||||
同时创建 ai_conversations + ai_messages 记录。
|
||||
|
||||
营业日分界点:每日 08:00(BUSINESS_DAY_START_HOUR 环境变量,默认 8)。
|
||||
|
||||
app_id = "app2_finance"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.prompts.app2_finance_prompt import build_prompt
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app2_finance"
|
||||
|
||||
# 8 个时间维度编码
|
||||
TIME_DIMENSIONS = (
|
||||
"this_month",
|
||||
"last_month",
|
||||
"this_week",
|
||||
"last_week",
|
||||
"last_3_months",
|
||||
"this_quarter",
|
||||
"last_quarter",
|
||||
"last_6_months",
|
||||
)
|
||||
|
||||
|
||||
def get_business_date() -> date:
|
||||
"""根据营业日分界点计算当前营业日。
|
||||
|
||||
分界点前(如 07:59)视为前一天营业日,
|
||||
分界点及之后(如 08:00)视为当天营业日。
|
||||
"""
|
||||
hour = int(os.environ.get("BUSINESS_DAY_START_HOUR", "8"))
|
||||
now = datetime.now()
|
||||
if now.hour < hour:
|
||||
return (now - timedelta(days=1)).date()
|
||||
return now.date()
|
||||
|
||||
|
||||
def compute_time_range(dimension: str, business_date: date) -> tuple[date, date]:
|
||||
"""计算时间维度对应的日期范围 [start, end](闭区间)。
|
||||
|
||||
Args:
|
||||
dimension: 时间维度编码
|
||||
business_date: 当前营业日
|
||||
|
||||
Returns:
|
||||
(start_date, end_date) 元组
|
||||
"""
|
||||
y, m, d = business_date.year, business_date.month, business_date.day
|
||||
|
||||
if dimension == "this_month":
|
||||
start = date(y, m, 1)
|
||||
return start, business_date
|
||||
|
||||
if dimension == "last_month":
|
||||
prev = _month_offset(y, m, -1)
|
||||
start = date(prev[0], prev[1], 1)
|
||||
end = date(y, m, 1) - timedelta(days=1)
|
||||
return start, end
|
||||
|
||||
if dimension == "this_week":
|
||||
# 周一起算
|
||||
weekday = business_date.weekday() # 0=周一
|
||||
start = business_date - timedelta(days=weekday)
|
||||
return start, business_date
|
||||
|
||||
if dimension == "last_week":
|
||||
weekday = business_date.weekday()
|
||||
this_monday = business_date - timedelta(days=weekday)
|
||||
last_monday = this_monday - timedelta(days=7)
|
||||
last_sunday = this_monday - timedelta(days=1)
|
||||
return last_monday, last_sunday
|
||||
|
||||
if dimension == "last_3_months":
|
||||
# 当前月 - 3 ~ 当前月 - 1
|
||||
end_ym = _month_offset(y, m, -1)
|
||||
start_ym = _month_offset(y, m, -3)
|
||||
start = date(start_ym[0], start_ym[1], 1)
|
||||
# end = 上月最后一天
|
||||
end = date(y, m, 1) - timedelta(days=1)
|
||||
return start, end
|
||||
|
||||
if dimension == "this_quarter":
|
||||
q_start_month = ((m - 1) // 3) * 3 + 1
|
||||
start = date(y, q_start_month, 1)
|
||||
return start, business_date
|
||||
|
||||
if dimension == "last_quarter":
|
||||
q_start_month = ((m - 1) // 3) * 3 + 1
|
||||
# 上季度结束 = 本季度第一天 - 1
|
||||
this_q_start = date(y, q_start_month, 1)
|
||||
end = this_q_start - timedelta(days=1)
|
||||
# 上季度开始
|
||||
ly, lm = end.year, end.month
|
||||
lq_start_month = ((lm - 1) // 3) * 3 + 1
|
||||
start = date(ly, lq_start_month, 1)
|
||||
return start, end
|
||||
|
||||
if dimension == "last_6_months":
|
||||
# 当前月 - 6 ~ 当前月 - 1
|
||||
end_ym = _month_offset(y, m, -1)
|
||||
start_ym = _month_offset(y, m, -6)
|
||||
start = date(start_ym[0], start_ym[1], 1)
|
||||
end = date(y, m, 1) - timedelta(days=1)
|
||||
return start, end
|
||||
|
||||
raise ValueError(f"未知时间维度: {dimension}")
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App2 财务洞察调用。
|
||||
|
||||
Args:
|
||||
context: 包含 site_id, time_dimension, user_id(默认'system'), nickname(默认'')
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(insights 数组)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
time_dimension = context["time_dimension"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 构建 Prompt
|
||||
prompt_context = {
|
||||
"site_id": site_id,
|
||||
"time_dimension": time_dimension,
|
||||
"current_data": context.get("current_data", {}),
|
||||
"previous_data": context.get("previous_data", {}),
|
||||
}
|
||||
messages = build_prompt(prompt_context)
|
||||
|
||||
# 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"time_dimension": time_dimension},
|
||||
)
|
||||
|
||||
# 写入 system prompt 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
# 写入 user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 写入缓存
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP2_FINANCE.value,
|
||||
site_id=site_id,
|
||||
target_id=time_dimension,
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App2 财务洞察完成: site_id=%s dimension=%s conversation_id=%s tokens=%d",
|
||||
site_id, time_dimension, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _month_offset(year: int, month: int, offset: int) -> tuple[int, int]:
|
||||
"""计算月份偏移,返回 (year, month)。"""
|
||||
# 转为 0-based 计算
|
||||
total = (year * 12 + (month - 1)) + offset
|
||||
return total // 12, total % 12 + 1
|
||||
213
apps/backend/app/ai/apps/app3_clue.py
Normal file
213
apps/backend/app/ai/apps/app3_clue.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""应用 3:客户数据维客线索分析(骨架)。
|
||||
|
||||
客户新增消费时自动触发,通过 AI 分析客户数据提取维客线索。
|
||||
线索 category 限定 3 个枚举值:客户基础、消费习惯、玩法偏好。
|
||||
线索提供者统一标记为"系统"。
|
||||
|
||||
使用 items_sum 口径(= table_charge_money + goods_money
|
||||
+ assistant_pd_money + assistant_cx_money + electricity_money),
|
||||
禁止使用 consume_money。
|
||||
|
||||
app_id = "app3_clue"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app3_clue"
|
||||
|
||||
|
||||
def build_prompt(
|
||||
context: dict,
|
||||
cache_svc: AICacheService | None = None,
|
||||
) -> list[dict]:
|
||||
"""构建 Prompt 消息列表。
|
||||
|
||||
P5-A 阶段:返回占位 Prompt,标注待细化字段。
|
||||
P5-B 阶段(P9-T1):补充 consumption_records 等完整数据。
|
||||
|
||||
Args:
|
||||
context: 包含 site_id, member_id, nickname 等
|
||||
cache_svc: 缓存服务,用于获取 reference 历史数据
|
||||
|
||||
Returns:
|
||||
消息列表 [{"role": "system", "content": ...}, {"role": "user", ...}]
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
|
||||
# 构建 reference:App6 线索 + 最近 2 套 App8 历史(附 generated_at)
|
||||
reference = _build_reference(site_id, member_id, cache_svc)
|
||||
|
||||
system_content = {
|
||||
"task": "分析客户消费数据,提取维客线索。",
|
||||
"app_id": APP_ID,
|
||||
"rules": {
|
||||
"category_enum": ["客户基础", "消费习惯", "玩法偏好"],
|
||||
"providers": "系统",
|
||||
"amount_caliber": "items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money",
|
||||
"禁止使用": "consume_money",
|
||||
},
|
||||
"output_format": {
|
||||
"clues": [
|
||||
{
|
||||
"category": "枚举值(客户基础/消费习惯/玩法偏好)",
|
||||
"summary": "一句话摘要",
|
||||
"detail": "详细说明",
|
||||
"emoji": "表情符号",
|
||||
}
|
||||
]
|
||||
},
|
||||
# TODO: P9-T1 细化 - consumption_records 等客户消费数据
|
||||
"data": {
|
||||
"consumption_records": "待 P9-T1 补充",
|
||||
"member_info": "待 P9-T1 补充",
|
||||
},
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
user_content = (
|
||||
f"请分析会员 {member_id} 的消费数据,提取维客线索。"
|
||||
"每条线索包含 category、summary、detail、emoji 四个字段。"
|
||||
"category 必须是:客户基础、消费习惯、玩法偏好 之一。"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
|
||||
def _build_reference(
|
||||
site_id: int,
|
||||
member_id: int,
|
||||
cache_svc: AICacheService | None,
|
||||
) -> dict:
|
||||
"""构建 Prompt reference 字段。
|
||||
|
||||
包含:
|
||||
- App6 备注分析线索(最新一条,如有)
|
||||
- 最近 2 套 App8 维客线索整理历史(附 generated_at)
|
||||
|
||||
缓存不存在时返回空对象 {}。
|
||||
"""
|
||||
if cache_svc is None:
|
||||
return {}
|
||||
|
||||
reference: dict = {}
|
||||
target_id = str(member_id)
|
||||
|
||||
# App6 备注分析线索
|
||||
app6_latest = cache_svc.get_latest(
|
||||
CacheTypeEnum.APP6_NOTE_ANALYSIS.value, site_id, target_id,
|
||||
)
|
||||
if app6_latest:
|
||||
reference["app6_note_clues"] = {
|
||||
"result_json": app6_latest.get("result_json"),
|
||||
"generated_at": app6_latest.get("created_at"),
|
||||
}
|
||||
|
||||
# 最近 2 套 App8 历史
|
||||
app8_history = cache_svc.get_history(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id, limit=2,
|
||||
)
|
||||
if app8_history:
|
||||
reference["app8_history"] = [
|
||||
{
|
||||
"result_json": h.get("result_json"),
|
||||
"generated_at": h.get("created_at"),
|
||||
}
|
||||
for h in app8_history
|
||||
]
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App3 客户数据维客线索分析。
|
||||
|
||||
流程:
|
||||
1. build_prompt 构建 Prompt
|
||||
2. bailian.chat_json 调用百炼
|
||||
3. 写入 conversation + messages
|
||||
4. 写入 ai_cache
|
||||
5. 返回结果
|
||||
|
||||
Args:
|
||||
context: site_id, member_id, user_id(默认'system'), nickname(默认'')
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(clues 数组)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 1. 构建 Prompt
|
||||
messages = build_prompt(context, cache_svc)
|
||||
|
||||
# 2. 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"member_id": member_id},
|
||||
)
|
||||
|
||||
# 写入 system + user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 3. 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 4. 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 5. 写入缓存
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP3_CLUE.value,
|
||||
site_id=site_id,
|
||||
target_id=str(member_id),
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App3 线索分析完成: site_id=%s member_id=%s conversation_id=%s tokens=%d",
|
||||
site_id, member_id, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
200
apps/backend/app/ai/apps/app4_analysis.py
Normal file
200
apps/backend/app/ai/apps/app4_analysis.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""应用 4:关系分析/任务建议(骨架)。
|
||||
|
||||
助教参与新结算或被分配召回任务时自动触发,
|
||||
生成关系分析和任务建议。
|
||||
|
||||
Prompt reference 包含 App8 最新 + 最近 2 套历史(附 generated_at)。
|
||||
缓存不存在时 reference 传空对象,标注"暂无历史线索"。
|
||||
|
||||
app_id = "app4_analysis"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app4_analysis"
|
||||
|
||||
|
||||
def build_prompt(
|
||||
context: dict,
|
||||
cache_svc: AICacheService | None = None,
|
||||
) -> list[dict]:
|
||||
"""构建 Prompt 消息列表。
|
||||
|
||||
P5-A 阶段:返回占位 Prompt,标注待细化字段。
|
||||
P5-B 阶段(P6-T4):补充 service_history、assistant_info 等完整数据。
|
||||
|
||||
Args:
|
||||
context: 包含 site_id, assistant_id, member_id
|
||||
cache_svc: 缓存服务,用于获取 reference 历史数据
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
assistant_id = context["assistant_id"]
|
||||
member_id = context["member_id"]
|
||||
|
||||
# 构建 reference:App8 最新 + 最近 2 套历史
|
||||
reference = _build_reference(site_id, member_id, cache_svc)
|
||||
|
||||
system_content = {
|
||||
"task": "分析助教与客户的关系,生成任务建议。",
|
||||
"app_id": APP_ID,
|
||||
"output_format": {
|
||||
"task_description": "任务描述文本",
|
||||
"action_suggestions": ["建议1", "建议2"],
|
||||
"one_line_summary": "一句话总结",
|
||||
},
|
||||
# TODO: P6-T4 细化 - service_history、assistant_info
|
||||
"data": {
|
||||
"service_history": "待 P6-T4 补充",
|
||||
"assistant_info": "待 P6-T4 补充",
|
||||
},
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
# 缓存不存在时在 user prompt 中标注
|
||||
no_history_hint = ""
|
||||
if not reference:
|
||||
no_history_hint = "(暂无历史线索,请基于现有信息分析)"
|
||||
|
||||
user_content = (
|
||||
f"请分析助教 {assistant_id} 与会员 {member_id} 的关系,"
|
||||
f"生成任务建议。{no_history_hint}"
|
||||
"返回 task_description、action_suggestions、one_line_summary 三个字段。"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
|
||||
def _build_reference(
|
||||
site_id: int,
|
||||
member_id: int,
|
||||
cache_svc: AICacheService | None,
|
||||
) -> dict:
|
||||
"""构建 Prompt reference 字段。
|
||||
|
||||
包含:
|
||||
- App8 最新维客线索(如有)
|
||||
- 最近 2 套 App8 历史(附 generated_at)
|
||||
|
||||
缓存不存在时返回空对象 {}。
|
||||
"""
|
||||
if cache_svc is None:
|
||||
return {}
|
||||
|
||||
reference: dict = {}
|
||||
target_id = str(member_id)
|
||||
|
||||
# App8 最新
|
||||
app8_latest = cache_svc.get_latest(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id,
|
||||
)
|
||||
if app8_latest:
|
||||
reference["app8_latest"] = {
|
||||
"result_json": app8_latest.get("result_json"),
|
||||
"generated_at": app8_latest.get("created_at"),
|
||||
}
|
||||
|
||||
# 最近 2 套 App8 历史
|
||||
app8_history = cache_svc.get_history(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id, limit=2,
|
||||
)
|
||||
if app8_history:
|
||||
reference["app8_history"] = [
|
||||
{
|
||||
"result_json": h.get("result_json"),
|
||||
"generated_at": h.get("created_at"),
|
||||
}
|
||||
for h in app8_history
|
||||
]
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App4 关系分析。
|
||||
|
||||
Args:
|
||||
context: site_id, assistant_id, member_id
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(task_description, action_suggestions, one_line_summary)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
assistant_id = context["assistant_id"]
|
||||
member_id = context["member_id"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 1. 构建 Prompt
|
||||
messages = build_prompt(context, cache_svc)
|
||||
|
||||
# 2. 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"assistant_id": assistant_id, "member_id": member_id},
|
||||
)
|
||||
|
||||
# 写入 system + user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 3. 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 4. 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 5. 写入缓存(target_id = {assistant_id}_{member_id})
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP4_ANALYSIS.value,
|
||||
site_id=site_id,
|
||||
target_id=f"{assistant_id}_{member_id}",
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App4 关系分析完成: site_id=%s assistant=%s member=%s conversation_id=%s tokens=%d",
|
||||
site_id, assistant_id, member_id, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
182
apps/backend/app/ai/apps/app5_tactics.py
Normal file
182
apps/backend/app/ai/apps/app5_tactics.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""应用 5:话术参考(骨架)。
|
||||
|
||||
App4 完成后自动联动触发,接收 App4 完整返回结果
|
||||
作为 Prompt 中的 task_suggestion 字段。
|
||||
|
||||
Prompt reference 包含最近 2 套 App8 历史(附 generated_at)。
|
||||
|
||||
app_id = "app5_tactics"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app5_tactics"
|
||||
|
||||
|
||||
def build_prompt(
|
||||
context: dict,
|
||||
cache_svc: AICacheService | None = None,
|
||||
) -> list[dict]:
|
||||
"""构建 Prompt 消息列表。
|
||||
|
||||
P5-A 阶段:返回占位 Prompt,标注待细化字段。
|
||||
P5-B 阶段(P6-T4):补充 service_history、assistant_info(随 App4 同步)。
|
||||
|
||||
Args:
|
||||
context: 包含 site_id, assistant_id, member_id, app4_result(dict)
|
||||
cache_svc: 缓存服务,用于获取 reference 历史数据
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
assistant_id = context["assistant_id"]
|
||||
member_id = context["member_id"]
|
||||
app4_result = context.get("app4_result", {})
|
||||
|
||||
# 构建 reference:最近 2 套 App8 历史
|
||||
reference = _build_reference(site_id, member_id, cache_svc)
|
||||
|
||||
system_content = {
|
||||
"task": "基于关系分析和任务建议,生成沟通话术参考。",
|
||||
"app_id": APP_ID,
|
||||
"task_suggestion": app4_result,
|
||||
"output_format": {
|
||||
"tactics": [
|
||||
{"scenario": "场景描述", "script": "话术内容"}
|
||||
]
|
||||
},
|
||||
# TODO: P6-T4 细化 - service_history、assistant_info(随 App4 同步)
|
||||
"data": {
|
||||
"service_history": "待 P6-T4 补充",
|
||||
"assistant_info": "待 P6-T4 补充",
|
||||
},
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
user_content = (
|
||||
f"请为助教 {assistant_id} 生成与会员 {member_id} 沟通的话术参考。"
|
||||
"返回 tactics 数组,每条包含 scenario 和 script 字段。"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
|
||||
def _build_reference(
|
||||
site_id: int,
|
||||
member_id: int,
|
||||
cache_svc: AICacheService | None,
|
||||
) -> dict:
|
||||
"""构建 Prompt reference 字段。
|
||||
|
||||
包含最近 2 套 App8 历史(附 generated_at)。
|
||||
缓存不存在时返回空对象 {}。
|
||||
"""
|
||||
if cache_svc is None:
|
||||
return {}
|
||||
|
||||
reference: dict = {}
|
||||
target_id = str(member_id)
|
||||
|
||||
# 最近 2 套 App8 历史
|
||||
app8_history = cache_svc.get_history(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id, limit=2,
|
||||
)
|
||||
if app8_history:
|
||||
reference["app8_history"] = [
|
||||
{
|
||||
"result_json": h.get("result_json"),
|
||||
"generated_at": h.get("created_at"),
|
||||
}
|
||||
for h in app8_history
|
||||
]
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App5 话术参考。
|
||||
|
||||
Args:
|
||||
context: site_id, assistant_id, member_id, app4_result(dict)
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(tactics 数组)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
assistant_id = context["assistant_id"]
|
||||
member_id = context["member_id"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 1. 构建 Prompt
|
||||
messages = build_prompt(context, cache_svc)
|
||||
|
||||
# 2. 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"assistant_id": assistant_id, "member_id": member_id},
|
||||
)
|
||||
|
||||
# 写入 system + user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 3. 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 4. 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 5. 写入缓存(target_id = {assistant_id}_{member_id})
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP5_TACTICS.value,
|
||||
site_id=site_id,
|
||||
target_id=f"{assistant_id}_{member_id}",
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App5 话术参考完成: site_id=%s assistant=%s member=%s conversation_id=%s tokens=%d",
|
||||
site_id, assistant_id, member_id, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
217
apps/backend/app/ai/apps/app6_note.py
Normal file
217
apps/backend/app/ai/apps/app6_note.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""应用 6:备注分析(骨架)。
|
||||
|
||||
助教提交备注后自动触发,通过 AI 分析备注内容,
|
||||
提取维客线索并评分。
|
||||
|
||||
返回 score(1-10)+ clues 数组。
|
||||
评分规则:6 分为标准分,重复/低价值/时效性低酌情扣分,高价值信息酌情加分。
|
||||
线索 category 限定 6 个枚举值。
|
||||
线索提供者标记为当前备注提供人(context.noted_by_name)。
|
||||
|
||||
app_id = "app6_note"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app6_note"
|
||||
|
||||
|
||||
def build_prompt(
|
||||
context: dict,
|
||||
cache_svc: AICacheService | None = None,
|
||||
) -> list[dict]:
|
||||
"""构建 Prompt 消息列表。
|
||||
|
||||
P5-A 阶段:返回占位 Prompt,标注待细化字段。
|
||||
P5-B 阶段(P9-T1):补充 consumption_data 等完整数据。
|
||||
|
||||
Args:
|
||||
context: 包含 site_id, member_id, note_content, noted_by_name
|
||||
cache_svc: 缓存服务,用于获取 reference 历史数据
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
note_content = context.get("note_content", "")
|
||||
noted_by_name = context.get("noted_by_name", "")
|
||||
|
||||
# 构建 reference:App3 线索 + 最近 2 套 App8 历史
|
||||
reference = _build_reference(site_id, member_id, cache_svc)
|
||||
|
||||
system_content = {
|
||||
"task": "分析备注内容,提取维客线索并评分。",
|
||||
"app_id": APP_ID,
|
||||
"rules": {
|
||||
"category_enum": [
|
||||
"客户基础", "消费习惯", "玩法偏好",
|
||||
"促销偏好", "社交关系", "重要反馈",
|
||||
],
|
||||
"providers": noted_by_name,
|
||||
"scoring": "6 分为标准分,重复/低价值/时效性低酌情扣分,高价值信息酌情加分",
|
||||
"score_range": "1-10",
|
||||
},
|
||||
"output_format": {
|
||||
"score": "1-10 整数",
|
||||
"clues": [
|
||||
{
|
||||
"category": "枚举值(6 选 1)",
|
||||
"summary": "一句话摘要",
|
||||
"detail": "详细说明",
|
||||
"emoji": "表情符号",
|
||||
}
|
||||
],
|
||||
},
|
||||
"note_content": note_content,
|
||||
"noted_by_name": noted_by_name,
|
||||
# TODO: P9-T1 细化 - consumption_data 等客户消费数据
|
||||
"data": {
|
||||
"consumption_data": "待 P9-T1 补充",
|
||||
},
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
user_content = (
|
||||
f"请分析以下备注内容,提取维客线索并评分。\n"
|
||||
f"备注提供人:{noted_by_name}\n"
|
||||
f"备注内容:{note_content}\n"
|
||||
"返回 score(1-10 整数)和 clues 数组。"
|
||||
"category 必须是:客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈 之一。"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
|
||||
def _build_reference(
|
||||
site_id: int,
|
||||
member_id: int,
|
||||
cache_svc: AICacheService | None,
|
||||
) -> dict:
|
||||
"""构建 Prompt reference 字段。
|
||||
|
||||
包含:
|
||||
- App3 客户数据线索(最新一条,如有)
|
||||
- 最近 2 套 App8 维客线索整理历史(附 generated_at)
|
||||
|
||||
缓存不存在时返回空对象 {}。
|
||||
"""
|
||||
if cache_svc is None:
|
||||
return {}
|
||||
|
||||
reference: dict = {}
|
||||
target_id = str(member_id)
|
||||
|
||||
# App3 客户数据线索
|
||||
app3_latest = cache_svc.get_latest(
|
||||
CacheTypeEnum.APP3_CLUE.value, site_id, target_id,
|
||||
)
|
||||
if app3_latest:
|
||||
reference["app3_clues"] = {
|
||||
"result_json": app3_latest.get("result_json"),
|
||||
"generated_at": app3_latest.get("created_at"),
|
||||
}
|
||||
|
||||
# 最近 2 套 App8 历史
|
||||
app8_history = cache_svc.get_history(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id, limit=2,
|
||||
)
|
||||
if app8_history:
|
||||
reference["app8_history"] = [
|
||||
{
|
||||
"result_json": h.get("result_json"),
|
||||
"generated_at": h.get("created_at"),
|
||||
}
|
||||
for h in app8_history
|
||||
]
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App6 备注分析。
|
||||
|
||||
Args:
|
||||
context: site_id, member_id, note_content, noted_by_name
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(score + clues 数组)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 1. 构建 Prompt
|
||||
messages = build_prompt(context, cache_svc)
|
||||
|
||||
# 2. 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"member_id": member_id},
|
||||
)
|
||||
|
||||
# 写入 system + user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 3. 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 4. 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 5. 写入缓存(score 存入 ai_cache.score)
|
||||
score = result.get("score")
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP6_NOTE_ANALYSIS.value,
|
||||
site_id=site_id,
|
||||
target_id=str(member_id),
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
score=score,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App6 备注分析完成: site_id=%s member_id=%s score=%s conversation_id=%s tokens=%d",
|
||||
site_id, member_id, score, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
200
apps/backend/app/ai/apps/app7_customer.py
Normal file
200
apps/backend/app/ai/apps/app7_customer.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""应用 7:客户分析(骨架)。
|
||||
|
||||
消费事件链中 App8 完成后串行触发,生成客户全量分析与运营建议。
|
||||
|
||||
使用 items_sum 口径(= table_charge_money + goods_money
|
||||
+ assistant_pd_money + assistant_cx_money + electricity_money),
|
||||
禁止使用 consume_money。
|
||||
|
||||
对主观信息(来自备注)标注【来源:XXX,请甄别信息真实性】。
|
||||
|
||||
app_id = "app7_customer"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app7_customer"
|
||||
|
||||
|
||||
def build_prompt(
|
||||
context: dict,
|
||||
cache_svc: AICacheService | None = None,
|
||||
) -> list[dict]:
|
||||
"""构建 Prompt 消息列表。
|
||||
|
||||
P5-A 阶段:返回占位 Prompt,标注待细化字段。
|
||||
P5-B 阶段(P9-T1):补充 objective_data 等完整数据。
|
||||
|
||||
Args:
|
||||
context: 包含 site_id, member_id
|
||||
cache_svc: 缓存服务,用于获取 reference 历史数据
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
|
||||
# 构建 reference:最新 + 最近 2 套 App8 历史
|
||||
reference = _build_reference(site_id, member_id, cache_svc)
|
||||
|
||||
system_content = {
|
||||
"task": "综合分析客户数据,生成运营策略建议。",
|
||||
"app_id": APP_ID,
|
||||
"rules": {
|
||||
"amount_caliber": "items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money",
|
||||
"禁止使用": "consume_money",
|
||||
"subjective_info_label": "对主观信息(来自备注)标注【来源:XXX,请甄别信息真实性】",
|
||||
},
|
||||
"output_format": {
|
||||
"strategies": [
|
||||
{"title": "策略标题", "content": "策略内容"}
|
||||
],
|
||||
"summary": "一句话总结",
|
||||
},
|
||||
# TODO: P9-T1 细化 - objective_data 等客户消费数据
|
||||
"data": {
|
||||
"objective_data": "待 P9-T1 补充",
|
||||
},
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
user_content = (
|
||||
f"请综合分析会员 {member_id} 的客户数据,生成运营策略建议。"
|
||||
"返回 strategies 数组(每条含 title 和 content)和 summary 字段。"
|
||||
"对来自备注的主观信息,请标注【来源:XXX,请甄别信息真实性】。"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
|
||||
def _build_reference(
|
||||
site_id: int,
|
||||
member_id: int,
|
||||
cache_svc: AICacheService | None,
|
||||
) -> dict:
|
||||
"""构建 Prompt reference 字段。
|
||||
|
||||
包含:
|
||||
- App8 最新维客线索(如有)
|
||||
- 最近 2 套 App8 历史(附 generated_at)
|
||||
|
||||
缓存不存在时返回空对象 {}。
|
||||
"""
|
||||
if cache_svc is None:
|
||||
return {}
|
||||
|
||||
reference: dict = {}
|
||||
target_id = str(member_id)
|
||||
|
||||
# App8 最新
|
||||
app8_latest = cache_svc.get_latest(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id,
|
||||
)
|
||||
if app8_latest:
|
||||
reference["app8_latest"] = {
|
||||
"result_json": app8_latest.get("result_json"),
|
||||
"generated_at": app8_latest.get("created_at"),
|
||||
}
|
||||
|
||||
# 最近 2 套 App8 历史
|
||||
app8_history = cache_svc.get_history(
|
||||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id, limit=2,
|
||||
)
|
||||
if app8_history:
|
||||
reference["app8_history"] = [
|
||||
{
|
||||
"result_json": h.get("result_json"),
|
||||
"generated_at": h.get("created_at"),
|
||||
}
|
||||
for h in app8_history
|
||||
]
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App7 客户分析。
|
||||
|
||||
Args:
|
||||
context: site_id, member_id
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(strategies 数组 + summary)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 1. 构建 Prompt
|
||||
messages = build_prompt(context, cache_svc)
|
||||
|
||||
# 2. 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"member_id": member_id},
|
||||
)
|
||||
|
||||
# 写入 system + user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 3. 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 4. 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 5. 写入缓存
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP7_CUSTOMER_ANALYSIS.value,
|
||||
site_id=site_id,
|
||||
target_id=str(member_id),
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App7 客户分析完成: site_id=%s member_id=%s conversation_id=%s tokens=%d",
|
||||
site_id, member_id, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
211
apps/backend/app/ai/apps/app8_consolidation.py
Normal file
211
apps/backend/app/ai/apps/app8_consolidation.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""应用 8:维客线索整理。
|
||||
|
||||
接收 App3(消费分析)和 App6(备注分析)的线索,
|
||||
通过百炼 AI 整合去重,然后全量替换写入 member_retention_clue 表。
|
||||
|
||||
app_id = "app8_consolidation"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.prompts.app8_consolidation_prompt import build_prompt
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
from app.database import get_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ID = "app8_consolidation"
|
||||
|
||||
|
||||
class ClueWriter:
|
||||
"""维客线索全量替换写入器。
|
||||
|
||||
DELETE source IN ('ai_consumption', 'ai_note') → INSERT 新线索(事务)。
|
||||
人工线索(source='manual')不受影响。
|
||||
"""
|
||||
|
||||
def replace_ai_clues(
|
||||
self,
|
||||
member_id: int,
|
||||
site_id: int,
|
||||
clues: list[dict],
|
||||
) -> int:
|
||||
"""全量替换该客户的 AI 来源线索,返回写入数量。
|
||||
|
||||
在单个事务中执行 DELETE + INSERT,失败时回滚保留原有线索。
|
||||
|
||||
字段映射:
|
||||
- category → category
|
||||
- emoji + " " + summary → summary(如 "📅 偏好周末下午时段消费")
|
||||
- detail → detail
|
||||
- providers → recorded_by_name
|
||||
- source: 根据 providers 判断(见 _determine_source)
|
||||
- recorded_by_assistant_id: NULL(系统触发)
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 1. 删除该客户所有 AI 来源线索
|
||||
cur.execute(
|
||||
"""
|
||||
DELETE FROM member_retention_clue
|
||||
WHERE member_id = %s AND site_id = %s
|
||||
AND source IN ('ai_consumption', 'ai_note')
|
||||
""",
|
||||
(member_id, site_id),
|
||||
)
|
||||
|
||||
# 2. 插入新线索
|
||||
for clue in clues:
|
||||
emoji = clue.get("emoji", "")
|
||||
raw_summary = clue.get("summary", "")
|
||||
summary = f"{emoji} {raw_summary}" if emoji else raw_summary
|
||||
source = _determine_source(clue.get("providers", ""))
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO member_retention_clue
|
||||
(member_id, site_id, category, summary, detail,
|
||||
source, recorded_by_name, recorded_by_assistant_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, NULL)
|
||||
""",
|
||||
(
|
||||
member_id,
|
||||
site_id,
|
||||
clue.get("category", ""),
|
||||
summary,
|
||||
clue.get("detail", ""),
|
||||
source,
|
||||
clue.get("providers", ""),
|
||||
),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return len(clues)
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _determine_source(providers: str) -> str:
|
||||
"""根据 providers 判断 source 值。
|
||||
|
||||
- 纯 App3(providers 仅含"系统")→ ai_consumption
|
||||
- 纯 App6(providers 不含"系统")→ ai_note
|
||||
- 混合来源 → ai_consumption
|
||||
"""
|
||||
if not providers:
|
||||
return "ai_consumption"
|
||||
provider_list = [p.strip() for p in providers.split(",")]
|
||||
has_system = "系统" in provider_list
|
||||
has_human = any(p != "系统" for p in provider_list if p)
|
||||
if has_system and not has_human:
|
||||
# 纯 App3(系统自动分析)
|
||||
return "ai_consumption"
|
||||
elif has_human and not has_system:
|
||||
# 纯 App6(人工备注分析)
|
||||
return "ai_note"
|
||||
else:
|
||||
# 混合来源
|
||||
return "ai_consumption"
|
||||
|
||||
|
||||
async def run(
|
||||
context: dict,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> dict:
|
||||
"""执行 App8 维客线索整理。
|
||||
|
||||
流程:
|
||||
1. build_prompt 构建 Prompt
|
||||
2. bailian.chat_json 调用百炼
|
||||
3. 写入 conversation + messages
|
||||
4. 写入 ai_cache
|
||||
5. ClueWriter 全量替换 member_retention_clue
|
||||
6. 返回结果
|
||||
|
||||
Args:
|
||||
context: site_id, member_id, app3_clues, app6_clues,
|
||||
app3_generated_at, app6_generated_at
|
||||
bailian: 百炼客户端
|
||||
cache_svc: 缓存服务
|
||||
conv_svc: 对话服务
|
||||
|
||||
Returns:
|
||||
百炼返回的结构化 JSON(clues 数组)
|
||||
"""
|
||||
site_id = context["site_id"]
|
||||
member_id = context["member_id"]
|
||||
user_id = context.get("user_id", "system")
|
||||
nickname = context.get("nickname", "")
|
||||
|
||||
# 1. 构建 Prompt
|
||||
messages = build_prompt(context)
|
||||
|
||||
# 2. 创建对话记录
|
||||
conversation_id = conv_svc.create_conversation(
|
||||
user_id=user_id,
|
||||
nickname=nickname,
|
||||
app_id=APP_ID,
|
||||
site_id=site_id,
|
||||
source_context={"member_id": member_id},
|
||||
)
|
||||
|
||||
# 写入 system + user 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="system",
|
||||
content=messages[0]["content"],
|
||||
)
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=messages[1]["content"],
|
||||
)
|
||||
|
||||
# 3. 调用百炼 API
|
||||
result, tokens_used = await bailian.chat_json(messages)
|
||||
|
||||
# 4. 写入 assistant 消息
|
||||
conv_svc.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=json.dumps(result, ensure_ascii=False),
|
||||
tokens_used=tokens_used,
|
||||
)
|
||||
|
||||
# 5. 写入缓存
|
||||
cache_svc.write_cache(
|
||||
cache_type=CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value,
|
||||
site_id=site_id,
|
||||
target_id=str(member_id),
|
||||
result_json=result,
|
||||
triggered_by=f"user:{user_id}",
|
||||
)
|
||||
|
||||
# 6. 全量替换 member_retention_clue
|
||||
clues = result.get("clues", [])
|
||||
if clues:
|
||||
writer = ClueWriter()
|
||||
written = writer.replace_ai_clues(member_id, site_id, clues)
|
||||
logger.info(
|
||||
"App8 线索写入完成: site_id=%s member_id=%s written=%d",
|
||||
site_id, member_id, written,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"App8 线索整理完成: site_id=%s member_id=%s conversation_id=%s tokens=%d",
|
||||
site_id, member_id, conversation_id, tokens_used,
|
||||
)
|
||||
|
||||
return result
|
||||
338
apps/backend/app/ai/dispatcher.py
Normal file
338
apps/backend/app/ai/dispatcher.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""AI 事件调度与调用链编排器。
|
||||
|
||||
根据业务事件(消费、备注、任务分配)编排 AI 应用调用链,
|
||||
确保执行顺序和数据依赖正确。
|
||||
|
||||
调用链:
|
||||
- 消费事件(无助教):App3 → App8 → App7
|
||||
- 消费事件(有助教):App3 → App8 → App7 + App4 → App5
|
||||
- 备注事件:App6 → App8
|
||||
- 任务分配事件:App4 → App5(读已有 App8 缓存)
|
||||
|
||||
容错策略:
|
||||
- 某步失败记录错误日志,后续应用使用已有缓存继续
|
||||
- 失败应用写入失败 conversation 记录
|
||||
- 整条链后台异步执行,不阻塞业务请求
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Callable, Coroutine
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.cache_service import AICacheService
|
||||
from app.ai.conversation_service import ConversationService
|
||||
from app.ai.schemas import CacheTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIDispatcher:
|
||||
"""AI 应用调用链编排器。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bailian: BailianClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
) -> None:
|
||||
self.bailian = bailian
|
||||
self.cache_svc = cache_svc
|
||||
self.conv_svc = conv_svc
|
||||
|
||||
async def handle_consumption_event(
|
||||
self,
|
||||
member_id: int,
|
||||
site_id: int,
|
||||
settle_id: int,
|
||||
assistant_id: int | None = None,
|
||||
) -> None:
|
||||
"""消费事件链:App3 → App8 → App7(+ App4 → App5 如有助教)。"""
|
||||
from app.ai.apps.app3_clue import run as app3_run
|
||||
from app.ai.apps.app4_analysis import run as app4_run
|
||||
from app.ai.apps.app5_tactics import run as app5_run
|
||||
from app.ai.apps.app7_customer import run as app7_run
|
||||
from app.ai.apps.app8_consolidation import run as app8_run
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"member_id": member_id,
|
||||
"site_id": site_id,
|
||||
"settle_id": settle_id,
|
||||
"user_id": "system",
|
||||
"nickname": "",
|
||||
}
|
||||
|
||||
# 步骤 1:App3 线索分析
|
||||
app3_result = await self._run_step("app3_clue", app3_run, context)
|
||||
|
||||
# 步骤 2:App8 线索整理(需要 App3 的 clues)
|
||||
app8_context = {**context}
|
||||
# 从 App3 结果提取 clues;同时从缓存获取 App6 已有线索
|
||||
if app3_result:
|
||||
app8_context["app3_clues"] = app3_result.get("clues", [])
|
||||
app8_context["app3_generated_at"] = None # 刚生成,无需时间戳
|
||||
else:
|
||||
app8_context["app3_clues"] = []
|
||||
app8_context["app3_generated_at"] = None
|
||||
|
||||
# 从缓存获取 App6 已有线索
|
||||
app6_cache = self.cache_svc.get_latest(
|
||||
CacheTypeEnum.APP6_NOTE_ANALYSIS.value, site_id, str(member_id),
|
||||
)
|
||||
if app6_cache:
|
||||
app6_result_json = app6_cache.get("result_json", {})
|
||||
if isinstance(app6_result_json, str):
|
||||
try:
|
||||
app6_result_json = json.loads(app6_result_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
app6_result_json = {}
|
||||
app8_context["app6_clues"] = app6_result_json.get("clues", [])
|
||||
app8_context["app6_generated_at"] = app6_cache.get("created_at")
|
||||
else:
|
||||
app8_context["app6_clues"] = []
|
||||
app8_context["app6_generated_at"] = None
|
||||
|
||||
await self._run_step("app8_consolidation", app8_run, app8_context)
|
||||
|
||||
# 步骤 3:App7 客户分析
|
||||
await self._run_step("app7_customer", app7_run, context)
|
||||
|
||||
# 步骤 4(可选):如有助教,App4 → App5
|
||||
if assistant_id is not None:
|
||||
app4_context = {**context, "assistant_id": assistant_id}
|
||||
app4_result = await self._run_step("app4_analysis", app4_run, app4_context)
|
||||
|
||||
app5_context = {
|
||||
**context,
|
||||
"assistant_id": assistant_id,
|
||||
"app4_result": app4_result or {},
|
||||
}
|
||||
await self._run_step("app5_tactics", app5_run, app5_context)
|
||||
|
||||
async def handle_note_event(
|
||||
self,
|
||||
member_id: int,
|
||||
site_id: int,
|
||||
note_id: int,
|
||||
note_content: str,
|
||||
noted_by_name: str,
|
||||
) -> None:
|
||||
"""备注事件链:App6 → App8。"""
|
||||
from app.ai.apps.app6_note import run as app6_run
|
||||
from app.ai.apps.app8_consolidation import run as app8_run
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"member_id": member_id,
|
||||
"site_id": site_id,
|
||||
"note_id": note_id,
|
||||
"note_content": note_content,
|
||||
"noted_by_name": noted_by_name,
|
||||
"user_id": "system",
|
||||
"nickname": "",
|
||||
}
|
||||
|
||||
# 步骤 1:App6 备注分析
|
||||
app6_result = await self._run_step("app6_note", app6_run, context)
|
||||
|
||||
# 步骤 2:App8 线索整理(需要 App6 的 clues)
|
||||
app8_context: dict[str, Any] = {
|
||||
"member_id": member_id,
|
||||
"site_id": site_id,
|
||||
"user_id": "system",
|
||||
"nickname": "",
|
||||
}
|
||||
|
||||
if app6_result:
|
||||
app8_context["app6_clues"] = app6_result.get("clues", [])
|
||||
app8_context["app6_generated_at"] = None
|
||||
else:
|
||||
app8_context["app6_clues"] = []
|
||||
app8_context["app6_generated_at"] = None
|
||||
|
||||
# 从缓存获取 App3 已有线索
|
||||
app3_cache = self.cache_svc.get_latest(
|
||||
CacheTypeEnum.APP3_CLUE.value, site_id, str(member_id),
|
||||
)
|
||||
if app3_cache:
|
||||
app3_result_json = app3_cache.get("result_json", {})
|
||||
if isinstance(app3_result_json, str):
|
||||
try:
|
||||
app3_result_json = json.loads(app3_result_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
app3_result_json = {}
|
||||
app8_context["app3_clues"] = app3_result_json.get("clues", [])
|
||||
app8_context["app3_generated_at"] = app3_cache.get("created_at")
|
||||
else:
|
||||
app8_context["app3_clues"] = []
|
||||
app8_context["app3_generated_at"] = None
|
||||
|
||||
await self._run_step("app8_consolidation", app8_run, app8_context)
|
||||
|
||||
async def handle_task_assign_event(
|
||||
self,
|
||||
assistant_id: int,
|
||||
member_id: int,
|
||||
site_id: int,
|
||||
task_type: str,
|
||||
) -> None:
|
||||
"""任务分配事件链:App4 → App5(读已有 App8 缓存)。"""
|
||||
from app.ai.apps.app4_analysis import run as app4_run
|
||||
from app.ai.apps.app5_tactics import run as app5_run
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"assistant_id": assistant_id,
|
||||
"member_id": member_id,
|
||||
"site_id": site_id,
|
||||
"task_type": task_type,
|
||||
"user_id": "system",
|
||||
"nickname": "",
|
||||
}
|
||||
|
||||
# 步骤 1:App4 关系分析
|
||||
app4_result = await self._run_step("app4_analysis", app4_run, context)
|
||||
|
||||
# 步骤 2:App5 话术参考
|
||||
app5_context = {
|
||||
**context,
|
||||
"app4_result": app4_result or {},
|
||||
}
|
||||
await self._run_step("app5_tactics", app5_run, app5_context)
|
||||
|
||||
async def _run_chain(
|
||||
self,
|
||||
chain: list[tuple[str, Callable[..., Coroutine], dict]],
|
||||
) -> None:
|
||||
"""串行执行调用链,某步失败记录日志后继续。
|
||||
|
||||
Args:
|
||||
chain: [(app_name, run_func, context), ...] 的列表
|
||||
"""
|
||||
for app_name, run_func, ctx in chain:
|
||||
await self._run_step(app_name, run_func, ctx)
|
||||
|
||||
async def _run_step(
|
||||
self,
|
||||
app_name: str,
|
||||
run_func: Callable[..., Coroutine],
|
||||
context: dict,
|
||||
) -> dict | None:
|
||||
"""执行单个应用步骤,失败时记录日志并写入失败 conversation。
|
||||
|
||||
Returns:
|
||||
应用返回结果,失败时返回 None
|
||||
"""
|
||||
try:
|
||||
result = await run_func(
|
||||
context,
|
||||
self.bailian,
|
||||
self.cache_svc,
|
||||
self.conv_svc,
|
||||
)
|
||||
logger.info("调用链步骤成功: %s", app_name)
|
||||
return result
|
||||
except Exception:
|
||||
logger.exception("调用链步骤失败: %s", app_name)
|
||||
# 写入失败 conversation 记录
|
||||
try:
|
||||
site_id = context.get("site_id", 0)
|
||||
conv_id = self.conv_svc.create_conversation(
|
||||
user_id="system",
|
||||
nickname="",
|
||||
app_id=app_name,
|
||||
site_id=site_id,
|
||||
source_context={"error": True, "chain_step": app_name},
|
||||
)
|
||||
self.conv_svc.add_message(
|
||||
conversation_id=conv_id,
|
||||
role="system",
|
||||
content=f"调用链步骤 {app_name} 执行失败",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("写入失败 conversation 记录也失败: %s", app_name)
|
||||
return None
|
||||
|
||||
def _create_ai_event_handlers(dispatcher: AIDispatcher) -> dict[str, Callable]:
|
||||
"""创建 AI 事件处理器,用于注册到 trigger_scheduler。
|
||||
|
||||
每个处理器从 payload 提取参数,通过 asyncio.create_task 后台执行,
|
||||
不阻塞同步的 fire_event 调用。
|
||||
|
||||
Returns:
|
||||
{event_job_type: handler_func} 映射
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
def _get_or_create_loop() -> asyncio.AbstractEventLoop:
|
||||
"""获取当前事件循环,兼容同步调用场景。"""
|
||||
try:
|
||||
return asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.new_event_loop()
|
||||
|
||||
def handle_consumption_settled(payload: dict | None = None, **_kw: Any) -> None:
|
||||
"""消费结算事件处理器(同步入口,内部异步执行)。"""
|
||||
if not payload:
|
||||
logger.warning("consumption_settled 事件缺少 payload")
|
||||
return
|
||||
loop = _get_or_create_loop()
|
||||
loop.create_task(
|
||||
dispatcher.handle_consumption_event(
|
||||
member_id=payload["member_id"],
|
||||
site_id=payload["site_id"],
|
||||
settle_id=payload["settle_id"],
|
||||
assistant_id=payload.get("assistant_id"),
|
||||
)
|
||||
)
|
||||
|
||||
def handle_note_created(payload: dict | None = None, **_kw: Any) -> None:
|
||||
"""备注创建事件处理器。"""
|
||||
if not payload:
|
||||
logger.warning("note_created 事件缺少 payload")
|
||||
return
|
||||
loop = _get_or_create_loop()
|
||||
loop.create_task(
|
||||
dispatcher.handle_note_event(
|
||||
member_id=payload["member_id"],
|
||||
site_id=payload["site_id"],
|
||||
note_id=payload["note_id"],
|
||||
note_content=payload.get("note_content", ""),
|
||||
noted_by_name=payload.get("noted_by_name", ""),
|
||||
)
|
||||
)
|
||||
|
||||
def handle_task_assigned(payload: dict | None = None, **_kw: Any) -> None:
|
||||
"""任务分配事件处理器。"""
|
||||
if not payload:
|
||||
logger.warning("task_assigned 事件缺少 payload")
|
||||
return
|
||||
loop = _get_or_create_loop()
|
||||
loop.create_task(
|
||||
dispatcher.handle_task_assign_event(
|
||||
assistant_id=payload["assistant_id"],
|
||||
member_id=payload["member_id"],
|
||||
site_id=payload["site_id"],
|
||||
task_type=payload.get("task_type", ""),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"ai_consumption_settled": handle_consumption_settled,
|
||||
"ai_note_created": handle_note_created,
|
||||
"ai_task_assigned": handle_task_assigned,
|
||||
}
|
||||
|
||||
|
||||
def register_ai_handlers(dispatcher: AIDispatcher) -> None:
|
||||
"""将 AI 事件处理器注册到 trigger_scheduler。
|
||||
|
||||
在 FastAPI lifespan 中调用,将三个 AI 事件处理器
|
||||
注册为 trigger_scheduler 的 job handler。
|
||||
"""
|
||||
from app.services.trigger_scheduler import register_job
|
||||
|
||||
handlers = _create_ai_event_handlers(dispatcher)
|
||||
for job_type, handler in handlers.items():
|
||||
register_job(job_type, handler)
|
||||
logger.info("已注册 AI 事件处理器: %s", job_type)
|
||||
145
apps/backend/app/ai/prompts/app2_finance_prompt.py
Normal file
145
apps/backend/app/ai/prompts/app2_finance_prompt.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""应用 2 财务洞察 Prompt 模板。
|
||||
|
||||
构建包含当期和上期收入结构的完整 Prompt,供百炼 API 生成财务洞察。
|
||||
|
||||
收入字段映射(严格遵守 items_sum 口径):
|
||||
- table_fee = table_charge_money(台费)
|
||||
- assistant_pd = assistant_pd_money(陪打费)
|
||||
- assistant_cx = assistant_cx_money(超休费)
|
||||
- goods = goods_money(商品收入)
|
||||
- recharge = 充值 pay_amount settle_type=5(充值收入)
|
||||
|
||||
禁止使用 consume_money,统一使用:
|
||||
items_sum = table_charge_money + goods_money + assistant_pd_money
|
||||
+ assistant_cx_money + electricity_money
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def build_prompt(context: dict) -> list[dict]:
|
||||
"""构建 App2 财务洞察 Prompt 消息列表。
|
||||
|
||||
Args:
|
||||
context: 包含以下字段:
|
||||
- site_id: int,门店 ID
|
||||
- time_dimension: str,时间维度编码
|
||||
- current_data: dict,当期数据
|
||||
- previous_data: dict,上期数据
|
||||
|
||||
Returns:
|
||||
messages 列表(system + user),供 BailianClient.chat_json 调用
|
||||
"""
|
||||
site_id = context.get("site_id", 0)
|
||||
time_dimension = context.get("time_dimension", "")
|
||||
current_data = context.get("current_data", {})
|
||||
previous_data = context.get("previous_data", {})
|
||||
|
||||
system_content = _build_system_content(
|
||||
site_id=site_id,
|
||||
time_dimension=time_dimension,
|
||||
current_data=current_data,
|
||||
previous_data=previous_data,
|
||||
)
|
||||
|
||||
user_content = (
|
||||
f"请根据以上数据,为门店 {site_id} 生成 {_dimension_label(time_dimension)} 的财务洞察分析。"
|
||||
"以 JSON 格式返回,包含 insights 数组,每项含 seq(序号)、title(标题)、body(正文)。"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
|
||||
def _build_system_content(
|
||||
*,
|
||||
site_id: int,
|
||||
time_dimension: str,
|
||||
current_data: dict,
|
||||
previous_data: dict,
|
||||
) -> dict:
|
||||
"""构建 system prompt JSON 结构。"""
|
||||
return {
|
||||
"task": (
|
||||
"你是台球门店的财务分析 AI 助手。"
|
||||
"根据提供的当期和上期经营数据,生成结构化的财务洞察。"
|
||||
"分析维度包括:收入结构变化、各收入项占比、环比趋势、异常波动。"
|
||||
"输出 JSON 格式:{\"insights\": [{\"seq\": 1, \"title\": \"...\", \"body\": \"...\"}]}"
|
||||
),
|
||||
"data": {
|
||||
"site_id": site_id,
|
||||
"time_dimension": time_dimension,
|
||||
"time_dimension_label": _dimension_label(time_dimension),
|
||||
"current_period": _build_period_data(current_data),
|
||||
"previous_period": _build_period_data(previous_data),
|
||||
},
|
||||
"reference": {
|
||||
"field_mapping": {
|
||||
"items_sum": (
|
||||
"table_charge_money + goods_money + assistant_pd_money"
|
||||
" + assistant_cx_money + electricity_money"
|
||||
),
|
||||
"table_fee": "table_charge_money(台费收入)",
|
||||
"assistant_pd": "assistant_pd_money(陪打费)",
|
||||
"assistant_cx": "assistant_cx_money(超休费)",
|
||||
"goods": "goods_money(商品收入)",
|
||||
"recharge": "充值 pay_amount(settle_type=5,充值收入)",
|
||||
"electricity": "electricity_money(电费,当前未启用,全为 0)",
|
||||
},
|
||||
"rules": [
|
||||
"统一使用 items_sum 口径计算营收总额",
|
||||
"助教费用必须拆分为 assistant_pd_money(陪打)和 assistant_cx_money(超休)",
|
||||
"支付渠道恒等式:balance_amount = recharge_card_amount + gift_card_amount",
|
||||
"金额单位:元(CNY),保留两位小数",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_period_data(data: dict) -> dict:
|
||||
"""构建单期数据结构,确保字段名遵守 items_sum 口径。"""
|
||||
return {
|
||||
# 收入结构(items_sum 口径)
|
||||
"table_charge_money": data.get("table_charge_money", 0),
|
||||
"goods_money": data.get("goods_money", 0),
|
||||
"assistant_pd_money": data.get("assistant_pd_money", 0),
|
||||
"assistant_cx_money": data.get("assistant_cx_money", 0),
|
||||
"electricity_money": data.get("electricity_money", 0),
|
||||
# 充值收入
|
||||
"recharge_income": data.get("recharge_income", 0),
|
||||
# 储值资产
|
||||
"balance_pay": data.get("balance_pay", 0),
|
||||
"recharge_card_pay": data.get("recharge_card_pay", 0),
|
||||
"gift_card_pay": data.get("gift_card_pay", 0),
|
||||
# 费用汇总
|
||||
"discount_amount": data.get("discount_amount", 0),
|
||||
"adjust_amount": data.get("adjust_amount", 0),
|
||||
# 平台结算
|
||||
"platform_settlement_amount": data.get("platform_settlement_amount", 0),
|
||||
"groupbuy_pay_amount": data.get("groupbuy_pay_amount", 0),
|
||||
# 汇总
|
||||
"order_count": data.get("order_count", 0),
|
||||
"member_count": data.get("member_count", 0),
|
||||
}
|
||||
|
||||
|
||||
# 时间维度编码 → 中文标签
|
||||
_DIMENSION_LABELS: dict[str, str] = {
|
||||
"this_month": "本月",
|
||||
"last_month": "上月",
|
||||
"this_week": "本周",
|
||||
"last_week": "上周",
|
||||
"last_3_months": "近三个月",
|
||||
"this_quarter": "本季度",
|
||||
"last_quarter": "上季度",
|
||||
"last_6_months": "近六个月",
|
||||
}
|
||||
|
||||
|
||||
def _dimension_label(dimension: str) -> str:
|
||||
"""将时间维度编码转为中文标签。"""
|
||||
return _DIMENSION_LABELS.get(dimension, dimension)
|
||||
93
apps/backend/app/ai/prompts/app8_consolidation_prompt.py
Normal file
93
apps/backend/app/ai/prompts/app8_consolidation_prompt.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""应用 8:维客线索整理 Prompt 模板。
|
||||
|
||||
接收 App3(消费分析)和 App6(备注分析)的全部线索,
|
||||
整合去重后输出统一维客线索。
|
||||
|
||||
分类标签限定 6 个枚举值(与 member_retention_clue CHECK 约束一致):
|
||||
客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈。
|
||||
|
||||
合并规则:
|
||||
- 相似线索合并,providers 以逗号分隔
|
||||
- 其余线索原文返回
|
||||
- 最小改动原则
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def build_prompt(context: dict) -> list[dict]:
|
||||
"""构建 App8 维客线索整理 Prompt。
|
||||
|
||||
Args:
|
||||
context: 包含以下字段:
|
||||
- site_id: int
|
||||
- member_id: int
|
||||
- app3_clues: list[dict] — App3 产出的线索列表
|
||||
- app6_clues: list[dict] — App6 产出的线索列表
|
||||
- app3_generated_at: str | None — App3 线索生成时间
|
||||
- app6_generated_at: str | None — App6 线索生成时间
|
||||
|
||||
Returns:
|
||||
消息列表 [{"role": "system", ...}, {"role": "user", ...}]
|
||||
"""
|
||||
member_id = context["member_id"]
|
||||
app3_clues = context.get("app3_clues", [])
|
||||
app6_clues = context.get("app6_clues", [])
|
||||
app3_generated_at = context.get("app3_generated_at")
|
||||
app6_generated_at = context.get("app6_generated_at")
|
||||
|
||||
system_content = {
|
||||
"task": "整合去重来自消费分析和备注分析的维客线索,输出统一线索列表。",
|
||||
"app_id": "app8_consolidation",
|
||||
"rules": {
|
||||
"category_enum": [
|
||||
"客户基础", "消费习惯", "玩法偏好",
|
||||
"促销偏好", "社交关系", "重要反馈",
|
||||
],
|
||||
"merge_strategy": (
|
||||
"相似线索合并为一条,providers 以逗号分隔(如 '系统,张三');"
|
||||
"不相似的线索原文保留,不做修改。最小改动原则。"
|
||||
),
|
||||
"output_format": {
|
||||
"clues": [
|
||||
{
|
||||
"category": "枚举值(6 选 1)",
|
||||
"summary": "一句话摘要",
|
||||
"detail": "详细说明",
|
||||
"emoji": "表情符号",
|
||||
"providers": "提供者(逗号分隔)",
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"input": {
|
||||
"app3_clues": {
|
||||
"source": "消费数据分析(App3)",
|
||||
"generated_at": app3_generated_at,
|
||||
"clues": app3_clues,
|
||||
},
|
||||
"app6_clues": {
|
||||
"source": "备注分析(App6)",
|
||||
"generated_at": app6_generated_at,
|
||||
"clues": app6_clues,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
user_content = (
|
||||
f"请整合会员 {member_id} 的维客线索。\n"
|
||||
"输入包含两个来源的线索:App3(消费数据分析)和 App6(备注分析)。\n"
|
||||
"规则:\n"
|
||||
"1. 相似线索合并为一条,providers 字段以逗号分隔多个提供者\n"
|
||||
"2. 不相似的线索原文保留\n"
|
||||
"3. category 必须是:客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈 之一\n"
|
||||
"4. 每条线索包含 category、summary、detail、emoji、providers 五个字段\n"
|
||||
"5. 最小改动原则,尽量保留原始表述"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": json.dumps(system_content, ensure_ascii=False)},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
Reference in New Issue
Block a user