Skip to content

Commit d97fdca

Browse files
authored
Merge branch 'genai-utils-e2e-dev' into util-genai-inference
2 parents cab5ca0 + 521824f commit d97fdca

File tree

43 files changed

+3574
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3574
-12
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Installation
2+
============
3+
4+
Option 1: pip + requirements.txt
5+
---------------------------------
6+
::
7+
8+
python3 -m venv .venv
9+
source .venv/bin/activate
10+
pip install -r requirements.txt
11+
12+
Option 2: Poetry
13+
----------------
14+
::
15+
16+
poetry install
17+
18+
Running Tests
19+
=============
20+
21+
After installing dependencies, simply run:
22+
23+
::
24+
25+
pytest
26+
27+
This will discover and run `tests/test_sdk.py`.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "opentelemetry-genai-sdk"
7+
dynamic = ["version"]
8+
description = "OpenTelemetry GenAI SDK"
9+
readme = "README.rst"
10+
license = "Apache-2.0"
11+
requires-python = ">=3.8"
12+
authors = [
13+
{ name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
14+
]
15+
classifiers = [
16+
"Development Status :: 4 - Beta",
17+
"Intended Audience :: Developers",
18+
"License :: OSI Approved :: Apache Software License",
19+
"Programming Language :: Python",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.9",
22+
"Programming Language :: Python :: 3.10",
23+
"Programming Language :: Python :: 3.11",
24+
"Programming Language :: Python :: 3.12",
25+
"Programming Language :: Python :: 3.13",
26+
]
27+
dependencies = [
28+
"opentelemetry-api ~= 1.36.0",
29+
"opentelemetry-instrumentation ~= 0.57b0",
30+
"opentelemetry-semantic-conventions ~= 0.57b0",
31+
]
32+
33+
[project.optional-dependencies]
34+
test = [
35+
"pytest>=7.0.0",
36+
]
37+
# evaluation = ["deepevals>=0.1.0", "openlit-sdk>=0.1.0"]
38+
39+
[project.urls]
40+
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-genai-sdk"
41+
Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"
42+
43+
[tool.hatch.version]
44+
path = "src/opentelemetry/genai/sdk/version.py"
45+
46+
[tool.hatch.build.targets.sdist]
47+
include = [
48+
"/src",
49+
"/tests",
50+
]
51+
52+
[tool.hatch.build.targets.wheel]
53+
packages = ["src/opentelemetry"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# OpenTelemetry SDK
2+
opentelemetry-api>=1.34.0
3+
opentelemetry-sdk>=1.34.0
4+
5+
# Testing
6+
pytest>=7.0.0
7+
8+
# (Optional) evaluation libraries
9+
# deepevals>=0.1.0
10+
# openlit-sdk>=0.1.0
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 .types import LLMInvocation, ToolInvocation
21+
from .exporters import SpanMetricEventExporter, SpanMetricExporter
22+
from .data import Message, ChatGeneration, Error, ToolOutput, ToolFunction
23+
24+
from opentelemetry.instrumentation.langchain.version import __version__
25+
from opentelemetry.metrics import get_meter
26+
from opentelemetry.trace import get_tracer
27+
from opentelemetry._events import get_event_logger
28+
from opentelemetry._logs import get_logger
29+
from opentelemetry.semconv.schemas import Schemas
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+
def __init__(self, exporter_type_full: bool = True, **kwargs):
38+
tracer_provider = kwargs.get("tracer_provider")
39+
self._tracer = get_tracer(
40+
__name__, __version__, tracer_provider, schema_url=Schemas.V1_28_0.value
41+
)
42+
43+
meter_provider = kwargs.get("meter_provider")
44+
self._meter = get_meter(
45+
__name__, __version__, meter_provider, schema_url=Schemas.V1_28_0.value
46+
)
47+
48+
event_logger_provider = kwargs.get("event_logger_provider")
49+
self._event_logger = get_event_logger(
50+
__name__, __version__, event_logger_provider=event_logger_provider, schema_url=Schemas.V1_28_0.value
51+
)
52+
53+
logger_provider = kwargs.get("logger_provider")
54+
self._logger = get_logger(
55+
__name__, __version__, logger_provider=logger_provider, schema_url=Schemas.V1_28_0.value
56+
)
57+
58+
self._exporter = (
59+
SpanMetricEventExporter(tracer=self._tracer, meter=self._meter, event_logger=self._event_logger, logger=self._event_logger)
60+
if exporter_type_full
61+
else SpanMetricExporter(tracer=self._tracer, meter=self._meter)
62+
)
63+
64+
self._llm_registry: dict[UUID, LLMInvocation] = {}
65+
self._tool_registry: dict[UUID, ToolInvocation] = {}
66+
self._lock = Lock()
67+
68+
def start_llm(self, prompts: List[Message], tool_functions: List[ToolFunction], run_id: UUID, parent_run_id: Optional[UUID] = None, **attributes):
69+
invocation = LLMInvocation(messages=prompts , tool_functions=tool_functions, run_id=run_id, parent_run_id=parent_run_id, attributes=attributes)
70+
with self._lock:
71+
self._llm_registry[invocation.run_id] = invocation
72+
self._exporter.init_llm(invocation)
73+
74+
def stop_llm(self, run_id: UUID, chat_generations: List[ChatGeneration], **attributes) -> LLMInvocation:
75+
with self._lock:
76+
invocation = self._llm_registry.pop(run_id)
77+
invocation.end_time = time.time()
78+
invocation.chat_generations = chat_generations
79+
invocation.attributes.update(attributes)
80+
self._exporter.export_llm(invocation)
81+
return invocation
82+
83+
def fail_llm(self, run_id: UUID, error: Error, **attributes) -> LLMInvocation:
84+
with self._lock:
85+
invocation = self._llm_registry.pop(run_id)
86+
invocation.end_time = time.time()
87+
invocation.attributes.update(**attributes)
88+
self._exporter.error_llm(error, invocation)
89+
return invocation
90+
91+
def start_tool(self, input_str: str, run_id: UUID, parent_run_id: Optional[UUID] = None, **attributes):
92+
invocation = ToolInvocation(input_str=input_str , run_id=run_id, parent_run_id=parent_run_id, attributes=attributes)
93+
with self._lock:
94+
self._tool_registry[invocation.run_id] = invocation
95+
self._exporter.init_tool(invocation)
96+
97+
def stop_tool(self, run_id: UUID, output: ToolOutput, **attributes) -> ToolInvocation:
98+
with self._lock:
99+
invocation = self._tool_registry.pop(run_id)
100+
invocation.end_time = time.time()
101+
invocation.output = output
102+
self._exporter.export_tool(invocation)
103+
return invocation
104+
105+
def fail_tool(self, run_id: UUID, error: Error, **attributes) -> ToolInvocation:
106+
with self._lock:
107+
invocation = self._tool_registry.pop(run_id)
108+
invocation.end_time = time.time()
109+
invocation.attributes.update(**attributes)
110+
self._exporter.error_tool(error, invocation)
111+
return invocation
112+
113+
# Singleton accessor
114+
_default_client: TelemetryClient | None = None
115+
116+
def get_telemetry_client(exporter_type_full: bool = True, **kwargs) -> TelemetryClient:
117+
global _default_client
118+
if _default_client is None:
119+
_default_client = TelemetryClient(exporter_type_full=exporter_type_full, **kwargs)
120+
return _default_client
121+
122+
# Module‐level convenience functions
123+
def llm_start(prompts: List[Message], run_id: UUID, parent_run_id: Optional[UUID] = None, **attributes):
124+
return get_telemetry_client().start_llm(prompts=prompts, run_id=run_id, parent_run_id=parent_run_id, **attributes)
125+
126+
def llm_stop(run_id: UUID, chat_generations: List[ChatGeneration], **attributes) -> LLMInvocation:
127+
return get_telemetry_client().stop_llm(run_id=run_id, chat_generations=chat_generations, **attributes)
128+
129+
def llm_fail(run_id: UUID, error: Error, **attributes) -> LLMInvocation:
130+
return get_telemetry_client().fail_llm(run_id=run_id, error=error, **attributes)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
@dataclass
11+
class ToolFunction:
12+
name: str
13+
description: str
14+
parameters: str
15+
16+
@dataclass
17+
class ToolFunctionCall:
18+
id: str
19+
name: str
20+
arguments: str
21+
type: str
22+
23+
@dataclass
24+
class Message:
25+
content: str
26+
type: str
27+
name: str
28+
tool_call_id: str
29+
tool_function_calls: List[ToolFunctionCall] = field(default_factory=list)
30+
31+
@dataclass
32+
class ChatGeneration:
33+
content: str
34+
type: str
35+
finish_reason: str = None
36+
tool_function_calls: List[ToolFunctionCall] = field(default_factory=list)
37+
38+
@dataclass
39+
class Error:
40+
message: str
41+
type: type[BaseException]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from deepeval.models import DeepEvalBaseLLM
2+
from deepeval.test_case import LLMTestCase
3+
from deepeval.metrics import AnswerRelevancyMetric
4+
5+
6+
def evaluate_answer_relevancy_metric(prompt:str, output:str, retrieval_context:list) -> AnswerRelevancyMetric:
7+
test_case = LLMTestCase(input=prompt,
8+
actual_output=output,
9+
retrieval_context=retrieval_context,)
10+
relevancy_metric = AnswerRelevancyMetric(threshold=0.5)
11+
relevancy_metric.measure(test_case)
12+
print(relevancy_metric.score, relevancy_metric.reason)
13+
return relevancy_metric

0 commit comments

Comments
 (0)