201 lines
6.7 KiB
Python
201 lines
6.7 KiB
Python
import json
|
|
|
|
from opentelemetry.instrumentation.ollama.utils import dont_throw, should_send_prompts
|
|
from opentelemetry.semconv._incubating.attributes import (
|
|
gen_ai_attributes as GenAIAttributes,
|
|
)
|
|
from opentelemetry.semconv_ai import (
|
|
LLMRequestTypeValues,
|
|
SpanAttributes,
|
|
)
|
|
|
|
|
|
def _set_span_attribute(span, name, value):
|
|
if value is not None:
|
|
if value != "":
|
|
span.set_attribute(name, value)
|
|
return
|
|
|
|
|
|
@dont_throw
|
|
def set_input_attributes(span, llm_request_type, kwargs):
|
|
if not span.is_recording():
|
|
return
|
|
if should_send_prompts():
|
|
json_data = kwargs.get("json", {})
|
|
|
|
if llm_request_type == LLMRequestTypeValues.CHAT:
|
|
_set_span_attribute(span, f"{GenAIAttributes.GEN_AI_PROMPT}.0.role", "user")
|
|
for index, message in enumerate(json_data.get("messages")):
|
|
_set_span_attribute(
|
|
span,
|
|
f"{GenAIAttributes.GEN_AI_PROMPT}.{index}.content",
|
|
message.get("content"),
|
|
)
|
|
_set_span_attribute(
|
|
span,
|
|
f"{GenAIAttributes.GEN_AI_PROMPT}.{index}.role",
|
|
message.get("role"),
|
|
)
|
|
_set_prompts(span, json_data.get("messages"))
|
|
if json_data.get("tools"):
|
|
set_tools_attributes(span, json_data.get("tools"))
|
|
else:
|
|
_set_span_attribute(span, f"{GenAIAttributes.GEN_AI_PROMPT}.0.role", "user")
|
|
_set_span_attribute(
|
|
span, f"{GenAIAttributes.GEN_AI_PROMPT}.0.content", json_data.get("prompt")
|
|
)
|
|
|
|
|
|
@dont_throw
|
|
def set_model_input_attributes(span, kwargs):
|
|
if not span.is_recording():
|
|
return
|
|
json_data = kwargs.get("json", {})
|
|
_set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, json_data.get("model"))
|
|
_set_span_attribute(
|
|
span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
|
|
)
|
|
|
|
|
|
@dont_throw
|
|
def set_response_attributes(span, token_histogram, llm_request_type, response):
|
|
if not span.is_recording():
|
|
return
|
|
|
|
if should_send_prompts():
|
|
if llm_request_type == LLMRequestTypeValues.COMPLETION:
|
|
_set_span_attribute(
|
|
span,
|
|
f"{GenAIAttributes.GEN_AI_COMPLETION}.0.content",
|
|
response.get("response"),
|
|
)
|
|
_set_span_attribute(
|
|
span, f"{GenAIAttributes.GEN_AI_COMPLETION}.0.role", "assistant"
|
|
)
|
|
elif llm_request_type == LLMRequestTypeValues.CHAT:
|
|
index = 0
|
|
prefix = f"{GenAIAttributes.GEN_AI_COMPLETION}.{index}"
|
|
_set_span_attribute(
|
|
span, f"{prefix}.content", response.get("message").get("content")
|
|
)
|
|
_set_span_attribute(
|
|
span, f"{prefix}.role", response.get("message").get("role")
|
|
)
|
|
|
|
|
|
@dont_throw
|
|
def set_model_response_attributes(span, token_histogram, llm_request_type, response):
|
|
if llm_request_type == LLMRequestTypeValues.EMBEDDING or not span.is_recording():
|
|
return
|
|
|
|
_set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.get("model"))
|
|
|
|
input_tokens = response.get("prompt_eval_count") or 0
|
|
output_tokens = response.get("eval_count") or 0
|
|
|
|
_set_span_attribute(
|
|
span,
|
|
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
|
|
input_tokens + output_tokens,
|
|
)
|
|
_set_span_attribute(
|
|
span,
|
|
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
|
|
output_tokens,
|
|
)
|
|
_set_span_attribute(
|
|
span,
|
|
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
|
|
input_tokens,
|
|
)
|
|
_set_span_attribute(span, GenAIAttributes.GEN_AI_SYSTEM, "Ollama")
|
|
|
|
if (
|
|
token_histogram is not None
|
|
and isinstance(input_tokens, int)
|
|
and input_tokens >= 0
|
|
):
|
|
token_histogram.record(
|
|
input_tokens,
|
|
attributes={
|
|
GenAIAttributes.GEN_AI_SYSTEM: "Ollama",
|
|
GenAIAttributes.GEN_AI_TOKEN_TYPE: "input",
|
|
GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.get("model"),
|
|
},
|
|
)
|
|
|
|
if (
|
|
token_histogram is not None
|
|
and isinstance(output_tokens, int)
|
|
and output_tokens >= 0
|
|
):
|
|
token_histogram.record(
|
|
output_tokens,
|
|
attributes={
|
|
GenAIAttributes.GEN_AI_SYSTEM: "Ollama",
|
|
GenAIAttributes.GEN_AI_TOKEN_TYPE: "output",
|
|
GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.get("model"),
|
|
},
|
|
)
|
|
|
|
|
|
def set_tools_attributes(span, tools):
|
|
if not tools:
|
|
return
|
|
|
|
for i, tool in enumerate(tools):
|
|
function = tool.get("function")
|
|
if not function:
|
|
continue
|
|
|
|
prefix = f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{i}"
|
|
_set_span_attribute(span, f"{prefix}.name", function.get("name"))
|
|
_set_span_attribute(span, f"{prefix}.description", function.get("description"))
|
|
_set_span_attribute(
|
|
span, f"{prefix}.parameters", json.dumps(function.get("parameters"))
|
|
)
|
|
|
|
|
|
def _set_prompts(span, messages):
|
|
if not span.is_recording() or messages is None:
|
|
return
|
|
if not should_send_prompts():
|
|
return
|
|
for i, msg in enumerate(messages):
|
|
prefix = f"{GenAIAttributes.GEN_AI_PROMPT}.{i}"
|
|
|
|
_set_span_attribute(span, f"{prefix}.role", msg.get("role"))
|
|
if msg.get("content"):
|
|
content = msg.get("content")
|
|
if isinstance(content, list):
|
|
content = json.dumps(content)
|
|
_set_span_attribute(span, f"{prefix}.content", content)
|
|
if msg.get("tool_call_id"):
|
|
_set_span_attribute(span, f"{prefix}.tool_call_id", msg.get("tool_call_id"))
|
|
tool_calls = msg.get("tool_calls")
|
|
if tool_calls:
|
|
for i, tool_call in enumerate(tool_calls):
|
|
function = tool_call.get("function")
|
|
_set_span_attribute(
|
|
span,
|
|
f"{prefix}.tool_calls.{i}.id",
|
|
tool_call.get("id"),
|
|
)
|
|
_set_span_attribute(
|
|
span,
|
|
f"{prefix}.tool_calls.{i}.name",
|
|
function.get("name"),
|
|
)
|
|
# record arguments: ensure it's a JSON string for span attributes
|
|
raw_args = function.get("arguments")
|
|
if isinstance(raw_args, dict):
|
|
arg_str = json.dumps(raw_args)
|
|
else:
|
|
arg_str = raw_args
|
|
_set_span_attribute(
|
|
span,
|
|
f"{prefix}.tool_calls.{i}.arguments",
|
|
arg_str,
|
|
)
|