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.
- witdem/__init__.py +8 -0
- witdem/adapters/__init__.py +13 -0
- witdem/adapters/base.py +31 -0
- witdem/adapters/haystack_adapter.py +59 -0
- witdem/adapters/providers.py +93 -0
- witdem/adapters/registry.py +51 -0
- witdem/analytics/__init__.py +74 -0
- witdem/analytics/contracts/__init__.py +31 -0
- witdem/analytics/contracts/read_models.py +252 -0
- witdem/analytics/core.py +108 -0
- witdem/analytics/cost.py +533 -0
- witdem/analytics/derived.py +104 -0
- witdem/analytics/identity.py +319 -0
- witdem/analytics/operations.py +442 -0
- witdem/analytics/queries/entities/models.sql +10 -0
- witdem/analytics/queries/entities/provider_values.sql +4 -0
- witdem/analytics/queries/entities/providers.sql +9 -0
- witdem/analytics/queries/execution/business_outcome.sql +5 -0
- witdem/analytics/queries/execution/evaluations_for_execution.sql +4 -0
- witdem/analytics/queries/execution/events_for_execution.sql +4 -0
- witdem/analytics/queries/execution/execution_detail.sql +3 -0
- witdem/analytics/queries/execution/execution_population.sql +4 -0
- witdem/analytics/queries/execution/execution_timeline.sql +4 -0
- witdem/analytics/queries/execution/links_for_execution.sql +3 -0
- witdem/analytics/queries/execution/outcomes_for_execution.sql +4 -0
- witdem/analytics/queries/execution/product_goal.sql +5 -0
- witdem/analytics/queries/execution/runtime_outcome.sql +8 -0
- witdem/analytics/queries/failures/failure_patterns.sql +11 -0
- witdem/analytics/queries/overview/cost_summary.sql +8 -0
- witdem/analytics/queries/overview/execution_health.sql +31 -0
- witdem/analytics/queries/overview/performance.sql +9 -0
- witdem/analytics/queries/overview/product_goals.sql +4 -0
- witdem/analytics/queries/overview/success_metrics.sql +11 -0
- witdem/analytics/queries/paths/loops.sql +4 -0
- witdem/analytics/queries/paths/path_frequency.sql +3 -0
- witdem/analytics/queries/shared/capabilities.sql +12 -0
- witdem/analytics/queries/shared/evaluations_count.sql +2 -0
- witdem/analytics/queries/shared/events_capabilities.sql +4 -0
- witdem/analytics/queries/shared/filter_completed.sql +7 -0
- witdem/analytics/queries/shared/filter_end_date.sql +1 -0
- witdem/analytics/queries/shared/filter_failed.sql +7 -0
- witdem/analytics/queries/shared/filter_has_failure.sql +6 -0
- witdem/analytics/queries/shared/filter_provider.sql +6 -0
- witdem/analytics/queries/shared/filter_running.sql +1 -0
- witdem/analytics/queries/shared/filter_start_date.sql +1 -0
- witdem/analytics/queries/shared/metadata.sql +1 -0
- witdem/analytics/queries/shared/outcomes_count.sql +2 -0
- witdem/analytics/read_model.py +360 -0
- witdem/analytics/repository/__init__.py +6 -0
- witdem/analytics/repository/analytics_repository.py +3429 -0
- witdem/analytics/repository/backend.py +51 -0
- witdem/analytics/repository/sql_loader.py +44 -0
- witdem/analytics/repository/state.py +55 -0
- witdem/analytics/runtime.py +1311 -0
- witdem/analytics/schema/analytics_tables.sql +116 -0
- witdem/analytics/schema.py +108 -0
- witdem/analytics/serving.py +477 -0
- witdem/api.py +145 -0
- witdem/auth.py +64 -0
- witdem/cli.py +728 -0
- witdem/config.py +88 -0
- witdem/dashboard/__init__.py +5 -0
- witdem/dashboard/app.py +216 -0
- witdem/dashboard/service.py +952 -0
- witdem/dashboard/static/assets/echarts-runtime-mIDt9UdM.js +19 -0
- witdem/dashboard/static/assets/index-B2TO5dg7.css +1 -0
- witdem/dashboard/static/assets/index-DnxvhzHV.js +56 -0
- witdem/dashboard/static/assets/witdem-mark-purple-B7xvtfh0.png +0 -0
- witdem/dashboard/static/index.html +3 -0
- witdem/elt/__init__.py +7 -0
- witdem/elt/adapter_stage.py +235 -0
- witdem/elt/publisher.py +43 -0
- witdem/elt/worker.py +205 -0
- witdem/elt/workspace/pipelines/normalize.pipeline.json +59 -0
- witdem/evaluation_campaigns.py +108 -0
- witdem/ingest/__init__.py +1 -0
- witdem/ingest/corpus.py +223 -0
- witdem/ingest/correlate.py +374 -0
- witdem/ingest/live_db.py +1239 -0
- witdem/ingest/otlp_http.py +249 -0
- witdem/ingest/raw_store.py +195 -0
- witdem/ingest/sdk_ingest.py +183 -0
- witdem/ingest/sdk_store.py +172 -0
- witdem/integrations/__init__.py +12 -0
- witdem/integrations/adapters/__init__.py +14 -0
- witdem/integrations/adapters/claude.py +158 -0
- witdem/integrations/adapters/langchain.py +173 -0
- witdem/integrations/adapters/langgraph.py +117 -0
- witdem/integrations/adapters/openai_agents.py +294 -0
- witdem/integrations/adapters/otel.py +34 -0
- witdem/integrations/mapping.py +266 -0
- witdem/integrations/models/__init__.py +4 -0
- witdem/integrations/models/normalized_operation.py +42 -0
- witdem/integrations/models/normalized_span.py +60 -0
- witdem/integrations/normalizers/__init__.py +5 -0
- witdem/integrations/normalizers/genai.py +157 -0
- witdem/integrations/normalizers/openinference.py +149 -0
- witdem/integrations/normalizers/otel.py +244 -0
- witdem/lifecycle.py +316 -0
- witdem/pricing/__init__.py +10 -0
- witdem/pricing/catalog.yaml +1004 -0
- witdem/pricing/sources.yaml +186 -0
- witdem/pricing/update.py +187 -0
- witdem/protocol.py +7 -0
- witdem/py.typed +1 -0
- witdem/retention.py +162 -0
- witdem/telemetry/__init__.py +5 -0
- witdem/telemetry/otel.py +193 -0
- witdem/update.py +212 -0
- witdem/workflows.py +782 -0
- witdem_analytics-0.1.0.dist-info/METADATA +287 -0
- witdem_analytics-0.1.0.dist-info/RECORD +115 -0
- witdem_analytics-0.1.0.dist-info/WHEEL +4 -0
- witdem_analytics-0.1.0.dist-info/entry_points.txt +2 -0
- 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"]
|
witdem/adapters/base.py
ADDED
|
@@ -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
|