-
Notifications
You must be signed in to change notification settings - Fork 10
Discord: Automate Stat Messages #1012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Prajna1999
wants to merge
15
commits into
main
Choose a base branch
from
feat/automate-stats-messages-basic-queries
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+451
−1
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
e222617
feat: basic stats queries
Prajna1999 db92ae6
feat: routes for health_probe
Prajna1999 53ba5ed
chore: cleanup
Prajna1999 e228b43
Merge branch 'main' into feat/automate-stats-messages-basic-queries
Prajna1999 034b501
yolo
Prajna1999 bcc57ea
add cron for daily-stats
Prajna1999 f322b37
Merge branch 'main' into feat/automate-stats-messages-basic-queries
Ayush8923 ff8dd33
Merge branch 'main' into feat/automate-stats-messages-basic-queries
Ayush8923 48f96b9
Merge branch 'main' into feat/automate-stats-messages-basic-queries
Prajna1999 8a0186e
Merge branch 'main' into feat/automate-stats-messages-basic-queries
Prajna1999 ca35534
fix comments
Prajna1999 819d149
Merge branch 'main' into feat/automate-stats-messages-basic-queries
Prajna1999 ac11c46
simplify SQL queries and add last 24 hours stats
Prajna1999 bf5baff
one tiny senior engineer instruction and ost request raises
Prajna1999 24407b9
fix codecoverage
Prajna1999 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| from typing import Any | ||
|
|
||
| from sqlalchemy import text | ||
| from sqlmodel import Session | ||
|
|
||
| # Every query reports two rolling windows in one pass: the last 24 hours and the | ||
| # last 7 days (168 hours), broken down per organization and per project. The | ||
| # 24h FILTER count is a subset of the rows the 7d WHERE already selected. | ||
|
|
||
| LLM_CALLS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| COUNT(*) FILTER (WHERE l.inserted_at >= now() - INTERVAL '24 hours') AS calls_24h, | ||
| COUNT(*) AS calls_7d | ||
| FROM llm_call l | ||
| INNER JOIN organization o ON l.organization_id = o.id | ||
| INNER JOIN project p ON l.project_id = p.id | ||
| WHERE l.inserted_at >= now() - INTERVAL '168 hours' | ||
| AND l.deleted_at IS NULL | ||
| GROUP BY o.name, p.name | ||
| ORDER BY calls_7d DESC | ||
| """ | ||
|
|
||
| LLM_TOKENS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| l.model AS model, | ||
| COALESCE(SUM((l.usage->>'total_tokens')::INTEGER) | ||
| FILTER (WHERE l.inserted_at >= now() - INTERVAL '24 hours'), 0) AS tokens_24h, | ||
| COALESCE(SUM((l.usage->>'total_tokens')::INTEGER), 0) AS tokens_7d | ||
| FROM llm_call l | ||
| INNER JOIN organization o ON l.organization_id = o.id | ||
| INNER JOIN project p ON l.project_id = p.id | ||
| WHERE l.inserted_at >= now() - INTERVAL '168 hours' | ||
| AND l.deleted_at IS NULL | ||
| GROUP BY o.name, p.name, l.model | ||
| ORDER BY tokens_7d DESC | ||
| """ | ||
|
|
||
| LLM_MODALITY = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| CASE | ||
| WHEN l.input_type = 'text' AND l.output_type = 'text' THEN 'TEXT' | ||
| WHEN l.input_type = 'audio' AND l.output_type = 'text' THEN 'STT' | ||
| WHEN l.input_type = 'text' AND l.output_type = 'audio' THEN 'TTS' | ||
| ELSE 'OTHER' | ||
| END AS modality, | ||
| COUNT(*) FILTER (WHERE l.inserted_at >= now() - INTERVAL '24 hours') AS calls_24h, | ||
| COUNT(*) AS calls_7d | ||
| FROM llm_call l | ||
| INNER JOIN organization o ON l.organization_id = o.id | ||
| INNER JOIN project p ON l.project_id = p.id | ||
| WHERE l.inserted_at >= now() - INTERVAL '168 hours' | ||
| AND l.deleted_at IS NULL | ||
| GROUP BY o.name, p.name, modality | ||
| ORDER BY o.name, p.name, modality | ||
| """ | ||
|
|
||
| JOBS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| j.job_type AS job_type, | ||
| COUNT(*) FILTER (WHERE j.inserted_at >= now() - INTERVAL '24 hours') AS jobs_24h, | ||
| COUNT(*) AS jobs_7d | ||
| FROM job j | ||
| INNER JOIN project p ON j.project_id = p.id | ||
| INNER JOIN organization o ON p.organization_id = o.id | ||
| WHERE j.inserted_at >= now() - INTERVAL '168 hours' | ||
| GROUP BY o.name, p.name, j.job_type | ||
| ORDER BY o.name, p.name, j.job_type | ||
| """ | ||
|
|
||
| EVALUATION_RUNS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| COUNT(*) FILTER (WHERE t.inserted_at >= now() - INTERVAL '24 hours') AS count_24h, | ||
| COUNT(*) AS count_7d | ||
| FROM evaluation_run t | ||
| INNER JOIN organization o ON t.organization_id = o.id | ||
| INNER JOIN project p ON t.project_id = p.id | ||
| WHERE t.inserted_at >= now() - INTERVAL '168 hours' | ||
| GROUP BY o.name, p.name | ||
| ORDER BY count_7d DESC | ||
| """ | ||
|
|
||
| STT_RESULTS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| COUNT(*) FILTER (WHERE t.inserted_at >= now() - INTERVAL '24 hours') AS count_24h, | ||
| COUNT(*) AS count_7d | ||
| FROM stt_result t | ||
| INNER JOIN organization o ON t.organization_id = o.id | ||
| INNER JOIN project p ON t.project_id = p.id | ||
| WHERE t.inserted_at >= now() - INTERVAL '168 hours' | ||
| GROUP BY o.name, p.name | ||
| ORDER BY count_7d DESC | ||
| """ | ||
|
|
||
| TTS_RESULTS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| COUNT(*) FILTER (WHERE t.inserted_at >= now() - INTERVAL '24 hours') AS count_24h, | ||
| COUNT(*) AS count_7d | ||
| FROM tts_result t | ||
| INNER JOIN organization o ON t.organization_id = o.id | ||
| INNER JOIN project p ON t.project_id = p.id | ||
| WHERE t.inserted_at >= now() - INTERVAL '168 hours' | ||
| GROUP BY o.name, p.name | ||
| ORDER BY count_7d DESC | ||
| """ | ||
|
|
||
| ASSESSMENTS = """ | ||
| SELECT | ||
| o.name AS organization, | ||
| p.name AS project, | ||
| COUNT(*) FILTER (WHERE t.inserted_at >= now() - INTERVAL '24 hours') AS count_24h, | ||
| COUNT(*) AS count_7d | ||
| FROM assessment t | ||
| INNER JOIN organization o ON t.organization_id = o.id | ||
| INNER JOIN project p ON t.project_id = p.id | ||
| WHERE t.inserted_at >= now() - INTERVAL '168 hours' | ||
| GROUP BY o.name, p.name | ||
| ORDER BY count_7d DESC | ||
| """ | ||
|
|
||
|
|
||
| def _rows(session: Session, sql: str) -> list[dict[str, Any]]: | ||
| result = session.connection().execute(text(sql)) | ||
| return [dict(row) for row in result.mappings().all()] | ||
|
|
||
|
|
||
| def get_daily_stats(*, session: Session) -> dict[str, list[dict[str, Any]]]: | ||
| stats: dict[str, list[dict[str, Any]]] = {} | ||
| stats["LLM Calls"] = _rows(session, LLM_CALLS) | ||
| stats["LLM Tokens"] = _rows(session, LLM_TOKENS) | ||
| stats["LLM Modality"] = _rows(session, LLM_MODALITY) | ||
| stats["Jobs by Type"] = _rows(session, JOBS) | ||
| stats["Evaluation Runs"] = _rows(session, EVALUATION_RUNS) | ||
| stats["STT Results"] = _rows(session, STT_RESULTS) | ||
| stats["TTS Results"] = _rows(session, TTS_RESULTS) | ||
| stats["Assessments"] = _rows(session, ASSESSMENTS) | ||
| return stats |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import logging | ||
| from typing import Any | ||
|
|
||
| import requests | ||
|
|
||
| from app.core.config import settings | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| DISCORD_LIMIT = 1900 # Discord caps message content at 2000; leave headroom. | ||
| MAX_COL_WIDTH = 18 # Cap each column so wide tables don't wrap in Discord. | ||
|
|
||
|
|
||
| def _clip(text: str) -> str: | ||
| if len(text) <= MAX_COL_WIDTH: | ||
| return text | ||
| return text[: MAX_COL_WIDTH - 1] + "…" | ||
|
|
||
|
|
||
| def format_sections(stats: dict[str, list[dict[str, Any]]]) -> list[str]: | ||
| sections: list[str] = [] | ||
| for title, rows in stats.items(): | ||
| if not rows: | ||
| sections.append(f"**{title}**\n_no data_") | ||
| continue | ||
|
|
||
| columns = list(rows[0].keys()) | ||
|
|
||
| # Numeric columns get thousands separators and are right-aligned so they | ||
| # read as a clean column; text columns stay left-aligned. | ||
| numeric = { | ||
| c: all(isinstance(row[c], (int, float)) for row in rows) for c in columns | ||
| } | ||
|
|
||
| def cell(column: str, row: dict[str, Any]) -> str: | ||
| value = row[column] | ||
| text = f"{value:,}" if numeric[column] else str(value) | ||
| return _clip(text) | ||
|
|
||
| widths = {} | ||
| for column in columns: | ||
| cell_lengths = [len(cell(column, row)) for row in rows] | ||
| widths[column] = max(len(_clip(column)), max(cell_lengths)) | ||
|
|
||
| def align(text: str, column: str) -> str: | ||
| width = widths[column] | ||
| return text.rjust(width) if numeric[column] else text.ljust(width) | ||
|
|
||
| header = " ".join(align(_clip(column), column) for column in columns) | ||
| lines = [header.rstrip()] | ||
| for row in rows: | ||
| line = " ".join(align(cell(column, row), column) for column in columns) | ||
| lines.append(line.rstrip()) | ||
|
|
||
| table = "\n".join(lines) | ||
| sections.append(f"**{title}**\n```\n{table}\n```") | ||
| return sections | ||
|
|
||
|
|
||
| def post_to_discord(sections: list[str]) -> None: | ||
| url = settings.DISCORD_STATS_WEBHOOK_URL | ||
| if not url: | ||
| return | ||
|
|
||
| # Pack whole sections into messages under Discord's size cap so no code | ||
| # block is split across two posts. | ||
| chunk = "Daily Stats · last 24h and 7d (UTC)" | ||
| for section in sections: | ||
| if len(chunk) + len(section) + 2 > DISCORD_LIMIT: | ||
| _post(str(url), chunk) | ||
| chunk = "" | ||
| chunk = f"{chunk}\n\n{section}" if chunk else section | ||
| if chunk: | ||
| _post(str(url), chunk) | ||
|
|
||
|
|
||
| def _post(url: str, content: str) -> None: | ||
| try: | ||
| response = requests.post(url, json={"content": content}, timeout=5) | ||
| response.raise_for_status() | ||
| except requests.RequestException as e: | ||
| # Log only the exception type — the message can contain the webhook URL. | ||
| logger.warning(f"[_post] Webhook post failed: {type(e).__name__}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.