@salesforce/afv-skills 1.6.9 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +28 -23
  2. package/package.json +1 -1
  3. package/skills/developing-agentforce/README.md +112 -0
  4. package/skills/{agentforce-development → developing-agentforce}/SKILL.md +109 -16
  5. package/skills/{agentforce-development → developing-agentforce}/assets/agents/README.md +2 -2
  6. package/skills/developing-agentforce/assets/agents/order-service.agent +272 -0
  7. package/skills/developing-agentforce/assets/agents/verification-gate.agent +280 -0
  8. package/skills/{agentforce-development → developing-agentforce}/assets/bundle-meta.xml +1 -1
  9. package/skills/{agentforce-development → developing-agentforce}/references/actions-reference.md +20 -0
  10. package/skills/{agentforce-development → developing-agentforce}/references/agent-design-and-spec-creation.md +1 -1
  11. package/skills/{agentforce-development → developing-agentforce}/references/agent-metadata-and-lifecycle.md +3 -3
  12. package/skills/{agentforce-development → developing-agentforce}/references/agent-script-core-language.md +40 -3
  13. package/skills/{agentforce-development → developing-agentforce}/references/agent-user-setup.md +60 -57
  14. package/skills/{agentforce-development → developing-agentforce}/references/agent-validation-and-debugging.md +22 -20
  15. package/skills/developing-agentforce/references/architecture-patterns.md +158 -0
  16. package/skills/developing-agentforce/references/complex-data-types.md +57 -0
  17. package/skills/developing-agentforce/references/deploy-reference.md +134 -0
  18. package/skills/developing-agentforce/references/discover-reference.md +102 -0
  19. package/skills/developing-agentforce/references/examples.md +350 -0
  20. package/skills/developing-agentforce/references/feature-validity.md +43 -0
  21. package/skills/developing-agentforce/references/instruction-resolution.md +545 -0
  22. package/skills/{agentforce-development → developing-agentforce}/references/known-issues.md +18 -18
  23. package/skills/{agentforce-development → developing-agentforce}/references/production-gotchas.md +24 -3
  24. package/skills/developing-agentforce/references/safety-review-reference.md +145 -0
  25. package/skills/{agentforce-development → developing-agentforce}/references/salesforce-cli-for-agents.md +9 -7
  26. package/skills/developing-agentforce/references/scaffold-reference.md +153 -0
  27. package/skills/developing-agentforce/references/scoring-rubric.md +24 -0
  28. package/skills/{agentforce-development → developing-agentforce}/references/version-history.md +2 -2
  29. package/skills/generating-flow/SKILL.md +3 -1
  30. package/skills/observing-agentforce/SKILL.md +368 -0
  31. package/skills/observing-agentforce/apex/AgentforceOptimizeService.cls +1262 -0
  32. package/skills/observing-agentforce/apex/AgentforceOptimizeService.cls-meta.xml +5 -0
  33. package/skills/observing-agentforce/references/improve-reference.md +359 -0
  34. package/skills/observing-agentforce/references/issue-classification.md +220 -0
  35. package/skills/observing-agentforce/references/reproduce-reference.md +131 -0
  36. package/skills/observing-agentforce/references/stdm-queries.md +381 -0
  37. package/skills/observing-agentforce/references/stdm-schema.md +189 -0
  38. package/skills/testing-agentforce/SKILL.md +335 -0
  39. package/skills/testing-agentforce/assets/basic-test-spec.yaml +59 -0
  40. package/skills/testing-agentforce/assets/guardrail-test-spec.yaml +101 -0
  41. package/skills/testing-agentforce/assets/standard-test-spec.yaml +123 -0
  42. package/skills/testing-agentforce/references/action-execution.md +241 -0
  43. package/skills/testing-agentforce/references/batch-testing.md +274 -0
  44. package/skills/testing-agentforce/references/preview-testing.md +353 -0
  45. package/skills/testing-agentforce/references/test-report-format.md +160 -0
  46. package/skills/testing-agentforce/references/troubleshooting.md +73 -0
  47. /package/skills/{agentforce-development → developing-agentforce}/assets/README-legacy.md +0 -0
  48. /package/skills/{agentforce-development → developing-agentforce}/assets/agent-spec-template.md +0 -0
  49. /package/skills/{agentforce-development → developing-agentforce}/assets/agents/hello-world.agent +0 -0
  50. /package/skills/{agentforce-development → developing-agentforce}/assets/agents/multi-topic.agent +0 -0
  51. /package/skills/{agentforce-development → developing-agentforce}/assets/agents/production-faq.agent +0 -0
  52. /package/skills/{agentforce-development → developing-agentforce}/assets/agents/production-faq.bundle-meta.xml +0 -0
  53. /package/skills/{agentforce-development → developing-agentforce}/assets/agents/simple-qa.agent +0 -0
  54. /package/skills/{agentforce-development → developing-agentforce}/assets/apex/models-api-queueable.cls +0 -0
  55. /package/skills/{agentforce-development → developing-agentforce}/assets/components/apex-action.agent +0 -0
  56. /package/skills/{agentforce-development → developing-agentforce}/assets/components/error-handling.agent +0 -0
  57. /package/skills/{agentforce-development → developing-agentforce}/assets/components/escalation-setup.agent +0 -0
  58. /package/skills/{agentforce-development → developing-agentforce}/assets/components/flow-action.agent +0 -0
  59. /package/skills/{agentforce-development → developing-agentforce}/assets/components/n-ary-conditions.agent +0 -0
  60. /package/skills/{agentforce-development → developing-agentforce}/assets/components/topic-with-actions.agent +0 -0
  61. /package/skills/{agentforce-development → developing-agentforce}/assets/deterministic-routing.agent +0 -0
  62. /package/skills/{agentforce-development → developing-agentforce}/assets/escalation-pattern.agent +0 -0
  63. /package/skills/{agentforce-development → developing-agentforce}/assets/flow-action-lookup.agent +0 -0
  64. /package/skills/{agentforce-development → developing-agentforce}/assets/hub-and-spoke.agent +0 -0
  65. /package/skills/{agentforce-development → developing-agentforce}/assets/invocable-apex-template.cls +0 -0
  66. /package/skills/{agentforce-development → developing-agentforce}/assets/local-info-agent-annotated.agent +0 -0
  67. /package/skills/{agentforce-development → developing-agentforce}/assets/metadata/basic-prompt-template.promptTemplate-meta.xml +0 -0
  68. /package/skills/{agentforce-development → developing-agentforce}/assets/metadata/genai-function-apex.xml +0 -0
  69. /package/skills/{agentforce-development → developing-agentforce}/assets/metadata/genai-function-flow.xml +0 -0
  70. /package/skills/{agentforce-development → developing-agentforce}/assets/metadata/genai-plugin.xml +0 -0
  71. /package/skills/{agentforce-development → developing-agentforce}/assets/metadata/http-callout-flow.flow-meta.xml +0 -0
  72. /package/skills/{agentforce-development → developing-agentforce}/assets/metadata/record-grounded-prompt.promptTemplate-meta.xml +0 -0
  73. /package/skills/{agentforce-development → developing-agentforce}/assets/minimal-starter.agent +0 -0
  74. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/README.md +0 -0
  75. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/action-callbacks.agent +0 -0
  76. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/advanced-input-bindings.agent +0 -0
  77. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/bidirectional-routing.agent +0 -0
  78. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/critical-input-collection.agent +0 -0
  79. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/delegation-routing.agent +0 -0
  80. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/lifecycle-events.agent +0 -0
  81. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/llm-controlled-actions.agent +0 -0
  82. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/multi-step-workflow.agent +0 -0
  83. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/open-gate-routing.agent +0 -0
  84. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/procedural-instructions.agent +0 -0
  85. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/prompt-template-action.agent +0 -0
  86. /package/skills/{agentforce-development → developing-agentforce}/assets/patterns/system-instruction-overrides.agent +0 -0
  87. /package/skills/{agentforce-development → developing-agentforce}/assets/prompt-rag-search.agent +0 -0
  88. /package/skills/{agentforce-development → developing-agentforce}/assets/template-multi-topic.agent +0 -0
  89. /package/skills/{agentforce-development → developing-agentforce}/assets/template-single-topic.agent +0 -0
  90. /package/skills/{agentforce-development → developing-agentforce}/assets/verification-gate.agent +0 -0
  91. /package/skills/{agentforce-development → developing-agentforce}/references/action-prompt-templates.md +0 -0
  92. /package/skills/{agentforce-development → developing-agentforce}/references/agent-access-guide.md +0 -0
  93. /package/skills/{agentforce-development → developing-agentforce}/references/agent-topic-map-diagrams.md +0 -0
  94. /package/skills/{agentforce-development → developing-agentforce}/references/minimal-examples.md +0 -0
@@ -0,0 +1,1262 @@
1
+ /**
2
+ * @description STDM query service for the agentforce-optimize Claude Code skill.
3
+ * Queries the Session Trace Data Model (STDM) in Data Cloud to retrieve
4
+ * session traces, conversation turns, messages, and steps for issue analysis.
5
+ *
6
+ * Deployed once per org by the agentforce-optimize skill (Phase 1 setup).
7
+ * All public methods accept dataSpaceName so no Data Space is hardcoded.
8
+ *
9
+ * Methods:
10
+ * findSessions(dataSpaceName, startIso, endIso, maxRows) → JSON List<SessionSummary>
11
+ * findSessions(dataSpaceName, startIso, endIso, maxRows, agentName) → JSON List<SessionSummary>
12
+ * getConversationDetails(dataSpaceName, sessionId) → JSON ConversationData
13
+ * getMultipleConversationDetails(dataSpaceName, sessionIds) → JSON List<ConversationData>
14
+ * getLlmStepDetails(dataSpaceName, stepIds) → JSON List<LlmStepDetail>
15
+ * getMomentInsights(dataSpaceName, sessionIds) → JSON List<SessionInsights>
16
+ * getAggregatedMetrics(dataSpaceName, startIso, endIso, maxRows, agentName) → JSON AggregatedMetrics
17
+ * runObservabilityQuery(List<ObservabilityInput>) → List<ObservabilityOutput> (@InvocableMethod)
18
+ */
19
+ public with sharing class AgentforceOptimizeService {
20
+
21
+ // =========================================================================
22
+ // Output wrappers
23
+ // =========================================================================
24
+
25
+ /** Lightweight session record returned by findSessions(). */
26
+ public class SessionSummary {
27
+ public String session_id;
28
+ public String start_time;
29
+ public String end_time;
30
+ public String channel;
31
+ public Long duration_ms;
32
+ /** How the session ended: e.g. USER_ENDED, AGENT_ENDED (null = in progress or not recorded) */
33
+ public String end_type;
34
+ }
35
+
36
+ /** A single user/agent message within a turn. */
37
+ public class MessageData {
38
+ public String message_id;
39
+ /** 'Input' (user) or 'Output' (agent) — raw STDM value */
40
+ public String message_type;
41
+ public String text;
42
+ public String sent_at;
43
+ }
44
+
45
+ /**
46
+ * A single internal step within a turn.
47
+ * All issue-detection fields are included:
48
+ * - error → non-null means ACTION_STEP failure (P1)
49
+ * - pre_vars / post_vars → null delta means variable not captured (P2)
50
+ * - duration_ms > 10 000 → slow action (P3)
51
+ * - generation_id → non-null on LLM_STEP; use getLlmStepDetails() to get the prompt
52
+ */
53
+ public class StepData {
54
+ public String step_id;
55
+ /** TOPIC_STEP | LLM_STEP | ACTION_STEP | SESSION_END | TRUST_GUARDRAILS_STEP */
56
+ public String step_type;
57
+ public String name;
58
+ public String start_time;
59
+ public String end_time;
60
+ public Long duration_ms;
61
+ /** Raw input to the step (JSON for ACTION_STEP; Python dict string for LLM_STEP) */
62
+ public String input;
63
+ /** Raw output from the step (JSON for ACTION_STEP; Python dict string for LLM_STEP) */
64
+ public String output;
65
+ /** Non-null indicates the step threw an error (only ACTION_STEP counts toward action_error_count) */
66
+ public String error;
67
+ /** Variable snapshot before this step (null when NOT_SET) */
68
+ public String pre_vars;
69
+ /** Variable snapshot after this step (null when NOT_SET) */
70
+ public String post_vars;
71
+ /** GenAiGeneration ID — non-null on LLM_STEP; pass to getLlmStepDetails() for full prompt/response */
72
+ public String generation_id;
73
+ /** GenAiGatewayRequest ID — non-null on LLM_STEP; links to raw gateway request */
74
+ public String gateway_request_id;
75
+ }
76
+
77
+ /**
78
+ * One conversational turn (AiAgentInteraction of type TURN).
79
+ * Contains all messages and steps for that turn.
80
+ */
81
+ public class TurnData {
82
+ public String interaction_id;
83
+ /** Topic API name — null/mismatch signals a misroute (P1) */
84
+ public String topic;
85
+ public String start_time;
86
+ public String end_time;
87
+ public Long duration_ms;
88
+ /** Telemetry trace ID for distributed tracing correlation */
89
+ public String telemetry_trace_id;
90
+ public List<MessageData> messages;
91
+ public List<StepData> steps;
92
+
93
+ public TurnData() {
94
+ messages = new List<MessageData>();
95
+ steps = new List<StepData>();
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Full conversation for one session: session header + ordered turns.
101
+ * turn_count and action_error_count are pre-computed for quick triage.
102
+ */
103
+ public class ConversationData {
104
+ public String session_id;
105
+ public String start_time;
106
+ public String end_time;
107
+ public String channel;
108
+ public Long duration_ms;
109
+ /** How the session ended (null = in progress or not recorded by Data Cloud) */
110
+ public String end_type;
111
+ /** Session-level variable snapshot from ssot__VariableText__c (null when absent) */
112
+ public String session_variables;
113
+ public Integer turn_count = 0;
114
+ public Integer action_error_count = 0;
115
+ public List<TurnData> turns;
116
+
117
+ public ConversationData() {
118
+ turns = new List<TurnData>();
119
+ }
120
+ }
121
+
122
+ /**
123
+ * LLM step detail retrieved from Einstein Audit & Feedback DMOs.
124
+ * Obtained by joining AiAgentInteractionStep with GenAIGeneration and GenAIGatewayRequest.
125
+ */
126
+ public class LlmStepDetail {
127
+ public String step_id;
128
+ public String interaction_id;
129
+ public String step_name;
130
+ /** Full prompt text from GenAIGatewayRequest__dlm.prompt__c */
131
+ public String prompt;
132
+ /** LLM response text from GenAIGeneration__dlm.responseText__c */
133
+ public String llm_response;
134
+ public String generation_id;
135
+ public String gateway_request_id;
136
+ }
137
+
138
+ /** A single intent moment within a session (from AiAgentMoment DMO). */
139
+ public class MomentData {
140
+ public String moment_id;
141
+ public String session_id;
142
+ public String start_time;
143
+ public String end_time;
144
+ public Long duration_ms;
145
+ public String request_summary;
146
+ public String response_summary;
147
+ public String agent_api_name;
148
+ public String agent_version;
149
+ /** Quality score 1-5 from AiAgentTagAssociation → AiAgentTag.Value */
150
+ public Integer quality_score;
151
+ /** LLM-generated reasoning for the quality score */
152
+ public String quality_reasoning;
153
+ public MomentData() {}
154
+ }
155
+
156
+ /** RAG quality metrics from the AiRetrieverQualityMetric DMO. */
157
+ public class RetrieverMetricData {
158
+ public String metric_id;
159
+ public String gateway_request_id;
160
+ public String retriever_request_id;
161
+ public String retriever_api_name;
162
+ public String user_utterance;
163
+ public Decimal faithfulness;
164
+ public Decimal answer_relevance;
165
+ public Decimal context_precision;
166
+ }
167
+
168
+ /** Per-session insights rollup (moments + retriever metrics). */
169
+ public class SessionInsights {
170
+ public String session_id;
171
+ public String start_time;
172
+ public String end_time;
173
+ public String end_type;
174
+ public Long duration_ms;
175
+ public Integer turn_count;
176
+ public Integer moment_count;
177
+ public Decimal avg_quality_score;
178
+ public Integer action_error_count;
179
+ public List<MomentData> moments;
180
+ public List<RetrieverMetricData> retriever_metrics;
181
+ public String debug_message;
182
+ public SessionInsights() {
183
+ moments = new List<MomentData>();
184
+ retriever_metrics = new List<RetrieverMetricData>();
185
+ }
186
+ }
187
+
188
+ /** Aggregated metrics across multiple sessions. */
189
+ public class AggregatedMetrics {
190
+ public Integer total_sessions;
191
+ public Integer total_moments;
192
+ public Integer total_turns;
193
+ public Decimal avg_quality_score;
194
+ public Decimal avg_session_duration_sec;
195
+ public Map<String, Integer> end_type_counts;
196
+ public Map<String, Integer> top_intents;
197
+ public Map<String, Integer> quality_distribution;
198
+ public Decimal abandonment_rate;
199
+ public Decimal escalation_rate;
200
+ public Decimal deflection_rate;
201
+ public Decimal avg_faithfulness;
202
+ public Decimal avg_answer_relevance;
203
+ public Decimal avg_context_precision;
204
+ public List<String> unavailable_dmos;
205
+ }
206
+
207
+ // =========================================================================
208
+ // Public API
209
+ // =========================================================================
210
+
211
+ /**
212
+ * Find recent sessions within a date range, optionally filtered to a specific agent.
213
+ *
214
+ * Agent filtering tries two strategies in order:
215
+ * 1. Direct: ssot__AiAgentApiName__c = agentApiName on the participant DMO (no SOQL needed)
216
+ * 2. Fallback: GenAiPlannerDefinition (SOQL) → ssot__ParticipantId__c IN plannerIds
217
+ * Both 15-char and 18-char ID formats are included to handle DMO inconsistency.
218
+ *
219
+ * If both strategies return empty, the query falls back to all sessions.
220
+ *
221
+ * @param dataSpaceName Data Cloud Data Space API name (discovered in Phase 0)
222
+ * @param startIso ISO 8601 UTC start, e.g. '2025-03-01T00:00:00.000Z'
223
+ * @param endIso ISO 8601 UTC end
224
+ * @param maxRows Maximum sessions to return (e.g. 20)
225
+ * @param agentApiName Agent display name / MasterLabel to filter by (null = all agents)
226
+ * @return JSON-serialized List<SessionSummary>
227
+ */
228
+ public static String findSessions(String dataSpaceName, String startIso, String endIso, Integer maxRows, String agentApiName) {
229
+ // Step 1: Find sessions that have actual TURN interactions (skip empty preview sessions).
230
+ // Empty sessions (sf agent preview, builder pings) create AiAgentSession + SESSION_END
231
+ // interaction records but never TURN records. Querying for TURN directly ensures we only
232
+ // return sessions with real conversation data.
233
+ String turnSessionSql =
234
+ 'SELECT DISTINCT ssot__AiAgentSessionId__c '
235
+ + 'FROM "ssot__AiAgentInteraction__dlm" '
236
+ + 'WHERE ssot__AiAgentInteractionType__c = \'TURN\' '
237
+ + ' AND ssot__StartTimestamp__c >= \'' + startIso + '\' '
238
+ + ' AND ssot__StartTimestamp__c <= \'' + endIso + '\'';
239
+
240
+ ConnectApi.CdpQueryOutputV2 turnResult = runQuery(turnSessionSql, dataSpaceName);
241
+ Set<String> sessionsWithTurns = new Set<String>();
242
+ if (turnResult != null && turnResult.data != null) {
243
+ for (ConnectApi.CdpQueryV2Row row : turnResult.data) {
244
+ String sid = col(row.rowData, 0);
245
+ if (sid != null) sessionsWithTurns.add(sid);
246
+ }
247
+ }
248
+ System.debug(LoggingLevel.DEBUG, 'Sessions with turns in date range: ' + sessionsWithTurns.size());
249
+
250
+ // Step 2: Build agent filter (optional)
251
+ String sessionFilter = '';
252
+ if (String.isNotBlank(agentApiName)) {
253
+ // Strategy 1: filter directly by ssot__AiAgentApiName__c (simplest, no SOQL)
254
+ String partSqlByName =
255
+ 'SELECT ssot__AiAgentSessionId__c '
256
+ + 'FROM "ssot__AiAgentSessionParticipant__dlm" '
257
+ + 'WHERE ssot__AiAgentApiName__c = \'' + String.escapeSingleQuotes(agentApiName) + '\'';
258
+ ConnectApi.CdpQueryOutputV2 nameResult = runQuery(partSqlByName, dataSpaceName);
259
+ List<String> sessionIds = extractSessionIds(nameResult);
260
+
261
+ if (!sessionIds.isEmpty()) {
262
+ sessionFilter = ' AND ssot__Id__c IN (\'' + String.join(sessionIds, '\',\'') + '\') ';
263
+ System.debug(LoggingLevel.DEBUG, 'Agent filter (AiAgentApiName): '
264
+ + sessionIds.size() + ' session(s) for agent: ' + agentApiName);
265
+ } else {
266
+ // Strategy 2: GenAiPlannerDefinition SOQL → ssot__ParticipantId__c
267
+ System.debug(LoggingLevel.DEBUG,
268
+ 'AiAgentApiName filter returned no sessions; trying GenAiPlannerDefinition fallback');
269
+ List<String> plannerIds = resolvePlannerIds(agentApiName);
270
+ if (!plannerIds.isEmpty()) {
271
+ String pInClause = '(\'' + String.join(plannerIds, '\',\'') + '\')';
272
+ String partSqlById =
273
+ 'SELECT ssot__AiAgentSessionId__c '
274
+ + 'FROM "ssot__AiAgentSessionParticipant__dlm" '
275
+ + 'WHERE ssot__ParticipantId__c IN ' + pInClause;
276
+ ConnectApi.CdpQueryOutputV2 idResult = runQuery(partSqlById, dataSpaceName);
277
+ List<String> sessionIds2 = extractSessionIds(idResult);
278
+ if (!sessionIds2.isEmpty()) {
279
+ sessionFilter = ' AND ssot__Id__c IN (\'' + String.join(sessionIds2, '\',\'') + '\') ';
280
+ System.debug(LoggingLevel.DEBUG, 'Agent filter (PlannerIds): '
281
+ + plannerIds.size() + ' planner(s), ' + sessionIds2.size() + ' session(s)');
282
+ } else {
283
+ System.debug(LoggingLevel.WARN,
284
+ 'No sessions found for agent: ' + agentApiName + ' — returning all sessions');
285
+ }
286
+ } else {
287
+ System.debug(LoggingLevel.WARN,
288
+ 'Agent not found: ' + agentApiName + ' — returning sessions for all agents');
289
+ }
290
+ }
291
+ }
292
+
293
+ // Step 3: Query sessions, preferring those with actual turns
294
+ String turnFilter = '';
295
+ if (!sessionsWithTurns.isEmpty()) {
296
+ List<String> turnList = new List<String>(sessionsWithTurns);
297
+ turnFilter = ' AND ssot__Id__c IN (\'' + String.join(turnList, '\',\'') + '\') ';
298
+ }
299
+
300
+ String sql =
301
+ 'SELECT ssot__Id__c, ssot__StartTimestamp__c, ssot__EndTimestamp__c, '
302
+ + ' ssot__AiAgentChannelType__c, ssot__AiAgentSessionEndType__c '
303
+ + 'FROM "ssot__AiAgentSession__dlm" '
304
+ + 'WHERE ssot__StartTimestamp__c >= \'' + startIso + '\' '
305
+ + ' AND ssot__StartTimestamp__c <= \'' + endIso + '\' '
306
+ + sessionFilter
307
+ + turnFilter
308
+ + 'ORDER BY ssot__StartTimestamp__c DESC '
309
+ + 'LIMIT ' + maxRows;
310
+
311
+ ConnectApi.CdpQueryOutputV2 result = runQuery(sql, dataSpaceName);
312
+ List<SessionSummary> sessions = new List<SessionSummary>();
313
+
314
+ if (result != null && result.data != null) {
315
+ for (ConnectApi.CdpQueryV2Row row : result.data) {
316
+ SessionSummary s = new SessionSummary();
317
+ s.session_id = col(row.rowData, 0);
318
+ s.start_time = col(row.rowData, 1);
319
+ s.end_time = col(row.rowData, 2);
320
+ s.channel = col(row.rowData, 3);
321
+ s.end_type = notSet(col(row.rowData, 4));
322
+ s.duration_ms = durationMs(s.start_time, s.end_time);
323
+ sessions.add(s);
324
+ }
325
+ }
326
+ return JSON.serialize(sessions);
327
+ }
328
+
329
+ /**
330
+ * Overload that queries sessions for all agents (no agent filter).
331
+ * Kept for backwards compatibility; prefer the 5-argument version.
332
+ */
333
+ public static String findSessions(String dataSpaceName, String startIso, String endIso, Integer maxRows) {
334
+ return findSessions(dataSpaceName, startIso, endIso, maxRows, null);
335
+ }
336
+
337
+ /**
338
+ * Retrieve full conversation details for a single session.
339
+ * Fetches interactions, messages, and steps (with error/variable/generation fields).
340
+ *
341
+ * @param dataSpaceName Data Cloud Data Space API name
342
+ * @param sessionId ssot__Id__c of the AiAgentSession
343
+ * @return JSON-serialized ConversationData
344
+ */
345
+ public static String getConversationDetails(String dataSpaceName, String sessionId) {
346
+ if (String.isBlank(sessionId)) return null;
347
+
348
+ ConversationData convo = new ConversationData();
349
+ convo.session_id = sessionId;
350
+
351
+ // --- Session header ---
352
+ String sessionSql =
353
+ 'SELECT ssot__StartTimestamp__c, ssot__EndTimestamp__c, ssot__AiAgentChannelType__c, '
354
+ + ' ssot__AiAgentSessionEndType__c, ssot__VariableText__c '
355
+ + 'FROM "ssot__AiAgentSession__dlm" '
356
+ + 'WHERE ssot__Id__c = \'' + String.escapeSingleQuotes(sessionId) + '\'';
357
+
358
+ ConnectApi.CdpQueryOutputV2 sessionResult = runQuery(sessionSql, dataSpaceName);
359
+ if (sessionResult != null && sessionResult.data != null && !sessionResult.data.isEmpty()) {
360
+ List<Object> r = sessionResult.data[0].rowData;
361
+ convo.start_time = col(r, 0);
362
+ convo.end_time = col(r, 1);
363
+ convo.channel = col(r, 2);
364
+ convo.end_type = notSet(col(r, 3));
365
+ convo.session_variables = notSet(col(r, 4));
366
+ convo.duration_ms = durationMs(convo.start_time, convo.end_time);
367
+ }
368
+
369
+ // --- Interactions (turns) ---
370
+ // ssot__TopicApiName__c included for misroute detection
371
+ // ssot__TelemetryTraceId__c included for distributed tracing
372
+ String interSql =
373
+ 'SELECT ssot__Id__c, ssot__TopicApiName__c, ssot__AiAgentInteractionType__c, '
374
+ + ' ssot__StartTimestamp__c, ssot__EndTimestamp__c, ssot__TelemetryTraceId__c '
375
+ + 'FROM "ssot__AiAgentInteraction__dlm" '
376
+ + 'WHERE ssot__AiAgentSessionId__c = \'' + String.escapeSingleQuotes(sessionId) + '\' '
377
+ + 'ORDER BY ssot__StartTimestamp__c';
378
+
379
+ ConnectApi.CdpQueryOutputV2 interResult = runQuery(interSql, dataSpaceName);
380
+ if (interResult == null || interResult.data == null || interResult.data.isEmpty()) {
381
+ return JSON.serialize(convo);
382
+ }
383
+
384
+ Map<String, TurnData> turnById = new Map<String, TurnData>();
385
+ List<String> turnIds = new List<String>();
386
+
387
+ for (ConnectApi.CdpQueryV2Row row : interResult.data) {
388
+ String interType = col(row.rowData, 2);
389
+ // SESSION_END is a meta interaction, not a user turn
390
+ if ('SESSION_END'.equalsIgnoreCase(interType)) continue;
391
+
392
+ TurnData t = new TurnData();
393
+ t.interaction_id = col(row.rowData, 0);
394
+ t.topic = col(row.rowData, 1);
395
+ t.start_time = col(row.rowData, 3);
396
+ t.end_time = col(row.rowData, 4);
397
+ t.duration_ms = durationMs(t.start_time, t.end_time);
398
+ t.telemetry_trace_id = notSet(col(row.rowData, 5));
399
+ turnById.put(t.interaction_id, t);
400
+ turnIds.add(t.interaction_id);
401
+ convo.turns.add(t);
402
+ }
403
+ convo.turn_count = convo.turns.size();
404
+
405
+ if (turnIds.isEmpty()) return JSON.serialize(convo);
406
+
407
+ String inClause = '(\'' + String.join(turnIds, '\',\'') + '\')';
408
+
409
+ // --- Messages ---
410
+ String msgSql =
411
+ 'SELECT ssot__Id__c, ssot__AiAgentInteractionId__c, '
412
+ + ' ssot__AiAgentInteractionMessageType__c, ssot__ContentText__c, '
413
+ + ' ssot__MessageSentTimestamp__c '
414
+ + 'FROM "ssot__AiAgentInteractionMessage__dlm" '
415
+ + 'WHERE ssot__AiAgentInteractionId__c IN ' + inClause + ' '
416
+ + 'ORDER BY ssot__MessageSentTimestamp__c';
417
+
418
+ ConnectApi.CdpQueryOutputV2 msgResult = runQuery(msgSql, dataSpaceName);
419
+ if (msgResult != null && msgResult.data != null) {
420
+ for (ConnectApi.CdpQueryV2Row row : msgResult.data) {
421
+ TurnData t = turnById.get(col(row.rowData, 1));
422
+ if (t == null) continue;
423
+
424
+ MessageData m = new MessageData();
425
+ m.message_id = col(row.rowData, 0);
426
+ m.message_type = col(row.rowData, 2); // 'Input' or 'Output'
427
+ m.text = col(row.rowData, 3);
428
+ m.sent_at = col(row.rowData, 4);
429
+ t.messages.add(m);
430
+ }
431
+ }
432
+
433
+ // Infer message types when ssot__AiAgentInteractionMessageType__c is null.
434
+ // In STDM, messages within a turn alternate: user Input first, then agent Output.
435
+ // If ALL messages in a turn have null type, assign by position (odd=Input, even=Output).
436
+ for (TurnData t : convo.turns) {
437
+ Boolean anyNull = false;
438
+ Boolean allNull = true;
439
+ for (MessageData m : t.messages) {
440
+ if (m.message_type == null) { anyNull = true; }
441
+ else { allNull = false; }
442
+ }
443
+ if (anyNull && allNull) {
444
+ // All null — infer from position: 1st=Input, 2nd=Output, 3rd=Input, ...
445
+ for (Integer i = 0; i < t.messages.size(); i++) {
446
+ t.messages[i].message_type = (Math.mod(i, 2) == 0) ? 'Input' : 'Output';
447
+ }
448
+ } else if (anyNull) {
449
+ // Mixed — fill gaps by inferring the opposite of the nearest known neighbor
450
+ for (Integer i = 0; i < t.messages.size(); i++) {
451
+ if (t.messages[i].message_type == null) {
452
+ // Look at previous message for context
453
+ if (i > 0 && t.messages[i - 1].message_type != null) {
454
+ t.messages[i].message_type = 'Input'.equals(t.messages[i - 1].message_type)
455
+ ? 'Output' : 'Input';
456
+ } else if (i == 0) {
457
+ t.messages[i].message_type = 'Input'; // first message is always user
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ // --- Steps ---
465
+ // All issue-detection fields are selected:
466
+ // ssot__ErrorMessageText__c → Action error (P1)
467
+ // ssot__InputValueText__c → Wrong action input (P2)
468
+ // ssot__OutputValueText__c → Action output / TRUST_GUARDRAILS adherence dict
469
+ // ssot__PreStepVariableText__c → Pre-step variable snapshot (P2)
470
+ // ssot__PostStepVariableText__c → Post-step variable snapshot (P2)
471
+ // ssot__EndTimestamp__c → Step duration for slow action (P3)
472
+ // ssot__GenerationId__c → Links to GenAIGeneration__dlm (LLM audit)
473
+ // ssot__GenAiGatewayRequestId__c → Links to GenAIGatewayRequest__dlm (prompt text)
474
+ String stepSql =
475
+ 'SELECT ssot__Id__c, ssot__AiAgentInteractionId__c, '
476
+ + ' ssot__AiAgentInteractionStepType__c, ssot__Name__c, '
477
+ + ' ssot__StartTimestamp__c, ssot__EndTimestamp__c, '
478
+ + ' ssot__InputValueText__c, ssot__OutputValueText__c, '
479
+ + ' ssot__ErrorMessageText__c, '
480
+ + ' ssot__PreStepVariableText__c, ssot__PostStepVariableText__c, '
481
+ + ' ssot__GenerationId__c, ssot__GenAiGatewayRequestId__c '
482
+ + 'FROM "ssot__AiAgentInteractionStep__dlm" '
483
+ + 'WHERE ssot__AiAgentInteractionId__c IN ' + inClause + ' '
484
+ + 'ORDER BY ssot__StartTimestamp__c';
485
+
486
+ ConnectApi.CdpQueryOutputV2 stepResult = runQuery(stepSql, dataSpaceName);
487
+ if (stepResult != null && stepResult.data != null) {
488
+ for (ConnectApi.CdpQueryV2Row row : stepResult.data) {
489
+ TurnData t = turnById.get(col(row.rowData, 1));
490
+ if (t == null) continue;
491
+
492
+ StepData s = new StepData();
493
+ s.step_id = col(row.rowData, 0);
494
+ s.step_type = col(row.rowData, 2);
495
+ s.name = col(row.rowData, 3);
496
+ s.start_time = col(row.rowData, 4);
497
+ s.end_time = col(row.rowData, 5);
498
+ s.duration_ms = durationMs(s.start_time, s.end_time);
499
+ s.input = notSet(col(row.rowData, 6));
500
+ s.output = notSet(col(row.rowData, 7));
501
+ s.error = notSet(col(row.rowData, 8));
502
+ s.pre_vars = notSet(col(row.rowData, 9));
503
+ s.post_vars = notSet(col(row.rowData, 10));
504
+ s.generation_id = notSet(col(row.rowData, 11));
505
+ s.gateway_request_id = notSet(col(row.rowData, 12));
506
+
507
+ if (s.error != null && 'ACTION_STEP'.equalsIgnoreCase(s.step_type)) convo.action_error_count++;
508
+ t.steps.add(s);
509
+ }
510
+ }
511
+
512
+ return JSON.serialize(convo);
513
+ }
514
+
515
+ /**
516
+ * Retrieve full conversation details for multiple sessions.
517
+ *
518
+ * @param dataSpaceName Data Cloud Data Space API name
519
+ * @param sessionIds List of ssot__Id__c values (keep under 20 to avoid CPU limits)
520
+ * @return JSON-serialized List<ConversationData>
521
+ */
522
+ public static String getMultipleConversationDetails(String dataSpaceName, List<String> sessionIds) {
523
+ List<ConversationData> results = new List<ConversationData>();
524
+ if (sessionIds == null || sessionIds.isEmpty()) return JSON.serialize(results);
525
+
526
+ for (String sid : sessionIds) {
527
+ String detail = getConversationDetails(dataSpaceName, sid);
528
+ if (String.isNotBlank(detail)) {
529
+ ConversationData convo = (ConversationData) JSON.deserialize(detail, ConversationData.class);
530
+ results.add(convo);
531
+ }
532
+ }
533
+ return JSON.serialize(results);
534
+ }
535
+
536
+ /**
537
+ * Retrieve LLM prompt and response for a set of LLM_STEP records by joining the
538
+ * Einstein Audit & Feedback DMOs (GenAIGatewayRequest and GenAIGeneration).
539
+ *
540
+ * Typical use: after finding LLM_STEP records with non-null generation_id in a
541
+ * ConversationData, pass those step IDs here to see what prompt was sent and what
542
+ * the model actually returned — useful for diagnosing LOW instruction adherence.
543
+ *
544
+ * @param dataSpaceName Data Cloud Data Space API name
545
+ * @param stepIds List of ssot__Id__c values from LLM_STEP StepData records
546
+ * @return JSON-serialized List<LlmStepDetail>
547
+ */
548
+ public static String getLlmStepDetails(String dataSpaceName, List<String> stepIds) {
549
+ if (stepIds == null || stepIds.isEmpty()) return JSON.serialize(new List<LlmStepDetail>());
550
+
551
+ String inClause = '(\'' + String.join(stepIds, '\',\'') + '\')';
552
+ String sql =
553
+ 'SELECT s.ssot__Id__c, s.ssot__AiAgentInteractionId__c, s.ssot__Name__c, '
554
+ + ' r.prompt__c, g.responseText__c, '
555
+ + ' s.ssot__GenerationId__c, s.ssot__GenAiGatewayRequestId__c '
556
+ + 'FROM "ssot__AiAgentInteractionStep__dlm" s '
557
+ + 'LEFT JOIN "GenAIGeneration__dlm" g '
558
+ + ' ON s.ssot__GenerationId__c = g.generationId__c '
559
+ + 'LEFT JOIN "GenAIGatewayRequest__dlm" r '
560
+ + ' ON s.ssot__GenAiGatewayRequestId__c = r.gatewayRequestId__c '
561
+ + 'WHERE s.ssot__Id__c IN ' + inClause;
562
+
563
+ ConnectApi.CdpQueryOutputV2 result = runQuery(sql, dataSpaceName);
564
+ List<LlmStepDetail> details = new List<LlmStepDetail>();
565
+
566
+ if (result != null && result.data != null) {
567
+ for (ConnectApi.CdpQueryV2Row row : result.data) {
568
+ LlmStepDetail d = new LlmStepDetail();
569
+ d.step_id = col(row.rowData, 0);
570
+ d.interaction_id = col(row.rowData, 1);
571
+ d.step_name = col(row.rowData, 2);
572
+ d.prompt = notSet(col(row.rowData, 3));
573
+ d.llm_response = notSet(col(row.rowData, 4));
574
+ d.generation_id = notSet(col(row.rowData, 5));
575
+ d.gateway_request_id = notSet(col(row.rowData, 6));
576
+ details.add(d);
577
+ }
578
+ }
579
+ return JSON.serialize(details);
580
+ }
581
+
582
+ /**
583
+ * Retrieve moment insights (intent summaries, durations) and retriever quality metrics
584
+ * for a set of sessions. Gracefully degrades if DMOs are unavailable.
585
+ *
586
+ * @param dataSpaceName Data Cloud Data Space API name
587
+ * @param sessionIds List of ssot__Id__c values from AiAgentSession
588
+ * @return JSON-serialized List<SessionInsights>
589
+ */
590
+ public static String getMomentInsights(String dataSpaceName, List<String> sessionIds) {
591
+ List<SessionInsights> results = new List<SessionInsights>();
592
+ if (sessionIds == null || sessionIds.isEmpty()) return JSON.serialize(results);
593
+
594
+ String inClause = '(\'' + String.join(sessionIds, '\',\'') + '\')';
595
+
596
+ // --- Session headers ---
597
+ String sessionSql =
598
+ 'SELECT ssot__Id__c, ssot__StartTimestamp__c, ssot__EndTimestamp__c, '
599
+ + ' ssot__AiAgentSessionEndType__c '
600
+ + 'FROM "ssot__AiAgentSession__dlm" '
601
+ + 'WHERE ssot__Id__c IN ' + inClause;
602
+
603
+ ConnectApi.CdpQueryOutputV2 sessionResult = runQuery(sessionSql, dataSpaceName);
604
+ Map<String, SessionInsights> insightsById = new Map<String, SessionInsights>();
605
+
606
+ if (sessionResult != null && sessionResult.data != null) {
607
+ for (ConnectApi.CdpQueryV2Row row : sessionResult.data) {
608
+ SessionInsights si = new SessionInsights();
609
+ si.session_id = col(row.rowData, 0);
610
+ si.start_time = col(row.rowData, 1);
611
+ si.end_time = col(row.rowData, 2);
612
+ si.end_type = notSet(col(row.rowData, 3));
613
+ si.duration_ms = durationMs(si.start_time, si.end_time);
614
+ insightsById.put(si.session_id, si);
615
+ results.add(si);
616
+ }
617
+ }
618
+
619
+ // --- Turn counts ---
620
+ String turnSql =
621
+ 'SELECT ssot__AiAgentSessionId__c, COUNT(*) '
622
+ + 'FROM "ssot__AiAgentInteraction__dlm" '
623
+ + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' '
624
+ + ' AND ssot__AiAgentInteractionType__c = \'TURN\' '
625
+ + 'GROUP BY ssot__AiAgentSessionId__c';
626
+
627
+ ConnectApi.CdpQueryOutputV2 turnResult = runQuery(turnSql, dataSpaceName);
628
+ if (turnResult != null && turnResult.data != null) {
629
+ for (ConnectApi.CdpQueryV2Row row : turnResult.data) {
630
+ SessionInsights si = insightsById.get(col(row.rowData, 0));
631
+ if (si != null) {
632
+ si.turn_count = Integer.valueOf(col(row.rowData, 1));
633
+ }
634
+ }
635
+ }
636
+
637
+ // --- Action error counts ---
638
+ String errSql =
639
+ 'SELECT ssot__AiAgentInteraction__dlm.ssot__AiAgentSessionId__c, COUNT(*) '
640
+ + 'FROM "ssot__AiAgentInteractionStep__dlm" '
641
+ + 'JOIN "ssot__AiAgentInteraction__dlm" '
642
+ + ' ON ssot__AiAgentInteractionStep__dlm.ssot__AiAgentInteractionId__c = ssot__AiAgentInteraction__dlm.ssot__Id__c '
643
+ + 'WHERE ssot__AiAgentInteraction__dlm.ssot__AiAgentSessionId__c IN ' + inClause + ' '
644
+ + ' AND ssot__AiAgentInteractionStep__dlm.ssot__AiAgentInteractionStepType__c = \'ACTION_STEP\' '
645
+ + ' AND ssot__AiAgentInteractionStep__dlm.ssot__ErrorMessageText__c IS NOT NULL '
646
+ + ' AND ssot__AiAgentInteractionStep__dlm.ssot__ErrorMessageText__c != \'NOT_SET\' '
647
+ + 'GROUP BY ssot__AiAgentInteraction__dlm.ssot__AiAgentSessionId__c';
648
+
649
+ ConnectApi.CdpQueryOutputV2 errResult = runQuery(errSql, dataSpaceName);
650
+ if (errResult != null && errResult.data != null) {
651
+ for (ConnectApi.CdpQueryV2Row row : errResult.data) {
652
+ SessionInsights si = insightsById.get(col(row.rowData, 0));
653
+ if (si != null) {
654
+ si.action_error_count = Integer.valueOf(col(row.rowData, 1));
655
+ }
656
+ }
657
+ }
658
+
659
+ // --- Moments (graceful degradation) ---
660
+ Boolean momentAvailable = isDmoAvailable('ssot__AiAgentMoment__dlm', dataSpaceName);
661
+ if (momentAvailable) {
662
+ String momentSql =
663
+ 'SELECT ssot__Id__c, ssot__AiAgentSessionId__c, '
664
+ + ' ssot__StartTimestamp__c, ssot__EndTimestamp__c, '
665
+ + ' ssot__RequestSummaryText__c, ssot__ResponseSummaryText__c, '
666
+ + ' ssot__AiAgentApiName__c, ssot__AiAgentVersionApiName__c '
667
+ + 'FROM "ssot__AiAgentMoment__dlm" '
668
+ + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' '
669
+ + 'ORDER BY ssot__StartTimestamp__c';
670
+
671
+ ConnectApi.CdpQueryOutputV2 momentResult = runQuery(momentSql, dataSpaceName);
672
+ if (momentResult != null && momentResult.data != null) {
673
+ for (ConnectApi.CdpQueryV2Row row : momentResult.data) {
674
+ MomentData m = new MomentData();
675
+ m.moment_id = col(row.rowData, 0);
676
+ m.session_id = col(row.rowData, 1);
677
+ m.start_time = col(row.rowData, 2);
678
+ m.end_time = col(row.rowData, 3);
679
+ m.duration_ms = durationMs(m.start_time, m.end_time);
680
+ m.request_summary = notSet(col(row.rowData, 4));
681
+ m.response_summary = notSet(col(row.rowData, 5));
682
+ m.agent_api_name = notSet(col(row.rowData, 6));
683
+ m.agent_version = notSet(col(row.rowData, 7));
684
+
685
+ SessionInsights si = insightsById.get(m.session_id);
686
+ if (si != null) si.moments.add(m);
687
+ }
688
+ }
689
+ // --- Quality scores via AiAgentTagAssociation → AiAgentTag ---
690
+ if (isDmoAvailable('ssot__AiAgentTagAssociation__dlm', dataSpaceName)) {
691
+ // Collect all moment IDs for the quality score query
692
+ List<String> momentIds = new List<String>();
693
+ Map<String, MomentData> momentById = new Map<String, MomentData>();
694
+ for (SessionInsights si : results) {
695
+ for (MomentData m : si.moments) {
696
+ momentIds.add(m.moment_id);
697
+ momentById.put(m.moment_id, m);
698
+ }
699
+ }
700
+
701
+ if (!momentIds.isEmpty()) {
702
+ String momentInClause = '(\'' + String.join(momentIds, '\',\'') + '\')';
703
+ String qualitySql =
704
+ 'SELECT ta.ssot__AiAgentMomentId__c, t.ssot__Value__c, '
705
+ + ' ta.ssot__AssociationReasonText__c '
706
+ + 'FROM "ssot__AiAgentTagAssociation__dlm" ta '
707
+ + 'JOIN "ssot__AiAgentTag__dlm" t '
708
+ + ' ON ta.ssot__AiAgentTagId__c = t.ssot__Id__c '
709
+ + 'WHERE ta.ssot__AiAgentMomentId__c IN ' + momentInClause;
710
+
711
+ ConnectApi.CdpQueryOutputV2 qualityResult = runQuery(qualitySql, dataSpaceName);
712
+ if (qualityResult != null && qualityResult.data != null) {
713
+ for (ConnectApi.CdpQueryV2Row row : qualityResult.data) {
714
+ String momentId = col(row.rowData, 0);
715
+ MomentData m = momentById.get(momentId);
716
+ if (m != null) {
717
+ m.quality_score = toInteger(col(row.rowData, 1));
718
+ m.quality_reasoning = notSet(col(row.rowData, 2));
719
+ }
720
+ }
721
+ }
722
+ }
723
+ }
724
+ } else {
725
+ for (SessionInsights si : results) {
726
+ si.debug_message = 'AiAgentMoment DMO not available in this org';
727
+ }
728
+ }
729
+
730
+ // Set moment_count and avg_quality_score per session
731
+ for (SessionInsights si : results) {
732
+ si.moment_count = si.moments.size();
733
+ Integer scoreSum = 0;
734
+ Integer scoreCount = 0;
735
+ for (MomentData m : si.moments) {
736
+ if (m.quality_score != null) {
737
+ scoreSum += m.quality_score;
738
+ scoreCount++;
739
+ }
740
+ }
741
+ if (scoreCount > 0) {
742
+ si.avg_quality_score = Decimal.valueOf(scoreSum) / scoreCount;
743
+ }
744
+ }
745
+
746
+ // --- Retriever quality metrics (graceful degradation) ---
747
+ Boolean retrieverAvailable = isDmoAvailable('ssot__AiRetrieverQualityMetric__dlm', dataSpaceName);
748
+ if (retrieverAvailable) {
749
+ // Retriever metrics link to sessions via gateway request IDs on LLM steps.
750
+ // Query all retriever metrics for gateway requests that belong to these sessions.
751
+ String retrieverSql =
752
+ 'SELECT r.ssot__Id__c, r.ssot__AiGatewayRequestId__c, '
753
+ + ' r.ssot__AiRetrieverRequestId__c, r.ssot__RetrieverApiName__c, '
754
+ + ' r.ssot__UserUtteranceText__c, '
755
+ + ' r.ssot__FaithfulnessRelevancyScoreNumber__c, '
756
+ + ' r.ssot__AnswerRelevancyScoreNumber__c, '
757
+ + ' r.ssot__ContextPrecisionScoreNumber__c, '
758
+ + ' i.ssot__AiAgentSessionId__c '
759
+ + 'FROM "ssot__AiRetrieverQualityMetric__dlm" r '
760
+ + 'JOIN "ssot__AiAgentInteractionStep__dlm" s '
761
+ + ' ON r.ssot__AiGatewayRequestId__c = s.ssot__GenAiGatewayRequestId__c '
762
+ + 'JOIN "ssot__AiAgentInteraction__dlm" i '
763
+ + ' ON s.ssot__AiAgentInteractionId__c = i.ssot__Id__c '
764
+ + 'WHERE i.ssot__AiAgentSessionId__c IN ' + inClause;
765
+
766
+ ConnectApi.CdpQueryOutputV2 retResult = runQuery(retrieverSql, dataSpaceName);
767
+ if (retResult != null && retResult.data != null) {
768
+ for (ConnectApi.CdpQueryV2Row row : retResult.data) {
769
+ RetrieverMetricData rm = new RetrieverMetricData();
770
+ rm.metric_id = col(row.rowData, 0);
771
+ rm.gateway_request_id = col(row.rowData, 1);
772
+ rm.retriever_request_id = col(row.rowData, 2);
773
+ rm.retriever_api_name = notSet(col(row.rowData, 3));
774
+ rm.user_utterance = notSet(col(row.rowData, 4));
775
+ rm.faithfulness = toDecimal(col(row.rowData, 5));
776
+ rm.answer_relevance = toDecimal(col(row.rowData, 6));
777
+ rm.context_precision = toDecimal(col(row.rowData, 7));
778
+
779
+ String sessionId = col(row.rowData, 8);
780
+ SessionInsights si = insightsById.get(sessionId);
781
+ if (si != null) si.retriever_metrics.add(rm);
782
+ }
783
+ }
784
+ }
785
+
786
+ return JSON.serialize(results);
787
+ }
788
+
789
+ /**
790
+ * Compute aggregated metrics across sessions in a date range.
791
+ * Includes session rates (abandonment, escalation, deflection), top intents from moments,
792
+ * and average RAG quality scores. Gracefully degrades when DMOs are unavailable.
793
+ *
794
+ * @param dataSpaceName Data Cloud Data Space API name
795
+ * @param startIso ISO 8601 UTC start timestamp
796
+ * @param endIso ISO 8601 UTC end timestamp
797
+ * @param maxRows Maximum sessions to aggregate over
798
+ * @param agentApiName Agent MasterLabel to filter by (null = all agents)
799
+ * @return JSON-serialized AggregatedMetrics
800
+ */
801
+ public static String getAggregatedMetrics(String dataSpaceName, String startIso, String endIso, Integer maxRows, String agentApiName) {
802
+ AggregatedMetrics metrics = new AggregatedMetrics();
803
+ metrics.end_type_counts = new Map<String, Integer>();
804
+ metrics.top_intents = new Map<String, Integer>();
805
+ metrics.unavailable_dmos = new List<String>();
806
+
807
+ // Step 1: Find sessions (reuse existing method)
808
+ String sessionsJson = findSessions(dataSpaceName, startIso, endIso, maxRows, agentApiName);
809
+ List<SessionSummary> sessions = (List<SessionSummary>) JSON.deserialize(sessionsJson, List<SessionSummary>.class);
810
+ metrics.total_sessions = sessions.size();
811
+
812
+ if (sessions.isEmpty()) return JSON.serialize(metrics);
813
+
814
+ // Compute session-level aggregates
815
+ Decimal totalDurationSec = 0;
816
+ Integer durationCount = 0;
817
+ for (SessionSummary s : sessions) {
818
+ // End type distribution
819
+ String endType = s.end_type != null ? s.end_type : 'UNKNOWN';
820
+ Integer cnt = metrics.end_type_counts.get(endType);
821
+ metrics.end_type_counts.put(endType, cnt != null ? cnt + 1 : 1);
822
+
823
+ // Duration
824
+ if (s.duration_ms != null) {
825
+ totalDurationSec += Decimal.valueOf(s.duration_ms) / 1000;
826
+ durationCount++;
827
+ }
828
+ }
829
+
830
+ if (durationCount > 0) {
831
+ metrics.avg_session_duration_sec = totalDurationSec / durationCount;
832
+ }
833
+
834
+ // Session rates
835
+ Integer userEnded = metrics.end_type_counts.get('USER_ENDED');
836
+ Integer agentEnded = metrics.end_type_counts.get('AGENT_ENDED');
837
+ Integer escalated = metrics.end_type_counts.get('ESCALATED');
838
+ Integer total = metrics.total_sessions;
839
+
840
+ metrics.abandonment_rate = Decimal.valueOf(userEnded != null ? userEnded : 0) / total;
841
+ metrics.deflection_rate = Decimal.valueOf(agentEnded != null ? agentEnded : 0) / total;
842
+ metrics.escalation_rate = Decimal.valueOf(escalated != null ? escalated : 0) / total;
843
+
844
+ // Collect session IDs for sub-queries
845
+ List<String> sessionIds = new List<String>();
846
+ for (SessionSummary s : sessions) {
847
+ sessionIds.add(s.session_id);
848
+ }
849
+ String inClause = '(\'' + String.join(sessionIds, '\',\'') + '\')';
850
+
851
+ // Step 2: Total turns
852
+ String turnSql =
853
+ 'SELECT COUNT(*) '
854
+ + 'FROM "ssot__AiAgentInteraction__dlm" '
855
+ + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' '
856
+ + ' AND ssot__AiAgentInteractionType__c = \'TURN\'';
857
+
858
+ ConnectApi.CdpQueryOutputV2 turnResult = runQuery(turnSql, dataSpaceName);
859
+ if (turnResult != null && turnResult.data != null && !turnResult.data.isEmpty()) {
860
+ metrics.total_turns = Integer.valueOf(col(turnResult.data[0].rowData, 0));
861
+ }
862
+
863
+ // Step 3: Moments — count + top intents
864
+ if (isDmoAvailable('ssot__AiAgentMoment__dlm', dataSpaceName)) {
865
+ // Total moment count
866
+ String momentCountSql =
867
+ 'SELECT COUNT(*) '
868
+ + 'FROM "ssot__AiAgentMoment__dlm" '
869
+ + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause;
870
+
871
+ ConnectApi.CdpQueryOutputV2 mcResult = runQuery(momentCountSql, dataSpaceName);
872
+ if (mcResult != null && mcResult.data != null && !mcResult.data.isEmpty()) {
873
+ metrics.total_moments = Integer.valueOf(col(mcResult.data[0].rowData, 0));
874
+ }
875
+
876
+ // Top intents by request summary (GROUP BY truncated summary)
877
+ String intentSql =
878
+ 'SELECT ssot__RequestSummaryText__c, COUNT(*) AS cnt '
879
+ + 'FROM "ssot__AiAgentMoment__dlm" '
880
+ + 'WHERE ssot__AiAgentSessionId__c IN ' + inClause + ' '
881
+ + ' AND ssot__RequestSummaryText__c IS NOT NULL '
882
+ + ' AND ssot__RequestSummaryText__c != \'NOT_SET\' '
883
+ + 'GROUP BY ssot__RequestSummaryText__c '
884
+ + 'ORDER BY cnt DESC '
885
+ + 'LIMIT 20';
886
+
887
+ ConnectApi.CdpQueryOutputV2 intentResult = runQuery(intentSql, dataSpaceName);
888
+ if (intentResult != null && intentResult.data != null) {
889
+ for (ConnectApi.CdpQueryV2Row row : intentResult.data) {
890
+ String intent = col(row.rowData, 0);
891
+ Integer intentCnt = Integer.valueOf(col(row.rowData, 1));
892
+ if (intent != null) {
893
+ // Truncate long summaries for the top_intents map key
894
+ if (intent.length() > 100) intent = intent.substring(0, 100) + '...';
895
+ metrics.top_intents.put(intent, intentCnt);
896
+ }
897
+ }
898
+ }
899
+ } else {
900
+ metrics.unavailable_dmos.add('ssot__AiAgentMoment__dlm');
901
+ }
902
+
903
+ // Step 3b: Quality scores via AiAgentTagAssociation → AiAgentTag
904
+ metrics.quality_distribution = new Map<String, Integer>();
905
+ if (isDmoAvailable('ssot__AiAgentTagAssociation__dlm', dataSpaceName)) {
906
+ // AVG quality score and distribution across all moments in these sessions
907
+ String qualityAvgSql =
908
+ 'SELECT t.ssot__Value__c, COUNT(*) AS cnt '
909
+ + 'FROM "ssot__AiAgentTagAssociation__dlm" ta '
910
+ + 'JOIN "ssot__AiAgentTag__dlm" t '
911
+ + ' ON ta.ssot__AiAgentTagId__c = t.ssot__Id__c '
912
+ + 'WHERE ta.ssot__AiAgentSessionId__c IN ' + inClause + ' '
913
+ + 'GROUP BY t.ssot__Value__c '
914
+ + 'ORDER BY t.ssot__Value__c';
915
+
916
+ ConnectApi.CdpQueryOutputV2 qualityResult = runQuery(qualityAvgSql, dataSpaceName);
917
+ if (qualityResult != null && qualityResult.data != null) {
918
+ Integer totalScore = 0;
919
+ Integer totalCount = 0;
920
+ for (ConnectApi.CdpQueryV2Row row : qualityResult.data) {
921
+ String scoreStr = col(row.rowData, 0);
922
+ Integer cnt = Integer.valueOf(col(row.rowData, 1));
923
+ if (scoreStr != null && cnt != null) {
924
+ Integer score = toInteger(scoreStr);
925
+ metrics.quality_distribution.put(String.valueOf(score), cnt);
926
+ if (score != null) {
927
+ totalScore += score * cnt;
928
+ totalCount += cnt;
929
+ }
930
+ }
931
+ }
932
+ if (totalCount > 0) {
933
+ metrics.avg_quality_score = Decimal.valueOf(totalScore) / totalCount;
934
+ }
935
+ }
936
+ } else {
937
+ metrics.unavailable_dmos.add('ssot__AiAgentTagAssociation__dlm');
938
+ }
939
+
940
+ // Step 4: Retriever quality averages
941
+ if (isDmoAvailable('ssot__AiRetrieverQualityMetric__dlm', dataSpaceName)) {
942
+ String retSql =
943
+ 'SELECT AVG(r.ssot__FaithfulnessRelevancyScoreNumber__c), '
944
+ + ' AVG(r.ssot__AnswerRelevancyScoreNumber__c), '
945
+ + ' AVG(r.ssot__ContextPrecisionScoreNumber__c) '
946
+ + 'FROM "ssot__AiRetrieverQualityMetric__dlm" r '
947
+ + 'JOIN "ssot__AiAgentInteractionStep__dlm" s '
948
+ + ' ON r.ssot__AiGatewayRequestId__c = s.ssot__GenAiGatewayRequestId__c '
949
+ + 'JOIN "ssot__AiAgentInteraction__dlm" i '
950
+ + ' ON s.ssot__AiAgentInteractionId__c = i.ssot__Id__c '
951
+ + 'WHERE i.ssot__AiAgentSessionId__c IN ' + inClause;
952
+
953
+ ConnectApi.CdpQueryOutputV2 retResult = runQuery(retSql, dataSpaceName);
954
+ if (retResult != null && retResult.data != null && !retResult.data.isEmpty()) {
955
+ metrics.avg_faithfulness = toDecimal(col(retResult.data[0].rowData, 0));
956
+ metrics.avg_answer_relevance = toDecimal(col(retResult.data[0].rowData, 1));
957
+ metrics.avg_context_precision = toDecimal(col(retResult.data[0].rowData, 2));
958
+ }
959
+ } else {
960
+ metrics.unavailable_dmos.add('ssot__AiRetrieverQualityMetric__dlm');
961
+ }
962
+
963
+ return JSON.serialize(metrics);
964
+ }
965
+
966
+ // =========================================================================
967
+ // Private helpers
968
+ // =========================================================================
969
+
970
+ /** Cache for DMO availability probes to avoid repeated queries. */
971
+ private static Map<String, Boolean> dmoAvailabilityCache = new Map<String, Boolean>();
972
+
973
+ /**
974
+ * Probe whether a DMO exists and is queryable in this org's Data Cloud.
975
+ * Results are cached for the transaction to avoid repeated probes.
976
+ */
977
+ private static Boolean isDmoAvailable(String dmoName, String dataSpaceName) {
978
+ if (dmoAvailabilityCache.containsKey(dmoName)) return dmoAvailabilityCache.get(dmoName);
979
+ ConnectApi.CdpQueryInput inp = new ConnectApi.CdpQueryInput();
980
+ inp.sql = 'SELECT COUNT(*) FROM "' + dmoName + '"';
981
+ try {
982
+ ConnectApi.CdpQueryOutputV2 result = ConnectApi.CdpQuery.queryAnsiSqlV2(inp, dataSpaceName);
983
+ dmoAvailabilityCache.put(dmoName, true);
984
+ return true;
985
+ } catch (Exception e) {
986
+ System.debug(LoggingLevel.WARN, 'DMO not available: ' + dmoName + ' — ' + e.getMessage());
987
+ dmoAvailabilityCache.put(dmoName, false);
988
+ return false;
989
+ }
990
+ }
991
+
992
+ /** Safely parse a string to Decimal; returns null on failure. */
993
+ private static Decimal toDecimal(String val) {
994
+ if (String.isBlank(val) || val == 'NOT_SET') return null;
995
+ try {
996
+ return Decimal.valueOf(val);
997
+ } catch (Exception e) {
998
+ return null;
999
+ }
1000
+ }
1001
+
1002
+ /** Safely parse a string to Integer (truncating decimals); returns null on failure. */
1003
+ private static Integer toInteger(String val) {
1004
+ if (String.isBlank(val) || val == 'NOT_SET') return null;
1005
+ try {
1006
+ return Decimal.valueOf(val).intValue();
1007
+ } catch (Exception e) {
1008
+ return null;
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Resolve an Agentforce agent name to its GenAiPlannerDefinition IDs.
1014
+ *
1015
+ * Each deployed agent version creates a GenAiPlannerDefinition whose MasterLabel
1016
+ * matches the agent's display name (e.g. 'TeslaSupportAgent'). Returning all
1017
+ * matching versions ensures historical sessions from older deployments are included.
1018
+ *
1019
+ * Also adds the 15-char version of each ID to handle STDM DMO inconsistency:
1020
+ * the participant DMO stores ssot__ParticipantId__c as either 15-char or 18-char.
1021
+ *
1022
+ * @param agentApiName MasterLabel of the agent (same as the agent's display name)
1023
+ * @return List of GenAiPlannerDefinition Ids in both 15-char and 18-char formats; empty if not found
1024
+ */
1025
+ private static List<String> resolvePlannerIds(String agentApiName) {
1026
+ List<String> ids = new List<String>();
1027
+ try {
1028
+ // Search by MasterLabel (exact match) OR DeveloperName pattern.
1029
+ // Agent Script agents create GenAiPlannerDefinition with DeveloperName
1030
+ // like 'OrderService_v1' and MasterLabel like 'Order Service'.
1031
+ // Accept the API name (no spaces) and match both patterns.
1032
+ String devNamePattern = agentApiName + '_%';
1033
+ List<GenAiPlannerDefinition> planners = [
1034
+ SELECT Id FROM GenAiPlannerDefinition
1035
+ WHERE MasterLabel = :agentApiName
1036
+ OR DeveloperName = :agentApiName
1037
+ OR DeveloperName LIKE :devNamePattern
1038
+ ];
1039
+ for (GenAiPlannerDefinition p : planners) {
1040
+ String id18 = String.valueOf(p.Id);
1041
+ ids.add(id18);
1042
+ // STDM DMO stores IDs inconsistently (15-char or 18-char) — include both
1043
+ if (id18.length() == 18) ids.add(id18.substring(0, 15));
1044
+ }
1045
+ if (ids.isEmpty()) {
1046
+ System.debug(LoggingLevel.WARN, 'No GenAiPlannerDefinition found for: ' + agentApiName);
1047
+ } else {
1048
+ System.debug(LoggingLevel.DEBUG, 'Resolved ' + planners.size() + ' planner version(s) for agent "'
1049
+ + agentApiName + '": ' + ids);
1050
+ }
1051
+ } catch (Exception e) {
1052
+ System.debug(LoggingLevel.WARN, 'resolvePlannerIds failed for "' + agentApiName + '": ' + e.getMessage());
1053
+ }
1054
+ return ids;
1055
+ }
1056
+
1057
+ /** Extract ssot__AiAgentSessionId__c values (col 0) from a participant DMO query result. */
1058
+ private static List<String> extractSessionIds(ConnectApi.CdpQueryOutputV2 partResult) {
1059
+ List<String> sessionIds = new List<String>();
1060
+ if (partResult != null && partResult.data != null) {
1061
+ for (ConnectApi.CdpQueryV2Row row : partResult.data) {
1062
+ String sid = col(row.rowData, 0);
1063
+ if (sid != null) sessionIds.add(sid);
1064
+ }
1065
+ }
1066
+ return sessionIds;
1067
+ }
1068
+
1069
+ private static ConnectApi.CdpQueryOutputV2 runQuery(String sql, String dataSpaceName) {
1070
+ System.debug(LoggingLevel.DEBUG, 'AgentforceOptimize CDP query (' + dataSpaceName + '): ' + sql);
1071
+ ConnectApi.CdpQueryInput inp = new ConnectApi.CdpQueryInput();
1072
+ inp.sql = sql;
1073
+ try {
1074
+ ConnectApi.CdpQueryOutputV2 result = ConnectApi.CdpQuery.queryAnsiSqlV2(inp, dataSpaceName);
1075
+ Integer rowCount = (result != null && result.data != null) ? result.data.size() : 0;
1076
+ System.debug(LoggingLevel.DEBUG, 'Rows returned: ' + rowCount);
1077
+ return result;
1078
+ } catch (Exception e) {
1079
+ System.debug(LoggingLevel.ERROR,
1080
+ 'CDP query failed [' + dataSpaceName + ']: ' + e.getMessage() + ' | SQL: ' + sql);
1081
+ return null;
1082
+ }
1083
+ }
1084
+
1085
+ /** Safely extract a column value as String; returns null for empty/null cells. */
1086
+ private static String col(List<Object> row, Integer i) {
1087
+ if (row == null || i >= row.size() || row[i] == null) return null;
1088
+ return String.valueOf(row[i]);
1089
+ }
1090
+
1091
+ /** Return null when the value is the STDM "NOT_SET" sentinel or blank. */
1092
+ private static String notSet(String val) {
1093
+ if (String.isBlank(val) || val == 'NOT_SET') return null;
1094
+ return val;
1095
+ }
1096
+
1097
+ /** Compute millisecond duration between two ISO timestamp strings; null on any failure. */
1098
+ private static Long durationMs(String startTs, String endTs) {
1099
+ if (String.isBlank(startTs) || String.isBlank(endTs)) return null;
1100
+ try {
1101
+ Datetime startDt = parseTs(startTs);
1102
+ Datetime endDt = parseTs(endTs);
1103
+ if (startDt == null || endDt == null) return null;
1104
+ return endDt.getTime() - startDt.getTime();
1105
+ } catch (Exception e) {
1106
+ return null;
1107
+ }
1108
+ }
1109
+
1110
+ /** Parse an ISO 8601 timestamp string into a Datetime, with fallback strategies. */
1111
+ private static Datetime parseTs(String ts) {
1112
+ if (String.isBlank(ts)) return null;
1113
+ // Strategy 1: ISO 8601 via JSON deserialise
1114
+ try {
1115
+ return (Datetime) JSON.deserialize('"' + ts + '"', Datetime.class);
1116
+ } catch (Exception e1) {
1117
+ System.debug(LoggingLevel.FINE, 'parseTs strategy 1 failed for "' + ts + '": ' + e1.getMessage());
1118
+ }
1119
+ // Strategy 2: Datetime.valueOf (handles 'yyyy-MM-dd HH:mm:ss' format)
1120
+ try {
1121
+ return Datetime.valueOf(ts);
1122
+ } catch (Exception e2) {
1123
+ System.debug(LoggingLevel.FINE, 'parseTs strategy 2 failed for "' + ts + '": ' + e2.getMessage());
1124
+ }
1125
+ // Strategy 3: epoch milliseconds
1126
+ try {
1127
+ return Datetime.newInstance(Long.valueOf(ts));
1128
+ } catch (Exception e3) {
1129
+ System.debug(LoggingLevel.FINE, 'parseTs strategy 3 failed for "' + ts + '": ' + e3.getMessage());
1130
+ }
1131
+ return null;
1132
+ }
1133
+
1134
+ // =========================================================================
1135
+ // Observability Query (@InvocableMethod for Flow / Agentforce actions)
1136
+ // =========================================================================
1137
+
1138
+ public class ObservabilityInput {
1139
+ @InvocableVariable(label='Query Type' required=true)
1140
+ public String queryType;
1141
+
1142
+ @InvocableVariable(label='Agent API Name' required=false)
1143
+ public String agentApiName;
1144
+
1145
+ @InvocableVariable(label='Topic API Name' required=false)
1146
+ public String topicApiName;
1147
+
1148
+ @InvocableVariable(label='Lookback Days' required=false)
1149
+ public Integer lookbackDays;
1150
+ }
1151
+
1152
+ public class ObservabilityOutput {
1153
+ @InvocableVariable(label='Query Result JSON')
1154
+ public String resultJson;
1155
+
1156
+ @InvocableVariable(label='Summary Text')
1157
+ public String summaryText;
1158
+ }
1159
+
1160
+ @InvocableMethod(label='Run Observability Query' description='Executes a Data Cloud observability query based on query type and optional filters.')
1161
+ public static List<ObservabilityOutput> runObservabilityQuery(List<ObservabilityInput> inputs) {
1162
+ List<ObservabilityOutput> results = new List<ObservabilityOutput>();
1163
+ for (ObservabilityInput input : inputs) {
1164
+ ObservabilityOutput out = new ObservabilityOutput();
1165
+ try {
1166
+ String query = buildObservabilityQuery(input);
1167
+ ConnectApi.CdpQueryInput cdpInput = new ConnectApi.CdpQueryInput();
1168
+ cdpInput.sql = query;
1169
+ ConnectApi.CdpQueryOutputV2 cdpOutput = ConnectApi.CdpQuery.queryAnsiSqlV2(cdpInput);
1170
+
1171
+ out.resultJson = JSON.serialize(cdpOutput);
1172
+ Integer rowCount = cdpOutput.rowCount != null ? cdpOutput.rowCount : 0;
1173
+
1174
+ if (rowCount == 0) {
1175
+ out.summaryText = 'Query executed for ' + input.queryType + '. No results found for the given filters and time range. SQL: ' + query;
1176
+ } else {
1177
+ out.summaryText = 'Query executed for ' + input.queryType + '. Found ' + rowCount + ' result(s). Use the metadata to map column positions to names in the data arrays.';
1178
+ }
1179
+ } catch (Exception e) {
1180
+ out.summaryText = 'Error executing ' + input.queryType + ' query: ' + e.getMessage();
1181
+ out.resultJson = '{"error":"' + e.getMessage().replace('"', '\\"') + '"}';
1182
+ }
1183
+ results.add(out);
1184
+ }
1185
+ return results;
1186
+ }
1187
+
1188
+ private static String buildObservabilityQuery(ObservabilityInput p) {
1189
+ Integer days = p.lookbackDays != null ? p.lookbackDays : 90;
1190
+ String cutoff = Datetime.now().addDays(-days).formatGmt('yyyy-MM-dd HH:mm:ss.SSS');
1191
+
1192
+ String agentFilter = String.isNotBlank(p.agentApiName)
1193
+ ? ' AND sp.aiAgentApiName__c = \'' + String.escapeSingleQuotes(p.agentApiName) + '\'' : '';
1194
+ String topicFilter = String.isNotBlank(p.topicApiName)
1195
+ ? ' AND i.topicApiName__c = \'' + String.escapeSingleQuotes(p.topicApiName) + '\'' : '';
1196
+
1197
+ if (p.queryType == 'KnowledgeGap') {
1198
+ return 'SELECT i.topicApiName__c, sp.aiAgentApiName__c, ' +
1199
+ 'AVG(q.ContextPrecisionScoreNumber__c) AS avg_precision, ' +
1200
+ 'AVG(q.AnswerRelevancyScoreNumber__c) AS avg_relevancy, ' +
1201
+ 'COUNT(*) AS total_interactions ' +
1202
+ 'FROM AIRetrieverQualityMetric__dll q ' +
1203
+ 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' +
1204
+ 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' +
1205
+ 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' +
1206
+ agentFilter + topicFilter +
1207
+ ' GROUP BY i.topicApiName__c, sp.aiAgentApiName__c ' +
1208
+ 'ORDER BY avg_precision ASC LIMIT 25';
1209
+
1210
+ } else if (p.queryType == 'Hallucination') {
1211
+ return 'SELECT sp.aiAgentApiName__c, i.topicApiName__c, ' +
1212
+ 'AVG(q.FaithfulnessRelevancyScoreNumber__c) AS avg_faithfulness, ' +
1213
+ 'COUNT(*) AS total_interactions ' +
1214
+ 'FROM AIRetrieverQualityMetric__dll q ' +
1215
+ 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' +
1216
+ 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' +
1217
+ 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' +
1218
+ ' AND q.FaithfulnessRelevancyScoreNumber__c < 0.8' +
1219
+ agentFilter + topicFilter +
1220
+ ' GROUP BY sp.aiAgentApiName__c, i.topicApiName__c ' +
1221
+ 'ORDER BY avg_faithfulness ASC LIMIT 25';
1222
+
1223
+ } else if (p.queryType == 'RetrievalQuality') {
1224
+ return 'SELECT q.RetrieverApiName__c, i.topicApiName__c, sp.aiAgentApiName__c, ' +
1225
+ 'AVG(q.ContextPrecisionScoreNumber__c) AS avg_precision, COUNT(*) AS total ' +
1226
+ 'FROM AIRetrieverQualityMetric__dll q ' +
1227
+ 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' +
1228
+ 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' +
1229
+ 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' +
1230
+ agentFilter + topicFilter +
1231
+ ' GROUP BY q.RetrieverApiName__c, i.topicApiName__c, sp.aiAgentApiName__c ' +
1232
+ 'ORDER BY avg_precision ASC LIMIT 25';
1233
+
1234
+ } else if (p.queryType == 'AnswerRelevancy') {
1235
+ return 'SELECT sp.aiAgentApiName__c, i.topicApiName__c, ' +
1236
+ 'AVG(q.AnswerRelevancyScoreNumber__c) AS avg_relevancy, COUNT(*) AS total ' +
1237
+ 'FROM AIRetrieverQualityMetric__dll q ' +
1238
+ 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' +
1239
+ 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' +
1240
+ 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' +
1241
+ ' AND q.AnswerRelevancyScoreNumber__c < 0.7' +
1242
+ agentFilter + topicFilter +
1243
+ ' GROUP BY sp.aiAgentApiName__c, i.topicApiName__c ' +
1244
+ 'ORDER BY avg_relevancy ASC LIMIT 25';
1245
+
1246
+ } else if (p.queryType == 'Leaderboard') {
1247
+ return 'SELECT sp.aiAgentApiName__c, i.topicApiName__c, ' +
1248
+ 'AVG(q.ContextPrecisionScoreNumber__c) AS avg_precision, ' +
1249
+ 'AVG(q.AnswerRelevancyScoreNumber__c) AS avg_relevancy, ' +
1250
+ 'AVG(q.FaithfulnessRelevancyScoreNumber__c) AS avg_faithfulness, ' +
1251
+ 'COUNT(*) AS total_interactions ' +
1252
+ 'FROM AIRetrieverQualityMetric__dll q ' +
1253
+ 'JOIN AIAgentInteraction__dll i ON q.RetrieverTraceId__c = i.telemetryTraceId__c ' +
1254
+ 'JOIN AIAgentSessionParticipant__dll sp ON sp.aiAgentSessionId__c = i.aiAgentSessionId__c ' +
1255
+ 'WHERE i.startTimestamp__c > \'' + cutoff + '\'' +
1256
+ agentFilter + topicFilter +
1257
+ ' GROUP BY sp.aiAgentApiName__c, i.topicApiName__c ' +
1258
+ 'ORDER BY avg_precision ASC LIMIT 25';
1259
+ }
1260
+ return 'SELECT COUNT(*) AS cnt FROM AIAgentSession__dll';
1261
+ }
1262
+ }