Skip to content

Commit 00c2091

Browse files
committed
WIP initial code import
1 parent bbcdf8b commit 00c2091

File tree

5 files changed

+1750
-0
lines changed

5 files changed

+1750
-0
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import time
16+
from threading import Lock
17+
from typing import List, Optional
18+
from uuid import UUID
19+
20+
from opentelemetry._events import get_event_logger
21+
from opentelemetry._logs import get_logger
22+
from opentelemetry.metrics import get_meter
23+
from opentelemetry.semconv.schemas import Schemas
24+
from opentelemetry.trace import get_tracer
25+
26+
from .data import ChatGeneration, Error, Message, ToolFunction, ToolOutput
27+
from .exporters import SpanMetricEventExporter, SpanMetricExporter
28+
from .types import LLMInvocation, ToolInvocation
29+
from .version import __version__
30+
31+
32+
class TelemetryClient:
33+
"""
34+
High-level client managing GenAI invocation lifecycles and exporting
35+
them as spans, metrics, and events.
36+
"""
37+
38+
def __init__(self, exporter_type_full: bool = True, **kwargs):
39+
tracer_provider = kwargs.get("tracer_provider")
40+
self._tracer = get_tracer(
41+
__name__,
42+
__version__,
43+
tracer_provider,
44+
schema_url=Schemas.V1_28_0.value,
45+
)
46+
47+
meter_provider = kwargs.get("meter_provider")
48+
self._meter = get_meter(
49+
__name__,
50+
__version__,
51+
meter_provider,
52+
schema_url=Schemas.V1_28_0.value,
53+
)
54+
55+
event_logger_provider = kwargs.get("event_logger_provider")
56+
self._event_logger = get_event_logger(
57+
__name__,
58+
__version__,
59+
event_logger_provider=event_logger_provider,
60+
schema_url=Schemas.V1_28_0.value,
61+
)
62+
63+
logger_provider = kwargs.get("logger_provider")
64+
self._logger = get_logger(
65+
__name__,
66+
__version__,
67+
logger_provider=logger_provider,
68+
schema_url=Schemas.V1_28_0.value,
69+
)
70+
71+
self._exporter = (
72+
SpanMetricEventExporter(
73+
tracer=self._tracer,
74+
meter=self._meter,
75+
event_logger=self._event_logger,
76+
logger=self._event_logger,
77+
)
78+
if exporter_type_full
79+
else SpanMetricExporter(tracer=self._tracer, meter=self._meter)
80+
)
81+
82+
self._llm_registry: dict[UUID, LLMInvocation] = {}
83+
self._tool_registry: dict[UUID, ToolInvocation] = {}
84+
self._lock = Lock()
85+
86+
def start_llm(
87+
self,
88+
prompts: List[Message],
89+
tool_functions: List[ToolFunction],
90+
run_id: UUID,
91+
parent_run_id: Optional[UUID] = None,
92+
**attributes,
93+
):
94+
invocation = LLMInvocation(
95+
messages=prompts,
96+
tool_functions=tool_functions,
97+
run_id=run_id,
98+
parent_run_id=parent_run_id,
99+
attributes=attributes,
100+
)
101+
with self._lock:
102+
self._llm_registry[invocation.run_id] = invocation
103+
self._exporter.init_llm(invocation)
104+
105+
def stop_llm(
106+
self,
107+
run_id: UUID,
108+
chat_generations: List[ChatGeneration],
109+
**attributes,
110+
) -> LLMInvocation:
111+
with self._lock:
112+
invocation = self._llm_registry.pop(run_id)
113+
invocation.end_time = time.time()
114+
invocation.chat_generations = chat_generations
115+
invocation.attributes.update(attributes)
116+
self._exporter.export_llm(invocation)
117+
return invocation
118+
119+
def fail_llm(
120+
self, run_id: UUID, error: Error, **attributes
121+
) -> LLMInvocation:
122+
with self._lock:
123+
invocation = self._llm_registry.pop(run_id)
124+
invocation.end_time = time.time()
125+
invocation.attributes.update(**attributes)
126+
self._exporter.error_llm(error, invocation)
127+
return invocation
128+
129+
def start_tool(
130+
self,
131+
input_str: str,
132+
run_id: UUID,
133+
parent_run_id: Optional[UUID] = None,
134+
**attributes,
135+
):
136+
invocation = ToolInvocation(
137+
input_str=input_str,
138+
run_id=run_id,
139+
parent_run_id=parent_run_id,
140+
attributes=attributes,
141+
)
142+
with self._lock:
143+
self._tool_registry[invocation.run_id] = invocation
144+
self._exporter.init_tool(invocation)
145+
146+
def stop_tool(
147+
self, run_id: UUID, output: ToolOutput, **attributes
148+
) -> ToolInvocation:
149+
with self._lock:
150+
invocation = self._tool_registry.pop(run_id)
151+
invocation.end_time = time.time()
152+
invocation.output = output
153+
self._exporter.export_tool(invocation)
154+
return invocation
155+
156+
def fail_tool(
157+
self, run_id: UUID, error: Error, **attributes
158+
) -> ToolInvocation:
159+
with self._lock:
160+
invocation = self._tool_registry.pop(run_id)
161+
invocation.end_time = time.time()
162+
invocation.attributes.update(**attributes)
163+
self._exporter.error_tool(error, invocation)
164+
return invocation
165+
166+
167+
# Singleton accessor
168+
_default_client: TelemetryClient | None = None
169+
170+
171+
def get_telemetry_client(
172+
exporter_type_full: bool = True, **kwargs
173+
) -> TelemetryClient:
174+
global _default_client
175+
if _default_client is None:
176+
_default_client = TelemetryClient(
177+
exporter_type_full=exporter_type_full, **kwargs
178+
)
179+
return _default_client
180+
181+
182+
# Module‐level convenience functions
183+
def llm_start(
184+
prompts: List[Message],
185+
run_id: UUID,
186+
parent_run_id: Optional[UUID] = None,
187+
**attributes,
188+
):
189+
return get_telemetry_client().start_llm(
190+
prompts=prompts,
191+
run_id=run_id,
192+
parent_run_id=parent_run_id,
193+
**attributes,
194+
)
195+
196+
197+
def llm_stop(
198+
run_id: UUID, chat_generations: List[ChatGeneration], **attributes
199+
) -> LLMInvocation:
200+
return get_telemetry_client().stop_llm(
201+
run_id=run_id, chat_generations=chat_generations, **attributes
202+
)
203+
204+
205+
def llm_fail(run_id: UUID, error: Error, **attributes) -> LLMInvocation:
206+
return get_telemetry_client().fail_llm(
207+
run_id=run_id, error=error, **attributes
208+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from dataclasses import dataclass, field
2+
from typing import List
3+
4+
5+
@dataclass
6+
class ToolOutput:
7+
tool_call_id: str
8+
content: str
9+
10+
11+
@dataclass
12+
class ToolFunction:
13+
name: str
14+
description: str
15+
parameters: str
16+
17+
18+
@dataclass
19+
class ToolFunctionCall:
20+
id: str
21+
name: str
22+
arguments: str
23+
type: str
24+
25+
26+
@dataclass
27+
class Message:
28+
content: str
29+
type: str
30+
name: str
31+
tool_call_id: str
32+
tool_function_calls: List[ToolFunctionCall] = field(default_factory=list)
33+
34+
35+
@dataclass
36+
class ChatGeneration:
37+
content: str
38+
type: str
39+
finish_reason: str = None
40+
tool_function_calls: List[ToolFunctionCall] = field(default_factory=list)
41+
42+
43+
@dataclass
44+
class Error:
45+
message: str
46+
type: type[BaseException]

0 commit comments

Comments
 (0)