zooid 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. zooid/__init__.py +25 -0
  2. zooid/_version.py +1 -0
  3. zooid/context.py +218 -0
  4. zooid/dashboards/trust_center_dashboard.json +102 -0
  5. zooid/dashboards/voice_agent_dashboard.json +114 -0
  6. zooid/exporters/__init__.py +1 -0
  7. zooid/exporters/signoz.py +49 -0
  8. zooid/instrumentor.py +150 -0
  9. zooid/metrics.py +108 -0
  10. zooid/scaffold/__init__.py +6 -0
  11. zooid/scaffold/cli.py +94 -0
  12. zooid/scaffold/generator.py +69 -0
  13. zooid/scaffold/templates/__init__.py +1 -0
  14. zooid/scaffold/templates/agent.py +262 -0
  15. zooid/scaffold/templates/common.py +95 -0
  16. zooid/scaffold/templates/frameworks.py +123 -0
  17. zooid/scaffold/templates/project.py +362 -0
  18. zooid/skills/zooid-instrumentation/SKILL.md +180 -0
  19. zooid/trust/__init__.py +27 -0
  20. zooid/trust/aggregator.py +64 -0
  21. zooid/trust/ai_advisor.py +64 -0
  22. zooid/trust/context_writer.py +115 -0
  23. zooid/trust/mcp_server.py +163 -0
  24. zooid/trust/recommendations.py +71 -0
  25. zooid/trust/reporter.py +40 -0
  26. zooid/trust/rules/__init__.py +9 -0
  27. zooid/trust/rules/base.py +61 -0
  28. zooid/trust/rules/naming_rules.py +58 -0
  29. zooid/trust/rules/resource_rules.py +86 -0
  30. zooid/trust/rules/span_rules.py +125 -0
  31. zooid/trust/rules/voice_rules.py +50 -0
  32. zooid/trust/scorer.py +87 -0
  33. zooid/wrappers/__init__.py +14 -0
  34. zooid/wrappers/base_wrapper.py +68 -0
  35. zooid/wrappers/deepgram_wrapper.py +121 -0
  36. zooid/wrappers/elevenlabs_wrapper.py +125 -0
  37. zooid/wrappers/openai_wrapper.py +180 -0
  38. zooid-0.1.0.dist-info/METADATA +164 -0
  39. zooid-0.1.0.dist-info/RECORD +42 -0
  40. zooid-0.1.0.dist-info/WHEEL +4 -0
  41. zooid-0.1.0.dist-info/entry_points.txt +3 -0
  42. zooid-0.1.0.dist-info/licenses/LICENSE +21 -0
zooid/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from ._version import __version__
4
+ from .instrumentor import instrument_voice_app
5
+ from .context import (
6
+ voice_turn,
7
+ VoiceTurnContext,
8
+ create_traced_task,
9
+ traced_callback,
10
+ get_current_turn,
11
+ )
12
+ from .metrics import VoiceMetrics
13
+ from .wrappers.base_wrapper import unpatch_all
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "instrument_voice_app",
18
+ "voice_turn",
19
+ "VoiceTurnContext",
20
+ "create_traced_task",
21
+ "traced_callback",
22
+ "VoiceMetrics",
23
+ "get_current_turn",
24
+ "unpatch_all",
25
+ ]
zooid/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
zooid/context.py ADDED
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextvars
5
+ import inspect
6
+ import time
7
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Coroutine, Dict, Optional, TypeVar, cast
8
+ import functools
9
+
10
+ from opentelemetry import trace, context as otel_context
11
+ from opentelemetry.trace.status import Status, StatusCode
12
+
13
+ tracer = trace.get_tracer(__name__)
14
+
15
+ _voice_turn_var: contextvars.ContextVar[Optional["VoiceTurnContext"]] = contextvars.ContextVar(
16
+ "_voice_turn_var", default=None
17
+ )
18
+
19
+ def get_current_turn() -> Optional[VoiceTurnContext]:
20
+ """Get the current voice turn context if one exists."""
21
+ return _voice_turn_var.get()
22
+
23
+ class VoiceTurnContext:
24
+ """Context manager for tracking voice turn timing and metrics."""
25
+
26
+ def __init__(
27
+ self,
28
+ architecture_mode: str = "sequential",
29
+ attributes: Optional[Dict[str, Any]] = None
30
+ ):
31
+ self.architecture_mode = architecture_mode
32
+ self.attributes = attributes or {}
33
+ self.start_time: float = 0.0
34
+ self.ttfa_ms: Optional[float] = None
35
+ self.stt_duration_ms: Optional[float] = None
36
+ self.llm_first_token_ms: Optional[float] = None
37
+ self.span: Optional[trace.Span] = None
38
+ self._token: Optional[contextvars.Token] = None
39
+ self._span_token: Optional[object] = None
40
+
41
+ @classmethod
42
+ def get_current(cls) -> Optional["VoiceTurnContext"]:
43
+ """Class method alias to get current voice turn context."""
44
+ return get_current_turn()
45
+
46
+ @property
47
+ def tracer(self) -> trace.Tracer:
48
+ """Expose tracer instance."""
49
+ return tracer
50
+
51
+ def record_stt_complete(self) -> None:
52
+ if self.start_time:
53
+ self.stt_duration_ms = (time.monotonic() - self.start_time) * 1000.0
54
+
55
+ def mark_stt_complete(self) -> None:
56
+ self.record_stt_complete()
57
+
58
+ def record_llm_first_token(self) -> None:
59
+ if self.start_time and self.llm_first_token_ms is None:
60
+ self.llm_first_token_ms = (time.monotonic() - self.start_time) * 1000.0
61
+
62
+ def mark_llm_first_token(self) -> None:
63
+ self.record_llm_first_token()
64
+
65
+ def record_ttfa(self) -> None:
66
+ if self.start_time and self.ttfa_ms is None:
67
+ self.ttfa_ms = (time.monotonic() - self.start_time) * 1000.0
68
+ if self.span:
69
+ self.span.set_attribute("voice.time_to_first_audio_ms", self.ttfa_ms)
70
+
71
+ def mark_first_audio(self) -> None:
72
+ self.record_ttfa()
73
+
74
+ async def __aenter__(self) -> "VoiceTurnContext":
75
+ self.start_time = time.monotonic()
76
+
77
+ attrs = {
78
+ "voice.architecture_mode": self.architecture_mode,
79
+ **self.attributes
80
+ }
81
+
82
+ self.span = tracer.start_span("voice.turn", attributes=attrs)
83
+ self._span_token = otel_context.attach(trace.set_span_in_context(self.span))
84
+ self._token = _voice_turn_var.set(self)
85
+
86
+ return self
87
+
88
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
89
+ try:
90
+ if exc_type is not None:
91
+ if self.span:
92
+ self.span.set_status(Status(StatusCode.ERROR, str(exc_val)))
93
+ if issubclass(exc_type, asyncio.CancelledError):
94
+ self.span.set_attribute("voice.interrupted", True)
95
+
96
+ if self.span:
97
+ if self.ttfa_ms is not None:
98
+ self.span.set_attribute("voice.time_to_first_audio_ms", self.ttfa_ms)
99
+ if self.stt_duration_ms is not None:
100
+ self.span.set_attribute("voice.stt_duration_ms", self.stt_duration_ms)
101
+ if self.llm_first_token_ms is not None:
102
+ self.span.set_attribute("voice.llm_first_token_ms", self.llm_first_token_ms)
103
+ self.span.end()
104
+ finally:
105
+ if self._span_token is not None:
106
+ otel_context.detach(self._span_token)
107
+ if self._token is not None:
108
+ _voice_turn_var.reset(self._token)
109
+
110
+ def voice_turn(architecture_mode: str = "sequential") -> Callable:
111
+ """Decorator to wrap a function in a VoiceTurnContext."""
112
+ def decorator(func: Callable) -> Callable:
113
+ @functools.wraps(func)
114
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
115
+ async with VoiceTurnContext(architecture_mode=architecture_mode):
116
+ return await func(*args, **kwargs)
117
+ return wrapper
118
+ return decorator
119
+
120
+ T = TypeVar("T")
121
+
122
+ def create_traced_task(coro: Coroutine[Any, Any, T], name: Optional[str] = None) -> asyncio.Task[T]:
123
+ """Create an asyncio task that inherits the current OpenTelemetry and Voice turn context."""
124
+ current_otel_context = otel_context.get_current()
125
+ current_voice_turn = _voice_turn_var.get()
126
+
127
+ async def wrapped_coro() -> T:
128
+ token = otel_context.attach(current_otel_context)
129
+ vt_token = _voice_turn_var.set(current_voice_turn)
130
+ try:
131
+ return await coro
132
+ finally:
133
+ otel_context.detach(token)
134
+ _voice_turn_var.reset(vt_token)
135
+
136
+ return asyncio.create_task(wrapped_coro(), name=name)
137
+
138
+ def traced_callback(func: Callable) -> Callable:
139
+ """Decorator for preserving context in callbacks (sync or async)."""
140
+ current_otel_context = otel_context.get_current()
141
+ current_voice_turn = _voice_turn_var.get()
142
+
143
+ if inspect.iscoroutinefunction(func):
144
+ @functools.wraps(func)
145
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
146
+ token = otel_context.attach(current_otel_context)
147
+ vt_token = _voice_turn_var.set(current_voice_turn)
148
+ try:
149
+ return await func(*args, **kwargs)
150
+ finally:
151
+ otel_context.detach(token)
152
+ _voice_turn_var.reset(vt_token)
153
+ return async_wrapper
154
+ else:
155
+ @functools.wraps(func)
156
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
157
+ token = otel_context.attach(current_otel_context)
158
+ vt_token = _voice_turn_var.set(current_voice_turn)
159
+ try:
160
+ return func(*args, **kwargs)
161
+ finally:
162
+ otel_context.detach(token)
163
+ _voice_turn_var.reset(vt_token)
164
+ return sync_wrapper
165
+
166
+ async def traced_async_generator(
167
+ original: AsyncGenerator[T, None],
168
+ span: trace.Span,
169
+ on_first_item: Optional[Callable[[], None]] = None
170
+ ) -> AsyncGenerator[T, None]:
171
+ """Wrap an async generator to maintain context and track chunks."""
172
+ current_otel_context = otel_context.get_current()
173
+ current_voice_turn = _voice_turn_var.get()
174
+
175
+ chunk_count = 0
176
+
177
+ try:
178
+ token = otel_context.attach(current_otel_context)
179
+ vt_token = _voice_turn_var.set(current_voice_turn)
180
+
181
+ try:
182
+ async for item in original:
183
+ if chunk_count == 0 and on_first_item:
184
+ on_first_item()
185
+ chunk_count += 1
186
+
187
+ # Detach before yielding to avoid leaking context to consumer
188
+ otel_context.detach(token)
189
+ _voice_turn_var.reset(vt_token)
190
+
191
+ yield item
192
+
193
+ # Re-attach after consumer returns control
194
+ token = otel_context.attach(current_otel_context)
195
+ vt_token = _voice_turn_var.set(current_voice_turn)
196
+
197
+ finally:
198
+ # Final detach in case we broke out of the loop
199
+ # Or if it completed normally, we still need to detach the re-attached context
200
+ try:
201
+ otel_context.detach(token)
202
+ except Exception:
203
+ pass
204
+ try:
205
+ _voice_turn_var.reset(vt_token)
206
+ except Exception:
207
+ pass
208
+
209
+ span.set_attribute("gen.chunk_count", chunk_count)
210
+ span.end()
211
+ except asyncio.CancelledError:
212
+ span.set_status(Status(StatusCode.ERROR, "Cancelled"))
213
+ span.end()
214
+ raise
215
+ except Exception as e:
216
+ span.set_status(Status(StatusCode.ERROR, str(e)))
217
+ span.end()
218
+ raise
@@ -0,0 +1,102 @@
1
+ {
2
+ "title": "Zooid Trust Center",
3
+ "description": "Dashboard for voice agent instrumentation scoring and health",
4
+ "widgets": [
5
+ {
6
+ "id": "w1",
7
+ "title": "Instrumentation Score",
8
+ "description": "Average instrumentation score across services",
9
+ "panelType": "value",
10
+ "query": {
11
+ "metrics": [
12
+ {
13
+ "name": "instrumentation_score",
14
+ "aggregate": "avg",
15
+ "groupBy": ["service_name"]
16
+ }
17
+ ]
18
+ },
19
+ "layout": { "w": 4, "h": 2, "x": 0, "y": 0 }
20
+ },
21
+ {
22
+ "id": "w2",
23
+ "title": "Score by Service",
24
+ "description": "Detailed scores per service",
25
+ "panelType": "table",
26
+ "query": {
27
+ "metrics": [
28
+ {
29
+ "name": "instrumentation_score",
30
+ "aggregate": "avg",
31
+ "groupBy": ["service_name"]
32
+ }
33
+ ]
34
+ },
35
+ "layout": { "w": 8, "h": 4, "x": 4, "y": 0 }
36
+ },
37
+ {
38
+ "id": "w3",
39
+ "title": "Score Trend",
40
+ "description": "Score over time",
41
+ "panelType": "graph",
42
+ "query": {
43
+ "metrics": [
44
+ {
45
+ "name": "instrumentation_score",
46
+ "aggregate": "avg",
47
+ "groupBy": ["service_name"]
48
+ }
49
+ ]
50
+ },
51
+ "layout": { "w": 6, "h": 4, "x": 0, "y": 2 }
52
+ },
53
+ {
54
+ "id": "w4",
55
+ "title": "Top Violations",
56
+ "description": "Most frequent instrumentation violations",
57
+ "panelType": "bar",
58
+ "query": {
59
+ "metrics": [
60
+ {
61
+ "name": "instrumentation_violations_total",
62
+ "aggregate": "sum",
63
+ "groupBy": ["rule_id"]
64
+ }
65
+ ]
66
+ },
67
+ "layout": { "w": 6, "h": 4, "x": 6, "y": 4 }
68
+ },
69
+ {
70
+ "id": "w5",
71
+ "title": "Unscorable Spans",
72
+ "description": "Total unscorable spans",
73
+ "panelType": "value",
74
+ "query": {
75
+ "metrics": [
76
+ {
77
+ "name": "instrumentation_unscorable_total",
78
+ "aggregate": "sum"
79
+ }
80
+ ]
81
+ },
82
+ "layout": { "w": 4, "h": 2, "x": 0, "y": 6 }
83
+ },
84
+ {
85
+ "id": "w6",
86
+ "title": "Category Distribution",
87
+ "description": "Distribution of violation categories",
88
+ "panelType": "pie",
89
+ "query": {
90
+ "metrics": [
91
+ {
92
+ "name": "instrumentation_violations_total",
93
+ "aggregate": "sum",
94
+ "groupBy": ["category"]
95
+ }
96
+ ]
97
+ },
98
+ "layout": { "w": 4, "h": 4, "x": 4, "y": 6 }
99
+ }
100
+ ],
101
+ "layout": []
102
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "title": "Voice Agent Dashboard",
3
+ "description": "Performance and health metrics for voice agents",
4
+ "widgets": [
5
+ {
6
+ "id": "w1",
7
+ "title": "TTFA (p50)",
8
+ "description": "Time to First Audio (50th percentile)",
9
+ "panelType": "value",
10
+ "query": {
11
+ "metrics": [
12
+ {
13
+ "name": "voice_time_to_first_audio_ms",
14
+ "aggregate": "p50"
15
+ }
16
+ ]
17
+ },
18
+ "layout": { "w": 4, "h": 2, "x": 0, "y": 0 }
19
+ },
20
+ {
21
+ "id": "w2",
22
+ "title": "TTFA Distribution",
23
+ "description": "Histogram of Time to First Audio",
24
+ "panelType": "graph",
25
+ "query": {
26
+ "metrics": [
27
+ {
28
+ "name": "voice_ttfa_distribution",
29
+ "aggregate": "sum"
30
+ }
31
+ ]
32
+ },
33
+ "layout": { "w": 8, "h": 4, "x": 4, "y": 0 }
34
+ },
35
+ {
36
+ "id": "w3",
37
+ "title": "TTFA by Architecture",
38
+ "description": "Time to First Audio categorized by voice architecture",
39
+ "panelType": "bar",
40
+ "query": {
41
+ "metrics": [
42
+ {
43
+ "name": "voice_time_to_first_audio_ms",
44
+ "aggregate": "avg",
45
+ "groupBy": ["voice_architecture"]
46
+ }
47
+ ]
48
+ },
49
+ "layout": { "w": 6, "h": 4, "x": 0, "y": 2 }
50
+ },
51
+ {
52
+ "id": "w4",
53
+ "title": "Stage Latency",
54
+ "description": "Latency for STT, LLM, TTS stages",
55
+ "panelType": "table",
56
+ "query": {
57
+ "metrics": [
58
+ {
59
+ "name": "voice_stage_latency_ms",
60
+ "aggregate": "avg",
61
+ "groupBy": ["stage_name"]
62
+ }
63
+ ]
64
+ },
65
+ "layout": { "w": 6, "h": 4, "x": 6, "y": 4 }
66
+ },
67
+ {
68
+ "id": "w5",
69
+ "title": "Total Turns",
70
+ "description": "Total number of conversational turns",
71
+ "panelType": "value",
72
+ "query": {
73
+ "metrics": [
74
+ {
75
+ "name": "voice_turns_total",
76
+ "aggregate": "sum"
77
+ }
78
+ ]
79
+ },
80
+ "layout": { "w": 4, "h": 2, "x": 0, "y": 6 }
81
+ },
82
+ {
83
+ "id": "w6",
84
+ "title": "Error Rate",
85
+ "description": "Percentage of turns with errors",
86
+ "panelType": "value",
87
+ "query": {
88
+ "metrics": [
89
+ {
90
+ "name": "voice_errors_total",
91
+ "aggregate": "sum"
92
+ }
93
+ ]
94
+ },
95
+ "layout": { "w": 4, "h": 2, "x": 4, "y": 6 }
96
+ },
97
+ {
98
+ "id": "w7",
99
+ "title": "Interruptions",
100
+ "description": "User interruptions during generation",
101
+ "panelType": "value",
102
+ "query": {
103
+ "metrics": [
104
+ {
105
+ "name": "voice_interruptions_total",
106
+ "aggregate": "sum"
107
+ }
108
+ ]
109
+ },
110
+ "layout": { "w": 4, "h": 2, "x": 8, "y": 6 }
111
+ }
112
+ ],
113
+ "layout": []
114
+ }
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, Optional
5
+
6
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
7
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
8
+
9
+
10
+ @dataclass
11
+ class SigNozConfig:
12
+ endpoint: str = "localhost:4317"
13
+ insecure: bool = True
14
+ headers: Dict[str, str] = field(default_factory=dict)
15
+
16
+ @property
17
+ def formatted_endpoint(self) -> str:
18
+ if "://" in self.endpoint:
19
+ return self.endpoint
20
+ return f"http://{self.endpoint}" if self.insecure else f"https://{self.endpoint}"
21
+
22
+
23
+ def create_signoz_exporter(
24
+ endpoint: str = "localhost:4317",
25
+ insecure: bool = True,
26
+ headers: Optional[Dict[str, str]] = None
27
+ ) -> OTLPSpanExporter:
28
+ """Helper function to create a SigNoz specific OTLP Span Exporter."""
29
+ config = SigNozConfig(endpoint=endpoint, insecure=insecure, headers=headers or {})
30
+
31
+ return OTLPSpanExporter(
32
+ endpoint=config.formatted_endpoint,
33
+ insecure=config.insecure,
34
+ headers=config.headers
35
+ )
36
+
37
+ def create_signoz_metric_exporter(
38
+ endpoint: str = "localhost:4317",
39
+ insecure: bool = True,
40
+ headers: Optional[Dict[str, str]] = None
41
+ ) -> OTLPMetricExporter:
42
+ """Helper function to create a SigNoz specific OTLP Metric Exporter."""
43
+ config = SigNozConfig(endpoint=endpoint, insecure=insecure, headers=headers or {})
44
+
45
+ return OTLPMetricExporter(
46
+ endpoint=config.formatted_endpoint,
47
+ insecure=config.insecure,
48
+ headers=config.headers
49
+ )
zooid/instrumentor.py ADDED
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from typing import Optional, Any
6
+
7
+ from opentelemetry import trace
8
+ from opentelemetry import metrics
9
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION, DEPLOYMENT_ENVIRONMENT
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
12
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
13
+ from opentelemetry.sdk.metrics import MeterProvider
14
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
15
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
16
+
17
+ from .metrics import VoiceMetrics
18
+ from ._version import __version__
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class InstrumentationHandle:
23
+ def __init__(
24
+ self,
25
+ tracer_provider: TracerProvider,
26
+ meter_provider: MeterProvider,
27
+ voice_metrics: VoiceMetrics,
28
+ trust_processor: Optional[Any] = None,
29
+ context_writer: Optional[Any] = None
30
+ ):
31
+ self.tracer_provider = tracer_provider
32
+ self.meter_provider = meter_provider
33
+ self.voice_metrics = voice_metrics
34
+ self.trust_processor = trust_processor
35
+ self.context_writer = context_writer
36
+
37
+ def shutdown(self) -> None:
38
+ """Shutdown the providers and the background context writer."""
39
+ if self.context_writer is not None:
40
+ try:
41
+ self.context_writer.stop()
42
+ except Exception as e:
43
+ logger.error("Failed to stop context writer: %s", e)
44
+ try:
45
+ self.tracer_provider.shutdown()
46
+ self.meter_provider.shutdown()
47
+ except Exception as e:
48
+ logger.error("Failed to shutdown OpenTelemetry providers: %s", e)
49
+
50
+ def __enter__(self) -> "InstrumentationHandle":
51
+ return self
52
+
53
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
54
+ self.shutdown()
55
+
56
+ def instrument_voice_app(
57
+ service_name: str = 'voice-agent',
58
+ signoz_endpoint: str = 'localhost:4317',
59
+ insecure: bool = True,
60
+ enable_trust_center: bool = True,
61
+ trust_center_mode: str = 'template',
62
+ llm_endpoint: Optional[str] = None,
63
+ export_interval_ms: int = 5000,
64
+ context_path: Optional[str] = None
65
+ ) -> InstrumentationHandle:
66
+ """Instrument a voice application with OpenTelemetry for SigNoz."""
67
+
68
+ resource = Resource.create({
69
+ SERVICE_NAME: service_name,
70
+ SERVICE_VERSION: __version__,
71
+ DEPLOYMENT_ENVIRONMENT: "production",
72
+ "telemetry.sdk.name": "zooid",
73
+ "telemetry.sdk.language": "python",
74
+ "telemetry.sdk.version": __version__,
75
+ })
76
+
77
+ endpoint_url = signoz_endpoint if "://" in signoz_endpoint else f"http://{signoz_endpoint}"
78
+
79
+ # Trace setup
80
+ tracer_provider = TracerProvider(resource=resource)
81
+ span_exporter = OTLPSpanExporter(endpoint=endpoint_url, insecure=insecure)
82
+ span_processor = BatchSpanProcessor(span_exporter)
83
+ tracer_provider.add_span_processor(span_processor)
84
+
85
+ trust_processor = None
86
+ context_writer = None
87
+ if enable_trust_center:
88
+ try:
89
+ from .trust import InstrumentationScoreProcessor
90
+ trust_processor = InstrumentationScoreProcessor(
91
+ mode=trust_center_mode,
92
+ llm_endpoint=llm_endpoint,
93
+ )
94
+ tracer_provider.add_span_processor(trust_processor)
95
+
96
+ # Mirror the live score to .zooid/context.json for coding agents / MCP.
97
+ from .trust.context_writer import ContextSyncWriter, DEFAULT_PATH
98
+ path = context_path or os.environ.get("ZOOID_CONTEXT_PATH", DEFAULT_PATH)
99
+ context_writer = ContextSyncWriter(
100
+ trust_processor.aggregator, service_name, path=path
101
+ ).start()
102
+ except ImportError:
103
+ logger.warning("Could not import InstrumentationScoreProcessor. Ensure it exists in .trust module.")
104
+ except Exception as e:
105
+ logger.warning("Failed to start Trust Center / context writer: %s", e)
106
+
107
+ trace.set_tracer_provider(tracer_provider)
108
+
109
+ # Metrics setup
110
+ metric_exporter = OTLPMetricExporter(endpoint=endpoint_url, insecure=insecure)
111
+ metric_reader = PeriodicExportingMetricReader(
112
+ metric_exporter, export_interval_millis=export_interval_ms
113
+ )
114
+ meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
115
+ metrics.set_meter_provider(meter_provider)
116
+
117
+ voice_metrics = VoiceMetrics(meter_provider)
118
+
119
+ # Patch libraries (ignoring ImportError for optional dependencies)
120
+ try:
121
+ from .wrappers.openai_wrapper import patch_openai
122
+ patch_openai()
123
+ except ImportError:
124
+ pass
125
+ except Exception as e:
126
+ logger.warning("Failed to patch OpenAI: %s", e)
127
+
128
+ try:
129
+ from .wrappers.elevenlabs_wrapper import patch_elevenlabs
130
+ patch_elevenlabs()
131
+ except ImportError:
132
+ pass
133
+ except Exception as e:
134
+ logger.warning("Failed to patch ElevenLabs: %s", e)
135
+
136
+ try:
137
+ from .wrappers.deepgram_wrapper import patch_deepgram
138
+ patch_deepgram()
139
+ except ImportError:
140
+ pass
141
+ except Exception as e:
142
+ logger.warning("Failed to patch Deepgram: %s", e)
143
+
144
+ return InstrumentationHandle(
145
+ tracer_provider=tracer_provider,
146
+ meter_provider=meter_provider,
147
+ voice_metrics=voice_metrics,
148
+ trust_processor=trust_processor,
149
+ context_writer=context_writer
150
+ )