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

211 lines
6.0 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.
"""应用 2财务洞察。
8 个时间维度独立调用,每次调用结果写入 ai_cache
同时创建 ai_conversations + ai_messages 记录。
营业日分界点:每日 08:00BUSINESS_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:
百炼返回的结构化 JSONinsights 数组)
"""
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