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.
- traceai_livekit-0.1.0/PKG-INFO +71 -0
- traceai_livekit-0.1.0/README.md +55 -0
- traceai_livekit-0.1.0/pyproject.toml +23 -0
- traceai_livekit-0.1.0/traceai_livekit/__init__.py +33 -0
- traceai_livekit-0.1.0/traceai_livekit/exporters/__init__.py +14 -0
- traceai_livekit-0.1.0/traceai_livekit/exporters/base_exporter.py +325 -0
- traceai_livekit-0.1.0/traceai_livekit/exporters/grpc_exporter.py +33 -0
- traceai_livekit-0.1.0/traceai_livekit/exporters/http_exporter.py +33 -0
- traceai_livekit-0.1.0/traceai_livekit/integration.py +198 -0
|
@@ -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)
|