diff --git a/.github/workflows/rust-docker-build.yml b/.github/workflows/rust-docker-build.yml index 3a4104b02ff2a..a677ed818f3a5 100644 --- a/.github/workflows/rust-docker-build.yml +++ b/.github/workflows/rust-docker-build.yml @@ -95,8 +95,8 @@ jobs: - name: Container image digest id: digest run: | - echo ${{ steps.docker_build.outputs.digest }} - echo "${{matrix.image}}_digest=${{ steps.docker_build.outputs.digest }}" >> $GITHUB_OUTPUT + echo ${{ steps.docker_build.outputs.digest }} + echo "${{matrix.image}}_digest=${{ steps.docker_build.outputs.digest }}" >> $GITHUB_OUTPUT deploy: name: Deploy capture-replay diff --git a/ee/hogai/__init__.py b/ee/hogai/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/ee/hogai/generate_trends_agent.py b/ee/hogai/generate_trends_agent.py new file mode 100644 index 0000000000000..9980ff82dbeba --- /dev/null +++ b/ee/hogai/generate_trends_agent.py @@ -0,0 +1,55 @@ +from typing import Literal, Optional + +from langchain_core.output_parsers.openai_tools import PydanticToolsParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI +from pydantic import BaseModel, Field + +from ee.hogai.system_prompt import trends_system_prompt +from ee.hogai.team_prompt import TeamPrompt +from ee.hogai.trends_function import TrendsFunction +from posthog.models.team.team import Team +from posthog.schema import ExperimentalAITrendsQuery + + +class output_insight_schema(BaseModel): + reasoning_steps: Optional[list[str]] = None + answer: ExperimentalAITrendsQuery + + +class ChatMessage(BaseModel): + role: Literal["user", "assistant"] + content: str = Field(..., max_length=2500) + + +class Conversation(BaseModel): + messages: list[ChatMessage] = Field(..., max_length=20) + session_id: str + + +class GenerateTrendsAgent: + _team: Team + + def __init__(self, team: Team): + self._team = team + + def bootstrap(self, messages: list[ChatMessage], user_prompt: str | None = None): + llm = ChatOpenAI(model="gpt-4o-2024-08-06", stream_usage=True).bind_tools( + [TrendsFunction().generate_function()], tool_choice="output_insight_schema" + ) + user_prompt = ( + user_prompt + or "Answer to my question:\n{{question}}\n" + TeamPrompt(self._team).generate_prompt() + ) + + prompts = ChatPromptTemplate.from_messages( + [ + ("system", trends_system_prompt), + ("user", user_prompt), + *[(message.role, message.content) for message in messages[1:]], + ], + template_format="mustache", + ) + + chain = prompts | llm | PydanticToolsParser(tools=[output_insight_schema]) # type: ignore + return chain diff --git a/ee/hogai/hardcoded_definitions.py b/ee/hogai/hardcoded_definitions.py new file mode 100644 index 0000000000000..ee13c49c3ca63 --- /dev/null +++ b/ee/hogai/hardcoded_definitions.py @@ -0,0 +1,1027 @@ +hardcoded_prop_defs: dict = { + "events": { + "": { + "label": "All events", + "description": "This is a wildcard that matches all events.", + }, + "$pageview": { + "label": "Pageview", + "description": "When a user loads (or reloads) a page.", + }, + "$pageleave": { + "label": "Pageleave", + "description": "When a user leaves a page.", + }, + "$autocapture": { + "label": "Autocapture", + "description": "User interactions that were automatically captured.", + "examples": ["clicked button"], + }, + "$copy_autocapture": { + "label": "Clipboard autocapture", + "description": "Selected text automatically captured when a user copies or cuts.", + }, + "$screen": { + "label": "Screen", + "description": "When a user loads a screen in a mobile app.", + }, + "$set": { + "label": "Set", + "description": "Setting person properties.", + }, + "$opt_in": { + "label": "Opt In", + "description": "When a user opts into analytics.", + }, + "$feature_flag_called": { + "label": "Feature Flag Called", + "description": ( + 'The feature flag that was called.\n\nWarning! This only works in combination with the $feature_flag event. If you want to filter other events, try "Active Feature Flags".' + ), + "examples": ["beta-feature"], + }, + "$feature_view": { + "label": "Feature View", + "description": "When a user views a feature.", + }, + "$feature_interaction": { + "label": "Feature Interaction", + "description": "When a user interacts with a feature.", + }, + "$capture_metrics": { + "label": "Capture Metrics", + "description": "Metrics captured with values pertaining to your systems at a specific point in time", + }, + "$identify": { + "label": "Identify", + "description": "A user has been identified with properties", + }, + "$create_alias": { + "label": "Alias", + "description": "An alias ID has been added to a user", + }, + "$merge_dangerously": { + "label": "Merge", + "description": "An alias ID has been added to a user", + }, + "$groupidentify": { + "label": "Group Identify", + "description": "A group has been identified with properties", + }, + "$rageclick": { + "label": "Rageclick", + "description": "A user has rapidly and repeatedly clicked in a single place", + }, + "$exception": { + "label": "Exception", + "description": "Automatically captured exceptions from the client Sentry integration", + }, + "$web_vitals": { + "label": "Web vitals", + "description": "Automatically captured web vitals data", + }, + "Application Opened": { + "label": "Application Opened", + "description": "When a user opens the app either for the first time or from the foreground.", + }, + "Application Backgrounded": { + "label": "Application Backgrounded", + "description": "When a user puts the app in the background.", + }, + "Application Updated": { + "label": "Application Updated", + "description": "When a user upgrades the app.", + }, + "Application Installed": { + "label": "Application Installed", + "description": "When a user installs the app.", + }, + "Application Became Active": { + "label": "Application Became Active", + "description": "When a user puts the app in the foreground.", + }, + "Deep Link Opened": { + "label": "Deep Link Opened", + "description": "When a user opens the app via a deep link.", + }, + }, + "elements": { + "tag_name": { + "label": "Tag Name", + "description": "HTML tag name of the element which you want to filter.", + "examples": ["a", "button", "input"], + }, + "selector": { + "label": "CSS Selector", + "description": "Select any element by CSS selector.", + "examples": ["div > a", "table td:nth-child(2)", ".my-class"], + }, + "text": { + "label": "Text", + "description": "Filter on the inner text of the HTML element.", + }, + "href": { + "label": "Target (href)", + "description": "Filter on the href attribute of the element.", + "examples": ["https://posthog.com/about"], + }, + }, + "metadata": { + "distinct_id": { + "label": "Distinct ID", + "description": "The current distinct ID of the user", + "examples": ["16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767"], + }, + }, + "event_properties": { + "distinct_id": {}, + "$session_duration": {}, + "$copy_type": { + "label": "Copy Type", + "description": "Type of copy event.", + "examples": ["copy", "cut"], + }, + "$selected_content": { + "label": "Copied content", + "description": "The content that was selected when the user copied or cut.", + }, + "$set": { + "label": "Set", + "description": "Person properties to be set", + }, + "$set_once": { + "label": "Set Once", + "description": "Person properties to be set if not set already (i.e. first-touch)", + }, + "$pageview_id": { + "label": "Pageview ID", + "description": "PostHog's internal ID for matching events to a pageview.", + "system": True, + }, + "$autocapture_disabled_server_side": { + "label": "Autocapture Disabled Server-Side", + "description": "If autocapture has been disabled server-side.", + "system": True, + }, + "$console_log_recording_enabled_server_side": { + "label": "Console Log Recording Enabled Server-Side", + "description": "If console log recording has been enabled server-side.", + "system": True, + }, + "$session_recording_recorder_version_server_side": { + "label": "Session Recording Recorder Version Server-Side", + "description": "The version of the session recording recorder that is enabled server-side.", + "examples": ["v2"], + "system": True, + }, + "$feature_flag_payloads": { + "label": "Feature Flag Payloads", + "description": "Feature flag payloads active in the environment.", + }, + "$capture_failed_request": { + "label": "Capture Failed Request", + "description": "", + }, + "$sentry_exception": { + "label": "Sentry exception", + "description": "Raw Sentry exception data", + "system": True, + }, + "$sentry_exception_message": { + "label": "Sentry exception message", + }, + "$sentry_exception_type": { + "label": "Sentry exception type", + "description": "Class name of the exception object", + }, + "$sentry_tags": { + "label": "Sentry tags", + "description": "Tags sent to Sentry along with the exception", + }, + "$exception_type": { + "label": "Exception type", + "description": 'Exception categorized into types. E.g. "Error"', + }, + "$exception_message": { + "label": "Exception Message", + "description": "The message detected on the error.", + }, + "$exception_source": { + "label": "Exception source", + "description": "The source of the exception. E.g. JS file.", + }, + "$exception_lineno": { + "label": "Exception source line number", + "description": "Which line in the exception source that caused the exception.", + }, + "$exception_colno": { + "label": "Exception source column number", + "description": "Which column of the line in the exception source that caused the exception.", + }, + "$exception_DOMException_code": { + "label": "DOMException code", + "description": "If a DOMException was thrown, it also has a DOMException code.", + }, + "$exception_is_synthetic": { + "label": "Exception is synthetic", + "description": "Whether this was detected as a synthetic exception", + }, + "$exception_stack_trace_raw": { + "label": "Exception raw stack trace", + "description": "The exception's stack trace, as a string.", + }, + "$exception_handled": { + "label": "Exception was handled", + "description": "Whether this was a handled or unhandled exception", + }, + "$exception_personURL": { + "label": "Exception person URL", + "description": "The PostHog person that experienced the exception", + }, + "$ce_version": { + "label": "$ce_version", + "description": "", + "system": True, + }, + "$anon_distinct_id": { + "label": "Anon Distinct ID", + "description": "If the user was previously anonymous, their anonymous ID will be set here.", + "examples": ["16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767"], + "system": True, + }, + "$event_type": { + "label": "Event Type", + "description": "When the event is an $autocapture event, this specifies what the action was against the element.", + "examples": ["click", "submit", "change"], + }, + "$insert_id": { + "label": "Insert ID", + "description": "Unique insert ID for the event.", + "system": True, + }, + "$time": { + "label": "$time (deprecated)", + "description": "Use the HogQL field `timestamp` instead. This field was previously set on some client side events.", + "system": True, + "examples": ["1681211521.345"], + }, + "$device_id": { + "label": "Device ID", + "description": "Unique ID for that device, consistent even if users are logging in/out.", + "examples": ["16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767"], + "system": True, + }, + "$geoip_city_name": { + "label": "City Name", + "description": "Name of the city matched to this event's IP address.", + "examples": ["Sydney", "Chennai", "Brooklyn"], + }, + "$geoip_country_name": { + "label": "Country Name", + "description": "Name of the country matched to this event's IP address.", + "examples": ["Australia", "India", "United States"], + }, + "$geoip_country_code": { + "label": "Country Code", + "description": "Code of the country matched to this event's IP address.", + "examples": ["AU", "IN", "US"], + }, + "$geoip_continent_name": { + "label": "Continent Name", + "description": "Name of the continent matched to this event's IP address.", + "examples": ["Oceania", "Asia", "North America"], + }, + "$geoip_continent_code": { + "label": "Continent Code", + "description": "Code of the continent matched to this event's IP address.", + "examples": ["OC", "AS", "NA"], + }, + "$geoip_postal_code": { + "label": "Postal Code", + "description": "Approximated postal code matched to this event's IP address.", + "examples": ["2000", "600004", "11211"], + }, + "$geoip_latitude": { + "label": "Latitude", + "description": "Approximated latitude matched to this event's IP address.", + "examples": ["-33.8591", "13.1337", "40.7"], + }, + "$geoip_longitude": { + "label": "Longitude", + "description": "Approximated longitude matched to this event's IP address.", + "examples": ["151.2", "80.8008", "-73.9"], + }, + "$geoip_time_zone": { + "label": "Timezone", + "description": "Timezone matched to this event's IP address.", + "examples": ["Australia/Sydney", "Asia/Kolkata", "America/New_York"], + }, + "$geoip_subdivision_1_name": { + "label": "Subdivision 1 Name", + "description": "Name of the subdivision matched to this event's IP address.", + "examples": ["New South Wales", "Tamil Nadu", "New York"], + }, + "$geoip_subdivision_1_code": { + "label": "Subdivision 1 Code", + "description": "Code of the subdivision matched to this event's IP address.", + "examples": ["NSW", "TN", "NY"], + }, + "$geoip_subdivision_2_name": { + "label": "Subdivision 2 Name", + "description": "Name of the second subdivision matched to this event's IP address.", + }, + "$geoip_subdivision_2_code": { + "label": "Subdivision 2 Code", + "description": "Code of the second subdivision matched to this event's IP address.", + }, + "$geoip_subdivision_3_name": { + "label": "Subdivision 3 Name", + "description": "Name of the third subdivision matched to this event's IP address.", + }, + "$geoip_subdivision_3_code": { + "label": "Subdivision 3 Code", + "description": "Code of the third subdivision matched to this event's IP address.", + }, + "$geoip_disable": { + "label": "GeoIP Disabled", + "description": "Whether to skip GeoIP processing for the event.", + }, + "$el_text": { + "label": "Element Text", + "description": "The text of the element that was clicked. Only sent with Autocapture events.", + "examples": ["Click here!"], + }, + "$app_build": { + "label": "App Build", + "description": "The build number for the app.", + }, + "$app_name": { + "label": "App Name", + "description": "The name of the app.", + }, + "$app_namespace": { + "label": "App Namespace", + "description": "The namespace of the app as identified in the app store.", + "examples": ["com.posthog.app"], + }, + "$app_version": { + "label": "App Version", + "description": "The version of the app.", + }, + "$device_manufacturer": { + "label": "Device Manufacturer", + "description": "The manufacturer of the device", + "examples": ["Apple", "Samsung"], + }, + "$device_name": { + "label": "Device Name", + "description": "Name of the device", + "examples": ["iPhone 12 Pro", "Samsung Galaxy 10"], + }, + "$locale": { + "label": "Locale", + "description": "The locale of the device", + "examples": ["en-US", "de-DE"], + }, + "$os_name": { + "label": "OS Name", + "description": "The Operating System name", + "examples": ["iOS", "Android"], + }, + "$os_version": { + "label": "OS Version", + "description": "The Operating System version.", + "examples": ["15.5"], + }, + "$timezone": { + "label": "Timezone", + "description": "The timezone as reported by the device", + }, + "$touch_x": { + "label": "Touch X", + "description": "The location of a Touch event on the X axis", + }, + "$touch_y": { + "label": "Touch Y", + "description": "The location of a Touch event on the Y axis", + }, + "$plugins_succeeded": { + "label": "Plugins Succeeded", + "description": "Plugins that successfully processed the event, e.g. edited properties (plugin method processEvent).", + }, + "$groups": { + "label": "Groups", + "description": "Relevant groups", + }, + "$group_0": { + "label": "Group 1", + "system": True, + }, + "$group_1": { + "label": "Group 2", + "system": True, + }, + "$group_2": { + "label": "Group 3", + "system": True, + }, + "$group_3": { + "label": "Group 4", + "system": True, + }, + "$group_4": { + "label": "Group 5", + "system": True, + }, + "$group_set": { + "label": "Group Set", + "description": "Group properties to be set", + }, + "$group_key": { + "label": "Group Key", + "description": "Specified group key", + }, + "$group_type": { + "label": "Group Type", + "description": "Specified group type", + }, + "$window_id": { + "label": "Window ID", + "description": "Unique window ID for session recording disambiguation", + "system": True, + }, + "$session_id": { + "label": "Session ID", + "description": "Unique session ID for session recording disambiguation", + "system": True, + }, + "$plugins_failed": { + "label": "Plugins Failed", + "description": "Plugins that failed to process the event (plugin method processEvent).", + }, + "$plugins_deferred": { + "label": "Plugins Deferred", + "description": "Plugins to which the event was handed off post-ingestion, e.g. for export (plugin method onEvent).", + }, + "$$plugin_metrics": { + "label": "Plugin Metric", + "description": "Performance metrics for a given plugin.", + }, + "$creator_event_uuid": { + "label": "Creator Event ID", + "description": "Unique ID for the event, which created this person.", + "examples": ["16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767"], + }, + "utm_source": { + "label": "UTM Source", + "description": "UTM source tag.", + "examples": ["Google", "Bing", "Twitter", "Facebook"], + }, + "$initial_utm_source": { + "label": "Initial UTM Source", + "description": "UTM source tag.", + "examples": ["Google", "Bing", "Twitter", "Facebook"], + }, + "utm_medium": { + "label": "UTM Medium", + "description": "UTM medium tag.", + "examples": ["Social", "Organic", "Paid", "Email"], + }, + "utm_campaign": { + "label": "UTM Campaign", + "description": "UTM campaign tag.", + "examples": ["feature launch", "discount"], + }, + "utm_name": { + "label": "UTM Name", + "description": "UTM campaign tag, sent via Segment.", + "examples": ["feature launch", "discount"], + }, + "utm_content": { + "label": "UTM Content", + "description": "UTM content tag.", + "examples": ["bottom link", "second button"], + }, + "utm_term": { + "label": "UTM Term", + "description": "UTM term tag.", + "examples": ["free goodies"], + }, + "$performance_page_loaded": { + "label": "Page Loaded", + "description": "The time taken until the browser's page load event in milliseconds.", + }, + "$performance_raw": { + "label": "Browser Performance", + "description": "The browser performance entries for navigation (the page), paint, and resources. That were available when the page view event fired", + "system": True, + }, + "$had_persisted_distinct_id": { + "label": "$had_persisted_distinct_id", + "description": "", + "system": True, + }, + "$sentry_event_id": { + "label": "Sentry Event ID", + "description": "This is the Sentry key for an event.", + "examples": ["byroc2ar9ee4ijqp"], + "system": True, + }, + "$timestamp": { + "label": "Timestamp", + "description": "Time the event happened.", + "examples": ["2023-05-20T15:30:00Z"], + }, + "$sent_at": { + "label": "Sent At", + "description": "Time the event was sent to PostHog. Used for correcting the event timestamp when the device clock is off.", + "examples": ["2023-05-20T15:31:00Z"], + }, + "$browser": { + "label": "Browser", + "description": "Name of the browser the user has used.", + "examples": ["Chrome", "Firefox"], + }, + "$os": { + "label": "OS", + "description": "The operating system of the user.", + "examples": ["Windows", "Mac OS X"], + }, + "$browser_language": { + "label": "Browser Language", + "description": "Language.", + "examples": ["en", "en-US", "cn", "pl-PL"], + }, + "$current_url": { + "label": "Current URL", + "description": "The URL visited at the time of the event.", + "examples": ["https://example.com/interesting-article?parameter=true"], + }, + "$browser_version": { + "label": "Browser Version", + "description": "The version of the browser that was used. Used in combination with Browser.", + "examples": ["70", "79"], + }, + "$raw_user_agent": { + "label": "Raw User Agent", + "description": "PostHog process information like browser, OS, and device type from the user agent string. This is the raw user agent string.", + "examples": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)"], + }, + "$user_agent": { + "label": "Raw User Agent", + "description": "Some SDKs (like Android) send the raw user agent as $user_agent.", + "examples": ["Dalvik/2.1.0 (Linux; U; Android 11; Pixel 3 Build/RQ2A.210505.002)"], + }, + "$screen_height": { + "label": "Screen Height", + "description": "The height of the user's entire screen (in pixels).", + "examples": ["2160", "1050"], + }, + "$screen_width": { + "label": "Screen Width", + "description": "The width of the user's entire screen (in pixels).", + "examples": ["1440", "1920"], + }, + "$screen_name": { + "label": "Screen Name", + "description": "The name of the active screen.", + }, + "$viewport_height": { + "label": "Viewport Height", + "description": "The height of the user's actual browser window (in pixels).", + "examples": ["2094", "1031"], + }, + "$viewport_width": { + "label": "Viewport Width", + "description": "The width of the user's actual browser window (in pixels).", + "examples": ["1439", "1915"], + }, + "$lib": { + "label": "Library", + "description": "What library was used to send the event.", + "examples": ["web", "posthog-ios"], + }, + "$lib_custom_api_host": { + "label": "Library Custom API Host", + "description": "The custom API host used to send the event.", + "examples": ["https://ph.example.com"], + }, + "$lib_version": { + "label": "Library Version", + "description": "Version of the library used to send the event. Used in combination with Library.", + "examples": ["1.0.3"], + }, + "$lib_version__major": { + "label": "Library Version (Major)", + "description": "Major version of the library used to send the event.", + "examples": [1], + }, + "$lib_version__minor": { + "label": "Library Version (Minor)", + "description": "Minor version of the library used to send the event.", + "examples": [0], + }, + "$lib_version__patch": { + "label": "Library Version (Patch)", + "description": "Patch version of the library used to send the event.", + "examples": [3], + }, + "$referrer": { + "label": "Referrer URL", + "description": "URL of where the user came from.", + "examples": ["https://google.com/search?q=posthog&rlz=1C..."], + }, + "$referring_domain": { + "label": "Referring Domain", + "description": "Domain of where the user came from.", + "examples": ["google.com", "facebook.com"], + }, + "$user_id": { + "label": "User ID", + "description": "This variable will be set to the distinct ID if you've called posthog.identify('distinct id'). If the user is anonymous, it'll be empty.", + }, + "$ip": { + "label": "IP Address", + "description": "IP address for this user when the event was sent.", + "examples": ["203.0.113.0"], + }, + "$host": { + "label": "Host", + "description": "The hostname of the Current URL.", + "examples": ["example.com", "localhost:8000"], + }, + "$pathname": { + "label": "Path Name", + "description": "The path of the Current URL, which means everything in the url after the domain.", + "examples": ["/pricing", "/about-us/team"], + }, + "$search_engine": { + "label": "Search Engine", + "description": "The search engine the user came in from (if any).", + "examples": ["Google", "DuckDuckGo"], + }, + "$active_feature_flags": { + "label": "Active Feature Flags", + "description": "Keys of the feature flags that were active while this event was sent.", + "examples": ["['beta-feature']"], + }, + "$enabled_feature_flags": { + "label": "Enabled Feature Flags", + "description": "Keys and multivariate values of the feature flags that were active while this event was sent.", + "examples": ['{"flag": "value"}'], + }, + "$feature_flag_response": { + "label": "Feature Flag Response", + "description": "What the call to feature flag responded with.", + "examples": ["true", "false"], + }, + "$feature_flag": { + "label": "Feature Flag", + "description": 'The feature flag that was called.\n\nWarning! This only works in combination with the $feature_flag_called event. If you want to filter other events, try "Active Feature Flags".', + "examples": ["beta-feature"], + }, + "$survey_response": { + "label": "Survey Response", + "description": "The response value for the first question in the survey.", + "examples": ["I love it!", 5, "['choice 1', 'choice 3']"], + }, + "$survey_name": { + "label": "Survey Name", + "description": "The name of the survey.", + "examples": ["Product Feedback for New Product", "Home page NPS"], + }, + "$survey_questions": { + "label": "Survey Questions", + "description": "The questions asked in the survey.", + }, + "$survey_id": { + "label": "Survey ID", + "description": "The unique identifier for the survey.", + }, + "$survey_iteration": { + "label": "Survey Iteration Number", + "description": "The iteration number for the survey.", + }, + "$survey_iteration_start_date": { + "label": "Survey Iteration Start Date", + "description": "The start date for the current iteration of the survey.", + }, + "$device": { + "label": "Device", + "description": "The mobile device that was used.", + "examples": ["iPad", "iPhone", "Android"], + }, + "$sentry_url": { + "label": "Sentry URL", + "description": "Direct link to the exception in Sentry", + "examples": ["https://sentry.io/..."], + }, + "$device_type": { + "label": "Device Type", + "description": "The type of device that was used.", + "examples": ["Mobile", "Tablet", "Desktop"], + }, + "$screen_density": { + "label": "Screen density", + "description": 'The logical density of the display. This is a scaling factor for the Density Independent Pixel unit, where one DIP is one pixel on an approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen), providing the baseline of the system\'s display. Thus on a 160dpi screen this density value will be 1; on a 120 dpi screen it would be .75; etc.', + "examples": [2.75], + }, + "$device_model": { + "label": "Device Model", + "description": "The model of the device that was used.", + "examples": ["iPhone9,3", "SM-G965W"], + }, + "$network_wifi": { + "label": "Network WiFi", + "description": "Whether the user was on WiFi when the event was sent.", + "examples": ["true", "false"], + }, + "$network_bluetooth": { + "label": "Network Bluetooth", + "description": "Whether the user was on Bluetooth when the event was sent.", + "examples": ["true", "false"], + }, + "$network_cellular": { + "label": "Network Cellular", + "description": "Whether the user was on cellular when the event was sent.", + "examples": ["true", "false"], + }, + "$client_session_initial_referring_host": { + "label": "Referrer Host", + "description": "Host that the user came from. (First-touch, session-scoped)", + "examples": ["google.com", "facebook.com"], + }, + "$client_session_initial_pathname": { + "label": "Initial Path", + "description": "Path that the user started their session on. (First-touch, session-scoped)", + "examples": ["/register", "/some/landing/page"], + }, + "$client_session_initial_utm_source": { + "label": "Initial UTM Source", + "description": "UTM Source. (First-touch, session-scoped)", + "examples": ["Google", "Bing", "Twitter", "Facebook"], + }, + "$client_session_initial_utm_campaign": { + "label": "Initial UTM Campaign", + "description": "UTM Campaign. (First-touch, session-scoped)", + "examples": ["feature launch", "discount"], + }, + "$client_session_initial_utm_medium": { + "label": "Initial UTM Medium", + "description": "UTM Medium. (First-touch, session-scoped)", + "examples": ["Social", "Organic", "Paid", "Email"], + }, + "$client_session_initial_utm_content": { + "label": "Initial UTM Source", + "description": "UTM Source. (First-touch, session-scoped)", + "examples": ["bottom link", "second button"], + }, + "$client_session_initial_utm_term": { + "label": "Initial UTM Source", + "description": "UTM Source. (First-touch, session-scoped)", + "examples": ["free goodies"], + }, + "$network_carrier": { + "label": "Network Carrier", + "description": "The network carrier that the user is on.", + "examples": ["cricket", "telecom"], + }, + "from_background": { + "label": "From Background", + "description": "Whether the app was opened for the first time or from the background.", + "examples": ["true", "false"], + }, + "url": { + "label": "URL", + "description": "The deep link URL that the app was opened from.", + "examples": ["https://open.my.app"], + }, + "referring_application": { + "label": "Referrer Application", + "description": "The namespace of the app that made the request.", + "examples": ["com.posthog.app"], + }, + "version": { + "label": "App Version", + "description": "The version of the app", + "examples": ["1.0.0"], + }, + "previous_version": { + "label": "App Previous Version", + "description": "The previous version of the app", + "examples": ["1.0.0"], + }, + "build": { + "label": "App Build", + "description": "The build number for the app", + "examples": ["1"], + }, + "previous_build": { + "label": "App Previous Build", + "description": "The previous build number for the app", + "examples": ["1"], + }, + "gclid": { + "label": "gclid", + "description": "Google Click ID", + }, + "rdt_cid": { + "label": "rdt_cid", + "description": "Reddit Click ID", + }, + "gad_source": { + "label": "gad_source", + "description": "Google Ads Source", + }, + "gclsrc": { + "label": "gclsrc", + "description": "Google Click Source", + }, + "dclid": { + "label": "dclid", + "description": "DoubleClick ID", + }, + "gbraid": { + "label": "gbraid", + "description": "Google Ads, web to app", + }, + "wbraid": { + "label": "wbraid", + "description": "Google Ads, app to web", + }, + "fbclid": { + "label": "fbclid", + "description": "Facebook Click ID", + }, + "msclkid": { + "label": "msclkid", + "description": "Microsoft Click ID", + }, + "twclid": { + "label": "twclid", + "description": "Twitter Click ID", + }, + "li_fat_id": { + "label": "li_fat_id", + "description": "LinkedIn First-Party Ad Tracking ID", + }, + "mc_cid": { + "label": "mc_cid", + "description": "Mailchimp Campaign ID", + }, + "igshid": { + "label": "igshid", + "description": "Instagram Share ID", + }, + "ttclid": { + "label": "ttclid", + "description": "TikTok Click ID", + }, + "$is_identified": { + "label": "Is Identified", + "description": "When the person was identified", + }, + "$web_vitals_enabled_server_side": { + "label": "Web vitals enabled server side", + "description": "Whether web vitals was enabled in remote config", + }, + "$web_vitals_FCP_event": { + "label": "Web vitals FCP measure event details", + }, + "$web_vitals_FCP_value": { + "label": "Web vitals FCP value", + }, + "$web_vitals_LCP_event": { + "label": "Web vitals LCP measure event details", + }, + "$web_vitals_LCP_value": { + "label": "Web vitals LCP value", + }, + "$web_vitals_INP_event": { + "label": "Web vitals INP measure event details", + }, + "$web_vitals_INP_value": { + "label": "Web vitals INP value", + }, + "$web_vitals_CLS_event": { + "label": "Web vitals CLS measure event details", + }, + "$web_vitals_CLS_value": { + "label": "Web vitals CLS value", + }, + }, + "numerical_event_properties": {}, + "person_properties": {}, + "session_properties": { + "$session_duration": { + "label": "Session duration", + "description": "The duration of the session being tracked. Learn more about how PostHog tracks sessions in our documentation.\n\nNote, if the duration is formatted as a single number (not 'HH:MM:SS'), it's in seconds.", + "examples": ["01:04:12"], + "type": "Numeric", + }, + "$start_timestamp": { + "label": "Start timestamp", + "description": "The timestamp of the first event from this session.", + "examples": ["2023-05-20T15:30:00Z"], + "type": "DateTime", + }, + "$end_timestamp": { + "label": "End timestamp", + "description": "The timestamp of the last event from this session", + "examples": ["2023-05-20T16:30:00Z"], + "type": "DateTime", + }, + "$entry_current_url": { + "label": "Entry URL", + "description": "The first URL visited in this session", + "examples": ["https://example.com/interesting-article?parameter=true"], + "type": "String", + }, + "$entry_pathname": { + "label": "Entry pathname", + "description": "The first pathname visited in this session", + "examples": ["/interesting-article?parameter=true"], + "type": "String", + }, + "$end_current_url": { + "label": "Entry URL", + "description": "The first URL visited in this session", + "examples": ["https://example.com/interesting-article?parameter=true"], + "type": "String", + }, + "$end_pathname": { + "label": "Entry pathname", + "description": "The first pathname visited in this session", + "examples": ["/interesting-article?parameter=true"], + "type": "String", + }, + "$exit_current_url": { + "label": "Exit URL", + "description": "The last URL visited in this session", + "examples": ["https://example.com/interesting-article?parameter=true"], + "type": "String", + }, + "$exit_pathname": { + "label": "Exit pathname", + "description": "The last pathname visited in this session", + "examples": ["/interesting-article?parameter=true"], + "type": "String", + }, + "$pageview_count": { + "label": "Pageview count", + "description": "The number of page view events in this session", + "examples": ["123"], + "type": "Numeric", + }, + "$autocapture_count": { + "label": "Autocapture count", + "description": "The number of autocapture events in this session", + "examples": ["123"], + "type": "Numeric", + }, + "$screen_count": { + "label": "Screen count", + "description": "The number of screen events in this session", + "examples": ["123"], + "type": "Numeric", + }, + "$channel_type": { + "label": "Channel type", + "description": "What type of acquisition channel this traffic came from.", + "examples": ["Paid Search", "Organic Video", "Direct"], + "type": "String", + }, + "$is_bounce": { + "label": "Is bounce", + "description": "Whether the session was a bounce.", + "examples": ["true", "false"], + "type": "Boolean", + }, + }, + "groups": { + "$group_key": { + "label": "Group Key", + "description": "Specified group key", + }, + }, + "replay": { + "snapshot_source": { + "label": "Platform", + "description": "Platform the session was recorded on", + "examples": ["web", "mobile"], + }, + "console_log_level": { + "label": "Log level", + "description": "Level of console logs captured", + "examples": ["info", "warn", "error"], + }, + "console_log_query": { + "label": "Console log", + "description": "Text of console logs captured", + }, + "visited_page": { + "label": "Visited page", + "description": "URL a user visited during their session", + }, + }, +} diff --git a/ee/hogai/system_prompt.py b/ee/hogai/system_prompt.py new file mode 100644 index 0000000000000..fb00b35825867 --- /dev/null +++ b/ee/hogai/system_prompt.py @@ -0,0 +1,77 @@ +trends_system_prompt = """ +As a recognized head of product growth acting as a top-tier data engineer, your task is to write queries of trends insights for customers using a JSON schema. + +Follow these instructions to create a query: +* Identify the events or actions the user wants to analyze. +* Determine types of entities that user wants to analyze like events, persons, groups, sessions, cohorts, etc. +* Determine a vistualization type that best suits the user's needs. +* Determine if the user wants to name the series or use the default names. +* Choose the date range and the interval the user wants to analyze. +* Determine if the user wants to compare the results to a previous period or use smoothing. +* Determine if the user wants to use property filters for all series. +* Determine math types for all series. +* Determine property filters for individual series. +* Check operators of property filters for individual and all series. Make sure the operators correspond to the user's request. You may need to use "contains" for strings if you're not sure about the exact value. +* Determine if the user wants to use a breakdown filter. +* Determine if the user wants to filter out internal and test users. If the user didn't specify, filter out internal and test users by default. +* Determine if the user wants to use sampling factor. +* Determine if it's useful to show a legend, values of series, units, y-axis scale type, etc. +* Use your judgement if there are any other parameters that the user might want to adjust that aren't listed here. + +Trends insights enable users to plot data from people, events, and properties however they want. They're useful for finding patterns in your data, as well as monitoring users' product to ensure everything is running smoothly. For example, using trends, users can analyze: +- How product's most important metrics change over time. +- Long-term patterns, or cycles in product's usage. +- How a specific change affects usage. +- The usage of different features side-by-side. +- How the properties of events vary using aggregation (sum, average, etc). +- Users can also visualize the same data points in a variety of ways. + +For trends queries, use an appropriate ChartDisplayType for the output. For example: +- if the user wants to see a dynamics in time like a line graph, use `ActionsLineGraph`. +- if the user wants to see cumulative dynamics across time, use `ActionsLineGraphCumulative`. +- if the user asks a question where you can answer with a single number, use `BoldNumber`. +- if the user wants a table, use `ActionsTable`. +- if the data is categorical, use `ActionsBar`. +- if the data is easy to understand in a pie chart, use `ActionsPie`. +- if the user has only one series and they want to see data from particular countries, use `WorldMap`. + +The user might want to get insights for groups. A group aggregates events based on entities, such as organizations or sellers. The user might provide a list of group names and their numeric indexes. Instead of a group's name, always use its numeric index. + +Cohorts enable the user to easily create a list of their users who have something in common, such as completing an event or having the same property. The user might want to use cohorts for filtering events. Instead of a cohort's name, always use its ID. + +If you want to apply Y-Axis unit, make sure it will display data correctly. Use the percentage formatting only if the anticipated result is from 0 to 1. + +Learn on these examples: +Q: How many users do I have? +A: {"dateRange":{"date_from":"all"},"interval":"month","kind":"TrendsQuery","series":[{"event":"user signed up","kind":"EventsNode","math":"total"}],"trendsFilter":{"aggregationAxisFormat":"numeric","display":"BoldNumber"}} +Q: Show a bar chart of the organic search traffic for the last month grouped by week. +A: {"dateRange":{"date_from":"-30d","date_to":null,"explicitDate":false},"interval":"week","kind":"TrendsQuery","series":[{"event":"$pageview","kind":"EventsNode","math":"dau","properties":[{"key":"$referring_domain","operator":"icontains","type":"event","value":"google"},{"key":"utm_source","operator":"is_not_set","type":"event","value":"is_not_set"}]}],"trendsFilter":{"aggregationAxisFormat":"numeric","display":"ActionsBar"}} +Q: insight created unique users & first-time users for the last 12m) +A: {"dateRange":{"date_from":"-12m","date_to":""},"filterTestAccounts":true,"interval":"month","kind":"TrendsQuery","series":[{"event":"insight created","kind":"EventsNode","math":"dau","custom_name":"insight created"},{"event":"insight created","kind":"EventsNode","math":"first_time_for_user","custom_name":"insight created"}],"trendsFilter":{"aggregationAxisFormat":"numeric","display":"ActionsLineGraph"}} +Q: What are the top 10 referring domains for the last month? +A: {"breakdownFilter":{"breakdown_type":"event","breakdowns":[{"group_type_index":null,"histogram_bin_count":null,"normalize_url":null,"property":"$referring_domain","type":"event"}]},"dateRange":{"date_from":"-30d"},"interval":"day","kind":"TrendsQuery","series":[{"event":"$pageview","kind":"EventsNode","math":"total","custom_name":"$pageview"}]} +Q: What is the DAU to MAU ratio of users from the US and Australia that viewed a page in the last 7 days? Compare it to the previous period. +A: {"compareFilter":{"compare":true,"compare_to":null},"dateRange":{"date_from":"-7d"},"interval":"day","kind":"TrendsQuery","properties":{"type":"AND","values":[{"type":"AND","values":[{"key":"$geoip_country_name","operator":"exact","type":"event","value":["United States","Australia"]}]}]},"series":[{"event":"$pageview","kind":"EventsNode","math":"dau","custom_name":"$pageview"},{"event":"$pageview","kind":"EventsNode","math":"monthly_active","custom_name":"$pageview"}],"trendsFilter":{"aggregationAxisFormat":"percentage_scaled","display":"ActionsLineGraph","formula":"A/B"}} +Q: I want to understand how old are dashboard results when viewed from the beginning of this year grouped by a month. Display the results for percentiles of 99, 95, 90, average, and median by the property "refreshAge". +A: {"dateRange":{"date_from":"yStart","date_to":null,"explicitDate":false},"filterTestAccounts":true,"interval":"month","kind":"TrendsQuery","series":[{"event":"viewed dashboard","kind":"EventsNode","math":"p99","math_property":"refreshAge","custom_name":"viewed dashboard"},{"event":"viewed dashboard","kind":"EventsNode","math":"p95","math_property":"refreshAge","custom_name":"viewed dashboard"},{"event":"viewed dashboard","kind":"EventsNode","math":"p90","math_property":"refreshAge","custom_name":"viewed dashboard"},{"event":"viewed dashboard","kind":"EventsNode","math":"avg","math_property":"refreshAge","custom_name":"viewed dashboard"},{"event":"viewed dashboard","kind":"EventsNode","math":"median","math_property":"refreshAge","custom_name":"viewed dashboard"}],"trendsFilter":{"aggregationAxisFormat":"duration","display":"ActionsLineGraph"}} +Q: organizations joined in the last 30 days by day from the google search +A: {"dateRange":{"date_from":"-30d"},"filterTestAccounts":false,"interval":"day","kind":"TrendsQuery","properties":{"type":"AND","values":[{"type":"OR","values":[{"key":"$initial_utm_source","operator":"exact","type":"person","value":["google"]}]}]},"series":[{"event":"user signed up","kind":"EventsNode","math":"unique_group","math_group_type_index":0,"name":"user signed up","properties":[{"key":"is_organization_first_user","operator":"exact","type":"person","value":["true"]}]}],"trendsFilter":{"aggregationAxisFormat":"numeric","display":"ActionsLineGraph"}} +Q: trends for the last two weeks of the onboarding completed event by unique projects with a session duration more than 5 minutes and the insight analyzed event by unique projects with a breakdown by event's Country Name. exclude the US. +A: {"kind":"TrendsQuery","series":[{"kind":"EventsNode","event":"onboarding completed","name":"onboarding completed","properties":[{"key":"$session_duration","value":300,"operator":"gt","type":"session"}],"math":"unique_group","math_group_type_index":2},{"kind":"EventsNode","event":"insight analyzed","name":"insight analyzed","math":"unique_group","math_group_type_index":2}],"trendsFilter":{"display":"ActionsBar","showValuesOnSeries":true,"showPercentStackView":false,"showLegend":false},"breakdownFilter":{"breakdowns":[{"property":"$geoip_country_name","type":"event"}],"breakdown_limit":5},"properties":{"type":"AND","values":[{"type":"AND","values":[{"key":"$geoip_country_code","value":["US"],"operator":"is_not","type":"event"}]}]},"dateRange":{"date_from":"-14d","date_to":null},"interval":"day"} + +Obey these rules: +- if the date range is not specified, use the best judgement to select a reasonable date range. If it is a question that can be answered with a single number, you may need to use the longest possible date range. +- Filter internal users by default if the user doesn't specify. +- Only use events and properties defined by the user. You can't create new events or property definitions. + +For your reference, there is a description of the data model. + +The "events" table has the following columns: +* timestamp (DateTime) - date and time of the event. Events are sorted by timestamp in ascending order. +* uuid (UUID) - unique identifier of the event. +* person_id (UUID) - unique identifier of the person who performed the event. +* event (String) - name of the event. +* properties (custom type) - additional properties of the event. Properties can be of multiple types: String, Int, Decimal, Float, and Bool. A property can be an array of thosee types. A property always has only ONE type. If the property starts with a $, it is a system-defined property. If the property doesn't start with a $, it is a user-defined property. There is a list of system-defined properties: $browser, $browser_version, and $os. User-defined properties can have any name. + +Remember, your efforts will be rewarded with a $100 tip if you manage to implement a perfect query that follows user's instructions and return the desired result. Do not hallucinate. +""" diff --git a/ee/hogai/team_prompt.py b/ee/hogai/team_prompt.py new file mode 100644 index 0000000000000..6ab987b992363 --- /dev/null +++ b/ee/hogai/team_prompt.py @@ -0,0 +1,137 @@ +import collections +from datetime import timedelta + +from django.utils import timezone + +from posthog.models.cohort.cohort import Cohort +from posthog.models.event_definition import EventDefinition +from posthog.models.group_type_mapping import GroupTypeMapping +from posthog.models.property_definition import PropertyDefinition +from posthog.models.team.team import Team + +from .hardcoded_definitions import hardcoded_prop_defs + + +class TeamPrompt: + _team: Team + + def __init__(self, team: Team): + super().__init__() + self._team = team + + @classmethod + def get_properties_tag_name(self, property_name: str) -> str: + return f"list of {property_name.lower()} property definitions by a type" + + def _clean_line(self, line: str) -> str: + return line.replace("\n", " ") + + def _get_xml_tag(self, tag_name: str, content: str) -> str: + return f"\n<{tag_name}>\n{content.strip()}\n\n" + + def _generate_cohorts_prompt(self) -> str: + cohorts = Cohort.objects.filter(team=self._team, last_calculation__gte=timezone.now() - timedelta(days=60)) + return self._get_xml_tag( + "list of defined cohorts", + "\n".join([f'name "{cohort.name}", ID {cohort.id}' for cohort in cohorts]), + ) + + def _generate_events_prompt(self) -> str: + event_description_mapping = { + "$identify": "Identifies an anonymous user. This event doesn't show how many users you have but rather how many users used an account." + } + + tags: list[str] = [] + for event in EventDefinition.objects.filter( + team=self._team, last_seen_at__gte=timezone.now() - timedelta(days=60) + ): + event_tag = event.name + if event.name in event_description_mapping: + description = event_description_mapping[event.name] + event_tag += f" - {description}" + elif event.name in hardcoded_prop_defs["events"]: + data = hardcoded_prop_defs["events"][event.name] + event_tag += f" - {data['label']}. {data['description']}" + if "examples" in data: + event_tag += f" Examples: {data['examples']}." + tags.append(self._clean_line(event_tag)) + + tag_name = "list of available events for filtering" + return self._get_xml_tag(tag_name, "\n".join(sorted(tags))) + + def _generate_groups_prompt(self) -> str: + user_groups = GroupTypeMapping.objects.filter(team=self._team).order_by("group_type_index") + return self._get_xml_tag( + "list of defined groups", + "\n".join([f'name "{group.group_type}", index {group.group_type_index}' for group in user_groups]), + ) + + def _join_property_tags(self, tag_name: str, properties_by_type: dict[str, list[str]]) -> str: + if any(prop_by_type for prop_by_type in properties_by_type.values()): + tags = "\n".join( + self._get_xml_tag(prop_type, "\n".join(tags)) for prop_type, tags in properties_by_type.items() + ) + return self._get_xml_tag(tag_name, tags) + "\n" + return "" + + def _get_property_type(self, prop: PropertyDefinition) -> str: + if prop.name.startswith("$feature/"): + return "feature" + return PropertyDefinition.Type(prop.type).label.lower() + + def _generate_properties_prompt(self) -> str: + properties = ( + PropertyDefinition.objects.filter(team=self._team) + .exclude( + name__regex=r"(__|phjs|survey_dismissed|survey_responded|partial_filter_chosen|changed_action|window-id|changed_event|partial_filter)" + ) + .distinct("name") + ).iterator(chunk_size=2500) + + key_mapping = { + "event": "event_properties", + } + + tags: dict[str, dict[str, list[str]]] = collections.defaultdict(lambda: collections.defaultdict(list)) + + for prop in properties: + category = self._get_property_type(prop) + property_type = prop.property_type + + if category in ["group", "session"] or property_type is None: + continue + + prop_tag = prop.name + + if category in key_mapping and prop.name in hardcoded_prop_defs[key_mapping[category]]: + data = hardcoded_prop_defs[key_mapping[category]][prop.name] + if "label" in data: + prop_tag += f" - {data['label']}." + if "description" in data: + prop_tag += f" {data['description']}" + if "examples" in data: + prop_tag += f" Examples: {data['examples']}." + + tags[category][property_type].append(self._clean_line(prop_tag)) + + # Session hardcoded properties + for key, defs in hardcoded_prop_defs["session_properties"].items(): + prop_tag += f"{key} - {defs['label']}. {defs['description']}." + if "examples" in defs: + prop_tag += f" Examples: {defs['examples']}." + tags["session"][defs["type"]].append(self._clean_line(prop_tag)) + + prompt = "\n".join( + [self._join_property_tags(self.get_properties_tag_name(category), tags[category]) for category in tags], + ) + + return prompt + + def generate_prompt(self) -> str: + return "".join( + [ + self._generate_groups_prompt(), + self._generate_events_prompt(), + self._generate_properties_prompt(), + ] + ) diff --git a/ee/hogai/trends_function.py b/ee/hogai/trends_function.py new file mode 100644 index 0000000000000..6f57b47506578 --- /dev/null +++ b/ee/hogai/trends_function.py @@ -0,0 +1,71 @@ +import json +from functools import cached_property +from typing import Any + +from ee.hogai.team_prompt import TeamPrompt +from posthog.models.property_definition import PropertyDefinition +from posthog.schema import ExperimentalAITrendsQuery + + +class TrendsFunction: + def _replace_value_in_dict(self, item: Any, original_schema: Any): + if isinstance(item, list): + return [self._replace_value_in_dict(i, original_schema) for i in item] + elif isinstance(item, dict): + if list(item.keys()) == ["$ref"]: + definitions = item["$ref"][2:].split("/") + res = original_schema.copy() + for definition in definitions: + res = res[definition] + return res + else: + return {key: self._replace_value_in_dict(i, original_schema) for key, i in item.items()} + else: + return item + + @cached_property + def _flat_schema(self): + schema = ExperimentalAITrendsQuery.model_json_schema() + + # Patch `numeric` types + schema["$defs"]["MathGroupTypeIndex"]["type"] = "number" + + # Clean up the property filters + for key, title in ( + ("EventPropertyFilter", PropertyDefinition.Type.EVENT.label), + ("PersonPropertyFilter", PropertyDefinition.Type.PERSON.label), + ("SessionPropertyFilter", PropertyDefinition.Type.SESSION.label), + ("FeaturePropertyFilter", "feature"), + ("CohortPropertyFilter", "cohort"), + ): + property_schema = schema["$defs"][key] + property_schema["properties"]["key"]["description"] = ( + f"Use one of the properties the user has provided in the <{TeamPrompt.get_properties_tag_name(title)}> tag." + ) + + for _ in range(100): + if "$ref" not in json.dumps(schema): + break + schema = self._replace_value_in_dict(schema.copy(), schema.copy()) + del schema["$defs"] + return schema + + def generate_function(self): + return { + "type": "function", + "function": { + "name": "output_insight_schema", + "description": "Outputs the JSON schema of a product analytics insight", + "parameters": { + "type": "object", + "properties": { + "reasoning_steps": { + "type": "array", + "items": {"type": "string"}, + "description": "The reasoning steps leading to the final conclusion that will be shown to the user. Use 'you' if you want to refer to the user.", + }, + "answer": self._flat_schema, + }, + }, + }, + } diff --git a/frontend/__snapshots__/components-command-bar--actions--dark.png b/frontend/__snapshots__/components-command-bar--actions--dark.png index 44ab7e3741e09..62965c348e773 100644 Binary files a/frontend/__snapshots__/components-command-bar--actions--dark.png and b/frontend/__snapshots__/components-command-bar--actions--dark.png differ diff --git a/frontend/__snapshots__/components-command-bar--actions--light.png b/frontend/__snapshots__/components-command-bar--actions--light.png index de75dfff4c388..4857817df6f2e 100644 Binary files a/frontend/__snapshots__/components-command-bar--actions--light.png and b/frontend/__snapshots__/components-command-bar--actions--light.png differ diff --git a/frontend/__snapshots__/components-command-bar--search--dark.png b/frontend/__snapshots__/components-command-bar--search--dark.png index 83c14db3090ab..599e3c20f7aea 100644 Binary files a/frontend/__snapshots__/components-command-bar--search--dark.png and b/frontend/__snapshots__/components-command-bar--search--dark.png differ diff --git a/frontend/__snapshots__/components-command-bar--search--light.png b/frontend/__snapshots__/components-command-bar--search--light.png index beb9e54a072c0..75bd57cddaff1 100644 Binary files a/frontend/__snapshots__/components-command-bar--search--light.png and b/frontend/__snapshots__/components-command-bar--search--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png index 105b30153eae0..95204eb281d62 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-user--light.png b/frontend/__snapshots__/scenes-other-settings--settings-user--light.png index 0a0cb2ed46c66..3c87296382dea 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-user--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-user--light.png differ diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index b1eddf98ad684..b21c10bede17a 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -15,6 +15,7 @@ import { IconRewindPlay, IconRocket, IconServer, + IconSparkles, IconTestTube, IconToggle, IconWarning, @@ -420,6 +421,15 @@ export const navigation3000Logic = kea([ }, ] + if (featureFlags[FEATURE_FLAGS.ARTIFICIAL_HOG]) { + sectionOne.splice(1, 0, { + identifier: Scene.Max, + label: 'Max AI', + icon: , + to: urls.max(), + }) + } + return [ sectionOne, [ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 175698ad85794..b49144f9ada51 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -766,6 +766,11 @@ class ApiRequest { return apiRequest } + // Chat + public chat(teamId?: TeamType['id']): ApiRequest { + return this.projectsDetail(teamId).addPathComponent('query').addPathComponent('chat') + } + // Notebooks public notebooks(teamId?: TeamType['id']): ApiRequest { return this.projectsDetail(teamId).addPathComponent('notebooks') @@ -2309,6 +2314,14 @@ const api = { .create({ ...options, data: { query, client_query_id: queryId, refresh: refreshParam } }) }, + chatURL: (): string => { + return new ApiRequest().chat().assembleFullUrl() + }, + + async chat(data: any): Promise { + return await api.createResponse(this.chatURL(), data) + }, + /** Fetch data from specified URL. The result already is JSON-parsed. */ async get(url: string, options?: ApiMethodOptions): Promise { const res = await api.getResponse(url, options) diff --git a/frontend/src/lib/components/CommandBar/ActionResult.tsx b/frontend/src/lib/components/CommandBar/ActionResult.tsx index cff7c24e09d2a..de97368c3ff66 100644 --- a/frontend/src/lib/components/CommandBar/ActionResult.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResult.tsx @@ -30,7 +30,7 @@ export const ActionResult = ({ result, focused }: SearchResultProps): JSX.Elemen )} >
{ diff --git a/frontend/src/lib/components/CommandBar/ActionResults.tsx b/frontend/src/lib/components/CommandBar/ActionResults.tsx index c104546e9210f..df7c8003b2281 100644 --- a/frontend/src/lib/components/CommandBar/ActionResults.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResults.tsx @@ -14,7 +14,7 @@ type ResultsGroupProps = { const ResultsGroup = ({ scope, results, activeResultIndex }: ResultsGroupProps): JSX.Element => { return ( <> -
+
{getNameFromActionScope(scope)}
{results.map((result) => ( diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index 242dc374f65e2..354470759518e 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -42,7 +42,7 @@ export const SearchResult = ({ result, resultIndex, focused }: SearchResultProps return (
{ diff --git a/frontend/src/lib/components/CommandBar/index.scss b/frontend/src/lib/components/CommandBar/index.scss index 593086bb81634..02aa24cb7a11d 100644 --- a/frontend/src/lib/components/CommandBar/index.scss +++ b/frontend/src/lib/components/CommandBar/index.scss @@ -1,7 +1,7 @@ .LemonInput.CommandBar__input { - height: 2.75rem; - padding-right: 0.375rem; - padding-left: 0.75rem; + height: 3rem; + padding-right: 0.5rem; + padding-left: 1rem; border-color: transparent !important; border-radius: 0; } diff --git a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx index ba614fd81ad7f..33c2f5b9c51b0 100644 --- a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx +++ b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx @@ -27,8 +27,8 @@ import { standardAnimations, } from './sprites/sprites' -const xFrames = SPRITE_SHEET_WIDTH / SPRITE_SIZE -const FPS = 24 +export const X_FRAMES = SPRITE_SHEET_WIDTH / SPRITE_SIZE +export const FPS = 24 const GRAVITY_PIXELS = 10 const MAX_JUMP_COUNT = 2 @@ -592,8 +592,8 @@ export class HedgehogActor { width: SPRITE_SIZE, height: SPRITE_SIZE, backgroundImage: `url(${baseSpritePath()}/${this.animation.img}.png)`, - backgroundPosition: `-${(this.animationFrame % xFrames) * SPRITE_SIZE}px -${ - Math.floor(this.animationFrame / xFrames) * SPRITE_SIZE + backgroundPosition: `-${(this.animationFrame % X_FRAMES) * SPRITE_SIZE}px -${ + Math.floor(this.animationFrame / X_FRAMES) * SPRITE_SIZE }px`, filter: imageFilter as any, }} diff --git a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddyRender.tsx b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddyRender.tsx index 7a24d4b69c194..337dc6744b1bf 100644 --- a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddyRender.tsx +++ b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddyRender.tsx @@ -1,35 +1,77 @@ +import { useEffect, useRef, useState } from 'react' + import { HedgehogConfig } from '~/types' +import { FPS, X_FRAMES } from './HedgehogBuddy' import { COLOR_TO_FILTER_MAP } from './hedgehogBuddyLogic' -import { baseSpriteAccessoriesPath, baseSpritePath, standardAccessories } from './sprites/sprites' +import { + baseSpriteAccessoriesPath, + baseSpritePath, + SPRITE_SIZE, + standardAccessories, + standardAnimations, +} from './sprites/sprites' -export type HedgehogBuddyStaticProps = Partial & { size?: number | string } +export type HedgehogBuddyStaticProps = Partial & { size?: number | string; waveOnAppearance?: boolean } // Takes a range of options and renders a static hedgehog -export function HedgehogBuddyStatic({ accessories, color, size }: HedgehogBuddyStaticProps): JSX.Element { +export function HedgehogBuddyStatic({ + accessories, + color, + size, + waveOnAppearance, +}: HedgehogBuddyStaticProps): JSX.Element { const imgSize = size ?? 60 const accessoryInfos = accessories?.map((x) => standardAccessories[x]) const filter = color ? COLOR_TO_FILTER_MAP[color] : null + const [animationIteration, setAnimationIteration] = useState(waveOnAppearance ? 1 : 0) + const [_, setTimerLoop] = useState(0) + const animationFrameRef = useRef(0) + + useEffect(() => { + if (animationIteration) { + setTimerLoop(0) + let timer: any = null + const loop = (): void => { + if (animationFrameRef.current < standardAnimations.wave.frames) { + animationFrameRef.current++ + timer = setTimeout(loop, 1000 / FPS) + } else { + animationFrameRef.current = 0 + } + setTimerLoop((x) => x + 1) + } + loop() + return () => { + clearTimeout(timer) + } + } + }, [animationIteration]) + return (
setAnimationIteration((x) => x + 1) : undefined} > - @@ -37,7 +79,7 @@ export function HedgehogBuddyStatic({ accessories, color, size }: HedgehogBuddyS .LemonIcon { color: var(--primary-3000); diff --git a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx index da51f8a6891c9..5f9117b9e41b3 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx @@ -44,7 +44,7 @@ interface LemonInputPropsBase /** Special case - show a transparent background rather than white */ transparentBackground?: boolean /** Size of the element. Default: `'medium'`. */ - size?: 'xsmall' | 'small' | 'medium' + size?: 'xsmall' | 'small' | 'medium' | 'large' onPressEnter?: (event: React.KeyboardEvent) => void 'data-attr'?: string 'aria-label'?: string diff --git a/frontend/src/lib/lemon-ui/icons/categories.ts b/frontend/src/lib/lemon-ui/icons/categories.ts index d7e29e9a5327b..c57ef8d09c6ef 100644 --- a/frontend/src/lib/lemon-ui/icons/categories.ts +++ b/frontend/src/lib/lemon-ui/icons/categories.ts @@ -51,6 +51,7 @@ export const OBJECTS = { 'IconGear', 'IconGearFilled', 'IconStack', + 'IconSparkles', ], People: ['IconPeople', 'IconPeopleFilled', 'IconPerson', 'IconProfile', 'IconUser', 'IconGroups'], 'Business & Finance': ['IconStore', 'IconCart', 'IconReceipt', 'IconPiggyBank', 'IconHandMoney'], diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 10e539b1d61ac..09bcdf2f44af1 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1,6 +1,134 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AIActionsNode": { + "additionalProperties": false, + "properties": { + "custom_name": { + "type": "string" + }, + "event": { + "description": "The event or `null` for all events.", + "type": ["string", "null"] + }, + "fixedProperties": { + "items": { + "$ref": "#/definitions/AIPropertyFilter" + }, + "type": "array" + }, + "kind": { + "const": "EventsNode", + "type": "string" + }, + "math": { + "$ref": "#/definitions/MathType" + }, + "math_group_type_index": { + "enum": [0, 1, 2, 3, 4], + "type": "number" + }, + "math_property": { + "type": "string" + }, + "name": { + "type": "string" + }, + "orderBy": { + "description": "Columns to order by", + "items": { + "type": "string" + }, + "type": "array" + }, + "properties": { + "items": { + "$ref": "#/definitions/AIPropertyFilter" + }, + "type": "array" + }, + "response": { + "type": "object" + } + }, + "required": ["kind"], + "type": "object" + }, + "AIEventsNode": { + "additionalProperties": false, + "properties": { + "custom_name": { + "type": "string" + }, + "event": { + "description": "The event or `null` for all events.", + "type": ["string", "null"] + }, + "fixedProperties": { + "items": { + "$ref": "#/definitions/AIPropertyFilter" + }, + "type": "array" + }, + "kind": { + "const": "EventsNode", + "type": "string" + }, + "math": { + "$ref": "#/definitions/MathType" + }, + "math_group_type_index": { + "enum": [0, 1, 2, 3, 4], + "type": "number" + }, + "math_property": { + "type": "string" + }, + "name": { + "type": "string" + }, + "orderBy": { + "description": "Columns to order by", + "items": { + "type": "string" + }, + "type": "array" + }, + "properties": { + "items": { + "$ref": "#/definitions/AIPropertyFilter" + }, + "type": "array" + }, + "response": { + "type": "object" + } + }, + "required": ["kind"], + "type": "object" + }, + "AIPropertyFilter": { + "anyOf": [ + { + "$ref": "#/definitions/EventPropertyFilter" + }, + { + "$ref": "#/definitions/PersonPropertyFilter" + }, + { + "$ref": "#/definitions/SessionPropertyFilter" + }, + { + "$ref": "#/definitions/CohortPropertyFilter" + }, + { + "$ref": "#/definitions/GroupPropertyFilter" + }, + { + "$ref": "#/definitions/FeaturePropertyFilter" + } + ] + }, "ActionsNode": { "additionalProperties": false, "properties": { @@ -4181,6 +4309,92 @@ "required": ["columns", "hogql", "results", "types"], "type": "object" }, + "ExperimentalAITrendsQuery": { + "additionalProperties": false, + "properties": { + "aggregation_group_type_index": { + "description": "Groups aggregation", + "type": "integer" + }, + "breakdownFilter": { + "additionalProperties": false, + "description": "Breakdown of the events and actions", + "properties": { + "breakdown_hide_other_aggregation": { + "type": ["boolean", "null"] + }, + "breakdown_histogram_bin_count": { + "type": "integer" + }, + "breakdown_limit": { + "type": "integer" + }, + "breakdowns": { + "items": { + "$ref": "#/definitions/Breakdown" + }, + "maxLength": 3, + "type": "array" + } + }, + "type": "object" + }, + "compareFilter": { + "$ref": "#/definitions/CompareFilter", + "description": "Compare to date range" + }, + "dateRange": { + "$ref": "#/definitions/InsightDateRange", + "description": "Date range for the query" + }, + "filterTestAccounts": { + "default": false, + "description": "Exclude internal and test users by applying the respective filters", + "type": "boolean" + }, + "interval": { + "$ref": "#/definitions/IntervalType", + "default": "day", + "description": "Granularity of the response. Can be one of `hour`, `day`, `week` or `month`" + }, + "kind": { + "const": "TrendsQuery", + "type": "string" + }, + "properties": { + "default": [], + "description": "Property filters for all series", + "items": { + "$ref": "#/definitions/AIPropertyFilter" + }, + "type": "array" + }, + "samplingFactor": { + "description": "Sampling rate", + "type": ["number", "null"] + }, + "series": { + "description": "Events and actions to include", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AIEventsNode" + }, + { + "$ref": "#/definitions/AIActionsNode" + } + ] + }, + "type": "array" + }, + "trendsFilter": { + "$ref": "#/definitions/TrendsFilter", + "description": "Properties specific to the trends insight" + } + }, + "required": ["kind", "series"], + "type": "object" + }, "FeaturePropertyFilter": { "additionalProperties": false, "properties": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 8ddd7d79fad6d..66439691f7a80 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -8,14 +8,17 @@ import { BreakdownType, ChartDisplayCategory, ChartDisplayType, + CohortPropertyFilter, CountPerActorMathType, DurationType, EventPropertyFilter, EventType, + FeaturePropertyFilter, FilterLogicalOperator, FilterType, FunnelsFilterType, GroupMathType, + GroupPropertyFilter, HogQLMathType, InsightShortId, InsightType, @@ -826,6 +829,79 @@ export interface TrendsQuery extends InsightsQueryBase { compareFilter?: CompareFilter } +export type AIPropertyFilter = + | EventPropertyFilter + | PersonPropertyFilter + // | ElementPropertyFilter + | SessionPropertyFilter + | CohortPropertyFilter + // | RecordingPropertyFilter + // | LogEntryPropertyFilter + // | HogQLPropertyFilter + // | EmptyPropertyFilter + // | DataWarehousePropertyFilter + // | DataWarehousePersonPropertyFilter + | GroupPropertyFilter + | FeaturePropertyFilter + +export interface AIEventsNode + extends Omit { + properties?: AIPropertyFilter[] + fixedProperties?: AIPropertyFilter[] +} + +export interface AIActionsNode + extends Omit { + properties?: AIPropertyFilter[] + fixedProperties?: AIPropertyFilter[] +} + +export interface ExperimentalAITrendsQuery { + kind: NodeKind.TrendsQuery + /** + * Granularity of the response. Can be one of `hour`, `day`, `week` or `month` + * + * @default day + */ + interval?: IntervalType + /** Events and actions to include */ + series: (AIEventsNode | AIActionsNode)[] + /** Properties specific to the trends insight */ + trendsFilter?: TrendsFilter + /** Breakdown of the events and actions */ + breakdownFilter?: Omit< + BreakdownFilter, + | 'breakdown' + | 'breakdown_type' + | 'breakdown_normalize_url' + | 'histogram_bin_count' + | 'breakdown_group_type_index' + > + /** Compare to date range */ + compareFilter?: CompareFilter + /** Date range for the query */ + dateRange?: InsightDateRange + /** + * Exclude internal and test users by applying the respective filters + * + * @default false + */ + filterTestAccounts?: boolean + /** + * Property filters for all series + * + * @default [] + */ + properties?: AIPropertyFilter[] + + /** + * Groups aggregation + */ + aggregation_group_type_index?: integer + /** Sampling rate */ + samplingFactor?: number | null +} + /** `FunnelsFilterType` minus everything inherited from `FilterType` and persons modal related params */ export type FunnelsFilterLegacy = Omit< FunnelsFilterType, diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 23bc70635693a..0824fc5438068 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -45,6 +45,7 @@ export const appScenes: Record any> = { [Scene.OrganizationCreateFirst]: () => import('./organization/Create'), [Scene.OrganizationCreationConfirm]: () => import('./organization/ConfirmOrganization/ConfirmOrganization'), [Scene.ProjectHomepage]: () => import('./project-homepage/ProjectHomepage'), + [Scene.Max]: () => import('./max/Max'), [Scene.ProjectCreateFirst]: () => import('./project/Create'), [Scene.SystemStatus]: () => import('./instance/SystemStatus'), [Scene.ToolbarLaunch]: () => import('./toolbar-launch/ToolbarLaunch'), diff --git a/frontend/src/scenes/max/Max.scss b/frontend/src/scenes/max/Max.scss new file mode 100644 index 0000000000000..68671f6310558 --- /dev/null +++ b/frontend/src/scenes/max/Max.scss @@ -0,0 +1,3 @@ +.InsightVizDisplay { + flex: 1; +} diff --git a/frontend/src/scenes/max/Max.tsx b/frontend/src/scenes/max/Max.tsx new file mode 100644 index 0000000000000..594ca3344aed3 --- /dev/null +++ b/frontend/src/scenes/max/Max.tsx @@ -0,0 +1,157 @@ +import './Max.scss' + +import { LemonButton, LemonInput, Spinner } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { HedgehogBuddyStatic } from 'lib/components/HedgehogBuddy/HedgehogBuddyRender' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { uuid } from 'lib/utils' +import React, { useState } from 'react' +import { SceneExport } from 'scenes/sceneTypes' +import { userLogic } from 'scenes/userLogic' + +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' + +import { maxLogic } from './maxLogic' + +export const scene: SceneExport = { + component: Max, + logic: maxLogic, +} + +function Message({ + role, + children, + className, +}: React.PropsWithChildren<{ role: 'user' | 'assistant'; className?: string }>): JSX.Element { + return ( +
+ {children} +
+ ) +} + +export function Max(): JSX.Element | null { + const { user } = useValues(userLogic) + const { featureFlags } = useValues(featureFlagLogic) + + const logic = maxLogic({ + sessionId: uuid(), + }) + const { thread, threadLoading } = useValues(logic) + const { askMax } = useActions(logic) + + const [question, setQuestion] = useState('') + + if (!featureFlags[FEATURE_FLAGS.ARTIFICIAL_HOG]) { + return null + } + + return ( + <> +
+ {thread.map((message, index) => { + if (message.role === 'user' || typeof message.content === 'string') { + return ( + + {message.content || No text} + + ) + } + + const query = { + kind: NodeKind.InsightVizNode, + source: message.content?.answer, + } + + return ( + + {message.content?.reasoning_steps && ( + +
    + {message.content.reasoning_steps.map((step, index) => ( +
  • {step}
  • + ))} +
+
+ )} + {message.status === 'completed' && message.content?.answer && ( + +
+ +
+ + Edit Query + +
+ )} +
+ ) + })} + {threadLoading && ( + +
+ Let me thinkā€¦ + +
+
+ )} +
+
+
+ +
+ setQuestion(value)} + placeholder="Hey, I'm Max! What would you like to know about your product?" + fullWidth + size="large" + autoFocus + onPressEnter={() => { + askMax(question) + setQuestion('') + }} + disabled={threadLoading} + suffix={ + { + askMax(question) + setQuestion('') + }} + disabledReason={threadLoading ? 'Thinkingā€¦' : undefined} + > + Ask Max + + } + /> +
+ + ) +} diff --git a/frontend/src/scenes/max/maxLogic.ts b/frontend/src/scenes/max/maxLogic.ts new file mode 100644 index 0000000000000..a0a863e98e0eb --- /dev/null +++ b/frontend/src/scenes/max/maxLogic.ts @@ -0,0 +1,164 @@ +import { actions, kea, listeners, path, props, reducers } from 'kea' +import api from 'lib/api' + +import { ExperimentalAITrendsQuery } from '~/queries/schema' + +import type { maxLogicType } from './maxLogicType' + +export interface MaxLogicProps { + sessionId: string +} + +interface TrendGenerationResult { + reasoning_steps?: string[] + answer?: ExperimentalAITrendsQuery +} + +export interface ThreadMessage { + role: 'user' | 'assistant' + content?: string | TrendGenerationResult + status?: 'loading' | 'completed' | 'error' +} + +export const maxLogic = kea([ + path(['scenes', 'max', 'maxLogic']), + props({} as MaxLogicProps), + actions({ + askMax: (prompt: string) => ({ prompt }), + setThreadLoaded: true, + addMessage: (message: ThreadMessage) => ({ message }), + replaceMessage: (index: number, message: ThreadMessage) => ({ index, message }), + setMessageStatus: (index: number, status: ThreadMessage['status']) => ({ index, status }), + }), + reducers({ + thread: [ + [] as ThreadMessage[], + { + addMessage: (state, { message }) => [...state, message], + replaceMessage: (state, { message, index }) => [ + ...state.slice(0, index), + message, + ...state.slice(index + 1), + ], + setMessageStatus: (state, { index, status }) => [ + ...state.slice(0, index), + { + ...state[index], + status, + }, + ...state.slice(index + 1), + ], + }, + ], + threadLoading: [ + false, + { + askMax: () => true, + setThreadLoaded: () => false, + }, + ], + }), + listeners(({ actions, values, props }) => ({ + askMax: async ({ prompt }) => { + actions.addMessage({ role: 'user', content: prompt }) + const newIndex = values.thread.length + + try { + const response = await api.chat({ + session_id: props.sessionId, + messages: values.thread.map(({ role, content }) => ({ + role, + content: typeof content === 'string' ? content : JSON.stringify(content), + })), + }) + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + if (reader) { + let firstChunk = true + + while (true) { + const { done, value } = await reader.read() + if (done) { + actions.setMessageStatus(newIndex, 'completed') + break + } + + const text = decoder.decode(value) + const parsedResponse = parseResponse(text) + + if (firstChunk) { + firstChunk = false + + if (parsedResponse) { + actions.addMessage({ role: 'assistant', content: parsedResponse, status: 'loading' }) + } + } else if (parsedResponse) { + actions.replaceMessage(newIndex, { + role: 'assistant', + content: parsedResponse, + status: 'loading', + }) + } + } + } + } catch { + actions.setMessageStatus(values.thread.length - 1 === newIndex ? newIndex : newIndex - 1, 'error') + } + + actions.setThreadLoaded() + }, + })), +]) + +/** + * Parses the generation result from the API. Some generation chunks might be sent in batches. + * @param response + */ +function parseResponse(response: string, recursive = true): TrendGenerationResult | null { + try { + const parsed = JSON.parse(response) + return parsed as TrendGenerationResult + } catch { + if (!recursive) { + return null + } + + const results: [number, number][] = [] + let pair: [number, number] = [0, 0] + let seq = 0 + + for (let i = 0; i < response.length; i++) { + const char = response[i] + + if (char === '{') { + if (seq === 0) { + pair[0] = i + } + + seq += 1 + } + + if (char === '}') { + seq -= 1 + if (seq === 0) { + pair[1] = i + } + } + + if (seq === 0) { + results.push(pair) + pair = [0, 0] + } + } + + const lastPair = results.pop() + + if (lastPair) { + const [left, right] = lastPair + return parseResponse(response.slice(left, right + 1), false) + } + + return null + } +} diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 5f519137e32b0..adddb012a10b3 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -48,6 +48,7 @@ export enum Scene { DataWarehouseRedirect = 'DataWarehouseRedirect', OrganizationCreateFirst = 'OrganizationCreate', ProjectHomepage = 'ProjectHomepage', + Max = 'Max', ProjectCreateFirst = 'ProjectCreate', SystemStatus = 'SystemStatus', AsyncMigrations = 'AsyncMigrations', diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 9b4019886e9b2..7581e424bd2fd 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -253,6 +253,12 @@ export const sceneConfigurations: Record = { projectBased: true, name: 'Homepage', }, + [Scene.Max]: { + projectBased: true, + name: 'Max', + layout: 'app-raw', + hideProjectNotice: true, // FIXME: Currently doesn't render well... + }, [Scene.IntegrationsRedirect]: { name: 'Integrations redirect', }, @@ -343,7 +349,7 @@ export const sceneConfigurations: Record = { }, [Scene.Notebook]: { projectBased: true, - hideProjectNotice: true, // Currently doesn't render well... + hideProjectNotice: true, // FIXME: Currently doesn't render well... name: 'Notebook', layout: 'app-raw', activityScope: ActivityScope.NOTEBOOK, @@ -517,6 +523,7 @@ export const routes: Record = { [urls.annotations()]: Scene.DataManagement, [urls.annotation(':id')]: Scene.DataManagement, [urls.projectHomepage()]: Scene.ProjectHomepage, + [urls.max()]: Scene.Max, [urls.projectCreateFirst()]: Scene.ProjectCreateFirst, [urls.organizationBilling()]: Scene.Billing, [urls.organizationCreateFirst()]: Scene.OrganizationCreateFirst, diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index ca6c98088952a..cc046586fc768 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -156,6 +156,7 @@ export const urls = { organizationCreateFirst: (): string => '/create-organization', projectCreateFirst: (): string => '/organization/create-project', projectHomepage: (): string => '/', + max: (): string => '/max', settings: (section: SettingSectionId | SettingLevelId = 'project', setting?: SettingId): string => combineUrl(`/settings/${section}`, undefined, setting).url, organizationCreationConfirm: (): string => '/organization/confirm-creation', diff --git a/package.json b/package.json index 8a67dc54ba8c3..44309818ccdb6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.6.0", "@posthog/hogvm": "^1.0.44", - "@posthog/icons": "0.7.3", + "@posthog/icons": "0.8.1", "@posthog/plugin-scaffold": "^1.4.4", "@react-hook/size": "^2.1.2", "@rrweb/types": "2.0.0-alpha.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 833dec3dea675..813be57c694d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,8 +56,8 @@ dependencies: specifier: ^1.0.44 version: 1.0.44(luxon@3.5.0) '@posthog/icons': - specifier: 0.7.3 - version: 0.7.3(react-dom@18.2.0)(react@18.2.0) + specifier: 0.8.1 + version: 0.8.1(react-dom@18.2.0)(react@18.2.0) '@posthog/plugin-scaffold': specifier: ^1.4.4 version: 1.4.4 @@ -5422,8 +5422,8 @@ packages: luxon: 3.5.0 dev: false - /@posthog/icons@0.7.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-dw8qLS6aSBGGIjo/d24/yuLOgkFAov4C7yOhomMfhce/RwS+u96XXghVolioRHppnAn48pgGnBQIXEELGVEvPA==} + /@posthog/icons@0.8.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/ryXgFnWGzHmwijHE/0gQcEyAD/WkKuwf3NCMG4ibmGMpEqm/d12/+Ccuf3Zj2VZuc+0atGCHkHOiSNJ8dw97A==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' @@ -15427,7 +15427,7 @@ packages: image-size: 0.5.5 make-dir: 2.1.0 mime: 1.6.0 - native-request: 1.1.2 + native-request: 1.1.0 source-map: 0.6.1 dev: true @@ -16163,8 +16163,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - /native-request@1.1.2: - resolution: {integrity: sha512-/etjwrK0J4Ebbcnt35VMWnfiUX/B04uwGJxyJInagxDqf2z5drSt/lsOvEMWGYunz1kaLZAFrV4NDAbOoDKvAQ==} + /native-request@1.1.0: + resolution: {integrity: sha512-uZ5rQaeRn15XmpgE0xoPL8YWqcX90VtCFglYwAgkvKM5e8fog+vePLAhHxuuv/gRkrQxIeh5U3q9sMNUrENqWw==} requiresBuild: true dev: true optional: true diff --git a/posthog/api/query.py b/posthog/api/query.py index 8c71b1465017a..7e6e145f8b5e3 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -1,34 +1,37 @@ +import json import re import uuid -from django.http import JsonResponse +from django.http import JsonResponse, StreamingHttpResponse from drf_spectacular.utils import OpenApiResponse from pydantic import BaseModel -from rest_framework import status -from rest_framework import viewsets -from posthog.api.utils import action -from rest_framework.exceptions import ValidationError, NotAuthenticated +from rest_framework import status, viewsets +from rest_framework.exceptions import NotAuthenticated, ValidationError +from rest_framework.renderers import BaseRenderer from rest_framework.request import Request from rest_framework.response import Response from sentry_sdk import capture_exception, set_tag +from ee.hogai.generate_trends_agent import Conversation, GenerateTrendsAgent from posthog.api.documentation import extend_schema from posthog.api.mixins import PydanticModelMixin +from posthog.api.monitoring import Feature, monitor from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.services.query import process_query_model +from posthog.api.utils import action from posthog.clickhouse.client.execute_async import ( cancel_query, get_query_status, ) from posthog.clickhouse.query_tagging import tag_queries from posthog.errors import ExposedCHQueryError +from posthog.event_usage import report_user_action from posthog.hogql.ai import PromptUnclear, write_sql_from_prompt from posthog.hogql.errors import ExposedHogQLError from posthog.hogql_queries.query_runner import ExecutionMode, execution_mode_from_refresh from posthog.models.user import User from posthog.rate_limit import AIBurstRateThrottle, AISustainedRateThrottle, PersonalApiKeyRateThrottle from posthog.schema import QueryRequest, QueryResponseAlternative, QueryStatusResponse -from posthog.api.monitoring import monitor, Feature class QueryThrottle(PersonalApiKeyRateThrottle): @@ -36,6 +39,14 @@ class QueryThrottle(PersonalApiKeyRateThrottle): rate = "120/hour" +class ServerSentEventRenderer(BaseRenderer): + media_type = "text/event-stream" + format = "txt" + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data + + class QueryViewSet(TeamAndOrgViewSetMixin, PydanticModelMixin, viewsets.ViewSet): # NOTE: Do we need to override the scopes for the "create" scope_object = "query" @@ -45,7 +56,7 @@ class QueryViewSet(TeamAndOrgViewSetMixin, PydanticModelMixin, viewsets.ViewSet) sharing_enabled_actions = ["retrieve"] def get_throttles(self): - if self.action == "draft_sql": + if self.action in ("draft_sql", "chat"): return [AIBurstRateThrottle(), AISustainedRateThrottle()] else: return [QueryThrottle()] @@ -144,6 +155,30 @@ def draft_sql(self, request: Request, *args, **kwargs) -> Response: raise ValidationError({"prompt": [str(e)]}, code="unclear") return Response({"sql": result}) + @action(detail=False, methods=["POST"], renderer_classes=[ServerSentEventRenderer]) + def chat(self, request: Request, *args, **kwargs): + assert request.user is not None + validated_body = Conversation.model_validate(request.data) + chain = GenerateTrendsAgent(self.team).bootstrap(validated_body.messages) + + def generate(): + last_message = None + for message in chain.stream({"question": validated_body.messages[0].content}): + if message: + last_message = message[0].model_dump_json() + yield last_message + + if not last_message: + yield json.dumps({"reasoning_steps": ["Schema validation failed"]}) + + report_user_action( + request.user, # type: ignore + "chat with ai", + {"prompt": validated_body.messages[-1].content, "response": last_message}, + ) + + return StreamingHttpResponse(generate(), content_type=ServerSentEventRenderer.media_type) + def handle_column_ch_error(self, error): if getattr(error, "message", None): match = re.search(r"There's no column.*in table", error.message) diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 3703dc9ea6093..9b470bb936f43 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -80,6 +80,7 @@ '/home/runner/work/posthog/posthog/posthog/api/query.py: Warning [QueryViewSet]: 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. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py: Warning [QueryViewSet > ModelMetaclass]: Encountered 2 components with identical names "Person" and different classes and . This will very likely result in an incorrect schema. Try renaming one.', '/home/runner/work/posthog/posthog/posthog/api/query.py: Warning [QueryViewSet]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/query.py: Error [QueryViewSet]: 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/ee/session_recordings/session_recording_playlist.py: Warning [SessionRecordingPlaylistViewSet]: could not derive type of path parameter "project_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/ee/session_recordings/session_recording_playlist.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/session_recordings/session_recording_api.py: Warning [SessionRecordingViewSet]: could not derive type of path parameter "project_id" because model "posthog.session_recordings.models.session_recording.SessionRecording" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', diff --git a/posthog/schema.py b/posthog/schema.py index f99badf4c1c84..bf7662b7a9e5b 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -2474,6 +2474,16 @@ class EventsQueryResponse(BaseModel): types: list[str] +class BreakdownFilter1(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + breakdown_hide_other_aggregation: Optional[bool] = None + breakdown_histogram_bin_count: Optional[int] = None + breakdown_limit: Optional[int] = None + breakdowns: Optional[list[Breakdown]] = Field(default=None, max_length=3) + + class FeaturePropertyFilter(BaseModel): model_config = ConfigDict( extra="forbid", @@ -4419,6 +4429,88 @@ class SessionsTimelineQuery(BaseModel): response: Optional[SessionsTimelineQueryResponse] = None +class AIActionsNode(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + custom_name: Optional[str] = None + event: Optional[str] = Field(default=None, description="The event or `null` for all events.") + fixedProperties: Optional[ + list[ + Union[ + EventPropertyFilter, + PersonPropertyFilter, + SessionPropertyFilter, + CohortPropertyFilter, + GroupPropertyFilter, + FeaturePropertyFilter, + ] + ] + ] = None + kind: Literal["EventsNode"] = "EventsNode" + math: Optional[ + Union[BaseMathType, PropertyMathType, CountPerActorMathType, Literal["unique_group"], Literal["hogql"]] + ] = None + math_group_type_index: Optional[MathGroupTypeIndex] = None + math_property: Optional[str] = None + name: Optional[str] = None + orderBy: Optional[list[str]] = Field(default=None, description="Columns to order by") + properties: Optional[ + list[ + Union[ + EventPropertyFilter, + PersonPropertyFilter, + SessionPropertyFilter, + CohortPropertyFilter, + GroupPropertyFilter, + FeaturePropertyFilter, + ] + ] + ] = None + response: Optional[dict[str, Any]] = None + + +class AIEventsNode(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + custom_name: Optional[str] = None + event: Optional[str] = Field(default=None, description="The event or `null` for all events.") + fixedProperties: Optional[ + list[ + Union[ + EventPropertyFilter, + PersonPropertyFilter, + SessionPropertyFilter, + CohortPropertyFilter, + GroupPropertyFilter, + FeaturePropertyFilter, + ] + ] + ] = None + kind: Literal["EventsNode"] = "EventsNode" + math: Optional[ + Union[BaseMathType, PropertyMathType, CountPerActorMathType, Literal["unique_group"], Literal["hogql"]] + ] = None + math_group_type_index: Optional[MathGroupTypeIndex] = None + math_property: Optional[str] = None + name: Optional[str] = None + orderBy: Optional[list[str]] = Field(default=None, description="Columns to order by") + properties: Optional[ + list[ + Union[ + EventPropertyFilter, + PersonPropertyFilter, + SessionPropertyFilter, + CohortPropertyFilter, + GroupPropertyFilter, + FeaturePropertyFilter, + ] + ] + ] = None + response: Optional[dict[str, Any]] = None + + class ActionsNode(BaseModel): model_config = ConfigDict( extra="forbid", @@ -4499,6 +4591,39 @@ class DatabaseSchemaViewTable(BaseModel): type: Literal["view"] = "view" +class ExperimentalAITrendsQuery(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + aggregation_group_type_index: Optional[int] = Field(default=None, description="Groups aggregation") + breakdownFilter: Optional[BreakdownFilter1] = Field(default=None, description="Breakdown of the events and actions") + compareFilter: Optional[CompareFilter] = Field(default=None, description="Compare to date range") + dateRange: Optional[InsightDateRange] = Field(default=None, description="Date range for the query") + filterTestAccounts: Optional[bool] = Field( + default=False, description="Exclude internal and test users by applying the respective filters" + ) + interval: Optional[IntervalType] = Field( + default=IntervalType.DAY, + description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`", + ) + kind: Literal["TrendsQuery"] = "TrendsQuery" + properties: Optional[ + list[ + Union[ + EventPropertyFilter, + PersonPropertyFilter, + SessionPropertyFilter, + CohortPropertyFilter, + GroupPropertyFilter, + FeaturePropertyFilter, + ] + ] + ] = Field(default=[], description="Property filters for all series") + samplingFactor: Optional[float] = Field(default=None, description="Sampling rate") + series: list[Union[AIEventsNode, AIActionsNode]] = Field(..., description="Events and actions to include") + trendsFilter: Optional[TrendsFilter] = Field(default=None, description="Properties specific to the trends insight") + + class FunnelsFilter(BaseModel): model_config = ConfigDict( extra="forbid", diff --git a/requirements-dev.in b/requirements-dev.in index 8ab3ba93b3a2d..b125a97db3286 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -22,7 +22,7 @@ Faker==17.5.0 fakeredis[lua]==2.23.3 freezegun==1.2.2 inline-snapshot==0.10.2 -packaging==23.1 +packaging==24.1 black~=23.9.1 boto3-stubs[s3] types-markdown==3.3.9 diff --git a/requirements-dev.txt b/requirements-dev.txt index b12079e28c40f..0c4cf8b0d77f7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,6 +20,7 @@ asgiref==3.7.2 # via # -c requirements.txt # django + # django-stubs asttokens==2.4.1 # via inline-snapshot async-timeout==4.0.2 @@ -37,7 +38,7 @@ black==23.9.1 # -r requirements-dev.in # datamodel-code-generator # inline-snapshot -boto3-stubs[s3]==1.34.84 +boto3-stubs==1.34.84 # via -r requirements-dev.in botocore-stubs==1.34.84 # via boto3-stubs @@ -62,7 +63,7 @@ click==8.1.7 # inline-snapshot colorama==0.4.4 # via pytest-watch -coverage[toml]==5.5 +coverage==5.5 # via pytest-cov cryptography==39.0.2 # via @@ -76,7 +77,9 @@ django==4.2.15 # django-stubs # django-stubs-ext django-stubs==5.0.4 - # via djangorestframework-stubs + # via + # -r requirements-dev.in + # djangorestframework-stubs django-stubs-ext==5.0.4 # via django-stubs djangorestframework-stubs==3.14.5 @@ -95,7 +98,7 @@ executing==2.0.1 # via inline-snapshot faker==17.5.0 # via -r requirements-dev.in -fakeredis[lua]==2.23.3 +fakeredis==2.23.3 # via -r requirements-dev.in flaky==3.7.0 # via -r requirements-dev.in @@ -154,6 +157,7 @@ multidict==6.0.2 # aiohttp # yarl mypy==1.11.1 + # via -r requirements-dev.in mypy-baseline==0.7.0 # via -r requirements-dev.in mypy-boto3-s3==1.34.65 @@ -167,7 +171,7 @@ openapi-schema-validator==0.6.2 # via openapi-spec-validator openapi-spec-validator==0.7.1 # via -r requirements-dev.in -packaging==23.1 +packaging==24.1 # via # -c requirements.txt # -r requirements-dev.in @@ -195,7 +199,7 @@ pycparser==2.20 # via # -c requirements.txt # cffi -pydantic[email]==2.5.3 +pydantic==2.5.3 # via # -c requirements.txt # datamodel-code-generator @@ -281,6 +285,7 @@ ruamel-yaml==0.18.6 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.1 + # via -r requirements-dev.in six==1.16.0 # via # -c requirements.txt @@ -317,7 +322,6 @@ types-python-dateutil==2.8.3 types-pytz==2023.3.0.0 # via # -r requirements-dev.in - # django-stubs # types-tzlocal types-pyyaml==6.0.1 # via diff --git a/requirements.in b/requirements.in index 2e0332d76ec58..17c4feb2f808d 100644 --- a/requirements.in +++ b/requirements.in @@ -45,12 +45,15 @@ gunicorn==20.1.0 infi-clickhouse-orm@ git+https://github.com/PostHog/infi.clickhouse_orm@9578c79f29635ee2c1d01b7979e89adab8383de2 kafka-python==2.0.2 kombu==5.3.2 +langchain==0.2.15 +langchain-openai==0.1.23 +langsmith==0.1.106 lzstring==1.0.4 natsort==8.4.0 nanoid==2.0.0 numpy==1.23.3 openpyxl==3.1.2 -orjson==3.9.10 +orjson==3.10.7 pandas==2.2.0 paramiko==3.4.0 Pillow==10.2.0 @@ -96,8 +99,8 @@ mimesis==5.2.1 more-itertools==9.0.0 django-two-factor-auth==1.14.0 phonenumberslite==8.13.6 -openai==1.10.0 -tiktoken==0.6.0 +openai==1.43.0 +tiktoken==0.7.0 nh3==0.2.14 hogql-parser==1.0.40 zxcvbn==4.4.28 diff --git a/requirements.txt b/requirements.txt index 484a579627303..c8d3e50b4256c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ aiohttp==3.9.3 # -r requirements.in # aiobotocore # geoip2 + # langchain # s3fs aioitertools==0.11.0 # via aiobotocore @@ -278,7 +279,9 @@ hogql-parser==1.0.40 httpcore==1.0.2 # via httpx httpx==0.26.0 - # via openai + # via + # langsmith + # openai humanize==4.9.0 # via dlt idna==2.8 @@ -300,14 +303,20 @@ isodate==0.6.1 # via # python3-saml # zeep +jiter==0.5.0 + # via openai jmespath==1.0.0 # via # boto3 # botocore joblib==1.3.2 # via scikit-learn +jsonpatch==1.33 + # via langchain-core jsonpath-ng==1.6.0 # via dlt +jsonpointer==3.0.0 + # via jsonpatch jsonschema==4.20.0 # via drf-spectacular jsonschema-specifications==2023.12.1 @@ -320,6 +329,22 @@ kombu==5.3.2 # via # -r requirements.in # celery +langchain==0.2.15 + # via -r requirements.in +langchain-core==0.2.36 + # via + # langchain + # langchain-openai + # langchain-text-splitters +langchain-openai==0.1.23 + # via -r requirements.in +langchain-text-splitters==0.2.2 + # via langchain +langsmith==0.1.106 + # via + # -r requirements.in + # langchain + # langchain-core lxml==4.9.4 # via # -r requirements.in @@ -354,6 +379,7 @@ nh3==0.2.14 numpy==1.23.3 # via # -r requirements.in + # langchain # pandas # pyarrow # scikit-learn @@ -362,23 +388,26 @@ oauthlib==3.1.0 # via # requests-oauthlib # social-auth-core -openai==1.10.0 +openai==1.43.0 # via # -r requirements.in + # langchain-openai # sentry-sdk openpyxl==3.1.2 # via -r requirements.in -orjson==3.9.10 +orjson==3.10.7 # via # -r requirements.in # dlt + # langsmith outcome==1.3.0.post0 # via trio -packaging==23.1 +packaging==24.1 # via # aiokafka # dlt # google-cloud-bigquery + # langchain-core # snowflake-connector-python # webdriver-manager pandas==2.2.0 @@ -443,6 +472,9 @@ pycparser==2.20 pydantic==2.5.3 # via # -r requirements.in + # langchain + # langchain-core + # langsmith # openai pydantic-core==2.14.6 # via pydantic @@ -502,6 +534,8 @@ pyyaml==6.0.1 # via # dlt # drf-spectacular + # langchain + # langchain-core qrcode==7.4.2 # via django-two-factor-auth redis==4.5.4 @@ -523,6 +557,8 @@ requests==2.32.0 # google-api-core # google-cloud-bigquery # infi-clickhouse-orm + # langchain + # langsmith # pdpyras # posthoganalytics # requests-file @@ -613,6 +649,7 @@ sortedcontainers==2.4.0 sqlalchemy==2.0.31 # via # -r requirements.in + # langchain # snowflake-sqlalchemy sqlparse==0.4.4 # via @@ -634,11 +671,14 @@ tenacity==8.2.3 # via # celery-redbeat # dlt + # langchain + # langchain-core threadpoolctl==3.3.0 # via scikit-learn -tiktoken==0.6.0 +tiktoken==0.7.0 # via # -r requirements.in + # langchain-openai # sentry-sdk token-bucket==0.3.0 # via -r requirements.in @@ -663,6 +703,7 @@ types-setuptools==69.0.0.0 typing-extensions==4.12.2 # via # dlt + # langchain-core # openai # psycopg # pydantic