/** * @description STDM query service for the agentforce-optimize Claude Code skill. * Queries the Session Trace Data Model (STDM) in Data Cloud to retrieve * session traces, conversation turns, messages, and steps for issue analysis. * * Deployed once per org by the agentforce-optimize skill (Phase 1 setup). * All public methods accept dataSpaceName so no Data Space is hardcoded. * * Methods: * findSessions(dataSpaceName, startIso, endIso, maxRows) → JSON List * findSessions(dataSpaceName, startIso, endIso, maxRows, agentName) → JSON List * getConversationDetails(dataSpaceName, sessionId) → JSON ConversationData * getMultipleConversationDetails(dataSpaceName, sessionIds) → JSON List * getLlmStepDetails(dataSpaceName, stepIds) → JSON List * getMomentInsights(dataSpaceName, sessionIds) → JSON List * getAggregatedMetrics(dataSpaceName, startIso, endIso, maxRows, agentName) → JSON AggregatedMetrics * runObservabilityQuery(List) → List (@InvocableMethod) */ public with sharing class AgentforceOptimizeService { // ========================================================================= // Output wrappers // ========================================================================= /** Lightweight session record returned by findSessions(). */ public class SessionSummary { public String session_id; public String start_time; public String end_time; public String channel; public Long duration_ms; /** How the session ended: e.g. USER_ENDED, AGENT_ENDED (null = in progress or not recorded) */ public String end_type; } /** A single user/agent message within a turn. */ public class MessageData { public String message_id; /** 'Input' (user) or 'Output' (agent) — raw STDM value */ public String message_type; public String text; public String sent_at; } /** * A single internal step within a turn. * All issue-detection fields are included: * - error → non-null means ACTION_STEP failure (P1) * - pre_vars / post_vars → null delta means variable not captured (P2) * - duration_ms > 10 000 → slow action (P3) * - generation_id → non-null on LLM_STEP; use getLlmStepDetails() to get the prompt */ public class StepData { public String step_id; /** TOPIC_STEP | LLM_STEP | ACTION_STEP | SESSION_END | TRUST_GUARDRAILS_STEP */ public String step_type; public String name; public String start_time; public String end_time; public Long duration_ms; /** Raw input to the step (JSON for ACTION_STEP; Python dict string for LLM_STEP) */ public String input; /** Raw output from the step (JSON for ACTION_STEP; Python dict string for LLM_STEP) */ public String output; /** Non-null indicates the step threw an error (only ACTION_STEP counts toward action_error_count) */ public String error; /** Variable snapshot before this step (null when NOT_SET) */ public String pre_vars; /** Variable snapshot after this step (null when NOT_SET) */ public String post_vars; /** GenAiGeneration ID — non-null on LLM_STEP; pass to getLlmStepDetails() for full prompt/response */ public String generation_id; /** GenAiGatewayRequest ID — non-null on LLM_STEP; links to raw gateway request */ public String gateway_request_id; } /** * One conversational turn (AiAgentInteraction of type TURN). * Contains all messages and steps for that turn. */ public class TurnData { public String interaction_id; /** Subagent API name (stored as `topic` in STDM) — null/mismatch signals a misroute (P1) */ public String topic; public String start_time; public String end_time; public Long duration_ms; /** Telemetry trace ID for distributed tracing correlation */ public String telemetry_trace_id; public List messages; public List steps; public TurnData() { messages = new List(); steps = new List(); } } /** * Full conversation for one session: session header + ordered turns. * turn_count and action_error_count are pre-computed for quick triage. */ public class ConversationData { public String session_id; public String start_time; public String end_time; public String channel; public Long duration_ms; /** How the session ended (null = in progress or not recorded by Data Cloud) */ public String end_type; /** Session-level variable snapshot from ssot__VariableText__c (null when absent) */ public String session_variables; public Integer turn_count = 0; public Integer action_error_count = 0; public List turns; public ConversationData() { turns = new List(); } } /** * LLM step detail retrieved from Einstein Audit & Feedback DMOs. * Obtained by joining AiAgentInteractionStep with GenAIGeneration and GenAIGatewayRequest. */ public class LlmStepDetail { public String step_id; public String interaction_id; public String step_name; /** Full prompt text from GenAIGatewayRequest__dlm.prompt__c */ public String prompt; /** LLM response text from GenAIGeneration__dlm.responseText__c */ public String llm_response; public String generation_id; public String gateway_request_id; } /** A single intent moment within a session (from AiAgentMoment DMO). */ public class MomentData { public String moment_id; public String session_id; public String start_time; public String end_time; public Long duration_ms; public String request_summary; public String response_summary; public String agent_api_name; public String agent_version; /** Quality score 1-5 from AiAgentTagAssociation → AiAgentTag.Value */ public Integer quality_score; /** LLM-generated reasoning for the quality score */ public String quality_reasoning; public MomentData() {} } /** RAG quality metrics from the AiRetrieverQualityMetric DMO. */ public class RetrieverMetricData { public String metric_id; public String gateway_request_id; public String retriever_request_id; public String retriever_api_name; public String user_utterance; public Decimal faithfulness; public Decimal answer_relevance; public Decimal context_precision; } /** Per-session insights rollup (moments + retriever metrics). */ public class SessionInsights { public String session_id; public String start_time; public String end_time; public String end_type; public Long duration_ms; public Integer turn_count; public Integer moment_count; public Decimal avg_quality_score; public Integer action_error_count; public List moments; public List retriever_metrics; public String debug_message; public SessionInsights() { moments = new List(); retriever_metrics = new List(); } } /** Aggregated metrics across multiple sessions. */ public class AggregatedMetrics { public Integer total_sessions; public Integer total_moments; public Integer total_turns; public Decimal avg_quality_score; public Decimal avg_session_duration_sec; public Map end_type_counts; public Map top_intents; public Map quality_distribution; public Decimal abandonment_rate; public Decimal escalation_rate; public Decimal deflection_rate; public Decimal avg_faithfulness; public Decimal avg_answer_relevance; public Decimal avg_context_precision; public List unavailable_dmos; } // ========================================================================= // Public API // ========================================================================= /** * Find recent sessions within a date range, optionally filtered to a specific agent. * * Agent filtering tries two strategies in order: * 1. Direct: ssot__AiAgentApiName__c = agentApiName on the participant DMO (no SOQL needed) * 2. Fallback: GenAiPlannerDefinition (SOQL) → ssot__ParticipantId__c IN plannerIds * Both 15-char and 18-char ID formats are included to handle DMO inconsistency. * * If both strategies return empty, the query falls back to all sessions. * * @param dataSpaceName Data Cloud Data Space API name (discovered in Phase 0) * @param startIso ISO 8601 UTC start, e.g. '2025-03-01T00:00:00.000Z' * @param endIso ISO 8601 UTC end * @param maxRows Maximum sessions to return (e.g. 20) * @param agentApiName Agent display name / MasterLabel to filter by (null = all agents) * @return JSON-serialized List */ public static String findSessions(String dataSpaceName, String startIso, String endIso, Integer maxRows, String agentApiName) { // Step 1: Find sessions that have actual TURN interactions (skip empty preview sessions). // Empty sessions (sf agent preview, builder pings) create AiAgentSession + SESSION_END // interaction records but never TURN records. Querying for TURN directly ensures we only // return sessions with real conversation data. String turnSessionSql = 'SELECT DISTINCT ssot__AiAgentSessionId__c ' + 'FROM "ssot__AiAgentInteraction__dlm" ' + 'WHERE ssot__AiAgentInteractionType__c = \'TURN\' ' + ' AND ssot__StartTimestamp__c >= \'' + startIso + '\' ' + ' AND ssot__StartTimestamp__c <= \'' + endIso + '\''; ConnectApi.CdpQueryOutputV2 turnResult = runQuery(turnSessionSql, dataSpaceName); Set sessionsWithTurns = new Set(); if (turnResult != null && turnResult.data != null) { for (ConnectApi.CdpQueryV2Row row : turnResult.data) { String sid = col(row.rowData, 0); if (sid != null) sessionsWithTurns.add(sid); } } System.debug(LoggingLevel.DEBUG, 'Sessions with turns in date range: ' + sessionsWithTurns.size()); // Step 2: Build agent filter (optional) String sessionFilter = ''; if (String.isNotBlank(agentApiName)) { // Strategy 1: filter directly by ssot__AiAgentApiName__c (simplest, no SOQL) String partSqlByName = 'SELECT ssot__AiAgentSessionId__c ' + 'FROM "ssot__AiAgentSessionParticipant__dlm" ' + 'WHERE ssot__AiAgentApiName__c = \'' + String.escapeSingleQuotes(agentApiName) + '\''; ConnectApi.CdpQueryOutputV2 nameResult = runQuery(partSqlByName, dataSpaceName); List sessionIds = extractSessionIds(nameResult); if (!sessionIds.isEmpty()) { sessionFilter = ' AND ssot__Id__c IN (\'' + String.join(sessionIds, '\',\'') + '\') '; System.debug(LoggingLevel.DEBUG, 'Agent filter (AiAgentApiName): ' + sessionIds.size() + ' session(s) for agent: ' + agentApiName); } else { // Strategy 2: GenAiPlannerDefinition SOQL → ssot__ParticipantId__c System.debug(LoggingLevel.DEBUG, 'AiAgentApiName filter returned no sessions; trying GenAiPlannerDefinition fallback'); List plannerIds = resolvePlannerIds(agentApiName); if (!plannerIds.isEmpty()) { String pInClause = '(\'' + String.join(plannerIds, '\',\'') + '\')'; String partSqlById = 'SELECT ssot__AiAgentSessionId__c ' + 'FROM "ssot__AiAgentSessionParticipant__dlm" ' + 'WHERE ssot__ParticipantId__c IN ' + pInClause; ConnectApi.CdpQueryOutputV2 idResult = runQuery(partSqlById, dataSpaceName); List sessionIds2 = extractSessionIds(idResult); if (!sessionIds2.isEmpty()) { sessionFilter = ' AND ssot__Id__c IN (\'' + String.join(sessionIds2, '\',\'') + '\') '; System.debug(LoggingLevel.DEBUG, 'Agent filter (PlannerIds): ' + plannerIds.size() + ' planner(s), ' + sessionIds2.size() + ' session(s)'); } else { System.debug(LoggingLevel.WARN, 'No sessions found for agent: ' + agentApiName + ' — returning all sessions'); } } else { System.debug(LoggingLevel.WARN, 'Agent not found: ' + agentApiName + ' — returning sessions for all agents'); } } } // Step 3: Query sessions, preferring those with actual turns String turnFilter = ''; if (!sessionsWithTurns.isEmpty()) { List turnList = new List(sessionsWithTurns); turnFilter = ' AND ssot__Id__c IN (\'' + String.join(turnList, '\',\'') + '\') '; } String sql = 'SELECT ssot__Id__c, ssot__StartTimestamp__c, ssot__EndTimestamp__c, ' + ' ssot__AiAgentChannelType__c, ssot__AiAgentSessionEndType__c ' + 'FROM "ssot__AiAgentSession__dlm" ' + 'WHERE ssot__StartTimestamp__c >= \'' + startIso + '\' ' + ' AND ssot__StartTimestamp__c <= \'' + endIso + '\' ' + sessionFilter + turnFilter + 'ORDER BY ssot__StartTimestamp__c DESC ' + 'LIMIT ' + maxRows; ConnectApi.CdpQueryOutputV2 result = runQuery(sql, dataSpaceName); List sessions = new List(); if (result != null && result.data != null) { for (ConnectApi.CdpQueryV2Row row : result.data) { SessionSummary s = new SessionSummary(); s.session_id = col(row.rowData, 0); s.start_time = col(row.rowData, 1); s.end_time = col(row.rowData, 2); s.channel = col(row.rowData, 3); s.end_type = notSet(col(row.rowData, 4)); s.duration_ms = durationMs(s.start_time, s.end_time); sessions.add(s); } } return JSON.serialize(sessions); } /** * Overload that queries sessions for all agents (no agent filter). * Kept for backwards compatibility; prefer the 5-argument version. */ public static String findSessions(String dataSpaceName, String startIso, String endIso, Integer maxRows) { return findSessions(dataSpaceName, startIso, endIso, maxRows, null); } /** * Retrieve full conversation details for a single session. * Fetches interactions, messages, and steps (with error/variable/generation fields). * * @param dataSpaceName Data Cloud Data Space API name * @param sessionId ssot__Id__c of the AiAgentSession * @return JSON-serialized ConversationData */ public static String getConversationDetails(String dataSpaceName, String sessionId) { if (String.isBlank(sessionId)) return null; ConversationData convo = new ConversationData(); convo.session_id = sessionId; // --- Session header --- String sessionSql = 'SELECT ssot__StartTimestamp__c, ssot__EndTimestamp__c, ssot__AiAgentChannelType__c, ' + ' ssot__AiAgentSessionEndType__c, ssot__VariableText__c ' + 'FROM "ssot__AiAgentSession__dlm" ' + 'WHERE ssot__Id__c = \'' + String.escapeSingleQuotes(sessionId) + '\''; ConnectApi.CdpQueryOutputV2 sessionResult = runQuery(sessionSql, dataSpaceName); if (sessionResult != null && sessionResult.data != null && !sessionResult.data.isEmpty()) { List r = sessionResult.data[0].rowData; convo.start_time = col(r, 0); convo.end_time = col(r, 1); convo.channel = col(r, 2); convo.end_type = notSet(col(r, 3)); convo.session_variables = notSet(col(r, 4)); convo.duration_ms = durationMs(convo.start_time, convo.end_time); } // --- Interactions (turns) --- // ssot__TopicApiName__c included for misroute detection // ssot__TelemetryTraceId__c included for distributed tracing String interSql = 'SELECT ssot__Id__c, ssot__TopicApiName__c, ssot__AiAgentInteractionType__c, ' + ' ssot__StartTimestamp__c, ssot__EndTimestamp__c, ssot__TelemetryTraceId__c ' + 'FROM "ssot__AiAgentInteraction__dlm" ' + 'WHERE ssot__AiAgentSessionId__c = \'' + String.escapeSingleQuotes(sessionId) + '\' ' + 'ORDER BY ssot__StartTimestamp__c'; ConnectApi.CdpQueryOutputV2 interResult = runQuery(interSql, dataSpaceName); if (interResult == null || interResult.data == null || interResult.data.isEmpty()) { return JSON.serialize(convo); } Map turnById = new Map(); List turnIds = new List(); for (ConnectApi.CdpQueryV2Row row : interResult.data) { String interType = col(row.rowData, 2); // SESSION_END is a meta interaction, not a user turn if ('SESSION_END'.equalsIgnoreCase(interType)) continue; TurnData t = new TurnData(); t.interaction_id = col(row.rowData, 0); t.topic = col(row.rowData, 1); t.start_time = col(row.rowData, 3); t.end_time = col(row.rowData, 4); t.duration_ms = durationMs(t.start_time, t.end_time); t.telemetry_trace_id = notSet(col(row.rowData, 5)); turnById.put(t.interaction_id, t); turnIds.add(t.interaction_id); convo.turns.add(t); } convo.turn_count = convo.turns.size(); if (turnIds.isEmpty()) return JSON.serialize(convo); String inClause = '(\'' + String.join(turnIds, '\',\'') + '\')'; // --- Messages --- String msgSql = 'SELECT ssot__Id__c, ssot__AiAgentInteractionId__c, ' + ' ssot__AiAgentInteractionMessageType__c, ssot__ContentText__c, ' + ' ssot__MessageSentTimestamp__c ' + 'FROM "ssot__AiAgentInteractionMessage__dlm" ' + 'WHERE ssot__AiAgentInteractionId__c IN ' + inClause + ' ' + 'ORDER BY ssot__MessageSentTimestamp__c'; ConnectApi.CdpQueryOutputV2 msgResult = runQuery(msgSql, dataSpaceName); if (msgResult != null && msgResult.data != null) { for (ConnectApi.CdpQueryV2Row row : msgResult.data) { TurnData t = turnById.get(col(row.rowData, 1)); if (t == null) continue; MessageData m = new MessageData(); m.message_id = col(row.rowData, 0); m.message_type = col(row.rowData, 2); // 'Input' or 'Output' m.text = col(row.rowData, 3); m.sent_at = col(row.rowData, 4); t.messages.add(m); } } // Infer message types when ssot__AiAgentInteractionMessageType__c is null. // In STDM, messages within a turn alternate: user Input first, then agent Output. // If ALL messages in a turn have null type, assign by position (odd=Input, even=Output). for (TurnData t : convo.turns) { Boolean anyNull = false; Boolean allNull = true; for (MessageData m : t.messages) { if (m.message_type == null) { anyNull = true; } else { allNull = false; } } if (anyNull && allNull) { // All null — infer from position: 1st=Input, 2nd=Output, 3rd=Input, ... for (Integer i = 0; i < t.messages.size(); i++) { t.messages[i].message_type = (Math.mod(i, 2) == 0) ? 'Input' : 'Output'; } } else if (anyNull) { // Mixed — fill gaps by inferring the opposite of the nearest known neighbor for (Integer i = 0; i < t.messages.size(); i++) { if (t.messages[i].message_type == null) { // Look at previous message for context if (i > 0 && t.messages[i - 1].message_type != null) { t.messages[i].message_type = 'Input'.equals(t.messages[i - 1].message_type) ? 'Output' : 'Input'; } else if (i == 0) { t.messages[i].message_type = 'Input'; // first message is always user } } } } } // --- Steps --- // All issue-detection fields are selected: // ssot__ErrorMessageText__c → Action error (P1) // ssot__InputValueText__c → Wrong action input (P2) // ssot__OutputValueText__c → Action output / TRUST_GUARDRAILS adherence dict // ssot__PreStepVariableText__c → Pre-step variable snapshot (P2) // ssot__PostStepVariableText__c → Post-step variable snapshot (P2) // ssot__EndTimestamp__c → Step duration for slow action (P3) // ssot__GenerationId__c → Links to GenAIGeneration__dlm (LLM audit) // ssot__GenAiGatewayRequestId__c → Links to GenAIGatewayRequest__dlm (prompt text) String stepSql = 'SELECT ssot__Id__c, ssot__AiAgentInteractionId__c, ' + ' ssot__AiAgentInteractionStepType__c, ssot__Name__c, ' + ' ssot__StartTimestamp__c, ssot__EndTimestamp__c, ' + ' ssot__InputValueText__c, ssot__OutputValueText__c, ' + ' ssot__ErrorMessageText__c, ' + ' ssot__PreStepVariableText__c, ssot__PostStepVariableText__c, ' + ' ssot__GenerationId__c, ssot__GenAiGatewayRequestId__c ' + 'FROM "ssot__AiAgentInteractionStep__dlm" ' + 'WHERE ssot__AiAgentInteractionId__c IN ' + inClause + ' ' + 'ORDER BY ssot__StartTimestamp__c'; ConnectApi.CdpQueryOutputV2 stepResult = runQuery(stepSql, dataSpaceName); if (stepResult != null && stepResult.data != null) { for (ConnectApi.CdpQueryV2Row row : stepResult.data) { TurnData t = turnById.get(col(row.rowData, 1)); if (t == null) continue; StepData s = new StepData(); s.step_id = col(row.rowData, 0); s.step_type = col(row.rowData, 2); s.name = col(row.rowData, 3); s.start_time = col(row.rowData, 4); s.end_time = col(row.rowData, 5); s.duration_ms = durationMs(s.start_time, s.end_time); s.input = notSet(col(row.rowData, 6)); s.output = notSet(col(row.rowData, 7)); s.error = notSet(col(row.rowData, 8)); s.pre_vars = notSet(col(row.rowData, 9)); s.post_vars = notSet(col(row.rowData, 10)); s.generation_id = notSet(col(row.rowData, 11)); s.gateway_request_id = notSet(col(row.rowData, 12)); if (s.error != null && 'ACTION_STEP'.equalsIgnoreCase(s.step_type)) convo.action_error_count++; t.steps.add(s); } } return JSON.serialize(convo); } /** * Retrieve full conversation details for multiple sessions. * * @param dataSpaceName Data Cloud Data Space API name * @param sessionIds List of ssot__Id__c values (keep under 20 to avoid CPU limits) * @return JSON-serialized List */ public static String getMultipleConversationDetails(String dataSpaceName, List sessionIds) { List results = new List(); if (sessionIds == null || sessionIds.isEmpty()) return JSON.serialize(results); for (String sid : sessionIds) { String detail = getConversationDetails(dataSpaceName, sid); if (String.isNotBlank(detail)) { ConversationData convo = (ConversationData) JSON.deserialize(detail, ConversationData.class); results.add(convo); } } return JSON.serialize(results); } /** * Retrieve LLM prompt and response for a set of LLM_STEP records by joining the * Einstein Audit & Feedback DMOs (GenAIGatewayRequest and GenAIGeneration). * * Typical use: after finding LLM_STEP records with non-null generation_id in a * ConversationData, pass those step IDs here to see what prompt was sent and what * the model actually returned — useful for diagnosing LOW instruction adherence. * * @param dataSpaceName Data Cloud Data Space API name * @param stepIds List of ssot__Id__c values from LLM_STEP StepData records * @return JSON-serialized List */ public static String getLlmStepDetails(String dataSpaceName, List stepIds) { if (stepIds == null || stepIds.isEmpty()) return JSON.serialize(new List()); String inClause = '(\'' + String.join(stepIds, '\',\'') + '\')'; String sql = 'SELECT s.ssot__Id__c, s.ssot__AiAgentInteractionId__c, s.ssot__Name__c, ' + ' r.prompt__c, g.responseText__c, ' + ' s.ssot__GenerationId__c, s.ssot__GenAiGatewayRequestId__c ' + 'FROM "ssot__AiAgentInteractionStep__dlm" s ' + 'LEFT JOIN "GenAIGeneration__dlm" g ' + ' ON s.ssot__GenerationId__c = g.generationId__c ' + 'LEFT JOIN "GenAIGatewayRequest__dlm" r ' + ' ON s.ssot__GenAiGatewayRequestId__c = r.gatewayRequestId__c ' + 'WHERE s.ssot__Id__c IN ' + inClause; ConnectApi.CdpQueryOutputV2 result = runQuery(sql, dataSpaceName); List details = new List(); if (result != null && result.data != null) { for (ConnectApi.CdpQueryV2Row row : result.data) { LlmStepDetail d = new LlmStepDetail(); d.step_id = col(row.rowData, 0); d.interaction_id = col(row.rowData, 1); d.step_name = col(row.rowData, 2); d.prompt = notSet(col(row.rowData, 3)); d.llm_response = notSet(col(row.rowData, 4)); d.generation_id = notSet(col(row.rowData, 5)); d.gateway_request_id = notSet(col(row.rowData, 6)); details.add(d); } } return JSON.serialize(details); } /** * Retrieve moment insights (intent summaries, durations) and retriever quality metrics * for a set of sessions. Gracefully degrades if DMOs are unavailable. * * @param dataSpaceName Data Cloud Data Space API name * @param sessionIds List of ssot__Id__c values from AiAgentSession * @return JSON-serialized List */ public static String getMomentInsights(String dataSpaceName, List sessionIds) { List results = new List(); if (sessionIds == null || sessionIds.isEmpty()) return JSON.serialize(results); String inClause = '(\'' + String.join(sessionIds, '\',\'') + '\')'; // --- Session headers --- String sessionSql = 'SELECT ssot__Id__c, ssot__StartTimestamp__c, ssot__EndTimestamp__c, ' + ' ssot__AiAgentSessionEndType__c ' + 'FROM "ssot__AiAgentSession__dlm" ' + 'WHERE ssot__Id__c IN ' + inClause; ConnectApi.CdpQueryOutputV2 sessionResult = runQuery(sessionSql, dataSpaceName); Map insightsById = new Map(); if (sessionResult != null && sessionResult.data != null) { for (ConnectApi.CdpQueryV2Row row : sessionResult.data) { SessionInsights si = new SessionInsights(); si.session_id = col(row.rowData, 0); si.start_time = col(row.rowData, 1); si.end_time = col(row.rowData, 2); si.end_type = notSet(col(row.rowData, 3)); si.duration_ms = durationMs(si.start_time, si.end_time); insightsById.put(si.session_id, si); results.add(si); } } // --- Turn counts --- String turnSql = 'SELECT ssot__AiAgentSessionId__c, COUNT(*) ' + 'FROM "ssot__AiAgentInteraction__dlm" ' + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' ' + ' AND ssot__AiAgentInteractionType__c = \'TURN\' ' + 'GROUP BY ssot__AiAgentSessionId__c'; ConnectApi.CdpQueryOutputV2 turnResult = runQuery(turnSql, dataSpaceName); if (turnResult != null && turnResult.data != null) { for (ConnectApi.CdpQueryV2Row row : turnResult.data) { SessionInsights si = insightsById.get(col(row.rowData, 0)); if (si != null) { si.turn_count = Integer.valueOf(col(row.rowData, 1)); } } } // --- Action error counts --- String errSql = 'SELECT ssot__AiAgentInteraction__dlm.ssot__AiAgentSessionId__c, COUNT(*) ' + 'FROM "ssot__AiAgentInteractionStep__dlm" ' + 'JOIN "ssot__AiAgentInteraction__dlm" ' + ' ON ssot__AiAgentInteractionStep__dlm.ssot__AiAgentInteractionId__c = ssot__AiAgentInteraction__dlm.ssot__Id__c ' + 'WHERE ssot__AiAgentInteraction__dlm.ssot__AiAgentSessionId__c IN ' + inClause + ' ' + ' AND ssot__AiAgentInteractionStep__dlm.ssot__AiAgentInteractionStepType__c = \'ACTION_STEP\' ' + ' AND ssot__AiAgentInteractionStep__dlm.ssot__ErrorMessageText__c IS NOT NULL ' + ' AND ssot__AiAgentInteractionStep__dlm.ssot__ErrorMessageText__c != \'NOT_SET\' ' + 'GROUP BY ssot__AiAgentInteraction__dlm.ssot__AiAgentSessionId__c'; ConnectApi.CdpQueryOutputV2 errResult = runQuery(errSql, dataSpaceName); if (errResult != null && errResult.data != null) { for (ConnectApi.CdpQueryV2Row row : errResult.data) { SessionInsights si = insightsById.get(col(row.rowData, 0)); if (si != null) { si.action_error_count = Integer.valueOf(col(row.rowData, 1)); } } } // --- Moments (graceful degradation) --- Boolean momentAvailable = isDmoAvailable('ssot__AiAgentMoment__dlm', dataSpaceName); if (momentAvailable) { String momentSql = 'SELECT ssot__Id__c, ssot__AiAgentSessionId__c, ' + ' ssot__StartTimestamp__c, ssot__EndTimestamp__c, ' + ' ssot__RequestSummaryText__c, ssot__ResponseSummaryText__c, ' + ' ssot__AiAgentApiName__c, ssot__AiAgentVersionApiName__c ' + 'FROM "ssot__AiAgentMoment__dlm" ' + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' ' + 'ORDER BY ssot__StartTimestamp__c'; ConnectApi.CdpQueryOutputV2 momentResult = runQuery(momentSql, dataSpaceName); if (momentResult != null && momentResult.data != null) { for (ConnectApi.CdpQueryV2Row row : momentResult.data) { MomentData m = new MomentData(); m.moment_id = col(row.rowData, 0); m.session_id = col(row.rowData, 1); m.start_time = col(row.rowData, 2); m.end_time = col(row.rowData, 3); m.duration_ms = durationMs(m.start_time, m.end_time); m.request_summary = notSet(col(row.rowData, 4)); m.response_summary = notSet(col(row.rowData, 5)); m.agent_api_name = notSet(col(row.rowData, 6)); m.agent_version = notSet(col(row.rowData, 7)); SessionInsights si = insightsById.get(m.session_id); if (si != null) si.moments.add(m); } } // --- Quality scores via AiAgentTagAssociation → AiAgentTag --- if (isDmoAvailable('ssot__AiAgentTagAssociation__dlm', dataSpaceName)) { // Collect all moment IDs for the quality score query List momentIds = new List(); Map momentById = new Map(); for (SessionInsights si : results) { for (MomentData m : si.moments) { momentIds.add(m.moment_id); momentById.put(m.moment_id, m); } } if (!momentIds.isEmpty()) { String momentInClause = '(\'' + String.join(momentIds, '\',\'') + '\')'; String qualitySql = 'SELECT ta.ssot__AiAgentMomentId__c, t.ssot__Value__c, ' + ' ta.ssot__AssociationReasonText__c ' + 'FROM "ssot__AiAgentTagAssociation__dlm" ta ' + 'JOIN "ssot__AiAgentTag__dlm" t ' + ' ON ta.ssot__AiAgentTagId__c = t.ssot__Id__c ' + 'WHERE ta.ssot__AiAgentMomentId__c IN ' + momentInClause; ConnectApi.CdpQueryOutputV2 qualityResult = runQuery(qualitySql, dataSpaceName); if (qualityResult != null && qualityResult.data != null) { for (ConnectApi.CdpQueryV2Row row : qualityResult.data) { String momentId = col(row.rowData, 0); MomentData m = momentById.get(momentId); if (m != null) { m.quality_score = toInteger(col(row.rowData, 1)); m.quality_reasoning = notSet(col(row.rowData, 2)); } } } } } } else { for (SessionInsights si : results) { si.debug_message = 'AiAgentMoment DMO not available in this org'; } } // Set moment_count and avg_quality_score per session for (SessionInsights si : results) { si.moment_count = si.moments.size(); Integer scoreSum = 0; Integer scoreCount = 0; for (MomentData m : si.moments) { if (m.quality_score != null) { scoreSum += m.quality_score; scoreCount++; } } if (scoreCount > 0) { si.avg_quality_score = Decimal.valueOf(scoreSum) / scoreCount; } } // --- Retriever quality metrics (graceful degradation) --- Boolean retrieverAvailable = isDmoAvailable('ssot__AiRetrieverQualityMetric__dlm', dataSpaceName); if (retrieverAvailable) { // Retriever metrics link to sessions via gateway request IDs on LLM steps. // Query all retriever metrics for gateway requests that belong to these sessions. String retrieverSql = 'SELECT r.ssot__Id__c, r.ssot__AiGatewayRequestId__c, ' + ' r.ssot__AiRetrieverRequestId__c, r.ssot__RetrieverApiName__c, ' + ' r.ssot__UserUtteranceText__c, ' + ' r.ssot__FaithfulnessRelevancyScoreNumber__c, ' + ' r.ssot__AnswerRelevancyScoreNumber__c, ' + ' r.ssot__ContextPrecisionScoreNumber__c, ' + ' i.ssot__AiAgentSessionId__c ' + 'FROM "ssot__AiRetrieverQualityMetric__dlm" r ' + 'JOIN "ssot__AiAgentInteractionStep__dlm" s ' + ' ON r.ssot__AiGatewayRequestId__c = s.ssot__GenAiGatewayRequestId__c ' + 'JOIN "ssot__AiAgentInteraction__dlm" i ' + ' ON s.ssot__AiAgentInteractionId__c = i.ssot__Id__c ' + 'WHERE i.ssot__AiAgentSessionId__c IN ' + inClause; ConnectApi.CdpQueryOutputV2 retResult = runQuery(retrieverSql, dataSpaceName); if (retResult != null && retResult.data != null) { for (ConnectApi.CdpQueryV2Row row : retResult.data) { RetrieverMetricData rm = new RetrieverMetricData(); rm.metric_id = col(row.rowData, 0); rm.gateway_request_id = col(row.rowData, 1); rm.retriever_request_id = col(row.rowData, 2); rm.retriever_api_name = notSet(col(row.rowData, 3)); rm.user_utterance = notSet(col(row.rowData, 4)); rm.faithfulness = toDecimal(col(row.rowData, 5)); rm.answer_relevance = toDecimal(col(row.rowData, 6)); rm.context_precision = toDecimal(col(row.rowData, 7)); String sessionId = col(row.rowData, 8); SessionInsights si = insightsById.get(sessionId); if (si != null) si.retriever_metrics.add(rm); } } } return JSON.serialize(results); } /** * Compute aggregated metrics across sessions in a date range. * Includes session rates (abandonment, escalation, deflection), top intents from moments, * and average RAG quality scores. Gracefully degrades when DMOs are unavailable. * * @param dataSpaceName Data Cloud Data Space API name * @param startIso ISO 8601 UTC start timestamp * @param endIso ISO 8601 UTC end timestamp * @param maxRows Maximum sessions to aggregate over * @param agentApiName Agent MasterLabel to filter by (null = all agents) * @return JSON-serialized AggregatedMetrics */ public static String getAggregatedMetrics(String dataSpaceName, String startIso, String endIso, Integer maxRows, String agentApiName) { AggregatedMetrics metrics = new AggregatedMetrics(); metrics.end_type_counts = new Map(); metrics.top_intents = new Map(); metrics.unavailable_dmos = new List(); // Step 1: Find sessions (reuse existing method) String sessionsJson = findSessions(dataSpaceName, startIso, endIso, maxRows, agentApiName); List sessions = (List) JSON.deserialize(sessionsJson, List.class); metrics.total_sessions = sessions.size(); if (sessions.isEmpty()) return JSON.serialize(metrics); // Compute session-level aggregates Decimal totalDurationSec = 0; Integer durationCount = 0; for (SessionSummary s : sessions) { // End type distribution String endType = s.end_type != null ? s.end_type : 'UNKNOWN'; Integer cnt = metrics.end_type_counts.get(endType); metrics.end_type_counts.put(endType, cnt != null ? cnt + 1 : 1); // Duration if (s.duration_ms != null) { totalDurationSec += Decimal.valueOf(s.duration_ms) / 1000; durationCount++; } } if (durationCount > 0) { metrics.avg_session_duration_sec = totalDurationSec / durationCount; } // Session rates Integer userEnded = metrics.end_type_counts.get('USER_ENDED'); Integer agentEnded = metrics.end_type_counts.get('AGENT_ENDED'); Integer escalated = metrics.end_type_counts.get('ESCALATED'); Integer total = metrics.total_sessions; metrics.abandonment_rate = Decimal.valueOf(userEnded != null ? userEnded : 0) / total; metrics.deflection_rate = Decimal.valueOf(agentEnded != null ? agentEnded : 0) / total; metrics.escalation_rate = Decimal.valueOf(escalated != null ? escalated : 0) / total; // Collect session IDs for sub-queries List sessionIds = new List(); for (SessionSummary s : sessions) { sessionIds.add(s.session_id); } String inClause = '(\'' + String.join(sessionIds, '\',\'') + '\')'; // Step 2: Total turns String turnSql = 'SELECT COUNT(*) ' + 'FROM "ssot__AiAgentInteraction__dlm" ' + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' ' + ' AND ssot__AiAgentInteractionType__c = \'TURN\''; ConnectApi.CdpQueryOutputV2 turnResult = runQuery(turnSql, dataSpaceName); if (turnResult != null && turnResult.data != null && !turnResult.data.isEmpty()) { metrics.total_turns = Integer.valueOf(col(turnResult.data[0].rowData, 0)); } // Step 3: Moments — count + top intents if (isDmoAvailable('ssot__AiAgentMoment__dlm', dataSpaceName)) { // Total moment count String momentCountSql = 'SELECT COUNT(*) ' + 'FROM "ssot__AiAgentMoment__dlm" ' + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause; ConnectApi.CdpQueryOutputV2 mcResult = runQuery(momentCountSql, dataSpaceName); if (mcResult != null && mcResult.data != null && !mcResult.data.isEmpty()) { metrics.total_moments = Integer.valueOf(col(mcResult.data[0].rowData, 0)); } // Top intents by request summary (GROUP BY truncated summary) String intentSql = 'SELECT ssot__RequestSummaryText__c, COUNT(*) AS cnt ' + 'FROM "ssot__AiAgentMoment__dlm" ' + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' ' + ' AND ssot__RequestSummaryText__c IS NOT NULL ' + ' AND ssot__RequestSummaryText__c != \'NOT_SET\' ' + 'GROUP BY ssot__RequestSummaryText__c ' + 'ORDER BY cnt DESC ' + 'LIMIT 20'; ConnectApi.CdpQueryOutputV2 intentResult = runQuery(intentSql, dataSpaceName); if (intentResult != null && intentResult.data != null) { for (ConnectApi.CdpQueryV2Row row : intentResult.data) { String intent = col(row.rowData, 0); Integer intentCnt = Integer.valueOf(col(row.rowData, 1)); if (intent != null) { // Truncate long summaries for the top_intents map key if (intent.length() > 100) intent = intent.substring(0, 100) + '...'; metrics.top_intents.put(intent, intentCnt); } } } } else { metrics.unavailable_dmos.add('ssot__AiAgentMoment__dlm'); } // Step 3b: Quality scores via AiAgentTagAssociation → AiAgentTag metrics.quality_distribution = new Map(); if (isDmoAvailable('ssot__AiAgentTagAssociation__dlm', dataSpaceName)) { // AVG quality score and distribution across all moments in these sessions String qualityAvgSql = 'SELECT t.ssot__Value__c, COUNT(*) AS cnt ' + 'FROM "ssot__AiAgentTagAssociation__dlm" ta ' + 'JOIN "ssot__AiAgentTag__dlm" t ' + ' ON ta.ssot__AiAgentTagId__c = t.ssot__Id__c ' + 'WHERE ta.ssot__AiAgentSessionId__c IN ' + inClause + ' ' + 'GROUP BY t.ssot__Value__c ' + 'ORDER BY t.ssot__Value__c'; ConnectApi.CdpQueryOutputV2 qualityResult = runQuery(qualityAvgSql, dataSpaceName); if (qualityResult != null && qualityResult.data != null) { Integer totalScore = 0; Integer totalCount = 0; for (ConnectApi.CdpQueryV2Row row : qualityResult.data) { String scoreStr = col(row.rowData, 0); Integer cnt = Integer.valueOf(col(row.rowData, 1)); if (scoreStr != null && cnt != null) { Integer score = toInteger(scoreStr); metrics.quality_distribution.put(String.valueOf(score), cnt); if (score != null) { totalScore += score * cnt; totalCount += cnt; } } } if (totalCount > 0) { metrics.avg_quality_score = Decimal.valueOf(totalScore) / totalCount; } } } else { metrics.unavailable_dmos.add('ssot__AiAgentTagAssociation__dlm'); } // Step 4: Retriever quality averages if (isDmoAvailable('ssot__AiRetrieverQualityMetric__dlm', dataSpaceName)) { String retSql = 'SELECT AVG(r.ssot__FaithfulnessRelevancyScoreNumber__c), ' + ' AVG(r.ssot__AnswerRelevancyScoreNumber__c), ' + ' AVG(r.ssot__ContextPrecisionScoreNumber__c) ' + 'FROM "ssot__AiRetrieverQualityMetric__dlm" r ' + 'JOIN "ssot__AiAgentInteractionStep__dlm" s ' + ' ON r.ssot__AiGatewayRequestId__c = s.ssot__GenAiGatewayRequestId__c ' + 'JOIN "ssot__AiAgentInteraction__dlm" i ' + ' ON s.ssot__AiAgentInteractionId__c = i.ssot__Id__c ' + 'WHERE i.ssot__AiAgentSessionId__c IN ' + inClause; ConnectApi.CdpQueryOutputV2 retResult = runQuery(retSql, dataSpaceName); if (retResult != null && retResult.data != null && !retResult.data.isEmpty()) { metrics.avg_faithfulness = toDecimal(col(retResult.data[0].rowData, 0)); metrics.avg_answer_relevance = toDecimal(col(retResult.data[0].rowData, 1)); metrics.avg_context_precision = toDecimal(col(retResult.data[0].rowData, 2)); } } else { metrics.unavailable_dmos.add('ssot__AiRetrieverQualityMetric__dlm'); } return JSON.serialize(metrics); } // ========================================================================= // Private helpers // ========================================================================= /** Cache for DMO availability probes to avoid repeated queries. */ private static Map dmoAvailabilityCache = new Map(); /** * Probe whether a DMO exists and is queryable in this org's Data Cloud. * Results are cached for the transaction to avoid repeated probes. */ private static Boolean isDmoAvailable(String dmoName, String dataSpaceName) { if (dmoAvailabilityCache.containsKey(dmoName)) return dmoAvailabilityCache.get(dmoName); ConnectApi.CdpQueryInput inp = new ConnectApi.CdpQueryInput(); inp.sql = 'SELECT COUNT(*) FROM "' + dmoName + '"'; try { ConnectApi.CdpQueryOutputV2 result = ConnectApi.CdpQuery.queryAnsiSqlV2(inp, dataSpaceName); dmoAvailabilityCache.put(dmoName, true); return true; } catch (Exception e) { System.debug(LoggingLevel.WARN, 'DMO not available: ' + dmoName + ' — ' + e.getMessage()); dmoAvailabilityCache.put(dmoName, false); return false; } } /** Safely parse a string to Decimal; returns null on failure. */ private static Decimal toDecimal(String val) { if (String.isBlank(val) || val == 'NOT_SET') return null; try { return Decimal.valueOf(val); } catch (Exception e) { return null; } } /** Safely parse a string to Integer (truncating decimals); returns null on failure. */ private static Integer toInteger(String val) { if (String.isBlank(val) || val == 'NOT_SET') return null; try { return Decimal.valueOf(val).intValue(); } catch (Exception e) { return null; } } /** * Resolve an Agentforce agent name to its GenAiPlannerDefinition IDs. * * Each deployed agent version creates a GenAiPlannerDefinition whose MasterLabel * matches the agent's display name (e.g. 'TeslaSupportAgent'). Returning all * matching versions ensures historical sessions from older deployments are included. * * Also adds the 15-char version of each ID to handle STDM DMO inconsistency: * the participant DMO stores ssot__ParticipantId__c as either 15-char or 18-char. * * @param agentApiName MasterLabel of the agent (same as the agent's display name) * @return List of GenAiPlannerDefinition Ids in both 15-char and 18-char formats; empty if not found */ private static List resolvePlannerIds(String agentApiName) { List ids = new List(); try { // Search by MasterLabel (exact match) OR DeveloperName pattern. // Agent Script agents create GenAiPlannerDefinition with DeveloperName // like 'OrderService_v1' and MasterLabel like 'Order Service'. // Accept the API name (no spaces) and match both patterns. String devNamePattern = agentApiName + '_%'; List planners = [ SELECT Id FROM GenAiPlannerDefinition WHERE MasterLabel = :agentApiName OR DeveloperName = :agentApiName OR DeveloperName LIKE :devNamePattern ]; for (GenAiPlannerDefinition p : planners) { String id18 = String.valueOf(p.Id); ids.add(id18); // STDM DMO stores IDs inconsistently (15-char or 18-char) — include both if (id18.length() == 18) ids.add(id18.substring(0, 15)); } if (ids.isEmpty()) { System.debug(LoggingLevel.WARN, 'No GenAiPlannerDefinition found for: ' + agentApiName); } else { System.debug(LoggingLevel.DEBUG, 'Resolved ' + planners.size() + ' planner version(s) for agent "' + agentApiName + '": ' + ids); } } catch (Exception e) { System.debug(LoggingLevel.WARN, 'resolvePlannerIds failed for "' + agentApiName + '": ' + e.getMessage()); } return ids; } /** Extract ssot__AiAgentSessionId__c values (col 0) from a participant DMO query result. */ private static List extractSessionIds(ConnectApi.CdpQueryOutputV2 partResult) { List sessionIds = new List(); if (partResult != null && partResult.data != null) { for (ConnectApi.CdpQueryV2Row row : partResult.data) { String sid = col(row.rowData, 0); if (sid != null) sessionIds.add(sid); } } return sessionIds; } private static ConnectApi.CdpQueryOutputV2 runQuery(String sql, String dataSpaceName) { System.debug(LoggingLevel.DEBUG, 'AgentforceOptimize CDP query (' + dataSpaceName + '): ' + sql); ConnectApi.CdpQueryInput inp = new ConnectApi.CdpQueryInput(); inp.sql = sql; try { ConnectApi.CdpQueryOutputV2 result = ConnectApi.CdpQuery.queryAnsiSqlV2(inp, dataSpaceName); Integer rowCount = (result != null && result.data != null) ? result.data.size() : 0; System.debug(LoggingLevel.DEBUG, 'Rows returned: ' + rowCount); return result; } catch (Exception e) { System.debug(LoggingLevel.ERROR, 'CDP query failed [' + dataSpaceName + ']: ' + e.getMessage() + ' | SQL: ' + sql); return null; } } /** Safely extract a column value as String; returns null for empty/null cells. */ private static String col(List row, Integer i) { if (row == null || i >= row.size() || row[i] == null) return null; return String.valueOf(row[i]); } /** Return null when the value is the STDM "NOT_SET" sentinel or blank. */ private static String notSet(String val) { if (String.isBlank(val) || val == 'NOT_SET') return null; return val; } /** Compute millisecond duration between two ISO timestamp strings; null on any failure. */ private static Long durationMs(String startTs, String endTs) { if (String.isBlank(startTs) || String.isBlank(endTs)) return null; try { Datetime startDt = parseTs(startTs); Datetime endDt = parseTs(endTs); if (startDt == null || endDt == null) return null; return endDt.getTime() - startDt.getTime(); } catch (Exception e) { return null; } } /** Parse an ISO 8601 timestamp string into a Datetime, with fallback strategies. */ private static Datetime parseTs(String ts) { if (String.isBlank(ts)) return null; // Strategy 1: ISO 8601 via JSON deserialise try { return (Datetime) JSON.deserialize('"' + ts + '"', Datetime.class); } catch (Exception e1) { System.debug(LoggingLevel.FINE, 'parseTs strategy 1 failed for "' + ts + '": ' + e1.getMessage()); } // Strategy 2: Datetime.valueOf (handles 'yyyy-MM-dd HH:mm:ss' format) try { return Datetime.valueOf(ts); } catch (Exception e2) { System.debug(LoggingLevel.FINE, 'parseTs strategy 2 failed for "' + ts + '": ' + e2.getMessage()); } // Strategy 3: epoch milliseconds try { return Datetime.newInstance(Long.valueOf(ts)); } catch (Exception e3) { System.debug(LoggingLevel.FINE, 'parseTs strategy 3 failed for "' + ts + '": ' + e3.getMessage()); } return null; } // ========================================================================= // Observability Query (@InvocableMethod for Flow / Agentforce actions) // ========================================================================= public class ObservabilityInput { @InvocableVariable(label='Query Type' required=true) public String queryType; @InvocableVariable(label='Agent API Name' required=false) public String agentApiName; @InvocableVariable(label='Subagent API Name' required=false) public String topicApiName; @InvocableVariable(label='Lookback Days' required=false) public Integer lookbackDays; } public class ObservabilityOutput { @InvocableVariable(label='Query Result JSON') public String resultJson; @InvocableVariable(label='Summary Text') public String summaryText; } @InvocableMethod(label='Run Observability Query' description='Executes a Data Cloud observability query based on query type and optional filters.') public static List runObservabilityQuery(List inputs) { List results = new List(); for (ObservabilityInput input : inputs) { ObservabilityOutput out = new ObservabilityOutput(); try { String query = buildObservabilityQuery(input); ConnectApi.CdpQueryInput cdpInput = new ConnectApi.CdpQueryInput(); cdpInput.sql = query; ConnectApi.CdpQueryOutputV2 cdpOutput = ConnectApi.CdpQuery.queryAnsiSqlV2(cdpInput); out.resultJson = JSON.serialize(cdpOutput); Integer rowCount = cdpOutput.rowCount != null ? cdpOutput.rowCount : 0; if (rowCount == 0) { out.summaryText = 'Query executed for ' + input.queryType + '. No results found for the given filters and time range. SQL: ' + query; } else { out.summaryText = 'Query executed for ' + input.queryType + '. Found ' + rowCount + ' result(s). Use the metadata to map column positions to names in the data arrays.'; } } catch (Exception e) { out.summaryText = 'Error executing ' + input.queryType + ' query: ' + e.getMessage(); out.resultJson = '{"error":"' + e.getMessage().replace('"', '\\"') + '"}'; } results.add(out); } return results; } private static String buildObservabilityQuery(ObservabilityInput p) { Integer days = p.lookbackDays != null ? p.lookbackDays : 90; String cutoff = Datetime.now().addDays(-days).formatGmt('yyyy-MM-dd HH:mm:ss.SSS'); String agentFilter = String.isNotBlank(p.agentApiName) ? ' AND sp.aiAgentApiName__c = \'' + String.escapeSingleQuotes(p.agentApiName) + '\'' : ''; String topicFilter = String.isNotBlank(p.topicApiName) ? ' AND i.topicApiName__c = \'' + String.escapeSingleQuotes(p.topicApiName) + '\'' : ''; if (p.queryType == 'KnowledgeGap') { return 'SELECT i.topicApiName__c, sp.aiAgentApiName__c, ' + 'AVG(q.ContextPrecisionScoreNumber__c) AS avg_precision, ' + 'AVG(q.AnswerRelevancyScoreNumber__c) AS avg_relevancy, ' + 'COUNT(*) AS total_interactions ' + 'FROM AIRetrieverQualityMetric__dll q ' + 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' + 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' + 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' + agentFilter + topicFilter + ' GROUP BY i.topicApiName__c, sp.aiAgentApiName__c ' + 'ORDER BY avg_precision ASC LIMIT 25'; } else if (p.queryType == 'Hallucination') { return 'SELECT sp.aiAgentApiName__c, i.topicApiName__c, ' + 'AVG(q.FaithfulnessRelevancyScoreNumber__c) AS avg_faithfulness, ' + 'COUNT(*) AS total_interactions ' + 'FROM AIRetrieverQualityMetric__dll q ' + 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' + 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' + 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' + ' AND q.FaithfulnessRelevancyScoreNumber__c < 0.8' + agentFilter + topicFilter + ' GROUP BY sp.aiAgentApiName__c, i.topicApiName__c ' + 'ORDER BY avg_faithfulness ASC LIMIT 25'; } else if (p.queryType == 'RetrievalQuality') { return 'SELECT q.RetrieverApiName__c, i.topicApiName__c, sp.aiAgentApiName__c, ' + 'AVG(q.ContextPrecisionScoreNumber__c) AS avg_precision, COUNT(*) AS total ' + 'FROM AIRetrieverQualityMetric__dll q ' + 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' + 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' + 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' + agentFilter + topicFilter + ' GROUP BY q.RetrieverApiName__c, i.topicApiName__c, sp.aiAgentApiName__c ' + 'ORDER BY avg_precision ASC LIMIT 25'; } else if (p.queryType == 'AnswerRelevancy') { return 'SELECT sp.aiAgentApiName__c, i.topicApiName__c, ' + 'AVG(q.AnswerRelevancyScoreNumber__c) AS avg_relevancy, COUNT(*) AS total ' + 'FROM AIRetrieverQualityMetric__dll q ' + 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' + 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' + 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' + ' AND q.AnswerRelevancyScoreNumber__c < 0.7' + agentFilter + topicFilter + ' GROUP BY sp.aiAgentApiName__c, i.topicApiName__c ' + 'ORDER BY avg_relevancy ASC LIMIT 25'; } else if (p.queryType == 'Leaderboard') { return 'SELECT sp.aiAgentApiName__c, i.topicApiName__c, ' + 'AVG(q.ContextPrecisionScoreNumber__c) AS avg_precision, ' + 'AVG(q.AnswerRelevancyScoreNumber__c) AS avg_relevancy, ' + 'AVG(q.FaithfulnessRelevancyScoreNumber__c) AS avg_faithfulness, ' + 'COUNT(*) AS total_interactions ' + 'FROM AIRetrieverQualityMetric__dll q ' + 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' + 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' + 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' + agentFilter + topicFilter + ' GROUP BY sp.aiAgentApiName__c, i.topicApiName__c ' + 'ORDER BY avg_precision ASC LIMIT 25'; } return 'SELECT COUNT(*) AS cnt FROM AIAgentSession__dll'; } }