Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 59 additions & 21 deletions frontend/src/scenes/trends/persons-modal/PersonsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,10 @@ export function PersonsModal({
}

function getSessionId(event: Record<string, string>): string {
return event['$session_id'] ? event['$session_id'] : ``
return event['$session_id'] ? event['$session_id'] : `${event['id']}`
}

function processArrayInput(event: [], timestamp: any, output: string): ProcessedMessage {
function processArrayInput(event: [], timestamp: any, output: string, hightlight: boolean): ProcessedMessage {
// This function processes the array input when a session ID is present,

const lastInput = event[event.length - 1]
Expand All @@ -326,6 +326,7 @@ function processArrayInput(event: [], timestamp: any, output: string): Processed
output: output,
timestamp: timestamp,
history: processedHistory,
highlight: hightlight,
}
}

Expand All @@ -335,17 +336,20 @@ type ProcessedMessage = {
timestamp: string
history?: ProcessedMessage[]
metadata?: Record<string, any>
highlight?: boolean
}

function processStringInput(event: Record<string, any>, allEvents: []): ProcessedMessage {
const msg = {
const msg: ProcessedMessage = {
input: event['$llm_input'],
timestamp: event['timestamp'],
output: event['$llm_output'],
highlight: event['highlight'] ? true : false,
}

// add metadata to the message
const metadata = {}
const metadata: Record<string, any> = {}

Object.keys(event).forEach((key) => {
if (key.startsWith('user_') || key.startsWith('agent_')) {
metadata[key] = event[key]
Expand Down Expand Up @@ -380,14 +384,19 @@ function addTaskToDialogues(
}
let task = null
if (Array.isArray(event['$llm_input'])) {
task = processArrayInput(event['$llm_input'] as [], event['timestamp'], event['$llm_output'] as string)
task = processArrayInput(
event['$llm_input'] as [],
event['timestamp'],
event['$llm_output'] as string,
event['highlight'] as boolean
)
} else if (typeof event['$llm_input'] === 'string') {
task = processStringInput(event, llmEvents)
}
dialogues[sessionId].push(task)
}

function preProcessEvents(llmEvents: []): Record<string, unknown> {
function preProcessEvents(llmEvents: []): Record<string, any> {
/* Preprocess the events to segment them by session ID */
const segmentedDialogues = {}

Expand All @@ -399,6 +408,24 @@ function preProcessEvents(llmEvents: []): Record<string, unknown> {
return segmentedDialogues
}

function adjustHighlightedEvents(grpConvs: Record<string, Array<any>>): void {
// If every conversation task matches a filter or just a single task, no need to highlight anything
for (const convKey in grpConvs) {
if (grpConvs.hasOwnProperty(convKey)) {
const events = grpConvs[convKey]

const allHighlightTrue = events.every((event) => event.highlight)

// If all events have highlight set to true, set highlight to false for all events
if (allHighlightTrue) {
events.forEach((event) => {
event.highlight = false
})
}
}
}
}

interface ActorRowProps {
actor: ActorType
onOpenRecording: (sessionRecording: Pick<SessionRecordingType, 'id' | 'matching_events'>) => void
Expand All @@ -409,15 +436,15 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A
const [expanded, setExpanded] = useState(false)

const { ['$llm-events']: convs, ...remaining_props } = actor.properties
let segmentedConvs = {}
let grpConvs = {}

if (convs) {
// @ts-expect-error
segmentedConvs = preProcessEvents(convs, actor.distinct_ids[0])
grpConvs = preProcessEvents(convs, actor.distinct_ids[0])
adjustHighlightedEvents(grpConvs)
}

const [tab, setTab] = useState('properties')
const name = isGroupType(actor) ? groupDisplayId(actor.group_key, actor.properties) : asDisplay(actor)

const onOpenRecordingClick = (): void => {
if (!actor.matched_recordings) {
return
Expand Down Expand Up @@ -557,10 +584,10 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A
<div className="p-2 space-y-2 font-medium mt-1">
<div className="flex justify-between items-center px-2">
<span>
{pluralize(Object.keys(segmentedConvs).length, 'matched session')}
{pluralize(Object.keys(grpConvs).length, 'matched session')}
</span>
</div>
{Object.entries(segmentedConvs).map(([sessionId, conversation], index) => (
{Object.entries(grpConvs).map(([sessionId, conversation], index) => (
<ConvRow
key={index}
convId={`Session - ${sessionId}`}
Expand All @@ -571,10 +598,11 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A
timestamp: string
history: []
metadata: Record<string, any>
hightlight: boolean
}[]
}
expand={Object.keys(segmentedConvs).length === 1}
setBorder={index !== Object.keys(segmentedConvs).length - 1} // Don't set border for the last conversation
expand={Object.keys(grpConvs).length === 1}
setBorder={index !== Object.keys(grpConvs).length - 1} // Don't set border for the last conversation
/>
))}
</div>
Expand Down Expand Up @@ -602,7 +630,7 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A

interface ConvRowProps {
convId: string
conversation: { input: string; output: string; timestamp: string; history: []; metadata: Record<string, any> }[]
conversation: ProcessedMessage[]
expand: boolean
setBorder?: boolean
}
Expand All @@ -617,7 +645,6 @@ export function ConvRow({ convId, conversation, expand, setBorder }: ConvRowProp
const handleRowClick = (): void => {
setExpanded(!expanded)
}

return (
<div className="pt">
<div
Expand Down Expand Up @@ -653,6 +680,7 @@ export function ConvRow({ convId, conversation, expand, setBorder }: ConvRowProp
history={task.history}
isTask={true}
metadata={task.metadata}
highlight={task.highlight}
/>
</div>
))}
Expand All @@ -670,13 +698,23 @@ interface TaskProps {
expandHistory?: boolean
isTask?: boolean
metadata?: Record<string, any>
highlight?: boolean
}

export function Task({ input, output, timestamp, history, expandHistory, isTask, metadata }: TaskProps): JSX.Element {
export function Task({
input,
output,
timestamp,
history,
expandHistory,
isTask,
metadata,
highlight,
}: TaskProps): JSX.Element {
const user_class = 'user-avatar-div'
const agent_class = 'agent-avatar-div'
const user_metadata = {}
const agent_metadata = {}
const user_metadata: Record<string, any> = {}
const agent_metadata: Record<string, any> = {}

if (metadata) {
Object.keys(metadata).forEach((key) => {
Expand All @@ -695,7 +733,7 @@ export function Task({ input, output, timestamp, history, expandHistory, isTask,
}

return (
<div className={isTask ? 'border' : ''}>
<div className={`${isTask ? 'border' : ''} ${highlight ? 'border-gold' : ''}`}>
{timestamp && (
<div className={`flex justify-between pr-2 pl-2 ${expanded ? 'border-b' : ''}`}>
<span className="text-muted-alt">{new Date(timestamp).toLocaleString()}</span>
Expand Down Expand Up @@ -743,7 +781,7 @@ interface TaskRowProps {
addPaddingBot?: boolean
}
export function TaskRow({ role, avatarClass, utterance, isTask, metadata, addPaddingBot }: TaskRowProps): JSX.Element {
const clean_metadata = {}
const clean_metadata: Record<string, any> = {}
if (metadata) {
Object.keys(metadata).forEach((key) => {
if (metadata[key] !== 'OTHER') {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -743,3 +743,7 @@ body {
background-color: var(--secondary-3000-light);
border-radius: 1rem;
}

.border-gold {
border: 2px solid #e4a604;
}
32 changes: 32 additions & 0 deletions posthog/api/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,36 @@ def funnel(self, request: request.Request, **kwargs) -> response.Response:

return self._respond_with_cached_results(self.calculate_funnel_persons(request))

def tag_events_highlight(self, actors: List[Person], llm_results):
all_highlight_events = []
for actor in actors:
all_highlight_events.extend(actor["highlight_events"])
actor.pop("highlight_events")

for event in llm_results:
event_id = event.get("id")
if event_id in all_highlight_events:
event["highlight"] = True

return llm_results

def filter_llm_results(self, actors: List[Person], llm_results: Dict) -> List[Dict]:
# Filters conversations by session or event UUIDs to display in the llm-events tab.

all_matched_sessions = []
for actor in actors:
all_matched_sessions.extend(actor["matched_sessions"])

filtered_results = []
for event in llm_results:
# events without $session_id prop are treated as a sessions
# since they appear as a standalone conversation in the llm-events tab
session_id = event["properties"].get("$session_id", event.get("id"))
if session_id in all_matched_sessions:
filtered_results.append(event)

return filtered_results

def extend_actors_with_llm_events(self, filter, serialized_actors, request):
from posthog.models.event.query_event_list import query_events_list
from posthog.models.event.util import ClickhouseEventSerializer
Expand Down Expand Up @@ -733,6 +763,8 @@ def extend_actors_with_llm_events(self, filter, serialized_actors, request):
query_result[0:10000],
many=True,
).data
llm_ev_result = self.filter_llm_results(serialized_actors, llm_ev_result)
llm_ev_result = self.tag_events_highlight(serialized_actors, llm_ev_result)
serialized_actors = set_people_events(serialized_actors, llm_ev_result)

return serialized_actors
Expand Down
2 changes: 2 additions & 0 deletions posthog/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ def set_people_events(s_people, s_events):
or k in ["$session_id", "input", "output"]
}
props["timestamp"] = ev["timestamp"]
props["id"] = ev["id"]
props["highlight"] = ev.get("highlight")
grouped_events.setdefault(person_id, []).append(props)

# set the person event list in a property
Expand Down
25 changes: 24 additions & 1 deletion posthog/queries/actor_base_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,32 @@ def add_matched_recordings_to_serialized_actors(
).values_list("session_id", flat=True)
)
session_ids_with_recordings = session_ids_with_all_recordings.difference(session_ids_with_deleted_recordings)

matched_session_ids_by_actor_id: Dict[Union[uuid.UUID, str], Set[str]] = {
actor["id"]: set() for actor in serialized_actors
}
matched_events_ids_by_actor_id: Dict[Union[uuid.UUID, str], Set[str]] = {
actor["id"]: set() for actor in serialized_actors
}
matched_highlight_events_ids: Dict[Union[uuid.UUID, str], Set[str]] = {
actor["id"]: set() for actor in serialized_actors
}
matched_recordings_by_actor_id: Dict[Union[uuid.UUID, str], List[MatchedRecording]] = {}

for row in raw_result:
actor_id = row[0]
recording_events_by_session_id: Dict[str, List[EventInfoForRecording]] = {}
if len(row) > session_events_column_index - 1:
for event in row[session_events_column_index]:
event_id = event[1]
event_session_id = event[2]
# always add the matched events to highlights dict
matched_highlight_events_ids[actor_id].add(str(event_id))
# if the event has a session ID, add it to matched_session_ids_by_actor_id
if event_session_id:
matched_session_ids_by_actor_id[actor_id].add(str(event_session_id))
else:
# else, add the event ID to matched_events_ids_by_actor_id
matched_events_ids_by_actor_id[actor_id].add(str(event_id))
if event_session_id and event_session_id in session_ids_with_recordings:
recording_events_by_session_id.setdefault(event_session_id, []).append(
EventInfoForRecording(timestamp=event[0], uuid=event[1], window_id=event[3])
Expand All @@ -215,6 +234,10 @@ def add_matched_recordings_to_serialized_actors(
serialized_actors_with_recordings = []
for actor in serialized_actors:
actor["matched_recordings"] = matched_recordings_by_actor_id[actor["id"]]
actor["matched_sessions"] = list(matched_session_ids_by_actor_id[actor["id"]]) + list(
matched_events_ids_by_actor_id[actor["id"]]
)
actor["highlight_events"] = list(matched_highlight_events_ids[actor["id"]])
serialized_actors_with_recordings.append(actor)

return serialized_actors_with_recordings
Expand Down