From b9ac3ccf064cddd9670200247a2ac142f2a4dd97 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 12 Mar 2025 17:16:30 -0700 Subject: [PATCH 01/14] feat: add suppoort for responses api --- CHANGELOG.md | 6 + posthog/ai/openai/openai.py | 167 ++++++++++++++++++++++++++ posthog/ai/openai/openai_async.py | 165 ++++++++++++++++++++++++- posthog/ai/utils.py | 124 ++++++++++++++++--- posthog/test/ai/openai/test_openai.py | 53 +++++++- posthog/version.py | 2 +- 6 files changed, 495 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6f64e2..e6fa584d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ + + +## 3.20.0 – 2025-03-13 + +1. Add support for OpenAI Responses API. + ## 3.19.2 – 2025-03-11 1. Fix install requirements for analytics package diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index 3d88b169..2876ee29 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -32,6 +32,167 @@ def __init__(self, posthog_client: PostHogClient, **kwargs): self.chat = WrappedChat(self) self.embeddings = WrappedEmbeddings(self) self.beta = WrappedBeta(self) + self.responses = WrappedResponses(self) + + +class WrappedResponses(openai.resources.responses.Responses): + _client: OpenAI + + def create( + self, + posthog_distinct_id: Optional[str] = None, + posthog_trace_id: Optional[str] = None, + posthog_properties: Optional[Dict[str, Any]] = None, + posthog_privacy_mode: bool = False, + posthog_groups: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ): + if posthog_trace_id is None: + posthog_trace_id = uuid.uuid4() + + if kwargs.get("stream", False): + return self._create_streaming( + posthog_distinct_id, + posthog_trace_id, + posthog_properties, + posthog_privacy_mode, + posthog_groups, + **kwargs, + ) + + return call_llm_and_track_usage( + posthog_distinct_id, + self._client._ph_client, + "openai", + posthog_trace_id, + posthog_properties, + posthog_privacy_mode, + posthog_groups, + self._client.base_url, + super().create, + **kwargs, + ) + + def _create_streaming( + self, + posthog_distinct_id: Optional[str], + posthog_trace_id: Optional[str], + posthog_properties: Optional[Dict[str, Any]], + posthog_privacy_mode: bool, + posthog_groups: Optional[Dict[str, Any]], + **kwargs: Any, + ): + start_time = time.time() + usage_stats: Dict[str, int] = {} + final_content = [] + response = super().create(**kwargs) + + def generator(): + nonlocal usage_stats + nonlocal final_content + + try: + for chunk in response: + if hasattr(chunk, "type") and chunk.type == "response.completed": + res = chunk.response + if res.output and len(res.output) > 0: + final_content.append(res.output[0]) + + if hasattr(chunk, "usage") and chunk.usage: + usage_stats = { + k: getattr(chunk.usage, k, 0) + for k in [ + "input_tokens", + "output_tokens", + "total_tokens", + ] + } + + # Add support for cached tokens + if hasattr(chunk.usage, "output_tokens_details") and hasattr( + chunk.usage.output_tokens_details, "reasoning_tokens" + ): + usage_stats["reasoning_tokens"] = chunk.usage.output_tokens_details.reasoning_tokens + + if hasattr(chunk.usage, "input_tokens_details") and hasattr( + chunk.usage.input_tokens_details, "cached_tokens" + ): + usage_stats["cache_read_input_tokens"] = chunk.usage.input_tokens_details.cached_tokens + + yield chunk + + finally: + end_time = time.time() + latency = end_time - start_time + output = final_content + self._capture_streaming_event( + posthog_distinct_id, + posthog_trace_id, + posthog_properties, + posthog_privacy_mode, + posthog_groups, + kwargs, + usage_stats, + latency, + output, + ) + + return generator() + + def _capture_streaming_event( + self, + posthog_distinct_id: Optional[str], + posthog_trace_id: Optional[str], + posthog_properties: Optional[Dict[str, Any]], + posthog_privacy_mode: bool, + posthog_groups: Optional[Dict[str, Any]], + kwargs: Dict[str, Any], + usage_stats: Dict[str, int], + latency: float, + output: str, + tool_calls: Optional[List[Dict[str, Any]]] = None, + ): + if posthog_trace_id is None: + posthog_trace_id = uuid.uuid4() + + event_properties = { + "$ai_provider": "openai", + "$ai_model": kwargs.get("model"), + "$ai_model_parameters": get_model_params(kwargs), + "$ai_input": with_privacy_mode(self._client._ph_client, posthog_privacy_mode, kwargs.get("input")), + "$ai_output_choices": with_privacy_mode( + self._client._ph_client, + posthog_privacy_mode, + output, + ), + "$ai_http_status": 200, + "$ai_input_tokens": usage_stats.get("input_tokens", 0), + "$ai_output_tokens": usage_stats.get("output_tokens", 0), + "$ai_cache_read_input_tokens": usage_stats.get("cache_read_input_tokens", 0), + "$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0), + "$ai_latency": latency, + "$ai_trace_id": posthog_trace_id, + "$ai_base_url": str(self._client.base_url), + **(posthog_properties or {}), + } + + if tool_calls: + event_properties["$ai_tools"] = with_privacy_mode( + self._client._ph_client, + posthog_privacy_mode, + tool_calls, + ) + + if posthog_distinct_id is None: + event_properties["$process_person_profile"] = False + + if hasattr(self._client._ph_client, "capture"): + self._client._ph_client.capture( + distinct_id=posthog_distinct_id or posthog_trace_id, + event="$ai_generation", + properties=event_properties, + groups=posthog_groups, + ) class WrappedChat(openai.resources.chat.Chat): @@ -121,6 +282,11 @@ def generator(): ): usage_stats["cache_read_input_tokens"] = chunk.usage.prompt_tokens_details.cached_tokens + if hasattr(chunk.usage, "output_tokens_details") and hasattr( + chunk.usage.output_tokens_details, "reasoning_tokens" + ): + usage_stats["reasoning_tokens"] = chunk.usage.output_tokens_details.reasoning_tokens + if hasattr(chunk, "choices") and chunk.choices and len(chunk.choices) > 0: if chunk.choices[0].delta and chunk.choices[0].delta.content: content = chunk.choices[0].delta.content @@ -191,6 +357,7 @@ def _capture_streaming_event( "$ai_input_tokens": usage_stats.get("prompt_tokens", 0), "$ai_output_tokens": usage_stats.get("completion_tokens", 0), "$ai_cache_read_input_tokens": usage_stats.get("cache_read_input_tokens", 0), + "$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0), "$ai_latency": latency, "$ai_trace_id": posthog_trace_id, "$ai_base_url": str(self._client.base_url), diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index 9c2ed984..116b7ae0 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -31,6 +31,169 @@ def __init__(self, posthog_client: PostHogClient, **kwargs): self.chat = WrappedChat(self) self.embeddings = WrappedEmbeddings(self) self.beta = WrappedBeta(self) + self.responses = WrappedResponses(self) + + +class WrappedResponses(openai.resources.responses.Responses): + _client: AsyncOpenAI + + async def create( + self, + posthog_distinct_id: Optional[str] = None, + posthog_trace_id: Optional[str] = None, + posthog_properties: Optional[Dict[str, Any]] = None, + posthog_privacy_mode: bool = False, + posthog_groups: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ): + if posthog_trace_id is None: + posthog_trace_id = uuid.uuid4() + + if kwargs.get("stream", False): + return self._create_streaming( + posthog_distinct_id, + posthog_trace_id, + posthog_properties, + posthog_privacy_mode, + posthog_groups, + **kwargs, + ) + + return await call_llm_and_track_usage_async( + posthog_distinct_id, + self._client._ph_client, + "openai", + posthog_trace_id, + posthog_properties, + posthog_privacy_mode, + posthog_groups, + self._client.base_url, + super().create, + **kwargs, + ) + + async def _create_streaming( + self, + posthog_distinct_id: Optional[str], + posthog_trace_id: Optional[str], + posthog_properties: Optional[Dict[str, Any]], + posthog_privacy_mode: bool, + posthog_groups: Optional[Dict[str, Any]], + **kwargs: Any, + ): + start_time = time.time() + usage_stats: Dict[str, int] = {} + final_content = [] + response = super().create(**kwargs) + + async def async_generator(): + nonlocal usage_stats + nonlocal final_content + + try: + for chunk in response: + if hasattr(chunk, "type") and chunk.type == "response.completed": + res = chunk.response + if res.output and len(res.output) > 0: + final_content.append(res.output[0]) + + if hasattr(chunk, "usage") and chunk.usage: + usage_stats = { + k: getattr(chunk.usage, k, 0) + for k in [ + "input_tokens", + "output_tokens", + "total_tokens", + ] + } + + # Add support for cached tokens + if hasattr(chunk.usage, "output_tokens_details") and hasattr( + chunk.usage.output_tokens_details, "reasoning_tokens" + ): + usage_stats["reasoning_tokens"] = chunk.usage.output_tokens_details.reasoning_tokens + + if hasattr(chunk.usage, "input_tokens_details") and hasattr( + chunk.usage.input_tokens_details, "cached_tokens" + ): + usage_stats["cache_read_input_tokens"] = chunk.usage.input_tokens_details.cached_tokens + + yield chunk + + finally: + end_time = time.time() + latency = end_time - start_time + output = final_content + self._capture_streaming_event( + posthog_distinct_id, + posthog_trace_id, + posthog_properties, + posthog_privacy_mode, + posthog_groups, + kwargs, + usage_stats, + latency, + output, + ) + + return async_generator() + + async def _capture_streaming_event( + self, + posthog_distinct_id: Optional[str], + posthog_trace_id: Optional[str], + posthog_properties: Optional[Dict[str, Any]], + posthog_privacy_mode: bool, + posthog_groups: Optional[Dict[str, Any]], + kwargs: Dict[str, Any], + usage_stats: Dict[str, int], + latency: float, + output: str, + tool_calls: Optional[List[Dict[str, Any]]] = None, + ): + if posthog_trace_id is None: + posthog_trace_id = uuid.uuid4() + + event_properties = { + "$ai_provider": "openai", + "$ai_model": kwargs.get("model"), + "$ai_model_parameters": get_model_params(kwargs), + "$ai_input": with_privacy_mode(self._client._ph_client, posthog_privacy_mode, kwargs.get("input")), + "$ai_output_choices": with_privacy_mode( + self._client._ph_client, + posthog_privacy_mode, + output, + ), + "$ai_http_status": 200, + "$ai_input_tokens": usage_stats.get("input_tokens", 0), + "$ai_output_tokens": usage_stats.get("output_tokens", 0), + "$ai_cache_read_input_tokens": usage_stats.get("cache_read_input_tokens", 0), + "$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0), + "$ai_latency": latency, + "$ai_trace_id": posthog_trace_id, + "$ai_base_url": str(self._client.base_url), + **(posthog_properties or {}), + } + + if tool_calls: + event_properties["$ai_tools"] = with_privacy_mode( + self._client._ph_client, + posthog_privacy_mode, + tool_calls, + ) + + if posthog_distinct_id is None: + event_properties["$process_person_profile"] = False + + if hasattr(self._client._ph_client, "capture"): + await self._client._ph_client.capture( + distinct_id=posthog_distinct_id or posthog_trace_id, + event="$ai_generation", + properties=event_properties, + groups=posthog_groups, + ) + + class WrappedChat(openai.resources.chat.AsyncChat): @@ -206,7 +369,7 @@ async def _capture_streaming_event( event_properties["$process_person_profile"] = False if hasattr(self._client._ph_client, "capture"): - self._client._ph_client.capture( + await self._client._ph_client.capture( distinct_id=posthog_distinct_id or posthog_trace_id, event="$ai_generation", properties=event_properties, diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index 51217f80..e59d7a95 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -39,20 +39,46 @@ def get_usage(response, provider: str) -> Dict[str, Any]: } elif provider == "openai": cached_tokens = 0 + input_tokens = 0 + output_tokens = 0 + reasoning_tokens = 0 + + # responses api + if hasattr(response.usage, "input_tokens"): + input_tokens = response.usage.input_tokens + if hasattr(response.usage, "output_tokens"): + output_tokens = response.usage.output_tokens + if hasattr(response.usage, "input_tokens_details") and hasattr( + response.usage.input_tokens_details, "cached_tokens" + ): + cached_tokens = response.usage.input_tokens_details.cached_tokens + if hasattr(response.usage, "output_tokens_details" ) and hasattr( + response.usage.output_tokens_details, "reasoning_tokens" + ): + reasoning_tokens = response.usage.output_tokens_details.reasoning_tokens + + # chat completions + if hasattr(response.usage, "prompt_tokens"): + input_tokens = response.usage.prompt_tokens + if hasattr(response.usage, "completion_tokens"): + output_tokens = response.usage.completion_tokens if hasattr(response.usage, "prompt_tokens_details") and hasattr( response.usage.prompt_tokens_details, "cached_tokens" ): cached_tokens = response.usage.prompt_tokens_details.cached_tokens + return { - "input_tokens": response.usage.prompt_tokens, - "output_tokens": response.usage.completion_tokens, + "input_tokens": input_tokens, + "output_tokens": output_tokens, "cache_read_input_tokens": cached_tokens, + "reasoning_tokens": reasoning_tokens, } return { "input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, + "reasoning_tokens": 0, } @@ -85,12 +111,24 @@ def format_response_anthropic(response): def format_response_openai(response): output = [] - for choice in response.choices: - if choice.message.content: + if hasattr(response, "choices"): + for choice in response.choices: + # Handle Chat Completions response format + if hasattr(choice, "message") and choice.message and choice.message.content: + output.append( + { + "content": choice.message.content, + "role": choice.message.role, + } + ) + # Handle Responses API format + if hasattr(response, "output"): + for message in response.output: + output.append( { - "content": choice.message.content, - "role": choice.message.role, + "content": message.content, + "role": message.role, } ) return output @@ -101,23 +139,60 @@ def format_tool_calls(response, provider: str): if hasattr(response, "tools") and response.tools and len(response.tools) > 0: return response.tools elif provider == "openai": - if ( - hasattr(response, "choices") - and response.choices - and hasattr(response.choices[0].message, "tool_calls") - and response.choices[0].message.tool_calls - ): - return response.choices[0].message.tool_calls + # Handle both Chat Completions and Responses API + if hasattr(response, "choices") and response.choices: + # Check for tool_calls in message (Chat Completions format) + if (hasattr(response.choices[0], "message") and + hasattr(response.choices[0].message, "tool_calls") and + response.choices[0].message.tool_calls): + return response.choices[0].message.tool_calls + + # Check for tool_calls directly in response (Responses API format) + if (hasattr(response.choices[0], "tool_calls") and + response.choices[0].tool_calls): + return response.choices[0].tool_calls return None def merge_system_prompt(kwargs: Dict[str, Any], provider: str): - if provider != "anthropic": - return kwargs.get("messages") - messages = kwargs.get("messages") or [] - if kwargs.get("system") is None: - return messages - return [{"role": "system", "content": kwargs.get("system")}] + messages + if provider == "anthropic": + messages = kwargs.get("messages") or [] + if kwargs.get("system") is None: + return messages + return [{"role": "system", "content": kwargs.get("system")}] + messages + + # For OpenAI, handle both Chat Completions and Responses API + messages = [] + + if kwargs.get("messages"): + messages = kwargs.get("messages") + + if kwargs.get("input"): + if isinstance(kwargs.get("input"), list): + messages.extend(kwargs.get("input")) + else: + messages.append({"role": "user", "content": kwargs.get("input")}) + + # Check if system prompt is provided as a separate parameter + if kwargs.get("system") is not None: + has_system = any(msg.get("role") == "system" for msg in messages) + if not has_system: + messages = [{"role": "system", "content": kwargs.get("system")}] + messages + + # For Responses API, add instructions to the system prompt if provided + if kwargs.get("instructions") is not None: + # Find the system message if it exists + system_idx = next((i for i, msg in enumerate(messages) if msg.get("role") == "system"), None) + + if system_idx is not None: + # Append instructions to existing system message + system_content = messages[system_idx].get("content", "") + messages[system_idx]["content"] = f"{system_content}\n\n{kwargs.get('instructions')}" + else: + # Create a new system message with instructions + messages = [{"role": "system", "content": kwargs.get("instructions")}] + messages + + return messages def call_llm_and_track_usage( @@ -192,9 +267,16 @@ def call_llm_and_track_usage( if usage.get("cache_creation_input_tokens") is not None and usage.get("cache_creation_input_tokens", 0) > 0: event_properties["$ai_cache_creation_input_tokens"] = usage.get("cache_creation_input_tokens", 0) + if usage.get("reasoning_tokens") is not None and usage.get("reasoning_tokens", 0) > 0: + event_properties["$ai_reasoning_tokens"] = usage.get("reasoning_tokens", 0) + if posthog_distinct_id is None: event_properties["$process_person_profile"] = False + # Process instructions for Responses API + if provider == "openai" and kwargs.get("instructions") is not None: + event_properties["$ai_instructions"] = with_privacy_mode(ph_client, posthog_privacy_mode, kwargs.get("instructions")) + # send the event to posthog if hasattr(ph_client, "capture") and callable(ph_client.capture): ph_client.capture( @@ -281,6 +363,10 @@ async def call_llm_and_track_usage_async( if posthog_distinct_id is None: event_properties["$process_person_profile"] = False + # Process instructions for Responses API + if provider == "openai" and kwargs.get("instructions") is not None: + event_properties["$ai_instructions"] = with_privacy_mode(ph_client, posthog_privacy_mode, kwargs.get("instructions")) + # send the event to posthog if hasattr(ph_client, "capture") and callable(ph_client.capture): ph_client.capture( diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index 071d7af6..9462d685 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -12,6 +12,7 @@ from openai.types.completion_usage import CompletionUsage from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage from openai.types.embedding import Embedding +from openai.types.responses import Response, ResponseOutputItem, ResponseUsage, ResponseOutputMessage from posthog.ai.openai import OpenAI @@ -35,7 +36,10 @@ def mock_openai_response(): finish_reason="stop", index=0, message=ChatCompletionMessage( - content="Test response", + content={ + "type": "text", + "text": "Test response", + }, role="assistant", ), ) @@ -48,6 +52,31 @@ def mock_openai_response(): ) +@pytest.fixture +def mock_openai_response_with_responses_api(): + return Response( + id="test", + model="gpt-4o-mini", + object="response", + output=[ + ResponseOutputItem( + type="message", + role="assistant", + content=[ + ResponseOutputMessage( + type="text", + text="Test response", + ) + ], + ) + ], + usage=ResponseUsage( + input_tokens=10, + output_tokens=10, + total_tokens=20, + ), + ) + @pytest.fixture def mock_embedding_response(): return CreateEmbeddingResponse( @@ -499,3 +528,25 @@ def test_streaming_with_tool_calls(mock_client): # Check token usage assert props["$ai_input_tokens"] == 20 assert props["$ai_output_tokens"] == 15 + + +# test responses api +def test_responses_api(mock_client): + with patch("openai.resources.responses.Responses.create") as mock_create: + mock_create.return_value = mock_openai_response_with_responses_api + client = OpenAI(api_key="test-key", posthog_client=mock_client) + response = client.responses.create(model="gpt-4o-mini", input=[{"role": "user", "content": "Hello"}]) + assert response == mock_openai_response_with_responses_api + assert mock_client.capture.call_count == 1 + + call_args = mock_client.capture.call_args[1] + props = call_args["properties"] + print(props) + assert props["$ai_provider"] == "openai" + assert props["$ai_model"] == "gpt-4o-mini" + assert props["$ai_input"] == [{"role": "user", "content": "Hello"}] + assert props["$ai_output_choices"] == [{"role": "assistant", "content": "Test response"}] + assert props["$ai_input_tokens"] == 10 + assert props["$ai_output_tokens"] == 10 + assert props["$ai_http_status"] == 200 + assert isinstance(props["$ai_latency"], float) \ No newline at end of file diff --git a/posthog/version.py b/posthog/version.py index 7b5d56f5..adf8bb46 100644 --- a/posthog/version.py +++ b/posthog/version.py @@ -1,4 +1,4 @@ -VERSION = "3.19.1" +VERSION = "3.20.0" if __name__ == "__main__": print(VERSION, end="") # noqa: T201 From b9e9e9c7f27e7a6ddb80597da54df71696553fc4 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 12 Mar 2025 18:02:57 -0700 Subject: [PATCH 02/14] fix: test --- posthog/ai/openai/openai_async.py | 6 +++--- posthog/test/ai/openai/test_openai.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index 116b7ae0..b8ec849b 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -50,7 +50,7 @@ async def create( posthog_trace_id = uuid.uuid4() if kwargs.get("stream", False): - return self._create_streaming( + return await self._create_streaming( posthog_distinct_id, posthog_trace_id, posthog_properties, @@ -84,14 +84,14 @@ async def _create_streaming( start_time = time.time() usage_stats: Dict[str, int] = {} final_content = [] - response = super().create(**kwargs) + response = await super().create(**kwargs) async def async_generator(): nonlocal usage_stats nonlocal final_content try: - for chunk in response: + async for chunk in response: if hasattr(chunk, "type") and chunk.type == "response.completed": res = chunk.response if res.output and len(res.output) > 0: diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index 9462d685..0d98e9c1 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -541,7 +541,6 @@ def test_responses_api(mock_client): call_args = mock_client.capture.call_args[1] props = call_args["properties"] - print(props) assert props["$ai_provider"] == "openai" assert props["$ai_model"] == "gpt-4o-mini" assert props["$ai_input"] == [{"role": "user", "content": "Hello"}] From f0cfbab8808723aac226b1fb15714a2399a6d590 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 12 Mar 2025 18:03:33 -0700 Subject: [PATCH 03/14] fix: black --- posthog/ai/openai/openai.py | 18 +++++++------- posthog/ai/openai/openai_async.py | 20 +++++++-------- posthog/ai/utils.py | 35 +++++++++++++++------------ posthog/test/ai/openai/test_openai.py | 5 ++-- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index 2876ee29..debb5741 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -37,15 +37,15 @@ def __init__(self, posthog_client: PostHogClient, **kwargs): class WrappedResponses(openai.resources.responses.Responses): _client: OpenAI - + def create( - self, - posthog_distinct_id: Optional[str] = None, - posthog_trace_id: Optional[str] = None, - posthog_properties: Optional[Dict[str, Any]] = None, - posthog_privacy_mode: bool = False, - posthog_groups: Optional[Dict[str, Any]] = None, - **kwargs: Any, + self, + posthog_distinct_id: Optional[str] = None, + posthog_trace_id: Optional[str] = None, + posthog_properties: Optional[Dict[str, Any]] = None, + posthog_privacy_mode: bool = False, + posthog_groups: Optional[Dict[str, Any]] = None, + **kwargs: Any, ): if posthog_trace_id is None: posthog_trace_id = uuid.uuid4() @@ -72,7 +72,7 @@ def create( super().create, **kwargs, ) - + def _create_streaming( self, posthog_distinct_id: Optional[str], diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index b8ec849b..5c8d6334 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -36,15 +36,15 @@ def __init__(self, posthog_client: PostHogClient, **kwargs): class WrappedResponses(openai.resources.responses.Responses): _client: AsyncOpenAI - + async def create( - self, - posthog_distinct_id: Optional[str] = None, - posthog_trace_id: Optional[str] = None, - posthog_properties: Optional[Dict[str, Any]] = None, - posthog_privacy_mode: bool = False, - posthog_groups: Optional[Dict[str, Any]] = None, - **kwargs: Any, + self, + posthog_distinct_id: Optional[str] = None, + posthog_trace_id: Optional[str] = None, + posthog_properties: Optional[Dict[str, Any]] = None, + posthog_privacy_mode: bool = False, + posthog_groups: Optional[Dict[str, Any]] = None, + **kwargs: Any, ): if posthog_trace_id is None: posthog_trace_id = uuid.uuid4() @@ -71,7 +71,7 @@ async def create( super().create, **kwargs, ) - + async def _create_streaming( self, posthog_distinct_id: Optional[str], @@ -194,8 +194,6 @@ async def _capture_streaming_event( ) - - class WrappedChat(openai.resources.chat.AsyncChat): _client: AsyncOpenAI diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index e59d7a95..262883f1 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -52,7 +52,7 @@ def get_usage(response, provider: str) -> Dict[str, Any]: response.usage.input_tokens_details, "cached_tokens" ): cached_tokens = response.usage.input_tokens_details.cached_tokens - if hasattr(response.usage, "output_tokens_details" ) and hasattr( + if hasattr(response.usage, "output_tokens_details") and hasattr( response.usage.output_tokens_details, "reasoning_tokens" ): reasoning_tokens = response.usage.output_tokens_details.reasoning_tokens @@ -142,14 +142,15 @@ def format_tool_calls(response, provider: str): # Handle both Chat Completions and Responses API if hasattr(response, "choices") and response.choices: # Check for tool_calls in message (Chat Completions format) - if (hasattr(response.choices[0], "message") and - hasattr(response.choices[0].message, "tool_calls") and - response.choices[0].message.tool_calls): + if ( + hasattr(response.choices[0], "message") + and hasattr(response.choices[0].message, "tool_calls") + and response.choices[0].message.tool_calls + ): return response.choices[0].message.tool_calls - + # Check for tool_calls directly in response (Responses API format) - if (hasattr(response.choices[0], "tool_calls") and - response.choices[0].tool_calls): + if hasattr(response.choices[0], "tool_calls") and response.choices[0].tool_calls: return response.choices[0].tool_calls return None @@ -160,10 +161,10 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str): if kwargs.get("system") is None: return messages return [{"role": "system", "content": kwargs.get("system")}] + messages - + # For OpenAI, handle both Chat Completions and Responses API messages = [] - + if kwargs.get("messages"): messages = kwargs.get("messages") @@ -172,18 +173,18 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str): messages.extend(kwargs.get("input")) else: messages.append({"role": "user", "content": kwargs.get("input")}) - + # Check if system prompt is provided as a separate parameter if kwargs.get("system") is not None: has_system = any(msg.get("role") == "system" for msg in messages) if not has_system: messages = [{"role": "system", "content": kwargs.get("system")}] + messages - + # For Responses API, add instructions to the system prompt if provided if kwargs.get("instructions") is not None: # Find the system message if it exists system_idx = next((i for i, msg in enumerate(messages) if msg.get("role") == "system"), None) - + if system_idx is not None: # Append instructions to existing system message system_content = messages[system_idx].get("content", "") @@ -191,7 +192,7 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str): else: # Create a new system message with instructions messages = [{"role": "system", "content": kwargs.get("instructions")}] + messages - + return messages @@ -275,7 +276,9 @@ def call_llm_and_track_usage( # Process instructions for Responses API if provider == "openai" and kwargs.get("instructions") is not None: - event_properties["$ai_instructions"] = with_privacy_mode(ph_client, posthog_privacy_mode, kwargs.get("instructions")) + event_properties["$ai_instructions"] = with_privacy_mode( + ph_client, posthog_privacy_mode, kwargs.get("instructions") + ) # send the event to posthog if hasattr(ph_client, "capture") and callable(ph_client.capture): @@ -365,7 +368,9 @@ async def call_llm_and_track_usage_async( # Process instructions for Responses API if provider == "openai" and kwargs.get("instructions") is not None: - event_properties["$ai_instructions"] = with_privacy_mode(ph_client, posthog_privacy_mode, kwargs.get("instructions")) + event_properties["$ai_instructions"] = with_privacy_mode( + ph_client, posthog_privacy_mode, kwargs.get("instructions") + ) # send the event to posthog if hasattr(ph_client, "capture") and callable(ph_client.capture): diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index 0d98e9c1..baef18c8 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -12,7 +12,7 @@ from openai.types.completion_usage import CompletionUsage from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage from openai.types.embedding import Embedding -from openai.types.responses import Response, ResponseOutputItem, ResponseUsage, ResponseOutputMessage +from openai.types.responses import Response, ResponseOutputItem, ResponseOutputMessage, ResponseUsage from posthog.ai.openai import OpenAI @@ -77,6 +77,7 @@ def mock_openai_response_with_responses_api(): ), ) + @pytest.fixture def mock_embedding_response(): return CreateEmbeddingResponse( @@ -548,4 +549,4 @@ def test_responses_api(mock_client): assert props["$ai_input_tokens"] == 10 assert props["$ai_output_tokens"] == 10 assert props["$ai_http_status"] == 200 - assert isinstance(props["$ai_latency"], float) \ No newline at end of file + assert isinstance(props["$ai_latency"], float) From 7d9eb5b0821517205246f6a874ad435dcda66f6b Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 10:00:15 -0700 Subject: [PATCH 04/14] fix: test - hopefully --- posthog/ai/openai/openai.py | 1 + posthog/ai/utils.py | 52 ++++++++++++++++++++++----- posthog/test/ai/openai/test_openai.py | 18 +++++++--- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index debb5741..9b337459 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -10,6 +10,7 @@ from posthog.ai.utils import call_llm_and_track_usage, get_model_params, with_privacy_mode from posthog.client import Client as PostHogClient +from openai.resources.responses import Responses class OpenAI(openai.OpenAI): diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index 262883f1..404adc8c 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -123,14 +123,50 @@ def format_response_openai(response): ) # Handle Responses API format if hasattr(response, "output"): - for message in response.output: - - output.append( - { - "content": message.content, - "role": message.role, - } - ) + for item in response.output: + if item.type == "message": + # Extract text content from the content list + if hasattr(item, "content") and isinstance(item.content, list): + for content_item in item.content: + if ( + hasattr(content_item, "type") + and content_item.type == "input_text" + and hasattr(content_item, "text") + ): + output.append( + { + "content": content_item.text, + "role": item.role, + } + ) + elif hasattr(content_item, "text"): + output.append( + { + "content": content_item.text, + "role": item.role, + } + ) + elif ( + hasattr(content_item, "type") + and content_item.type == "input_image" + and hasattr(content_item, "image_url") + ): + output.append( + { + "content": { + "type": "image", + "image": content_item.image_url, + }, + "role": item.role, + } + ) + else: + output.append( + { + "content": item.content, + "role": item.role, + } + ) return output diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index baef18c8..004a22f3 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -64,7 +64,7 @@ def mock_openai_response_with_responses_api(): role="assistant", content=[ ResponseOutputMessage( - type="text", + type="input_text", text="Test response", ) ], @@ -532,16 +532,23 @@ def test_streaming_with_tool_calls(mock_client): # test responses api -def test_responses_api(mock_client): - with patch("openai.resources.responses.Responses.create") as mock_create: - mock_create.return_value = mock_openai_response_with_responses_api +def test_responses_api(mock_client, mock_openai_response_with_responses_api): + with patch("openai.resources.responses.Responses.create", return_value=mock_openai_response_with_responses_api): client = OpenAI(api_key="test-key", posthog_client=mock_client) - response = client.responses.create(model="gpt-4o-mini", input=[{"role": "user", "content": "Hello"}]) + response = client.responses.create( + model="gpt-4o-mini", + input=[{"role": "user", "content": "Hello"}], + posthog_distinct_id="test-id", + posthog_properties={"foo": "bar"}, + ) assert response == mock_openai_response_with_responses_api assert mock_client.capture.call_count == 1 call_args = mock_client.capture.call_args[1] props = call_args["properties"] + + assert call_args["distinct_id"] == "test-id" + assert call_args["event"] == "$ai_generation" assert props["$ai_provider"] == "openai" assert props["$ai_model"] == "gpt-4o-mini" assert props["$ai_input"] == [{"role": "user", "content": "Hello"}] @@ -549,4 +556,5 @@ def test_responses_api(mock_client): assert props["$ai_input_tokens"] == 10 assert props["$ai_output_tokens"] == 10 assert props["$ai_http_status"] == 200 + assert props["foo"] == "bar" assert isinstance(props["$ai_latency"], float) From dcd924b90bb2ab48f930439a0882b04c827cd4e4 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 10:03:05 -0700 Subject: [PATCH 05/14] fix: test - hopefully #2 --- posthog/ai/openai/openai.py | 1 - 1 file changed, 1 deletion(-) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index 9b337459..debb5741 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -10,7 +10,6 @@ from posthog.ai.utils import call_llm_and_track_usage, get_model_params, with_privacy_mode from posthog.client import Client as PostHogClient -from openai.resources.responses import Responses class OpenAI(openai.OpenAI): From 10e3c1189797907b2fe9c41121972e58014d52b8 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 16:11:57 -0700 Subject: [PATCH 06/14] fix: test - hopefully #3 --- posthog/ai/utils.py | 2 +- posthog/test/ai/openai/test_openai.py | 32 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index 404adc8c..ed514d08 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -130,7 +130,7 @@ def format_response_openai(response): for content_item in item.content: if ( hasattr(content_item, "type") - and content_item.type == "input_text" + and content_item.type == "output_text" and hasattr(content_item, "text") ): output.append( diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index 004a22f3..62a5ca93 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -12,7 +12,7 @@ from openai.types.completion_usage import CompletionUsage from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage from openai.types.embedding import Embedding -from openai.types.responses import Response, ResponseOutputItem, ResponseOutputMessage, ResponseUsage +from openai.types.responses import Response, ResponseUsage, ResponseOutputMessage, ResponseOutputText from posthog.ai.openai import OpenAI @@ -36,10 +36,7 @@ def mock_openai_response(): finish_reason="stop", index=0, message=ChatCompletionMessage( - content={ - "type": "text", - "text": "Test response", - }, + content="Test response", role="assistant", ), ) @@ -58,23 +55,39 @@ def mock_openai_response_with_responses_api(): id="test", model="gpt-4o-mini", object="response", + created_at=1741476542, + status="completed", + error=None, + incomplete_details=None, + instructions=None, + max_output_tokens=None, + tools=[], + tool_choice="auto", output=[ - ResponseOutputItem( + ResponseOutputMessage( + id="msg_123", type="message", role="assistant", + status="completed", content=[ - ResponseOutputMessage( - type="input_text", + ResponseOutputText( + type="output_text", text="Test response", + annotations=[], ) ], ) ], + parallel_tool_calls=True, + previous_response_id=None, usage=ResponseUsage( input_tokens=10, output_tokens=10, + output_tokens_details={"reasoning_tokens": 15}, total_tokens=20, ), + user=None, + metadata={}, ) @@ -537,7 +550,7 @@ def test_responses_api(mock_client, mock_openai_response_with_responses_api): client = OpenAI(api_key="test-key", posthog_client=mock_client) response = client.responses.create( model="gpt-4o-mini", - input=[{"role": "user", "content": "Hello"}], + input="Hello", posthog_distinct_id="test-id", posthog_properties={"foo": "bar"}, ) @@ -555,6 +568,7 @@ def test_responses_api(mock_client, mock_openai_response_with_responses_api): assert props["$ai_output_choices"] == [{"role": "assistant", "content": "Test response"}] assert props["$ai_input_tokens"] == 10 assert props["$ai_output_tokens"] == 10 + assert props["$ai_reasoning_tokens"] == 15 assert props["$ai_http_status"] == 200 assert props["foo"] == "bar" assert isinstance(props["$ai_latency"], float) From 20ec446095906c4aa4604456ba9f744d09502d9a Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 16:13:59 -0700 Subject: [PATCH 07/14] fix: test - hopefully #4 --- posthog/test/ai/openai/test_openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index 62a5ca93..25737b9b 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -12,7 +12,7 @@ from openai.types.completion_usage import CompletionUsage from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage from openai.types.embedding import Embedding -from openai.types.responses import Response, ResponseUsage, ResponseOutputMessage, ResponseOutputText +from openai.types.responses import Response, ResponseOutputMessage, ResponseOutputText, ResponseUsage from posthog.ai.openai import OpenAI From 5f7f1ff96966f548e9faa45fe37724ab1351eefe Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 17:35:16 -0700 Subject: [PATCH 08/14] fix: greptaile catch --- posthog/ai/openai/openai_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index 5c8d6334..a166ac05 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -124,7 +124,7 @@ async def async_generator(): end_time = time.time() latency = end_time - start_time output = final_content - self._capture_streaming_event( + await self._capture_streaming_event( posthog_distinct_id, posthog_trace_id, posthog_properties, From c1d75d7f469973b84b5f0ca28497633969caf54f Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 20:14:05 -0700 Subject: [PATCH 09/14] fix: mypy is not my friend --- posthog/ai/openai/openai.py | 10 +++++----- posthog/ai/openai/openai_async.py | 10 +++++----- posthog/ai/utils.py | 17 +++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index debb5741..a93f5a18 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -149,11 +149,11 @@ def _capture_streaming_event( kwargs: Dict[str, Any], usage_stats: Dict[str, int], latency: float, - output: str, + output: Any, tool_calls: Optional[List[Dict[str, Any]]] = None, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) event_properties = { "$ai_provider": "openai", @@ -216,7 +216,7 @@ def create( **kwargs: Any, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if kwargs.get("stream", False): return self._create_streaming( @@ -337,11 +337,11 @@ def _capture_streaming_event( kwargs: Dict[str, Any], usage_stats: Dict[str, int], latency: float, - output: str, + output: Any, tool_calls: Optional[List[Dict[str, Any]]] = None, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) event_properties = { "$ai_provider": "openai", diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index a166ac05..35f4038e 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -148,11 +148,11 @@ async def _capture_streaming_event( kwargs: Dict[str, Any], usage_stats: Dict[str, int], latency: float, - output: str, + output: Any, tool_calls: Optional[List[Dict[str, Any]]] = None, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) event_properties = { "$ai_provider": "openai", @@ -215,7 +215,7 @@ async def create( **kwargs: Any, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) # If streaming, handle streaming specifically if kwargs.get("stream", False): @@ -330,11 +330,11 @@ async def _capture_streaming_event( kwargs: Dict[str, Any], usage_stats: Dict[str, int], latency: float, - output: str, + output: Any, tool_calls: Optional[List[Dict[str, Any]]] = None, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) event_properties = { "$ai_provider": "openai", diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index ed514d08..d3b46389 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -1,6 +1,6 @@ import time import uuid -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, List from httpx import URL @@ -199,16 +199,17 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str): return [{"role": "system", "content": kwargs.get("system")}] + messages # For OpenAI, handle both Chat Completions and Responses API - messages = [] + messages: List[Dict[str, Any]] = [] - if kwargs.get("messages"): - messages = kwargs.get("messages") + if kwargs.get("messages") is not None: + messages = list(kwargs.get("messages", [])) - if kwargs.get("input"): - if isinstance(kwargs.get("input"), list): - messages.extend(kwargs.get("input")) + if kwargs.get("input") is not None: + input_data = kwargs.get("input") + if isinstance(input_data, list): + messages.extend(input_data) else: - messages.append({"role": "user", "content": kwargs.get("input")}) + messages.append({"role": "user", "content": input_data}) # Check if system prompt is provided as a separate parameter if kwargs.get("system") is not None: From 9fe47917907536755507faa0b7097c4aa3328685 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 20:15:10 -0700 Subject: [PATCH 10/14] fix: isort usort weallsort --- posthog/ai/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index d3b46389..94c9fa38 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -1,6 +1,6 @@ import time import uuid -from typing import Any, Callable, Dict, Optional, List +from typing import Any, Callable, Dict, List, Optional from httpx import URL From 86a25f48a11577938919a0763cc5b30b11147fa6 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 20:17:03 -0700 Subject: [PATCH 11/14] fix: noredef --- posthog/ai/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index 94c9fa38..ab906963 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -192,6 +192,7 @@ def format_tool_calls(response, provider: str): def merge_system_prompt(kwargs: Dict[str, Any], provider: str): + messages: List[Dict[str, Any]] = [] if provider == "anthropic": messages = kwargs.get("messages") or [] if kwargs.get("system") is None: @@ -199,8 +200,6 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str): return [{"role": "system", "content": kwargs.get("system")}] + messages # For OpenAI, handle both Chat Completions and Responses API - messages: List[Dict[str, Any]] = [] - if kwargs.get("messages") is not None: messages = list(kwargs.get("messages", [])) From 328063b9b3a9fcb88f96d19db453843a1459706e Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 20:24:50 -0700 Subject: [PATCH 12/14] fix: mypy baseline --- mypy-baseline.txt | 14 +------------- posthog/ai/anthropic/anthropic.py | 6 +++--- posthog/ai/anthropic/anthropic_async.py | 6 +++--- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 9d17951f..8d5e50b2 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -40,16 +40,4 @@ posthog/ai/utils.py:0: error: Function "builtins.any" is not valid as a type [v posthog/ai/utils.py:0: note: Perhaps you meant "typing.Any" instead of "any"? posthog/ai/utils.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] sentry_django_example/sentry_django_example/settings.py:0: error: Need type annotation for "ALLOWED_HOSTS" (hint: "ALLOWED_HOSTS: list[] = ...") [var-annotated] -sentry_django_example/sentry_django_example/settings.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "None") [assignment] -posthog/ai/openai/openai_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/openai/openai_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/openai/openai_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/openai/openai.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/openai/openai.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/openai/openai.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/anthropic/anthropic_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/anthropic/anthropic_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/anthropic/anthropic_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/anthropic/anthropic.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/anthropic/anthropic.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] -posthog/ai/anthropic/anthropic.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] +sentry_django_example/sentry_django_example/settings.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "None") [assignment] \ No newline at end of file diff --git a/posthog/ai/anthropic/anthropic.py b/posthog/ai/anthropic/anthropic.py index 276a4c03..b4fde16f 100644 --- a/posthog/ai/anthropic/anthropic.py +++ b/posthog/ai/anthropic/anthropic.py @@ -54,7 +54,7 @@ def create( **kwargs: Arguments passed to Anthropic's messages.create """ if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if kwargs.get("stream", False): return self._create_streaming( @@ -89,7 +89,7 @@ def stream( **kwargs: Any, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) return self._create_streaming( posthog_distinct_id, @@ -167,7 +167,7 @@ def _capture_streaming_event( output: str, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) event_properties = { "$ai_provider": "anthropic", diff --git a/posthog/ai/anthropic/anthropic_async.py b/posthog/ai/anthropic/anthropic_async.py index 0b155ca5..5dd05764 100644 --- a/posthog/ai/anthropic/anthropic_async.py +++ b/posthog/ai/anthropic/anthropic_async.py @@ -54,7 +54,7 @@ async def create( **kwargs: Arguments passed to Anthropic's messages.create """ if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if kwargs.get("stream", False): return await self._create_streaming( @@ -89,7 +89,7 @@ async def stream( **kwargs: Any, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) return await self._create_streaming( posthog_distinct_id, @@ -167,7 +167,7 @@ async def _capture_streaming_event( output: str, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) event_properties = { "$ai_provider": "anthropic", From f18e923138e3ef27bb75e26da626ba491845f3f7 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 22:12:50 -0700 Subject: [PATCH 13/14] fix: mypy --- posthog/ai/openai/openai.py | 4 ++-- posthog/ai/openai/openai_async.py | 4 ++-- posthog/ai/utils.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index a93f5a18..0c2be5fe 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -48,7 +48,7 @@ def create( **kwargs: Any, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if kwargs.get("stream", False): return self._create_streaming( @@ -408,7 +408,7 @@ def create( The response from OpenAI's embeddings.create call. """ if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) start_time = time.time() response = super().create(**kwargs) diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index 35f4038e..e12cd825 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -47,7 +47,7 @@ async def create( **kwargs: Any, ): if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if kwargs.get("stream", False): return await self._create_streaming( @@ -402,7 +402,7 @@ async def create( The response from OpenAI's embeddings.create call. """ if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) start_time = time.time() response = await super().create(**kwargs) diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index ab906963..ebee7cf4 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -269,7 +269,7 @@ def call_llm_and_track_usage( latency = end_time - start_time if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if response and hasattr(response, "usage"): usage = get_usage(response, provider) @@ -364,7 +364,7 @@ async def call_llm_and_track_usage_async( latency = end_time - start_time if posthog_trace_id is None: - posthog_trace_id = uuid.uuid4() + posthog_trace_id = str(uuid.uuid4()) if response and hasattr(response, "usage"): usage = get_usage(response, provider) From e19282b877d91c3c0f1922e4f67a0f75a5866d4f Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 13 Mar 2025 22:13:58 -0700 Subject: [PATCH 14/14] fix: mypy --- mypy-baseline.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 8d5e50b2..be96ec92 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -35,9 +35,7 @@ posthog/sentry/posthog_integration.py:0: error: Statement is unreachable [unrea posthog/ai/utils.py:0: error: Need type annotation for "output" (hint: "output: list[] = ...") [var-annotated] posthog/ai/utils.py:0: error: Function "builtins.any" is not valid as a type [valid-type] posthog/ai/utils.py:0: note: Perhaps you meant "typing.Any" instead of "any"? -posthog/ai/utils.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] posthog/ai/utils.py:0: error: Function "builtins.any" is not valid as a type [valid-type] posthog/ai/utils.py:0: note: Perhaps you meant "typing.Any" instead of "any"? -posthog/ai/utils.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment] sentry_django_example/sentry_django_example/settings.py:0: error: Need type annotation for "ALLOWED_HOSTS" (hint: "ALLOWED_HOSTS: list[] = ...") [var-annotated] sentry_django_example/sentry_django_example/settings.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "None") [assignment] \ No newline at end of file