diff --git a/frontend/__snapshots__/components-cards-insight-card--insight-card--dark.png b/frontend/__snapshots__/components-cards-insight-card--insight-card--dark.png index cde356c5dc..45961de674 100644 Binary files a/frontend/__snapshots__/components-cards-insight-card--insight-card--dark.png and b/frontend/__snapshots__/components-cards-insight-card--insight-card--dark.png differ diff --git a/frontend/__snapshots__/components-cards-insight-card--insight-card--light.png b/frontend/__snapshots__/components-cards-insight-card--insight-card--light.png index cd1e0c92e5..8037ca29c5 100644 Binary files a/frontend/__snapshots__/components-cards-insight-card--insight-card--light.png and b/frontend/__snapshots__/components-cards-insight-card--insight-card--light.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--dark.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--dark.png index cceadf532f..748cac1f05 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--dark.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--light.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--light.png index d8f23903e8..ca2dbf2156 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--light.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight--light.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--dark.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--dark.png index b71791b88a..dbbc759fcf 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--dark.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--light.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--light.png index ef3bd801b0..9984ab47ea 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--light.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight--light.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--dark.png b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--dark.png index 223b0b5a5e..2ccee1187f 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--dark.png and b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--light.png b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--light.png index 0deca4f01a..b19b2d10de 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--light.png and b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight--light.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--dark.png b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--dark.png index b6bcfa7b85..18aaec8349 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--dark.png and b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--light.png b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--light.png index 3797590e0e..08eed45dd9 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--light.png and b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--estimated-query-execution-time-too-long--light.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--estimated-query-execution-time-too-long--light.png index e89baf38d6..208c245f98 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--estimated-query-execution-time-too-long--light.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--estimated-query-execution-time-too-long--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png index 85e97d00c2..208c245f98 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png differ diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--dark.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--dark.png index 8c9369352f..1b8c362150 100644 Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--dark.png and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--light.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--light.png index 7e1de913ec..c4e8cfbcc5 100644 Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--light.png and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-docs--light.png differ diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx index 9ebe3c966a..695185a65d 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx @@ -299,10 +299,10 @@ export function PersonsModal({ } function getSessionId(event: Record): 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] @@ -326,6 +326,7 @@ function processArrayInput(event: [], timestamp: any, output: string): Processed output: output, timestamp: timestamp, history: processedHistory, + highlight: hightlight, } } @@ -335,17 +336,20 @@ type ProcessedMessage = { timestamp: string history?: ProcessedMessage[] metadata?: Record + highlight?: boolean } function processStringInput(event: Record, 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 = {} + Object.keys(event).forEach((key) => { if (key.startsWith('user_') || key.startsWith('agent_')) { metadata[key] = event[key] @@ -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 { +function preProcessEvents(llmEvents: []): Record { /* Preprocess the events to segment them by session ID */ const segmentedDialogues = {} @@ -399,6 +408,24 @@ function preProcessEvents(llmEvents: []): Record { return segmentedDialogues } +function adjustHighlightedEvents(grpConvs: Record>): 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) => void @@ -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 @@ -557,10 +584,10 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A
- {pluralize(Object.keys(segmentedConvs).length, 'matched session')} + {pluralize(Object.keys(grpConvs).length, 'matched session')}
- {Object.entries(segmentedConvs).map(([sessionId, conversation], index) => ( + {Object.entries(grpConvs).map(([sessionId, conversation], index) => ( + 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 /> ))}
@@ -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 }[] + conversation: ProcessedMessage[] expand: boolean setBorder?: boolean } @@ -617,7 +645,6 @@ export function ConvRow({ convId, conversation, expand, setBorder }: ConvRowProp const handleRowClick = (): void => { setExpanded(!expanded) } - return (
))} @@ -670,13 +698,23 @@ interface TaskProps { expandHistory?: boolean isTask?: boolean metadata?: Record + 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 = {} + const agent_metadata: Record = {} if (metadata) { Object.keys(metadata).forEach((key) => { @@ -695,7 +733,7 @@ export function Task({ input, output, timestamp, history, expandHistory, isTask, } return ( -
+
{timestamp && (
{new Date(timestamp).toLocaleString()} @@ -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 = {} if (metadata) { Object.keys(metadata).forEach((key) => { if (metadata[key] !== 'OTHER') { diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss index 9562c807be..055102356f 100644 --- a/frontend/src/styles/global.scss +++ b/frontend/src/styles/global.scss @@ -743,3 +743,7 @@ body { background-color: var(--secondary-3000-light); border-radius: 1rem; } + +.border-gold { + border: 2px solid #e4a604; +} diff --git a/posthog/api/person.py b/posthog/api/person.py index f9a7060162..e3a301a48c 100644 --- a/posthog/api/person.py +++ b/posthog/api/person.py @@ -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 @@ -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 diff --git a/posthog/api/utils.py b/posthog/api/utils.py index a26e6f65b5..fe677866e4 100644 --- a/posthog/api/utils.py +++ b/posthog/api/utils.py @@ -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 diff --git a/posthog/queries/actor_base_query.py b/posthog/queries/actor_base_query.py index 6757a14f1c..c2e498550c 100644 --- a/posthog/queries/actor_base_query.py +++ b/posthog/queries/actor_base_query.py @@ -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]) @@ -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