{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