mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat: Playground refactor + add observability (#32625)
This commit is contained in:
@@ -4,7 +4,7 @@ from rest_framework_extensions.routers import NestedRegistryItem
|
||||
import products.data_warehouse.backend.api.fix_hogql as fix_hogql
|
||||
import products.early_access_features.backend.api as early_access_feature
|
||||
from products.user_interviews.backend.api import UserInterviewViewSet
|
||||
from products.editor.backend.api import LLMProxyViewSet, MaxToolsViewSet
|
||||
from products.llm_observability.api import LLMProxyViewSet, MaxToolsViewSet
|
||||
from products.messaging.backend.api import MessageTemplatesViewSet
|
||||
import products.logs.backend.api as logs
|
||||
from posthog.api import data_color_theme, metalytics, project, wizard
|
||||
|
||||
@@ -94,8 +94,8 @@
|
||||
'/home/runner/work/posthog/posthog/posthog/session_recordings/session_recording_playlist_api.py: Warning [SessionRecordingPlaylistViewSet]: could not derive type of path parameter "session_recording_id" because model "posthog.session_recordings.models.session_recording_playlist.SessionRecordingPlaylist" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
'/home/runner/work/posthog/posthog/posthog/taxonomy/property_definition_api.py: Warning [PropertyDefinitionViewSet > PropertyDefinitionSerializer]: unable to resolve type hint for function "is_seen_on_filtered_events". Consider using a type hint or @extend_schema_field. Defaulting to string.',
|
||||
'/home/runner/work/posthog/posthog/products/early_access_features/backend/api.py: Warning [EarlyAccessFeatureViewSet]: could not derive type of path parameter "project_id" because model "products.early_access_features.backend.models.EarlyAccessFeature" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
"/home/runner/work/posthog/posthog/products/editor/backend/api/max_tools.py: Error [MaxToolsViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'MaxToolsViewSet' should either include a `serializer_class` attribute, or override the `get_serializer_class()` method.)",
|
||||
'/home/runner/work/posthog/posthog/products/editor/backend/api/max_tools.py: Warning [MaxToolsViewSet]: could not derive type of path parameter "project_id" because model "ee.models.assistant.Conversation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
"/home/runner/work/posthog/posthog/products/llm_observability/api/max_tools.py: Error [MaxToolsViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'MaxToolsViewSet' should either include a `serializer_class` attribute, or override the `get_serializer_class()` method.)",
|
||||
'/home/runner/work/posthog/posthog/products/llm_observability/api/max_tools.py: Warning [MaxToolsViewSet]: could not derive type of path parameter "project_id" because model "ee.models.assistant.Conversation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
'/home/runner/work/posthog/posthog/products/logs/backend/api.py: Error [LogsViewSet]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.',
|
||||
'/home/runner/work/posthog/posthog/products/logs/backend/api.py: Warning [LogsViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:project_id>) or annotating the parameter type with @extend_schema. Defaulting to "string".',
|
||||
'/home/runner/work/posthog/posthog/products/user_interviews/backend/api.py: Warning [UserInterviewViewSet]: could not derive type of path parameter "project_id" because model "products.user_interviews.backend.models.UserInterview" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
|
||||
@@ -321,15 +321,15 @@ class AISustainedRateThrottle(UserRateThrottle):
|
||||
rate = "40/day"
|
||||
|
||||
|
||||
class EditorProxyBurstRateThrottle(UserRateThrottle):
|
||||
scope = "editor_proxy_burst"
|
||||
class LLMProxyBurstRateThrottle(UserRateThrottle):
|
||||
scope = "llm_proxy_burst"
|
||||
rate = "30/minute"
|
||||
|
||||
|
||||
class EditorProxySustainedRateThrottle(UserRateThrottle):
|
||||
class LLMProxySustainedRateThrottle(UserRateThrottle):
|
||||
# Throttle class that's very aggressive and is used specifically on endpoints that hit OpenAI
|
||||
# Intended to block slower but sustained bursts of requests, per user
|
||||
scope = "editor_proxy_sustained"
|
||||
scope = "llm_proxy_sustained"
|
||||
rate = "500/hour"
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ AXES_META_PRECEDENCE_ORDER = ["HTTP_X_FORWARDED_FOR", "REMOTE_ADDR"]
|
||||
# NOTE: Add these definitions here and on `tach.toml`
|
||||
PRODUCTS_APPS = [
|
||||
"products.early_access_features",
|
||||
"products.editor",
|
||||
"products.links",
|
||||
"products.revenue_analytics",
|
||||
"products.user_interviews",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "@posthog/editor",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ from posthog.api.routing import TeamAndOrgViewSetMixin
|
||||
from posthog.auth import PersonalAPIKeyAuthentication
|
||||
from posthog.models.user import User
|
||||
from posthog.rate_limit import AIBurstRateThrottle, AISustainedRateThrottle
|
||||
from posthog.renderers import SafeJSONRenderer
|
||||
from products.editor.backend.api.proxy import ServerSentEventRenderer
|
||||
from posthog.renderers import SafeJSONRenderer, ServerSentEventRenderer
|
||||
|
||||
|
||||
class InsightsToolCallSerializer(serializers.Serializer):
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
ViewSet for Editor Proxy
|
||||
ViewSet for LLM Observability Proxy
|
||||
|
||||
Endpoints:
|
||||
- GET /api/llm_proxy/models
|
||||
@@ -9,6 +9,7 @@ Endpoints:
|
||||
|
||||
import json
|
||||
import posthoganalytics
|
||||
import uuid
|
||||
from rest_framework import viewsets
|
||||
from posthog.auth import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
@@ -17,13 +18,13 @@ from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from django.http import StreamingHttpResponse
|
||||
from rest_framework.response import Response
|
||||
from posthog.rate_limit import EditorProxyBurstRateThrottle, EditorProxySustainedRateThrottle
|
||||
from posthog.rate_limit import LLMProxyBurstRateThrottle, LLMProxySustainedRateThrottle
|
||||
from posthog.renderers import SafeJSONRenderer, ServerSentEventRenderer
|
||||
from products.editor.backend.providers.anthropic import AnthropicProvider, AnthropicConfig
|
||||
from products.editor.backend.providers.openai import OpenAIProvider, OpenAIConfig
|
||||
from products.editor.backend.providers.codestral import CodestralProvider, CodestralConfig
|
||||
from products.editor.backend.providers.inkeep import InkeepProvider, InkeepConfig
|
||||
from products.editor.backend.providers.gemini import GeminiProvider, GeminiConfig
|
||||
from products.llm_observability.providers.anthropic import AnthropicProvider, AnthropicConfig
|
||||
from products.llm_observability.providers.openai import OpenAIProvider, OpenAIConfig
|
||||
from products.llm_observability.providers.codestral import CodestralProvider, CodestralConfig
|
||||
from products.llm_observability.providers.inkeep import InkeepProvider, InkeepConfig
|
||||
from products.llm_observability.providers.gemini import GeminiProvider, GeminiConfig
|
||||
from posthog.settings import SERVER_GATEWAY_INTERFACE
|
||||
from ee.hogai.utils.asgi import SyncIterableToAsync
|
||||
from collections.abc import Generator, Callable
|
||||
@@ -64,8 +65,8 @@ class ProviderData(TypedDict):
|
||||
|
||||
class LLMProxyViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
ViewSet for Editor Proxy
|
||||
Proxies LLM calls from the editor
|
||||
ViewSet for LLM Observability Proxy
|
||||
Proxies LLM calls from the llm observability playground
|
||||
"""
|
||||
|
||||
authentication_classes = [SessionAuthentication]
|
||||
@@ -73,16 +74,14 @@ class LLMProxyViewSet(viewsets.ViewSet):
|
||||
renderer_classes = [SafeJSONRenderer, ServerSentEventRenderer]
|
||||
|
||||
def get_throttles(self):
|
||||
return [EditorProxyBurstRateThrottle(), EditorProxySustainedRateThrottle()]
|
||||
return [LLMProxyBurstRateThrottle(), LLMProxySustainedRateThrottle()]
|
||||
|
||||
def validate_feature_flag(self, request):
|
||||
result_session = SessionAuthentication().authenticate(request)
|
||||
if result_session is not None:
|
||||
user, _ = result_session
|
||||
else:
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
llm_observability_enabled = posthoganalytics.feature_enabled(
|
||||
"llm-observability-playground", user.email, person_properties={"email": user.email}
|
||||
"llm-observability-playground", request.user.email, person_properties={"email": request.user.email}
|
||||
)
|
||||
return llm_observability_enabled
|
||||
|
||||
@@ -139,6 +138,16 @@ class LLMProxyViewSet(viewsets.ViewSet):
|
||||
if isinstance(provider, Response): # Error response
|
||||
return provider
|
||||
|
||||
# Generate tracking parameters for PostHog observability
|
||||
trace_id = str(uuid.uuid4())
|
||||
distinct_id = getattr(request.user, "email", "") if request.user and request.user.is_authenticated else ""
|
||||
properties = {"ai_product": "playground"}
|
||||
groups = {} # placeholder for groups, maybe we should add team_id here or something????
|
||||
if request.user and request.user.is_authenticated:
|
||||
team_id = getattr(request.user, "team_id", None)
|
||||
if team_id:
|
||||
groups["team"] = str(team_id)
|
||||
|
||||
if mode == "completion" and hasattr(provider, "stream_response"):
|
||||
messages = serializer.validated_data.get("messages")
|
||||
if not self.validate_messages(messages):
|
||||
@@ -151,6 +160,10 @@ class LLMProxyViewSet(viewsets.ViewSet):
|
||||
"thinking": serializer.validated_data.get("thinking", False),
|
||||
"temperature": serializer.validated_data.get("temperature"),
|
||||
"max_tokens": serializer.validated_data.get("max_tokens"),
|
||||
"distinct_id": distinct_id,
|
||||
"trace_id": trace_id,
|
||||
"properties": properties,
|
||||
"groups": groups,
|
||||
}
|
||||
),
|
||||
request,
|
||||
@@ -164,6 +177,10 @@ class LLMProxyViewSet(viewsets.ViewSet):
|
||||
"stop": serializer.validated_data.get("stop"),
|
||||
"temperature": serializer.validated_data.get("temperature"),
|
||||
"max_tokens": serializer.validated_data.get("max_tokens"),
|
||||
"distinct_id": distinct_id,
|
||||
"trace_id": trace_id,
|
||||
"properties": properties,
|
||||
"groups": groups,
|
||||
}
|
||||
),
|
||||
request,
|
||||
@@ -211,10 +228,10 @@ class LLMProxyViewSet(viewsets.ViewSet):
|
||||
"""Return a list of available models across providers"""
|
||||
model_list: list[dict[str, str]] = []
|
||||
model_list += [
|
||||
{"id": m, "name": m, "provider": "Anthropic", "description": ""} for m in AnthropicConfig.SUPPORTED_MODELS
|
||||
{"id": m, "name": m, "provider": "OpenAI", "description": ""} for m in OpenAIConfig.SUPPORTED_MODELS
|
||||
]
|
||||
model_list += [
|
||||
{"id": m, "name": m, "provider": "OpenAI", "description": ""} for m in OpenAIConfig.SUPPORTED_MODELS
|
||||
{"id": m, "name": m, "provider": "Anthropic", "description": ""} for m in AnthropicConfig.SUPPORTED_MODELS
|
||||
]
|
||||
model_list += [
|
||||
{"id": m, "name": m, "provider": "Gemini", "description": ""} for m in GeminiConfig.SUPPORTED_MODELS
|
||||
@@ -1,5 +1,5 @@
|
||||
from posthog.test.base import BaseTest
|
||||
from products.editor.backend.chunking import ProgrammingLanguage, chunk_text
|
||||
from products.llm_observability.chunking import ProgrammingLanguage, chunk_text
|
||||
|
||||
from .util import load_fixture
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from posthog.test.base import BaseTest
|
||||
from products.editor.backend.chunking import chunk_text
|
||||
from products.llm_observability.chunking import chunk_text
|
||||
|
||||
from .util import load_fixture
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
from collections.abc import Generator
|
||||
from django.conf import settings
|
||||
import anthropic
|
||||
import posthoganalytics
|
||||
from posthoganalytics.ai.anthropic import Anthropic
|
||||
from anthropic.types import MessageParam, TextBlockParam, ThinkingConfigEnabledParam
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,7 +45,11 @@ class AnthropicConfig:
|
||||
|
||||
class AnthropicProvider:
|
||||
def __init__(self, model_id: str):
|
||||
self.client = anthropic.Anthropic(api_key=self.get_api_key())
|
||||
posthog_client = posthoganalytics.default_client
|
||||
if not posthog_client:
|
||||
raise ValueError("PostHog client not found")
|
||||
|
||||
self.client = Anthropic(api_key=self.get_api_key(), posthog_client=posthog_client)
|
||||
self.validate_model(model_id)
|
||||
self.model_id = model_id
|
||||
|
||||
@@ -105,6 +111,10 @@ class AnthropicProvider:
|
||||
thinking: bool = False,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
distinct_id: str = "",
|
||||
trace_id: str | None = None,
|
||||
properties: dict | None = None,
|
||||
groups: dict | None = None,
|
||||
) -> Generator[str, None]:
|
||||
"""
|
||||
Async generator function that yields SSE formatted data
|
||||
@@ -128,9 +138,10 @@ class AnthropicProvider:
|
||||
else:
|
||||
system_prompt = [TextBlockParam(**{"text": system, "type": "text", "cache_control": None})]
|
||||
formatted_messages = [MessageParam(content=msg["content"], role=msg["role"]) for msg in messages]
|
||||
|
||||
try:
|
||||
if reasoning_on:
|
||||
stream = self.client.messages.create(
|
||||
stream = self.client.messages.create( # type: ignore[call-overload]
|
||||
messages=formatted_messages,
|
||||
max_tokens=effective_max_tokens,
|
||||
model=self.model_id,
|
||||
@@ -140,24 +151,28 @@ class AnthropicProvider:
|
||||
thinking=ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=AnthropicConfig.MAX_THINKING_TOKENS
|
||||
),
|
||||
posthog_distinct_id=distinct_id,
|
||||
posthog_trace_id=trace_id or str(uuid.uuid4()),
|
||||
posthog_properties={**(properties or {}), "ai_product": "playground"},
|
||||
posthog_groups=groups or {},
|
||||
)
|
||||
else:
|
||||
stream = self.client.messages.create(
|
||||
stream = self.client.messages.create( # type: ignore[call-overload]
|
||||
messages=formatted_messages,
|
||||
max_tokens=effective_max_tokens,
|
||||
model=self.model_id,
|
||||
system=system_prompt,
|
||||
stream=True,
|
||||
temperature=effective_temperature,
|
||||
posthog_distinct_id=distinct_id,
|
||||
posthog_trace_id=trace_id or str(uuid.uuid4()),
|
||||
posthog_properties={**(properties or {}), "ai_product": "playground"},
|
||||
posthog_groups=groups or {},
|
||||
)
|
||||
except anthropic.APIError as e:
|
||||
except Exception as e:
|
||||
logger.exception(f"Anthropic API error: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'error': f'Anthropic API error'})}\n\n"
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'error': f'Unexpected error'})}\n\n"
|
||||
return
|
||||
|
||||
for chunk in stream:
|
||||
if chunk.type == "message_start":
|
||||
@@ -7,6 +7,8 @@ import json
|
||||
from collections.abc import Generator
|
||||
from django.conf import settings
|
||||
import mistralai
|
||||
import posthoganalytics
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,6 +46,10 @@ class CodestralProvider:
|
||||
stop: list[str],
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
distinct_id: str = "",
|
||||
trace_id: str | None = None,
|
||||
properties: dict | None = None,
|
||||
groups: dict | None = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generator function that yields SSE formatted data
|
||||
@@ -53,6 +59,21 @@ class CodestralProvider:
|
||||
effective_temperature = temperature if temperature is not None else CodestralConfig.TEMPERATURE
|
||||
effective_max_tokens = max_tokens if max_tokens is not None else CodestralConfig.MAX_TOKENS
|
||||
|
||||
# Manually track with PostHog since Codestral doesn't have native support
|
||||
posthoganalytics.capture(
|
||||
distinct_id=distinct_id,
|
||||
event="$ai_generation",
|
||||
properties={
|
||||
**(properties or {}),
|
||||
"ai_product": "playground",
|
||||
"provider": "codestral",
|
||||
"model": self.model_id,
|
||||
"trace_id": trace_id or str(uuid.uuid4()),
|
||||
"mode": "fim",
|
||||
},
|
||||
groups=groups,
|
||||
)
|
||||
|
||||
response = self.client.fim.stream(
|
||||
model=self.model_id,
|
||||
prompt=prompt,
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
from anthropic.types import MessageParam
|
||||
from google.genai.types import Part, Content, Blob, ContentListUnion
|
||||
from typing import cast
|
||||
from products.editor.backend.providers.formatters.anthropic_typeguards import (
|
||||
from products.llm_observability.providers.formatters.anthropic_typeguards import (
|
||||
is_base64_image_param,
|
||||
is_image_block_param,
|
||||
is_text_block_param,
|
||||
@@ -10,7 +10,7 @@ from openai.types.chat import (
|
||||
ChatCompletionMessageToolCallParam,
|
||||
)
|
||||
|
||||
from products.editor.backend.providers.formatters.anthropic_typeguards import (
|
||||
from products.llm_observability.providers.formatters.anthropic_typeguards import (
|
||||
is_base64_image_param,
|
||||
is_image_block_param,
|
||||
is_text_block_param,
|
||||
@@ -1,14 +1,16 @@
|
||||
import google.genai as genai
|
||||
from posthoganalytics.ai.gemini import genai
|
||||
from google.genai.types import GenerateContentConfig
|
||||
from google.genai.errors import APIError
|
||||
|
||||
import json
|
||||
from collections.abc import Generator
|
||||
from django.conf import settings
|
||||
import posthoganalytics
|
||||
from anthropic.types import MessageParam
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from products.editor.backend.providers.formatters.gemini_formatter import convert_anthropic_messages_to_gemini
|
||||
from products.llm_observability.providers.formatters.gemini_formatter import convert_anthropic_messages_to_gemini
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,7 +31,11 @@ class GeminiConfig:
|
||||
|
||||
class GeminiProvider:
|
||||
def __init__(self, model_id: str):
|
||||
self.client = genai.Client(api_key=self.get_api_key())
|
||||
posthog_client = posthoganalytics.default_client
|
||||
if not posthog_client:
|
||||
raise ValueError("PostHog client not found")
|
||||
|
||||
self.client = genai.Client(api_key=self.get_api_key(), posthog_client=posthog_client)
|
||||
self.validate_model(model_id)
|
||||
self.model_id = model_id
|
||||
|
||||
@@ -51,6 +57,10 @@ class GeminiProvider:
|
||||
thinking: bool = False,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
distinct_id: str = "",
|
||||
trace_id: str | None = None,
|
||||
properties: dict | None = None,
|
||||
groups: dict | None = None,
|
||||
) -> Generator[str, None]:
|
||||
"""
|
||||
Async generator function that yields SSE formatted data
|
||||
@@ -73,7 +83,12 @@ class GeminiProvider:
|
||||
model=self.model_id,
|
||||
contents=convert_anthropic_messages_to_gemini(messages),
|
||||
config=GenerateContentConfig(**config_kwargs),
|
||||
posthog_distinct_id=distinct_id,
|
||||
posthog_trace_id=trace_id or str(uuid.uuid4()),
|
||||
posthog_properties={**(properties or {}), "ai_product": "playground"},
|
||||
posthog_groups=groups or {},
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
if chunk.text:
|
||||
yield f"data: {json.dumps({'type': 'text', 'text': chunk.text})}\n\n"
|
||||
@@ -5,9 +5,11 @@ from django.conf import settings
|
||||
import openai
|
||||
from anthropic.types import MessageParam
|
||||
import logging
|
||||
import posthoganalytics
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from products.editor.backend.providers.formatters.openai_formatter import convert_to_openai_messages
|
||||
from products.llm_observability.providers.formatters.openai_formatter import convert_to_openai_messages
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,12 +39,30 @@ class InkeepProvider:
|
||||
thinking: bool = False,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
distinct_id: str = "",
|
||||
trace_id: str | None = None,
|
||||
properties: dict | None = None,
|
||||
groups: dict | None = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generator function that yields SSE formatted data
|
||||
"""
|
||||
|
||||
try:
|
||||
# Manually track with PostHog since Inkeep doesn't have native support
|
||||
posthoganalytics.capture(
|
||||
distinct_id=distinct_id,
|
||||
event="$ai_generation",
|
||||
properties={
|
||||
**(properties or {}),
|
||||
"ai_product": "playground",
|
||||
"provider": "inkeep",
|
||||
"model": self.model_id,
|
||||
"trace_id": trace_id or str(uuid.uuid4()),
|
||||
},
|
||||
groups=groups,
|
||||
)
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model_id,
|
||||
"stream": True,
|
||||
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
from collections.abc import Generator
|
||||
from django.conf import settings
|
||||
import openai
|
||||
import posthoganalytics
|
||||
from posthoganalytics.ai.openai import OpenAI
|
||||
from anthropic.types import MessageParam
|
||||
from openai.types import ReasoningEffort, CompletionUsage
|
||||
from openai.types.chat import (
|
||||
@@ -9,9 +10,10 @@ from openai.types.chat import (
|
||||
ChatCompletionSystemMessageParam,
|
||||
)
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from products.editor.backend.providers.formatters.openai_formatter import convert_to_openai_messages
|
||||
from products.llm_observability.providers.formatters.openai_formatter import convert_to_openai_messages
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,7 +49,11 @@ class OpenAIConfig:
|
||||
|
||||
class OpenAIProvider:
|
||||
def __init__(self, model_id: str):
|
||||
self.client = openai.OpenAI(api_key=self.get_api_key())
|
||||
posthog_client = posthoganalytics.default_client
|
||||
if not posthog_client:
|
||||
raise ValueError("PostHog client not found")
|
||||
|
||||
self.client = OpenAI(api_key=self.get_api_key(), posthog_client=posthog_client)
|
||||
self.validate_model(model_id)
|
||||
self.model_id = model_id
|
||||
|
||||
@@ -82,6 +88,10 @@ class OpenAIProvider:
|
||||
thinking: bool = False,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
distinct_id: str = "",
|
||||
trace_id: str | None = None,
|
||||
properties: dict | None = None,
|
||||
groups: dict | None = None,
|
||||
) -> Generator[str, None]:
|
||||
"""
|
||||
Async generator function that yields SSE formatted data
|
||||
@@ -101,6 +111,10 @@ class OpenAIProvider:
|
||||
"stream": True,
|
||||
"stream_options": {"include_usage": True},
|
||||
"temperature": effective_temperature,
|
||||
"posthog_distinct_id": distinct_id,
|
||||
"posthog_trace_id": trace_id or str(uuid.uuid4()),
|
||||
"posthog_properties": {**(properties or {}), "ai_product": "playground"},
|
||||
"posthog_groups": groups or {},
|
||||
}
|
||||
if max_tokens is not None:
|
||||
common["max_tokens"] = max_tokens
|
||||
@@ -143,11 +157,7 @@ class OpenAIProvider:
|
||||
if chunk.usage:
|
||||
yield from self.yield_usage(chunk.usage)
|
||||
|
||||
except openai.APIError as e:
|
||||
except Exception as e:
|
||||
logger.exception(f"OpenAI API error: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'error': f'OpenAI API error'})}\n\n"
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'error': f'Unexpected error'})}\n\n"
|
||||
return
|
||||
@@ -79,7 +79,7 @@ dependencies = [
|
||||
"pdpyras==5.2.0",
|
||||
"phonenumberslite==8.13.6",
|
||||
"pillow==10.2.0",
|
||||
"posthoganalytics>=3.24.1",
|
||||
"posthoganalytics>=4.2.0",
|
||||
"psutil==6.0.0",
|
||||
"psycopg2-binary==2.9.7",
|
||||
"psycopg[binary]==3.2.4",
|
||||
|
||||
@@ -28,7 +28,6 @@ depends_on = [
|
||||
|
||||
# NOTE: Add new product dependencies here and in settings/web.py PRODUCTS_APPS
|
||||
"products.early_access_features",
|
||||
"products.editor",
|
||||
"products.links",
|
||||
"products.revenue_analytics",
|
||||
"products.user_interviews",
|
||||
@@ -38,10 +37,6 @@ depends_on = [
|
||||
path = "products.early_access_features"
|
||||
depends_on = ["posthog"]
|
||||
|
||||
[[modules]]
|
||||
path = "products.editor"
|
||||
depends_on = ["posthog"]
|
||||
|
||||
[[modules]]
|
||||
path = "products.links"
|
||||
depends_on = ["posthog"]
|
||||
|
||||
18
uv.lock
generated
18
uv.lock
generated
@@ -3129,15 +3129,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b2/7bbf5e3607b04e745548dd8c17863700ca77746ab5e0cfdcac83a74d7afc/mistralai-1.6.0-py3-none-any.whl", hash = "sha256:6a4f4d6b5c9fff361741aa5513cd2917a81be520deeb0d33e963d1c31eae8c19", size = 288701 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monotonic"
|
||||
version = "1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/ca/8e91948b782ddfbd194f323e7e7d9ba12e5877addf04fb2bf8fca38e86ac/monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7", size = 7615 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "9.0.0"
|
||||
@@ -3993,7 +3984,7 @@ requires-dist = [
|
||||
{ name = "pdpyras", specifier = "==5.2.0" },
|
||||
{ name = "phonenumberslite", specifier = "==8.13.6" },
|
||||
{ name = "pillow", specifier = "==10.2.0" },
|
||||
{ name = "posthoganalytics", specifier = ">=3.24.1" },
|
||||
{ name = "posthoganalytics", specifier = ">=4.2.0" },
|
||||
{ name = "psutil", specifier = "==6.0.0" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = "==3.2.4" },
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.7" },
|
||||
@@ -4114,19 +4105,18 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "posthoganalytics"
|
||||
version = "3.24.1"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backoff" },
|
||||
{ name = "distro" },
|
||||
{ name = "monotonic" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/b7/f6617e31ae91929e71712b63e0d47a1703f8c15b8181e1ae85df85a00ff8/posthoganalytics-3.24.1.tar.gz", hash = "sha256:38aecaee8d2b9f63fa1d19ab22d35eb3128ee525d3800e464cadf2f3495e5666", size = 72226 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/05/90dd1ab541498c72864b0395f1b0b3dd3970c3a6cba93b9a540903ceb1f0/posthoganalytics-4.2.0.tar.gz", hash = "sha256:8638da531a3b232bf935b84e138353c643a5cd68a70a078f56f004e7a765956a", size = 80332 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/3a/a77494779f1e229922584bd3ed944e0b5ef00829720448d90da689f337c0/posthoganalytics-3.24.1-py2.py3-none-any.whl", hash = "sha256:e93b6f34e787f2e3cddce1a1913abc4be167b978c07385491f1dbfc3d9aeb392", size = 86501 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/91a2a96e803a56aabecdae7d572fb0a31dba61288b1520b950e6e9e3977c/posthoganalytics-4.2.0-py2.py3-none-any.whl", hash = "sha256:d77c3fc7ea43e637f8add48bfb8f297ad53b1c201716549a6306c0bd591f0852", size = 97514 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user