Skip to content

Commit

Permalink
feat: rejig session summary (#25290)
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra authored Oct 1, 2024
1 parent 4008edf commit 6d8bc6b
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 153 deletions.
7 changes: 1 addition & 6 deletions ee/session_recordings/ai/embeddings_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from posthog.session_recordings.queries.session_replay_events import SessionReplayEvents
from ee.session_recordings.ai.utils import (
SessionSummaryPromptData,
reduce_elements_chain,
simplify_window_id,
format_dates,
collapse_sequence_of_events,
Expand Down Expand Up @@ -263,11 +262,7 @@ def prepare(session_id: str, team: Team):
processed_sessions = collapse_sequence_of_events(
only_pageview_urls(
format_dates(
reduce_elements_chain(
simplify_window_id(
SessionSummaryPromptData(columns=session_events[0], results=session_events[1])
)
),
simplify_window_id(SessionSummaryPromptData(columns=session_events[0], results=session_events[1])),
start=datetime.datetime(1970, 1, 1, tzinfo=pytz.UTC), # epoch timestamp
)
)
Expand Down
32 changes: 3 additions & 29 deletions ee/session_recordings/ai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from typing import Any

from posthog.models.element import chain_to_elements
from hashlib import shake_256


Expand All @@ -18,6 +17,9 @@ class SessionSummaryPromptData:
# we replace URLs with a placeholder and then pass this mapping of placeholder to URL into the prompt
url_mapping: dict[str, str] = dataclasses.field(default_factory=dict)

# one for each result in results
processed_elements_chain: list[dict] = dataclasses.field(default_factory=list)

def is_empty(self) -> bool:
return not self.columns or not self.results

Expand All @@ -28,34 +30,6 @@ def column_index(self, column: str) -> int | None:
return None


def reduce_elements_chain(session_events: SessionSummaryPromptData) -> SessionSummaryPromptData:
if session_events.is_empty():
return session_events

# find elements_chain column index
elements_chain_index = session_events.column_index("elements_chain")

reduced_results = []
for result in session_events.results:
if elements_chain_index is None:
reduced_results.append(result)
continue

elements_chain: str | None = result[elements_chain_index]
if not elements_chain:
reduced_results.append(result)
continue

# the elements chain has lots of information that we don't need
elements = [e for e in chain_to_elements(elements_chain) if e.tag_name in e.USEFUL_ELEMENTS]

result_list = list(result)
result_list[elements_chain_index] = [{"tag": e.tag_name, "text": e.text, "href": e.href} for e in elements]
reduced_results.append(result_list)

return dataclasses.replace(session_events, results=reduced_results)


def simplify_window_id(session_events: SessionSummaryPromptData) -> SessionSummaryPromptData:
if session_events.is_empty():
return session_events
Expand Down
18 changes: 7 additions & 11 deletions ee/session_recordings/session_summary/summarize_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from ee.session_recordings.ai.utils import (
SessionSummaryPromptData,
reduce_elements_chain,
simplify_window_id,
deduplicate_urls,
format_dates,
Expand Down Expand Up @@ -81,11 +80,7 @@ def summarize_recording(recording: SessionRecording, user: User, team: Team):
prompt_data = deduplicate_urls(
collapse_sequence_of_events(
format_dates(
reduce_elements_chain(
simplify_window_id(
SessionSummaryPromptData(columns=session_events[0], results=session_events[1])
)
),
simplify_window_id(SessionSummaryPromptData(columns=session_events[0], results=session_events[1])),
start=start_time,
)
)
Expand All @@ -95,9 +90,7 @@ def summarize_recording(recording: SessionRecording, user: User, team: Team):

with timer("openai_completion"):
result = openai.chat.completions.create(
# model="gpt-4-1106-preview", # allows 128k tokens
# model="gpt-4", # allows 8k tokens
model="gpt-4o", # allows 128k tokens
model="gpt-4o-mini", # allows 128k tokens
temperature=0.7,
messages=[
{
Expand All @@ -107,6 +100,7 @@ def summarize_recording(recording: SessionRecording, user: User, team: Team):
We also gather events that occur like mouse clicks and key presses.
You write two or three sentence concise and simple summaries of those sessions based on a prompt.
You are more likely to mention errors or things that look like business success such as checkout events.
You always try to make the summary actionable. E.g. mentioning what someone clicked on, or summarizing errors they experienced.
You don't help with other knowledge.""",
},
{
Expand All @@ -117,22 +111,24 @@ def summarize_recording(recording: SessionRecording, user: User, team: Team):
{
"role": "user",
"content": f"""
URLs associated with the events can be found in this mapping {prompt_data.url_mapping}.
URLs associated with the events can be found in this mapping {prompt_data.url_mapping}. You never refer to URLs by their placeholder. Always refer to the URL with the simplest version e.g. posthog.com or posthog.com/replay
""",
},
{
"role": "user",
"content": f"""the session events I have are {prompt_data.results}.
with columns {prompt_data.columns}.
they give an idea of what happened and when,
if present the elements_chain extracted from the html can aid in understanding
if present the elements_chain_texts, elements_chain_elements, and elements_chain_href extracted from the html can aid in understanding what a user interacted with
but should not be directly used in your response""",
},
{
"role": "user",
"content": """
generate a two or three sentence summary of the session.
only summarize, don't offer advice.
use as concise and simple language as is possible.
Dont' refer to the session length unless it is notable for some reason.
assume a reading age of around 12 years old.
generate no text other than the summary.""",
},
Expand Down
45 changes: 43 additions & 2 deletions frontend/src/scenes/session-recordings/player/playerMetaLogic.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { eventWithTime } from '@rrweb/types'
import { connect, kea, key, listeners, path, props, selectors } from 'kea'
import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import api from 'lib/api'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { lemonToast } from 'lib/lemon-ui/LemonToast'
import { getCoreFilterDefinition } from 'lib/taxonomy'
import { ceilMsToClosestSecond, findLastIndex, objectsEqual } from 'lib/utils'
import posthog from 'posthog-js'
import { countryCodeToName } from 'scenes/insights/views/WorldMap'
import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic'
import {
Expand All @@ -27,6 +31,10 @@ const browserPropertyKeys = ['$geoip_country_code', '$browser', '$device_type',
const mobilePropertyKeys = ['$geoip_country_code', '$device_type', '$os_name']
const recordingPropertyKeys = ['click_count', 'keypress_count', 'console_error_count'] as const

export interface SessionSummaryResponse {
content: string
}

export const playerMetaLogic = kea<playerMetaLogicType>([
path((key) => ['scenes', 'session-recordings', 'player', 'playerMetaLogic', key]),
props({} as SessionRecordingPlayerLogicProps),
Expand Down Expand Up @@ -55,6 +63,32 @@ export const playerMetaLogic = kea<playerMetaLogicType>([
['maybeLoadPropertiesForSessions'],
],
})),
actions({
sessionSummaryFeedback: (feedback: 'good' | 'bad') => ({ feedback }),
}),
reducers(() => ({
summaryHasHadFeedback: [
false,
{
sessionSummaryFeedback: () => true,
},
],
})),
loaders(({ props }) => ({
sessionSummary: {
summarizeSession: async (): Promise<SessionSummaryResponse | null> => {
const id = props.sessionRecordingId || props.sessionRecordingData?.sessionRecordingId
if (!id) {
return null
}
const response = await api.recordings.summarize(id)
if (!response.content) {
lemonToast.warning('Unable to load session summary')
}
return { content: response.content }
},
},
})),
selectors(() => ({
sessionPerson: [
(s) => [s.sessionPlayerData],
Expand Down Expand Up @@ -199,11 +233,18 @@ export const playerMetaLogic = kea<playerMetaLogicType>([
},
],
})),
listeners(({ actions, values }) => ({
listeners(({ actions, values, props }) => ({
loadRecordingMetaSuccess: () => {
if (values.sessionPlayerMetaData && !values.recordingPropertiesLoading) {
actions.maybeLoadPropertiesForSessions([values.sessionPlayerMetaData])
}
},
sessionSummaryFeedback: ({ feedback }) => {
posthog.capture('session summary feedback', {
feedback,
session_summary: values.sessionSummary,
summarized_session_id: props.sessionRecordingId,
})
},
})),
])
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PersonDisplay } from '@posthog/apps-common'
import { useValues } from 'kea'
import { PropertiesTable } from 'lib/components/PropertiesTable'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { PlayerSidebarSessionSummary } from 'scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary'

import { PropertyDefinitionType } from '~/types'

Expand All @@ -16,6 +17,7 @@ export function PlayerSidebarOverviewTab(): JSX.Element {
return (
<div className="flex flex-col overflow-auto bg-bg-3000">
<PlayerSidebarOverviewGrid />
<PlayerSidebarSessionSummary />
<div className="font-bold bg-bg-light px-2 border-b py-3">
<PersonDisplay person={sessionPerson} withIcon noPopover />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { IconMagicWand, IconThumbsDown, IconThumbsUp } from '@posthog/icons'
import { useActions, useValues } from 'kea'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FEATURE_FLAGS } from 'lib/constants'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic'
import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'

function SessionSummary(): JSX.Element {
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { sessionSummary, summaryHasHadFeedback } = useValues(playerMetaLogic(logicProps))
const { sessionSummaryFeedback } = useActions(playerMetaLogic(logicProps))

return (
<div>
{sessionSummary.content}
<LemonDivider dashed={true} />
<div className="text-right">
<p>Is this a good summary?</p>
<div className="flex flex-row gap-2 justify-end">
<LemonButton
size="xsmall"
type="primary"
icon={<IconThumbsUp />}
disabledReason={summaryHasHadFeedback ? 'Thanks for your feedback!' : undefined}
onClick={() => {
sessionSummaryFeedback('good')
}}
/>
<LemonButton
size="xsmall"
type="primary"
icon={<IconThumbsDown />}
disabledReason={summaryHasHadFeedback ? 'Thanks for your feedback!' : undefined}
onClick={() => {
sessionSummaryFeedback('bad')
}}
/>
</div>
</div>
</div>
)
}

function LoadSessionSummaryButton(): JSX.Element {
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { sessionSummaryLoading } = useValues(playerMetaLogic(logicProps))
const { summarizeSession } = useActions(playerMetaLogic(logicProps))

return (
<LemonButton
size="small"
type="primary"
icon={<IconMagicWand />}
fullWidth={true}
data-attr="load-session-summary"
disabledReason={sessionSummaryLoading ? 'Loading...' : undefined}
onClick={summarizeSession}
>
Use AI to summarise this session
</LemonButton>
)
}

export function PlayerSidebarSessionSummary(): JSX.Element | null {
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { sessionSummary, sessionSummaryLoading } = useValues(playerMetaLogic(logicProps))

return (
<>
<FlaggedFeature flag={FEATURE_FLAGS.AI_SESSION_SUMMARY} match={true}>
<div className="rounded border bg-bg-light m-2 px-2 py-1">
<h2>AI Session Summary</h2>
{sessionSummaryLoading ? (
<>
Thinking... <Spinner />{' '}
</>
) : sessionSummary ? (
<SessionSummary />
) : (
<LoadSessionSummaryButton />
)}
</div>
</FlaggedFeature>
</>
)
}
Loading

0 comments on commit 6d8bc6b

Please sign in to comment.