Skip to content

Commit afce320

Browse files
authored
Add GoogleGenerativeAIInstrumentor (#932)
* Add GoogleGenerativeAIInstrumentor and top_k span attribute * Add LLM_REQUEST_SEED attribute to SpanAttributes class * Refactor attribute handling in Google Generative AI instrumentation and added new request configuration attributes to SpanAttributes * Refactor chat message attribute assignment to use MessageAttributes constants for improved clarity and consistency * Update provider import name for GoogleGenerativeAIInstrumentor from "google.generativeai" to "google.genai" for consistency. * Update library name from "google-generativeai" to "google-genai" for consistency with recent changes. * Remove token count tracking from content stream wrappers to streamline processing and improve performance. * Add token count tracking to content stream wrappers for improved text processing * Remove token count tracking from content stream wrappers to streamline processing and improve performance.
1 parent e203d13 commit afce320

File tree

12 files changed

+1062
-38
lines changed

12 files changed

+1062
-38
lines changed

agentops/instrumentation/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def get_instance(self) -> BaseInstrumentor:
7272
class_name="OpenAIAgentsInstrumentor",
7373
provider_import_name="agents",
7474
),
75+
InstrumentorLoader(
76+
module_name="agentops.instrumentation.google_generativeai",
77+
class_name="GoogleGenerativeAIInstrumentor",
78+
provider_import_name="google.genai",
79+
),
7580
]
7681

7782

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Google Generative AI (Gemini) Instrumentation
2+
3+
This module provides OpenTelemetry instrumentation for Google's Generative AI (Gemini) API. The instrumentation allows you to trace all API calls made using the `google-genai` Python SDK, capturing:
4+
5+
- Model parameters (temperature, max_tokens, etc.)
6+
- Prompt content (with privacy controls)
7+
- Response text and token usage
8+
- Streaming metrics
9+
- Token counting
10+
- Performance and error data
11+
12+
## Supported Features
13+
14+
The instrumentation covers all major API methods including:
15+
16+
### Client-Based API
17+
- `client.models.generate_content`
18+
- `client.models.generate_content_stream`
19+
- `client.models.count_tokens`
20+
- `client.models.compute_tokens`
21+
- And their corresponding async variants
22+
23+
## Metrics
24+
25+
The instrumentation captures the following metrics:
26+
27+
- Input tokens used
28+
- Output tokens generated
29+
- Total tokens consumed
30+
- Operation duration
31+
- Exception counts
32+
33+
These metrics are available as OpenTelemetry span attributes and can be viewed in your observability platform of choice when properly configured.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Google Generative AI (Gemini) API instrumentation.
2+
3+
This module provides instrumentation for the Google Generative AI (Gemini) API,
4+
including content generation, streaming, and chat functionality.
5+
"""
6+
7+
import logging
8+
from typing import Collection
9+
10+
def get_version() -> str:
11+
"""Get the version of the Google Generative AI SDK, or 'unknown' if not found
12+
13+
Attempts to retrieve the installed version of the Google Generative AI SDK using importlib.metadata.
14+
Falls back to 'unknown' if the version cannot be determined.
15+
16+
Returns:
17+
The version string of the Google Generative AI SDK or 'unknown'
18+
"""
19+
try:
20+
from importlib.metadata import version
21+
return version("google-genai")
22+
except ImportError:
23+
logger.debug("Could not find Google Generative AI SDK version")
24+
return "unknown"
25+
26+
LIBRARY_NAME = "google-genai"
27+
LIBRARY_VERSION: str = get_version()
28+
29+
logger = logging.getLogger(__name__)
30+
31+
# Import after defining constants to avoid circular imports
32+
from agentops.instrumentation.google_generativeai.instrumentor import GoogleGenerativeAIInstrumentor # noqa: E402
33+
34+
__all__ = [
35+
"LIBRARY_NAME",
36+
"LIBRARY_VERSION",
37+
"GoogleGenerativeAIInstrumentor",
38+
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Attribute extractors for Google Generative AI instrumentation."""
2+
3+
from agentops.instrumentation.google_generativeai.attributes.common import (
4+
get_common_instrumentation_attributes,
5+
extract_request_attributes,
6+
)
7+
from agentops.instrumentation.google_generativeai.attributes.model import (
8+
get_model_attributes,
9+
get_generate_content_attributes,
10+
get_stream_attributes,
11+
get_token_counting_attributes,
12+
)
13+
from agentops.instrumentation.google_generativeai.attributes.chat import (
14+
get_chat_attributes,
15+
)
16+
17+
__all__ = [
18+
"get_common_instrumentation_attributes",
19+
"extract_request_attributes",
20+
"get_model_attributes",
21+
"get_generate_content_attributes",
22+
"get_stream_attributes",
23+
"get_chat_attributes",
24+
"get_token_counting_attributes",
25+
]
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Chat attribute extraction for Google Generative AI instrumentation."""
2+
3+
from typing import Dict, Any, Optional, Tuple, List, Union
4+
5+
from agentops.logging import logger
6+
from agentops.semconv import SpanAttributes, LLMRequestTypeValues, MessageAttributes
7+
from agentops.instrumentation.common.attributes import AttributeMap
8+
from agentops.instrumentation.google_generativeai.attributes.common import (
9+
extract_request_attributes,
10+
get_common_instrumentation_attributes,
11+
)
12+
from agentops.instrumentation.google_generativeai.attributes.model import (
13+
_extract_content_from_prompt,
14+
_set_response_attributes,
15+
)
16+
17+
18+
def _extract_message_content(message: Any) -> str:
19+
"""Extract text content from a chat message.
20+
21+
Handles the various message formats in the Gemini chat API.
22+
23+
Args:
24+
message: The message to extract content from
25+
26+
Returns:
27+
Extracted text as a string
28+
"""
29+
if isinstance(message, str):
30+
return message
31+
32+
if isinstance(message, dict):
33+
if "content" in message:
34+
return _extract_content_from_prompt(message["content"])
35+
if "text" in message:
36+
return message["text"]
37+
38+
if hasattr(message, "content"):
39+
return _extract_content_from_prompt(message.content)
40+
41+
if hasattr(message, "text"):
42+
return message.text
43+
44+
return ""
45+
46+
47+
def _set_chat_history_attributes(attributes: AttributeMap, args: Tuple, kwargs: Dict[str, Any]) -> None:
48+
"""Extract and set chat history attributes from the request.
49+
50+
Args:
51+
attributes: The attribute dictionary to update
52+
args: Positional arguments to the method
53+
kwargs: Keyword arguments to the method
54+
"""
55+
messages = []
56+
if 'message' in kwargs:
57+
messages = [kwargs['message']]
58+
elif args and len(args) > 0:
59+
messages = [args[0]]
60+
elif 'messages' in kwargs:
61+
messages = kwargs['messages']
62+
63+
if not messages:
64+
return
65+
66+
for i, message in enumerate(messages):
67+
try:
68+
content = _extract_message_content(message)
69+
if content:
70+
role = "user"
71+
72+
if isinstance(message, dict) and "role" in message:
73+
role = message["role"]
74+
elif hasattr(message, "role"):
75+
role = message.role
76+
77+
attributes[MessageAttributes.PROMPT_CONTENT.format(i=i)] = content
78+
attributes[MessageAttributes.PROMPT_ROLE.format(i=i)] = role
79+
except Exception as e:
80+
logger.debug(f"Error extracting chat message at index {i}: {e}")
81+
82+
83+
def get_chat_attributes(
84+
args: Optional[Tuple] = None,
85+
kwargs: Optional[Dict[str, Any]] = None,
86+
return_value: Optional[Any] = None,
87+
) -> AttributeMap:
88+
"""Extract attributes for chat session methods.
89+
90+
This function handles attribute extraction for chat session operations,
91+
particularly the send_message method.
92+
93+
Args:
94+
args: Positional arguments to the method
95+
kwargs: Keyword arguments to the method
96+
return_value: Return value from the method
97+
98+
Returns:
99+
Dictionary of extracted attributes
100+
"""
101+
attributes = get_common_instrumentation_attributes()
102+
attributes[SpanAttributes.LLM_SYSTEM] = "Gemini"
103+
attributes[SpanAttributes.LLM_REQUEST_TYPE] = LLMRequestTypeValues.CHAT.value
104+
105+
if kwargs:
106+
kwargs_attributes = extract_request_attributes(kwargs)
107+
attributes.update(kwargs_attributes)
108+
109+
chat_session = None
110+
if args and len(args) >= 1:
111+
chat_session = args[0]
112+
113+
if chat_session and hasattr(chat_session, "model"):
114+
if isinstance(chat_session.model, str):
115+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = chat_session.model
116+
elif hasattr(chat_session.model, "name"):
117+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = chat_session.model.name
118+
119+
if args or kwargs:
120+
_set_chat_history_attributes(attributes, args or (), kwargs or {})
121+
122+
if return_value is not None:
123+
_set_response_attributes(attributes, return_value)
124+
125+
return attributes
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Common attribute extraction for Google Generative AI instrumentation."""
2+
3+
from typing import Dict, Any, Optional
4+
5+
from agentops.logging import logger
6+
from agentops.semconv import InstrumentationAttributes, SpanAttributes, LLMRequestTypeValues
7+
from agentops.instrumentation.common.attributes import AttributeMap, get_common_attributes, _extract_attributes_from_mapping
8+
from agentops.instrumentation.google_generativeai import LIBRARY_NAME, LIBRARY_VERSION
9+
10+
# Common mapping for config parameters
11+
REQUEST_CONFIG_ATTRIBUTES: AttributeMap = {
12+
SpanAttributes.LLM_REQUEST_TEMPERATURE: "temperature",
13+
SpanAttributes.LLM_REQUEST_MAX_TOKENS: "max_output_tokens",
14+
SpanAttributes.LLM_REQUEST_TOP_P: "top_p",
15+
SpanAttributes.LLM_REQUEST_TOP_K: "top_k",
16+
SpanAttributes.LLM_REQUEST_SEED: "seed",
17+
SpanAttributes.LLM_REQUEST_SYSTEM_INSTRUCTION: "system_instruction",
18+
SpanAttributes.LLM_REQUEST_PRESENCE_PENALTY: "presence_penalty",
19+
SpanAttributes.LLM_REQUEST_FREQUENCY_PENALTY: "frequency_penalty",
20+
SpanAttributes.LLM_REQUEST_STOP_SEQUENCES: "stop_sequences",
21+
SpanAttributes.LLM_REQUEST_CANDIDATE_COUNT: "candidate_count",
22+
}
23+
24+
def get_common_instrumentation_attributes() -> AttributeMap:
25+
"""Get common instrumentation attributes for the Google Generative AI instrumentation.
26+
27+
This combines the generic AgentOps attributes with Google Generative AI specific library attributes.
28+
29+
Returns:
30+
Dictionary of common instrumentation attributes
31+
"""
32+
attributes = get_common_attributes()
33+
attributes.update({
34+
InstrumentationAttributes.LIBRARY_NAME: LIBRARY_NAME,
35+
InstrumentationAttributes.LIBRARY_VERSION: LIBRARY_VERSION,
36+
})
37+
return attributes
38+
39+
40+
def extract_request_attributes(kwargs: Dict[str, Any]) -> AttributeMap:
41+
"""Extract request attributes from the function arguments.
42+
43+
Extracts common request parameters that apply to both content generation
44+
and chat completions, focusing on model parameters and generation settings.
45+
46+
Args:
47+
kwargs: Request keyword arguments
48+
49+
Returns:
50+
Dictionary of extracted request attributes
51+
"""
52+
attributes = {}
53+
54+
if 'model' in kwargs:
55+
model = kwargs["model"]
56+
57+
# Handle string model names
58+
if isinstance(model, str):
59+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = model
60+
# Handle model objects with _model_name or name attribute
61+
elif hasattr(model, '_model_name'):
62+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = model._model_name
63+
elif hasattr(model, 'name'):
64+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = model.name
65+
66+
config = kwargs.get('config')
67+
68+
if config:
69+
try:
70+
attributes.update(_extract_attributes_from_mapping(
71+
config.__dict__ if hasattr(config, '__dict__') else config,
72+
REQUEST_CONFIG_ATTRIBUTES
73+
))
74+
except Exception as e:
75+
logger.debug(f"Error extracting config parameters: {e}")
76+
77+
if 'stream' in kwargs:
78+
attributes[SpanAttributes.LLM_REQUEST_STREAMING] = kwargs['stream']
79+
80+
return attributes

0 commit comments

Comments
 (0)