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.
- witdem/__init__.py +3 -0
- witdem/adapters/__init__.py +13 -0
- witdem/adapters/base.py +31 -0
- witdem/adapters/haystack_adapter.py +59 -0
- witdem/adapters/providers.py +110 -0
- witdem/adapters/registry.py +51 -0
- witdem/analytics/__init__.py +74 -0
- witdem/analytics/contracts/__init__.py +29 -0
- witdem/analytics/contracts/read_models.py +199 -0
- witdem/analytics/core.py +108 -0
- witdem/analytics/cost.py +531 -0
- witdem/analytics/derived.py +104 -0
- witdem/analytics/identity.py +319 -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 +337 -0
- witdem/analytics/repository/__init__.py +6 -0
- witdem/analytics/repository/analytics_repository.py +2335 -0
- witdem/analytics/repository/backend.py +51 -0
- witdem/analytics/repository/sql_loader.py +42 -0
- witdem/analytics/repository/state.py +55 -0
- witdem/analytics/runtime.py +1321 -0
- witdem/analytics/schema/analytics_tables.sql +73 -0
- witdem/analytics/schema.py +108 -0
- witdem/analytics/serving.py +465 -0
- witdem/api.py +144 -0
- witdem/auth.py +64 -0
- witdem/cli.py +520 -0
- witdem/config.py +85 -0
- witdem/dashboard/__init__.py +5 -0
- witdem/dashboard/app.py +129 -0
- witdem/dashboard/service.py +178 -0
- witdem/dashboard/static/assets/advanced-workflow-graph-CAdAoHLC.js +7 -0
- witdem/dashboard/static/assets/index-BCd8FOuZ.js +73 -0
- witdem/dashboard/static/assets/index-CnSnjiif.css +1 -0
- witdem/dashboard/static/index.html +3 -0
- witdem/elt/__init__.py +7 -0
- witdem/elt/adapter_stage.py +115 -0
- witdem/elt/publisher.py +36 -0
- witdem/elt/worker.py +193 -0
- witdem/elt/workspace/pipelines/normalize.pipeline.json +59 -0
- witdem/ingest/__init__.py +1 -0
- witdem/ingest/corpus.py +222 -0
- witdem/ingest/correlate.py +374 -0
- witdem/ingest/live_db.py +633 -0
- witdem/ingest/otlp_http.py +241 -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 +141 -0
- witdem/integrations/normalizers/openinference.py +109 -0
- witdem/integrations/normalizers/otel.py +176 -0
- witdem/pricing/__init__.py +10 -0
- witdem/pricing/catalog.yaml +1004 -0
- witdem/pricing/sources.yaml +186 -0
- witdem/pricing/update.py +191 -0
- witdem/protocol.py +5 -0
- witdem/py.typed +1 -0
- witdem/retention.py +164 -0
- witdem/telemetry/__init__.py +5 -0
- witdem/telemetry/otel.py +193 -0
- witdem_analytics-0.2.0.dist-info/METADATA +218 -0
- witdem_analytics-0.2.0.dist-info/RECORD +108 -0
- witdem_analytics-0.2.0.dist-info/WHEEL +4 -0
- witdem_analytics-0.2.0.dist-info/entry_points.txt +2 -0
witdem/__init__.py
ADDED
|
@@ -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,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
|
witdem/analytics/core.py
ADDED
|
@@ -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)
|