aice-observability 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.
- aice_observability/__init__.py +5 -0
- aice_observability/ai/__init__.py +0 -0
- aice_observability/ai/agents.py +169 -0
- aice_observability/ai/evaluation.py +0 -0
- aice_observability/ai/instrumentation/__init__.py +0 -0
- aice_observability/ai/instrumentation/anthropic.py +20 -0
- aice_observability/ai/instrumentation/base.py +25 -0
- aice_observability/ai/instrumentation/gemini.py +20 -0
- aice_observability/ai/instrumentation/langchain.py +16 -0
- aice_observability/ai/instrumentation/openai.py +19 -0
- aice_observability/ai/instrumentation/registry.py +53 -0
- aice_observability/ai/instrumentation/vertexai.py +18 -0
- aice_observability/ai/metrics.py +216 -0
- aice_observability/ai/rag.py +169 -0
- aice_observability/ai/registry.py +0 -0
- aice_observability/ai/tracing.py +281 -0
- aice_observability/builder.py +168 -0
- aice_observability/core/__init__.py +0 -0
- aice_observability/core/config.py +69 -0
- aice_observability/core/exporters.py +27 -0
- aice_observability/core/providers.py +58 -0
- aice_observability/core/resources.py +28 -0
- aice_observability/frameworks/__init__.py +0 -0
- aice_observability/frameworks/django.py +7 -0
- aice_observability/frameworks/fastapi.py +19 -0
- aice_observability/frameworks/flask.py +13 -0
- aice_observability/integrations/__init__.py +0 -0
- aice_observability/integrations/asyncpg.py +20 -0
- aice_observability/integrations/botocore.py +17 -0
- aice_observability/integrations/celery.py +23 -0
- aice_observability/integrations/httpx.py +19 -0
- aice_observability/integrations/psycopg.py +17 -0
- aice_observability/integrations/psycopg2.py +7 -0
- aice_observability/integrations/redis.py +17 -0
- aice_observability/integrations/sqlalchemy.py +7 -0
- aice_observability/propagation.py +82 -0
- aice_observability/py.typed +0 -0
- aice_observability-0.2.0.dist-info/METADATA +806 -0
- aice_observability-0.2.0.dist-info/RECORD +40 -0
- aice_observability-0.2.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level tracing helpers for AI agent orchestration.
|
|
3
|
+
|
|
4
|
+
Unlike tracing.py, which records a single provider call,
|
|
5
|
+
this module records the lifecycle of an AI agent.
|
|
6
|
+
|
|
7
|
+
Example hierarchy
|
|
8
|
+
|
|
9
|
+
invoke_agent
|
|
10
|
+
OpenAI chat
|
|
11
|
+
execute_tool
|
|
12
|
+
retrieve_documents
|
|
13
|
+
Gemini chat
|
|
14
|
+
execute_tool
|
|
15
|
+
Anthropic chat
|
|
16
|
+
|
|
17
|
+
Everything underneath automatically becomes part of the same trace.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
from opentelemetry.trace import SpanKind
|
|
25
|
+
|
|
26
|
+
from aice_observability.ai.tracing import _span
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def invoke_agent(
|
|
31
|
+
*,
|
|
32
|
+
agent: str,
|
|
33
|
+
conversation_id: str | None = None,
|
|
34
|
+
session_id: str | None = None,
|
|
35
|
+
extra_attributes: dict | None = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Records one complete execution of an AI agent.
|
|
39
|
+
|
|
40
|
+
This should wrap the outermost orchestration loop.
|
|
41
|
+
|
|
42
|
+
One invoke_agent span may contain
|
|
43
|
+
|
|
44
|
+
• multiple LLM calls
|
|
45
|
+
• multiple retrievals
|
|
46
|
+
• multiple tool executions
|
|
47
|
+
• sub-agents
|
|
48
|
+
|
|
49
|
+
``session_id`` maps to ``gen_ai.session.id`` pass it only for a genuine
|
|
50
|
+
multi-run session. A single run is not a session; use
|
|
51
|
+
``extra_attributes={"<namespace>.run.id": run_id}`` instead so dashboards
|
|
52
|
+
grouping by session do not conflate the two.
|
|
53
|
+
|
|
54
|
+
Example
|
|
55
|
+
|
|
56
|
+
with invoke_agent(agent="ResearchAgent"):
|
|
57
|
+
...
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
attributes = {
|
|
61
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
62
|
+
"gen_ai.agent.name": agent,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if conversation_id:
|
|
66
|
+
attributes["gen_ai.conversation.id"] = conversation_id
|
|
67
|
+
|
|
68
|
+
if session_id:
|
|
69
|
+
attributes["gen_ai.session.id"] = session_id
|
|
70
|
+
|
|
71
|
+
if extra_attributes:
|
|
72
|
+
attributes.update(extra_attributes)
|
|
73
|
+
|
|
74
|
+
with _span(
|
|
75
|
+
f"Agent {agent}",
|
|
76
|
+
kind=SpanKind.INTERNAL,
|
|
77
|
+
attributes=attributes,
|
|
78
|
+
) as span:
|
|
79
|
+
|
|
80
|
+
yield span
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@contextmanager
|
|
84
|
+
def execute_tool(
|
|
85
|
+
*,
|
|
86
|
+
tool: str,
|
|
87
|
+
extra_attributes: dict | None = None,
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Records execution of one tool.
|
|
91
|
+
|
|
92
|
+
Example
|
|
93
|
+
|
|
94
|
+
with execute_tool(tool="Vector Search"):
|
|
95
|
+
...
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
attributes = {
|
|
99
|
+
"gen_ai.operation.name": "execute_tool",
|
|
100
|
+
"gen_ai.tool.name": tool,
|
|
101
|
+
}
|
|
102
|
+
if extra_attributes:
|
|
103
|
+
attributes.update(extra_attributes)
|
|
104
|
+
|
|
105
|
+
with _span(
|
|
106
|
+
f"Tool {tool}",
|
|
107
|
+
kind=SpanKind.INTERNAL,
|
|
108
|
+
attributes=attributes,
|
|
109
|
+
) as span:
|
|
110
|
+
|
|
111
|
+
yield span
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@contextmanager
|
|
115
|
+
def invoke_subagent(
|
|
116
|
+
*,
|
|
117
|
+
agent: str,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Records a child agent.
|
|
121
|
+
|
|
122
|
+
This becomes a nested span beneath the current agent.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
with _span(
|
|
126
|
+
f"Subagent {agent}",
|
|
127
|
+
kind=SpanKind.INTERNAL,
|
|
128
|
+
attributes={
|
|
129
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
130
|
+
"gen_ai.agent.name": agent,
|
|
131
|
+
},
|
|
132
|
+
) as span:
|
|
133
|
+
|
|
134
|
+
yield span
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@contextmanager
|
|
138
|
+
def execute_workflow(
|
|
139
|
+
*,
|
|
140
|
+
name: str,
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
Records an end-to-end AI workflow.
|
|
144
|
+
|
|
145
|
+
Example
|
|
146
|
+
|
|
147
|
+
Upload CSV
|
|
148
|
+
▼
|
|
149
|
+
Profile data
|
|
150
|
+
▼
|
|
151
|
+
Retrieve metadata
|
|
152
|
+
▼
|
|
153
|
+
Run LLM
|
|
154
|
+
▼
|
|
155
|
+
Validate
|
|
156
|
+
▼
|
|
157
|
+
Store output
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
with _span(
|
|
161
|
+
f"Workflow {name}",
|
|
162
|
+
kind=SpanKind.INTERNAL,
|
|
163
|
+
attributes={
|
|
164
|
+
"gen_ai.operation.name": "workflow",
|
|
165
|
+
"workflow.name": name,
|
|
166
|
+
},
|
|
167
|
+
) as span:
|
|
168
|
+
|
|
169
|
+
yield span
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from anthropic import Anthropic
|
|
2
|
+
|
|
3
|
+
from aice_observability.ai.tracing import trace_provider_call
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnthropicProvider:
|
|
7
|
+
|
|
8
|
+
def __init__(self, client: Anthropic):
|
|
9
|
+
self.client = client
|
|
10
|
+
|
|
11
|
+
def chat(self, **kwargs):
|
|
12
|
+
|
|
13
|
+
model = kwargs.get("model", "unknown")
|
|
14
|
+
|
|
15
|
+
return trace_provider_call(
|
|
16
|
+
provider="anthropic",
|
|
17
|
+
model=model,
|
|
18
|
+
operation="messages.create",
|
|
19
|
+
fn=lambda: self.client.messages.create(**kwargs),
|
|
20
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LLMProvider(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Base class implemented by every supported LLM provider.
|
|
11
|
+
|
|
12
|
+
Implementations are intentionally very small.
|
|
13
|
+
|
|
14
|
+
Their responsibility is simply to execute the SDK call while emitting
|
|
15
|
+
OpenTelemetry spans that follow the GenAI semantic conventions.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def provider(self) -> str:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def chat(self, **kwargs: Any) -> Any:
|
|
25
|
+
...
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from google import genai
|
|
2
|
+
|
|
3
|
+
from aice_observability.ai.tracing import trace_provider_call
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GeminiProvider:
|
|
7
|
+
|
|
8
|
+
def __init__(self, client: genai.Client):
|
|
9
|
+
self.client = client
|
|
10
|
+
|
|
11
|
+
def generate(self, **kwargs):
|
|
12
|
+
|
|
13
|
+
model = kwargs.get("model", "unknown")
|
|
14
|
+
|
|
15
|
+
return trace_provider_call(
|
|
16
|
+
provider="gemini",
|
|
17
|
+
model=model,
|
|
18
|
+
operation="generate_content",
|
|
19
|
+
fn=lambda: self.client.models.generate_content(**kwargs),
|
|
20
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from aice_observability.ai.tracing import trace_provider_call
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LangChainProvider:
|
|
5
|
+
|
|
6
|
+
def __init__(self, chain):
|
|
7
|
+
self.chain = chain
|
|
8
|
+
|
|
9
|
+
def invoke(self, inputs):
|
|
10
|
+
|
|
11
|
+
return trace_provider_call(
|
|
12
|
+
provider="langchain",
|
|
13
|
+
model=self.chain.__class__.__name__,
|
|
14
|
+
operation="invoke",
|
|
15
|
+
fn=lambda: self.chain.invoke(inputs),
|
|
16
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
from aice_observability.ai.tracing import trace_provider_call
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OpenAIProvider:
|
|
6
|
+
|
|
7
|
+
def __init__(self, client: OpenAI):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
def chat(self, **kwargs):
|
|
11
|
+
|
|
12
|
+
model = kwargs.get("model", "unknown")
|
|
13
|
+
|
|
14
|
+
return trace_provider_call(
|
|
15
|
+
provider="openai",
|
|
16
|
+
model=model,
|
|
17
|
+
operation="chat",
|
|
18
|
+
fn=lambda: self.client.responses.create(**kwargs),
|
|
19
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry for AI instrumentors.
|
|
3
|
+
|
|
4
|
+
Observability.start() should not know about every provider individually.
|
|
5
|
+
|
|
6
|
+
Instead it delegates to this registry.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from aice_observability.core.config import ObservabilityConfig
|
|
14
|
+
|
|
15
|
+
from .base import AIInstrumentor
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InstrumentationRegistry:
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._instrumentors: list[AIInstrumentor] = []
|
|
24
|
+
|
|
25
|
+
def register(
|
|
26
|
+
self,
|
|
27
|
+
instrumentor: AIInstrumentor,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._instrumentors.append(instrumentor)
|
|
30
|
+
|
|
31
|
+
def instrument_all(
|
|
32
|
+
self,
|
|
33
|
+
config: ObservabilityConfig,
|
|
34
|
+
) -> None:
|
|
35
|
+
|
|
36
|
+
for instrumentor in self._instrumentors:
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
|
|
40
|
+
success = instrumentor.instrument(config)
|
|
41
|
+
|
|
42
|
+
if success:
|
|
43
|
+
logger.info(
|
|
44
|
+
"Instrumented %s",
|
|
45
|
+
instrumentor.name,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
except Exception:
|
|
49
|
+
|
|
50
|
+
logger.exception(
|
|
51
|
+
"Failed to instrument %s",
|
|
52
|
+
instrumentor.name,
|
|
53
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from vertexai.generative_models import GenerativeModel
|
|
2
|
+
|
|
3
|
+
from aice_observability.ai.tracing import trace_provider_call
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VertexAIProvider:
|
|
7
|
+
|
|
8
|
+
def __init__(self, model: GenerativeModel):
|
|
9
|
+
self.model = model
|
|
10
|
+
|
|
11
|
+
def generate(self, prompt: str):
|
|
12
|
+
|
|
13
|
+
return trace_provider_call(
|
|
14
|
+
provider="vertexai",
|
|
15
|
+
model=self.model._model_name,
|
|
16
|
+
operation="generate_content",
|
|
17
|
+
fn=lambda: self.model.generate_content(prompt),
|
|
18
|
+
)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTelemetry metrics for AI applications.
|
|
3
|
+
|
|
4
|
+
Unlike tracing.py, which records one span per request, this module
|
|
5
|
+
produces aggregated metrics suitable for Prometheus.
|
|
6
|
+
|
|
7
|
+
All metrics intentionally use LOW-CARDINALITY labels.
|
|
8
|
+
|
|
9
|
+
Do NOT attach
|
|
10
|
+
|
|
11
|
+
user IDs
|
|
12
|
+
workspace IDs
|
|
13
|
+
prompts
|
|
14
|
+
document IDs
|
|
15
|
+
conversation IDs
|
|
16
|
+
|
|
17
|
+
Those belong in traces instead.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from opentelemetry import metrics
|
|
23
|
+
|
|
24
|
+
_meter = metrics.get_meter("observability.ai")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# COUNTERS
|
|
28
|
+
llm_requests = _meter.create_counter(
|
|
29
|
+
"gen_ai.client.requests",
|
|
30
|
+
description="Total LLM requests",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
llm_failures = _meter.create_counter(
|
|
34
|
+
"gen_ai.client.failures",
|
|
35
|
+
description="Failed LLM requests",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
tool_calls = _meter.create_counter(
|
|
39
|
+
"gen_ai.tool.calls",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
tool_failures = _meter.create_counter(
|
|
43
|
+
"gen_ai.tool.failures",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
retrieval_requests = _meter.create_counter(
|
|
47
|
+
"gen_ai.retrieval.requests",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
embedding_requests = _meter.create_counter(
|
|
51
|
+
"gen_ai.embedding.requests",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
#: Budget governance events (per-run warning / per-run exceeded / tenant
|
|
55
|
+
#: window exhausted). A single `kind` label keeps cardinality at three;
|
|
56
|
+
#: tenants stay off metrics per the cardinality rule.
|
|
57
|
+
budget_events = _meter.create_counter(
|
|
58
|
+
"gen_ai.budget.events",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# HISTOGRAMS
|
|
63
|
+
llm_latency = _meter.create_histogram(
|
|
64
|
+
"gen_ai.client.duration",
|
|
65
|
+
unit="s",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
tool_latency = _meter.create_histogram(
|
|
69
|
+
"gen_ai.tool.duration",
|
|
70
|
+
unit="s",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
retrieval_latency = _meter.create_histogram(
|
|
74
|
+
"gen_ai.retrieval.duration",
|
|
75
|
+
unit="s",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
input_tokens = _meter.create_histogram(
|
|
79
|
+
"gen_ai.usage.input_tokens",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
output_tokens = _meter.create_histogram(
|
|
83
|
+
"gen_ai.usage.output_tokens",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
total_tokens = _meter.create_histogram(
|
|
87
|
+
"gen_ai.usage.total_tokens",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
cost_usd = _meter.create_histogram(
|
|
91
|
+
"gen_ai.usage.cost",
|
|
92
|
+
unit="USD",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
retrieved_documents = _meter.create_histogram(
|
|
96
|
+
"gen_ai.retrieval.documents",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def record_llm_request(*, provider: str, model: str):
|
|
101
|
+
llm_requests.add(
|
|
102
|
+
1,
|
|
103
|
+
{
|
|
104
|
+
"provider": provider,
|
|
105
|
+
"model": model,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def record_llm_failure(*, provider: str, model: str):
|
|
111
|
+
llm_failures.add(
|
|
112
|
+
1,
|
|
113
|
+
{
|
|
114
|
+
"provider": provider,
|
|
115
|
+
"model": model,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def record_latency(*, provider: str, model: str, duration: float):
|
|
121
|
+
llm_latency.record(
|
|
122
|
+
duration,
|
|
123
|
+
{
|
|
124
|
+
"provider": provider,
|
|
125
|
+
"model": model,
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def record_token_usage(*, provider: str, model: str, input: int,output: int):
|
|
131
|
+
|
|
132
|
+
input_tokens.record(
|
|
133
|
+
input,
|
|
134
|
+
{
|
|
135
|
+
"provider": provider,
|
|
136
|
+
"model": model,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
output_tokens.record(
|
|
141
|
+
output,
|
|
142
|
+
{
|
|
143
|
+
"provider": provider,
|
|
144
|
+
"model": model,
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
total_tokens.record(
|
|
149
|
+
input + output,
|
|
150
|
+
{
|
|
151
|
+
"provider": provider,
|
|
152
|
+
"model": model,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def record_cost(*, provider: str, model: str, usd: float):
|
|
158
|
+
cost_usd.record(
|
|
159
|
+
usd,
|
|
160
|
+
{
|
|
161
|
+
"provider": provider,
|
|
162
|
+
"model": model,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def record_tool_call(tool: str):
|
|
168
|
+
tool_calls.add(
|
|
169
|
+
1,
|
|
170
|
+
{
|
|
171
|
+
"tool": tool,
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def record_tool_latency(*, tool: str, duration: float):
|
|
177
|
+
tool_latency.record(
|
|
178
|
+
duration,
|
|
179
|
+
{
|
|
180
|
+
"tool": tool,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def record_retrieval(*, vector_store: str, document_count: int, duration: float):
|
|
186
|
+
|
|
187
|
+
retrieval_requests.add(
|
|
188
|
+
1,
|
|
189
|
+
{
|
|
190
|
+
"store": vector_store,
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
retrieval_latency.record(
|
|
195
|
+
duration,
|
|
196
|
+
{
|
|
197
|
+
"store": vector_store,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
retrieved_documents.record(
|
|
202
|
+
document_count,
|
|
203
|
+
{
|
|
204
|
+
"store": vector_store,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def record_budget_event(kind: str):
|
|
210
|
+
"""
|
|
211
|
+
One budget governance event. ``kind`` is fixed-cardinality by design:
|
|
212
|
+
warning, run_exceeded, tenant_exhausted anything else is dropped by the
|
|
213
|
+
caller, not by an allowlist here, so a new kind surfaces loudly in a
|
|
214
|
+
dashboard rather than silently never being counted.
|
|
215
|
+
"""
|
|
216
|
+
budget_events.add(1, {"kind": kind})
|