Files
Neo-ZQYY/apps/backend/app/ai/apps/app8_consolidation.py
2026-03-15 10:15:02 +08:00

212 lines
6.4 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.
"""应用 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 值。
- 纯 App3providers 仅含"系统")→ ai_consumption
- 纯 App6providers 不含"系统")→ 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:
百炼返回的结构化 JSONclues 数组)
"""
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