Files
ERP/monitoring/grafana/dashboards/bot-observability.json
T
whqreza cb621ca5d0 feat(monitoring): multi-model dashboard v2.1 — tabs, per-model records, voice playback + MinIO dual-write
Problem: the dashboard was Jev-centric; NLU/arbiter/fallback DeepSeek, STT
providers and the audio-enhancer were barely visible, and stored texts/voices
had no real access path.

Bot (additive; reply path untouched):
- nlu.result now records latency_ms + the DeepSeek usage tokens
- matching.arbitration/fallback.result fill the model + latency_ms columns
  (payload unchanged; SQL coalesces column/payload so old rows still count)
- storage.py: MinIO dual-write layer — every voice (raw + denoised) and user
  attachment is mirrored fire-and-forget into bucket bot-media; the blocking
  minio SDK runs via asyncio.to_thread (senior-engineer review) so the event
  loop is never stalled; empty MINIO_* env = layer off; bucket auto-created
- requirements: minio>=7.2

Backend (admin-gated, read-only):
- GET /bot-metrics/models — one record per model (jev, deepseek-nlu,
  deepseek-arbiter, deepseek-fallback, audio-enhancer + dynamic per-provider
  STT) with calls/ok-rate/p50/p95/tokens/last-seen + version-drift vs the pin
- GET /bot-metrics/models/{id}/daily — per-model daily calls/fails/tokens/p50
- GET /bot-metrics/stages — every stage tagged with its Persian pipeline
  section (صوت/درک پیام/تطبیق/ابهام و انتخاب/پیش‌نویس)
- /traces now carries input_kind + a 120-char text excerpt (consensus →
  input.text → first STT transcript)
- media serving: POST /media-url mints a short-lived HMAC URL (audio/a tags
  cannot send Authorization); GET /media streams from MinIO bot-media with
  the read-only ./bot/data mount as fallback for pre-MinIO files; hard ref
  validation (voices|attachments only, no .., extension whitelist)
- router exempted from the global Bearer dependency (files.py pattern) —
  every endpoint keeps its own require_role('admin')

Frontend /bot-monitor (RTL, recharts):
- four tabs: نمای کلی / مدل‌ها (records table + per-model expandable daily
  charts: calls/fails, tokens, p50) / بخش‌ها (sections grouped) / پیام‌ها
- message drawer rebuilt: inline <audio controls> for raw + denoised voice,
  download links, every STT transcript + consensus side-by-side, user
  attachments, then the event timeline
- terminology: «ترس» → «پرونده‌ی پیام» with tooltips + a one-line legend

Grafana: bot-observability gains a «کارکرد مدل‌ها» table (stage→model
mapping for legacy rows); the explorer gains a message text column.

Seed: real (tiny) ogg files written + mirrored to MinIO, NLU usage tokens,
column-level model/latency on matching events.

Verified locally: /models returns all 7 records incl. per-STT and NLU tokens;
media 200 via signed URL (MinIO) and 401 without; ../ and .py rejected (400);
disk fallback serves a legacy file; pm-role 403 on every new endpoint;
bucket auto-created with 14 mirrored objects; tsc + vite build clean;
Grafana re-provisions 24 panels.
2026-10-25 22:02:40 +03:30

662 lines
23 KiB
JSON
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.
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 1,
"refresh": "5m",
"schemaVersion": 41,
"tags": [
"erp",
"bot"
],
"time": {
"from": "now-7d",
"to": "now"
},
"timezone": "browser",
"title": "بات — Observability خط لوله",
"uid": "bot-observability",
"version": 1,
"panels": [
{
"type": "row",
"title": "نمای کلی",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 100
},
{
"type": "stat",
"title": "روندها (ترافیک واقعی)",
"id": 1,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 0,
"y": 1
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT count(*) AS value FROM bot_traces WHERE coalesce(source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(created_at)"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"title": "رخدادها",
"id": 2,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 4,
"y": 1
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT count(*) AS value FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at)"
}
]
},
{
"type": "stat",
"title": "خطای Jev / فالبک",
"id": 3,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 8,
"y": 1
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT count(*) AS value FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE (e.stage='jev.failure' OR (e.stage='jev.result' AND NOT e.ok) OR e.stage LIKE 'matching.%.failure') AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at)"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"title": "p95 تأخیر Jev (ms)",
"id": 4,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 12,
"y": 1
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY e.latency_ms)),0) AS value FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='jev.result' AND e.latency_ms IS NOT NULL AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at)"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms"
},
"overrides": []
}
},
{
"type": "stat",
"title": "ثبت نهایی پیش‌نویس",
"id": 5,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 16,
"y": 1
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT count(*) AS value FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='draft.commit' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at)"
}
]
},
{
"type": "stat",
"title": "رخداد این ماه (گارد آرشیو ۱۰۰k)",
"id": 6,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 20,
"y": 1
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT count(*) AS value FROM bot_trace_events WHERE created_at >= date_trunc('month', now())"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 50000
},
{
"color": "red",
"value": 100000
}
]
}
},
"overrides": []
}
},
{
"type": "row",
"title": "حجم و تأخیر",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 5
},
"id": 101
},
{
"type": "timeseries",
"title": "رخداد در روز (ترافیک واقعی)",
"id": 10,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 6
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT $__timeGroupAlias(e.created_at,'1d'), count(*) AS value FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1 ORDER BY 1"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1
}
},
"overrides": []
}
},
{
"type": "timeseries",
"title": "تأخیر p50/p95 به تفکیک مرحله",
"id": 11,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 6
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT $__timeGroupAlias(e.created_at,'1h'), round(percentile_cont(0.5) WITHIN GROUP (ORDER BY e.latency_ms)) AS p50, round(percentile_cont(0.95) WITHIN GROUP (ORDER BY e.latency_ms)) AS p95, e.stage AS metric FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('jev.result','nlu.result','stt.result','audio.enhance.result','matching.arbitration.result') AND e.latency_ms IS NOT NULL AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1, e.stage ORDER BY 1"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms"
},
"overrides": []
}
},
{
"type": "row",
"title": "تطبیق موجودیت (Jev + داور DeepSeek)",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 14
},
"id": 102
},
{
"type": "piechart",
"title": "وضعیت نهایی mentionها (exact/ambiguity/new)",
"id": 20,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 15
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT 'exact' AS metric, sum(coalesce((e.payload->'statuses'->>'exact')::int,0)) AS value FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('matching.arbitration.result','matching.fallback.result') AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) UNION ALL SELECT 'ambiguity', sum(coalesce((e.payload->'statuses'->>'ambiguity')::int,0)) FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('matching.arbitration.result','matching.fallback.result') AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) UNION ALL SELECT 'new', sum(coalesce((e.payload->'statuses'->>'new')::int,0)) FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('matching.arbitration.result','matching.fallback.result') AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) UNION ALL SELECT m->>'status', count(*) FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id, jsonb_array_elements(e.payload->'mentions') m WHERE e.stage='routing.decision' AND e.payload->>'source'='jev' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1"
}
],
"fieldConfig": {
"defaults": {},
"overrides": []
},
"options": {
"pieType": "donut",
"legend": {
"displayMode": "table",
"placement": "right"
}
}
},
{
"type": "table",
"title": "اقدامات داور DeepSeek روی پیشنهاد Jev",
"id": 21,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 15
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT coalesce(e.payload->'actions'->>'endorsed_jev','0')::int AS \"تأیید Jev\", coalesce(e.payload->'actions'->>'vetoed_to_new','0')::int AS \"وتو → جدید\", coalesce(e.payload->'actions'->>'demoted_to_ambiguity','0')::int AS \"تنزل → ابهام\", coalesce(e.payload->'actions'->>'other','0')::int AS \"سایر\", count(*) OVER () AS \"فراخوانی‌ها\" FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='matching.arbitration.result' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at)"
}
],
"fieldConfig": {
"defaults": {},
"overrides": []
}
},
{
"type": "timeseries",
"title": "توکن مصرفی در زمان (ورودی/خروجی)",
"id": 22,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 15
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT $__timeGroupAlias(e.created_at,'1d'), sum(coalesce((e.payload->'usage'->>'input_tokens')::bigint,0)) AS input, sum(coalesce((e.payload->'usage'->>'output_tokens')::bigint,0)) AS output FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('jev.result','matching.arbitration.result','matching.fallback.result') AND e.payload->'usage' IS NOT NULL AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1 ORDER BY 1"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 50
}
},
"overrides": []
}
},
{
"type": "table",
"title": "نسخه مدل Jev دیده‌شده (پین: jev-1.13.0)",
"id": 23,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 6,
"w": 8,
"x": 0,
"y": 23
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT coalesce(e.model,'?') AS \"مدل\", count(*) AS \"تعداد\", max(e.created_at)::text AS \"آخرین بار\" FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='jev.result' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1 ORDER BY 2 DESC"
}
]
},
{
"type": "timeseries",
"title": "فراخوانی مراحل تطبیق (Jev/داور/فالبک)",
"id": 24,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 6,
"w": 8,
"x": 8,
"y": 23
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT $__timeGroupAlias(e.created_at,'1d'), count(*) AS value, e.stage AS metric FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('jev.request','matching.arbitration.result','matching.fallback.result') AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1, e.stage ORDER BY 1"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 40,
"stacking": {
"mode": "normal"
}
}
},
"overrides": []
}
},
{
"type": "stat",
"title": "حجم Observability (گارد آرشیو ۵GB)",
"id": 25,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 6,
"w": 8,
"x": 16,
"y": 23
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT (pg_total_relation_size('bot_trace_events') + pg_total_relation_size('bot_traces')) AS value"
}
],
"fieldConfig": {
"defaults": {
"unit": "bytes",
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 3000000000
},
{
"color": "red",
"value": 5000000000
}
]
}
},
"overrides": []
}
},
{
"type": "row",
"title": "کیفیت و ورودی",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 29
},
"id": 103
},
{
"type": "table",
"title": "انتخاب کاربر (A/B واقعی)",
"id": 30,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 30
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT payload->>'action' AS \"اقدام\", coalesce(payload->>'arg','-') AS \"آرگومان\", count(*) AS \"تعداد\" FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='ambiguity.user_choice' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1,2 UNION ALL SELECT CASE WHEN (payload->>'idx')::int = 0 THEN 'انتخاب: گزینه اول (پیشنهاد مدل)' ELSE 'انتخاب: گزینه غیراول' END, '-', count(*) FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='user_choice.pick' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1"
}
]
},
{
"type": "table",
"title": "سلامت STT به تفکیک پرووایدر",
"id": 31,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 30
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT coalesce(e.payload->>'provider','?') AS \"پرووایدر\", count(*) FILTER (WHERE e.ok) AS \"موفق\", count(*) AS \"کل\", round(100.0*count(*) FILTER (WHERE e.ok)/greatest(count(*),1),1)::text || '%' AS \"نرخ موفقیت\", coalesce(round(percentile_cont(0.5) WITHIN GROUP (ORDER BY e.latency_ms))::text,'—') AS \"p50 (ms)\" FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='stt.result' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1 ORDER BY 2 DESC"
}
]
},
{
"type": "timeseries",
"title": "پیش‌پردازش صدا (audio.enhance)",
"id": 32,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 30
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT $__timeGroupAlias(e.created_at,'1d'), count(*) FILTER (WHERE e.ok) AS value, 'موفق' AS metric FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='audio.enhance.result' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1 UNION ALL SELECT $__timeGroupAlias(e.created_at,'1d'), count(*) FILTER (WHERE NOT e.ok), 'ناموفق (بازگشت به صدای خام)' FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage='audio.enhance.result' AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 50,
"stacking": {
"mode": "normal"
}
}
},
"overrides": []
}
},
{
"type": "table",
"title": "چرخه‌ی پیش‌نویس",
"id": 33,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 5,
"w": 24,
"x": 0,
"y": 38
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT e.stage AS \"مرحله\", count(*) AS \"تعداد\" FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE e.stage IN ('draft.render','draft.commit','draft.cancel','ambiguity.sent') AND coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) GROUP BY 1 ORDER BY 2 DESC"
}
]
},
{
"type": "row",
"title": "کارکرد مدل‌ها",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 45
},
"id": 110
},
{
"type": "table",
"title": "رکورد عملکرد مدل‌ها (فراخوانی/موفق/p95/توکن)",
"id": 111,
"datasource": {
"type": "postgres",
"uid": "bot-postgres"
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 46
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT 'Jev (تطبیق)' AS metric, count(*)::int AS value0, count(*) FILTER (WHERE e.ok)::int AS ok, coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY coalesce(e.latency_ms, nullif(e.payload->>'latency_ms','')::int))),0)::int AS p95, coalesce(sum((e.payload->'usage'->>'input_tokens')::bigint),0)::bigint AS tin, coalesce(sum((e.payload->'usage'->>'output_tokens')::bigint),0)::bigint AS tout FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) AND e.stage = 'jev.result' UNION ALL SELECT 'داور DeepSeek' AS metric, count(*)::int AS value0, count(*) FILTER (WHERE e.ok)::int AS ok, coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY coalesce(e.latency_ms, nullif(e.payload->>'latency_ms','')::int))),0)::int AS p95, coalesce(sum((e.payload->'usage'->>'input_tokens')::bigint),0)::bigint AS tin, coalesce(sum((e.payload->'usage'->>'output_tokens')::bigint),0)::bigint AS tout FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) AND e.stage = 'matching.arbitration.result' UNION ALL SELECT 'فالبک DeepSeek' AS metric, count(*)::int AS value0, count(*) FILTER (WHERE e.ok)::int AS ok, coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY coalesce(e.latency_ms, nullif(e.payload->>'latency_ms','')::int))),0)::int AS p95, coalesce(sum((e.payload->'usage'->>'input_tokens')::bigint),0)::bigint AS tin, coalesce(sum((e.payload->'usage'->>'output_tokens')::bigint),0)::bigint AS tout FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) AND e.stage = 'matching.fallback.result' UNION ALL SELECT 'NLU (DeepSeek)' AS metric, count(*)::int AS value0, count(*) FILTER (WHERE e.ok)::int AS ok, coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY coalesce(e.latency_ms, nullif(e.payload->>'latency_ms','')::int))),0)::int AS p95, coalesce(sum((e.payload->'usage'->>'input_tokens')::bigint),0)::bigint AS tin, coalesce(sum((e.payload->'usage'->>'output_tokens')::bigint),0)::bigint AS tout FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) AND e.stage = 'nlu.result' UNION ALL SELECT 'پیش‌پردازش صدا' AS metric, count(*)::int AS value0, count(*) FILTER (WHERE e.ok)::int AS ok, coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY coalesce(e.latency_ms, nullif(e.payload->>'latency_ms','')::int))),0)::int AS p95, coalesce(sum((e.payload->'usage'->>'input_tokens')::bigint),0)::bigint AS tin, coalesce(sum((e.payload->'usage'->>'output_tokens')::bigint),0)::bigint AS tout FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) AND e.stage = 'audio.enhance.result' UNION ALL SELECT coalesce(e.model, e.payload->>'provider','stt') || ' (STT)' AS metric, count(*)::int, count(*) FILTER (WHERE e.ok)::int, coalesce(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY coalesce(e.latency_ms, nullif(e.payload->>'latency_ms','')::int))),0)::int, 0::bigint, 0::bigint FROM bot_trace_events e JOIN bot_traces t ON e.trace_id=t.id WHERE coalesce(t.source,'bale') NOT IN ('calibration','demo') AND $__timeFilter(e.created_at) AND e.stage='stt.result' GROUP BY 1 ORDER BY 2 DESC"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto"
}
},
"overrides": []
}
}
],
"templating": {
"list": []
}
}