Files
cowork-local/application/monitoring/dashboard_query_service.py
T
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

121 lines
5.4 KiB
Python

"""DashboardQueryService - read-only usage/cost queries for the Dashboard
screen (R08-T13, extracted from ``ui/dashboard_tab.py::DashboardTab``, lines
196-199/256-261/284-322 of the original 437-line file: ``_pricing``,
``_period_range``'s date-math, and the ``period_totals``/``period_breakdown``
calls ``_refresh_chart`` made directly).
``ui/dashboard_tab.py`` called ``core/usage_tracker.py``/``core/model_
pricing.py`` directly from FIVE different methods spread across what is now
three widgets (``token_usage_card_widget.py``, ``usage_chart_widget.py``,
``habits_widget.py``) — each recomputing the same merged pricing dict. This
service is the one place that merge happens now; the three widgets share it
instead of each calling ``core.usage_tracker``/``core.model_pricing`` on
their own.
Pure Python: no Qt. Wraps ``core/usage_tracker.py`` (a plain-Python module
already) rather than reimplementing any of its date/cost math.
"""
from __future__ import annotations
from datetime import date, timedelta
from typing import Any, Dict, List, Tuple
class DashboardQueryService:
"""Usage/cost queries scoped to one ``AppContext``.
Args:
ctx: ``AppContext`` — read for ``ctx.config`` (pricing table,
currency, budget) and nothing else; this class does no I/O of
its own beyond what ``core.usage_tracker`` already does.
directory: Optional custom usage directory. If None, uses default USAGE_DIR.
"""
def __init__(self, ctx: Any, directory: Optional[Path] = None) -> None:
"""``directory`` để None thì đọc thư mục telemetry mặc định; test trỏ nó vào
``tmp_path`` để không chạm dữ liệu thật.
"""
self.ctx = ctx
self._directory = directory
def pricing(self) -> Dict[str, Any]:
"""The merged price table (defaults + user overrides), synced from
Monitoring's model-pricing table first so cost figures always agree
between the two screens."""
from cowork_local.core import model_pricing as mp
from cowork_local.core import usage_tracker as ut
mp.sync_to_usage(self.ctx.config)
return {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})}
def period_range(self, granularity: str, offset: int) -> Tuple[date, date]:
"""The SELECTED period as an inclusive ``(start, end)`` date range —
drives every widget on the screen (cards, chart, habits)."""
from cowork_local.core import usage_tracker as ut
start, end = ut.period_bounds(granularity, offset)
return start, end - timedelta(days=1) # load_events end is inclusive
def summary(self, start: date, end: date) -> Dict[str, Any]:
"""Everything the stat cards + habits panel need for one period:
the raw events, ``usage_tracker.summarize``'s aggregate stats, the
per-bucket costs, and their total — computed once so both widgets
read the same numbers instead of loading events twice."""
from cowork_local.core import usage_tracker as ut
events = ut.load_events(start, end, directory=self._directory)
pricing = self.pricing()
stats = ut.summarize(events)
costs = ut.cost_usd_events(events, pricing)
return {
"events": events,
"pricing": pricing,
"stats": stats,
"costs": costs,
"total_cost": sum(costs.values()),
}
def chart_series(self, granularity: str, offset: int, metric: str
) -> List[Tuple[str, float]]:
"""``(label, value)`` points for the spline chart — WEEK -> 7 days,
MONTH -> weeks, YEAR -> 12 months, in whichever ``metric``
("tokens" | "cost") was selected."""
from cowork_local.core import usage_tracker as ut
events = ut.load_events(directory=self._directory) # all events; breakdown slices by period
pricing = self.pricing()
parts = ut.period_breakdown(events, granularity, pricing, offset=offset)
mi = 0 if metric == "tokens" else 1 # (label, tokens, cost) -> +1 for the value
return [(row[0], float(row[mi + 1])) for row in parts]
def period_totals(self, granularity: str, offset: int) -> Tuple[float, float]:
"""``(tokens, cost)`` totals for one period — used to compute the
vs-previous-period delta the chart's reference line shows."""
from cowork_local.core import usage_tracker as ut
events = ut.load_events(directory=self._directory)
return ut.period_totals(events, granularity, self.pricing(), offset)
def period_range_label(self, granularity: str, offset: int) -> str:
"""Nhãn hiển thị của một kỳ (tuần/tháng/năm cộng độ lệch)."""
from cowork_local.core import usage_tracker as ut
return ut.period_range_label(granularity, offset)
def budget_status(self):
"""Tình trạng ngân sách: đã dùng bao nhiêu, còn lại bao nhiêu, có vượt ngưỡng chưa."""
from cowork_local.core import usage_tracker as ut
return ut.budget_status(self.ctx.config)
def set_budget(self, amount: float, currency: str) -> None:
"""Đặt hạn mức ngân sách mới — mở một chu kỳ đếm mới, chi tiêu trước đó không
còn được tính vào.
"""
from cowork_local.core import usage_tracker as ut
ut.set_budget(self.ctx.config, amount, currency)
__all__ = ["DashboardQueryService"]