Files
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
2026-08-31 05:15:13 +00:00

148 lines
6.5 KiB
Python
Raw Permalink 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.
"""Gộp mức dùng theo khoảng thời gian — R09-T02.
Ngày / tuần / tháng / quý: ranh giới khoảng, nhãn hiển thị, chuỗi số vẽ biểu
đồ. Thuần tính toán trên danh sách sự kiện, không đụng đĩa.
"""
from __future__ import annotations
import json
import threading
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from ..config import CONFIG_DIR
from . import model_pricing as mp
from .usage_cost import cost_usd_events
def bucketed_series(events: List[Dict[str, Any]], granularity: str = "day",
pricing: Dict[str, Any] = None, last: int = None) -> List[tuple]:
"""Group usage events into time buckets → ordered ``[(label, tokens, cost_usd)]``.
``granularity``: ``day`` (YYYY-MM-DD) · ``month`` (YYYY-MM) · ``year`` (YYYY).
``last`` keeps only the most recent N buckets (for the dashboard chart)."""
from collections import OrderedDict
pricing = pricing or {}
def _key(ts: Any) -> str:
"""Khoá gom nhóm của một mốc thời gian theo độ mịn (tuần/tháng/năm)."""
s = str(ts or "")[:10]
if granularity == "year":
return s[:4]
if granularity == "month":
return s[:7]
return s
buckets: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
for e in sorted(events, key=lambda ev: str(ev.get("ts", ""))):
k = _key(e.get("ts"))
if k:
buckets.setdefault(k, []).append(e)
out = []
for k, evs in buckets.items():
tokens = sum(int(e.get("in", 0) or 0) + int(e.get("out", 0) or 0)
+ int(e.get("cache", 0) or 0) for e in evs)
cost = sum(cost_usd_events(evs, pricing).values())
out.append((k, tokens, cost))
if last and len(out) > last:
out = out[-last:]
return out
def period_bounds(gran: str, offset: int, today: Optional[date] = None) -> tuple:
"""[start, end) dates of the period ``offset`` periods from the current one
(0 = current, -1 = the previous week/month/year). Weeks run Mon→Sun."""
from datetime import timedelta
today = today or date.today()
if gran == "week":
monday = today - timedelta(days=today.weekday()) # Monday of this week
start = monday + timedelta(weeks=offset)
return start, start + timedelta(days=7)
if gran == "year":
y = today.year + offset
return date(y, 1, 1), date(y + 1, 1, 1)
# month (default)
base = today.year * 12 + (today.month - 1) + offset
y, m = divmod(base, 12)
y2, m2 = divmod(base + 1, 12)
return date(y, m + 1, 1), date(y2, m2 + 1, 1)
def _period_label(gran: str, start: date) -> str:
"""Nhãn hiển thị của một kỳ: thứ Hai đầu tuần, YYYY-MM, hoặc năm."""
if gran == "week":
return start.isoformat() # the week's Monday (YYYY-MM-DD)
if gran == "year":
return str(start.year)
return start.strftime("%Y-%m")
def _sum_between(events: List[Dict[str, Any]], start: date, end: date,
pricing: Dict[str, Any]) -> tuple:
"""Tổng token và chi phí của các sự kiện trong khoảng ``[start, end)``."""
lo, hi = start.isoformat(), end.isoformat()
evs = [e for e in events if lo <= str(e.get("ts", ""))[:10] < hi]
tokens = sum(int(e.get("in", 0) or 0) + int(e.get("out", 0) or 0)
+ int(e.get("cache", 0) or 0) for e in evs)
cost = sum(cost_usd_events(evs, pricing).values()) if evs else 0.0
return tokens, cost
def period_totals(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
offset: int = 0, today: Optional[date] = None) -> tuple:
"""(tokens, cost_usd) for the single period ``offset`` periods from now."""
start, end = period_bounds(gran, offset, today)
return _sum_between(events, start, end, pricing)
def period_window(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
count: int, offset: int = 0, today: Optional[date] = None) -> List[tuple]:
"""``count`` consecutive, ZERO-FILLED periods ending at (current + offset),
ordered oldest→newest → ``[(label, tokens, cost_usd)]``. ``offset`` (≤ 0)
pages the window into the past for the Dashboard's prev/next navigation."""
out = []
for i in range(count - 1, -1, -1):
start, end = period_bounds(gran, offset - i, today)
tok, cost = _sum_between(events, start, end, pricing)
out.append((_period_label(gran, start), tok, cost))
return out
def period_breakdown(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
offset: int = 0, today: Optional[date] = None) -> List[tuple]:
"""Break the SELECTED period (``offset`` periods from now) into its sub-parts
→ ``[(label, tokens, cost_usd)]``:
· week → 7 days Mon→Sun (label ``MM/DD``)
· month → weeks W1…Wn (7-day chunks from the 1st)
· year → 12 months (label ``01``…``12``)."""
from datetime import timedelta
start, end = period_bounds(gran, offset, today)
out = []
if gran == "week":
for i in range(7):
d = start + timedelta(days=i)
tok, cost = _sum_between(events, d, d + timedelta(days=1), pricing)
out.append((d.strftime("%m/%d"), tok, cost))
elif gran == "year":
for m in range(1, 13):
ms = date(start.year, m, 1)
me = date(start.year + 1, 1, 1) if m == 12 else date(start.year, m + 1, 1)
tok, cost = _sum_between(events, ms, me, pricing)
out.append((f"{m:02d}", tok, cost))
else: # month → weeks W1..Wn
ndays = (end - start).days
wk, day = 1, 1
while day <= ndays:
ws = date(start.year, start.month, day)
we = date(start.year, start.month, day + 7) if day + 7 <= ndays else end
tok, cost = _sum_between(events, ws, we, pricing)
out.append((f"W{wk}", tok, cost))
wk += 1
day += 7
return out
def period_range_label(gran: str, offset: int, today: Optional[date] = None) -> str:
"""Human label for the selected period (shown in the Dashboard header) —
week → MM/DD – MM/DD, month → YYYY/MM, year → YYYY."""
from datetime import timedelta
start, end = period_bounds(gran, offset, today)
if gran == "week":
last_day = end - timedelta(days=1)
return f"{start.strftime('%m/%d')} – {last_day.strftime('%m/%d')}"
if gran == "year":
return str(start.year)
return start.strftime("%Y/%m")