(function Te
)
})
+interface AssistantMessageFormProps {
+ form: AssistantForm
+}
+
+function AssistantMessageForm({ form }: AssistantMessageFormProps): JSX.Element {
+ const { askMax } = useActions(maxLogic)
+ return (
+
+ {form.options.map((option) => (
+ askMax(option.value)}
+ size="small"
+ type={
+ option.variant && ['primary', 'secondary', 'tertiary'].includes(option.variant)
+ ? (option.variant as LemonButtonPropsBase['type'])
+ : 'secondary'
+ }
+ >
+ {option.value}
+
+ ))}
+
+ )
+}
+
function VisualizationAnswer({
message,
status,
diff --git a/frontend/src/scenes/max/__mocks__/chatResponse.mocks.ts b/frontend/src/scenes/max/__mocks__/chatResponse.mocks.ts
index 6acfc72bd5914..61b7d68b8520f 100644
--- a/frontend/src/scenes/max/__mocks__/chatResponse.mocks.ts
+++ b/frontend/src/scenes/max/__mocks__/chatResponse.mocks.ts
@@ -1,6 +1,7 @@
import {
AssistantGenerationStatusEvent,
AssistantGenerationStatusType,
+ AssistantMessage,
AssistantMessageType,
HumanMessage,
ReasoningMessage,
@@ -71,3 +72,24 @@ export const generationFailureChunk = generateChunk([
])
export const failureChunk = generateChunk(['event: message', `data: ${JSON.stringify(failureMessage)}`])
+
+const formMessage: AssistantMessage = {
+ type: AssistantMessageType.Assistant,
+ content: 'Does this look like a good summary of what your product does?',
+ id: 'assistant-1',
+ meta: {
+ form: {
+ options: [
+ {
+ value: 'Yes, save this',
+ variant: 'primary',
+ },
+ {
+ value: 'No, not quite right',
+ },
+ ],
+ },
+ },
+}
+
+export const formChunk = generateChunk(['event: message', `data: ${JSON.stringify(formMessage)}`])
diff --git a/frontend/src/scenes/max/maxLogic.ts b/frontend/src/scenes/max/maxLogic.ts
index 74316cc3ed439..713998840f347 100644
--- a/frontend/src/scenes/max/maxLogic.ts
+++ b/frontend/src/scenes/max/maxLogic.ts
@@ -5,7 +5,7 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducer
import { loaders } from 'kea-loaders'
import api, { ApiError } from 'lib/api'
import { uuid } from 'lib/utils'
-import { isHumanMessage, isReasoningMessage, isVisualizationMessage } from 'scenes/max/utils'
+import { isAssistantMessage, isHumanMessage, isReasoningMessage, isVisualizationMessage } from 'scenes/max/utils'
import { projectLogic } from 'scenes/projectLogic'
import {
@@ -318,6 +318,20 @@ export const maxLogic = kea([
return threadGrouped
},
],
+ formPending: [
+ (s) => [s.threadRaw],
+ (threadRaw) => {
+ const lastMessage = threadRaw[threadRaw.length - 1]
+ if (lastMessage && isAssistantMessage(lastMessage)) {
+ return !!lastMessage.meta?.form
+ }
+ return false
+ },
+ ],
+ inputDisabled: [
+ (s) => [s.threadLoading, s.formPending],
+ (threadLoading, formPending) => threadLoading || formPending,
+ ],
}),
afterMount(({ actions, values }) => {
// We only load suggestions on mount if the product description is already set
diff --git a/posthog/schema.py b/posthog/schema.py
index c76a742d444a3..325004e8cab1b 100644
--- a/posthog/schema.py
+++ b/posthog/schema.py
@@ -101,6 +101,14 @@ class AssistantEventType(StrEnum):
CONVERSATION = "conversation"
+class AssistantFormOption(BaseModel):
+ model_config = ConfigDict(
+ extra="forbid",
+ )
+ value: str
+ variant: Optional[str] = None
+
+
class AssistantFunnelsBreakdownType(StrEnum):
PERSON = "person"
EVENT = "event"
@@ -205,15 +213,6 @@ class AssistantGroupPropertyFilter3(BaseModel):
value: str = Field(..., description="Value must be a date in ISO 8601 format.")
-class AssistantMessage(BaseModel):
- model_config = ConfigDict(
- extra="forbid",
- )
- content: str
- id: Optional[str] = None
- type: Literal["ai"] = "ai"
-
-
class AssistantMessageType(StrEnum):
HUMAN = "human"
AI = "ai"
@@ -1791,6 +1790,13 @@ class AssistantDateTimePropertyFilter(BaseModel):
value: str = Field(..., description="Value must be a date in ISO 8601 format.")
+class AssistantForm(BaseModel):
+ model_config = ConfigDict(
+ extra="forbid",
+ )
+ options: list[AssistantFormOption]
+
+
class AssistantFunnelsBreakdownFilter(BaseModel):
model_config = ConfigDict(
extra="forbid",
@@ -1969,6 +1975,13 @@ class AssistantGroupPropertyFilter4(BaseModel):
type: Literal["group"] = "group"
+class AssistantMessageMetadata(BaseModel):
+ model_config = ConfigDict(
+ extra="forbid",
+ )
+ form: Optional[AssistantForm] = None
+
+
class AssistantSetPropertyFilter(BaseModel):
model_config = ConfigDict(
extra="forbid",
@@ -3677,6 +3690,16 @@ class AssistantInsightsQueryBase(BaseModel):
)
+class AssistantMessage(BaseModel):
+ model_config = ConfigDict(
+ extra="forbid",
+ )
+ content: str
+ id: Optional[str] = None
+ meta: Optional[AssistantMessageMetadata] = None
+ type: Literal["ai"] = "ai"
+
+
class AssistantRetentionFilter(BaseModel):
model_config = ConfigDict(
extra="forbid",
diff --git a/requirements-dev.txt b/requirements-dev.txt
index cc0e410715cd7..c5a8a3e50d60c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -89,7 +89,9 @@ cryptography==39.0.2
# -c requirements.txt
# types-paramiko
dataclasses-json==0.6.7
- # via langchain-community
+ # via
+ # -c requirements.txt
+ # langchain-community
datamodel-code-generator==0.26.1
# via -r requirements-dev.in
datasets==2.19.1
@@ -246,7 +248,9 @@ langchain==0.3.9
# langchain-community
# ragas
langchain-community==0.3.2
- # via ragas
+ # via
+ # -c requirements.txt
+ # ragas
langchain-core==0.3.21
# via
# -c requirements.txt
@@ -276,13 +280,19 @@ lazy-object-proxy==1.10.0
lupa==2.2
# via fakeredis
markdown-it-py==3.0.0
- # via rich
+ # via
+ # -c requirements.txt
+ # rich
markupsafe==2.1.5
# via jinja2
marshmallow==3.23.1
- # via dataclasses-json
+ # via
+ # -c requirements.txt
+ # dataclasses-json
mdurl==0.1.2
- # via markdown-it-py
+ # via
+ # -c requirements.txt
+ # markdown-it-py
multidict==6.0.5
# via
# -c requirements.txt
@@ -299,6 +309,7 @@ mypy-boto3-s3==1.34.65
# via boto3-stubs
mypy-extensions==1.0.0
# via
+ # -c requirements.txt
# -r requirements-dev.in
# black
# mypy
@@ -418,8 +429,10 @@ pydantic-core==2.23.4
# via
# -c requirements.txt
# pydantic
-pydantic-settings==2.6.1
- # via langchain-community
+pydantic-settings==2.7.0
+ # via
+ # -c requirements.txt
+ # langchain-community
pygments==2.18.0
# via rich
pysbd==0.3.4
@@ -660,7 +673,9 @@ typing-extensions==4.12.2
# typer
# typing-inspect
typing-inspect==0.9.0
- # via dataclasses-json
+ # via
+ # -c requirements.txt
+ # dataclasses-json
tzdata==2023.3
# via
# -c requirements.txt
diff --git a/requirements.in b/requirements.in
index 0e067e967257a..0b2197fa3945f 100644
--- a/requirements.in
+++ b/requirements.in
@@ -47,6 +47,7 @@ jsonref==1.1.0
kafka-python==2.0.2
kombu==5.3.2
langchain==0.3.9
+langchain-community==0.3.2
langchain-openai==0.2.11
langfuse==2.55.0
langgraph==0.2.56
@@ -114,3 +115,4 @@ xmlsec==1.3.13 # Do not change this version - it will break SAML
lxml==4.9.4 # Do not change this version - it will break SAML
grpcio~=1.63.2 # Version constrained so that `deepeval` can be installed in in dev
tenacity~=8.4.2 # Version constrained so that `deepeval` can be installed in in dev
+markdown-it-py~=3.0.0
diff --git a/requirements.txt b/requirements.txt
index 467cb5be0be3c..5ffa8f51524e0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,6 +14,7 @@ aiohttp==3.11.10
# aiobotocore
# geoip2
# langchain
+ # langchain-community
# s3fs
aioitertools==0.11.0
# via aiobotocore
@@ -135,6 +136,8 @@ cssselect==1.1.0
# via toronado
cssutils==1.0.2
# via toronado
+dataclasses-json==0.6.7
+ # via langchain-community
decorator==5.1.1
# via retry
defusedxml==0.6.0
@@ -350,10 +353,15 @@ kombu==5.3.2
# -r requirements.in
# celery
langchain==0.3.9
+ # via
+ # -r requirements.in
+ # langchain-community
+langchain-community==0.3.2
# via -r requirements.in
langchain-core==0.3.21
# via
# langchain
+ # langchain-community
# langchain-openai
# langchain-text-splitters
# langgraph
@@ -373,6 +381,7 @@ langgraph-sdk==0.1.43
langsmith==0.1.132
# via
# langchain
+ # langchain-community
# langchain-core
lxml==4.9.4
# via
@@ -387,8 +396,14 @@ lzstring==1.0.4
# via -r requirements.in
makefun==1.15.2
# via dlt
+markdown-it-py==3.0.0
+ # via -r requirements.in
+marshmallow==3.23.1
+ # via dataclasses-json
maxminddb==2.2.0
# via geoip2
+mdurl==0.1.2
+ # via markdown-it-py
mimesis==5.2.1
# via -r requirements.in
monotonic==1.5
@@ -404,6 +419,8 @@ multidict==6.0.5
# -r requirements.in
# aiohttp
# yarl
+mypy-extensions==1.0.0
+ # via typing-inspect
nanoid==2.0.0
# via -r requirements.in
natsort==8.4.0
@@ -414,6 +431,7 @@ numpy==1.23.3
# via
# -r requirements.in
# langchain
+ # langchain-community
# pandas
# pyarrow
# scikit-learn
@@ -444,6 +462,7 @@ packaging==24.1
# google-cloud-bigquery
# langchain-core
# langfuse
+ # marshmallow
# snowflake-connector-python
# sqlalchemy-bigquery
# webdriver-manager
@@ -522,8 +541,11 @@ pydantic==2.9.2
# langfuse
# langsmith
# openai
+ # pydantic-settings
pydantic-core==2.23.4
# via pydantic
+pydantic-settings==2.7.0
+ # via langchain-community
pyjwt==2.4.0
# via
# -r requirements.in
@@ -557,7 +579,9 @@ python-dateutil==2.8.2
# pendulum
# posthoganalytics
python-dotenv==0.21.0
- # via webdriver-manager
+ # via
+ # pydantic-settings
+ # webdriver-manager
python-statsd==2.1.0
# via django-statsd
python3-openid==3.1.0
@@ -582,6 +606,7 @@ pyyaml==6.0.1
# dlt
# drf-spectacular
# langchain
+ # langchain-community
# langchain-core
qrcode==7.4.2
# via django-two-factor-auth
@@ -605,6 +630,7 @@ requests==2.32.0
# google-cloud-bigquery
# infi-clickhouse-orm
# langchain
+ # langchain-community
# langfuse
# langsmith
# pdpyras
@@ -700,6 +726,7 @@ sqlalchemy==2.0.31
# via
# -r requirements.in
# langchain
+ # langchain-community
# snowflake-sqlalchemy
# sqlalchemy-bigquery
sqlalchemy-bigquery==1.11.0
@@ -726,6 +753,7 @@ tenacity==8.4.2
# celery-redbeat
# dlt
# langchain
+ # langchain-community
# langchain-core
threadpoolctl==3.3.0
# via scikit-learn
@@ -768,6 +796,9 @@ typing-extensions==4.12.2
# sqlalchemy
# stripe
# temporalio
+ # typing-inspect
+typing-inspect==0.9.0
+ # via dataclasses-json
tzdata==2023.3
# via
# celery