witdem-analytics 0.2.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 (108) hide show
  1. witdem/__init__.py +3 -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 +110 -0
  6. witdem/adapters/registry.py +51 -0
  7. witdem/analytics/__init__.py +74 -0
  8. witdem/analytics/contracts/__init__.py +29 -0
  9. witdem/analytics/contracts/read_models.py +199 -0
  10. witdem/analytics/core.py +108 -0
  11. witdem/analytics/cost.py +531 -0
  12. witdem/analytics/derived.py +104 -0
  13. witdem/analytics/identity.py +319 -0
  14. witdem/analytics/queries/entities/models.sql +10 -0
  15. witdem/analytics/queries/entities/provider_values.sql +4 -0
  16. witdem/analytics/queries/entities/providers.sql +9 -0
  17. witdem/analytics/queries/execution/business_outcome.sql +5 -0
  18. witdem/analytics/queries/execution/evaluations_for_execution.sql +4 -0
  19. witdem/analytics/queries/execution/events_for_execution.sql +4 -0
  20. witdem/analytics/queries/execution/execution_detail.sql +3 -0
  21. witdem/analytics/queries/execution/execution_population.sql +4 -0
  22. witdem/analytics/queries/execution/execution_timeline.sql +4 -0
  23. witdem/analytics/queries/execution/links_for_execution.sql +3 -0
  24. witdem/analytics/queries/execution/outcomes_for_execution.sql +4 -0
  25. witdem/analytics/queries/execution/product_goal.sql +5 -0
  26. witdem/analytics/queries/execution/runtime_outcome.sql +8 -0
  27. witdem/analytics/queries/failures/failure_patterns.sql +11 -0
  28. witdem/analytics/queries/overview/cost_summary.sql +8 -0
  29. witdem/analytics/queries/overview/execution_health.sql +31 -0
  30. witdem/analytics/queries/overview/performance.sql +9 -0
  31. witdem/analytics/queries/overview/product_goals.sql +4 -0
  32. witdem/analytics/queries/overview/success_metrics.sql +11 -0
  33. witdem/analytics/queries/paths/loops.sql +4 -0
  34. witdem/analytics/queries/paths/path_frequency.sql +3 -0
  35. witdem/analytics/queries/shared/capabilities.sql +12 -0
  36. witdem/analytics/queries/shared/evaluations_count.sql +2 -0
  37. witdem/analytics/queries/shared/events_capabilities.sql +4 -0
  38. witdem/analytics/queries/shared/filter_completed.sql +7 -0
  39. witdem/analytics/queries/shared/filter_end_date.sql +1 -0
  40. witdem/analytics/queries/shared/filter_failed.sql +7 -0
  41. witdem/analytics/queries/shared/filter_has_failure.sql +6 -0
  42. witdem/analytics/queries/shared/filter_provider.sql +6 -0
  43. witdem/analytics/queries/shared/filter_running.sql +1 -0
  44. witdem/analytics/queries/shared/filter_start_date.sql +1 -0
  45. witdem/analytics/queries/shared/metadata.sql +1 -0
  46. witdem/analytics/queries/shared/outcomes_count.sql +2 -0
  47. witdem/analytics/read_model.py +337 -0
  48. witdem/analytics/repository/__init__.py +6 -0
  49. witdem/analytics/repository/analytics_repository.py +2335 -0
  50. witdem/analytics/repository/backend.py +51 -0
  51. witdem/analytics/repository/sql_loader.py +42 -0
  52. witdem/analytics/repository/state.py +55 -0
  53. witdem/analytics/runtime.py +1321 -0
  54. witdem/analytics/schema/analytics_tables.sql +73 -0
  55. witdem/analytics/schema.py +108 -0
  56. witdem/analytics/serving.py +465 -0
  57. witdem/api.py +144 -0
  58. witdem/auth.py +64 -0
  59. witdem/cli.py +520 -0
  60. witdem/config.py +85 -0
  61. witdem/dashboard/__init__.py +5 -0
  62. witdem/dashboard/app.py +129 -0
  63. witdem/dashboard/service.py +178 -0
  64. witdem/dashboard/static/assets/advanced-workflow-graph-CAdAoHLC.js +7 -0
  65. witdem/dashboard/static/assets/index-BCd8FOuZ.js +73 -0
  66. witdem/dashboard/static/assets/index-CnSnjiif.css +1 -0
  67. witdem/dashboard/static/index.html +3 -0
  68. witdem/elt/__init__.py +7 -0
  69. witdem/elt/adapter_stage.py +115 -0
  70. witdem/elt/publisher.py +36 -0
  71. witdem/elt/worker.py +193 -0
  72. witdem/elt/workspace/pipelines/normalize.pipeline.json +59 -0
  73. witdem/ingest/__init__.py +1 -0
  74. witdem/ingest/corpus.py +222 -0
  75. witdem/ingest/correlate.py +374 -0
  76. witdem/ingest/live_db.py +633 -0
  77. witdem/ingest/otlp_http.py +241 -0
  78. witdem/ingest/raw_store.py +195 -0
  79. witdem/ingest/sdk_ingest.py +183 -0
  80. witdem/ingest/sdk_store.py +172 -0
  81. witdem/integrations/__init__.py +12 -0
  82. witdem/integrations/adapters/__init__.py +14 -0
  83. witdem/integrations/adapters/claude.py +158 -0
  84. witdem/integrations/adapters/langchain.py +173 -0
  85. witdem/integrations/adapters/langgraph.py +117 -0
  86. witdem/integrations/adapters/openai_agents.py +294 -0
  87. witdem/integrations/adapters/otel.py +34 -0
  88. witdem/integrations/mapping.py +266 -0
  89. witdem/integrations/models/__init__.py +4 -0
  90. witdem/integrations/models/normalized_operation.py +42 -0
  91. witdem/integrations/models/normalized_span.py +60 -0
  92. witdem/integrations/normalizers/__init__.py +5 -0
  93. witdem/integrations/normalizers/genai.py +141 -0
  94. witdem/integrations/normalizers/openinference.py +109 -0
  95. witdem/integrations/normalizers/otel.py +176 -0
  96. witdem/pricing/__init__.py +10 -0
  97. witdem/pricing/catalog.yaml +1004 -0
  98. witdem/pricing/sources.yaml +186 -0
  99. witdem/pricing/update.py +191 -0
  100. witdem/protocol.py +5 -0
  101. witdem/py.typed +1 -0
  102. witdem/retention.py +164 -0
  103. witdem/telemetry/__init__.py +5 -0
  104. witdem/telemetry/otel.py +193 -0
  105. witdem_analytics-0.2.0.dist-info/METADATA +218 -0
  106. witdem_analytics-0.2.0.dist-info/RECORD +108 -0
  107. witdem_analytics-0.2.0.dist-info/WHEEL +4 -0
  108. witdem_analytics-0.2.0.dist-info/entry_points.txt +2 -0
witdem/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Witdem runtime analytics product package."""
2
+
3
+ __version__ = "0.2.0"
@@ -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,110 @@
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
+ _MODEL_PREFIXES = {
33
+ "gpt-": "openai",
34
+ "o1": "openai",
35
+ "o3": "openai",
36
+ "o4": "openai",
37
+ "claude-": "anthropic",
38
+ "deepseek-": "deepseek",
39
+ "mistral-": "mistral",
40
+ "ministral-": "mistral",
41
+ "codestral-": "mistral",
42
+ }
43
+
44
+
45
+ def _first(attributes: Mapping[str, Any], *keys: str) -> Any:
46
+ for key in keys:
47
+ value = attributes.get(key)
48
+ if value is not None and str(value):
49
+ return value
50
+ return None
51
+
52
+
53
+ def _provider(attributes: Mapping[str, Any]) -> tuple[str | None, str | None]:
54
+ observed = _first(attributes, "gen_ai.provider.name", "gen_ai.system", "provider", "llm.provider")
55
+ if observed is not None:
56
+ canonical = _PROVIDER_ALIASES.get(str(observed).strip().casefold().replace("-", "_"))
57
+ return canonical or str(observed).strip().casefold(), "observed_attribute"
58
+ model = _first(attributes, "gen_ai.response.model", "gen_ai.request.model", "model", "llm.model_name")
59
+ lowered = str(model or "").casefold()
60
+ for prefix, provider in _MODEL_PREFIXES.items():
61
+ if lowered.startswith(prefix):
62
+ return provider, "model_prefix"
63
+ return None, None
64
+
65
+
66
+ def normalize_provider_spans(
67
+ spans: Sequence[Mapping[str, Any]],
68
+ ) -> tuple[list[dict[str, Any]], tuple[str, ...]]:
69
+ """Normalize provider evidence per span without choosing one run-wide provider."""
70
+
71
+ normalized: list[dict[str, Any]] = []
72
+ adapters: set[str] = set()
73
+ for raw in spans:
74
+ row = dict(raw)
75
+ attributes_value = row.get("attributes")
76
+ attributes = dict(attributes_value) if isinstance(attributes_value, Mapping) else {}
77
+ provider, source = _provider(attributes)
78
+ if provider is not None:
79
+ adapters.add(provider)
80
+ observed_provider = _first(
81
+ attributes,
82
+ "gen_ai.provider.name",
83
+ "gen_ai.system",
84
+ "provider",
85
+ "llm.provider",
86
+ )
87
+ if observed_provider is not None and str(observed_provider).strip().casefold() != provider:
88
+ attributes["witdem.provider_adapter.observed"] = str(observed_provider)
89
+ attributes["provider"] = provider
90
+ attributes["gen_ai.provider.name"] = provider
91
+ attributes["witdem.provider_adapter.name"] = provider
92
+ attributes["witdem.provider_adapter.version"] = __version__
93
+ attributes["witdem.provider_adapter.source"] = source
94
+ model = _first(
95
+ attributes,
96
+ "gen_ai.response.model",
97
+ "gen_ai.request.model",
98
+ "model",
99
+ "llm.model_name",
100
+ )
101
+ gen_ai_operation = str(attributes.get("gen_ai.operation.name") or "").casefold()
102
+ has_usage = any(str(key).startswith("gen_ai.usage.") for key in attributes)
103
+ if model is not None and (gen_ai_operation or has_usage):
104
+ # Framework spans often use a generic name such as "Component".
105
+ # The provider adapter has stronger evidence than that name and
106
+ # records the canonical kind for the runtime adapter to consume.
107
+ attributes["witdem.operation.kind"] = "model"
108
+ row["attributes"] = attributes
109
+ normalized.append(row)
110
+ 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,29 @@
1
+ """Stable, frontend-neutral analytics read models."""
2
+
3
+ from witdem.analytics.contracts.read_models import (
4
+ CostSummary,
5
+ ExecutionSummary,
6
+ FailureSummary,
7
+ MetadataSnapshot,
8
+ ModelSummary,
9
+ OverviewSnapshot,
10
+ PathSummary,
11
+ PerformanceSummary,
12
+ ProductGoalSummary,
13
+ ProviderSummary,
14
+ SemanticReplayRecord,
15
+ )
16
+
17
+ __all__ = [
18
+ "CostSummary",
19
+ "ExecutionSummary",
20
+ "FailureSummary",
21
+ "MetadataSnapshot",
22
+ "ModelSummary",
23
+ "OverviewSnapshot",
24
+ "PathSummary",
25
+ "PerformanceSummary",
26
+ "ProductGoalSummary",
27
+ "ProviderSummary",
28
+ "SemanticReplayRecord",
29
+ ]
@@ -0,0 +1,199 @@
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
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class ProductGoalSummary(_ReadModel):
50
+ total_runs: int
51
+ reported_runs: int
52
+ achieved_runs: int
53
+ decision_correct_runs: int
54
+ false_acceptances: int
55
+ false_rejections: int
56
+ escalation_errors: int
57
+ targeted_research_runs: int
58
+ targeted_research_successes: int
59
+ cost_per_achieved_goal: float | None
60
+ cost_measured_achieved_runs: int
61
+ time_per_achieved_goal: float | None
62
+ time_measured_achieved_runs: int
63
+ tokens_per_achieved_goal: float | None
64
+ token_measured_achieved_runs: int
65
+
66
+ @property
67
+ def cost_coverage(self) -> float:
68
+ return self.cost_measured_achieved_runs / self.achieved_runs if self.achieved_runs else 0.0
69
+
70
+ @property
71
+ def time_coverage(self) -> float:
72
+ return self.time_measured_achieved_runs / self.achieved_runs if self.achieved_runs else 0.0
73
+
74
+ @property
75
+ def token_coverage(self) -> float:
76
+ return self.token_measured_achieved_runs / self.achieved_runs if self.achieved_runs else 0.0
77
+
78
+ @property
79
+ def coverage(self) -> float:
80
+ return self.reported_runs / self.total_runs if self.total_runs else 0.0
81
+
82
+ @property
83
+ def success_rate(self) -> float:
84
+ return self.achieved_runs / self.reported_runs if self.reported_runs else 0.0
85
+
86
+ @property
87
+ def decision_correctness_rate(self) -> float:
88
+ return self.decision_correct_runs / self.reported_runs if self.reported_runs else 0.0
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class CostSummary(_ReadModel):
93
+ measured_cost: float | None
94
+ model_cost: float | None
95
+ tool_cost: float | None
96
+ cost_coverage: float
97
+ measured_cost_per_run: float | None
98
+ input_tokens: float | None
99
+ output_tokens: float | None
100
+ total_tokens: float | None
101
+ token_runs: int
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class OverviewSnapshot(_ReadModel):
106
+ """One coherent read model for the dashboard overview request."""
107
+
108
+ execution: ExecutionSummary
109
+ goals: ProductGoalSummary
110
+ costs: CostSummary
111
+ cost_unavailable: dict[str, int]
112
+ models: tuple[ModelSummary, ...]
113
+ providers: tuple[ProviderSummary, ...]
114
+ workflows: tuple[PerformanceSummary, ...]
115
+ stages: tuple[dict[str, Any], ...]
116
+ runtime_breakdown: dict[str, int]
117
+ outcome_breakdown: dict[str, int]
118
+ failures: tuple[FailureSummary, ...]
119
+ evaluations: tuple[dict[str, Any], ...]
120
+ goal_misses: tuple[dict[str, Any], ...]
121
+ goal_trend: tuple[dict[str, Any], ...]
122
+ goal_portfolio: tuple[dict[str, Any], ...]
123
+ assurance_summary: dict[str, int | float]
124
+ contracts: tuple[dict[str, Any], ...]
125
+ metadata: MetadataSnapshot
126
+
127
+
128
+ @dataclass(frozen=True, slots=True)
129
+ class MetadataSnapshot(_ReadModel):
130
+ """One coherent read model for dashboard capabilities and filter values."""
131
+
132
+ capabilities: Capabilities
133
+ filters: dict[str, tuple[str, ...]]
134
+ contracts: tuple[dict[str, Any], ...]
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class PerformanceSummary(_ReadModel):
139
+ label: str
140
+ runs: int
141
+ calls: int
142
+ completed: int
143
+ successful: int
144
+ failed: int
145
+ recovered: int
146
+ extra_work: int
147
+ measured_cost: float | None
148
+ cost_per_positive_run: float | None
149
+ time_per_positive_run: float | None
150
+ failed_run_cost: float | None
151
+ total_tokens: float | None
152
+ tokens_per_positive_run: float | None
153
+ failed_run_tokens: float | None
154
+ failure_rate: float
155
+ extra_work_rate: float
156
+ cost_coverage: float
157
+ semantics: str
158
+
159
+
160
+ @dataclass(frozen=True, slots=True)
161
+ class ProviderSummary(PerformanceSummary):
162
+ """Performance summary grouped by provider."""
163
+
164
+
165
+ @dataclass(frozen=True, slots=True)
166
+ class ModelSummary(PerformanceSummary):
167
+ """Performance summary grouped by model."""
168
+
169
+
170
+ @dataclass(frozen=True, slots=True)
171
+ class FailureSummary(_ReadModel):
172
+ failure_location: str
173
+ failure_key: str
174
+ kind: str
175
+ failures: int
176
+ executions: int
177
+ terminal_runs: int
178
+ recovered_runs: int
179
+ unknown_outcome_runs: int
180
+ providers: str | None
181
+ models: str | None
182
+ time_seconds: float
183
+ known_cost: float | None
184
+ total_tokens: float | None
185
+
186
+
187
+ @dataclass(frozen=True, slots=True)
188
+ class PathSummary(_ReadModel):
189
+ path: str
190
+ steps: tuple[str, ...]
191
+ path_signature: str
192
+ executions: int
193
+ completed: int
194
+ failures: int
195
+ failure_reports: int
196
+ time_seconds: float
197
+ usual_seconds: float | None
198
+ known_cost: float | None
199
+ total_tokens: float | None
@@ -0,0 +1,108 @@
1
+ """Small domain-neutral analytics records.
2
+
3
+ This module deliberately knows nothing about any business domain, framework, or
4
+ provider. Domain vocabulary is carried by strings and validated by a domain
5
+ layer before records are persisted.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+ from uuid import uuid4
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+
17
+ def utc_now() -> datetime:
18
+ """Return a timezone-aware timezone.utc timestamp."""
19
+
20
+ return datetime.now(timezone.utc)
21
+
22
+
23
+ class AnalyticsRecord(BaseModel):
24
+ """Strict base for persisted analytics records."""
25
+
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+
29
+ class Execution(AnalyticsRecord):
30
+ """One unit of workflow or runtime work."""
31
+
32
+ execution_id: str
33
+ runtime_id: str | None = None
34
+ started_at: datetime | None = None
35
+ ended_at: datetime | None = None
36
+ status: str | None = None
37
+ schema_version: str = "0.1.0"
38
+ attributes: dict[str, Any] = Field(default_factory=dict)
39
+
40
+
41
+ class Operation(AnalyticsRecord):
42
+ """Work performed within an execution."""
43
+
44
+ operation_id: str = Field(default_factory=lambda: uuid4().hex)
45
+ execution_id: str
46
+ trace_id: str | None = None
47
+ span_id: str | None = None
48
+ parent_span_id: str | None = None
49
+ kind: str
50
+ name: str
51
+ status: str | None = None
52
+ started_at: datetime | None = None
53
+ ended_at: datetime | None = None
54
+ attempt: int | None = Field(default=None, ge=1)
55
+ attributes: dict[str, Any] = Field(default_factory=dict)
56
+
57
+
58
+ class Event(AnalyticsRecord):
59
+ """A meaningful instantaneous occurrence."""
60
+
61
+ event_id: str = Field(default_factory=lambda: uuid4().hex)
62
+ execution_id: str
63
+ trace_id: str | None = None
64
+ span_id: str | None = None
65
+ timestamp: datetime = Field(default_factory=utc_now)
66
+ type: str = Field(min_length=1)
67
+ name: str = Field(min_length=1)
68
+ payload: dict[str, Any] = Field(default_factory=dict)
69
+ schema_version: str = "0.1.0"
70
+
71
+
72
+ class Link(AnalyticsRecord):
73
+ """A relationship not represented by operation parentage alone."""
74
+
75
+ link_id: str = Field(default_factory=lambda: uuid4().hex)
76
+ execution_id: str
77
+ source_id: str
78
+ target_id: str
79
+ relation: str = Field(min_length=1)
80
+ attributes: dict[str, Any] = Field(default_factory=dict)
81
+
82
+
83
+ class Evaluation(AnalyticsRecord):
84
+ """A structured assessment of an execution, operation, result, or object."""
85
+
86
+ evaluation_id: str = Field(default_factory=lambda: uuid4().hex)
87
+ execution_id: str
88
+ subject_id: str | None = None
89
+ name: str = Field(min_length=1)
90
+ value: Any = None
91
+ label: str | None = None
92
+ score: float | None = Field(default=None, ge=0, le=1)
93
+ source: str = Field(min_length=1)
94
+ confidence: float | None = Field(default=None, ge=0, le=1)
95
+ definition_version: str | None = None
96
+ attributes: dict[str, Any] = Field(default_factory=dict)
97
+
98
+
99
+ class Outcome(AnalyticsRecord):
100
+ """An externally meaningful result of an execution."""
101
+
102
+ outcome_id: str = Field(default_factory=lambda: uuid4().hex)
103
+ execution_id: str
104
+ name: str = Field(min_length=1)
105
+ status: str | None = None
106
+ value: Any = None
107
+ timestamp: datetime = Field(default_factory=utc_now)
108
+ attributes: dict[str, Any] = Field(default_factory=dict)