211 lines
6.0 KiB
Python
211 lines
6.0 KiB
Python
"""应用 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
|