posthog 7.4.2__tar.gz → 7.5.0__tar.gz
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.
- {posthog-7.4.2/posthog.egg-info → posthog-7.5.0}/PKG-INFO +1 -1
- {posthog-7.4.2 → posthog-7.5.0}/posthog/__init__.py +16 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/langchain/callbacks.py +57 -18
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/utils.py +202 -173
- {posthog-7.4.2 → posthog-7.5.0}/posthog/contexts.py +5 -6
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_contexts.py +26 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/version.py +1 -1
- {posthog-7.4.2 → posthog-7.5.0/posthog.egg-info}/PKG-INFO +1 -1
- {posthog-7.4.2 → posthog-7.5.0}/LICENSE +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/MANIFEST.in +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/README.md +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/anthropic/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/anthropic/anthropic.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/anthropic/anthropic_async.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/anthropic/anthropic_converter.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/anthropic/anthropic_providers.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/gemini/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/gemini/gemini.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/gemini/gemini_async.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/gemini/gemini_converter.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/langchain/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/openai/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/openai/openai.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/openai/openai_async.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/openai/openai_converter.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/openai/openai_providers.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/sanitization.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/ai/types.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/args.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/client.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/consumer.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/exception_capture.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/exception_utils.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/feature_flags.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/flag_definition_cache.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/integrations/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/integrations/django.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/poller.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/py.typed +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/request.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/__init__.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_before_send.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_client.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_consumer.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_exception_capture.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_feature_flag.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_feature_flag_result.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_feature_flags.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_flag_definition_cache.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_module.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_request.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_size_limited_dict.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_types.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/test/test_utils.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/types.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog/utils.py +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog.egg-info/SOURCES.txt +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog.egg-info/dependency_links.txt +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog.egg-info/requires.txt +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/posthog.egg-info/top_level.txt +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/pyproject.toml +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/setup.cfg +0 -0
- {posthog-7.4.2 → posthog-7.5.0}/setup.py +0 -0
|
@@ -29,6 +29,9 @@ from posthog.contexts import (
|
|
|
29
29
|
from posthog.contexts import (
|
|
30
30
|
tag as inner_tag,
|
|
31
31
|
)
|
|
32
|
+
from posthog.contexts import (
|
|
33
|
+
get_tags as inner_get_tags,
|
|
34
|
+
)
|
|
32
35
|
from posthog.exception_utils import (
|
|
33
36
|
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
|
|
34
37
|
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
|
|
@@ -190,6 +193,19 @@ def tag(name: str, value: Any):
|
|
|
190
193
|
return inner_tag(name, value)
|
|
191
194
|
|
|
192
195
|
|
|
196
|
+
def get_tags() -> Dict[str, Any]:
|
|
197
|
+
"""
|
|
198
|
+
Get all tags from the current context.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dict of all tags in the current context
|
|
202
|
+
|
|
203
|
+
Category:
|
|
204
|
+
Contexts
|
|
205
|
+
"""
|
|
206
|
+
return inner_get_tags()
|
|
207
|
+
|
|
208
|
+
|
|
193
209
|
"""Settings."""
|
|
194
210
|
api_key = None # type: Optional[str]
|
|
195
211
|
host = None # type: Optional[str]
|
|
@@ -22,8 +22,8 @@ from uuid import UUID
|
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
24
|
# LangChain 1.0+ and modern 0.x with langchain-core
|
|
25
|
-
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
26
25
|
from langchain_core.agents import AgentAction, AgentFinish
|
|
26
|
+
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
27
27
|
except (ImportError, ModuleNotFoundError):
|
|
28
28
|
# Fallback for older LangChain versions
|
|
29
29
|
from langchain.callbacks.base import BaseCallbackHandler
|
|
@@ -35,15 +35,15 @@ from langchain_core.messages import (
|
|
|
35
35
|
FunctionMessage,
|
|
36
36
|
HumanMessage,
|
|
37
37
|
SystemMessage,
|
|
38
|
-
ToolMessage,
|
|
39
38
|
ToolCall,
|
|
39
|
+
ToolMessage,
|
|
40
40
|
)
|
|
41
41
|
from langchain_core.outputs import ChatGeneration, LLMResult
|
|
42
42
|
from pydantic import BaseModel
|
|
43
43
|
|
|
44
44
|
from posthog import setup
|
|
45
|
-
from posthog.ai.utils import get_model_params, with_privacy_mode
|
|
46
45
|
from posthog.ai.sanitization import sanitize_langchain
|
|
46
|
+
from posthog.ai.utils import get_model_params, with_privacy_mode
|
|
47
47
|
from posthog.client import Client
|
|
48
48
|
|
|
49
49
|
log = logging.getLogger("posthog")
|
|
@@ -506,6 +506,14 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
506
506
|
if isinstance(outputs, BaseException):
|
|
507
507
|
event_properties["$ai_error"] = _stringify_exception(outputs)
|
|
508
508
|
event_properties["$ai_is_error"] = True
|
|
509
|
+
event_properties = _capture_exception_and_update_properties(
|
|
510
|
+
self._ph_client,
|
|
511
|
+
outputs,
|
|
512
|
+
self._distinct_id,
|
|
513
|
+
self._groups,
|
|
514
|
+
event_properties,
|
|
515
|
+
)
|
|
516
|
+
|
|
509
517
|
elif outputs is not None:
|
|
510
518
|
event_properties["$ai_output_state"] = with_privacy_mode(
|
|
511
519
|
self._ph_client, self._privacy_mode, outputs
|
|
@@ -576,10 +584,24 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
576
584
|
if run.tools:
|
|
577
585
|
event_properties["$ai_tools"] = run.tools
|
|
578
586
|
|
|
587
|
+
if self._properties:
|
|
588
|
+
event_properties.update(self._properties)
|
|
589
|
+
|
|
590
|
+
if self._distinct_id is None:
|
|
591
|
+
event_properties["$process_person_profile"] = False
|
|
592
|
+
|
|
579
593
|
if isinstance(output, BaseException):
|
|
580
594
|
event_properties["$ai_http_status"] = _get_http_status(output)
|
|
581
595
|
event_properties["$ai_error"] = _stringify_exception(output)
|
|
582
596
|
event_properties["$ai_is_error"] = True
|
|
597
|
+
|
|
598
|
+
event_properties = _capture_exception_and_update_properties(
|
|
599
|
+
self._ph_client,
|
|
600
|
+
output,
|
|
601
|
+
self._distinct_id,
|
|
602
|
+
self._groups,
|
|
603
|
+
event_properties,
|
|
604
|
+
)
|
|
583
605
|
else:
|
|
584
606
|
# Add usage
|
|
585
607
|
usage = _parse_usage(output, run.provider, run.model)
|
|
@@ -607,12 +629,6 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
607
629
|
self._ph_client, self._privacy_mode, completions
|
|
608
630
|
)
|
|
609
631
|
|
|
610
|
-
if self._properties:
|
|
611
|
-
event_properties.update(self._properties)
|
|
612
|
-
|
|
613
|
-
if self._distinct_id is None:
|
|
614
|
-
event_properties["$process_person_profile"] = False
|
|
615
|
-
|
|
616
632
|
self._ph_client.capture(
|
|
617
633
|
distinct_id=self._distinct_id or trace_id,
|
|
618
634
|
event="$ai_generation",
|
|
@@ -773,9 +789,11 @@ def _parse_usage_model(
|
|
|
773
789
|
for mapped_key, dataclass_key in field_mapping.items()
|
|
774
790
|
},
|
|
775
791
|
)
|
|
776
|
-
# For Anthropic providers, LangChain reports input_tokens as the sum of input
|
|
792
|
+
# For Anthropic providers, LangChain reports input_tokens as the sum of all input tokens.
|
|
777
793
|
# Our cost calculation expects them to be separate for Anthropic, so we subtract cache tokens.
|
|
778
|
-
#
|
|
794
|
+
# Both cache_read and cache_write tokens should be subtracted since Anthropic's raw API
|
|
795
|
+
# reports input_tokens as tokens NOT read from or used to create a cache.
|
|
796
|
+
# For other providers (OpenAI, etc.), input_tokens already excludes cache tokens as expected.
|
|
779
797
|
# Match logic consistent with plugin-server: exact match on provider OR substring match on model
|
|
780
798
|
is_anthropic = False
|
|
781
799
|
if provider and provider.lower() == "anthropic":
|
|
@@ -783,14 +801,14 @@ def _parse_usage_model(
|
|
|
783
801
|
elif model and "anthropic" in model.lower():
|
|
784
802
|
is_anthropic = True
|
|
785
803
|
|
|
786
|
-
if
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
and normalized_usage.cache_read_tokens
|
|
790
|
-
):
|
|
791
|
-
normalized_usage.input_tokens = max(
|
|
792
|
-
normalized_usage.input_tokens - normalized_usage.cache_read_tokens, 0
|
|
804
|
+
if is_anthropic and normalized_usage.input_tokens:
|
|
805
|
+
cache_tokens = (normalized_usage.cache_read_tokens or 0) + (
|
|
806
|
+
normalized_usage.cache_write_tokens or 0
|
|
793
807
|
)
|
|
808
|
+
if cache_tokens > 0:
|
|
809
|
+
normalized_usage.input_tokens = max(
|
|
810
|
+
normalized_usage.input_tokens - cache_tokens, 0
|
|
811
|
+
)
|
|
794
812
|
return normalized_usage
|
|
795
813
|
|
|
796
814
|
|
|
@@ -861,6 +879,27 @@ def _parse_usage(
|
|
|
861
879
|
return llm_usage
|
|
862
880
|
|
|
863
881
|
|
|
882
|
+
def _capture_exception_and_update_properties(
|
|
883
|
+
client: Client,
|
|
884
|
+
exception: BaseException,
|
|
885
|
+
distinct_id: Optional[Union[str, int, UUID]],
|
|
886
|
+
groups: Optional[Dict[str, Any]],
|
|
887
|
+
event_properties: Dict[str, Any],
|
|
888
|
+
):
|
|
889
|
+
if client.enable_exception_autocapture:
|
|
890
|
+
exception_id = client.capture_exception(
|
|
891
|
+
exception,
|
|
892
|
+
distinct_id=distinct_id,
|
|
893
|
+
groups=groups,
|
|
894
|
+
properties=event_properties,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
if exception_id:
|
|
898
|
+
event_properties["$exception_event_id"] = exception_id
|
|
899
|
+
|
|
900
|
+
return event_properties
|
|
901
|
+
|
|
902
|
+
|
|
864
903
|
def _get_http_status(error: BaseException) -> int:
|
|
865
904
|
# OpenAI: https://github.com/openai/openai-python/blob/main/src/openai/_exceptions.py
|
|
866
905
|
# Anthropic: https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/_exceptions.py
|
|
@@ -2,14 +2,15 @@ import time
|
|
|
2
2
|
import uuid
|
|
3
3
|
from typing import Any, Callable, Dict, List, Optional, cast
|
|
4
4
|
|
|
5
|
-
from posthog
|
|
6
|
-
from posthog.ai.types import FormattedMessage, StreamingEventData, TokenUsage
|
|
5
|
+
from posthog import get_tags, identify_context, new_context, tag
|
|
7
6
|
from posthog.ai.sanitization import (
|
|
8
|
-
sanitize_openai,
|
|
9
7
|
sanitize_anthropic,
|
|
10
8
|
sanitize_gemini,
|
|
11
9
|
sanitize_langchain,
|
|
10
|
+
sanitize_openai,
|
|
12
11
|
)
|
|
12
|
+
from posthog.ai.types import FormattedMessage, StreamingEventData, TokenUsage
|
|
13
|
+
from posthog.client import Client as PostHogClient
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def merge_usage_stats(
|
|
@@ -256,94 +257,108 @@ def call_llm_and_track_usage(
|
|
|
256
257
|
usage: TokenUsage = TokenUsage()
|
|
257
258
|
error_params: Dict[str, Any] = {}
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"$
|
|
294
|
-
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
"$ai_latency": latency,
|
|
300
|
-
"$ai_trace_id": posthog_trace_id,
|
|
301
|
-
"$ai_base_url": str(base_url),
|
|
302
|
-
**(posthog_properties or {}),
|
|
303
|
-
**(error_params or {}),
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
|
307
|
-
|
|
308
|
-
if available_tool_calls:
|
|
309
|
-
event_properties["$ai_tools"] = available_tool_calls
|
|
310
|
-
|
|
311
|
-
cache_read = usage.get("cache_read_input_tokens")
|
|
312
|
-
if cache_read is not None and cache_read > 0:
|
|
313
|
-
event_properties["$ai_cache_read_input_tokens"] = cache_read
|
|
314
|
-
|
|
315
|
-
cache_creation = usage.get("cache_creation_input_tokens")
|
|
316
|
-
if cache_creation is not None and cache_creation > 0:
|
|
317
|
-
event_properties["$ai_cache_creation_input_tokens"] = cache_creation
|
|
318
|
-
|
|
319
|
-
reasoning = usage.get("reasoning_tokens")
|
|
320
|
-
if reasoning is not None and reasoning > 0:
|
|
321
|
-
event_properties["$ai_reasoning_tokens"] = reasoning
|
|
322
|
-
|
|
323
|
-
web_search_count = usage.get("web_search_count")
|
|
324
|
-
if web_search_count is not None and web_search_count > 0:
|
|
325
|
-
event_properties["$ai_web_search_count"] = web_search_count
|
|
326
|
-
|
|
327
|
-
if posthog_distinct_id is None:
|
|
328
|
-
event_properties["$process_person_profile"] = False
|
|
329
|
-
|
|
330
|
-
# Process instructions for Responses API
|
|
331
|
-
if provider == "openai" and kwargs.get("instructions") is not None:
|
|
332
|
-
event_properties["$ai_instructions"] = with_privacy_mode(
|
|
333
|
-
ph_client, posthog_privacy_mode, kwargs.get("instructions")
|
|
260
|
+
with new_context(client=ph_client, capture_exceptions=False):
|
|
261
|
+
if posthog_distinct_id:
|
|
262
|
+
identify_context(posthog_distinct_id)
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
response = call_method(**kwargs)
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
error = exc
|
|
268
|
+
http_status = getattr(
|
|
269
|
+
exc, "status_code", 0
|
|
270
|
+
) # default to 0 becuase its likely an SDK error
|
|
271
|
+
error_params = {
|
|
272
|
+
"$ai_is_error": True,
|
|
273
|
+
"$ai_error": exc.__str__(),
|
|
274
|
+
}
|
|
275
|
+
# TODO: Add exception capture for OpenAI/Anthropic/Gemini wrappers when
|
|
276
|
+
# enable_exception_autocapture is True, similar to LangChain callbacks.
|
|
277
|
+
# See _capture_exception_and_update_properties in langchain/callbacks.py
|
|
278
|
+
finally:
|
|
279
|
+
end_time = time.time()
|
|
280
|
+
latency = end_time - start_time
|
|
281
|
+
|
|
282
|
+
if posthog_trace_id is None:
|
|
283
|
+
posthog_trace_id = str(uuid.uuid4())
|
|
284
|
+
|
|
285
|
+
if response and (
|
|
286
|
+
hasattr(response, "usage")
|
|
287
|
+
or (provider == "gemini" and hasattr(response, "usage_metadata"))
|
|
288
|
+
):
|
|
289
|
+
usage = get_usage(response, provider)
|
|
290
|
+
|
|
291
|
+
messages = merge_system_prompt(kwargs, provider)
|
|
292
|
+
sanitized_messages = sanitize_messages(messages, provider)
|
|
293
|
+
|
|
294
|
+
tag("$ai_provider", provider)
|
|
295
|
+
tag("$ai_model", kwargs.get("model") or getattr(response, "model", None))
|
|
296
|
+
tag("$ai_model_parameters", get_model_params(kwargs))
|
|
297
|
+
tag(
|
|
298
|
+
"$ai_input",
|
|
299
|
+
with_privacy_mode(ph_client, posthog_privacy_mode, sanitized_messages),
|
|
334
300
|
)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
event="$ai_generation",
|
|
341
|
-
properties=event_properties,
|
|
342
|
-
groups=posthog_groups,
|
|
301
|
+
tag(
|
|
302
|
+
"$ai_output_choices",
|
|
303
|
+
with_privacy_mode(
|
|
304
|
+
ph_client, posthog_privacy_mode, format_response(response, provider)
|
|
305
|
+
),
|
|
343
306
|
)
|
|
307
|
+
tag("$ai_http_status", http_status)
|
|
308
|
+
tag("$ai_input_tokens", usage.get("input_tokens", 0))
|
|
309
|
+
tag("$ai_output_tokens", usage.get("output_tokens", 0))
|
|
310
|
+
tag("$ai_latency", latency)
|
|
311
|
+
tag("$ai_trace_id", posthog_trace_id)
|
|
312
|
+
tag("$ai_base_url", str(base_url))
|
|
313
|
+
|
|
314
|
+
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
|
315
|
+
|
|
316
|
+
if available_tool_calls:
|
|
317
|
+
tag("$ai_tools", available_tool_calls)
|
|
318
|
+
|
|
319
|
+
cache_read = usage.get("cache_read_input_tokens")
|
|
320
|
+
if cache_read is not None and cache_read > 0:
|
|
321
|
+
tag("$ai_cache_read_input_tokens", cache_read)
|
|
322
|
+
|
|
323
|
+
cache_creation = usage.get("cache_creation_input_tokens")
|
|
324
|
+
if cache_creation is not None and cache_creation > 0:
|
|
325
|
+
tag("$ai_cache_creation_input_tokens", cache_creation)
|
|
326
|
+
|
|
327
|
+
reasoning = usage.get("reasoning_tokens")
|
|
328
|
+
if reasoning is not None and reasoning > 0:
|
|
329
|
+
tag("$ai_reasoning_tokens", reasoning)
|
|
330
|
+
|
|
331
|
+
web_search_count = usage.get("web_search_count")
|
|
332
|
+
if web_search_count is not None and web_search_count > 0:
|
|
333
|
+
tag("$ai_web_search_count", web_search_count)
|
|
334
|
+
|
|
335
|
+
if posthog_distinct_id is None:
|
|
336
|
+
tag("$process_person_profile", False)
|
|
337
|
+
|
|
338
|
+
# Process instructions for Responses API
|
|
339
|
+
if provider == "openai" and kwargs.get("instructions") is not None:
|
|
340
|
+
tag(
|
|
341
|
+
"$ai_instructions",
|
|
342
|
+
with_privacy_mode(
|
|
343
|
+
ph_client, posthog_privacy_mode, kwargs.get("instructions")
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# send the event to posthog
|
|
348
|
+
if hasattr(ph_client, "capture") and callable(ph_client.capture):
|
|
349
|
+
ph_client.capture(
|
|
350
|
+
distinct_id=posthog_distinct_id or posthog_trace_id,
|
|
351
|
+
event="$ai_generation",
|
|
352
|
+
properties={
|
|
353
|
+
**get_tags(),
|
|
354
|
+
**(posthog_properties or {}),
|
|
355
|
+
**(error_params or {}),
|
|
356
|
+
},
|
|
357
|
+
groups=posthog_groups,
|
|
358
|
+
)
|
|
344
359
|
|
|
345
|
-
|
|
346
|
-
|
|
360
|
+
if error:
|
|
361
|
+
raise error
|
|
347
362
|
|
|
348
363
|
return response
|
|
349
364
|
|
|
@@ -367,94 +382,108 @@ async def call_llm_and_track_usage_async(
|
|
|
367
382
|
usage: TokenUsage = TokenUsage()
|
|
368
383
|
error_params: Dict[str, Any] = {}
|
|
369
384
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
"$
|
|
405
|
-
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"$ai_latency": latency,
|
|
411
|
-
"$ai_trace_id": posthog_trace_id,
|
|
412
|
-
"$ai_base_url": str(base_url),
|
|
413
|
-
**(posthog_properties or {}),
|
|
414
|
-
**(error_params or {}),
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
|
418
|
-
|
|
419
|
-
if available_tool_calls:
|
|
420
|
-
event_properties["$ai_tools"] = available_tool_calls
|
|
421
|
-
|
|
422
|
-
cache_read = usage.get("cache_read_input_tokens")
|
|
423
|
-
if cache_read is not None and cache_read > 0:
|
|
424
|
-
event_properties["$ai_cache_read_input_tokens"] = cache_read
|
|
425
|
-
|
|
426
|
-
cache_creation = usage.get("cache_creation_input_tokens")
|
|
427
|
-
if cache_creation is not None and cache_creation > 0:
|
|
428
|
-
event_properties["$ai_cache_creation_input_tokens"] = cache_creation
|
|
429
|
-
|
|
430
|
-
reasoning = usage.get("reasoning_tokens")
|
|
431
|
-
if reasoning is not None and reasoning > 0:
|
|
432
|
-
event_properties["$ai_reasoning_tokens"] = reasoning
|
|
433
|
-
|
|
434
|
-
web_search_count = usage.get("web_search_count")
|
|
435
|
-
if web_search_count is not None and web_search_count > 0:
|
|
436
|
-
event_properties["$ai_web_search_count"] = web_search_count
|
|
437
|
-
|
|
438
|
-
if posthog_distinct_id is None:
|
|
439
|
-
event_properties["$process_person_profile"] = False
|
|
440
|
-
|
|
441
|
-
# Process instructions for Responses API
|
|
442
|
-
if provider == "openai" and kwargs.get("instructions") is not None:
|
|
443
|
-
event_properties["$ai_instructions"] = with_privacy_mode(
|
|
444
|
-
ph_client, posthog_privacy_mode, kwargs.get("instructions")
|
|
385
|
+
with new_context(client=ph_client, capture_exceptions=False):
|
|
386
|
+
if posthog_distinct_id:
|
|
387
|
+
identify_context(posthog_distinct_id)
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
response = await call_async_method(**kwargs)
|
|
391
|
+
except Exception as exc:
|
|
392
|
+
error = exc
|
|
393
|
+
http_status = getattr(
|
|
394
|
+
exc, "status_code", 0
|
|
395
|
+
) # default to 0 because its likely an SDK error
|
|
396
|
+
error_params = {
|
|
397
|
+
"$ai_is_error": True,
|
|
398
|
+
"$ai_error": exc.__str__(),
|
|
399
|
+
}
|
|
400
|
+
# TODO: Add exception capture for OpenAI/Anthropic/Gemini wrappers when
|
|
401
|
+
# enable_exception_autocapture is True, similar to LangChain callbacks.
|
|
402
|
+
# See _capture_exception_and_update_properties in langchain/callbacks.py
|
|
403
|
+
finally:
|
|
404
|
+
end_time = time.time()
|
|
405
|
+
latency = end_time - start_time
|
|
406
|
+
|
|
407
|
+
if posthog_trace_id is None:
|
|
408
|
+
posthog_trace_id = str(uuid.uuid4())
|
|
409
|
+
|
|
410
|
+
if response and (
|
|
411
|
+
hasattr(response, "usage")
|
|
412
|
+
or (provider == "gemini" and hasattr(response, "usage_metadata"))
|
|
413
|
+
):
|
|
414
|
+
usage = get_usage(response, provider)
|
|
415
|
+
|
|
416
|
+
messages = merge_system_prompt(kwargs, provider)
|
|
417
|
+
sanitized_messages = sanitize_messages(messages, provider)
|
|
418
|
+
|
|
419
|
+
tag("$ai_provider", provider)
|
|
420
|
+
tag("$ai_model", kwargs.get("model") or getattr(response, "model", None))
|
|
421
|
+
tag("$ai_model_parameters", get_model_params(kwargs))
|
|
422
|
+
tag(
|
|
423
|
+
"$ai_input",
|
|
424
|
+
with_privacy_mode(ph_client, posthog_privacy_mode, sanitized_messages),
|
|
445
425
|
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
event="$ai_generation",
|
|
452
|
-
properties=event_properties,
|
|
453
|
-
groups=posthog_groups,
|
|
426
|
+
tag(
|
|
427
|
+
"$ai_output_choices",
|
|
428
|
+
with_privacy_mode(
|
|
429
|
+
ph_client, posthog_privacy_mode, format_response(response, provider)
|
|
430
|
+
),
|
|
454
431
|
)
|
|
432
|
+
tag("$ai_http_status", http_status)
|
|
433
|
+
tag("$ai_input_tokens", usage.get("input_tokens", 0))
|
|
434
|
+
tag("$ai_output_tokens", usage.get("output_tokens", 0))
|
|
435
|
+
tag("$ai_latency", latency)
|
|
436
|
+
tag("$ai_trace_id", posthog_trace_id)
|
|
437
|
+
tag("$ai_base_url", str(base_url))
|
|
438
|
+
|
|
439
|
+
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
|
440
|
+
|
|
441
|
+
if available_tool_calls:
|
|
442
|
+
tag("$ai_tools", available_tool_calls)
|
|
443
|
+
|
|
444
|
+
cache_read = usage.get("cache_read_input_tokens")
|
|
445
|
+
if cache_read is not None and cache_read > 0:
|
|
446
|
+
tag("$ai_cache_read_input_tokens", cache_read)
|
|
447
|
+
|
|
448
|
+
cache_creation = usage.get("cache_creation_input_tokens")
|
|
449
|
+
if cache_creation is not None and cache_creation > 0:
|
|
450
|
+
tag("$ai_cache_creation_input_tokens", cache_creation)
|
|
451
|
+
|
|
452
|
+
reasoning = usage.get("reasoning_tokens")
|
|
453
|
+
if reasoning is not None and reasoning > 0:
|
|
454
|
+
tag("$ai_reasoning_tokens", reasoning)
|
|
455
|
+
|
|
456
|
+
web_search_count = usage.get("web_search_count")
|
|
457
|
+
if web_search_count is not None and web_search_count > 0:
|
|
458
|
+
tag("$ai_web_search_count", web_search_count)
|
|
459
|
+
|
|
460
|
+
if posthog_distinct_id is None:
|
|
461
|
+
tag("$process_person_profile", False)
|
|
462
|
+
|
|
463
|
+
# Process instructions for Responses API
|
|
464
|
+
if provider == "openai" and kwargs.get("instructions") is not None:
|
|
465
|
+
tag(
|
|
466
|
+
"$ai_instructions",
|
|
467
|
+
with_privacy_mode(
|
|
468
|
+
ph_client, posthog_privacy_mode, kwargs.get("instructions")
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# send the event to posthog
|
|
473
|
+
if hasattr(ph_client, "capture") and callable(ph_client.capture):
|
|
474
|
+
ph_client.capture(
|
|
475
|
+
distinct_id=posthog_distinct_id or posthog_trace_id,
|
|
476
|
+
event="$ai_generation",
|
|
477
|
+
properties={
|
|
478
|
+
**get_tags(),
|
|
479
|
+
**(posthog_properties or {}),
|
|
480
|
+
**(error_params or {}),
|
|
481
|
+
},
|
|
482
|
+
groups=posthog_groups,
|
|
483
|
+
)
|
|
455
484
|
|
|
456
|
-
|
|
457
|
-
|
|
485
|
+
if error:
|
|
486
|
+
raise error
|
|
458
487
|
|
|
459
488
|
return response
|
|
460
489
|
|
|
@@ -62,14 +62,13 @@ class ContextScope:
|
|
|
62
62
|
return None
|
|
63
63
|
|
|
64
64
|
def collect_tags(self) -> Dict[str, Any]:
|
|
65
|
-
tags = self.tags.copy()
|
|
66
65
|
if self.parent and not self.fresh:
|
|
67
66
|
# We want child tags to take precedence over parent tags,
|
|
68
|
-
# so
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
tags
|
|
72
|
-
return tags
|
|
67
|
+
# so collect parent tags first, then update with child tags.
|
|
68
|
+
tags = self.parent.collect_tags()
|
|
69
|
+
tags.update(self.tags)
|
|
70
|
+
return tags
|
|
71
|
+
return self.tags.copy()
|
|
73
72
|
|
|
74
73
|
def get_capture_exception_code_variables(self) -> Optional[bool]:
|
|
75
74
|
if self.capture_exception_code_variables is not None:
|
|
@@ -191,6 +191,32 @@ class TestContexts(unittest.TestCase):
|
|
|
191
191
|
assert get_context_distinct_id() == "user123"
|
|
192
192
|
assert get_context_session_id() == "session456"
|
|
193
193
|
|
|
194
|
+
def test_child_tags_override_parent_tags_in_non_fresh_context(self):
|
|
195
|
+
with new_context(fresh=True):
|
|
196
|
+
tag("shared_key", "parent_value")
|
|
197
|
+
tag("parent_only", "parent")
|
|
198
|
+
|
|
199
|
+
with new_context(fresh=False):
|
|
200
|
+
# Child should inherit parent tags
|
|
201
|
+
assert get_tags()["parent_only"] == "parent"
|
|
202
|
+
|
|
203
|
+
# Child sets same key - should override parent
|
|
204
|
+
tag("shared_key", "child_value")
|
|
205
|
+
tag("child_only", "child")
|
|
206
|
+
|
|
207
|
+
tags = get_tags()
|
|
208
|
+
# Child value should win for shared key
|
|
209
|
+
assert tags["shared_key"] == "child_value"
|
|
210
|
+
# Both parent and child tags should be present
|
|
211
|
+
assert tags["parent_only"] == "parent"
|
|
212
|
+
assert tags["child_only"] == "child"
|
|
213
|
+
|
|
214
|
+
# Parent context should be unchanged
|
|
215
|
+
parent_tags = get_tags()
|
|
216
|
+
assert parent_tags["shared_key"] == "parent_value"
|
|
217
|
+
assert parent_tags["parent_only"] == "parent"
|
|
218
|
+
assert "child_only" not in parent_tags
|
|
219
|
+
|
|
194
220
|
def test_scoped_decorator_with_context_ids(self):
|
|
195
221
|
@scoped()
|
|
196
222
|
def function_with_context():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|