212 lines
6.4 KiB
Python
212 lines
6.4 KiB
Python
"""应用 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
|