posthoganalytics 6.7.0__py3-none-any.whl → 7.4.3__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.
- posthoganalytics/__init__.py +84 -7
- posthoganalytics/ai/anthropic/__init__.py +10 -0
- posthoganalytics/ai/anthropic/anthropic.py +95 -65
- posthoganalytics/ai/anthropic/anthropic_async.py +95 -65
- posthoganalytics/ai/anthropic/anthropic_converter.py +443 -0
- posthoganalytics/ai/gemini/__init__.py +15 -1
- posthoganalytics/ai/gemini/gemini.py +66 -71
- posthoganalytics/ai/gemini/gemini_async.py +423 -0
- posthoganalytics/ai/gemini/gemini_converter.py +652 -0
- posthoganalytics/ai/langchain/callbacks.py +58 -13
- posthoganalytics/ai/openai/__init__.py +16 -1
- posthoganalytics/ai/openai/openai.py +140 -149
- posthoganalytics/ai/openai/openai_async.py +127 -82
- posthoganalytics/ai/openai/openai_converter.py +741 -0
- posthoganalytics/ai/sanitization.py +248 -0
- posthoganalytics/ai/types.py +125 -0
- posthoganalytics/ai/utils.py +339 -356
- posthoganalytics/client.py +345 -97
- posthoganalytics/contexts.py +81 -0
- posthoganalytics/exception_utils.py +250 -2
- posthoganalytics/feature_flags.py +26 -10
- posthoganalytics/flag_definition_cache.py +127 -0
- posthoganalytics/integrations/django.py +157 -19
- posthoganalytics/request.py +203 -23
- posthoganalytics/test/test_client.py +250 -22
- posthoganalytics/test/test_exception_capture.py +418 -0
- posthoganalytics/test/test_feature_flag_result.py +441 -2
- posthoganalytics/test/test_feature_flags.py +308 -104
- posthoganalytics/test/test_flag_definition_cache.py +612 -0
- posthoganalytics/test/test_module.py +0 -8
- posthoganalytics/test/test_request.py +536 -0
- posthoganalytics/test/test_utils.py +4 -1
- posthoganalytics/types.py +40 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
- posthoganalytics-7.4.3.dist-info/RECORD +57 -0
- posthoganalytics-6.7.0.dist-info/RECORD +0 -49
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,9 @@ import time
|
|
|
3
3
|
import uuid
|
|
4
4
|
from typing import Any, Dict, Optional
|
|
5
5
|
|
|
6
|
+
from posthoganalytics.ai.types import TokenUsage, StreamingEventData
|
|
7
|
+
from posthoganalytics.ai.utils import merge_system_prompt
|
|
8
|
+
|
|
6
9
|
try:
|
|
7
10
|
from google import genai
|
|
8
11
|
except ImportError:
|
|
@@ -13,9 +16,15 @@ except ImportError:
|
|
|
13
16
|
from posthoganalytics import setup
|
|
14
17
|
from posthoganalytics.ai.utils import (
|
|
15
18
|
call_llm_and_track_usage,
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
capture_streaming_event,
|
|
20
|
+
merge_usage_stats,
|
|
21
|
+
)
|
|
22
|
+
from posthoganalytics.ai.gemini.gemini_converter import (
|
|
23
|
+
extract_gemini_usage_from_chunk,
|
|
24
|
+
extract_gemini_content_from_chunk,
|
|
25
|
+
format_gemini_streaming_output,
|
|
18
26
|
)
|
|
27
|
+
from posthoganalytics.ai.sanitization import sanitize_gemini
|
|
19
28
|
from posthoganalytics.client import Client as PostHogClient
|
|
20
29
|
|
|
21
30
|
|
|
@@ -71,6 +80,7 @@ class Client:
|
|
|
71
80
|
posthog_groups: Default groups for all calls (can be overridden per call)
|
|
72
81
|
**kwargs: Additional arguments (for future compatibility)
|
|
73
82
|
"""
|
|
83
|
+
|
|
74
84
|
self._ph_client = posthog_client or setup()
|
|
75
85
|
|
|
76
86
|
if self._ph_client is None:
|
|
@@ -132,6 +142,7 @@ class Models:
|
|
|
132
142
|
posthog_groups: Default groups for all calls
|
|
133
143
|
**kwargs: Additional arguments (for future compatibility)
|
|
134
144
|
"""
|
|
145
|
+
|
|
135
146
|
self._ph_client = posthog_client or setup()
|
|
136
147
|
|
|
137
148
|
if self._ph_client is None:
|
|
@@ -149,14 +160,19 @@ class Models:
|
|
|
149
160
|
# Add Vertex AI parameters if provided
|
|
150
161
|
if vertexai is not None:
|
|
151
162
|
client_args["vertexai"] = vertexai
|
|
163
|
+
|
|
152
164
|
if credentials is not None:
|
|
153
165
|
client_args["credentials"] = credentials
|
|
166
|
+
|
|
154
167
|
if project is not None:
|
|
155
168
|
client_args["project"] = project
|
|
169
|
+
|
|
156
170
|
if location is not None:
|
|
157
171
|
client_args["location"] = location
|
|
172
|
+
|
|
158
173
|
if debug_config is not None:
|
|
159
174
|
client_args["debug_config"] = debug_config
|
|
175
|
+
|
|
160
176
|
if http_options is not None:
|
|
161
177
|
client_args["http_options"] = http_options
|
|
162
178
|
|
|
@@ -174,6 +190,7 @@ class Models:
|
|
|
174
190
|
raise ValueError(
|
|
175
191
|
"API key must be provided either as parameter or via GOOGLE_API_KEY/API_KEY environment variable"
|
|
176
192
|
)
|
|
193
|
+
|
|
177
194
|
client_args["api_key"] = api_key
|
|
178
195
|
|
|
179
196
|
self._client = genai.Client(**client_args)
|
|
@@ -188,6 +205,7 @@ class Models:
|
|
|
188
205
|
call_groups: Optional[Dict[str, Any]],
|
|
189
206
|
):
|
|
190
207
|
"""Merge call-level PostHog parameters with client defaults."""
|
|
208
|
+
|
|
191
209
|
# Use call-level values if provided, otherwise fall back to defaults
|
|
192
210
|
distinct_id = (
|
|
193
211
|
call_distinct_id
|
|
@@ -203,6 +221,7 @@ class Models:
|
|
|
203
221
|
|
|
204
222
|
# Merge properties: default properties + call properties (call properties override)
|
|
205
223
|
properties = dict(self._default_properties)
|
|
224
|
+
|
|
206
225
|
if call_properties:
|
|
207
226
|
properties.update(call_properties)
|
|
208
227
|
|
|
@@ -238,6 +257,7 @@ class Models:
|
|
|
238
257
|
posthog_groups: Group analytics properties (overrides client default)
|
|
239
258
|
**kwargs: Arguments passed to Gemini's generate_content
|
|
240
259
|
"""
|
|
260
|
+
|
|
241
261
|
# Merge PostHog parameters
|
|
242
262
|
distinct_id, trace_id, properties, privacy_mode, groups = (
|
|
243
263
|
self._merge_posthog_params(
|
|
@@ -276,7 +296,7 @@ class Models:
|
|
|
276
296
|
**kwargs: Any,
|
|
277
297
|
):
|
|
278
298
|
start_time = time.time()
|
|
279
|
-
usage_stats:
|
|
299
|
+
usage_stats: TokenUsage = TokenUsage(input_tokens=0, output_tokens=0)
|
|
280
300
|
accumulated_content = []
|
|
281
301
|
|
|
282
302
|
kwargs_without_stream = {"model": model, "contents": contents, **kwargs}
|
|
@@ -284,28 +304,27 @@ class Models:
|
|
|
284
304
|
|
|
285
305
|
def generator():
|
|
286
306
|
nonlocal usage_stats
|
|
287
|
-
nonlocal accumulated_content
|
|
307
|
+
nonlocal accumulated_content
|
|
288
308
|
try:
|
|
289
309
|
for chunk in response:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if
|
|
301
|
-
accumulated_content.append(
|
|
310
|
+
# Extract usage stats from chunk
|
|
311
|
+
chunk_usage = extract_gemini_usage_from_chunk(chunk)
|
|
312
|
+
|
|
313
|
+
if chunk_usage:
|
|
314
|
+
# Gemini reports cumulative totals, not incremental values
|
|
315
|
+
merge_usage_stats(usage_stats, chunk_usage, mode="cumulative")
|
|
316
|
+
|
|
317
|
+
# Extract content from chunk (now returns content blocks)
|
|
318
|
+
content_block = extract_gemini_content_from_chunk(chunk)
|
|
319
|
+
|
|
320
|
+
if content_block is not None:
|
|
321
|
+
accumulated_content.append(content_block)
|
|
302
322
|
|
|
303
323
|
yield chunk
|
|
304
324
|
|
|
305
325
|
finally:
|
|
306
326
|
end_time = time.time()
|
|
307
327
|
latency = end_time - start_time
|
|
308
|
-
output = "".join(accumulated_content)
|
|
309
328
|
|
|
310
329
|
self._capture_streaming_event(
|
|
311
330
|
model,
|
|
@@ -318,7 +337,7 @@ class Models:
|
|
|
318
337
|
kwargs,
|
|
319
338
|
usage_stats,
|
|
320
339
|
latency,
|
|
321
|
-
|
|
340
|
+
accumulated_content,
|
|
322
341
|
)
|
|
323
342
|
|
|
324
343
|
return generator()
|
|
@@ -333,63 +352,39 @@ class Models:
|
|
|
333
352
|
privacy_mode: bool,
|
|
334
353
|
groups: Optional[Dict[str, Any]],
|
|
335
354
|
kwargs: Dict[str, Any],
|
|
336
|
-
usage_stats:
|
|
355
|
+
usage_stats: TokenUsage,
|
|
337
356
|
latency: float,
|
|
338
|
-
output:
|
|
357
|
+
output: Any,
|
|
339
358
|
):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"$ai_base_url": self._base_url,
|
|
363
|
-
**(properties or {}),
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if distinct_id is None:
|
|
367
|
-
event_properties["$process_person_profile"] = False
|
|
368
|
-
|
|
369
|
-
if hasattr(self._ph_client, "capture"):
|
|
370
|
-
self._ph_client.capture(
|
|
371
|
-
distinct_id=distinct_id,
|
|
372
|
-
event="$ai_generation",
|
|
373
|
-
properties=event_properties,
|
|
374
|
-
groups=groups,
|
|
375
|
-
)
|
|
359
|
+
# Prepare standardized event data
|
|
360
|
+
formatted_input = self._format_input(contents, **kwargs)
|
|
361
|
+
sanitized_input = sanitize_gemini(formatted_input)
|
|
362
|
+
|
|
363
|
+
event_data = StreamingEventData(
|
|
364
|
+
provider="gemini",
|
|
365
|
+
model=model,
|
|
366
|
+
base_url=self._base_url,
|
|
367
|
+
kwargs=kwargs,
|
|
368
|
+
formatted_input=sanitized_input,
|
|
369
|
+
formatted_output=format_gemini_streaming_output(output),
|
|
370
|
+
usage_stats=usage_stats,
|
|
371
|
+
latency=latency,
|
|
372
|
+
distinct_id=distinct_id,
|
|
373
|
+
trace_id=trace_id,
|
|
374
|
+
properties=properties,
|
|
375
|
+
privacy_mode=privacy_mode,
|
|
376
|
+
groups=groups,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Use the common capture function
|
|
380
|
+
capture_streaming_event(self._ph_client, event_data)
|
|
376
381
|
|
|
377
|
-
def _format_input(self, contents):
|
|
382
|
+
def _format_input(self, contents, **kwargs):
|
|
378
383
|
"""Format input contents for PostHog tracking"""
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
for item in contents:
|
|
384
|
-
if isinstance(item, str):
|
|
385
|
-
formatted.append({"role": "user", "content": item})
|
|
386
|
-
elif hasattr(item, "text"):
|
|
387
|
-
formatted.append({"role": "user", "content": item.text})
|
|
388
|
-
else:
|
|
389
|
-
formatted.append({"role": "user", "content": str(item)})
|
|
390
|
-
return formatted
|
|
391
|
-
else:
|
|
392
|
-
return [{"role": "user", "content": str(contents)}]
|
|
384
|
+
|
|
385
|
+
# Create kwargs dict with contents for merge_system_prompt
|
|
386
|
+
input_kwargs = {"contents": contents, **kwargs}
|
|
387
|
+
return merge_system_prompt(input_kwargs, "gemini")
|
|
393
388
|
|
|
394
389
|
def generate_content_stream(
|
|
395
390
|
self,
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from posthoganalytics.ai.types import TokenUsage, StreamingEventData
|
|
7
|
+
from posthoganalytics.ai.utils import merge_system_prompt
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from google import genai
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ModuleNotFoundError(
|
|
13
|
+
"Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from posthoganalytics import setup
|
|
17
|
+
from posthoganalytics.ai.utils import (
|
|
18
|
+
call_llm_and_track_usage_async,
|
|
19
|
+
capture_streaming_event,
|
|
20
|
+
merge_usage_stats,
|
|
21
|
+
)
|
|
22
|
+
from posthoganalytics.ai.gemini.gemini_converter import (
|
|
23
|
+
extract_gemini_usage_from_chunk,
|
|
24
|
+
extract_gemini_content_from_chunk,
|
|
25
|
+
format_gemini_streaming_output,
|
|
26
|
+
)
|
|
27
|
+
from posthoganalytics.ai.sanitization import sanitize_gemini
|
|
28
|
+
from posthoganalytics.client import Client as PostHogClient
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncClient:
|
|
32
|
+
"""
|
|
33
|
+
An async drop-in replacement for genai.Client that automatically sends LLM usage events to PostHog.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
client = AsyncClient(
|
|
37
|
+
api_key="your_api_key",
|
|
38
|
+
posthog_client=posthog_client,
|
|
39
|
+
posthog_distinct_id="default_user", # Optional defaults
|
|
40
|
+
posthog_properties={"team": "ai"} # Optional defaults
|
|
41
|
+
)
|
|
42
|
+
response = await client.models.generate_content(
|
|
43
|
+
model="gemini-2.0-flash",
|
|
44
|
+
contents=["Hello world"],
|
|
45
|
+
posthog_distinct_id="specific_user" # Override default
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
_ph_client: PostHogClient
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
api_key: Optional[str] = None,
|
|
54
|
+
vertexai: Optional[bool] = None,
|
|
55
|
+
credentials: Optional[Any] = None,
|
|
56
|
+
project: Optional[str] = None,
|
|
57
|
+
location: Optional[str] = None,
|
|
58
|
+
debug_config: Optional[Any] = None,
|
|
59
|
+
http_options: Optional[Any] = None,
|
|
60
|
+
posthog_client: Optional[PostHogClient] = None,
|
|
61
|
+
posthog_distinct_id: Optional[str] = None,
|
|
62
|
+
posthog_properties: Optional[Dict[str, Any]] = None,
|
|
63
|
+
posthog_privacy_mode: bool = False,
|
|
64
|
+
posthog_groups: Optional[Dict[str, Any]] = None,
|
|
65
|
+
**kwargs,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Args:
|
|
69
|
+
api_key: Google AI API key. If not provided, will use GOOGLE_API_KEY or API_KEY environment variable (not required for Vertex AI)
|
|
70
|
+
vertexai: Whether to use Vertex AI authentication
|
|
71
|
+
credentials: Vertex AI credentials object
|
|
72
|
+
project: GCP project ID for Vertex AI
|
|
73
|
+
location: GCP location for Vertex AI
|
|
74
|
+
debug_config: Debug configuration for the client
|
|
75
|
+
http_options: HTTP options for the client
|
|
76
|
+
posthog_client: PostHog client for tracking usage
|
|
77
|
+
posthog_distinct_id: Default distinct ID for all calls (can be overridden per call)
|
|
78
|
+
posthog_properties: Default properties for all calls (can be overridden per call)
|
|
79
|
+
posthog_privacy_mode: Default privacy mode for all calls (can be overridden per call)
|
|
80
|
+
posthog_groups: Default groups for all calls (can be overridden per call)
|
|
81
|
+
**kwargs: Additional arguments (for future compatibility)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
self._ph_client = posthog_client or setup()
|
|
85
|
+
|
|
86
|
+
if self._ph_client is None:
|
|
87
|
+
raise ValueError("posthog_client is required for PostHog tracking")
|
|
88
|
+
|
|
89
|
+
self.models = AsyncModels(
|
|
90
|
+
api_key=api_key,
|
|
91
|
+
vertexai=vertexai,
|
|
92
|
+
credentials=credentials,
|
|
93
|
+
project=project,
|
|
94
|
+
location=location,
|
|
95
|
+
debug_config=debug_config,
|
|
96
|
+
http_options=http_options,
|
|
97
|
+
posthog_client=self._ph_client,
|
|
98
|
+
posthog_distinct_id=posthog_distinct_id,
|
|
99
|
+
posthog_properties=posthog_properties,
|
|
100
|
+
posthog_privacy_mode=posthog_privacy_mode,
|
|
101
|
+
posthog_groups=posthog_groups,
|
|
102
|
+
**kwargs,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AsyncModels:
|
|
107
|
+
"""
|
|
108
|
+
Async Models interface that mimics genai.Client().aio.models with PostHog tracking.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
_ph_client: PostHogClient # Not None after __init__ validation
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
api_key: Optional[str] = None,
|
|
116
|
+
vertexai: Optional[bool] = None,
|
|
117
|
+
credentials: Optional[Any] = None,
|
|
118
|
+
project: Optional[str] = None,
|
|
119
|
+
location: Optional[str] = None,
|
|
120
|
+
debug_config: Optional[Any] = None,
|
|
121
|
+
http_options: Optional[Any] = None,
|
|
122
|
+
posthog_client: Optional[PostHogClient] = None,
|
|
123
|
+
posthog_distinct_id: Optional[str] = None,
|
|
124
|
+
posthog_properties: Optional[Dict[str, Any]] = None,
|
|
125
|
+
posthog_privacy_mode: bool = False,
|
|
126
|
+
posthog_groups: Optional[Dict[str, Any]] = None,
|
|
127
|
+
**kwargs,
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Args:
|
|
131
|
+
api_key: Google AI API key. If not provided, will use GOOGLE_API_KEY or API_KEY environment variable (not required for Vertex AI)
|
|
132
|
+
vertexai: Whether to use Vertex AI authentication
|
|
133
|
+
credentials: Vertex AI credentials object
|
|
134
|
+
project: GCP project ID for Vertex AI
|
|
135
|
+
location: GCP location for Vertex AI
|
|
136
|
+
debug_config: Debug configuration for the client
|
|
137
|
+
http_options: HTTP options for the client
|
|
138
|
+
posthog_client: PostHog client for tracking usage
|
|
139
|
+
posthog_distinct_id: Default distinct ID for all calls
|
|
140
|
+
posthog_properties: Default properties for all calls
|
|
141
|
+
posthog_privacy_mode: Default privacy mode for all calls
|
|
142
|
+
posthog_groups: Default groups for all calls
|
|
143
|
+
**kwargs: Additional arguments (for future compatibility)
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
self._ph_client = posthog_client or setup()
|
|
147
|
+
|
|
148
|
+
if self._ph_client is None:
|
|
149
|
+
raise ValueError("posthog_client is required for PostHog tracking")
|
|
150
|
+
|
|
151
|
+
# Store default PostHog settings
|
|
152
|
+
self._default_distinct_id = posthog_distinct_id
|
|
153
|
+
self._default_properties = posthog_properties or {}
|
|
154
|
+
self._default_privacy_mode = posthog_privacy_mode
|
|
155
|
+
self._default_groups = posthog_groups
|
|
156
|
+
|
|
157
|
+
# Build genai.Client arguments
|
|
158
|
+
client_args: Dict[str, Any] = {}
|
|
159
|
+
|
|
160
|
+
# Add Vertex AI parameters if provided
|
|
161
|
+
if vertexai is not None:
|
|
162
|
+
client_args["vertexai"] = vertexai
|
|
163
|
+
|
|
164
|
+
if credentials is not None:
|
|
165
|
+
client_args["credentials"] = credentials
|
|
166
|
+
|
|
167
|
+
if project is not None:
|
|
168
|
+
client_args["project"] = project
|
|
169
|
+
|
|
170
|
+
if location is not None:
|
|
171
|
+
client_args["location"] = location
|
|
172
|
+
|
|
173
|
+
if debug_config is not None:
|
|
174
|
+
client_args["debug_config"] = debug_config
|
|
175
|
+
|
|
176
|
+
if http_options is not None:
|
|
177
|
+
client_args["http_options"] = http_options
|
|
178
|
+
|
|
179
|
+
# Handle API key authentication
|
|
180
|
+
if vertexai:
|
|
181
|
+
# For Vertex AI, api_key is optional
|
|
182
|
+
if api_key is not None:
|
|
183
|
+
client_args["api_key"] = api_key
|
|
184
|
+
else:
|
|
185
|
+
# For non-Vertex AI mode, api_key is required (backwards compatibility)
|
|
186
|
+
if api_key is None:
|
|
187
|
+
api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("API_KEY")
|
|
188
|
+
|
|
189
|
+
if api_key is None:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"API key must be provided either as parameter or via GOOGLE_API_KEY/API_KEY environment variable"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
client_args["api_key"] = api_key
|
|
195
|
+
|
|
196
|
+
self._client = genai.Client(**client_args)
|
|
197
|
+
self._base_url = "https://generativelanguage.googleapis.com"
|
|
198
|
+
|
|
199
|
+
def _merge_posthog_params(
|
|
200
|
+
self,
|
|
201
|
+
call_distinct_id: Optional[str],
|
|
202
|
+
call_trace_id: Optional[str],
|
|
203
|
+
call_properties: Optional[Dict[str, Any]],
|
|
204
|
+
call_privacy_mode: Optional[bool],
|
|
205
|
+
call_groups: Optional[Dict[str, Any]],
|
|
206
|
+
):
|
|
207
|
+
"""Merge call-level PostHog parameters with client defaults."""
|
|
208
|
+
|
|
209
|
+
# Use call-level values if provided, otherwise fall back to defaults
|
|
210
|
+
distinct_id = (
|
|
211
|
+
call_distinct_id
|
|
212
|
+
if call_distinct_id is not None
|
|
213
|
+
else self._default_distinct_id
|
|
214
|
+
)
|
|
215
|
+
privacy_mode = (
|
|
216
|
+
call_privacy_mode
|
|
217
|
+
if call_privacy_mode is not None
|
|
218
|
+
else self._default_privacy_mode
|
|
219
|
+
)
|
|
220
|
+
groups = call_groups if call_groups is not None else self._default_groups
|
|
221
|
+
|
|
222
|
+
# Merge properties: default properties + call properties (call properties override)
|
|
223
|
+
properties = dict(self._default_properties)
|
|
224
|
+
|
|
225
|
+
if call_properties:
|
|
226
|
+
properties.update(call_properties)
|
|
227
|
+
|
|
228
|
+
if call_trace_id is None:
|
|
229
|
+
call_trace_id = str(uuid.uuid4())
|
|
230
|
+
|
|
231
|
+
return distinct_id, call_trace_id, properties, privacy_mode, groups
|
|
232
|
+
|
|
233
|
+
async def generate_content(
|
|
234
|
+
self,
|
|
235
|
+
model: str,
|
|
236
|
+
contents,
|
|
237
|
+
posthog_distinct_id: Optional[str] = None,
|
|
238
|
+
posthog_trace_id: Optional[str] = None,
|
|
239
|
+
posthog_properties: Optional[Dict[str, Any]] = None,
|
|
240
|
+
posthog_privacy_mode: Optional[bool] = None,
|
|
241
|
+
posthog_groups: Optional[Dict[str, Any]] = None,
|
|
242
|
+
**kwargs: Any,
|
|
243
|
+
):
|
|
244
|
+
"""
|
|
245
|
+
Generate content using Gemini's API while tracking usage in PostHog.
|
|
246
|
+
|
|
247
|
+
This method signature exactly matches genai.Client().aio.models.generate_content()
|
|
248
|
+
with additional PostHog tracking parameters.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
model: The model to use (e.g., 'gemini-2.0-flash')
|
|
252
|
+
contents: The input content for generation
|
|
253
|
+
posthog_distinct_id: ID to associate with the usage event (overrides client default)
|
|
254
|
+
posthog_trace_id: Trace UUID for linking events (auto-generated if not provided)
|
|
255
|
+
posthog_properties: Extra properties to include in the event (merged with client defaults)
|
|
256
|
+
posthog_privacy_mode: Whether to redact sensitive information (overrides client default)
|
|
257
|
+
posthog_groups: Group analytics properties (overrides client default)
|
|
258
|
+
**kwargs: Arguments passed to Gemini's generate_content
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# Merge PostHog parameters
|
|
262
|
+
distinct_id, trace_id, properties, privacy_mode, groups = (
|
|
263
|
+
self._merge_posthog_params(
|
|
264
|
+
posthog_distinct_id,
|
|
265
|
+
posthog_trace_id,
|
|
266
|
+
posthog_properties,
|
|
267
|
+
posthog_privacy_mode,
|
|
268
|
+
posthog_groups,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
kwargs_with_contents = {"model": model, "contents": contents, **kwargs}
|
|
273
|
+
|
|
274
|
+
return await call_llm_and_track_usage_async(
|
|
275
|
+
distinct_id,
|
|
276
|
+
self._ph_client,
|
|
277
|
+
"gemini",
|
|
278
|
+
trace_id,
|
|
279
|
+
properties,
|
|
280
|
+
privacy_mode,
|
|
281
|
+
groups,
|
|
282
|
+
self._base_url,
|
|
283
|
+
self._client.aio.models.generate_content,
|
|
284
|
+
**kwargs_with_contents,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def _generate_content_streaming(
|
|
288
|
+
self,
|
|
289
|
+
model: str,
|
|
290
|
+
contents,
|
|
291
|
+
distinct_id: Optional[str],
|
|
292
|
+
trace_id: Optional[str],
|
|
293
|
+
properties: Optional[Dict[str, Any]],
|
|
294
|
+
privacy_mode: bool,
|
|
295
|
+
groups: Optional[Dict[str, Any]],
|
|
296
|
+
**kwargs: Any,
|
|
297
|
+
):
|
|
298
|
+
start_time = time.time()
|
|
299
|
+
usage_stats: TokenUsage = TokenUsage(input_tokens=0, output_tokens=0)
|
|
300
|
+
accumulated_content = []
|
|
301
|
+
|
|
302
|
+
kwargs_without_stream = {"model": model, "contents": contents, **kwargs}
|
|
303
|
+
response = await self._client.aio.models.generate_content_stream(
|
|
304
|
+
**kwargs_without_stream
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
async def async_generator():
|
|
308
|
+
nonlocal usage_stats
|
|
309
|
+
nonlocal accumulated_content
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
async for chunk in response:
|
|
313
|
+
# Extract usage stats from chunk
|
|
314
|
+
chunk_usage = extract_gemini_usage_from_chunk(chunk)
|
|
315
|
+
|
|
316
|
+
if chunk_usage:
|
|
317
|
+
# Gemini reports cumulative totals, not incremental values
|
|
318
|
+
merge_usage_stats(usage_stats, chunk_usage, mode="cumulative")
|
|
319
|
+
|
|
320
|
+
# Extract content from chunk (now returns content blocks)
|
|
321
|
+
content_block = extract_gemini_content_from_chunk(chunk)
|
|
322
|
+
|
|
323
|
+
if content_block is not None:
|
|
324
|
+
accumulated_content.append(content_block)
|
|
325
|
+
|
|
326
|
+
yield chunk
|
|
327
|
+
|
|
328
|
+
finally:
|
|
329
|
+
end_time = time.time()
|
|
330
|
+
latency = end_time - start_time
|
|
331
|
+
|
|
332
|
+
self._capture_streaming_event(
|
|
333
|
+
model,
|
|
334
|
+
contents,
|
|
335
|
+
distinct_id,
|
|
336
|
+
trace_id,
|
|
337
|
+
properties,
|
|
338
|
+
privacy_mode,
|
|
339
|
+
groups,
|
|
340
|
+
kwargs,
|
|
341
|
+
usage_stats,
|
|
342
|
+
latency,
|
|
343
|
+
accumulated_content,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return async_generator()
|
|
347
|
+
|
|
348
|
+
def _capture_streaming_event(
|
|
349
|
+
self,
|
|
350
|
+
model: str,
|
|
351
|
+
contents,
|
|
352
|
+
distinct_id: Optional[str],
|
|
353
|
+
trace_id: Optional[str],
|
|
354
|
+
properties: Optional[Dict[str, Any]],
|
|
355
|
+
privacy_mode: bool,
|
|
356
|
+
groups: Optional[Dict[str, Any]],
|
|
357
|
+
kwargs: Dict[str, Any],
|
|
358
|
+
usage_stats: TokenUsage,
|
|
359
|
+
latency: float,
|
|
360
|
+
output: Any,
|
|
361
|
+
):
|
|
362
|
+
# Prepare standardized event data
|
|
363
|
+
formatted_input = self._format_input(contents, **kwargs)
|
|
364
|
+
sanitized_input = sanitize_gemini(formatted_input)
|
|
365
|
+
|
|
366
|
+
event_data = StreamingEventData(
|
|
367
|
+
provider="gemini",
|
|
368
|
+
model=model,
|
|
369
|
+
base_url=self._base_url,
|
|
370
|
+
kwargs=kwargs,
|
|
371
|
+
formatted_input=sanitized_input,
|
|
372
|
+
formatted_output=format_gemini_streaming_output(output),
|
|
373
|
+
usage_stats=usage_stats,
|
|
374
|
+
latency=latency,
|
|
375
|
+
distinct_id=distinct_id,
|
|
376
|
+
trace_id=trace_id,
|
|
377
|
+
properties=properties,
|
|
378
|
+
privacy_mode=privacy_mode,
|
|
379
|
+
groups=groups,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Use the common capture function
|
|
383
|
+
capture_streaming_event(self._ph_client, event_data)
|
|
384
|
+
|
|
385
|
+
def _format_input(self, contents, **kwargs):
|
|
386
|
+
"""Format input contents for PostHog tracking"""
|
|
387
|
+
|
|
388
|
+
# Create kwargs dict with contents for merge_system_prompt
|
|
389
|
+
input_kwargs = {"contents": contents, **kwargs}
|
|
390
|
+
return merge_system_prompt(input_kwargs, "gemini")
|
|
391
|
+
|
|
392
|
+
async def generate_content_stream(
|
|
393
|
+
self,
|
|
394
|
+
model: str,
|
|
395
|
+
contents,
|
|
396
|
+
posthog_distinct_id: Optional[str] = None,
|
|
397
|
+
posthog_trace_id: Optional[str] = None,
|
|
398
|
+
posthog_properties: Optional[Dict[str, Any]] = None,
|
|
399
|
+
posthog_privacy_mode: Optional[bool] = None,
|
|
400
|
+
posthog_groups: Optional[Dict[str, Any]] = None,
|
|
401
|
+
**kwargs: Any,
|
|
402
|
+
):
|
|
403
|
+
# Merge PostHog parameters
|
|
404
|
+
distinct_id, trace_id, properties, privacy_mode, groups = (
|
|
405
|
+
self._merge_posthog_params(
|
|
406
|
+
posthog_distinct_id,
|
|
407
|
+
posthog_trace_id,
|
|
408
|
+
posthog_properties,
|
|
409
|
+
posthog_privacy_mode,
|
|
410
|
+
posthog_groups,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return await self._generate_content_streaming(
|
|
415
|
+
model,
|
|
416
|
+
contents,
|
|
417
|
+
distinct_id,
|
|
418
|
+
trace_id,
|
|
419
|
+
properties,
|
|
420
|
+
privacy_mode,
|
|
421
|
+
groups,
|
|
422
|
+
**kwargs,
|
|
423
|
+
)
|