微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
1
apps/backend/app/ai/__init__.py
Normal file
1
apps/backend/app/ai/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# AI 集成模块:百炼 API 封装、8 个 AI 应用、事件调度、缓存管理
|
||||
1
apps/backend/app/ai/apps/__init__.py
Normal file
1
apps/backend/app/ai/apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# AI 应用子模块:app1_chat ~ app8_consolidation
|
||||
273
apps/backend/app/ai/bailian_client.py
Normal file
273
apps/backend/app/ai/bailian_client.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""百炼 API 统一封装层。
|
||||
|
||||
使用 openai Python SDK(百炼兼容 OpenAI 协议),提供流式和非流式两种调用模式。
|
||||
所有 AI 应用通过此客户端统一调用阿里云通义千问。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
import openai
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 异常类 ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class BailianApiError(Exception):
|
||||
"""百炼 API 调用失败(重试耗尽后)。"""
|
||||
|
||||
def __init__(self, message: str, status_code: int | None = None):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class BailianJsonParseError(Exception):
|
||||
"""百炼 API 返回的 JSON 解析失败。"""
|
||||
|
||||
def __init__(self, message: str, raw_content: str = ""):
|
||||
super().__init__(message)
|
||||
self.raw_content = raw_content
|
||||
|
||||
|
||||
class BailianAuthError(BailianApiError):
|
||||
"""百炼 API Key 无效(HTTP 401)。"""
|
||||
|
||||
def __init__(self, message: str = "API Key 无效或已过期"):
|
||||
super().__init__(message, status_code=401)
|
||||
|
||||
|
||||
# ── 客户端 ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class BailianClient:
|
||||
"""百炼 API 统一封装层。
|
||||
|
||||
使用 openai.AsyncOpenAI 客户端,base_url 指向百炼端点。
|
||||
提供流式(chat_stream)和非流式(chat_json)两种调用模式。
|
||||
"""
|
||||
|
||||
# 重试配置
|
||||
MAX_RETRIES = 3
|
||||
BASE_INTERVAL = 1 # 秒
|
||||
|
||||
def __init__(self, api_key: str, base_url: str, model: str):
|
||||
"""初始化百炼客户端。
|
||||
|
||||
Args:
|
||||
api_key: 百炼 API Key(环境变量 BAILIAN_API_KEY)
|
||||
base_url: 百炼 API 端点(环境变量 BAILIAN_BASE_URL)
|
||||
model: 模型标识,如 qwen-plus(环境变量 BAILIAN_MODEL)
|
||||
"""
|
||||
self.model = model
|
||||
self._client = openai.AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
async def chat_stream(
|
||||
self,
|
||||
messages: list[dict],
|
||||
*,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2000,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""流式调用,逐 chunk yield 文本。用于应用 1 SSE。
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
temperature: 温度参数,默认 0.7
|
||||
max_tokens: 最大 token 数,默认 2000
|
||||
|
||||
Yields:
|
||||
文本 chunk
|
||||
"""
|
||||
messages = self._inject_current_time(messages)
|
||||
response = await self._call_with_retry(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=True,
|
||||
)
|
||||
async for chunk in response:
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
async def chat_json(
|
||||
self,
|
||||
messages: list[dict],
|
||||
*,
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 4000,
|
||||
) -> tuple[dict, int]:
|
||||
"""非流式调用,返回解析后的 JSON dict 和 tokens_used。
|
||||
|
||||
用于应用 2-8 的结构化输出。使用 response_format={"type": "json_object"}
|
||||
确保返回合法 JSON。
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
temperature: 温度参数,默认 0.3(结构化输出用低温度)
|
||||
max_tokens: 最大 token 数,默认 4000
|
||||
|
||||
Returns:
|
||||
(parsed_json_dict, tokens_used) 元组
|
||||
|
||||
Raises:
|
||||
BailianJsonParseError: 响应内容无法解析为 JSON
|
||||
BailianApiError: API 调用失败(重试耗尽后)
|
||||
"""
|
||||
messages = self._inject_current_time(messages)
|
||||
response = await self._call_with_retry(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=False,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
raw_content = response.choices[0].message.content or ""
|
||||
tokens_used = response.usage.total_tokens if response.usage else 0
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw_content)
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.error("百炼 API 返回非法 JSON: %s", raw_content[:500])
|
||||
raise BailianJsonParseError(
|
||||
f"JSON 解析失败: {e}",
|
||||
raw_content=raw_content,
|
||||
) from e
|
||||
|
||||
return parsed, tokens_used
|
||||
|
||||
def _inject_current_time(self, messages: list[dict]) -> list[dict]:
|
||||
"""纯函数:在首条消息的 content(JSON 字符串)中注入 current_time 字段。
|
||||
|
||||
- 深拷贝输入,不修改原始 messages
|
||||
- 首条消息 content 尝试解析为 JSON,注入 current_time
|
||||
- 如果首条消息 content 不是 JSON,则包装为 JSON
|
||||
- 其余消息不变
|
||||
- current_time 格式:ISO 8601 精确到秒,如 2026-03-08T14:30:00
|
||||
|
||||
Args:
|
||||
messages: 原始消息列表
|
||||
|
||||
Returns:
|
||||
注入 current_time 后的新消息列表
|
||||
"""
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
result = copy.deepcopy(messages)
|
||||
first = result[0]
|
||||
content = first.get("content", "")
|
||||
now_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
try:
|
||||
parsed = json.loads(content)
|
||||
if isinstance(parsed, dict):
|
||||
parsed["current_time"] = now_str
|
||||
else:
|
||||
# content 是合法 JSON 但不是 dict(如数组、字符串),包装为 dict
|
||||
parsed = {"original_content": parsed, "current_time": now_str}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# content 不是 JSON,包装为 dict
|
||||
parsed = {"content": content, "current_time": now_str}
|
||||
|
||||
first["content"] = json.dumps(parsed, ensure_ascii=False)
|
||||
return result
|
||||
|
||||
async def _call_with_retry(self, **kwargs: Any) -> Any:
|
||||
"""带指数退避的重试封装。
|
||||
|
||||
重试策略:
|
||||
- 最多重试 MAX_RETRIES 次(默认 3 次)
|
||||
- 间隔:BASE_INTERVAL × 2^(n-1),即 1s → 2s → 4s
|
||||
- HTTP 4xx:不重试,直接抛出(401 → BailianAuthError)
|
||||
- HTTP 5xx / 超时:重试
|
||||
|
||||
Args:
|
||||
**kwargs: 传递给 openai client 的参数
|
||||
|
||||
Returns:
|
||||
API 响应对象
|
||||
|
||||
Raises:
|
||||
BailianAuthError: API Key 无效(HTTP 401)
|
||||
BailianApiError: API 调用失败(重试耗尽后)
|
||||
"""
|
||||
is_stream = kwargs.get("stream", False)
|
||||
last_error: Exception | None = None
|
||||
|
||||
for attempt in range(self.MAX_RETRIES):
|
||||
try:
|
||||
if is_stream:
|
||||
# 流式调用:返回 async iterator
|
||||
return await self._client.chat.completions.create(**kwargs)
|
||||
else:
|
||||
return await self._client.chat.completions.create(**kwargs)
|
||||
|
||||
except openai.AuthenticationError as e:
|
||||
# 401:API Key 无效,不重试
|
||||
logger.error("百炼 API 认证失败: %s", e)
|
||||
raise BailianAuthError(str(e)) from e
|
||||
|
||||
except openai.BadRequestError as e:
|
||||
# 400:请求参数错误,不重试
|
||||
logger.error("百炼 API 请求参数错误: %s", e)
|
||||
raise BailianApiError(str(e), status_code=400) from e
|
||||
|
||||
except openai.RateLimitError as e:
|
||||
# 429:限流,不重试(属于 4xx)
|
||||
logger.error("百炼 API 限流: %s", e)
|
||||
raise BailianApiError(str(e), status_code=429) from e
|
||||
|
||||
except openai.PermissionDeniedError as e:
|
||||
# 403:权限不足,不重试
|
||||
logger.error("百炼 API 权限不足: %s", e)
|
||||
raise BailianApiError(str(e), status_code=403) from e
|
||||
|
||||
except openai.NotFoundError as e:
|
||||
# 404:资源不存在,不重试
|
||||
logger.error("百炼 API 资源不存在: %s", e)
|
||||
raise BailianApiError(str(e), status_code=404) from e
|
||||
|
||||
except openai.UnprocessableEntityError as e:
|
||||
# 422:不可处理,不重试
|
||||
logger.error("百炼 API 不可处理的请求: %s", e)
|
||||
raise BailianApiError(str(e), status_code=422) from e
|
||||
|
||||
except (openai.InternalServerError, openai.APIConnectionError, openai.APITimeoutError) as e:
|
||||
# 5xx / 超时 / 连接错误:重试
|
||||
last_error = e
|
||||
if attempt < self.MAX_RETRIES - 1:
|
||||
wait_time = self.BASE_INTERVAL * (2 ** attempt)
|
||||
logger.warning(
|
||||
"百炼 API 调用失败(第 %d/%d 次),%ds 后重试: %s",
|
||||
attempt + 1,
|
||||
self.MAX_RETRIES,
|
||||
wait_time,
|
||||
e,
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
logger.error(
|
||||
"百炼 API 调用失败,已达最大重试次数 %d: %s",
|
||||
self.MAX_RETRIES,
|
||||
e,
|
||||
)
|
||||
|
||||
# 重试耗尽
|
||||
status_code = getattr(last_error, "status_code", None)
|
||||
raise BailianApiError(
|
||||
f"百炼 API 调用失败(重试 {self.MAX_RETRIES} 次后): {last_error}",
|
||||
status_code=status_code,
|
||||
) from last_error
|
||||
188
apps/backend/app/ai/cache_service.py
Normal file
188
apps/backend/app/ai/cache_service.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
AI 缓存读写服务。
|
||||
|
||||
负责 biz.ai_cache 表的 CRUD 和保留策略管理。
|
||||
所有查询和写入操作强制 site_id 隔离。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AICacheService:
|
||||
"""AI 缓存读写服务。"""
|
||||
|
||||
def get_latest(
|
||||
self,
|
||||
cache_type: str,
|
||||
site_id: int,
|
||||
target_id: str,
|
||||
) -> dict | None:
|
||||
"""查询最新缓存记录。
|
||||
|
||||
按 (cache_type, site_id, target_id) 查询 created_at 最新的一条。
|
||||
无记录时返回 None。
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, cache_type, site_id, target_id,
|
||||
result_json, score, triggered_by,
|
||||
created_at, expires_at
|
||||
FROM biz.ai_cache
|
||||
WHERE cache_type = %s AND site_id = %s AND target_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(cache_type, site_id, target_id),
|
||||
)
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return _row_to_dict(columns, row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_history(
|
||||
self,
|
||||
cache_type: str,
|
||||
site_id: int,
|
||||
target_id: str,
|
||||
limit: int = 2,
|
||||
) -> list[dict]:
|
||||
"""查询历史缓存记录(按 created_at DESC),用于 Prompt reference。
|
||||
|
||||
无记录时返回空列表。
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, cache_type, site_id, target_id,
|
||||
result_json, score, triggered_by,
|
||||
created_at, expires_at
|
||||
FROM biz.ai_cache
|
||||
WHERE cache_type = %s AND site_id = %s AND target_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
(cache_type, site_id, target_id, limit),
|
||||
)
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
rows = cur.fetchall()
|
||||
return [_row_to_dict(columns, row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def write_cache(
|
||||
self,
|
||||
cache_type: str,
|
||||
site_id: int,
|
||||
target_id: str,
|
||||
result_json: dict,
|
||||
triggered_by: str | None = None,
|
||||
score: int | None = None,
|
||||
expires_at: datetime | None = None,
|
||||
) -> int:
|
||||
"""写入缓存记录,返回 id。写入后清理超限记录。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.ai_cache
|
||||
(cache_type, site_id, target_id, result_json,
|
||||
triggered_by, score, expires_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
cache_type,
|
||||
site_id,
|
||||
target_id,
|
||||
json.dumps(result_json, ensure_ascii=False),
|
||||
triggered_by,
|
||||
score,
|
||||
expires_at,
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
cache_id: int = row[0]
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 写入成功后清理超限记录(失败仅记录警告,不影响写入结果)
|
||||
try:
|
||||
deleted = self._cleanup_excess(cache_type, site_id, target_id)
|
||||
if deleted > 0:
|
||||
logger.info(
|
||||
"清理超限缓存: cache_type=%s site_id=%s target_id=%s 删除=%d",
|
||||
cache_type, site_id, target_id, deleted,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"清理超限缓存失败: cache_type=%s site_id=%s target_id=%s",
|
||||
cache_type, site_id, target_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return cache_id
|
||||
|
||||
def _cleanup_excess(
|
||||
self,
|
||||
cache_type: str,
|
||||
site_id: int,
|
||||
target_id: str,
|
||||
max_count: int = 500,
|
||||
) -> int:
|
||||
"""清理超限记录,保留最近 max_count 条,返回删除数量。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 删除超出保留上限的最旧记录
|
||||
cur.execute(
|
||||
"""
|
||||
DELETE FROM biz.ai_cache
|
||||
WHERE id IN (
|
||||
SELECT id FROM biz.ai_cache
|
||||
WHERE cache_type = %s AND site_id = %s AND target_id = %s
|
||||
ORDER BY created_at DESC
|
||||
OFFSET %s
|
||||
)
|
||||
""",
|
||||
(cache_type, site_id, target_id, max_count),
|
||||
)
|
||||
deleted = cur.rowcount
|
||||
conn.commit()
|
||||
return deleted
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _row_to_dict(columns: list[str], row: tuple) -> dict:
|
||||
"""将数据库行转换为 dict,处理特殊类型序列化。"""
|
||||
result = {}
|
||||
for col, val in zip(columns, row):
|
||||
if isinstance(val, datetime):
|
||||
result[col] = val.isoformat()
|
||||
else:
|
||||
result[col] = val
|
||||
return result
|
||||
160
apps/backend/app/ai/conversation_service.py
Normal file
160
apps/backend/app/ai/conversation_service.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
对话记录持久化服务。
|
||||
|
||||
负责 biz.ai_conversations 和 biz.ai_messages 两张表的 CRUD。
|
||||
所有 8 个 AI 应用的每次调用都通过本服务记录对话和消息。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConversationService:
|
||||
"""AI 对话记录持久化服务。"""
|
||||
|
||||
def create_conversation(
|
||||
self,
|
||||
user_id: int | str,
|
||||
nickname: str,
|
||||
app_id: str,
|
||||
site_id: int,
|
||||
source_page: str | None = None,
|
||||
source_context: dict | None = None,
|
||||
) -> int:
|
||||
"""创建对话记录,返回 conversation_id。
|
||||
|
||||
系统自动调用时 user_id 为 'system'。
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.ai_conversations
|
||||
(user_id, nickname, app_id, site_id, source_page, source_context)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
str(user_id),
|
||||
nickname,
|
||||
app_id,
|
||||
site_id,
|
||||
source_page,
|
||||
json.dumps(source_context, ensure_ascii=False) if source_context else None,
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row[0]
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
conversation_id: int,
|
||||
role: str,
|
||||
content: str,
|
||||
tokens_used: int | None = None,
|
||||
) -> int:
|
||||
"""添加消息记录,返回 message_id。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.ai_messages
|
||||
(conversation_id, role, content, tokens_used)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(conversation_id, role, content, tokens_used),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row[0]
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_conversations(
|
||||
self,
|
||||
user_id: int | str,
|
||||
site_id: int,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> list[dict]:
|
||||
"""查询用户历史对话列表,按 created_at 降序,分页。"""
|
||||
offset = (page - 1) * page_size
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, user_id, nickname, app_id, site_id,
|
||||
source_page, source_context, created_at
|
||||
FROM biz.ai_conversations
|
||||
WHERE user_id = %s AND site_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
(str(user_id), site_id, page_size, offset),
|
||||
)
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
rows = cur.fetchall()
|
||||
return [
|
||||
_row_to_dict(columns, row)
|
||||
for row in rows
|
||||
]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_messages(
|
||||
self,
|
||||
conversation_id: int,
|
||||
) -> list[dict]:
|
||||
"""查询对话的所有消息,按 created_at 升序。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, conversation_id, role, content,
|
||||
tokens_used, created_at
|
||||
FROM biz.ai_messages
|
||||
WHERE conversation_id = %s
|
||||
ORDER BY created_at ASC
|
||||
""",
|
||||
(conversation_id,),
|
||||
)
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
rows = cur.fetchall()
|
||||
return [
|
||||
_row_to_dict(columns, row)
|
||||
for row in rows
|
||||
]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _row_to_dict(columns: list[str], row: tuple) -> dict:
|
||||
"""将数据库行转换为 dict,处理特殊类型序列化。"""
|
||||
result = {}
|
||||
for col, val in zip(columns, row):
|
||||
if isinstance(val, datetime):
|
||||
result[col] = val.isoformat()
|
||||
else:
|
||||
result[col] = val
|
||||
return result
|
||||
1
apps/backend/app/ai/prompts/__init__.py
Normal file
1
apps/backend/app/ai/prompts/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# AI Prompt 模板子模块
|
||||
157
apps/backend/app/ai/schemas.py
Normal file
157
apps/backend/app/ai/schemas.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""AI 集成层 Pydantic 模型定义。
|
||||
|
||||
覆盖请求/响应体、缓存类型枚举、线索模型、各应用结果模型。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ── 请求/响应 ──
|
||||
|
||||
|
||||
class ChatStreamRequest(BaseModel):
|
||||
"""SSE 流式对话请求体"""
|
||||
|
||||
message: str
|
||||
source_page: str | None = None
|
||||
page_context: dict | None = None
|
||||
screen_content: str | None = None
|
||||
|
||||
|
||||
class SSEEvent(BaseModel):
|
||||
"""SSE 事件"""
|
||||
|
||||
type: str # chunk / done / error
|
||||
content: str | None = None
|
||||
conversation_id: int | None = None
|
||||
tokens_used: int | None = None
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ── 缓存类型枚举 ──
|
||||
|
||||
|
||||
class CacheTypeEnum(str, enum.Enum):
|
||||
APP2_FINANCE = "app2_finance"
|
||||
APP3_CLUE = "app3_clue"
|
||||
APP4_ANALYSIS = "app4_analysis"
|
||||
APP5_TACTICS = "app5_tactics"
|
||||
APP6_NOTE_ANALYSIS = "app6_note_analysis"
|
||||
APP7_CUSTOMER_ANALYSIS = "app7_customer_analysis"
|
||||
APP8_CLUE_CONSOLIDATED = "app8_clue_consolidated"
|
||||
|
||||
|
||||
# ── 线索相关 ──
|
||||
|
||||
|
||||
class App3CategoryEnum(str, enum.Enum):
|
||||
"""App3 线索分类(3 个枚举值)"""
|
||||
|
||||
CUSTOMER_BASIC = "客户基础"
|
||||
CONSUMPTION_HABIT = "消费习惯"
|
||||
PLAY_PREFERENCE = "玩法偏好"
|
||||
|
||||
|
||||
class App6CategoryEnum(str, enum.Enum):
|
||||
"""App6/8 线索分类(6 个枚举值)"""
|
||||
|
||||
CUSTOMER_BASIC = "客户基础"
|
||||
CONSUMPTION_HABIT = "消费习惯"
|
||||
PLAY_PREFERENCE = "玩法偏好"
|
||||
PROMO_PREFERENCE = "促销偏好"
|
||||
SOCIAL_RELATION = "社交关系"
|
||||
IMPORTANT_FEEDBACK = "重要反馈"
|
||||
|
||||
|
||||
class ClueItem(BaseModel):
|
||||
"""单条线索(App3/App6 共用)"""
|
||||
|
||||
category: str
|
||||
summary: str
|
||||
detail: str
|
||||
emoji: str
|
||||
|
||||
|
||||
class ConsolidatedClueItem(BaseModel):
|
||||
"""整合后线索(App8,含 providers)"""
|
||||
|
||||
category: str
|
||||
summary: str
|
||||
detail: str
|
||||
emoji: str
|
||||
providers: str
|
||||
|
||||
|
||||
# ── 各应用结果模型 ──
|
||||
|
||||
|
||||
class App2InsightItem(BaseModel):
|
||||
"""App2 财务洞察单条"""
|
||||
|
||||
seq: int
|
||||
title: str
|
||||
body: str
|
||||
|
||||
|
||||
class App2Result(BaseModel):
|
||||
"""App2 财务洞察结果"""
|
||||
|
||||
insights: list[App2InsightItem]
|
||||
|
||||
|
||||
class App3Result(BaseModel):
|
||||
"""App3 客户数据维客线索结果"""
|
||||
|
||||
clues: list[ClueItem]
|
||||
|
||||
|
||||
class App4Result(BaseModel):
|
||||
"""App4 关系分析结果"""
|
||||
|
||||
task_description: str
|
||||
action_suggestions: list[str]
|
||||
one_line_summary: str
|
||||
|
||||
|
||||
class App5TacticsItem(BaseModel):
|
||||
"""App5 话术单条"""
|
||||
|
||||
scenario: str
|
||||
script: str
|
||||
|
||||
|
||||
class App5Result(BaseModel):
|
||||
"""App5 话术参考结果"""
|
||||
|
||||
tactics: list[App5TacticsItem]
|
||||
|
||||
|
||||
class App6Result(BaseModel):
|
||||
"""App6 备注分析结果"""
|
||||
|
||||
score: int = Field(ge=1, le=10)
|
||||
clues: list[ClueItem]
|
||||
|
||||
|
||||
class App7StrategyItem(BaseModel):
|
||||
"""App7 客户分析策略单条"""
|
||||
|
||||
title: str
|
||||
content: str
|
||||
|
||||
|
||||
class App7Result(BaseModel):
|
||||
"""App7 客户分析结果"""
|
||||
|
||||
strategies: list[App7StrategyItem]
|
||||
summary: str
|
||||
|
||||
|
||||
class App8Result(BaseModel):
|
||||
"""App8 维客线索整理结果"""
|
||||
|
||||
clues: list[ConsolidatedClueItem]
|
||||
Reference in New Issue
Block a user