witdem-analytics 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. witdem/__init__.py +8 -0
  2. witdem/adapters/__init__.py +13 -0
  3. witdem/adapters/base.py +31 -0
  4. witdem/adapters/haystack_adapter.py +59 -0
  5. witdem/adapters/providers.py +93 -0
  6. witdem/adapters/registry.py +51 -0
  7. witdem/analytics/__init__.py +74 -0
  8. witdem/analytics/contracts/__init__.py +31 -0
  9. witdem/analytics/contracts/read_models.py +252 -0
  10. witdem/analytics/core.py +108 -0
  11. witdem/analytics/cost.py +533 -0
  12. witdem/analytics/derived.py +104 -0
  13. witdem/analytics/identity.py +319 -0
  14. witdem/analytics/operations.py +442 -0
  15. witdem/analytics/queries/entities/models.sql +10 -0
  16. witdem/analytics/queries/entities/provider_values.sql +4 -0
  17. witdem/analytics/queries/entities/providers.sql +9 -0
  18. witdem/analytics/queries/execution/business_outcome.sql +5 -0
  19. witdem/analytics/queries/execution/evaluations_for_execution.sql +4 -0
  20. witdem/analytics/queries/execution/events_for_execution.sql +4 -0
  21. witdem/analytics/queries/execution/execution_detail.sql +3 -0
  22. witdem/analytics/queries/execution/execution_population.sql +4 -0
  23. witdem/analytics/queries/execution/execution_timeline.sql +4 -0
  24. witdem/analytics/queries/execution/links_for_execution.sql +3 -0
  25. witdem/analytics/queries/execution/outcomes_for_execution.sql +4 -0
  26. witdem/analytics/queries/execution/product_goal.sql +5 -0
  27. witdem/analytics/queries/execution/runtime_outcome.sql +8 -0
  28. witdem/analytics/queries/failures/failure_patterns.sql +11 -0
  29. witdem/analytics/queries/overview/cost_summary.sql +8 -0
  30. witdem/analytics/queries/overview/execution_health.sql +31 -0
  31. witdem/analytics/queries/overview/performance.sql +9 -0
  32. witdem/analytics/queries/overview/product_goals.sql +4 -0
  33. witdem/analytics/queries/overview/success_metrics.sql +11 -0
  34. witdem/analytics/queries/paths/loops.sql +4 -0
  35. witdem/analytics/queries/paths/path_frequency.sql +3 -0
  36. witdem/analytics/queries/shared/capabilities.sql +12 -0
  37. witdem/analytics/queries/shared/evaluations_count.sql +2 -0
  38. witdem/analytics/queries/shared/events_capabilities.sql +4 -0
  39. witdem/analytics/queries/shared/filter_completed.sql +7 -0
  40. witdem/analytics/queries/shared/filter_end_date.sql +1 -0
  41. witdem/analytics/queries/shared/filter_failed.sql +7 -0
  42. witdem/analytics/queries/shared/filter_has_failure.sql +6 -0
  43. witdem/analytics/queries/shared/filter_provider.sql +6 -0
  44. witdem/analytics/queries/shared/filter_running.sql +1 -0
  45. witdem/analytics/queries/shared/filter_start_date.sql +1 -0
  46. witdem/analytics/queries/shared/metadata.sql +1 -0
  47. witdem/analytics/queries/shared/outcomes_count.sql +2 -0
  48. witdem/analytics/read_model.py +360 -0
  49. witdem/analytics/repository/__init__.py +6 -0
  50. witdem/analytics/repository/analytics_repository.py +3429 -0
  51. witdem/analytics/repository/backend.py +51 -0
  52. witdem/analytics/repository/sql_loader.py +44 -0
  53. witdem/analytics/repository/state.py +55 -0
  54. witdem/analytics/runtime.py +1311 -0
  55. witdem/analytics/schema/analytics_tables.sql +116 -0
  56. witdem/analytics/schema.py +108 -0
  57. witdem/analytics/serving.py +477 -0
  58. witdem/api.py +145 -0
  59. witdem/auth.py +64 -0
  60. witdem/cli.py +728 -0
  61. witdem/config.py +88 -0
  62. witdem/dashboard/__init__.py +5 -0
  63. witdem/dashboard/app.py +216 -0
  64. witdem/dashboard/service.py +952 -0
  65. witdem/dashboard/static/assets/echarts-runtime-mIDt9UdM.js +19 -0
  66. witdem/dashboard/static/assets/index-B2TO5dg7.css +1 -0
  67. witdem/dashboard/static/assets/index-DnxvhzHV.js +56 -0
  68. witdem/dashboard/static/assets/witdem-mark-purple-B7xvtfh0.png +0 -0
  69. witdem/dashboard/static/index.html +3 -0
  70. witdem/elt/__init__.py +7 -0
  71. witdem/elt/adapter_stage.py +235 -0
  72. witdem/elt/publisher.py +43 -0
  73. witdem/elt/worker.py +205 -0
  74. witdem/elt/workspace/pipelines/normalize.pipeline.json +59 -0
  75. witdem/evaluation_campaigns.py +108 -0
  76. witdem/ingest/__init__.py +1 -0
  77. witdem/ingest/corpus.py +223 -0
  78. witdem/ingest/correlate.py +374 -0
  79. witdem/ingest/live_db.py +1239 -0
  80. witdem/ingest/otlp_http.py +249 -0
  81. witdem/ingest/raw_store.py +195 -0
  82. witdem/ingest/sdk_ingest.py +183 -0
  83. witdem/ingest/sdk_store.py +172 -0
  84. witdem/integrations/__init__.py +12 -0
  85. witdem/integrations/adapters/__init__.py +14 -0
  86. witdem/integrations/adapters/claude.py +158 -0
  87. witdem/integrations/adapters/langchain.py +173 -0
  88. witdem/integrations/adapters/langgraph.py +117 -0
  89. witdem/integrations/adapters/openai_agents.py +294 -0
  90. witdem/integrations/adapters/otel.py +34 -0
  91. witdem/integrations/mapping.py +266 -0
  92. witdem/integrations/models/__init__.py +4 -0
  93. witdem/integrations/models/normalized_operation.py +42 -0
  94. witdem/integrations/models/normalized_span.py +60 -0
  95. witdem/integrations/normalizers/__init__.py +5 -0
  96. witdem/integrations/normalizers/genai.py +157 -0
  97. witdem/integrations/normalizers/openinference.py +149 -0
  98. witdem/integrations/normalizers/otel.py +244 -0
  99. witdem/lifecycle.py +316 -0
  100. witdem/pricing/__init__.py +10 -0
  101. witdem/pricing/catalog.yaml +1004 -0
  102. witdem/pricing/sources.yaml +186 -0
  103. witdem/pricing/update.py +187 -0
  104. witdem/protocol.py +7 -0
  105. witdem/py.typed +1 -0
  106. witdem/retention.py +162 -0
  107. witdem/telemetry/__init__.py +5 -0
  108. witdem/telemetry/otel.py +193 -0
  109. witdem/update.py +212 -0
  110. witdem/workflows.py +782 -0
  111. witdem_analytics-0.1.0.dist-info/METADATA +287 -0
  112. witdem_analytics-0.1.0.dist-info/RECORD +115 -0
  113. witdem_analytics-0.1.0.dist-info/WHEEL +4 -0
  114. witdem_analytics-0.1.0.dist-info/entry_points.txt +2 -0
  115. witdem_analytics-0.1.0.dist-info/licenses/LICENSE +201 -0
witdem/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Witdem runtime analytics product package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("witdem-analytics")
7
+ except PackageNotFoundError: # pragma: no cover - only an unpackaged source tree
8
+ __version__ = "0+unknown"
@@ -0,0 +1,13 @@
1
+ """Runtime adapter boundary: framework-specific span interpretation lives here.
2
+
3
+ The analytics core (``analytics/core.py``, ``analytics/identity.py``,
4
+ ``analytics/runtime.py``) stays framework-neutral. Adapters translate a raw
5
+ runtime's span/attribute conventions into the canonical
6
+ ``Execution``/``Operation``/``Link`` graph.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from witdem.adapters.base import RuntimeAdapter
12
+
13
+ __all__ = ["RuntimeAdapter"]
@@ -0,0 +1,31 @@
1
+ """Public runtime adapter protocol used by built-in and external adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any, Protocol, runtime_checkable
7
+
8
+ from witdem.analytics.runtime import NormalizedExecutionGraph
9
+
10
+
11
+ @runtime_checkable
12
+ class RuntimeAdapter(Protocol):
13
+ """Detects and normalizes one raw-runtime span convention.
14
+
15
+ ``spans`` are raw span envelopes in the same shape
16
+ ``telemetry.otel.JsonlSpanExporter`` produces (trace_id, span_id,
17
+ parent_span_id, name, kind, attributes, status, start_time_unix_nano,
18
+ end_time_unix_nano, events, resource, instrumentation_scope) —
19
+ ``ingest.otlp_http`` reconstructs this same shape from OTLP protobuf.
20
+ """
21
+
22
+ def detect(self, spans: Sequence[Mapping[str, Any]]) -> bool: ...
23
+
24
+ def normalize(
25
+ self,
26
+ spans: Sequence[Mapping[str, Any]],
27
+ *,
28
+ execution_id: str | None = None,
29
+ runtime_id: str | None = None,
30
+ providers: Sequence[Mapping[str, Any]] | None = None,
31
+ ) -> NormalizedExecutionGraph: ...
@@ -0,0 +1,59 @@
1
+ """Haystack/OpenTelemetry runtime adapter.
2
+
3
+ Thin wrapper around the existing, already-tested
4
+ ``analytics.runtime.normalize_haystack_spans`` (see ``docs/architecture.md`` §3).
5
+ All framework-specific *detection* knowledge (attribute/scope naming) lives
6
+ here, isolated behind the ``RuntimeAdapter`` boundary; ``normalize()`` itself
7
+ does not reimplement or modify the 650-line, already-tested normalizer it
8
+ wraps.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Mapping, Sequence
14
+ from typing import Any
15
+
16
+ from witdem.analytics.runtime import NormalizedExecutionGraph, normalize_haystack_spans
17
+
18
+ _HAYSTACK_ATTRIBUTE_PREFIX = "haystack."
19
+ _HAYSTACK_SCOPE_HINTS = ("haystack", "opentelemetry-haystack")
20
+
21
+
22
+ class HaystackAdapter:
23
+ """``RuntimeAdapter`` for Haystack-instrumented OpenTelemetry spans.
24
+
25
+ ``detect()`` looks for the same physical conventions
26
+ ``normalize_haystack_spans`` itself reads span-by-span: any
27
+ ``haystack.*``-prefixed attribute key (e.g. ``haystack.component.name``,
28
+ ``haystack.tool.name``, ``haystack.agent.step``), or an instrumentation
29
+ scope name that mentions "haystack" / "opentelemetry-haystack".
30
+ """
31
+
32
+ def detect(self, spans: Sequence[Mapping[str, Any]]) -> bool:
33
+ for span in spans:
34
+ attributes = span.get("attributes")
35
+ if isinstance(attributes, Mapping) and any(
36
+ str(key).startswith(_HAYSTACK_ATTRIBUTE_PREFIX) for key in attributes
37
+ ):
38
+ return True
39
+ scope = span.get("instrumentation_scope")
40
+ if isinstance(scope, Mapping):
41
+ name = str(scope.get("name") or "").casefold()
42
+ if any(hint in name for hint in _HAYSTACK_SCOPE_HINTS):
43
+ return True
44
+ return False
45
+
46
+ def normalize(
47
+ self,
48
+ spans: Sequence[Mapping[str, Any]],
49
+ *,
50
+ execution_id: str | None = None,
51
+ runtime_id: str | None = None,
52
+ providers: Sequence[Mapping[str, Any]] | None = None,
53
+ ) -> NormalizedExecutionGraph:
54
+ return normalize_haystack_spans(
55
+ spans,
56
+ execution_id=execution_id,
57
+ runtime_id=runtime_id,
58
+ providers=providers,
59
+ )
@@ -0,0 +1,93 @@
1
+ """Provider normalization executed after landing and before runtime adaptation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from witdem import __version__
9
+
10
+ _PROVIDER_ALIASES = {
11
+ "openai": "openai",
12
+ "azure_openai": "azure_openai",
13
+ "azure.openai": "azure_openai",
14
+ "azure": "azure_openai",
15
+ "anthropic": "anthropic",
16
+ "claude": "anthropic",
17
+ "deepseek": "deepseek",
18
+ "mistral": "mistral",
19
+ "mistralai": "mistral",
20
+ "amazon_bedrock": "amazon_bedrock",
21
+ "aws.bedrock": "amazon_bedrock",
22
+ "bedrock": "amazon_bedrock",
23
+ "google": "google",
24
+ "google.vertex": "google",
25
+ "vertex": "google",
26
+ "vertex_ai": "google",
27
+ "gemini": "google",
28
+ "cohere": "cohere",
29
+ "ollama": "ollama",
30
+ }
31
+
32
+
33
+ def _first(attributes: Mapping[str, Any], *keys: str) -> Any:
34
+ for key in keys:
35
+ value = attributes.get(key)
36
+ if value is not None and str(value):
37
+ return value
38
+ return None
39
+
40
+
41
+ def _provider(attributes: Mapping[str, Any]) -> tuple[str | None, str | None]:
42
+ observed = _first(attributes, "gen_ai.provider.name", "gen_ai.system", "provider", "llm.provider")
43
+ if observed is not None:
44
+ canonical = _PROVIDER_ALIASES.get(str(observed).strip().casefold().replace("-", "_"))
45
+ return canonical or str(observed).strip().casefold(), "observed_attribute"
46
+ return None, None
47
+
48
+
49
+ def normalize_provider_spans(
50
+ spans: Sequence[Mapping[str, Any]],
51
+ ) -> tuple[list[dict[str, Any]], tuple[str, ...]]:
52
+ """Normalize provider evidence per span without choosing one run-wide provider."""
53
+
54
+ normalized: list[dict[str, Any]] = []
55
+ adapters: set[str] = set()
56
+ for raw in spans:
57
+ row = dict(raw)
58
+ attributes_value = row.get("attributes")
59
+ attributes = dict(attributes_value) if isinstance(attributes_value, Mapping) else {}
60
+ provider, source = _provider(attributes)
61
+ if provider is not None:
62
+ adapters.add(provider)
63
+ observed_provider = _first(
64
+ attributes,
65
+ "gen_ai.provider.name",
66
+ "gen_ai.system",
67
+ "provider",
68
+ "llm.provider",
69
+ )
70
+ if observed_provider is not None and str(observed_provider).strip().casefold() != provider:
71
+ attributes["witdem.provider_adapter.observed"] = str(observed_provider)
72
+ attributes["provider"] = provider
73
+ attributes["gen_ai.provider.name"] = provider
74
+ attributes["witdem.provider_adapter.name"] = provider
75
+ attributes["witdem.provider_adapter.version"] = __version__
76
+ attributes["witdem.provider_adapter.source"] = source
77
+ model = _first(
78
+ attributes,
79
+ "gen_ai.response.model",
80
+ "gen_ai.request.model",
81
+ "model",
82
+ "llm.model_name",
83
+ )
84
+ gen_ai_operation = str(attributes.get("gen_ai.operation.name") or "").casefold()
85
+ has_usage = any(str(key).startswith("gen_ai.usage.") for key in attributes)
86
+ if model is not None and (gen_ai_operation or has_usage):
87
+ # Framework spans often use a generic name such as "Component".
88
+ # The provider adapter has stronger evidence than that name and
89
+ # records the canonical kind for the runtime adapter to consume.
90
+ attributes["witdem.operation.kind"] = "model"
91
+ row["attributes"] = attributes
92
+ normalized.append(row)
93
+ return normalized, tuple(sorted(adapters))
@@ -0,0 +1,51 @@
1
+ """Runtime adapter registry (see ``docs/architecture.md`` §3).
2
+
3
+ Used by the OTLP ingestion path (``ingest.correlate``) to select the first
4
+ matching runtime-specific adapter.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from collections.abc import Mapping, Sequence
11
+ from typing import Any
12
+
13
+ from witdem.adapters.base import RuntimeAdapter
14
+ from witdem.adapters.haystack_adapter import HaystackAdapter
15
+ from witdem.integrations.adapters.claude import ClaudeAdapter
16
+ from witdem.integrations.adapters.langchain import LangChainAdapter
17
+ from witdem.integrations.adapters.langgraph import LangGraphAdapter
18
+ from witdem.integrations.adapters.openai_agents import OpenAIAgentsAdapter
19
+ from witdem.integrations.adapters.otel import OTelAdapter
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _REGISTERED_ADAPTERS: tuple[RuntimeAdapter, ...] = (
24
+ LangGraphAdapter(),
25
+ OpenAIAgentsAdapter(),
26
+ ClaudeAdapter(),
27
+ LangChainAdapter(),
28
+ HaystackAdapter(),
29
+ OTelAdapter(),
30
+ )
31
+
32
+ _FALLBACK_ADAPTER: RuntimeAdapter = OTelAdapter()
33
+
34
+
35
+ def detect_adapter(spans: Sequence[Mapping[str, Any]]) -> RuntimeAdapter:
36
+ """Return the first registered adapter whose ``detect()`` is True.
37
+
38
+ Falls back to :class:`OTelAdapter` (with a debug log noting the
39
+ fallback) when no registered adapter positively matches -- see
40
+ ``docs/architecture.md`` §3 and the module docstring above.
41
+ """
42
+
43
+ for adapter in _REGISTERED_ADAPTERS:
44
+ if adapter.detect(spans):
45
+ return adapter
46
+ logger.debug(
47
+ "detect_adapter: no adapter positively matched %d spans; falling back to %s",
48
+ len(spans),
49
+ type(_FALLBACK_ADAPTER).__name__,
50
+ )
51
+ return _FALLBACK_ADAPTER
@@ -0,0 +1,74 @@
1
+ """Public, domain-neutral Witdem analytics API."""
2
+
3
+ from witdem.analytics.contracts import (
4
+ CostSummary,
5
+ ExecutionSummary,
6
+ FailureSummary,
7
+ ModelSummary,
8
+ PathSummary,
9
+ PerformanceSummary,
10
+ ProviderSummary,
11
+ )
12
+ from witdem.analytics.core import Evaluation, Event, Execution, Link, Operation, Outcome
13
+ from witdem.analytics.derived import derived_termination_category
14
+ from witdem.analytics.identity import (
15
+ canonical_operation_key,
16
+ canonical_path_signature,
17
+ canonical_stage_key,
18
+ canonical_tool_key,
19
+ display_operation,
20
+ display_path,
21
+ display_stage,
22
+ display_tool,
23
+ )
24
+ from witdem.analytics.runtime import (
25
+ NormalizedExecutionGraph,
26
+ ReplayGraph,
27
+ derive_replay_graph,
28
+ derive_runtime_insights,
29
+ find_similar_executions,
30
+ normalize_haystack_spans,
31
+ )
32
+ from witdem.analytics.schema import (
33
+ AGGREGATE_COLUMNS,
34
+ ANALYTICS_COLUMN_TYPES,
35
+ ANALYTICS_COLUMNS,
36
+ ANALYTICS_TABLES,
37
+ V2_ANALYTICS_TABLES,
38
+ )
39
+
40
+ __all__ = [
41
+ "Event",
42
+ "Execution",
43
+ "Evaluation",
44
+ "Link",
45
+ "Operation",
46
+ "Outcome",
47
+ "ExecutionSummary",
48
+ "CostSummary",
49
+ "ProviderSummary",
50
+ "ModelSummary",
51
+ "FailureSummary",
52
+ "PerformanceSummary",
53
+ "PathSummary",
54
+ "NormalizedExecutionGraph",
55
+ "ReplayGraph",
56
+ "normalize_haystack_spans",
57
+ "derive_replay_graph",
58
+ "derive_runtime_insights",
59
+ "find_similar_executions",
60
+ "derived_termination_category",
61
+ "canonical_operation_key",
62
+ "canonical_path_signature",
63
+ "canonical_stage_key",
64
+ "canonical_tool_key",
65
+ "display_operation",
66
+ "display_path",
67
+ "display_stage",
68
+ "display_tool",
69
+ "ANALYTICS_TABLES",
70
+ "ANALYTICS_COLUMNS",
71
+ "ANALYTICS_COLUMN_TYPES",
72
+ "AGGREGATE_COLUMNS",
73
+ "V2_ANALYTICS_TABLES",
74
+ ]
@@ -0,0 +1,31 @@
1
+ """Stable, frontend-neutral analytics read models."""
2
+
3
+ from witdem.analytics.contracts.read_models import (
4
+ CostSummary,
5
+ ExecutionSummary,
6
+ FailureSummary,
7
+ MeasurementCoverage,
8
+ MetadataSnapshot,
9
+ ModelSummary,
10
+ OverviewSnapshot,
11
+ PathSummary,
12
+ PerformanceSummary,
13
+ ProductGoalSummary,
14
+ ProviderSummary,
15
+ SemanticReplayRecord,
16
+ )
17
+
18
+ __all__ = [
19
+ "CostSummary",
20
+ "ExecutionSummary",
21
+ "FailureSummary",
22
+ "MetadataSnapshot",
23
+ "MeasurementCoverage",
24
+ "ModelSummary",
25
+ "OverviewSnapshot",
26
+ "PathSummary",
27
+ "PerformanceSummary",
28
+ "ProductGoalSummary",
29
+ "ProviderSummary",
30
+ "SemanticReplayRecord",
31
+ ]
@@ -0,0 +1,252 @@
1
+ """Typed analytics results shared by UI, API, and future frontend clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ from witdem.analytics.repository.state import Capabilities
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class _ReadModel:
15
+ """Common serialization boundary for frontend/API adapters."""
16
+
17
+ def to_dict(self) -> dict[str, Any]:
18
+ return asdict(self)
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class SemanticReplayRecord(_ReadModel):
23
+ record_id: str
24
+ kind: str
25
+ name: str
26
+ timestamp: datetime | None
27
+ status: str | None
28
+ value: Any
29
+ attributes: dict[str, Any]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class ExecutionSummary(_ReadModel):
34
+ total_runs: int
35
+ successful_runs: int
36
+ failed_runs: int
37
+ running_runs: int
38
+ recovered_runs: int
39
+ extra_work_runs: int
40
+ avg_duration_seconds: float | None
41
+ measured_cost: float | None
42
+ cost_coverage: float
43
+ business_successful_runs: int
44
+ business_unsuccessful_runs: int
45
+ business_reported_runs: int
46
+ terminal_runs: int
47
+ unknown_runs: int
48
+ attention_runs: int
49
+ runtime_success_rate: float
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class MeasurementCoverage(_ReadModel):
54
+ """Completeness for a measurement that is not applicable to every run."""
55
+
56
+ total_runs: int
57
+ applicable_runs: int
58
+ complete_runs: int
59
+ partial_runs: int
60
+ missing_runs: int
61
+ not_applicable_runs: int
62
+ eligible_operations: int
63
+ measured_operations: int
64
+
65
+ @property
66
+ def coverage(self) -> float:
67
+ return self.complete_runs / self.applicable_runs if self.applicable_runs else 0.0
68
+
69
+ @property
70
+ def operation_coverage(self) -> float:
71
+ return self.measured_operations / self.eligible_operations if self.eligible_operations else 0.0
72
+
73
+ def to_dict(self) -> dict[str, Any]:
74
+ return {**asdict(self), "coverage": self.coverage, "operation_coverage": self.operation_coverage}
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class ProductGoalSummary(_ReadModel):
79
+ total_runs: int
80
+ reported_runs: int
81
+ achieved_runs: int
82
+ decision_correct_runs: int
83
+ false_acceptances: int
84
+ false_rejections: int
85
+ escalation_errors: int
86
+ targeted_research_runs: int
87
+ targeted_research_successes: int
88
+ cost_per_achieved_goal: float | None
89
+ cost_measured_achieved_runs: int
90
+ time_per_achieved_goal: float | None
91
+ time_measured_achieved_runs: int
92
+ tokens_per_achieved_goal: float | None
93
+ token_measured_achieved_runs: int
94
+
95
+ @property
96
+ def cost_coverage(self) -> float:
97
+ return self.cost_measured_achieved_runs / self.achieved_runs if self.achieved_runs else 0.0
98
+
99
+ @property
100
+ def time_coverage(self) -> float:
101
+ return self.time_measured_achieved_runs / self.achieved_runs if self.achieved_runs else 0.0
102
+
103
+ @property
104
+ def token_coverage(self) -> float:
105
+ return self.token_measured_achieved_runs / self.achieved_runs if self.achieved_runs else 0.0
106
+
107
+ @property
108
+ def coverage(self) -> float:
109
+ return self.reported_runs / self.total_runs if self.total_runs else 0.0
110
+
111
+ @property
112
+ def success_rate(self) -> float:
113
+ return self.achieved_runs / self.reported_runs if self.reported_runs else 0.0
114
+
115
+ @property
116
+ def decision_correctness_rate(self) -> float:
117
+ return self.decision_correct_runs / self.reported_runs if self.reported_runs else 0.0
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class CostSummary(_ReadModel):
122
+ measured_cost: float | None
123
+ model_cost: float | None
124
+ tool_cost: float | None
125
+ cost_coverage: float
126
+ measured_cost_per_run: float | None
127
+ input_tokens: float | None
128
+ output_tokens: float | None
129
+ total_tokens: float | None
130
+ token_runs: int
131
+ cost: MeasurementCoverage
132
+ tokens: MeasurementCoverage
133
+
134
+ def to_dict(self) -> dict[str, Any]:
135
+ payload = asdict(self)
136
+ payload["cost"] = self.cost.to_dict()
137
+ payload["tokens"] = self.tokens.to_dict()
138
+ return payload
139
+
140
+
141
+ @dataclass(frozen=True, slots=True)
142
+ class OverviewSnapshot(_ReadModel):
143
+ """One coherent read model for the dashboard overview request."""
144
+
145
+ execution: ExecutionSummary
146
+ goals: ProductGoalSummary
147
+ costs: CostSummary
148
+ cost_unavailable: dict[str, int]
149
+ models: tuple[ModelSummary, ...]
150
+ providers: tuple[ProviderSummary, ...]
151
+ workflows: tuple[PerformanceSummary, ...]
152
+ stages: tuple[dict[str, Any], ...]
153
+ runtime_breakdown: dict[str, int]
154
+ outcome_breakdown: dict[str, int]
155
+ failures: tuple[FailureSummary, ...]
156
+ evaluations: tuple[dict[str, Any], ...]
157
+ goal_misses: tuple[dict[str, Any], ...]
158
+ goal_trend: tuple[dict[str, Any], ...]
159
+ goal_portfolio: tuple[dict[str, Any], ...]
160
+ assurance_summary: dict[str, int | float]
161
+ contracts: tuple[dict[str, Any], ...]
162
+ metadata: MetadataSnapshot
163
+
164
+
165
+ @dataclass(frozen=True, slots=True)
166
+ class MetadataSnapshot(_ReadModel):
167
+ """One coherent read model for dashboard capabilities and filter values."""
168
+
169
+ capabilities: Capabilities
170
+ filters: dict[str, tuple[str, ...]]
171
+ contracts: tuple[dict[str, Any], ...]
172
+
173
+
174
+ @dataclass(frozen=True, slots=True)
175
+ class PerformanceSummary(_ReadModel):
176
+ label: str
177
+ runs: int
178
+ calls: int
179
+ completed: int
180
+ successful: int
181
+ failed: int
182
+ recovered: int
183
+ extra_work: int
184
+ measured_cost: float | None
185
+ cost_per_positive_run: float | None
186
+ time_per_positive_run: float | None
187
+ failed_run_cost: float | None
188
+ total_tokens: float | None
189
+ tokens_per_positive_run: float | None
190
+ failed_run_tokens: float | None
191
+ failure_rate: float
192
+ extra_work_rate: float
193
+ cost_coverage: float
194
+ semantics: str
195
+ participant_id: str
196
+ dimension: str
197
+ provider_id: str | None
198
+ model_id: str | None
199
+ model_family: str | None
200
+ vendor_id: str | None
201
+ active_seconds: float
202
+ p50_call_seconds: float | None
203
+ p95_call_seconds: float | None
204
+ cost_eligible_operations: int
205
+ cost_measured_operations: int
206
+ token_eligible_operations: int
207
+ token_measured_operations: int
208
+
209
+
210
+ @dataclass(frozen=True, slots=True)
211
+ class ProviderSummary(PerformanceSummary):
212
+ """Performance summary grouped by provider."""
213
+
214
+
215
+ @dataclass(frozen=True, slots=True)
216
+ class ModelSummary(PerformanceSummary):
217
+ """Performance summary grouped by model."""
218
+
219
+
220
+ @dataclass(frozen=True, slots=True)
221
+ class FailureSummary(_ReadModel):
222
+ failure_location: str
223
+ failure_key: str
224
+ kind: str
225
+ failures: int
226
+ executions: int
227
+ terminal_runs: int
228
+ recovered_runs: int
229
+ unknown_outcome_runs: int
230
+ providers: str | None
231
+ models: str | None
232
+ time_seconds: float
233
+ known_cost: float | None
234
+ total_tokens: float | None
235
+ affected_run_time_seconds: float
236
+ affected_run_cost: float | None
237
+ affected_run_tokens: float | None
238
+
239
+
240
+ @dataclass(frozen=True, slots=True)
241
+ class PathSummary(_ReadModel):
242
+ path: str
243
+ steps: tuple[str, ...]
244
+ path_signature: str
245
+ executions: int
246
+ completed: int
247
+ failures: int
248
+ failure_reports: int
249
+ time_seconds: float
250
+ usual_seconds: float | None
251
+ known_cost: float | None
252
+ total_tokens: float | None