langtrace-python-sdk 2.1.28__py3-none-any.whl → 2.2.1__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.
- examples/cohere_example/chat.py +1 -0
- examples/cohere_example/chat_stream.py +3 -0
- examples/gemini_example/__init__.py +6 -0
- examples/gemini_example/function_tools.py +62 -0
- examples/gemini_example/main.py +91 -0
- examples/langchain_example/__init__.py +8 -0
- examples/langchain_example/groq_example.py +28 -15
- examples/ollama_example/basic.py +1 -0
- examples/openai_example/__init__.py +1 -0
- examples/openai_example/async_tool_calling_nonstreaming.py +1 -1
- examples/openai_example/chat_completion.py +1 -1
- examples/openai_example/embeddings_create.py +1 -0
- examples/openai_example/images_edit.py +2 -2
- examples/vertexai_example/__init__.py +6 -0
- examples/vertexai_example/main.py +214 -0
- langtrace_python_sdk/constants/instrumentation/common.py +2 -0
- langtrace_python_sdk/constants/instrumentation/gemini.py +12 -0
- langtrace_python_sdk/constants/instrumentation/vertexai.py +42 -0
- langtrace_python_sdk/instrumentation/__init__.py +4 -0
- langtrace_python_sdk/instrumentation/anthropic/patch.py +68 -96
- langtrace_python_sdk/instrumentation/chroma/patch.py +29 -29
- langtrace_python_sdk/instrumentation/cohere/patch.py +143 -242
- langtrace_python_sdk/instrumentation/gemini/__init__.py +3 -0
- langtrace_python_sdk/instrumentation/gemini/instrumentation.py +36 -0
- langtrace_python_sdk/instrumentation/gemini/patch.py +186 -0
- langtrace_python_sdk/instrumentation/groq/patch.py +82 -125
- langtrace_python_sdk/instrumentation/ollama/patch.py +62 -65
- langtrace_python_sdk/instrumentation/openai/patch.py +190 -494
- langtrace_python_sdk/instrumentation/qdrant/patch.py +6 -6
- langtrace_python_sdk/instrumentation/vertexai/__init__.py +3 -0
- langtrace_python_sdk/instrumentation/vertexai/instrumentation.py +33 -0
- langtrace_python_sdk/instrumentation/vertexai/patch.py +131 -0
- langtrace_python_sdk/langtrace.py +7 -1
- langtrace_python_sdk/utils/__init__.py +14 -3
- langtrace_python_sdk/utils/llm.py +311 -6
- langtrace_python_sdk/version.py +1 -1
- {langtrace_python_sdk-2.1.28.dist-info → langtrace_python_sdk-2.2.1.dist-info}/METADATA +26 -19
- {langtrace_python_sdk-2.1.28.dist-info → langtrace_python_sdk-2.2.1.dist-info}/RECORD +55 -36
- tests/anthropic/test_anthropic.py +28 -27
- tests/cohere/test_cohere_chat.py +36 -36
- tests/cohere/test_cohere_embed.py +12 -9
- tests/cohere/test_cohere_rerank.py +18 -11
- tests/groq/cassettes/test_async_chat_completion.yaml +113 -0
- tests/groq/cassettes/test_async_chat_completion_streaming.yaml +2232 -0
- tests/groq/cassettes/test_chat_completion.yaml +114 -0
- tests/groq/cassettes/test_chat_completion_streaming.yaml +2512 -0
- tests/groq/conftest.py +33 -0
- tests/groq/test_groq.py +142 -0
- tests/openai/cassettes/test_async_chat_completion_streaming.yaml +28 -28
- tests/openai/test_chat_completion.py +53 -67
- tests/openai/test_image_generation.py +47 -24
- tests/utils.py +40 -5
- {langtrace_python_sdk-2.1.28.dist-info → langtrace_python_sdk-2.2.1.dist-info}/WHEEL +0 -0
- {langtrace_python_sdk-2.1.28.dist-info → langtrace_python_sdk-2.2.1.dist-info}/entry_points.txt +0 -0
- {langtrace_python_sdk-2.1.28.dist-info → langtrace_python_sdk-2.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|
|
17
17
|
import json
|
|
18
18
|
from langtrace.trace_attributes import DatabaseSpanAttributes
|
|
19
19
|
from langtrace_python_sdk.utils.silently_fail import silently_fail
|
|
20
|
-
from langtrace_python_sdk.utils
|
|
20
|
+
from langtrace_python_sdk.utils import set_span_attribute
|
|
21
21
|
from opentelemetry import baggage, trace
|
|
22
22
|
from opentelemetry.trace import SpanKind
|
|
23
23
|
from opentelemetry.trace.status import Status, StatusCode
|
|
@@ -64,7 +64,7 @@ def collection_patch(method, version, tracer):
|
|
|
64
64
|
) as span:
|
|
65
65
|
collection_name = kwargs.get("collection_name") or args[0]
|
|
66
66
|
operation = api["OPERATION"]
|
|
67
|
-
|
|
67
|
+
set_span_attribute(span, "db.collection.name", collection_name)
|
|
68
68
|
|
|
69
69
|
if operation == "add":
|
|
70
70
|
_set_upload_attributes(span, args, kwargs, "documents")
|
|
@@ -111,7 +111,7 @@ def _set_upsert_attributes(span, args, kwargs):
|
|
|
111
111
|
else:
|
|
112
112
|
# In case of using Batch.
|
|
113
113
|
length = len(points.ids)
|
|
114
|
-
|
|
114
|
+
set_span_attribute(span, "db.upsert.points_count", length)
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
@silently_fail
|
|
@@ -123,16 +123,16 @@ def _set_upload_attributes(span, args, kwargs, field):
|
|
|
123
123
|
# In case of using Batch.
|
|
124
124
|
length = len(docs.ids)
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
set_span_attribute(span, f"db.upload.{field}_count", length)
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
@silently_fail
|
|
130
130
|
def _set_search_attributes(span, args, kwargs):
|
|
131
131
|
limit = kwargs.get("limit") or 10
|
|
132
|
-
|
|
132
|
+
set_span_attribute(span, "db.query.top_k", limit)
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
@silently_fail
|
|
136
136
|
def _set_batch_search_attributes(span, args, kwargs, method):
|
|
137
137
|
requests = kwargs.get("requests") or []
|
|
138
|
-
|
|
138
|
+
set_span_attribute(span, f"db.{method}.requests_count", len(requests))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Collection
|
|
2
|
+
from importlib_metadata import version as v
|
|
3
|
+
from langtrace_python_sdk.constants.instrumentation.vertexai import APIS
|
|
4
|
+
from wrapt import wrap_function_wrapper as _W
|
|
5
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
6
|
+
from opentelemetry.trace import get_tracer
|
|
7
|
+
from .patch import patch_vertexai
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VertexAIInstrumentation(BaseInstrumentor):
|
|
11
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
12
|
+
return ["google-cloud-aiplatform >= 1.0.0"]
|
|
13
|
+
|
|
14
|
+
def _instrument(self, **kwargs):
|
|
15
|
+
trace_provider = kwargs.get("tracer_provider")
|
|
16
|
+
tracer = get_tracer(__name__, "", trace_provider)
|
|
17
|
+
version = v("google-cloud-aiplatform")
|
|
18
|
+
|
|
19
|
+
for _, api_config in APIS.items():
|
|
20
|
+
|
|
21
|
+
module = api_config.get("module")
|
|
22
|
+
operation = api_config.get("operation")
|
|
23
|
+
method = api_config.get("method")
|
|
24
|
+
name = f"{method}.{operation}"
|
|
25
|
+
|
|
26
|
+
_W(
|
|
27
|
+
module=module,
|
|
28
|
+
name=name,
|
|
29
|
+
wrapper=patch_vertexai(name, version, tracer),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _uninstrument(self, **kwargs):
|
|
33
|
+
pass
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from langtrace_python_sdk.constants.instrumentation.common import SERVICE_PROVIDERS
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from langtrace_python_sdk.utils.llm import (
|
|
6
|
+
calculate_prompt_tokens,
|
|
7
|
+
get_extra_attributes,
|
|
8
|
+
get_langtrace_attributes,
|
|
9
|
+
get_llm_request_attributes,
|
|
10
|
+
get_llm_url,
|
|
11
|
+
set_event_completion,
|
|
12
|
+
set_span_attributes,
|
|
13
|
+
set_usage_attributes,
|
|
14
|
+
StreamWrapper,
|
|
15
|
+
)
|
|
16
|
+
from langtrace.trace_attributes import LLMSpanAttributes, SpanAttributes
|
|
17
|
+
from langtrace_python_sdk.utils.silently_fail import silently_fail
|
|
18
|
+
from opentelemetry.trace import Tracer, SpanKind, Span
|
|
19
|
+
from opentelemetry import trace
|
|
20
|
+
from opentelemetry.trace.propagation import set_span_in_context
|
|
21
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def patch_vertexai(name, version, tracer: Tracer):
|
|
26
|
+
def traced_method(wrapped, instance, args, kwargs):
|
|
27
|
+
service_provider = SERVICE_PROVIDERS["VERTEXAI"]
|
|
28
|
+
prompts = serialize_prompts(args, kwargs)
|
|
29
|
+
span_attributes = {
|
|
30
|
+
**get_langtrace_attributes(version, service_provider),
|
|
31
|
+
**get_llm_request_attributes(
|
|
32
|
+
kwargs,
|
|
33
|
+
prompts=prompts,
|
|
34
|
+
model=get_llm_model(instance),
|
|
35
|
+
),
|
|
36
|
+
**get_llm_url(instance),
|
|
37
|
+
SpanAttributes.LLM_PATH: "",
|
|
38
|
+
**get_extra_attributes(),
|
|
39
|
+
}
|
|
40
|
+
attributes = LLMSpanAttributes(**span_attributes)
|
|
41
|
+
span = tracer.start_span(
|
|
42
|
+
name=name,
|
|
43
|
+
kind=SpanKind.CLIENT,
|
|
44
|
+
context=set_span_in_context(trace.get_current_span()),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
set_span_attributes(span, attributes)
|
|
49
|
+
result = wrapped(*args, **kwargs)
|
|
50
|
+
if is_streaming_response(result):
|
|
51
|
+
prompt_tokens = 0
|
|
52
|
+
for message in kwargs.get("message", {}):
|
|
53
|
+
prompt_tokens += calculate_prompt_tokens(
|
|
54
|
+
json.dumps(message), kwargs.get("model")
|
|
55
|
+
)
|
|
56
|
+
return StreamWrapper(
|
|
57
|
+
stream=result, span=span, prompt_tokens=prompt_tokens
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
set_response_attributes(span, result)
|
|
61
|
+
span.set_status(StatusCode.OK)
|
|
62
|
+
span.end()
|
|
63
|
+
return result
|
|
64
|
+
except Exception as error:
|
|
65
|
+
span.record_exception(error)
|
|
66
|
+
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
67
|
+
span.end()
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
return traced_method
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@silently_fail
|
|
74
|
+
def set_response_attributes(span: Span, result):
|
|
75
|
+
|
|
76
|
+
if hasattr(result, "text"):
|
|
77
|
+
set_event_completion(span, [{"role": "assistant", "content": result.text}])
|
|
78
|
+
|
|
79
|
+
if hasattr(result, "usage_metadata"):
|
|
80
|
+
usage = result.usage_metadata
|
|
81
|
+
input_tokens = usage.prompt_token_count
|
|
82
|
+
output_tokens = usage.candidates_token_count
|
|
83
|
+
|
|
84
|
+
set_usage_attributes(
|
|
85
|
+
span, {"input_tokens": input_tokens, "output_tokens": output_tokens}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if hasattr(result, "_prediction_response"):
|
|
89
|
+
usage = result._prediction_response.metadata.get("tokenMetadata")
|
|
90
|
+
input_tokens = usage.get("inputTokenCount").get("totalTokens")
|
|
91
|
+
output_tokens = usage.get("outputTokenCount").get("totalTokens")
|
|
92
|
+
set_usage_attributes(
|
|
93
|
+
span, {"input_tokens": input_tokens, "output_tokens": output_tokens}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_streaming_response(response):
|
|
98
|
+
return isinstance(response, types.GeneratorType) or isinstance(
|
|
99
|
+
response, types.AsyncGeneratorType
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_llm_model(instance):
|
|
104
|
+
llm_model = "unknown"
|
|
105
|
+
if hasattr(instance, "_model_id"):
|
|
106
|
+
llm_model = instance._model_id
|
|
107
|
+
if hasattr(instance, "_model_name"):
|
|
108
|
+
llm_model = instance._model_name.replace("publishers/google/models/", "")
|
|
109
|
+
return llm_model
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def serialize_prompts(args, kwargs):
|
|
113
|
+
prompt = ""
|
|
114
|
+
if args is not None and len(args) > 0:
|
|
115
|
+
for arg in args:
|
|
116
|
+
if isinstance(arg, str):
|
|
117
|
+
prompt = f"{prompt}{arg}\n"
|
|
118
|
+
elif isinstance(arg, list):
|
|
119
|
+
for subarg in arg:
|
|
120
|
+
if type(subarg).__name__ == "Part":
|
|
121
|
+
prompt = f"{prompt}{json.dumps(subarg.to_dict())}\n"
|
|
122
|
+
else:
|
|
123
|
+
prompt = f"{prompt}{subarg}\n"
|
|
124
|
+
else:
|
|
125
|
+
prompt = [
|
|
126
|
+
{
|
|
127
|
+
"role": "user",
|
|
128
|
+
"content": kwargs.get("prompt") or kwargs.get("message"),
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
return prompt
|
|
@@ -53,6 +53,8 @@ from langtrace_python_sdk.instrumentation import (
|
|
|
53
53
|
WeaviateInstrumentation,
|
|
54
54
|
OllamaInstrumentor,
|
|
55
55
|
DspyInstrumentation,
|
|
56
|
+
VertexAIInstrumentation,
|
|
57
|
+
GeminiInstrumentation,
|
|
56
58
|
)
|
|
57
59
|
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|
58
60
|
from colorama import Fore
|
|
@@ -68,6 +70,7 @@ def init(
|
|
|
68
70
|
api_host: Optional[str] = LANGTRACE_REMOTE_URL,
|
|
69
71
|
disable_instrumentations: Optional[DisableInstrumentations] = None,
|
|
70
72
|
disable_tracing_for_functions: Optional[InstrumentationMethods] = None,
|
|
73
|
+
service_name: Optional[str] = None,
|
|
71
74
|
):
|
|
72
75
|
|
|
73
76
|
host = (
|
|
@@ -77,7 +80,7 @@ def init(
|
|
|
77
80
|
print(Fore.GREEN + "Initializing Langtrace SDK.." + Fore.RESET)
|
|
78
81
|
sampler = LangtraceSampler(disabled_methods=disable_tracing_for_functions)
|
|
79
82
|
provider = TracerProvider(
|
|
80
|
-
resource=Resource.create({"service.name": sys.argv[0]}),
|
|
83
|
+
resource=Resource.create({"service.name": service_name or sys.argv[0]}),
|
|
81
84
|
sampler=sampler,
|
|
82
85
|
)
|
|
83
86
|
|
|
@@ -112,6 +115,8 @@ def init(
|
|
|
112
115
|
"ollama": OllamaInstrumentor(),
|
|
113
116
|
"dspy": DspyInstrumentation(),
|
|
114
117
|
"crewai": CrewAIInstrumentation(),
|
|
118
|
+
"vertexai": VertexAIInstrumentation(),
|
|
119
|
+
"gemini": GeminiInstrumentation(),
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
init_instrumentations(disable_instrumentations, all_instrumentations)
|
|
@@ -142,6 +147,7 @@ def init_instrumentations(
|
|
|
142
147
|
):
|
|
143
148
|
if disable_instrumentations is None:
|
|
144
149
|
for idx, (name, v) in enumerate(all_instrumentations.items()):
|
|
150
|
+
|
|
145
151
|
v.instrument()
|
|
146
152
|
else:
|
|
147
153
|
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
from openai import NOT_GIVEN
|
|
1
2
|
from .sdk_version_checker import SDKVersionChecker
|
|
3
|
+
from opentelemetry.trace import Span
|
|
4
|
+
from langtrace.trace_attributes import SpanAttributes
|
|
2
5
|
|
|
3
6
|
|
|
4
|
-
def set_span_attribute(span, name, value):
|
|
7
|
+
def set_span_attribute(span: Span, name, value):
|
|
5
8
|
if value is not None:
|
|
6
|
-
if value != "":
|
|
7
|
-
|
|
9
|
+
if value != "" or value != NOT_GIVEN:
|
|
10
|
+
if name == SpanAttributes.LLM_PROMPTS:
|
|
11
|
+
span.add_event(
|
|
12
|
+
name=SpanAttributes.LLM_CONTENT_PROMPT,
|
|
13
|
+
attributes={
|
|
14
|
+
SpanAttributes.LLM_PROMPTS: value,
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
else:
|
|
18
|
+
span.set_attribute(name, value)
|
|
8
19
|
return
|
|
9
20
|
|
|
10
21
|
|
|
@@ -14,10 +14,22 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME
|
|
18
|
+
from langtrace_python_sdk.utils import set_span_attribute
|
|
19
|
+
from openai import NOT_GIVEN
|
|
17
20
|
from tiktoken import get_encoding
|
|
18
21
|
|
|
19
|
-
from langtrace_python_sdk.constants.instrumentation.common import
|
|
22
|
+
from langtrace_python_sdk.constants.instrumentation.common import (
|
|
23
|
+
LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY,
|
|
24
|
+
TIKTOKEN_MODEL_MAPPING,
|
|
25
|
+
)
|
|
20
26
|
from langtrace_python_sdk.constants.instrumentation.openai import OPENAI_COST_TABLE
|
|
27
|
+
from langtrace.trace_attributes import SpanAttributes, Event
|
|
28
|
+
from importlib_metadata import version as v
|
|
29
|
+
import json
|
|
30
|
+
from opentelemetry import baggage
|
|
31
|
+
from opentelemetry.trace import Span
|
|
32
|
+
from opentelemetry.trace.status import StatusCode
|
|
21
33
|
|
|
22
34
|
|
|
23
35
|
def estimate_tokens(prompt):
|
|
@@ -29,6 +41,15 @@ def estimate_tokens(prompt):
|
|
|
29
41
|
return 0
|
|
30
42
|
|
|
31
43
|
|
|
44
|
+
def set_event_completion_chunk(span: Span, chunk):
|
|
45
|
+
span.add_event(
|
|
46
|
+
name=SpanAttributes.LLM_CONTENT_COMPLETION_CHUNK,
|
|
47
|
+
attributes={
|
|
48
|
+
SpanAttributes.LLM_CONTENT_COMPLETION_CHUNK: json.dumps(chunk),
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
32
53
|
def estimate_tokens_using_tiktoken(prompt, model):
|
|
33
54
|
"""
|
|
34
55
|
Estimate the number of tokens in a prompt using tiktoken."""
|
|
@@ -60,11 +81,110 @@ def calculate_price_from_usage(model, usage):
|
|
|
60
81
|
return 0
|
|
61
82
|
|
|
62
83
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
def get_langtrace_attributes(version, service_provider, vendor_type="llm"):
|
|
85
|
+
return {
|
|
86
|
+
SpanAttributes.LANGTRACE_SDK_NAME: LANGTRACE_SDK_NAME,
|
|
87
|
+
SpanAttributes.LANGTRACE_VERSION: v(LANGTRACE_SDK_NAME),
|
|
88
|
+
SpanAttributes.LANGTRACE_SERVICE_VERSION: version,
|
|
89
|
+
SpanAttributes.LANGTRACE_SERVICE_NAME: service_provider,
|
|
90
|
+
SpanAttributes.LANGTRACE_SERVICE_TYPE: vendor_type,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_llm_request_attributes(kwargs, prompts=None, model=None):
|
|
95
|
+
|
|
96
|
+
user = kwargs.get("user", None)
|
|
97
|
+
if prompts is None:
|
|
98
|
+
prompts = (
|
|
99
|
+
[{"role": user or "user", "content": kwargs.get("prompt")}]
|
|
100
|
+
if "prompt" in kwargs
|
|
101
|
+
else None
|
|
102
|
+
)
|
|
103
|
+
top_k = (
|
|
104
|
+
kwargs.get("n", None)
|
|
105
|
+
or kwargs.get("k", None)
|
|
106
|
+
or kwargs.get("top_k", None)
|
|
107
|
+
or kwargs.get("top_n", None)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
top_p = kwargs.get("p", None) or kwargs.get("top_p", None)
|
|
111
|
+
tools = kwargs.get("tools", None)
|
|
112
|
+
return {
|
|
113
|
+
SpanAttributes.LLM_REQUEST_MODEL: model or kwargs.get("model"),
|
|
114
|
+
SpanAttributes.LLM_IS_STREAMING: kwargs.get("stream"),
|
|
115
|
+
SpanAttributes.LLM_REQUEST_TEMPERATURE: kwargs.get("temperature"),
|
|
116
|
+
SpanAttributes.LLM_TOP_K: top_k,
|
|
117
|
+
SpanAttributes.LLM_PROMPTS: json.dumps(prompts) if prompts else None,
|
|
118
|
+
SpanAttributes.LLM_USER: user,
|
|
119
|
+
SpanAttributes.LLM_REQUEST_TOP_P: top_p,
|
|
120
|
+
SpanAttributes.LLM_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
|
|
121
|
+
SpanAttributes.LLM_SYSTEM_FINGERPRINT: kwargs.get("system_fingerprint"),
|
|
122
|
+
SpanAttributes.LLM_PRESENCE_PENALTY: kwargs.get("presence_penalty"),
|
|
123
|
+
SpanAttributes.LLM_FREQUENCY_PENALTY: kwargs.get("frequency_penalty"),
|
|
124
|
+
SpanAttributes.LLM_REQUEST_SEED: kwargs.get("seed"),
|
|
125
|
+
SpanAttributes.LLM_TOOLS: json.dumps(tools) if tools else None,
|
|
126
|
+
SpanAttributes.LLM_REQUEST_LOGPROPS: kwargs.get("logprobs"),
|
|
127
|
+
SpanAttributes.LLM_REQUEST_LOGITBIAS: kwargs.get("logit_bias"),
|
|
128
|
+
SpanAttributes.LLM_REQUEST_TOP_LOGPROPS: kwargs.get("top_logprobs"),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_extra_attributes():
|
|
133
|
+
extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
|
|
134
|
+
return extra_attributes or {}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_llm_url(instance):
|
|
138
|
+
return {
|
|
139
|
+
SpanAttributes.LLM_URL: get_base_url(instance),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_base_url(instance):
|
|
144
|
+
return (
|
|
145
|
+
str(instance._client._base_url)
|
|
146
|
+
if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
|
|
147
|
+
else ""
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_streaming(kwargs):
|
|
152
|
+
return not (
|
|
153
|
+
kwargs.get("stream") is False
|
|
154
|
+
or kwargs.get("stream") is None
|
|
155
|
+
or kwargs.get("stream") == NOT_GIVEN
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def set_usage_attributes(span, usage):
|
|
160
|
+
if usage is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
input_tokens = usage.get("input_tokens") or usage.get("prompt_tokens") or 0
|
|
164
|
+
output_tokens = usage.get("output_tokens") or usage.get("completion_tokens") or 0
|
|
165
|
+
|
|
166
|
+
set_span_attribute(
|
|
167
|
+
span,
|
|
168
|
+
SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
|
|
169
|
+
input_tokens,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
set_span_attribute(
|
|
173
|
+
span,
|
|
174
|
+
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
|
|
175
|
+
output_tokens,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
set_span_attribute(
|
|
179
|
+
span,
|
|
180
|
+
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
|
|
181
|
+
input_tokens + output_tokens,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if "search_units" in usage:
|
|
185
|
+
set_span_attribute(
|
|
186
|
+
span, SpanAttributes.LLM_USAGE_SEARCH_UNITS, usage["search_units"]
|
|
187
|
+
)
|
|
68
188
|
|
|
69
189
|
|
|
70
190
|
def get_tool_calls(item):
|
|
@@ -77,3 +197,188 @@ def get_tool_calls(item):
|
|
|
77
197
|
if hasattr(item, "tool_calls") and item.tool_calls is not None:
|
|
78
198
|
return item.tool_calls
|
|
79
199
|
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def set_event_completion(span: Span, result_content):
|
|
203
|
+
|
|
204
|
+
span.add_event(
|
|
205
|
+
name=SpanAttributes.LLM_CONTENT_COMPLETION,
|
|
206
|
+
attributes={
|
|
207
|
+
SpanAttributes.LLM_COMPLETIONS: json.dumps(result_content),
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def set_span_attributes(span: Span, attributes: dict):
|
|
213
|
+
for field, value in attributes.model_dump(by_alias=True).items():
|
|
214
|
+
set_span_attribute(span, field, value)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class StreamWrapper:
|
|
218
|
+
span: Span
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self, stream, span, prompt_tokens, function_call=False, tool_calls=False
|
|
222
|
+
):
|
|
223
|
+
self.stream = stream
|
|
224
|
+
self.span = span
|
|
225
|
+
self.prompt_tokens = prompt_tokens
|
|
226
|
+
self.function_call = function_call
|
|
227
|
+
self.tool_calls = tool_calls
|
|
228
|
+
self.result_content = []
|
|
229
|
+
self.completion_tokens = 0
|
|
230
|
+
self._span_started = False
|
|
231
|
+
self.setup()
|
|
232
|
+
|
|
233
|
+
def setup(self):
|
|
234
|
+
if not self._span_started:
|
|
235
|
+
self.span.add_event(Event.STREAM_START.value)
|
|
236
|
+
self._span_started = True
|
|
237
|
+
|
|
238
|
+
def cleanup(self):
|
|
239
|
+
if self._span_started:
|
|
240
|
+
self.span.add_event(Event.STREAM_END.value)
|
|
241
|
+
set_span_attribute(
|
|
242
|
+
self.span,
|
|
243
|
+
SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
|
|
244
|
+
self.prompt_tokens,
|
|
245
|
+
)
|
|
246
|
+
set_span_attribute(
|
|
247
|
+
self.span,
|
|
248
|
+
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
|
|
249
|
+
self.completion_tokens,
|
|
250
|
+
)
|
|
251
|
+
set_span_attribute(
|
|
252
|
+
self.span,
|
|
253
|
+
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
|
|
254
|
+
self.prompt_tokens + self.completion_tokens,
|
|
255
|
+
)
|
|
256
|
+
set_event_completion(
|
|
257
|
+
self.span,
|
|
258
|
+
[
|
|
259
|
+
{
|
|
260
|
+
"role": "assistant",
|
|
261
|
+
"content": "".join(self.result_content),
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
self.span.set_status(StatusCode.OK)
|
|
267
|
+
self.span.end()
|
|
268
|
+
self._span_started = False
|
|
269
|
+
|
|
270
|
+
def __enter__(self):
|
|
271
|
+
self.setup()
|
|
272
|
+
return self
|
|
273
|
+
|
|
274
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
275
|
+
self.cleanup()
|
|
276
|
+
|
|
277
|
+
async def __aenter__(self):
|
|
278
|
+
self.setup()
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
282
|
+
self.cleanup()
|
|
283
|
+
|
|
284
|
+
def __iter__(self):
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
def __next__(self):
|
|
288
|
+
try:
|
|
289
|
+
chunk = next(self.stream)
|
|
290
|
+
self.process_chunk(chunk)
|
|
291
|
+
return chunk
|
|
292
|
+
except StopIteration:
|
|
293
|
+
self.cleanup()
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
def __aiter__(self):
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
async def __anext__(self):
|
|
300
|
+
try:
|
|
301
|
+
chunk = await self.stream.__anext__()
|
|
302
|
+
self.process_chunk(chunk)
|
|
303
|
+
return chunk
|
|
304
|
+
except StopAsyncIteration:
|
|
305
|
+
self.cleanup()
|
|
306
|
+
raise StopAsyncIteration
|
|
307
|
+
|
|
308
|
+
def process_chunk(self, chunk):
|
|
309
|
+
if hasattr(chunk, "model") and chunk.model is not None:
|
|
310
|
+
set_span_attribute(
|
|
311
|
+
self.span,
|
|
312
|
+
SpanAttributes.LLM_RESPONSE_MODEL,
|
|
313
|
+
chunk.model,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if hasattr(chunk, "choices") and chunk.choices is not None:
|
|
317
|
+
content = []
|
|
318
|
+
if not self.function_call and not self.tool_calls:
|
|
319
|
+
for choice in chunk.choices:
|
|
320
|
+
if choice.delta and choice.delta.content is not None:
|
|
321
|
+
token_counts = estimate_tokens(choice.delta.content)
|
|
322
|
+
self.completion_tokens += token_counts
|
|
323
|
+
content = [choice.delta.content]
|
|
324
|
+
elif self.function_call:
|
|
325
|
+
for choice in chunk.choices:
|
|
326
|
+
if (
|
|
327
|
+
choice.delta
|
|
328
|
+
and choice.delta.function_call is not None
|
|
329
|
+
and choice.delta.function_call.arguments is not None
|
|
330
|
+
):
|
|
331
|
+
token_counts = estimate_tokens(
|
|
332
|
+
choice.delta.function_call.arguments
|
|
333
|
+
)
|
|
334
|
+
self.completion_tokens += token_counts
|
|
335
|
+
content = [choice.delta.function_call.arguments]
|
|
336
|
+
elif self.tool_calls:
|
|
337
|
+
for choice in chunk.choices:
|
|
338
|
+
if choice.delta and choice.delta.tool_calls is not None:
|
|
339
|
+
toolcalls = choice.delta.tool_calls
|
|
340
|
+
content = []
|
|
341
|
+
for tool_call in toolcalls:
|
|
342
|
+
if (
|
|
343
|
+
tool_call
|
|
344
|
+
and tool_call.function is not None
|
|
345
|
+
and tool_call.function.arguments is not None
|
|
346
|
+
):
|
|
347
|
+
token_counts = estimate_tokens(
|
|
348
|
+
tool_call.function.arguments
|
|
349
|
+
)
|
|
350
|
+
self.completion_tokens += token_counts
|
|
351
|
+
content.append(tool_call.function.arguments)
|
|
352
|
+
self.span.add_event(
|
|
353
|
+
Event.STREAM_OUTPUT.value,
|
|
354
|
+
{
|
|
355
|
+
SpanAttributes.LLM_CONTENT_COMPLETION_CHUNK: (
|
|
356
|
+
"".join(content)
|
|
357
|
+
if len(content) > 0 and content[0] is not None
|
|
358
|
+
else ""
|
|
359
|
+
)
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
if content:
|
|
363
|
+
self.result_content.append(content[0])
|
|
364
|
+
|
|
365
|
+
if hasattr(chunk, "text"):
|
|
366
|
+
token_counts = estimate_tokens(chunk.text)
|
|
367
|
+
self.completion_tokens += token_counts
|
|
368
|
+
content = [chunk.text]
|
|
369
|
+
self.span.add_event(
|
|
370
|
+
Event.STREAM_OUTPUT.value,
|
|
371
|
+
{
|
|
372
|
+
SpanAttributes.LLM_CONTENT_COMPLETION_CHUNK: (
|
|
373
|
+
"".join(content)
|
|
374
|
+
if len(content) > 0 and content[0] is not None
|
|
375
|
+
else ""
|
|
376
|
+
)
|
|
377
|
+
},
|
|
378
|
+
)
|
|
379
|
+
if content:
|
|
380
|
+
self.result_content.append(content[0])
|
|
381
|
+
|
|
382
|
+
if hasattr(chunk, "usage_metadata"):
|
|
383
|
+
self.completion_tokens = chunk.usage_metadata.candidates_token_count
|
|
384
|
+
self.prompt_tokens = chunk.usage_metadata.prompt_token_count
|
langtrace_python_sdk/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.1
|
|
1
|
+
__version__ = "2.2.1"
|