traceAI-livekit 0.1.0__tar.gz

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.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.3
2
+ Name: traceAI-livekit
3
+ Version: 0.1.0
4
+ Summary: OpenTelemetry instrumentation for LiveKit
5
+ Author: Future AGI
6
+ Author-email: no-reply@futureagi.com
7
+ Requires-Python: >3.9,<3.14
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: fi-instrumentation-otel (>=0.1.14)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # traceAI-livekit
17
+
18
+ OpenTelemetry instrumentation for LiveKit agents integrated with Future AGI.
19
+
20
+ This package provides automatic attribute mapping from LiveKit's native OpenTelemetry instrumentation to Future AGI's semantic conventions, ensuring rich and structured traces in your dashboard.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install traceai-livekit
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ To instrument your LiveKit agent, simply initialize `fi-instrumentation` and enable the attribute mapping **inside your agent's entrypoint**.
31
+
32
+ ```python
33
+ from livekit.agents import AgentServer, JobContext
34
+ from fi_instrumentation.otel import register, ProjectType
35
+ from traceai_livekit import enable_http_attribute_mapping
36
+
37
+ server = AgentServer()
38
+
39
+ @server.rtc_session()
40
+ async def entrypoint(ctx: JobContext):
41
+ # 1. Initialize TraceAI (inside entrypoint for multiprocessing safety)
42
+ register(
43
+ project_name="My LiveKit Agent",
44
+ project_type=ProjectType.OBSERVE,
45
+ set_global_tracer_provider=True,
46
+ )
47
+
48
+ # 2. Enable LiveKit attribute mapping
49
+ # This automatically hooks into LiveKit's telemetry system
50
+ enable_http_attribute_mapping()
51
+
52
+ # ... rest of your agent logic ...
53
+ await ctx.connect()
54
+
55
+ if __name__ == "__main__":
56
+ server.run()
57
+ ```
58
+
59
+ ## Features
60
+
61
+ * **Automatic Attribute Mapping**: Converts LiveKit attributes (like `lk.user_input`, `gen_ai.usage.input_tokens`) to Future AGI standard attributes (`input.value`, `llm.tokenCount.prompt`).
62
+ * **Raw Data Preservation**: Keeps original LiveKit attributes and adds `raw.input` / `raw.output` for full debugging context.
63
+ * **Multiprocessing Support**: Designed to work with LiveKit's worker process model.
64
+ * **Event Parsing**: Extracts LLM outputs from `gen_ai.choice` events when available.
65
+
66
+ ## Examples
67
+
68
+ Check the `examples/` directory for complete working agents:
69
+ * `simple_agent.py`: A basic voice assistant agent.
70
+ * `egress_agent.py`: An agent with call recording (Egress) enabled.
71
+
@@ -0,0 +1,55 @@
1
+ # traceAI-livekit
2
+
3
+ OpenTelemetry instrumentation for LiveKit agents integrated with Future AGI.
4
+
5
+ This package provides automatic attribute mapping from LiveKit's native OpenTelemetry instrumentation to Future AGI's semantic conventions, ensuring rich and structured traces in your dashboard.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install traceai-livekit
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ To instrument your LiveKit agent, simply initialize `fi-instrumentation` and enable the attribute mapping **inside your agent's entrypoint**.
16
+
17
+ ```python
18
+ from livekit.agents import AgentServer, JobContext
19
+ from fi_instrumentation.otel import register, ProjectType
20
+ from traceai_livekit import enable_http_attribute_mapping
21
+
22
+ server = AgentServer()
23
+
24
+ @server.rtc_session()
25
+ async def entrypoint(ctx: JobContext):
26
+ # 1. Initialize TraceAI (inside entrypoint for multiprocessing safety)
27
+ register(
28
+ project_name="My LiveKit Agent",
29
+ project_type=ProjectType.OBSERVE,
30
+ set_global_tracer_provider=True,
31
+ )
32
+
33
+ # 2. Enable LiveKit attribute mapping
34
+ # This automatically hooks into LiveKit's telemetry system
35
+ enable_http_attribute_mapping()
36
+
37
+ # ... rest of your agent logic ...
38
+ await ctx.connect()
39
+
40
+ if __name__ == "__main__":
41
+ server.run()
42
+ ```
43
+
44
+ ## Features
45
+
46
+ * **Automatic Attribute Mapping**: Converts LiveKit attributes (like `lk.user_input`, `gen_ai.usage.input_tokens`) to Future AGI standard attributes (`input.value`, `llm.tokenCount.prompt`).
47
+ * **Raw Data Preservation**: Keeps original LiveKit attributes and adds `raw.input` / `raw.output` for full debugging context.
48
+ * **Multiprocessing Support**: Designed to work with LiveKit's worker process model.
49
+ * **Event Parsing**: Extracts LLM outputs from `gen_ai.choice` events when available.
50
+
51
+ ## Examples
52
+
53
+ Check the `examples/` directory for complete working agents:
54
+ * `simple_agent.py`: A basic voice assistant agent.
55
+ * `egress_agent.py`: An agent with call recording (Egress) enabled.
@@ -0,0 +1,23 @@
1
+ [tool.poetry]
2
+ name = "traceAI-livekit"
3
+ version = "0.1.0"
4
+ description = "OpenTelemetry instrumentation for LiveKit"
5
+ authors = ["Future AGI <no-reply@futureagi.com>"]
6
+ readme = "README.md"
7
+ packages = [
8
+ { include = "traceai_livekit" }
9
+ ]
10
+
11
+ [tool.poetry.dependencies]
12
+ python = ">3.9,<3.14"
13
+ fi-instrumentation-otel = ">=0.1.14"
14
+
15
+ [tool.poetry.group.dev.dependencies]
16
+ livekit-agents = "*"
17
+ livekit-plugins-openai = "*"
18
+ livekit-plugins-silero = "*"
19
+ python-dotenv = "*"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,33 @@
1
+ """
2
+ TraceAI LiveKit integration.
3
+
4
+ This package provides integration between LiveKit and Future AGI's tracing system,
5
+ allowing LiveKit applications to send telemetry data with proper attribute mapping.
6
+ """
7
+
8
+ from .exporters import (
9
+ BaseMappedSpanExporter,
10
+ MappedGRPCSpanExporter,
11
+ MappedHTTPSpanExporter,
12
+ )
13
+ from .integration import (
14
+ create_mapped_grpc_exporter,
15
+ create_mapped_http_exporter,
16
+ enable_fi_attribute_mapping,
17
+ enable_grpc_attribute_mapping,
18
+ enable_http_attribute_mapping,
19
+ )
20
+
21
+ __version__ = "0.1.0"
22
+
23
+ __all__ = [
24
+ "enable_fi_attribute_mapping",
25
+ "enable_http_attribute_mapping",
26
+ "enable_grpc_attribute_mapping",
27
+ "create_mapped_http_exporter",
28
+ "create_mapped_grpc_exporter",
29
+ # Exporters
30
+ "MappedHTTPSpanExporter",
31
+ "MappedGRPCSpanExporter",
32
+ "BaseMappedSpanExporter",
33
+ ]
@@ -0,0 +1,14 @@
1
+ """
2
+ Exporters for LiveKit integration with Future AGI.
3
+ """
4
+
5
+ from .base_exporter import BaseMappedSpanExporter
6
+ from .grpc_exporter import MappedGRPCSpanExporter
7
+ from .http_exporter import MappedHTTPSpanExporter
8
+
9
+ __all__ = [
10
+ "MappedHTTPSpanExporter",
11
+ "MappedGRPCSpanExporter",
12
+ "BaseMappedSpanExporter",
13
+ ]
14
+
@@ -0,0 +1,325 @@
1
+ """
2
+ Base exporter for LiveKit integration with Future AGI.
3
+
4
+ This module provides the base class for mapped span exporters that convert
5
+ LiveKit attributes to Future AGI conventions.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict
10
+
11
+ from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
12
+
13
+ # Import Future AGI span attribute keys
14
+ from fi_instrumentation.fi_types import SpanAttributes
15
+
16
+
17
+ def _ensure_json_string(value: Any) -> str:
18
+ """Ensure a value is a valid JSON string."""
19
+ try:
20
+ if isinstance(value, str):
21
+ # Validate if string is already JSON
22
+ json.loads(value)
23
+ return value
24
+ return json.dumps(value)
25
+ except Exception:
26
+ return json.dumps(str(value))
27
+
28
+
29
+ def _detect_mime_type(value: Any) -> str:
30
+ """Detect the MIME type of a value."""
31
+ if isinstance(value, (dict, list)):
32
+ return "application/json"
33
+ if isinstance(value, str):
34
+ stripped = value.strip()
35
+ if (stripped.startswith("{") and stripped.endswith("}")) or (
36
+ stripped.startswith("[") and stripped.endswith("]")
37
+ ):
38
+ return "application/json"
39
+ return "text/plain"
40
+
41
+
42
+ def _map_attributes_to_fi_conventions(attributes: Dict[str, Any]) -> Dict[str, Any]:
43
+ """Map LiveKit attributes to Future AGI conventions."""
44
+ if not attributes:
45
+ return {}
46
+
47
+ mapped: Dict[str, Any] = dict(attributes) # start by preserving originals
48
+
49
+ # --- LiveKit Constant Keys (from livekit-agents/telemetry/trace_types.py) ---
50
+
51
+ # Common Keys
52
+ LK_SPEECH_ID = "lk.speech_id"
53
+ LK_AGENT_LABEL = "lk.agent_label"
54
+ LK_START_TIME = "lk.start_time"
55
+ LK_END_TIME = "lk.end_time"
56
+ LK_RETRY_COUNT = "lk.retry_count"
57
+
58
+ LK_PARTICIPANT_ID = "lk.participant_id"
59
+ LK_PARTICIPANT_IDENTITY = "lk.participant_identity"
60
+ LK_PARTICIPANT_KIND = "lk.participant_kind"
61
+
62
+ # Session Start
63
+ LK_JOB_ID = "lk.job_id"
64
+ LK_AGENT_NAME = "lk.agent_name"
65
+ LK_ROOM_NAME = "lk.room_name"
66
+ LK_SESSION_OPTIONS = "lk.session_options"
67
+
68
+ # Assistant Turn
69
+ LK_USER_INPUT = "lk.user_input"
70
+ LK_INSTRUCTIONS = "lk.instructions"
71
+ LK_SPEECH_INTERRUPTED = "lk.interrupted"
72
+
73
+ # LLM Node
74
+ LK_CHAT_CTX = "lk.chat_ctx"
75
+ LK_FUNCTION_TOOLS = "lk.function_tools"
76
+ LK_RESPONSE_TEXT = "lk.response.text"
77
+ LK_RESPONSE_FUNCTION_CALLS = "lk.response.function_calls"
78
+
79
+ # Function Tool
80
+ LK_FUNCTION_TOOL_NAME = "lk.function_tool.name"
81
+ LK_FUNCTION_TOOL_ARGS = "lk.function_tool.arguments"
82
+ LK_FUNCTION_TOOL_IS_ERROR = "lk.function_tool.is_error"
83
+ LK_FUNCTION_TOOL_OUTPUT = "lk.function_tool.output"
84
+
85
+ # TTS Node
86
+ LK_TTS_INPUT_TEXT = "lk.input_text"
87
+ LK_TTS_STREAMING = "lk.tts.streaming"
88
+ LK_TTS_LABEL = "lk.tts.label"
89
+
90
+ # EOU Detection & Metrics (used for Metadata)
91
+ LK_EOU_DELAY = "lk.eou.endpointing_delay"
92
+ LK_EOU_LANGUAGE = "lk.eou.language"
93
+ LK_USER_TRANSCRIPT = "lk.user_transcript"
94
+ LK_TRANSCRIPT_CONFIDENCE = "lk.transcript_confidence"
95
+ LK_TRANSCRIPTION_DELAY = "lk.transcription_delay"
96
+ LK_END_OF_TURN_DELAY = "lk.end_of_turn_delay"
97
+
98
+ # Standard OTel GenAI
99
+ ATTR_GEN_AI_OPERATION_NAME = "gen_ai.operation.name"
100
+ ATTR_GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
101
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"
102
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
103
+
104
+ # --- MAPPING LOGIC ---
105
+
106
+ # 1. LLM & Model Info
107
+ if ATTR_GEN_AI_REQUEST_MODEL in attributes:
108
+ mapped[SpanAttributes.LLM_MODEL_NAME] = attributes.get(ATTR_GEN_AI_REQUEST_MODEL)
109
+
110
+ # 2. Inputs & Outputs
111
+ # LiveKit separates User Input (in Assistant Turn) vs LLM Response vs TTS Input
112
+
113
+ input_val = None
114
+ output_val = None
115
+ raw_input_val = None
116
+ raw_output_val = None
117
+
118
+ # Case A: Assistant Turn (Top-level chain)
119
+ # Input: User speech/text (LK_USER_INPUT)
120
+ # Output: often not directly on this span, but might be aggregated or absent
121
+ if LK_USER_INPUT in attributes:
122
+ input_val = attributes.get(LK_USER_INPUT)
123
+ raw_input_val = attributes.get(LK_USER_INPUT)
124
+
125
+ # Case B: LLM Node
126
+ # Input: Chat Context (LK_CHAT_CTX) - usually a list of messages
127
+ # Output: Generated Text (LK_RESPONSE_TEXT) or Function Calls (LK_RESPONSE_FUNCTION_CALLS)
128
+ if LK_CHAT_CTX in attributes:
129
+
130
+ chat_ctx = attributes.get(LK_CHAT_CTX)
131
+ input_val = _ensure_json_string(chat_ctx)
132
+ raw_input_val = chat_ctx
133
+
134
+ if LK_RESPONSE_TEXT in attributes:
135
+ output_val = attributes.get(LK_RESPONSE_TEXT)
136
+ raw_output_val = output_val
137
+
138
+ if LK_RESPONSE_FUNCTION_CALLS in attributes:
139
+ # If there are function calls, this is also an output
140
+ func_calls = attributes.get(LK_RESPONSE_FUNCTION_CALLS)
141
+ if output_val:
142
+ # If we already have text, append function calls
143
+ output_val = f"{output_val}\nFunction Calls: {_ensure_json_string(func_calls)}"
144
+ raw_output_val = {"text": raw_output_val, "function_calls": func_calls}
145
+ else:
146
+ output_val = _ensure_json_string(func_calls)
147
+ raw_output_val = func_calls
148
+
149
+ # Case C: TTS Node
150
+ # Input: Text to speak (LK_TTS_INPUT_TEXT)
151
+ # Output: Audio (not usually in attributes, maybe just success)
152
+ if LK_TTS_INPUT_TEXT in attributes:
153
+ input_val = attributes.get(LK_TTS_INPUT_TEXT)
154
+ raw_input_val = attributes.get(LK_TTS_INPUT_TEXT)
155
+ # TTS output is audio, we don't have the bytes here usually.
156
+
157
+ # Case D: Function Tool Execution
158
+ # Input: Arguments (LK_FUNCTION_TOOL_ARGS)
159
+ # Output: Tool Output (LK_FUNCTION_TOOL_OUTPUT)
160
+ if LK_FUNCTION_TOOL_ARGS in attributes:
161
+ input_val = attributes.get(LK_FUNCTION_TOOL_ARGS)
162
+ raw_input_val = attributes.get(LK_FUNCTION_TOOL_ARGS)
163
+
164
+ if LK_FUNCTION_TOOL_OUTPUT in attributes:
165
+ output_val = attributes.get(LK_FUNCTION_TOOL_OUTPUT)
166
+ raw_output_val = attributes.get(LK_FUNCTION_TOOL_OUTPUT)
167
+
168
+ # Case E: Session Start
169
+ if LK_SESSION_OPTIONS in attributes and raw_input_val is None:
170
+ raw_input_val = attributes.get(LK_SESSION_OPTIONS)
171
+
172
+ # --- Set Input/Output Attributes ---
173
+ if raw_input_val is not None:
174
+ mapped[SpanAttributes.RAW_INPUT] = _ensure_json_string(raw_input_val)
175
+
176
+ if raw_output_val is not None:
177
+ mapped[SpanAttributes.RAW_OUTPUT] = _ensure_json_string(raw_output_val)
178
+
179
+ if input_val is not None and SpanAttributes.INPUT_VALUE not in mapped:
180
+ mime_type = _detect_mime_type(input_val)
181
+ mapped[SpanAttributes.INPUT_VALUE] = (
182
+ _ensure_json_string(input_val) if mime_type == "application/json" else input_val
183
+ )
184
+ mapped[SpanAttributes.INPUT_MIME_TYPE] = mime_type
185
+
186
+ if output_val is not None and SpanAttributes.OUTPUT_VALUE not in mapped:
187
+ mime_type = _detect_mime_type(output_val)
188
+ mapped[SpanAttributes.OUTPUT_VALUE] = (
189
+ _ensure_json_string(output_val) if mime_type == "application/json" else output_val
190
+ )
191
+ mapped[SpanAttributes.OUTPUT_MIME_TYPE] = mime_type
192
+
193
+
194
+ # 3. Token Usage
195
+ if ATTR_GEN_AI_USAGE_INPUT_TOKENS in attributes:
196
+ mapped[SpanAttributes.LLM_TOKEN_COUNT_PROMPT] = attributes.get(ATTR_GEN_AI_USAGE_INPUT_TOKENS)
197
+ if ATTR_GEN_AI_USAGE_OUTPUT_TOKENS in attributes:
198
+ mapped[SpanAttributes.LLM_TOKEN_COUNT_COMPLETION] = attributes.get(ATTR_GEN_AI_USAGE_OUTPUT_TOKENS)
199
+
200
+ # Calculate total if not present
201
+ try:
202
+ prompt_tokens = mapped.get(SpanAttributes.LLM_TOKEN_COUNT_PROMPT)
203
+ completion_tokens = mapped.get(SpanAttributes.LLM_TOKEN_COUNT_COMPLETION)
204
+ if isinstance(prompt_tokens, (int, float)) and isinstance(completion_tokens, (int, float)):
205
+ mapped[SpanAttributes.LLM_TOKEN_COUNT_TOTAL] = int(prompt_tokens) + int(completion_tokens)
206
+ except Exception:
207
+ pass
208
+
209
+ # 4. Tools
210
+ if LK_FUNCTION_TOOL_NAME in attributes:
211
+ mapped[SpanAttributes.TOOL_NAME] = attributes.get(LK_FUNCTION_TOOL_NAME)
212
+ mapped["tool_call.function.name"] = attributes.get(LK_FUNCTION_TOOL_NAME) # Standardize
213
+
214
+ if LK_FUNCTION_TOOLS in attributes:
215
+ # List of available tools provided to LLM
216
+ mapped[SpanAttributes.LLM_TOOLS] = _ensure_json_string(attributes.get(LK_FUNCTION_TOOLS))
217
+
218
+ # 5. Session & User Info
219
+ if LK_ROOM_NAME in attributes:
220
+ mapped[SpanAttributes.SESSION_ID] = attributes.get(LK_ROOM_NAME)
221
+
222
+ if LK_PARTICIPANT_IDENTITY in attributes:
223
+ mapped[SpanAttributes.USER_ID] = attributes.get(LK_PARTICIPANT_IDENTITY)
224
+
225
+ # 6. Metadata Aggregation
226
+ metadata: Dict[str, Any] = {}
227
+
228
+ # Collect all LiveKit specific keys into metadata
229
+ metadata_keys = [
230
+ LK_SPEECH_ID, LK_AGENT_LABEL, LK_JOB_ID, LK_AGENT_NAME, LK_SESSION_OPTIONS,
231
+ LK_PARTICIPANT_ID, LK_PARTICIPANT_KIND,
232
+ LK_INSTRUCTIONS, LK_SPEECH_INTERRUPTED,
233
+ LK_TTS_STREAMING, LK_TTS_LABEL,
234
+ LK_EOU_DELAY, LK_EOU_LANGUAGE, LK_TRANSCRIPT_CONFIDENCE, LK_TRANSCRIPTION_DELAY, LK_END_OF_TURN_DELAY,
235
+ LK_USER_TRANSCRIPT, # Might be redundant with input, but good context
236
+ LK_FUNCTION_TOOL_IS_ERROR
237
+ ]
238
+
239
+ for key in metadata_keys:
240
+ if key in attributes:
241
+ metadata[key] = attributes[key]
242
+
243
+ # Also include any "metrics" attributes if they exist
244
+ for key in attributes:
245
+ if "metrics" in key or "lk.eou" in key:
246
+ metadata[key] = attributes[key]
247
+
248
+ # Merge with existing metadata
249
+ if metadata:
250
+ existing = attributes.get(SpanAttributes.METADATA)
251
+ try:
252
+ if isinstance(existing, str):
253
+ existing = json.loads(existing)
254
+ if isinstance(existing, dict):
255
+ metadata = {**existing, **metadata}
256
+ except Exception:
257
+ pass
258
+ mapped[SpanAttributes.METADATA] = _ensure_json_string(metadata)
259
+
260
+ # 7. Span Kind Determination (fi.span.kind)
261
+ if SpanAttributes.FI_SPAN_KIND not in mapped:
262
+ span_kind = "UNKNOWN"
263
+
264
+ # Check based on attribute presence or span name (if available in attributes? No, span name is on the Span object)
265
+ # We infer from unique attributes.
266
+
267
+ if LK_FUNCTION_TOOL_NAME in attributes:
268
+ span_kind = "TOOL"
269
+ elif LK_CHAT_CTX in attributes or ATTR_GEN_AI_REQUEST_MODEL in attributes:
270
+ span_kind = "LLM"
271
+ elif LK_TTS_INPUT_TEXT in attributes:
272
+ # TTS is technically model inference, map to LLM for now or generic
273
+ span_kind = "LLM"
274
+ elif LK_USER_INPUT in attributes or LK_SPEECH_ID in attributes:
275
+ # Assistant Turn -> CHAIN
276
+ span_kind = "CHAIN"
277
+ elif LK_JOB_ID in attributes and LK_ROOM_NAME in attributes and LK_SESSION_OPTIONS in attributes:
278
+ # Session Start -> AGENT
279
+ span_kind = "AGENT"
280
+
281
+ mapped[SpanAttributes.FI_SPAN_KIND] = span_kind
282
+
283
+ return mapped
284
+
285
+
286
+ class BaseMappedSpanExporter(SpanExporter):
287
+ """Base class for span exporters that map LiveKit attributes to Future AGI conventions."""
288
+
289
+ def _convert_attributes(self, attributes):
290
+ """Convert attributes by mapping them to Future AGI conventions."""
291
+ if attributes is None:
292
+ base = {}
293
+ elif not isinstance(attributes, dict):
294
+ base = dict(attributes)
295
+ else:
296
+ base = attributes
297
+ try:
298
+ return _map_attributes_to_fi_conventions(base)
299
+ except Exception:
300
+ return base
301
+
302
+ def export(self, spans) -> SpanExportResult:
303
+ for span in spans:
304
+ try:
305
+ if (
306
+ hasattr(span, "_attributes")
307
+ and getattr(span, "_attributes") is not None
308
+ ):
309
+ original_attributes = getattr(span, "_attributes")
310
+
311
+ base_attributes = dict(original_attributes)
312
+
313
+ # Include span name in mapping logic if needed, though _convert_attributes currently only takes dict
314
+ # We could pass span name to _map_attributes_to_fi_conventions if we refactored,
315
+ # but attribute-based detection is usually robust enough for LiveKit.
316
+
317
+ mapped_attributes = self._convert_attributes(base_attributes)
318
+
319
+ setattr(span, "_attributes", mapped_attributes)
320
+
321
+ except Exception:
322
+ continue
323
+
324
+ return super().export(spans)
325
+
@@ -0,0 +1,33 @@
1
+ """
2
+ gRPC exporter for LiveKit integration with Future AGI.
3
+
4
+ This module provides a gRPC span exporter that maps LiveKit attributes
5
+ to Future AGI conventions.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from .base_exporter import BaseMappedSpanExporter
11
+
12
+ try:
13
+ # Use FI's gRPC exporter as base
14
+ from fi_instrumentation.otel import GRPCSpanExporter as _FIGRPCSpanExporter
15
+ except ImportError:
16
+ _FIGRPCSpanExporter = None
17
+
18
+
19
+ class MappedGRPCSpanExporter(BaseMappedSpanExporter, _FIGRPCSpanExporter):
20
+ """gRPC exporter that maps LiveKit attributes to Future AGI conventions."""
21
+
22
+ def __init__(self, *args: Any, **kwargs: Any):
23
+ """Initialize the mapped gRPC exporter."""
24
+ if _FIGRPCSpanExporter is None:
25
+ raise ImportError(
26
+ "fi_instrumentation is not installed. "
27
+ 'Please install the fi_instrumentation with: pip install "fi-instrumentation-otel[grpc]"'
28
+ )
29
+ super().__init__(*args, **kwargs)
30
+
31
+ def _convert_attributes(self, attributes):
32
+ """Override to use the base class mapping."""
33
+ return super()._convert_attributes(attributes)
@@ -0,0 +1,33 @@
1
+ """
2
+ HTTP exporter for LiveKit integration with Future AGI.
3
+
4
+ This module provides an HTTP span exporter that maps LiveKit attributes
5
+ to Future AGI conventions.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from .base_exporter import BaseMappedSpanExporter
11
+
12
+ try:
13
+ # Use FI's HTTP exporter as base
14
+ from fi_instrumentation.otel import HTTPSpanExporter as _FIHTTPSpanExporter
15
+ except ImportError:
16
+ _FIHTTPSpanExporter = None
17
+
18
+
19
+ class MappedHTTPSpanExporter(BaseMappedSpanExporter, _FIHTTPSpanExporter):
20
+ """HTTP exporter that maps LiveKit attributes to Future AGI conventions."""
21
+
22
+ def __init__(self, *args: Any, **kwargs: Any):
23
+ """Initialize the mapped HTTP exporter."""
24
+ if _FIHTTPSpanExporter is None:
25
+ raise ImportError(
26
+ "fi_instrumentation is not installed. "
27
+ 'Please install the fi_instrumentation with: pip install "fi-instrumentation-otel"'
28
+ )
29
+ super().__init__(*args, **kwargs)
30
+
31
+ def _convert_attributes(self, attributes):
32
+ """Override to use the base class mapping."""
33
+ return super()._convert_attributes(attributes)
@@ -0,0 +1,198 @@
1
+ """
2
+ Integration functions for LiveKit with Future AGI.
3
+
4
+ This module provides functions to install attribute mapping for LiveKit applications
5
+ by updating existing span exporters in the tracer provider.
6
+ """
7
+
8
+ import logging
9
+ from typing import Optional
10
+
11
+ from fi_instrumentation.otel import Transport
12
+ from opentelemetry import trace as trace_api
13
+
14
+ from .exporters import MappedGRPCSpanExporter, MappedHTTPSpanExporter
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def enable_fi_attribute_mapping(transport: Transport = Transport.HTTP) -> bool:
20
+ """
21
+ Install the mapping exporter by swapping FI's exporter instances.
22
+
23
+ This function:
24
+ 1. Finds existing span exporters in the global tracer provider
25
+ 2. Replaces them with mapped exporters that convert LiveKit attributes to Future AGI conventions.
26
+ 3. Automatically registers the global tracer provider with `livekit.agents.telemetry`.
27
+
28
+ Args:
29
+ transport: The transport protocol to match existing exporters (Transport enum)
30
+
31
+ Returns:
32
+ True if at least one exporter was swapped; False otherwise.
33
+ """
34
+ provider = trace_api.get_tracer_provider()
35
+ swapped_any = False
36
+
37
+ active = getattr(provider, "_active_span_processor", None)
38
+ if not active:
39
+ logger.warning("No active span processor found")
40
+ return False
41
+
42
+ # Handle both single processor and MultiSpanProcessor cases
43
+ processors = []
44
+ if hasattr(active, "_span_processors"):
45
+ processors = list(active._span_processors)
46
+ else:
47
+ processors = [active]
48
+
49
+ for proc in processors:
50
+ # Look for exporter in standard places
51
+ exporter = getattr(proc, "span_exporter", None)
52
+ if exporter is None:
53
+ exporter = getattr(proc, "_exporter", None)
54
+
55
+ if exporter is None:
56
+ continue
57
+
58
+ try:
59
+ if transport == Transport.HTTP:
60
+ from fi_instrumentation.otel import (
61
+ HTTPSpanExporter as _FIHTTPSpanExporter,
62
+ )
63
+
64
+ if isinstance(exporter, _FIHTTPSpanExporter):
65
+ endpoint = getattr(exporter, "_endpoint", None)
66
+ headers = getattr(exporter, "_headers", None)
67
+ new_exporter = MappedHTTPSpanExporter(
68
+ endpoint=endpoint, headers=headers
69
+ )
70
+
71
+ # Replace exporter in the processor
72
+ if hasattr(proc, "_batch_processor"):
73
+ # Some batch processor implementations wrap logic
74
+ setattr(proc._batch_processor, "_exporter", new_exporter)
75
+ elif hasattr(proc, "span_exporter"):
76
+ try:
77
+ setattr(proc, "span_exporter", new_exporter)
78
+ except AttributeError:
79
+ # Fallback for read-only property, try internal
80
+ if hasattr(proc, "_exporter"):
81
+ setattr(proc, "_exporter", new_exporter)
82
+ elif hasattr(proc, "_exporter"):
83
+ setattr(proc, "_exporter", new_exporter)
84
+
85
+ swapped_any = True
86
+ logger.info("Replaced HTTP exporter with mapped LiveKit HTTP exporter")
87
+
88
+ elif transport == Transport.GRPC:
89
+ from fi_instrumentation.otel import (
90
+ GRPCSpanExporter as _FIGRPCSpanExporter,
91
+ )
92
+
93
+ if isinstance(exporter, _FIGRPCSpanExporter):
94
+ endpoint = getattr(exporter, "_endpoint", None)
95
+ headers = getattr(exporter, "_headers", None)
96
+ new_exporter = MappedGRPCSpanExporter(
97
+ endpoint=endpoint, headers=headers
98
+ )
99
+
100
+ if hasattr(proc, "_batch_processor"):
101
+ setattr(proc._batch_processor, "_exporter", new_exporter)
102
+ elif hasattr(proc, "span_exporter"):
103
+ try:
104
+ setattr(proc, "span_exporter", new_exporter)
105
+ except AttributeError:
106
+ if hasattr(proc, "_exporter"):
107
+ setattr(proc, "_exporter", new_exporter)
108
+ elif hasattr(proc, "_exporter"):
109
+ setattr(proc, "_exporter", new_exporter)
110
+
111
+ swapped_any = True
112
+ logger.info("Replaced gRPC exporter with mapped LiveKit gRPC exporter")
113
+
114
+ except ImportError as e:
115
+ logger.warning(f"Failed to import required exporter: {e}")
116
+ continue
117
+ except Exception as e:
118
+ logger.warning(f"Failed to replace exporter: {e}")
119
+ continue
120
+
121
+ if not swapped_any:
122
+ logger.warning(f"No {transport.value} exporters found to replace")
123
+
124
+ # Automatically set the tracer provider for LiveKit agents
125
+ try:
126
+ from livekit.agents.telemetry import set_tracer_provider
127
+ set_tracer_provider(provider)
128
+ logger.info("Automatically configured LiveKit agents tracer provider")
129
+ except ImportError:
130
+ logger.warning("livekit.agents.telemetry not found. Skipping set_tracer_provider.")
131
+ except Exception as e:
132
+ logger.warning(f"Failed to set LiveKit tracer provider: {e}")
133
+
134
+ return swapped_any
135
+
136
+
137
+ def enable_http_attribute_mapping() -> bool:
138
+ """
139
+ Install HTTP attribute mapping for LiveKit and configure the LiveKit tracer provider.
140
+
141
+ Convenience function that calls enable_fi_attribute_mapping with HTTP transport.
142
+
143
+ Returns:
144
+ True if at least one exporter was swapped; False otherwise.
145
+ """
146
+ return enable_fi_attribute_mapping(transport=Transport.HTTP)
147
+
148
+
149
+ def enable_grpc_attribute_mapping() -> bool:
150
+ """
151
+ Install gRPC attribute mapping for LiveKit and configure the LiveKit tracer provider.
152
+
153
+ Convenience function that calls enable_fi_attribute_mapping with gRPC transport.
154
+
155
+ Returns:
156
+ True if at least one exporter was swapped; False otherwise.
157
+ """
158
+ return enable_fi_attribute_mapping(transport=Transport.GRPC)
159
+
160
+
161
+ def create_mapped_http_exporter(
162
+ endpoint: Optional[str] = None, headers: Optional[dict] = None
163
+ ):
164
+ """
165
+ Create a mapped HTTP exporter for LiveKit.
166
+
167
+ This function creates a new HTTP exporter that maps LiveKit attributes
168
+ to Future AGI conventions. It can be used independently without
169
+ modifying existing exporters.
170
+
171
+ Args:
172
+ endpoint: The collector endpoint (optional, will use environment defaults)
173
+ headers: Optional headers to include in requests (optional, will use environment defaults)
174
+
175
+ Returns:
176
+ A configured MappedHTTPSpanExporter instance
177
+ """
178
+ return MappedHTTPSpanExporter(endpoint=endpoint, headers=headers)
179
+
180
+
181
+ def create_mapped_grpc_exporter(
182
+ endpoint: Optional[str] = None, headers: Optional[dict] = None
183
+ ):
184
+ """
185
+ Create a mapped gRPC exporter for LiveKit.
186
+
187
+ This function creates a new gRPC exporter that maps LiveKit attributes
188
+ to Future AGI conventions. It can be used independently without
189
+ modifying existing exporters.
190
+
191
+ Args:
192
+ endpoint: The collector endpoint (optional, will use environment defaults)
193
+ headers: Optional headers to include in requests (optional, will use environment defaults)
194
+
195
+ Returns:
196
+ A configured MappedGRPCSpanExporter instance
197
+ """
198
+ return MappedGRPCSpanExporter(endpoint=endpoint, headers=headers)