@c8y/ngx-components 1023.78.7 → 1023.80.0

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 (64) hide show
  1. package/ai/agent-chat/index.d.ts +22 -11
  2. package/ai/agent-chat/index.d.ts.map +1 -1
  3. package/ai/ai-chat/index.d.ts +31 -10
  4. package/ai/ai-chat/index.d.ts.map +1 -1
  5. package/ai/index.d.ts +64 -49
  6. package/ai/index.d.ts.map +1 -1
  7. package/ecosystem/index.d.ts +34 -4
  8. package/ecosystem/index.d.ts.map +1 -1
  9. package/ecosystem/shared/index.d.ts +1 -1
  10. package/ecosystem/shared/index.d.ts.map +1 -1
  11. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs +237 -129
  12. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs.map +1 -1
  13. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs +112 -51
  14. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs.map +1 -1
  15. package/fesm2022/c8y-ngx-components-ai.mjs +92 -61
  16. package/fesm2022/c8y-ngx-components-ai.mjs.map +1 -1
  17. package/fesm2022/{c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-DFytXNdc.mjs → c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-C8QX6xlf.mjs} +3 -3
  18. package/fesm2022/c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-C8QX6xlf.mjs.map +1 -0
  19. package/fesm2022/c8y-ngx-components-dashboard-details-advanced-tab.mjs +2 -2
  20. package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs +7 -7
  21. package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs.map +1 -1
  22. package/fesm2022/c8y-ngx-components-device-enrolment-modal.mjs +3 -3
  23. package/fesm2022/c8y-ngx-components-device-enrolment-modal.mjs.map +1 -1
  24. package/fesm2022/c8y-ngx-components-device-grid.mjs +1 -1
  25. package/fesm2022/c8y-ngx-components-device-grid.mjs.map +1 -1
  26. package/fesm2022/c8y-ngx-components-device-list.mjs +2 -2
  27. package/fesm2022/c8y-ngx-components-device-list.mjs.map +1 -1
  28. package/fesm2022/c8y-ngx-components-ecosystem-shared.mjs +4 -1
  29. package/fesm2022/c8y-ngx-components-ecosystem-shared.mjs.map +1 -1
  30. package/fesm2022/c8y-ngx-components-ecosystem.mjs +151 -53
  31. package/fesm2022/c8y-ngx-components-ecosystem.mjs.map +1 -1
  32. package/fesm2022/c8y-ngx-components-feature-toggles-list.mjs +3 -3
  33. package/fesm2022/c8y-ngx-components-feature-toggles-list.mjs.map +1 -1
  34. package/fesm2022/c8y-ngx-components-search.mjs +2 -2
  35. package/fesm2022/c8y-ngx-components-search.mjs.map +1 -1
  36. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +31 -29
  37. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
  38. package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs +4 -4
  39. package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs.map +1 -1
  40. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +5 -5
  41. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
  42. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-list.mjs +2 -2
  43. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-list.mjs.map +1 -1
  44. package/fesm2022/c8y-ngx-components-widgets-implementations-pie-chart.mjs +2 -2
  45. package/fesm2022/c8y-ngx-components-widgets-implementations-pie-chart.mjs.map +1 -1
  46. package/fesm2022/c8y-ngx-components.mjs +5 -5
  47. package/fesm2022/c8y-ngx-components.mjs.map +1 -1
  48. package/index.d.ts +1 -0
  49. package/index.d.ts.map +1 -1
  50. package/locales/de.po +152 -198
  51. package/locales/es.po +126 -123
  52. package/locales/fr.po +151 -197
  53. package/locales/ja_JP.po +102 -113
  54. package/locales/ko.po +128 -127
  55. package/locales/locales.pot +101 -92
  56. package/locales/nl.po +129 -128
  57. package/locales/pl.po +126 -127
  58. package/locales/pt_BR.po +125 -126
  59. package/locales/zh_CN.po +126 -128
  60. package/locales/zh_TW.po +128 -129
  61. package/package.json +1 -1
  62. package/search/index.d.ts.map +1 -1
  63. package/widgets/implementations/asset-table/index.d.ts +1 -1
  64. package/fesm2022/c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-DFytXNdc.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { NgComponentOutlet, AsyncPipe } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { Injectable, inject, input, computed, output, signal, viewChildren, Component, Injector, Input } from '@angular/core';
3
+ import { Injectable, inject, input, computed, output, signal, viewChildren, ChangeDetectionStrategy, Component, Injector, Input } from '@angular/core';
4
4
  import { GainsightService, DatePipe, ModalService, AlertService, C8Y_PLUGIN_CONTEXT_PATH, AppStateService, Status, LoadingComponent, EmptyStateComponent, C8yTranslateDirective, GuideDocsComponent, GuideHrefDirective, MarkdownToHtmlPipe, C8yTranslatePipe, ContextRouteService } from '@c8y/ngx-components';
5
5
  import { TranslateService } from '@ngx-translate/core';
6
6
  import { AIService, defaultPruneMessagesForAgent } from '@c8y/ngx-components/ai';
@@ -12,64 +12,95 @@ import { of, isObservable } from 'rxjs';
12
12
 
13
13
  /** A stateless service used by agent-chat for managing chat history. Not public. */
14
14
  class ChatHistoryService {
15
+ /**
16
+ * Returns a message pruner function suitable for passing to `save()`, configured by the given options.
17
+ *
18
+ * The returned function:
19
+ * - Limits the total number of messages kept (if `config.maxMessages` is set)
20
+ * - Strips transient streaming parts (`tool-input-streaming`, `tool-executing`) from all assistant messages
21
+ * - Strips `reasoning` and `tool-result` parts from assistant messages older than the 10 most recent
22
+ * - Strips `tool-result` parts whose `toolName` is in `config.transientToolNames`
23
+ *
24
+ * @param config Optional configuration
25
+ */
26
+ static createDefaultSerializationMessagePruner(config) {
27
+ return (messages) => {
28
+ let msgs = messages;
29
+ // Apply maxMessages limit - keep most recent messages
30
+ if (config?.maxMessages && msgs.length > config.maxMessages) {
31
+ msgs = msgs.slice(-config.maxMessages);
32
+ }
33
+ const transientToolNames = new Set(config?.transientToolNames ?? []);
34
+ // NB: in future we could use tool metadata from the MCP servers to identify transient tools automatically
35
+ // Only keep tool results and reasoning for the most recent N messages
36
+ const recentMessageStartIndex = msgs.length - Math.min(10, msgs.length);
37
+ return msgs.map((msg, msgIndex) => {
38
+ if (msg.role !== 'assistant') {
39
+ return msg;
40
+ }
41
+ const isRecentMessage = msgIndex >= recentMessageStartIndex;
42
+ const filteredContent = msg.content.filter((part) => {
43
+ // Always strip transient streaming states
44
+ if (part.type === 'tool-input-streaming' || part.type === 'tool-executing') {
45
+ return false;
46
+ }
47
+ if (!isRecentMessage) {
48
+ // For older messages, only keep text and step-start
49
+ return part.type === 'text' || part.type === 'step-start';
50
+ }
51
+ // For recent messages, additionally filter transient tool results
52
+ if (part.type === 'tool-result') {
53
+ return !transientToolNames.has(part.toolName);
54
+ }
55
+ return true;
56
+ });
57
+ return { ...msg, content: filteredContent };
58
+ });
59
+ };
60
+ }
15
61
  /**
16
62
  * Save messages and suggestions to an opaque, versioned, JSON-serializable snapshot for persistence.
17
63
  *
18
- * Only recent tool results and reasoning are included to save space.
19
- * Additional options for compressing the history can be provided using the config parameters.
64
+ * By default, uses `createDefaultSerializationMessagePruner()` to strip transient content and limit
65
+ * the size of the serialized history. Pass a custom `pruneMessages` function to override this behaviour,
66
+ * for example to configure `maxMessages` or `transientToolNames`, or pass `msgs => msgs` to disable pruning.
20
67
  *
21
68
  * @param messages The current conversation messages to save
22
69
  * @param suggestions The current suggestions to include in the snapshot
23
- * @param config Optional configuration to limit what gets saved
70
+ * @param pruneMessages Optional function to prune/transform messages before serialization. If not specified, createDefaultSerializationMessagePruner is used.
24
71
  */
25
- save(messages, suggestions, config) {
26
- let msgs = messages;
27
- // Apply maxMessages limit - keep most recent messages
28
- if (config?.maxMessages && msgs.length > config.maxMessages) {
29
- msgs = msgs.slice(-config.maxMessages);
30
- }
31
- const transientToolNames = new Set(config?.transientToolNames || []);
32
- // NB: in future we could use tool metadata from the MCP servers to identify transient tools automatically
33
- // Only keep tool results and reasoning for the most recent N messages (=N/2 assistant messages)
34
- const recentMessageStartIndex = msgs.length - Math.min(10, msgs.length);
72
+ save(messages, suggestions, pruneMessages) {
73
+ const prune = pruneMessages ?? ChatHistoryService.createDefaultSerializationMessagePruner();
74
+ const msgs = prune(messages);
35
75
  const history = {
36
- messages: msgs.map((msg, msgIndex) => {
37
- const isRecentMessage = msgIndex >= recentMessageStartIndex;
76
+ messages: msgs.map(msg => {
77
+ if (msg.role === 'assistant') {
78
+ return {
79
+ role: msg.role,
80
+ // Note: AIMessagePart[] cannot be statically checked as JSON-safe because
81
+ // ToolCallPart.output is typed as `unknown`. Serializability is enforced at
82
+ // the boundary by the outer `as ChatHistory` (= JsonValue) cast.
83
+ content: msg.content,
84
+ ...(msg.timestamp ? { timestamp: msg.timestamp } : {})
85
+ };
86
+ }
38
87
  return {
39
88
  role: msg.role,
40
89
  content: msg.content,
41
- ...(msg.timestamp ? { timestamp: msg.timestamp } : {}),
42
- ...(msg.steps
43
- ? {
44
- steps: msg.steps.map(step => ({
45
- // Specifically include just the fields we're interested in
46
- type: step.type,
47
- text: step.text,
48
- aborted: step.aborted,
49
- // Strip out ALL toolCalls, since these are anyway transient
50
- toolCalls: undefined,
51
- // Keep recent reasoning only
52
- reasoning: isRecentMessage ? step.reasoning : undefined,
53
- // Keep toolResults only if: in recent 10 messages AND not transient
54
- toolResults: isRecentMessage && step.toolResults
55
- ? step.toolResults.filter(result => !transientToolNames.has(result.toolName))
56
- : undefined
57
- }))
58
- }
59
- : {})
90
+ ...('timestamp' in msg && msg.timestamp ? { timestamp: msg.timestamp } : {})
60
91
  };
61
92
  }),
62
- suggestions: suggestions,
63
- chatHistoryVersion: 1
93
+ suggestions,
94
+ chatHistoryVersion: 2
64
95
  };
65
- //console.debug("Saving chat history as: ", history);
66
96
  return history;
67
97
  }
68
98
  /**
69
99
  * Restore a previously saved chat history snapshot.
70
100
  *
71
101
  * Validates the snapshot format and returns the messages and suggestions.
72
- * Throws an error if the snapshot is invalid.
102
+ * Throws an error if the snapshot is invalid or the version is not supported.
103
+ * For a short time, version 1 snapshots are automatically migrated to the current format.
73
104
  *
74
105
  * @param history A snapshot previously returned by `save()`
75
106
  */
@@ -81,11 +112,22 @@ class ChatHistoryService {
81
112
  !Array.isArray(history['messages'])) {
82
113
  throw new Error('Invalid chat history format');
83
114
  }
84
- if (history['chatHistoryVersion'] !== 1) {
85
- throw new Error(`Cannot load chat history - expected version 1 but got ${history['chatHistoryVersion']}`);
115
+ const version = history['chatHistoryVersion'];
116
+ // TODO: remove v1 migration after a month or so once stored histories have been migrated
117
+ if (version === 1) {
118
+ return this.restoreV1(history);
119
+ }
120
+ if (version !== 2) {
121
+ throw new Error(`Cannot load chat history - expected version 2 but got ${version}`);
86
122
  }
87
123
  const messagesArray = history['messages'];
88
- if (messagesArray.some((msg, _) => !msg || typeof msg !== 'object' || !msg.role || typeof msg.content !== 'string')) {
124
+ if (messagesArray.some(msg => {
125
+ if (!msg || typeof msg !== 'object' || !msg.role)
126
+ return true;
127
+ if (msg.role === 'assistant')
128
+ return !Array.isArray(msg.content);
129
+ return typeof msg.content !== 'string';
130
+ })) {
89
131
  throw new Error('Invalid message format in chat history');
90
132
  }
91
133
  const suggestions = Array.isArray(history['suggestions'])
@@ -96,6 +138,51 @@ class ChatHistoryService {
96
138
  suggestions
97
139
  };
98
140
  }
141
+ restoreV1(history) {
142
+ const messagesArray = history['messages'];
143
+ if (messagesArray.some((msg) => !msg || typeof msg !== 'object' || !msg.role || typeof msg.content !== 'string')) {
144
+ throw new Error('Invalid message format in chat history');
145
+ }
146
+ const messages = messagesArray.map((msg) => {
147
+ if (msg.role !== 'assistant') {
148
+ return { role: msg.role, content: msg.content };
149
+ }
150
+ const parts = [];
151
+ const steps = msg.steps ?? [];
152
+ if (steps.length === 0) {
153
+ // No steps saved — fall back to the v1 string content
154
+ if (msg.content) {
155
+ parts.push({ type: 'text', text: msg.content });
156
+ }
157
+ }
158
+ else {
159
+ steps.forEach((step, i) => {
160
+ // step-start is a boundary between steps, never before the first
161
+ if (i > 0) {
162
+ parts.push({ type: 'step-start' });
163
+ }
164
+ if (step.reasoning) {
165
+ parts.push({ type: 'reasoning', text: step.reasoning });
166
+ }
167
+ if (step.text) {
168
+ parts.push({ type: 'text', text: step.text });
169
+ }
170
+ for (const toolResult of step.toolResults ?? []) {
171
+ parts.push(toolResult);
172
+ }
173
+ });
174
+ }
175
+ return {
176
+ role: 'assistant',
177
+ content: parts,
178
+ ...(msg.timestamp ? { timestamp: msg.timestamp } : {})
179
+ };
180
+ });
181
+ const suggestions = Array.isArray(history['suggestions'])
182
+ ? history['suggestions']
183
+ : [];
184
+ return { messages, suggestions };
185
+ }
99
186
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ChatHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
100
187
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ChatHistoryService, providedIn: 'root' }); }
101
188
  }
@@ -110,6 +197,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
110
197
  class UserAnalyticsService {
111
198
  constructor() {
112
199
  this.gainsightService = inject(GainsightService);
200
+ this.maxAnalyticsMessageLength = 4000;
113
201
  /** If needed we could set this to false in production to only include hashes of the messages,
114
202
  * if there's a decision that customer data shouldn't be uploaded to GainSight. */
115
203
  this.includeCustomerSensitiveDataInAnalytics = true;
@@ -146,16 +234,23 @@ class UserAnalyticsService {
146
234
  // NB: Do NOT send any message content to GainSight servers because that's proprietary/personal customer-owned data that we must protect
147
235
  // except when includeCustomerSensitiveDataInAnalytics is set;
148
236
  // include hashes of messages so we can always correlate with the history stored on our own servers if available
149
- const allAssistantText = assistantMessage.steps
150
- ? assistantMessage.steps
151
- ?.filter(s => s.text)
152
- .map(s => s.text || '')
153
- .join('\n---\n') || ''
154
- : assistantMessage.content;
155
- // Collect all unique tool names from each step's toolResults
156
- const toolNames = assistantMessage.steps
157
- ?.flatMap(s => s.toolResults?.map(tr => tr.toolName) || [])
158
- ?.filter((name, index, self) => self.indexOf(name) === index) || [];
237
+ const allTextParts = assistantMessage.content
238
+ .filter((p) => p.type === 'text')
239
+ .map(p => p.text);
240
+ const allText = allTextParts.join('\n---\n');
241
+ // If the full text is very long, prioritize the final step's text to keep analytics payloads under the limit
242
+ const lastStepStartIdx = assistantMessage.content.reduce((last, p, i) => (p.type === 'step-start' ? i : last), 0);
243
+ const finalStepText = assistantMessage.content
244
+ .slice(lastStepStartIdx + 1)
245
+ .filter((p) => p.type === 'text')
246
+ .map(p => p.text)
247
+ .join('\n---\n');
248
+ const allAssistantText = allText.length > this.maxAnalyticsMessageLength ? finalStepText : allText;
249
+ // Collect all unique tool names from tool-result parts
250
+ const toolResultParts = assistantMessage.content.filter((p) => p.type === 'tool-result');
251
+ const toolNames = toolResultParts
252
+ .map(tr => tr.toolName)
253
+ .filter((name, index, self) => self.indexOf(name) === index);
159
254
  return {
160
255
  assistantMessageTimestamp: assistantMessage.timestamp,
161
256
  // Track how long we're spending waiting for the AI
@@ -172,9 +267,9 @@ class UserAnalyticsService {
172
267
  : undefined,
173
268
  userMessageHash: userMessage ? await this._computeHash(userMessage.content) : '',
174
269
  userMessage: this.includeCustomerSensitiveDataInAnalytics ? userMessage.content : undefined,
175
- assistantMessageSteps: assistantMessage.steps?.length || 0,
176
270
  // nb: toolCalls here refers to completed calls (i.e. toolResults), we shouldn't have any calls still in progress at this point
177
- assistantMessageToolCallsCount: assistantMessage.steps?.reduce((sum, s) => sum + (s.toolResults?.length || 0), 0) || 0,
271
+ assistantMessageSteps: assistantMessage.content.filter(p => p.type === 'step-start').length,
272
+ assistantMessageToolCallsCount: toolResultParts.length,
178
273
  assistantMessageToolNames: toolNames.length === 0 ? undefined : toolNames.join(','),
179
274
  assistantMessageLines: allAssistantText.split('\n').length,
180
275
  userMessageLength: userMessage.content.length,
@@ -300,7 +395,7 @@ class AgentChatComponent {
300
395
  * that change detection works correctly. This is not necessary for changedPart.
301
396
  *
302
397
  * @param message The message from the AI assistant.
303
- * @param changedPart The part of the message that has changed, which can be modified in-place if desired. Currently only tool calls are included here.
398
+ * @param changedPart The part of the message that has changed, which can be modified in-place if desired.
304
399
  */
305
400
  this.preprocessAgentMessage = input(undefined, ...(ngDevMode ? [{ debugName: "preprocessAgentMessage" }] : []));
306
401
  /** Optional function to override how the message history is prepared and compacted ready for sending with
@@ -347,7 +442,7 @@ class AgentChatComponent {
347
442
  this._isLoading = signal(true, ...(ngDevMode ? [{ debugName: "_isLoading" }] : [])); // initially true while we do our initial ping of the agent health - prevents message flashing up before ready
348
443
  this.aiAgentManagerApplicationName = gettext('AI Agent Manager');
349
444
  /** Stores any error with the agent or backend microservice. This error message is already translated. */
350
- this._agentHealthError = signal(this.translateService.instant(gettext('Connecting to AI agent...')), ...(ngDevMode ? [{ debugName: "_agentHealthError" }] : []));
445
+ this._agentHealthError = signal(this.translateService.instant(gettext('Connecting to AI agent')), ...(ngDevMode ? [{ debugName: "_agentHealthError" }] : []));
351
446
  /** Stores the most recent agent health check response, or `undefined` while the initial check is in progress. */
352
447
  this._agentHealth = signal(undefined, ...(ngDevMode ? [{ debugName: "_agentHealth" }] : []));
353
448
  /** Stores any detailed error messages from the backend. */
@@ -363,6 +458,24 @@ class AgentChatComponent {
363
458
  // NB: this is NOT part of the API of this component and may change in future
364
459
  // (e.g. we may improve AIMessage class)
365
460
  this.messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : []));
461
+ /**
462
+ * Computed signal holding the last assistant message, which is the one which may be rapidly changing as we
463
+ * stream it back. This allows us to update the UI reactively without affecting the rest of the message history.
464
+ */
465
+ this.lastAssistantMessageContext = computed(() => {
466
+ const msgs = this.messages();
467
+ const lastMsg = msgs[msgs.length - 1];
468
+ const config = this.assistantMessageDisplayConfig();
469
+ const isLoadingAiResponse = this.isLoadingAiResponse();
470
+ if (lastMsg?.role !== 'assistant')
471
+ return null;
472
+ return {
473
+ message: lastMsg,
474
+ config: config,
475
+ isMessageLoading: isLoadingAiResponse,
476
+ messageDisplayIndex: 0
477
+ };
478
+ }, ...(ngDevMode ? [{ debugName: "lastAssistantMessageContext" }] : []));
366
479
  /** If the create agent button should be shown to the user. */
367
480
  this.canCreate = false;
368
481
  this.prompt = '';
@@ -427,10 +540,11 @@ class AgentChatComponent {
427
540
  * To save space, only recent tool results and reasoning are included.
428
541
  * Additional options for compressing the history can be provided using the config parameters.
429
542
  *
430
- * @param config Optional configuration to limit what gets saved (e.g., filter out specific tools, limit total message count)
543
+ * @param pruner Optional function to prune/transform messages before serialization.
544
+ * If not specified, `ChatHistoryService.createDefaultSerializationMessagePruner()` is used.
431
545
  */
432
- saveChatHistory(config) {
433
- return this.chatHistoryService.save(this.messages(), this.suggestions(), config);
546
+ saveChatHistory(pruner) {
547
+ return this.chatHistoryService.save(this.messages(), this.suggestions(), pruner);
434
548
  }
435
549
  /** Sends a message to the AI */
436
550
  async sendMessage(message) {
@@ -450,10 +564,6 @@ class AgentChatComponent {
450
564
  this.agentStreamError.set(undefined); // Clear any previous stream errors
451
565
  this.abortController = new AbortController();
452
566
  this._clientToolOutputs = {};
453
- const currentAssistantMessage = {
454
- role: 'assistant',
455
- content: ''
456
- };
457
567
  // Prepare messages for AI service, optionally including grounding message
458
568
  const pruneMessagesForAgent = this.pruneMessagesForAgent() || defaultPruneMessagesForAgent;
459
569
  const messagesForAI = pruneMessagesForAgent(this.messages());
@@ -466,13 +576,13 @@ class AgentChatComponent {
466
576
  };
467
577
  messagesForAI.splice(messagesForAI.length - 1, 0, groundingMsg);
468
578
  }
469
- this.messages.set([...this.messages(), currentAssistantMessage]);
579
+ this.messages.set([...this.messages(), { role: 'assistant', content: [] }]);
470
580
  const stream = await this.aiService.stream$(this.agentDefinition || this.agentName, messagesForAI, this.variables(), this.abortController, this.currentContextPath);
471
581
  if (this.assistantSubscription) {
472
582
  this.assistantSubscription.unsubscribe();
473
583
  }
474
584
  this.assistantSubscription = stream.subscribe({
475
- next: (response) => {
585
+ next: async (response) => {
476
586
  if (response?.changedPart?.type === 'response-metadata') {
477
587
  if (response.changedPart.model) {
478
588
  this.model = response.changedPart.model;
@@ -482,7 +592,17 @@ class AgentChatComponent {
482
592
  }
483
593
  }
484
594
  else {
485
- this.processAgentMessage(currentAssistantMessage, response.message, response.changedPart);
595
+ try {
596
+ await this.processAgentMessage(response.message, response.changedPart);
597
+ }
598
+ catch (error) {
599
+ console.error('Error processing agent message:', error, response.changedPart, response.message);
600
+ this.cancel();
601
+ const errorMessage = error instanceof Error
602
+ ? error.message
603
+ : gettext('An error occurred while processing the response from the AI agent.');
604
+ this.agentStreamError.set(errorMessage);
605
+ }
486
606
  }
487
607
  },
488
608
  complete: () => {
@@ -503,7 +623,7 @@ class AgentChatComponent {
503
623
  this.agentStreamError.set(errorMessage);
504
624
  // Remove the empty assistant message that was added
505
625
  const messages = this.messages();
506
- if (messages[messages.length - 1] === currentAssistantMessage) {
626
+ if (messages[messages.length - 1]?.role === 'assistant') {
507
627
  this.messages.set(messages.slice(0, -1));
508
628
  }
509
629
  this._isLoading.set(false);
@@ -572,6 +692,7 @@ class AgentChatComponent {
572
692
  * This can be used to provide output for tool calls that are implemented on the client,
573
693
  * and is typically called from `onToolResult`.
574
694
  *
695
+ * @param output: A string or JSON-serializable object.
575
696
  */
576
697
  addToolOutput(toolCallId, output, error) {
577
698
  // No validation of the id is performed here; validation is done when applying the override in processAgentMessage
@@ -636,96 +757,83 @@ class AgentChatComponent {
636
757
  this.canCreate = true;
637
758
  if (this.autoCreateAgents() && this.agentDefinition) {
638
759
  this.createAgent();
639
- return this.translateService.instant(gettext('Creating the "{{agentName}}" AI agent now...'), { agentName: this.agentName });
760
+ return this.translateService.instant(gettext('Creating the "{{agentName}}" AI agent now'), { agentName: this.agentName });
640
761
  }
641
762
  // In this case we show a "Create agent" button
642
763
  return this.translateService.instant(gettext('Create the "{{agentName}}" AI agent to get started.'), { agentName: this.agentName });
643
764
  }
644
765
  return this.translateService.instant(gettext('Configure the "{{agentName}}" agent using the {{agentManager}} to get started.'), { agentName: this.agentName, agentManager });
645
766
  }
646
- makeChangedPartNewInstance(updatedAssistantMsg, changedPart) {
647
- if (!changedPart || !updatedAssistantMsg.steps)
648
- return changedPart;
649
- const newInstance = { ...changedPart };
650
- // For tool calls we often want to modify the output in-place based on client-side information,
651
- // so we need to ensure changedPart is a new instance to avoid change detection issues
652
- if (typeof changedPart.type === 'string' && changedPart.type.startsWith('tool-')) {
653
- for (const step of updatedAssistantMsg.steps) {
654
- if (step.toolCalls) {
655
- const toolCallIndex = step.toolCalls.findIndex(tool => tool === changedPart);
656
- if (toolCallIndex !== -1) {
657
- step.toolCalls[toolCallIndex] = newInstance;
658
- return newInstance;
659
- }
660
- }
661
- if (step.toolResults) {
662
- const toolResultIndex = step.toolResults.findIndex(tool => tool === changedPart);
663
- if (toolResultIndex !== -1) {
664
- step.toolResults[toolResultIndex] = newInstance;
665
- return newInstance;
666
- }
667
- }
668
- }
669
- }
670
- // Should never happen; if we does we need to know
671
- console.error('Internal error - changed part not found in updated assistant message steps', changedPart, updatedAssistantMsg);
672
- return changedPart;
673
- }
674
767
  /**
675
768
  * Called as responses are incrementally streamed from the agent.
676
- * @param currentAssistantMsg The message already added to the messages array rendered by the UI,
677
- * and dynamically updated by this function as more output comes from the agent.
769
+ * Reads the current assistant message from the end of the messages array, processes the update,
770
+ * and replaces it with a new immutable message object to trigger change detection.
678
771
  * @param updatedAssistantMsg contains the latest data received from the agent
679
772
  * @param changedPart if provided, indicates which part of the message was changed in this update
680
773
  */
681
- async processAgentMessage(currentAssistantMsg, updatedAssistantMsg, changedPart) {
682
- // Apply preprocessing if provided
774
+ async processAgentMessage(updatedAssistantMsg, changedPart) {
775
+ const currentAssistantMsg = this.messages()[this.messages().length - 1];
776
+ // Must ensure changedPart is a new instance, otherwise change detection can be missed
777
+ if (changedPart && changedPart?.type !== 'response-metadata') {
778
+ const newInstance = { ...changedPart };
779
+ const idx = updatedAssistantMsg.content.findIndex(p => p === changedPart);
780
+ if (idx !== -1) {
781
+ updatedAssistantMsg.content[idx] = newInstance;
782
+ }
783
+ else {
784
+ // Should never happen; if it does we need to know
785
+ console.error('Internal error - changed part not found in updated assistant message content', changedPart, updatedAssistantMsg);
786
+ }
787
+ changedPart = newInstance;
788
+ }
789
+ // Apply preprocessing if provided. Is permitted to modify updatedAssistantMsg.content array
683
790
  const preprocess = this.preprocessAgentMessage();
684
791
  if (preprocess) {
685
792
  updatedAssistantMsg = preprocess(updatedAssistantMsg, changedPart);
686
793
  }
687
- // Must ensure changedPart is a new instance, otherwise change detection can fail
688
- // For now this only applies to tools
689
- changedPart = this.makeChangedPartNewInstance(updatedAssistantMsg, changedPart);
690
- currentAssistantMsg.content = updatedAssistantMsg.content;
691
794
  // Invoke tool callback - which may call addToolOutput
692
795
  if (changedPart?.type === 'tool-result') {
693
796
  this.debugLog(`Received tool result: ${changedPart.toolName}`, changedPart);
694
797
  this.onToolResult.emit(changedPart);
695
798
  }
696
- if (updatedAssistantMsg.steps) {
697
- currentAssistantMsg.steps = updatedAssistantMsg.steps; // don't need a deep copy for this use case, and must avoid that so pre-processor can add steps
698
- // Apply any pending tool output overrides to the incoming steps - and check that none of them refers to a non-existent item
699
- if (currentAssistantMsg.steps && Object.keys(this._clientToolOutputs).length > 0) {
700
- for (const [toolCallId, override] of Object.entries(this._clientToolOutputs)) {
701
- const toolResult = currentAssistantMsg.steps
702
- ?.flatMap(step => step.toolResults || [])
703
- .find(result => result.toolCallId === toolCallId);
704
- if (toolResult) {
705
- toolResult.output = override.output;
706
- toolResult.error = override.error;
707
- }
708
- else {
709
- throw new Error(`addToolOutput was called with a toolCallId '${toolCallId}' that does not exist in the current assistant message`);
710
- }
799
+ // Create new message object with updated content (immutable update)
800
+ // don't need a deep copy for content itself, and must avoid that so pre-processor can add steps
801
+ const newMsg = {
802
+ ...currentAssistantMsg,
803
+ content: updatedAssistantMsg.content
804
+ };
805
+ // Apply any pending tool output overrides - and check that none of them refers to a non-existent item
806
+ if (Object.keys(this._clientToolOutputs).length > 0) {
807
+ for (const [toolCallId, override] of Object.entries(this._clientToolOutputs)) {
808
+ const toolResult = newMsg.content.find((p) => p.type === 'tool-result' && p.toolCallId === toolCallId);
809
+ if (toolResult) {
810
+ toolResult.output = override.output;
811
+ toolResult.error = override.error;
812
+ }
813
+ else {
814
+ throw new Error(`addToolOutput was called with a toolCallId '${toolCallId}' that does not exist in the current assistant message`);
711
815
  }
712
816
  }
713
817
  }
714
818
  if (updatedAssistantMsg.finishReason) {
715
- this.debugLog(`Received assistant message with ${updatedAssistantMsg.steps?.length || 0} steps: `, updatedAssistantMsg);
819
+ this.debugLog(`Received assistant message with ${updatedAssistantMsg.content.filter(p => p.type === 'step-start').length} steps: `, updatedAssistantMsg);
716
820
  try {
717
- this.handleMessageFinish(currentAssistantMsg, updatedAssistantMsg);
821
+ this.handleMessageFinish(newMsg, updatedAssistantMsg);
718
822
  }
719
823
  finally {
720
824
  this._isLoading.set(false);
721
825
  }
722
826
  }
723
- // as we are mutating the currentAssistantMessage object
724
- // which is already in the messages array we need to emit a new array reference
725
- this.messages.set([...this.messages()]);
827
+ // Replace streaming message with new immutable instance to trigger change detection
828
+ this.messages.update(msgs => [...msgs.slice(0, -1), newMsg]);
726
829
  }
727
830
  handleMessageFinish(currentAssistantMsg, updatedAssistantMsg) {
728
831
  currentAssistantMsg.timestamp = new Date().toISOString();
832
+ for (const part of currentAssistantMsg.content) {
833
+ if (part.type === 'tool-input-streaming' || part.type === 'tool-executing') {
834
+ part.error = true;
835
+ }
836
+ }
729
837
  if (updatedAssistantMsg.finishReason === 'stop') {
730
838
  this.onMessageFinish.emit(currentAssistantMsg);
731
839
  void this.getAnalyticsMetadataContext()
@@ -735,24 +843,24 @@ class AgentChatComponent {
735
843
  .catch(error => {
736
844
  console.error('Error sending analytics:', error);
737
845
  });
738
- if (currentAssistantMsg.content.trim().length === 0) {
846
+ if (currentAssistantMsg.content.length === 0) {
739
847
  // Should be impossible, but adding this as a debugging aid, as it was seen once by a user
740
848
  console.warn('Received finish reason "stop" but assistant message content is empty. This may indicate an issue with the AI service or agent configuration.', currentAssistantMsg);
741
849
  }
742
850
  }
743
851
  else if (updatedAssistantMsg.finishReason === 'error') {
744
- if (currentAssistantMsg.content.trim().length === 0) {
852
+ if (currentAssistantMsg.content.length === 0) {
745
853
  // Should be impossible, but adding this as a debugging aid, as it was seen once by a user
746
854
  console.warn('Received finish reason "error" but assistant message content is empty. This may indicate an issue with the AI service or agent configuration.', currentAssistantMsg);
747
855
  }
748
856
  }
749
857
  }
750
858
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AgentChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
751
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: AgentChatComponent, isStandalone: true, selector: "c8y-agent-chat", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, chatConfig: { classPropertyName: "chatConfig", publicName: "chatConfig", isSignal: true, isRequired: false, transformFunction: null }, welcomeTemplate: { classPropertyName: "welcomeTemplate", publicName: "welcomeTemplate", isSignal: true, isRequired: false, transformFunction: null }, autoCreateAgents: { classPropertyName: "autoCreateAgents", publicName: "autoCreateAgents", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageComponent: { classPropertyName: "assistantMessageComponent", publicName: "assistantMessageComponent", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageDisplayConfig: { classPropertyName: "assistantMessageDisplayConfig", publicName: "assistantMessageDisplayConfig", isSignal: true, isRequired: false, transformFunction: null }, preprocessAgentMessage: { classPropertyName: "preprocessAgentMessage", publicName: "preprocessAgentMessage", isSignal: true, isRequired: false, transformFunction: null }, pruneMessagesForAgent: { classPropertyName: "pruneMessagesForAgent", publicName: "pruneMessagesForAgent", isSignal: true, isRequired: false, transformFunction: null }, initialChatHistory: { classPropertyName: "initialChatHistory", publicName: "initialChatHistory", isSignal: true, isRequired: false, transformFunction: null }, groundingContextProvider: { classPropertyName: "groundingContextProvider", publicName: "groundingContextProvider", isSignal: true, isRequired: false, transformFunction: null }, userAnalyticsContext: { classPropertyName: "userAnalyticsContext", publicName: "userAnalyticsContext", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onMessageFinish: "onMessageFinish", onToolResult: "onToolResult", onFeedback: "onFeedback" }, viewQueries: [{ propertyName: "assistantMessageComponents", predicate: AiChatAssistantMessageComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <span>{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message [message]=\"message\">\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltip]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentStreamError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <i\n class=\"c8y-icon c8y-icon-warning text-danger\"\n aria-hidden=\"true\"\n ></i>\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "component", type: AiChatComponent, selector: "c8y-ai-chat", inputs: ["isLoading", "disabled", "prompt", "suggestionsTemplate", "welcomeTemplate", "config"], outputs: ["onMessage", "onCancel"] }, { kind: "component", type: AiChatSuggestionComponent, selector: "c8y-ai-chat-suggestion", inputs: ["label", "prompt", "icon", "useAiButtons", "disabled"], outputs: ["suggestionClicked"] }, { kind: "component", type: AiChatMessageComponent, selector: "c8y-ai-chat-message", inputs: ["role", "message"] }, { kind: "component", type: AiChatMessageActionComponent, selector: "c8y-ai-chat-message-action", inputs: ["custom", "disabled", "tooltip", "icon"], outputs: ["actionClicked"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
859
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: AgentChatComponent, isStandalone: true, selector: "c8y-agent-chat", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, chatConfig: { classPropertyName: "chatConfig", publicName: "chatConfig", isSignal: true, isRequired: false, transformFunction: null }, welcomeTemplate: { classPropertyName: "welcomeTemplate", publicName: "welcomeTemplate", isSignal: true, isRequired: false, transformFunction: null }, autoCreateAgents: { classPropertyName: "autoCreateAgents", publicName: "autoCreateAgents", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageComponent: { classPropertyName: "assistantMessageComponent", publicName: "assistantMessageComponent", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageDisplayConfig: { classPropertyName: "assistantMessageDisplayConfig", publicName: "assistantMessageDisplayConfig", isSignal: true, isRequired: false, transformFunction: null }, preprocessAgentMessage: { classPropertyName: "preprocessAgentMessage", publicName: "preprocessAgentMessage", isSignal: true, isRequired: false, transformFunction: null }, pruneMessagesForAgent: { classPropertyName: "pruneMessagesForAgent", publicName: "pruneMessagesForAgent", isSignal: true, isRequired: false, transformFunction: null }, initialChatHistory: { classPropertyName: "initialChatHistory", publicName: "initialChatHistory", isSignal: true, isRequired: false, transformFunction: null }, groundingContextProvider: { classPropertyName: "groundingContextProvider", publicName: "groundingContextProvider", isSignal: true, isRequired: false, transformFunction: null }, userAnalyticsContext: { classPropertyName: "userAnalyticsContext", publicName: "userAnalyticsContext", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onMessageFinish: "onMessageFinish", onToolResult: "onToolResult", onFeedback: "onFeedback" }, viewQueries: [{ propertyName: "assistantMessageComponents", predicate: AiChatAssistantMessageComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <span>{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message [message]=\"message\">\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else if (\n message.role === 'assistant' && isLoadingAiResponse() && i === messages$.length - 1\n ) {\n <div>\n <!-- Last assistant message uses reactive computed context -->\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{ assistantMessageContext: lastAssistantMessageContext() }\"\n ></ng-container>\n </div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltip]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentStreamError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <i\n class=\"c8y-icon c8y-icon-warning text-danger\"\n aria-hidden=\"true\"\n ></i>\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "component", type: AiChatComponent, selector: "c8y-ai-chat", inputs: ["isLoading", "disabled", "prompt", "suggestionsTemplate", "welcomeTemplate", "config"], outputs: ["onMessage", "onCancel"] }, { kind: "component", type: AiChatSuggestionComponent, selector: "c8y-ai-chat-suggestion", inputs: ["label", "prompt", "icon", "useAiButtons", "disabled"], outputs: ["suggestionClicked"] }, { kind: "component", type: AiChatMessageComponent, selector: "c8y-ai-chat-message", inputs: ["role", "message"] }, { kind: "component", type: AiChatMessageActionComponent, selector: "c8y-ai-chat-message-action", inputs: ["custom", "disabled", "tooltip", "icon"], outputs: ["actionClicked"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
752
860
  }
753
861
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AgentChatComponent, decorators: [{
754
862
  type: Component,
755
- args: [{ selector: 'c8y-agent-chat', imports: [
863
+ args: [{ selector: 'c8y-agent-chat', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
756
864
  AiChatComponent,
757
865
  AiChatSuggestionComponent,
758
866
  AiChatMessageComponent,
@@ -767,7 +875,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
767
875
  C8yTranslateDirective,
768
876
  GuideDocsComponent,
769
877
  GuideHrefDirective
770
- ], template: "@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <span>{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message [message]=\"message\">\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltip]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentStreamError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <i\n class=\"c8y-icon c8y-icon-warning text-danger\"\n aria-hidden=\"true\"\n ></i>\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"] }]
878
+ ], template: "@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <span>{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message [message]=\"message\">\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else if (\n message.role === 'assistant' && isLoadingAiResponse() && i === messages$.length - 1\n ) {\n <div>\n <!-- Last assistant message uses reactive computed context -->\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{ assistantMessageContext: lastAssistantMessageContext() }\"\n ></ng-container>\n </div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltip]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentStreamError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <i\n class=\"c8y-icon c8y-icon-warning text-danger\"\n aria-hidden=\"true\"\n ></i>\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"] }]
771
879
  }], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], chatConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatConfig", required: false }] }], welcomeTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "welcomeTemplate", required: false }] }], autoCreateAgents: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCreateAgents", required: false }] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], assistantMessageComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageComponent", required: false }] }], assistantMessageDisplayConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageDisplayConfig", required: false }] }], preprocessAgentMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "preprocessAgentMessage", required: false }] }], pruneMessagesForAgent: [{ type: i0.Input, args: [{ isSignal: true, alias: "pruneMessagesForAgent", required: false }] }], initialChatHistory: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialChatHistory", required: false }] }], groundingContextProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "groundingContextProvider", required: false }] }], userAnalyticsContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "userAnalyticsContext", required: false }] }], onMessageFinish: [{ type: i0.Output, args: ["onMessageFinish"] }], onToolResult: [{ type: i0.Output, args: ["onToolResult"] }], onFeedback: [{ type: i0.Output, args: ["onFeedback"] }], assistantMessageComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => AiChatAssistantMessageComponent), { isSignal: true }] }] } });
772
880
 
773
881
  class WidgetAiChatSectionComponent {