feat(server): add workbench API, DSH planner worker and RAG pipeline
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""任务级成本/Token/耗时预估(交付报告 P2-1)。
|
||||
|
||||
思路:用本组织近期已结算的真实用量外推(历史均值 × 本次段数),
|
||||
没有历史数据时用保守常量估计。不依赖任何外部服务。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from .models import Campaign, LLMCall, Segment, UsageEvent
|
||||
from .billing import POINTS_PER_TOKEN
|
||||
|
||||
|
||||
# 无历史数据时的保守单段估计(deepseek 级别的小上下文生产段)
|
||||
FALLBACK_INPUT_TOKENS = 6000
|
||||
FALLBACK_OUTPUT_TOKENS = 2500
|
||||
FALLBACK_SECONDS = 90
|
||||
|
||||
|
||||
def estimate_campaign_cost(session: Session, campaign: Campaign, single_segment_id: int | None = None) -> dict:
|
||||
"""single_segment_id:逐段生成模式——只按指定段估算。"""
|
||||
if single_segment_id:
|
||||
segments = [s for s in session.exec(select(Segment).where(
|
||||
Segment.campaign_id == campaign.id)).all() if s.id == int(single_segment_id)]
|
||||
else:
|
||||
segments = session.exec(select(Segment).where(
|
||||
Segment.campaign_id == campaign.id)).all()
|
||||
seg_count = len(segments)
|
||||
if not seg_count:
|
||||
return {"ready": False, "reason": "任务还没有内容段,先完成导图拆解",
|
||||
"segments": 0}
|
||||
|
||||
org_id = int(campaign.organization_id or 1)
|
||||
|
||||
# 历史均值:本组织最近 200 条已测量 LLM 用量事件
|
||||
events = session.exec(select(UsageEvent).where(
|
||||
UsageEvent.organization_id == org_id,
|
||||
UsageEvent.service_type == "llm",
|
||||
).order_by(UsageEvent.id.desc()).limit(200)).all()
|
||||
measured = [e for e in events
|
||||
if e.status == "measured" and (e.input_quantity or 0) + (e.output_quantity or 0) > 0]
|
||||
|
||||
if measured:
|
||||
avg_in = sum(e.input_quantity or 0 for e in measured) / len(measured)
|
||||
avg_out = sum(e.output_quantity or 0 for e in measured) / len(measured)
|
||||
source = "history"
|
||||
else:
|
||||
avg_in, avg_out = float(FALLBACK_INPUT_TOKENS), float(FALLBACK_OUTPUT_TOKENS)
|
||||
source = "fallback"
|
||||
|
||||
# 耗时:近 200 次 LLM 调用的平均时长 × 每段约 3 次调用(生产+质检)+ 组合余量
|
||||
durations = [float(row.duration_ms or 0) for row in session.exec(
|
||||
select(LLMCall).order_by(LLMCall.id.desc()).limit(200)).all()
|
||||
if (row.duration_ms or 0) > 0]
|
||||
avg_seconds = FALLBACK_SECONDS
|
||||
if durations:
|
||||
avg_seconds = (sum(durations) / len(durations)) / 1000 * 3 + 15
|
||||
|
||||
total_in = avg_in * seg_count
|
||||
total_out = avg_out * seg_count
|
||||
total_tokens = int((total_in + total_out) * 1.6) # 1.6 倍:每段附带质检与组合分摊
|
||||
est_points = total_tokens * POINTS_PER_TOKEN
|
||||
est_seconds = int(avg_seconds * seg_count + 20)
|
||||
|
||||
# 费用:历史事件费率均值
|
||||
if measured:
|
||||
avg_in_rate = sum(e.input_rate or 0 for e in measured) / len(measured)
|
||||
avg_out_rate = sum(e.output_rate or 0 for e in measured) / len(measured)
|
||||
else:
|
||||
avg_in_rate = avg_out_rate = 0.0
|
||||
amount_minor = int(round((total_in / 1e6 * avg_in_rate + total_out / 1e6 * avg_out_rate) * 100))
|
||||
|
||||
return {
|
||||
"ready": True,
|
||||
"source": source,
|
||||
"segments": seg_count,
|
||||
"estimated_input_tokens": int(total_in),
|
||||
"estimated_output_tokens": int(total_out),
|
||||
"estimated_tokens": total_tokens,
|
||||
"estimated_points": est_points,
|
||||
"estimated_amount_minor": amount_minor,
|
||||
"estimated_seconds": est_seconds,
|
||||
"estimated_duration_text": (f"{est_seconds // 60} 分 {est_seconds % 60} 秒"
|
||||
if est_seconds >= 60 else f"{est_seconds} 秒"),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["estimate_campaign_cost"]
|
||||
|
||||
Reference in New Issue
Block a user