@c8y/ngx-components 1023.68.7 → 1023.70.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 (48) hide show
  1. package/ai/agent-chat/index.d.ts +235 -75
  2. package/ai/agent-chat/index.d.ts.map +1 -1
  3. package/ai/ai-chat/index.d.ts +176 -26
  4. package/ai/ai-chat/index.d.ts.map +1 -1
  5. package/ai/index.d.ts +309 -75
  6. package/ai/index.d.ts.map +1 -1
  7. package/asset-properties/index.d.ts.map +1 -1
  8. package/echart/index.d.ts +4 -0
  9. package/echart/index.d.ts.map +1 -1
  10. package/echart/models/index.d.ts +2 -0
  11. package/echart/models/index.d.ts.map +1 -1
  12. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs +680 -242
  13. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs.map +1 -1
  14. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs +343 -44
  15. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs.map +1 -1
  16. package/fesm2022/c8y-ngx-components-ai.mjs +187 -75
  17. package/fesm2022/c8y-ngx-components-ai.mjs.map +1 -1
  18. package/fesm2022/c8y-ngx-components-asset-properties.mjs +11 -5
  19. package/fesm2022/c8y-ngx-components-asset-properties.mjs.map +1 -1
  20. package/fesm2022/{c8y-ngx-components-computed-asset-properties-alarm-count-config.component-B2cy8gI7.mjs → c8y-ngx-components-computed-asset-properties-alarm-count-config.component-CPLDClTp.mjs} +3 -3
  21. package/fesm2022/{c8y-ngx-components-computed-asset-properties-alarm-count-config.component-B2cy8gI7.mjs.map → c8y-ngx-components-computed-asset-properties-alarm-count-config.component-CPLDClTp.mjs.map} +1 -1
  22. package/fesm2022/{c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-BYHnA-5R.mjs → c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-9be_iMQg.mjs} +30 -13
  23. package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-9be_iMQg.mjs.map +1 -0
  24. package/fesm2022/{c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-C4oL39m8.mjs → c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-B2em01_W.mjs} +3 -3
  25. package/fesm2022/c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-B2em01_W.mjs.map +1 -0
  26. package/fesm2022/{c8y-ngx-components-computed-asset-properties-event-count-config.component-DGwm6_C9.mjs → c8y-ngx-components-computed-asset-properties-event-count-config.component-CQuGa1RI.mjs} +3 -3
  27. package/fesm2022/c8y-ngx-components-computed-asset-properties-event-count-config.component-CQuGa1RI.mjs.map +1 -0
  28. package/fesm2022/{c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-BxfCjbYY.mjs → c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-CkmurxJv.mjs} +5 -5
  29. package/fesm2022/{c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-BxfCjbYY.mjs.map → c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-CkmurxJv.mjs.map} +1 -1
  30. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-CTK9zNUh.mjs +86 -0
  31. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-CTK9zNUh.mjs.map +1 -0
  32. package/fesm2022/c8y-ngx-components-computed-asset-properties.mjs +1 -1
  33. package/fesm2022/c8y-ngx-components-echart-models.mjs.map +1 -1
  34. package/fesm2022/c8y-ngx-components-echart.mjs +39 -18
  35. package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
  36. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +106 -4
  37. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
  38. package/fesm2022/c8y-ngx-components-widgets-implementations-html-widget.mjs +14 -122
  39. package/fesm2022/c8y-ngx-components-widgets-implementations-html-widget.mjs.map +1 -1
  40. package/locales/locales.pot +83 -30
  41. package/package.json +1 -1
  42. package/widgets/implementations/html-widget/index.d.ts +11 -50
  43. package/widgets/implementations/html-widget/index.d.ts.map +1 -1
  44. package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-BYHnA-5R.mjs.map +0 -1
  45. package/fesm2022/c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-C4oL39m8.mjs.map +0 -1
  46. package/fesm2022/c8y-ngx-components-computed-asset-properties-event-count-config.component-DGwm6_C9.mjs.map +0 -1
  47. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-C1cuxN3L.mjs +0 -92
  48. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-C1cuxN3L.mjs.map +0 -1
@@ -1,181 +1,555 @@
1
- import { NgClass, JsonPipe, AsyncPipe, NgComponentOutlet } from '@angular/common';
1
+ import { NgComponentOutlet, AsyncPipe } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { Input, Component, EventEmitter, inject, Output } from '@angular/core';
4
- import { ListGroupComponent, ListItemComponent, ListItemCollapseComponent, ListItemBodyComponent, ListItemIconComponent, MarkdownToHtmlPipe, C8yTranslatePipe, GainsightService, AlertService, C8Y_PLUGIN_CONTEXT_PATH, AppStateService, LoadingComponent, EmptyStateComponent, ContextRouteService } from '@c8y/ngx-components';
5
- import { AIService } from '@c8y/ngx-components/ai';
6
- import { AiChatComponent, AiChatSuggestionComponent, AiChatMessageComponent, AiChatMessageActionComponent } from '@c8y/ngx-components/ai/ai-chat';
3
+ import { Injectable, inject, input, computed, output, signal, viewChildren, Component, Injector, Input } from '@angular/core';
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
+ import { TranslateService } from '@ngx-translate/core';
6
+ import { AIService, defaultPruneMessagesForAgent } from '@c8y/ngx-components/ai';
7
7
  import { gettext } from '@c8y/ngx-components/gettext';
8
- import { BehaviorSubject, map, firstValueFrom, of, isObservable } from 'rxjs';
8
+ import { AiChatAssistantMessageComponent, AiChatComponent, AiChatSuggestionComponent, AiChatMessageComponent, AiChatMessageActionComponent } from '@c8y/ngx-components/ai/ai-chat';
9
+ import { CollapseModule } from 'ngx-bootstrap/collapse';
9
10
  import { WidgetConfigService, WidgetConfigFeedbackComponent } from '@c8y/ngx-components/context-dashboard';
11
+ import { of, isObservable } from 'rxjs';
10
12
 
11
- class AgentStepFeedbackComponent {
12
- constructor() {
13
- this.loading = false;
14
- this.collapsed = true;
15
- this.canCollapse = false;
16
- }
17
- ngOnInit() {
18
- if (this.step) {
19
- this.parseDefaultAgentStep(this.step);
13
+ /** A stateless service used by agent-chat for managing chat history. Not public. */
14
+ class ChatHistoryService {
15
+ /**
16
+ * Save messages and suggestions to an opaque, versioned, JSON-serializable snapshot for persistence.
17
+ *
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.
20
+ *
21
+ * @param messages The current conversation messages to save
22
+ * @param suggestions The current suggestions to include in the snapshot
23
+ * @param config Optional configuration to limit what gets saved
24
+ */
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);
20
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);
35
+ const history = {
36
+ messages: msgs.map((msg, msgIndex) => {
37
+ const isRecentMessage = msgIndex >= recentMessageStartIndex;
38
+ return {
39
+ role: msg.role,
40
+ 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
+ : {})
60
+ };
61
+ }),
62
+ suggestions: suggestions,
63
+ chatHistoryVersion: 1
64
+ };
65
+ //console.debug("Saving chat history as: ", history);
66
+ return history;
21
67
  }
22
- ngOnChanges(changes) {
23
- if (changes.step) {
24
- this.parseDefaultAgentStep(changes.step.currentValue);
68
+ /**
69
+ * Restore a previously saved chat history snapshot.
70
+ *
71
+ * Validates the snapshot format and returns the messages and suggestions.
72
+ * Throws an error if the snapshot is invalid.
73
+ *
74
+ * @param history A snapshot previously returned by `save()`
75
+ */
76
+ restore(history) {
77
+ // Validate history structure and messages
78
+ if (typeof history !== 'object' ||
79
+ history === null ||
80
+ !('messages' in history) ||
81
+ !Array.isArray(history['messages'])) {
82
+ throw new Error('Invalid chat history format');
25
83
  }
84
+ if (history['chatHistoryVersion'] !== 1) {
85
+ throw new Error(`Cannot load chat history - expected version 1 but got ${history['chatHistoryVersion']}`);
86
+ }
87
+ const messagesArray = history['messages'];
88
+ if (messagesArray.some((msg, _) => !msg || typeof msg !== 'object' || !msg.role || typeof msg.content !== 'string')) {
89
+ throw new Error('Invalid message format in chat history');
90
+ }
91
+ const suggestions = Array.isArray(history['suggestions'])
92
+ ? history['suggestions']
93
+ : [];
94
+ return {
95
+ messages: messagesArray,
96
+ suggestions
97
+ };
26
98
  }
27
- parseDefaultAgentStep(step) {
28
- if (step.reasoning) {
29
- this.label = gettext('Reasoning');
30
- this.loading = false;
31
- this.canCollapse = true;
32
- this.collapsed = false;
99
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ChatHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
100
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ChatHistoryService, providedIn: 'root' }); }
101
+ }
102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ChatHistoryService, decorators: [{
103
+ type: Injectable,
104
+ args: [{
105
+ providedIn: 'root'
106
+ }]
107
+ }] });
108
+
109
+ /** A service used to send product experience data to GainSight PX. Not public. */
110
+ class UserAnalyticsService {
111
+ constructor() {
112
+ this.gainsightService = inject(GainsightService);
113
+ /** If needed we could set this to false in production to only include hashes of the messages,
114
+ * if there's a decision that customer data shouldn't be uploaded to GainSight. */
115
+ this.includeCustomerSensitiveDataInAnalytics = true;
116
+ }
117
+ /** Create a dictionary of context from the agent-chat component that is useful to include in all GainSight messages */
118
+ async getAnalyticsMetadataContext(agentName, agentDefinition, userAnalyticsContext, model, systemPrompt) {
119
+ let systemPromptHash = undefined;
120
+ if (systemPrompt) {
121
+ systemPromptHash = await this._computeHash(systemPrompt.map(s => s.text).join('\n\n'));
33
122
  }
34
- if (step.toolCalls.length > 0 && !step.toolResults?.length) {
35
- this.label = gettext('Calling a tool');
36
- this.loading = true;
37
- this.canCollapse = true;
38
- this.collapsed = true;
123
+ else if (agentDefinition && agentDefinition.definition) {
124
+ // Fall back to getting it from the agent definition (if provided)
125
+ systemPromptHash = await this._computeHash(agentDefinition.definition.agent.system);
39
126
  }
40
- else if (step.toolResults?.length > 0) {
41
- this.label = gettext('Tool result');
42
- this.loading = false;
43
- this.canCollapse = true;
44
- this.collapsed = true;
127
+ return {
128
+ // User-defined context first
129
+ ...(userAnalyticsContext || {}),
130
+ model: model,
131
+ agent: agentName,
132
+ // Since we don't currently have any other version indicator available, a hash of the system prompt is useful to correlate feedback with the exact prompt used
133
+ systemPromptHash: systemPromptHash
134
+ };
135
+ }
136
+ /** Compute SHA-256 hash of a string for correlation purposes (assumes a secure context where crypto.subtle.digest is available) */
137
+ async _computeHash(message) {
138
+ const encoder = new TextEncoder();
139
+ const data = encoder.encode(message);
140
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
141
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
142
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
143
+ }
144
+ /** Summarize message metrics for GainSight analytics (without including any proprietary data) */
145
+ async _summarizeMessagesForGainSight(userMessage, assistantMessage, detailed = false) {
146
+ // NB: Do NOT send any message content to GainSight servers because that's proprietary/personal customer-owned data that we must protect
147
+ // except when includeCustomerSensitiveDataInAnalytics is set;
148
+ // 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) || [];
159
+ return {
160
+ assistantMessageTimestamp: assistantMessage.timestamp,
161
+ // Track how long we're spending waiting for the AI
162
+ assistantDurationSecs: assistantMessage.timestamp && userMessage.timestamp
163
+ ? (new Date(assistantMessage.timestamp).getTime() -
164
+ new Date(userMessage.timestamp).getTime()) /
165
+ 1000
166
+ : undefined,
167
+ assistantMessageHash: await this._computeHash(allAssistantText),
168
+ assistantMessage: this.includeCustomerSensitiveDataInAnalytics ? allAssistantText : undefined,
169
+ // although harder to read, it's useful to include the full message including tool calls and steps the feedback is due to bugs etc
170
+ assistantMessageJSON: this.includeCustomerSensitiveDataInAnalytics && detailed
171
+ ? JSON.stringify(assistantMessage)
172
+ : undefined,
173
+ userMessageHash: userMessage ? await this._computeHash(userMessage.content) : '',
174
+ userMessage: this.includeCustomerSensitiveDataInAnalytics ? userMessage.content : undefined,
175
+ assistantMessageSteps: assistantMessage.steps?.length || 0,
176
+ // 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,
178
+ assistantMessageToolNames: toolNames.length === 0 ? undefined : toolNames.join(','),
179
+ assistantMessageLines: allAssistantText.split('\n').length,
180
+ userMessageLength: userMessage.content.length,
181
+ userMessageLines: userMessage.content.split('\n').length
182
+ };
183
+ }
184
+ async sendAssistantMessageComplete(assistantMessage, allMessages, analyticsMetadataContext) {
185
+ // Can't use lastIndexOf(assistantMessage) here (as we for for feedback) because allMessages may not yet have been updated with this assistantMessage item
186
+ const userMessage = [...allMessages].reverse().find(m => m.role === 'user');
187
+ if (!userMessage) {
188
+ console.warn('No user message found for assistant message, skipping analytics event');
189
+ return;
45
190
  }
191
+ this.gainsightService.triggerEvent('ai.agent.message', {
192
+ messageCount: allMessages.length,
193
+ ...(await this._summarizeMessagesForGainSight(userMessage, assistantMessage)),
194
+ ...analyticsMetadataContext
195
+ });
196
+ }
197
+ /** Sent when there is a streaming error from the agent */
198
+ async sendAssistantMessageError(error, allMessages, analyticsMetadataContext) {
199
+ this.gainsightService.triggerEvent('ai.agent.error', {
200
+ errorMessage: error,
201
+ messageCount: allMessages.length,
202
+ ...analyticsMetadataContext
203
+ });
46
204
  }
47
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AgentStepFeedbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
48
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AgentStepFeedbackComponent, isStandalone: true, selector: "c8y-agent-step-feedback", inputs: { step: "step", label: "label", loading: "loading", collapsed: "collapsed", canCollapse: "canCollapse" }, host: { classAttribute: "agent-step-feedback" }, usesOnChanges: true, ngImport: i0, template: "@if (!step.reasoning) {\n <div [innerHTML]=\"step.text | markdownToHtml | async\"></div>\n}\n@if (label) {\n <c8y-list-group class=\"m-t-16 m-b-16\">\n <c8y-li\n [active]=\"!loading\"\n [collapsed]=\"collapsed\"\n >\n <c8y-li-icon>\n <span\n class=\"btn-ai btn-ai-hint btn-sm\"\n [ngClass]=\"{ working: loading }\"\n >\n <span></span>\n </span>\n </c8y-li-icon>\n <c8y-li-body>\n {{ label | translate }}\n </c8y-li-body>\n\n @if (canCollapse) {\n <c8y-li-collapse>\n @if (step.reasoning) {\n <div [innerHTML]=\"step.reasoning | markdownToHtml | async\"></div>\n } @else if (step) {\n <pre\n class=\"fit-w\"\n style=\"max-height: 320px\"\n >{{ step | json }}</pre\n >\n }\n </c8y-li-collapse>\n }\n </c8y-li>\n </c8y-list-group>\n}\n", dependencies: [{ kind: "component", type: ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: ListItemCollapseComponent, selector: "c8y-list-item-collapse, c8y-li-collapse", inputs: ["collapseWay"] }, { kind: "component", type: ListItemBodyComponent, selector: "c8y-list-item-body, c8y-li-body", inputs: ["body"] }, { kind: "component", type: ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
205
+ /** Sent when the agent is unavailable, for example because the microservice is not present or has insufficient permissions.
206
+ * This is useful to find out what kinds of problems users are mostly having to get access.
207
+ */
208
+ async sendAgentUnavailable(errorType) {
209
+ this.gainsightService.triggerEvent('ai.agent.unavailable', {
210
+ errorType: errorType
211
+ });
212
+ }
213
+ /** Rates an AI message as positive/negative, sending the feedback to GainSight */
214
+ async sendFeedbackOnAssistantMessage(assistantMessage, allMessages, positive, analyticsMetadataContext) {
215
+ const userMessage = allMessages[allMessages.lastIndexOf(assistantMessage) - 1];
216
+ const detailedFeedback = !positive;
217
+ this.gainsightService.triggerEvent('ai.agent.feedback', {
218
+ positive,
219
+ messageCount: allMessages.length,
220
+ ...(await this._summarizeMessagesForGainSight(userMessage, assistantMessage, detailedFeedback)),
221
+ ...analyticsMetadataContext
222
+ });
223
+ }
224
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UserAnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
225
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UserAnalyticsService, providedIn: 'root' }); }
49
226
  }
50
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AgentStepFeedbackComponent, decorators: [{
51
- type: Component,
52
- args: [{ selector: 'c8y-agent-step-feedback', standalone: true, imports: [
53
- ListGroupComponent,
54
- ListItemComponent,
55
- ListItemCollapseComponent,
56
- ListItemBodyComponent,
57
- ListItemIconComponent,
58
- NgClass,
59
- JsonPipe,
60
- MarkdownToHtmlPipe,
61
- AsyncPipe,
62
- C8yTranslatePipe
63
- ], host: { class: 'agent-step-feedback' }, template: "@if (!step.reasoning) {\n <div [innerHTML]=\"step.text | markdownToHtml | async\"></div>\n}\n@if (label) {\n <c8y-list-group class=\"m-t-16 m-b-16\">\n <c8y-li\n [active]=\"!loading\"\n [collapsed]=\"collapsed\"\n >\n <c8y-li-icon>\n <span\n class=\"btn-ai btn-ai-hint btn-sm\"\n [ngClass]=\"{ working: loading }\"\n >\n <span></span>\n </span>\n </c8y-li-icon>\n <c8y-li-body>\n {{ label | translate }}\n </c8y-li-body>\n\n @if (canCollapse) {\n <c8y-li-collapse>\n @if (step.reasoning) {\n <div [innerHTML]=\"step.reasoning | markdownToHtml | async\"></div>\n } @else if (step) {\n <pre\n class=\"fit-w\"\n style=\"max-height: 320px\"\n >{{ step | json }}</pre\n >\n }\n </c8y-li-collapse>\n }\n </c8y-li>\n </c8y-list-group>\n}\n" }]
64
- }], propDecorators: { step: [{
65
- type: Input,
66
- args: [{ required: true }]
67
- }], label: [{
68
- type: Input
69
- }], loading: [{
70
- type: Input
71
- }], collapsed: [{
72
- type: Input
73
- }], canCollapse: [{
74
- type: Input
75
- }] } });
227
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UserAnalyticsService, decorators: [{
228
+ type: Injectable,
229
+ args: [{
230
+ providedIn: 'root'
231
+ }]
232
+ }] });
76
233
 
77
234
  class AgentChatComponent {
78
235
  constructor() {
79
- this.suggestions = [];
80
- this.headline = gettext('Welcome!');
81
- this.autoInstallAgents = true;
82
- this.variables = {};
83
- this.stepRenderComponent = AgentStepFeedbackComponent;
84
- this.onMessageDelta = new EventEmitter();
85
- this.onMessageText = new EventEmitter();
86
- this.onMessageFinish = new EventEmitter();
87
- this.onToolResult = new EventEmitter();
88
- this.isLoading = false;
236
+ this.translateService = inject(TranslateService);
237
+ this.datePipe = inject(DatePipe);
238
+ this.modalService = inject(ModalService);
239
+ /** Debug logging function for AI messages. Can be enabled at runtime with localStorage.setItem('c8y-debug-log.agent-chat', 'true'); */
240
+ this.debugLog = (category => {
241
+ const isDebugEnabled = localStorage.getItem('c8y-debug-log.' + category) === 'true';
242
+ return (message, ...args) => isDebugEnabled && console.debug(`[${category} DEBUG] ${message}`, ...args);
243
+ })('agent-chat');
244
+ /**
245
+ * Identifies which AI agent from the AI Agent Manager service will be used by this component.
246
+ *
247
+ * Where possible, provide a `ClientAgentDefinition` rather than just a name.
248
+ *
249
+ * It is recommended to define a `toolCallConfig` in the same place you define `ClientAgentDefinition`,
250
+ * and to use the keys from `assistantMessageDisplayConfig.toolCallConfig`
251
+ * in the `ClientAgentDefinition` tools list.
252
+ */
253
+ this.agent = input.required(...(ngDevMode ? [{ debugName: "agent" }] : []));
254
+ /**
255
+ * Clickable suggestions buttons to display just above the chat, for likely next user actions.
256
+ * These may be hardcoded or be set by the AI agent (e.g. in a tool call).
257
+ * This input should be `undefined` to indicate that there are no new suggestions yet, and that any restored from the chat history should be used instead.
258
+ * You can use the `isWelcoming` output to show different suggestions while the initial welcome message is displayed.
259
+ */
260
+ this.suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
261
+ /** ChatConfig to customize the ai-chat component's configuration if desired. */
262
+ this.chatConfig = input(...(ngDevMode ? [undefined, { debugName: "chatConfig" }] : []));
263
+ /** Template for customizing the welcome view using `ng-template`. */
264
+ this.welcomeTemplate = input(...(ngDevMode ? [undefined, { debugName: "welcomeTemplate" }] : []));
265
+ /** Indicates whether the component has zero messages and is showing the welcome page. */
266
+ this.isWelcoming = computed(() => this.messages().length === 0, ...(ngDevMode ? [{ debugName: "isWelcoming" }] : []));
267
+ /** Configures whether this component should automatically create the provided `agent: ClientAgentDefinition`
268
+ * if an agent of that name doesn't already exist. This is an alternative to the usual mechanism of configuring
269
+ * agents using a `.agent.c8y.ts` file.
270
+ */
271
+ this.autoCreateAgents = input(true, ...(ngDevMode ? [{ debugName: "autoCreateAgents" }] : []));
272
+ /** Variables to pass to the AI agent for placeholders specified in the agent's system prompt. */
273
+ this.variables = input({}, ...(ngDevMode ? [{ debugName: "variables" }] : []));
274
+ /**
275
+ * Allows overriding the component used to render messages from the AI assistant, if the default
276
+ * AiChatAssistantMessageComponent does not do what you need.
277
+ *
278
+ * The component must accept a single input `assistantMessageContext: AssistantMessageContext`.
279
+ * This object contains all data needed to render the assistant message and is forward-compatible with future additions.
280
+ *
281
+ * Example usage:
282
+ * <c8y-agent-chat [assistantMessageComponent]="MyCustomAssistantMessageComponent"></c8y-agent-chat>
283
+ */
284
+ this.assistantMessageComponent = input(AiChatAssistantMessageComponent, ...(ngDevMode ? [{ debugName: "assistantMessageComponent" }] : []));
285
+ /**
286
+ * Configuration passed to `AiChatAssistantMessageComponent` for controlling how messages from the assistant
287
+ * are rendered, including tool calls.
288
+ *
289
+ * If you are performing any tool calls it is recommended to configure the `toolCallConfig`
290
+ * to provide localized display names for the tools used in your `AgentDefinitionConfig`.
291
+ */
292
+ this.assistantMessageDisplayConfig = input({}, ...(ngDevMode ? [{ debugName: "assistantMessageDisplayConfig" }] : []));
293
+ /**
294
+ * Optional function to preprocess messages from the assistant before they are displayed, for example if you need to remove special
295
+ * tagged sections from the content to move into simulated tool calls.
296
+ *
297
+ * This function may be called many times for each message, as more steps, tool calls and content are streamed from the agent.
298
+ *
299
+ * If you modify any part of the message other than the `changedPart`, you should replace it with a new shallow copy of the object so
300
+ * that change detection works correctly. This is not necessary for changedPart.
301
+ *
302
+ * @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.
304
+ */
305
+ this.preprocessAgentMessage = input(undefined, ...(ngDevMode ? [{ debugName: "preprocessAgentMessage" }] : []));
306
+ /** Optional function to override how the message history is prepared and compacted ready for sending with
307
+ * agent requests. This overrides the default behaviour from `defaultPruneMessagesForAgent`.
308
+ *
309
+ * For example this can be used to remove unnecessary information to minimize tokens, reduce the number of messages,
310
+ * or to make what is sent to the agent different from what is rendered in the UI.
311
+ */
312
+ this.pruneMessagesForAgent = input(undefined, ...(ngDevMode ? [{ debugName: "pruneMessagesForAgent" }] : []));
313
+ /** Input that provides a previously-saved chat history snapshot to restore. */
314
+ this.initialChatHistory = input(...(ngDevMode ? [undefined, { debugName: "initialChatHistory" }] : []));
315
+ /** Suggestions that were restored from the initial chat history; used if `suggestions` input is not yet set. */
316
+ this._restoredSuggestions = [];
317
+ /**
318
+ * An optional function that returns a message sent in AI agent requests just before each new user message to provide context or "grounding" to
319
+ * anchor the AI's response - such as the current state of the document the AI is helping with.
320
+ *
321
+ * Typically this string would begin with a special prefix (e.g. "[STATE_SNAPSHOT]") that is described by the system prompt.
322
+ *
323
+ * This message does not form part of the chat history, to avoid confusing the AI agent with older grounding state.
324
+ * It is injected into the message history just before the latest user message as per standard recommendations,
325
+ * since AI gives more attention to the recent content.
326
+ */
327
+ this.groundingContextProvider = input(...(ngDevMode ? [undefined, { debugName: "groundingContextProvider" }] : []));
89
328
  /**
90
- * A stream of AI messages representing the conversation.
329
+ * Provides application-defined fields to enrich the user feedback tracking data for this component.
330
+ *
331
+ * Do not include Intellectual Property or Personal Identifiable Information in this object, as it may
332
+ * be included in feedback data sent to third-party services.
91
333
  */
92
- this.messages$ = new BehaviorSubject([]);
93
- this.hasError = true;
334
+ this.userAnalyticsContext = input(...(ngDevMode ? [undefined, { debugName: "userAnalyticsContext" }] : []));
335
+ this.onMessageFinish = output();
336
+ /**
337
+ * Notified when a tool result is received from the agent.
338
+ *
339
+ * This is called before it is added to the message displayed in the component.
340
+ *
341
+ * Tools with a client/UI implementation may provide `output` for the tool
342
+ * (e.g. data to render in the UI) by calling `addToolOutput`.
343
+ */
344
+ this.onToolResult = output();
345
+ /** Notified when the user presses the positive or negative feedback buttons for an assistant message.*/
346
+ this.onFeedback = output();
347
+ 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
+ this.aiAgentManagerApplicationName = gettext('AI Agent Manager');
349
+ /** 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" }] : []));
351
+ /** Stores the most recent agent health check response, or `undefined` while the initial check is in progress. */
352
+ this._agentHealth = signal(undefined, ...(ngDevMode ? [{ debugName: "_agentHealth" }] : []));
353
+ /** Stores any detailed error messages from the backend. */
354
+ this.agentHealthDetailedMessages = signal(undefined, ...(ngDevMode ? [{ debugName: "agentHealthDetailedMessages" }] : []));
355
+ /**
356
+ * Stores any error that occurred during streaming the current message from the agent.
357
+ *
358
+ * If possible this string is translated already or registered with `gettext`.
359
+ * In practice, this may not be translated if it comes from the backend.
360
+ */
361
+ this.agentStreamError = signal(undefined, ...(ngDevMode ? [{ debugName: "agentStreamError" }] : []));
362
+ // A stream of AI messages representing the conversation.
363
+ // NB: this is NOT part of the API of this component and may change in future
364
+ // (e.g. we may improve AIMessage class)
365
+ this.messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : []));
366
+ /** If the create agent button should be shown to the user. */
94
367
  this.canCreate = false;
95
- this.errorMsg = '';
96
368
  this.prompt = '';
97
369
  this.aiService = inject(AIService);
98
- this.gainsightService = inject(GainsightService);
370
+ this.userAnalyticsService = inject(UserAnalyticsService);
99
371
  this.alertService = inject(AlertService);
372
+ this.chatHistoryService = inject(ChatHistoryService);
100
373
  this.pluginContextPath = inject(C8Y_PLUGIN_CONTEXT_PATH, { optional: true });
101
374
  this.appState = inject(AppStateService);
102
- this.defaultAgentStepRendererPipe = (source) => {
103
- return source.pipe(map(steps => steps.map(step => {
104
- if (step.reasoning || step.toolCalls || step.toolResults) {
105
- return { content: this.stepRenderComponent, origin: { ...step } };
106
- }
107
- return { content: step.text, origin: { ...step } };
108
- })));
109
- };
375
+ this.assistantMessageComponents = viewChildren(AiChatAssistantMessageComponent, ...(ngDevMode ? [{ debugName: "assistantMessageComponents" }] : []));
376
+ /**
377
+ * Stores client-supplied tool outputs to override tool results for a given toolCallId in the current assistant message.
378
+ * Cleared at the start of each new message.
379
+ */
380
+ this._clientToolOutputs = {};
110
381
  }
111
- set agent(value) {
112
- if (typeof value === 'string') {
113
- this.agentName = value;
114
- this.agentDefinition = undefined;
115
- }
116
- else {
117
- this.agentName = value.definition.name;
118
- this.agentDefinition = value;
119
- }
382
+ get agentName() {
383
+ const value = this.agent();
384
+ return typeof value === 'string' ? value : value.definition.name;
385
+ }
386
+ get agentDefinition() {
387
+ const value = this.agent();
388
+ if (!value || typeof value === 'string')
389
+ return undefined;
390
+ return value;
391
+ }
392
+ /** Is an AI agent request currently in progress. This can be used to disable UI elements while a reply (which might change the state) is being loaded. */
393
+ get isLoadingAiResponse() {
394
+ return this._isLoading.asReadonly();
395
+ }
396
+ /** Returns the most recent agent health check status such as `ready`, or an error. */
397
+ get agentHealthStatus() {
398
+ return this._agentHealth()?.status || 'loading';
120
399
  }
121
400
  get currentContextPath() {
122
401
  return this.pluginContextPath || this.appState.currentApplication?.value?.contextPath || '';
123
402
  }
124
403
  async ngOnInit() {
125
- if (this.agentDefinition.snapshot) {
126
- this.hasError = false;
127
- return;
128
- }
129
- this.isLoading = true;
404
+ // Restore chat history first, if provided
405
+ this._restoreChatHistory(this.initialChatHistory());
406
+ // Set the loading indicator to prevent interactions while we establish whether the agent is useable
407
+ this._isLoading.set(true);
130
408
  try {
131
409
  const agentHealth = await this.aiService.getAgentHealth(this.agentName, this.currentContextPath);
132
- this.composeErrorMessage(agentHealth);
410
+ this._agentHealth.set(agentHealth);
411
+ this.agentHealthDetailedMessages.set(agentHealth.messages?.join('\n'));
412
+ this._agentHealthError.set(this.handleAgentError(agentHealth));
413
+ if (this.agentDefinition && this.agentDefinition.snapshot) {
414
+ this.createAgent();
415
+ }
133
416
  }
134
417
  catch (ex) {
135
- this.errorMsg = gettext('Microservice not found. Please contact your administrator.');
136
- this.hasError = true;
418
+ console.warn('Error getting agent manager health:', ex);
419
+ this.userAnalyticsService.sendAgentUnavailable('agent-health-check-failed');
420
+ this._agentHealthError.set(this.translateService.instant(gettext('This tenant does not have the {{agentManager}} microservice. Please contact your administrator.'), { agentManager: this.translateService.instant(this.aiAgentManagerApplicationName) }));
137
421
  }
138
- this.isLoading = false;
422
+ this._isLoading.set(false);
423
+ }
424
+ /**
425
+ * Save the current chat history and suggestions to an opaque, versioned, JSON-serializable snapshot of the chat history for persistence.
426
+ *
427
+ * To save space, only recent tool results and reasoning are included.
428
+ * Additional options for compressing the history can be provided using the config parameters.
429
+ *
430
+ * @param config Optional configuration to limit what gets saved (e.g., filter out specific tools, limit total message count)
431
+ */
432
+ saveChatHistory(config) {
433
+ return this.chatHistoryService.save(this.messages(), this.suggestions(), config);
139
434
  }
435
+ /** Sends a message to the AI */
140
436
  async sendMessage(message) {
141
- this.messages$.next([...this.messages$.value, message]);
142
- this.isLoading = true;
437
+ if (this.isLoadingAiResponse()) {
438
+ // Probably not worth a user-facing error message, but since we assume everywhere that we don't have parallel chats going on at once,
439
+ // don't allow this (e.g. if called from a chat button or other part of the application's UI)
440
+ console.log('Ignoring chat message because previous AI response is still loading');
441
+ return;
442
+ }
443
+ // Collapse the thinking section of the previous assistant message before adding a new one
444
+ const components = this.assistantMessageComponents();
445
+ if (components.length > 0) {
446
+ components[components.length - 1].setThinkingExpanded(false);
447
+ }
448
+ this.messages.set([...this.messages(), message]);
449
+ this._isLoading.set(true);
450
+ this.agentStreamError.set(undefined); // Clear any previous stream errors
143
451
  this.abortController = new AbortController();
452
+ this._clientToolOutputs = {};
144
453
  const currentAssistantMessage = {
145
454
  role: 'assistant',
146
455
  content: ''
147
456
  };
148
- this.messages$.next([...this.messages$.value, currentAssistantMessage]);
149
- const stream = await this.aiService.stream$(this.agentDefinition || this.agentName, this.messages$.value.filter(m => m !== currentAssistantMessage), this.variables, this.abortController, this.currentContextPath);
457
+ // Prepare messages for AI service, optionally including grounding message
458
+ const pruneMessagesForAgent = this.pruneMessagesForAgent() || defaultPruneMessagesForAgent;
459
+ const messagesForAI = pruneMessagesForAgent(this.messages());
460
+ const groundingProvider = this.groundingContextProvider();
461
+ if (groundingProvider) {
462
+ // Insert grounding message before the last user message
463
+ const groundingMsg = {
464
+ role: 'user',
465
+ content: groundingProvider()
466
+ };
467
+ messagesForAI.splice(messagesForAI.length - 1, 0, groundingMsg);
468
+ }
469
+ this.messages.set([...this.messages(), currentAssistantMessage]);
470
+ const stream = await this.aiService.stream$(this.agentDefinition || this.agentName, messagesForAI, this.variables(), this.abortController, this.currentContextPath);
150
471
  if (this.assistantSubscription) {
151
472
  this.assistantSubscription.unsubscribe();
152
473
  }
153
- this.assistantSubscription = stream.subscribe((messageFromAssistant) => this.processAgentMessage(currentAssistantMessage, messageFromAssistant, this.agentName, message));
474
+ this.assistantSubscription = stream.subscribe({
475
+ next: (response) => {
476
+ if (response?.changedPart?.type === 'response-metadata') {
477
+ if (response.changedPart.model) {
478
+ this.model = response.changedPart.model;
479
+ }
480
+ if (response.changedPart.systemPrompt) {
481
+ this.systemPrompt = response.changedPart.systemPrompt;
482
+ }
483
+ }
484
+ else {
485
+ this.processAgentMessage(currentAssistantMessage, response.message, response.changedPart);
486
+ }
487
+ },
488
+ complete: () => {
489
+ if (this._isLoading()) {
490
+ // Should never happen, but worth knowing if it does (indicates bug in processAgentMessage)
491
+ console.error('AI response stream completed but message loading indicator was not reset');
492
+ }
493
+ },
494
+ error: async (error) => {
495
+ // Currently errors from the AIService are often a big JSON with no human-friendly message anywhere
496
+ // (e.g. {error:{error:{statusCode:400, name:"AI-APICallError", ...}},
497
+ // so we can only show the detail in the console and have to give a non-informative error to the user;
498
+ // but if the backend could add a user-friendly "message" we could render that
499
+ console.error('Error in AI stream:', error);
500
+ const errorMessage = typeof error === 'string'
501
+ ? error
502
+ : error?.message || gettext('An error occurred while communicating with the AI agent.');
503
+ this.agentStreamError.set(errorMessage);
504
+ // Remove the empty assistant message that was added
505
+ const messages = this.messages();
506
+ if (messages[messages.length - 1] === currentAssistantMessage) {
507
+ this.messages.set(messages.slice(0, -1));
508
+ }
509
+ this._isLoading.set(false);
510
+ this.userAnalyticsService.sendAssistantMessageError(errorMessage, this.messages(), await this.getAnalyticsMetadataContext());
511
+ }
512
+ });
154
513
  }
155
514
  ngOnDestroy() {
156
515
  this.cancel();
157
516
  }
158
- reprompt(userMessage) {
159
- const index = this.messages$.value.indexOf(userMessage);
517
+ async reprompt(userMessage) {
518
+ const index = this.messages().indexOf(userMessage);
160
519
  if (index > -1) {
161
- this.messages$.next(this.messages$.value.slice(0, index));
162
- this.prompt = userMessage.content;
520
+ const formattedTimestamp = userMessage.timestamp
521
+ ? this.datePipe.transform(userMessage.timestamp, 'adaptiveDate')
522
+ : gettext('(unknown time)');
523
+ try {
524
+ await this.modalService.confirm(gettext('Edit this prompt and delete the following messages'), this.translateService.instant(gettext('Are you sure you want to rewrite the conversation by editing your message from {{timestamp}}, and deleting the following {{messageCount}} assistant and user messages?'), {
525
+ timestamp: formattedTimestamp,
526
+ messageCount: this.messages().length - index - 1
527
+ }), Status.DANGER, { cancel: gettext('Cancel'), ok: gettext('Delete and replace') });
528
+ this.messages.set(this.messages().slice(0, index));
529
+ this.prompt = userMessage.content;
530
+ }
531
+ catch (e) {
532
+ if (!e)
533
+ return; // User dismissed the dialog, do nothing
534
+ throw e; // Unexpected error, rethrow to propagate
535
+ }
163
536
  }
164
537
  }
165
- rate(assistantMessage, positive) {
166
- const agentName = typeof this.agent === 'string' ? this.agent : this.agent.label;
167
- this.gainsightService.triggerEvent('ai.agent.feedback', {
168
- positive,
169
- assistant: assistantMessage,
170
- user: this.messages$.value[this.messages$.value.indexOf(assistantMessage) - 1],
171
- agent: agentName
538
+ /** Rates an AI message as positive/negative, sending the feedback to GainSight */
539
+ async rate(assistantMessage, positive) {
540
+ this.userAnalyticsService.sendFeedbackOnAssistantMessage(assistantMessage, this.messages(), positive, await this.getAnalyticsMetadataContext());
541
+ this.onFeedback.emit({
542
+ userMessage: this.messages()[this.messages().lastIndexOf(assistantMessage) - 1],
543
+ assistantMessage,
544
+ feedbackPositive: positive
172
545
  });
546
+ this.alertService.success(gettext('Thank you for the feedback!'));
173
547
  }
174
548
  reload(assistantMessage) {
175
- const index = this.messages$.value.indexOf(assistantMessage);
176
- const userMessage = this.messages$.value[index - 1];
549
+ const index = this.messages().indexOf(assistantMessage);
550
+ const userMessage = this.messages()[index - 1];
177
551
  if (index > -1) {
178
- this.messages$.next(this.messages$.value.slice(0, index - 1));
552
+ this.messages.set(this.messages().slice(0, index - 1));
179
553
  this.sendMessage({
180
554
  role: 'user',
181
555
  content: userMessage.content,
@@ -183,109 +557,198 @@ class AgentChatComponent {
183
557
  });
184
558
  }
185
559
  }
186
- async cancel() {
560
+ cancel() {
187
561
  if (this.abortController) {
188
562
  this.abortController.abort();
189
563
  }
190
- const lastMessage = this.messages$.value[this.messages$.value.length - 1];
191
- if (lastMessage && lastMessage.role === 'assistant') {
192
- if (lastMessage.content === '') {
193
- // if message is aborted before content is even populated, remove it entirely
194
- this.messages$.next(this.messages$.value.slice(0, -1));
195
- }
196
- else {
197
- // if message has content, mark the last step as aborted to indicate incomplete processing
198
- const lastMessageSteps = (await firstValueFrom(lastMessage.componentSteps$))
199
- .map(s => s.origin)
200
- .map((step, index, arr) => index === arr.length - 1 ? { ...step, aborted: true } : step) || [];
201
- if (lastMessageSteps.length) {
202
- lastMessage.componentSteps$ = of(lastMessageSteps).pipe(this.defaultAgentStepRendererPipe);
203
- this.messages$.next([...this.messages$.value]);
204
- }
205
- }
206
- }
207
564
  if (this.assistantSubscription) {
208
565
  this.assistantSubscription.unsubscribe();
209
566
  }
210
- this.isLoading = false;
567
+ this._isLoading.set(false);
568
+ }
569
+ /**
570
+ * Overrides the output of a specific tool result (in the current assistant message), including optionally indicating that an error occurred.
571
+ *
572
+ * This can be used to provide output for tool calls that are implemented on the client,
573
+ * and is typically called from `onToolResult`.
574
+ *
575
+ */
576
+ addToolOutput(toolCallId, output, error) {
577
+ // No validation of the id is performed here; validation is done when applying the override in processAgentMessage
578
+ this.debugLog(`Adding client-side tool ${error ? 'error' : 'output'} for ${toolCallId}: `, output);
579
+ this._clientToolOutputs[toolCallId] = { output: output, error: error ?? false };
211
580
  }
212
581
  async createAgent() {
213
- this.isLoading = true;
582
+ this._isLoading.set(true);
214
583
  try {
584
+ if (!this.agentDefinition) {
585
+ throw new Error('No agent definition provided');
586
+ }
215
587
  await this.aiService.createOrUpdateAgent(this.agentDefinition.definition);
216
- this.hasError = false;
588
+ this._agentHealthError.set(undefined);
217
589
  }
218
590
  catch (ex) {
219
- this.alertService.danger(gettext('Failed to create the agent.'));
220
- this.hasError = true;
591
+ // TODO: Distinguish permissions errors to provide a more specific error message.
592
+ console.error('Error configuring AI agent:', ex);
593
+ this.alertService.danger(gettext('Failed to configure the agent.'));
594
+ this._agentHealthError.set(this.translateService.instant(gettext('Failed to configure the agent.')));
221
595
  }
222
- this.isLoading = false;
596
+ this._isLoading.set(false);
223
597
  }
224
- composeErrorMessage(agentHealth) {
225
- if (agentHealth.exists) {
226
- this.errorMsg = '';
227
- this.hasError = false;
598
+ // Private methods go here:
599
+ /** Restore chat history from a previously-saved snapshot. Does nothing if the component already has some messages. */
600
+ _restoreChatHistory(history) {
601
+ if (!history)
228
602
  return;
229
- }
230
- this.hasError = true;
231
- if (!agentHealth.isProviderConfigured) {
232
- this.errorMsg = gettext('AI provider is not configured. Please contact your administrator.');
603
+ // Ignore updates to history once we have messages
604
+ if (this.messages().length > 0) {
233
605
  return;
234
606
  }
235
- if (agentHealth.canCreate && typeof this.agent === 'string') {
236
- this.errorMsg = gettext('The agent does not exist. Provide the agent definition in the agent manager to use it.');
237
- return;
607
+ const restored = this.chatHistoryService.restore(history);
608
+ this.messages.set(restored.messages);
609
+ this._restoredSuggestions = restored.suggestions;
610
+ }
611
+ async getAnalyticsMetadataContext() {
612
+ return await this.userAnalyticsService.getAnalyticsMetadataContext(this.agentName, this.agentDefinition, this.userAnalyticsContext(), this.model, this.systemPrompt);
613
+ }
614
+ handleAgentError(agentHealth) {
615
+ this.canCreate = false;
616
+ const agentManager = this.translateService.instant(this.aiAgentManagerApplicationName);
617
+ if (agentHealth.status != 'ready')
618
+ this.userAnalyticsService.sendAgentUnavailable(agentHealth.status || 'unknown-error');
619
+ // All of these errors are displayed under a headline banner stating "AI agent is not available"
620
+ // From end-user perspective it makes sense to recommend opening AI Agent Manager to configure their provider/agent
621
+ // (or, implicitly, to ask their admin to do that if they don't have access themselves).
622
+ // If they're the application author they could create a `.agent.c8y.ts` file instead, but we don't want to
623
+ // expose such low-level details in the UI.
624
+ switch (agentHealth.status) {
625
+ case 'ready':
626
+ return undefined;
627
+ case 'missing-provider':
628
+ return this.translateService.instant(gettext('Configure a provider in the {{agentManager}} to get started.'), { agentManager });
629
+ case 'missing-permissions':
630
+ return this.translateService.instant(gettext('Add AI Agent permissions to the current user or role to get started.'));
631
+ case 'missing-microservice':
632
+ return this.translateService.instant(gettext('This tenant does not have the {{agentManager}} microservice. Please contact your administrator.'), { agentManager });
238
633
  }
239
- if (agentHealth.canCreate && typeof this.agent !== 'string') {
634
+ // Otherwise it's a missing agent definition
635
+ if (agentHealth.canCreate && this.agentDefinition) {
240
636
  this.canCreate = true;
241
- if (this.autoInstallAgents && this.agentDefinition) {
637
+ if (this.autoCreateAgents() && this.agentDefinition) {
242
638
  this.createAgent();
243
- return;
639
+ return this.translateService.instant(gettext('Creating the "{{agentName}}" AI agent now...'), { agentName: this.agentName });
244
640
  }
245
- this.errorMsg = gettext('The agent does not exist. You can create it now.');
246
- return;
641
+ // In this case we show a "Create agent" button
642
+ return this.translateService.instant(gettext('Create the "{{agentName}}" AI agent to get started.'), { agentName: this.agentName });
247
643
  }
248
- if (!agentHealth.canCreate) {
249
- this.errorMsg = gettext('The agent does not exist. Please contact your administrator.');
250
- if (agentHealth.messages?.length) {
251
- this.errorMsg += '\n' + agentHealth.messages.join(' ');
644
+ return this.translateService.instant(gettext('Configure the "{{agentName}}" agent using the {{agentManager}} to get started.'), { agentName: this.agentName, agentManager });
645
+ }
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
+ }
252
668
  }
253
- return;
254
669
  }
255
- this.errorMsg = gettext('Unknown error. Please contact your administrator.');
256
- }
257
- processAgentMessage(currentAssistantMessage, messageFromAssistant, agentName, message) {
258
- currentAssistantMessage.content = messageFromAssistant.content;
259
- const steps = messageFromAssistant.steps || [];
260
- const lastStep = steps.length ? steps[steps.length - 1] : null;
261
- this.onMessageDelta.emit(lastStep.textDelta);
262
- this.onMessageText.emit(lastStep.text);
263
- currentAssistantMessage.componentSteps$ = of(steps).pipe(this.defaultAgentStepRendererPipe);
264
- if (messageFromAssistant.finishReason === 'stop') {
265
- currentAssistantMessage.timestamp = new Date().toISOString();
266
- this.onMessageFinish.emit(currentAssistantMessage.content);
267
- this.gainsightService.triggerEvent('ai.agent.message', {
268
- agent: agentName,
269
- user: message,
270
- assistant: currentAssistantMessage.content
271
- });
272
- this.isLoading = false;
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
+ /**
675
+ * 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.
678
+ * @param updatedAssistantMsg contains the latest data received from the agent
679
+ * @param changedPart if provided, indicates which part of the message was changed in this update
680
+ */
681
+ async processAgentMessage(currentAssistantMsg, updatedAssistantMsg, changedPart) {
682
+ // Apply preprocessing if provided
683
+ const preprocess = this.preprocessAgentMessage();
684
+ if (preprocess) {
685
+ updatedAssistantMsg = preprocess(updatedAssistantMsg, changedPart);
273
686
  }
274
- if (messageFromAssistant.finishReason === 'error') {
275
- currentAssistantMessage.timestamp = new Date().toISOString();
276
- this.isLoading = false;
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
+ // Invoke tool callback - which may call addToolOutput
692
+ if (changedPart?.type === 'tool-result') {
693
+ this.debugLog(`Received tool result: ${changedPart.toolName}`, changedPart);
694
+ this.onToolResult.emit(changedPart);
277
695
  }
278
- if (lastStep?.toolResults) {
279
- lastStep.toolResults.forEach(toolResult => {
280
- this.onToolResult.emit(toolResult);
281
- });
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
+ }
711
+ }
712
+ }
713
+ }
714
+ if (updatedAssistantMsg.finishReason) {
715
+ this.debugLog(`Received assistant message with ${updatedAssistantMsg.steps?.length || 0} steps: `, updatedAssistantMsg);
716
+ try {
717
+ this.handleMessageFinish(currentAssistantMsg, updatedAssistantMsg);
718
+ }
719
+ finally {
720
+ this._isLoading.set(false);
721
+ }
282
722
  }
283
723
  // as we are mutating the currentAssistantMessage object
284
724
  // which is already in the messages array we need to emit a new array reference
285
- this.messages$.next([...this.messages$.value]);
725
+ this.messages.set([...this.messages()]);
726
+ }
727
+ handleMessageFinish(currentAssistantMsg, updatedAssistantMsg) {
728
+ currentAssistantMsg.timestamp = new Date().toISOString();
729
+ if (updatedAssistantMsg.finishReason === 'stop') {
730
+ this.onMessageFinish.emit(currentAssistantMsg);
731
+ void this.getAnalyticsMetadataContext()
732
+ .then(analyticsContext => {
733
+ this.userAnalyticsService.sendAssistantMessageComplete(updatedAssistantMsg, this.messages(), analyticsContext);
734
+ })
735
+ .catch(error => {
736
+ console.error('Error sending analytics:', error);
737
+ });
738
+ if (currentAssistantMsg.content.trim().length === 0) {
739
+ // Should be impossible, but adding this as a debugging aid, as it was seen once by a user
740
+ 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
+ }
742
+ }
743
+ else if (updatedAssistantMsg.finishReason === 'error') {
744
+ if (currentAssistantMsg.content.trim().length === 0) {
745
+ // Should be impossible, but adding this as a debugging aid, as it was seen once by a user
746
+ 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
+ }
748
+ }
286
749
  }
287
750
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AgentChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
288
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AgentChatComponent, isStandalone: true, selector: "c8y-agent-chat", inputs: { agent: "agent", suggestions: "suggestions", title: "title", headline: "headline", welcomeText: "welcomeText", autoInstallAgents: "autoInstallAgents", variables: "variables", stepRenderComponent: "stepRenderComponent" }, outputs: { onMessageDelta: "onMessageDelta", onMessageText: "onMessageText", onMessageFinish: "onMessageFinish", onToolResult: "onToolResult" }, ngImport: i0, template: "@if (hasError && isLoading) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n}\n@if (hasError && !isLoading) {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'disclaimer'\"\n [title]=\"'An error occurred' | translate\"\n [subtitle]=\"errorMsg | translate\"\n [horizontal]=\"true\"\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 }\n </c8y-ui-empty-state>\n}\n\n@if (!hasError) {\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoading\"\n (onCancel)=\"cancel()\"\n [config]=\"{ title: title, headline: headline, welcomeText: welcomeText }\"\n [prompt]=\"prompt\"\n >\n @for (message of messages$ | async; track $index; let i = $index) {\n <c8y-ai-chat-message\n [message]=\"{ role: message.role, content: '', timestamp: message.timestamp }\"\n >\n @if (!message.componentSteps$) {\n <div [innerHTML]=\"message.content | markdownToHtml | async\"></div>\n }\n @for (step of message.componentSteps$ | async; track $index) {\n @if (typeof step.content === 'string') {\n <div [innerHTML]=\"step.content | markdownToHtml | async\"></div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"step.content\"\n [ngComponentOutletInputs]=\"{ step: step.origin }\"\n ></ng-container>\n }\n }\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]=\"isLoading\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && (i < (messages$ | async)?.length - 1 || !isLoading)) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoading\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && (i < (messages$ | async)?.length - 1 || !isLoading)) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoading\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && (i < (messages$ | async)?.length - 1 || !isLoading)) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoading\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n !message.content && message.role === 'assistant' && i === (messages$ | async)?.length - 1\n ) {\n <div class=\"text-center\">\n <c8y-loading></c8y-loading>\n </div>\n }\n </c8y-ai-chat-message>\n }\n\n @for (suggestion of suggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [label]=\"suggestion.label\"\n [prompt]=\"suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoading\"\n ></c8y-ai-chat-suggestion>\n }\n </c8y-ai-chat>\n}\n", dependencies: [{ kind: "component", type: AiChatComponent, selector: "c8y-ai-chat", inputs: ["isLoading", "disabled", "prompt", "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: ["disabled", "tooltip", "icon"], outputs: ["click"] }, { 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: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
751
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", 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" }] }); }
289
752
  }
290
753
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AgentChatComponent, decorators: [{
291
754
  type: Component,
@@ -299,52 +762,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
299
762
  AsyncPipe,
300
763
  NgComponentOutlet,
301
764
  C8yTranslatePipe,
302
- EmptyStateComponent
303
- ], template: "@if (hasError && isLoading) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n}\n@if (hasError && !isLoading) {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'disclaimer'\"\n [title]=\"'An error occurred' | translate\"\n [subtitle]=\"errorMsg | translate\"\n [horizontal]=\"true\"\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 }\n </c8y-ui-empty-state>\n}\n\n@if (!hasError) {\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoading\"\n (onCancel)=\"cancel()\"\n [config]=\"{ title: title, headline: headline, welcomeText: welcomeText }\"\n [prompt]=\"prompt\"\n >\n @for (message of messages$ | async; track $index; let i = $index) {\n <c8y-ai-chat-message\n [message]=\"{ role: message.role, content: '', timestamp: message.timestamp }\"\n >\n @if (!message.componentSteps$) {\n <div [innerHTML]=\"message.content | markdownToHtml | async\"></div>\n }\n @for (step of message.componentSteps$ | async; track $index) {\n @if (typeof step.content === 'string') {\n <div [innerHTML]=\"step.content | markdownToHtml | async\"></div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"step.content\"\n [ngComponentOutletInputs]=\"{ step: step.origin }\"\n ></ng-container>\n }\n }\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]=\"isLoading\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && (i < (messages$ | async)?.length - 1 || !isLoading)) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoading\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && (i < (messages$ | async)?.length - 1 || !isLoading)) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoading\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && (i < (messages$ | async)?.length - 1 || !isLoading)) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoading\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n !message.content && message.role === 'assistant' && i === (messages$ | async)?.length - 1\n ) {\n <div class=\"text-center\">\n <c8y-loading></c8y-loading>\n </div>\n }\n </c8y-ai-chat-message>\n }\n\n @for (suggestion of suggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [label]=\"suggestion.label\"\n [prompt]=\"suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoading\"\n ></c8y-ai-chat-suggestion>\n }\n </c8y-ai-chat>\n}\n" }]
304
- }], propDecorators: { agent: [{
305
- type: Input,
306
- args: [{ required: true }]
307
- }], suggestions: [{
308
- type: Input
309
- }], title: [{
310
- type: Input
311
- }], headline: [{
312
- type: Input
313
- }], welcomeText: [{
314
- type: Input
315
- }], autoInstallAgents: [{
316
- type: Input
317
- }], variables: [{
318
- type: Input
319
- }], stepRenderComponent: [{
320
- type: Input
321
- }], onMessageDelta: [{
322
- type: Output
323
- }], onMessageText: [{
324
- type: Output
325
- }], onMessageFinish: [{
326
- type: Output
327
- }], onToolResult: [{
328
- type: Output
329
- }] } });
765
+ EmptyStateComponent,
766
+ CollapseModule,
767
+ C8yTranslateDirective,
768
+ GuideDocsComponent,
769
+ 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"] }]
771
+ }], 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 }] }] } });
330
772
 
331
773
  class WidgetAiChatSectionComponent {
332
774
  constructor() {
775
+ this._componentConfig = signal({}, ...(ngDevMode ? [{ debugName: "_componentConfig" }] : []));
333
776
  this.suggestions = [];
334
- this.headline = gettext('Welcome!');
335
- this.title = gettext('What can I help you with?');
777
+ /** Provide the specified `contextVariableName` as a variable that can be used in the prompt. */
336
778
  this.useContextAsVariable = true;
337
779
  this.contextVariableName = 'c8yContext';
780
+ this.variables$ = of({});
781
+ this.injector = inject(Injector);
338
782
  this.widgetConfigService = inject(WidgetConfigService);
339
783
  this.alertService = inject(AlertService);
340
784
  this.contextRouteService = inject(ContextRouteService);
341
- /**
342
- * A component that is used to render each step in the chat. By default, it uses the `AgentStepFeedbackComponent`.
343
- * You can provide your own component by returning it from this callback.
344
- *
345
- * @returns A promise that resolves to a component type.
346
- */
347
- this.loadRenderStepComponent = () => Promise.resolve(AgentStepFeedbackComponent);
348
785
  /**
349
786
  * A callback that is invoked when a tool result is returned. The function can return either:
350
787
  * - `undefined` or `void`: No action is taken.
@@ -390,7 +827,10 @@ class WidgetAiChatSectionComponent {
390
827
  [this.contextVariableName]: contextData || this.widgetConfigService.currentConfig.device
391
828
  };
392
829
  }
393
- this._renderStepComponent = await this.loadRenderStepComponent();
830
+ if (this.loadComponentConfig) {
831
+ const config = await this.loadComponentConfig(this.injector);
832
+ this._componentConfig.set(config);
833
+ }
394
834
  }
395
835
  /**
396
836
  * Handles the tool result returned from the AI agent.
@@ -414,20 +854,18 @@ class WidgetAiChatSectionComponent {
414
854
  }
415
855
  }
416
856
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WidgetAiChatSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
417
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: WidgetAiChatSectionComponent, isStandalone: true, selector: "c8y-widget-ai-chat-section", inputs: { suggestions: "suggestions", agent: "agent", headline: "headline", title: "title", welcomeText: "welcomeText", useContextAsVariable: "useContextAsVariable", contextVariableName: "contextVariableName", variables: "variables", loadRenderStepComponent: "loadRenderStepComponent" }, ngImport: i0, template: "<c8y-widget-config-feedback>\n <div\n class=\"m-l-4 btn-ai btn-ai-hint btn-sm\"\n [title]=\"'AI code assistant' | translate\"\n >\n <span></span>\n </div>\n</c8y-widget-config-feedback>\n\n<c8y-agent-chat\n [title]=\"title | translate\"\n [headline]=\"headline | translate\"\n [welcomeText]=\"welcomeText | translate\"\n #agentChat\n [suggestions]=\"(agentChat.messages$ | async)?.length === 0 ? suggestions : []\"\n [agent]=\"agent\"\n [variables]=\"variables | async\"\n [stepRenderComponent]=\"_renderStepComponent\"\n (onToolResult)=\"toolResultHandler($event)\"\n></c8y-agent-chat>\n", dependencies: [{ kind: "component", type: AgentChatComponent, selector: "c8y-agent-chat", inputs: ["agent", "suggestions", "title", "headline", "welcomeText", "autoInstallAgents", "variables", "stepRenderComponent"], outputs: ["onMessageDelta", "onMessageText", "onMessageFinish", "onToolResult"] }, { kind: "component", type: WidgetConfigFeedbackComponent, selector: "c8y-widget-config-feedback" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
857
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: WidgetAiChatSectionComponent, isStandalone: true, selector: "c8y-widget-ai-chat-section", inputs: { loadComponentConfig: "loadComponentConfig", suggestions: "suggestions", agent: "agent", chatConfig: "chatConfig", useContextAsVariable: "useContextAsVariable", contextVariableName: "contextVariableName", variables: "variables", onToolResult: "onToolResult" }, ngImport: i0, template: "<c8y-widget-config-feedback>\n <div\n class=\"m-l-4 btn-ai btn-ai-hint btn-sm\"\n [title]=\"'AI code assistant' | translate\"\n >\n <span></span>\n </div>\n</c8y-widget-config-feedback>\n\n<c8y-agent-chat\n #agentChat\n [chatConfig]=\"chatConfig\"\n [suggestions]=\"agentChat.isWelcoming() ? suggestions : []\"\n [agent]=\"agent\"\n [variables]=\"variables | async\"\n (onToolResult)=\"toolResultHandler($event)\"\n [preprocessAgentMessage]=\"_componentConfig().preprocessAgentMessage\"\n [pruneMessagesForAgent]=\"_componentConfig().pruneMessagesForAgent\"\n [assistantMessageDisplayConfig]=\"_componentConfig().assistantMessageDisplayConfig || {}\"\n></c8y-agent-chat>\n", dependencies: [{ kind: "component", type: AgentChatComponent, selector: "c8y-agent-chat", inputs: ["agent", "suggestions", "chatConfig", "welcomeTemplate", "autoCreateAgents", "variables", "assistantMessageComponent", "assistantMessageDisplayConfig", "preprocessAgentMessage", "pruneMessagesForAgent", "initialChatHistory", "groundingContextProvider", "userAnalyticsContext"], outputs: ["onMessageFinish", "onToolResult", "onFeedback"] }, { kind: "component", type: WidgetConfigFeedbackComponent, selector: "c8y-widget-config-feedback" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
418
858
  }
419
859
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WidgetAiChatSectionComponent, decorators: [{
420
860
  type: Component,
421
- args: [{ selector: 'c8y-widget-ai-chat-section', imports: [AgentChatComponent, C8yTranslatePipe, AsyncPipe, WidgetConfigFeedbackComponent], standalone: true, template: "<c8y-widget-config-feedback>\n <div\n class=\"m-l-4 btn-ai btn-ai-hint btn-sm\"\n [title]=\"'AI code assistant' | translate\"\n >\n <span></span>\n </div>\n</c8y-widget-config-feedback>\n\n<c8y-agent-chat\n [title]=\"title | translate\"\n [headline]=\"headline | translate\"\n [welcomeText]=\"welcomeText | translate\"\n #agentChat\n [suggestions]=\"(agentChat.messages$ | async)?.length === 0 ? suggestions : []\"\n [agent]=\"agent\"\n [variables]=\"variables | async\"\n [stepRenderComponent]=\"_renderStepComponent\"\n (onToolResult)=\"toolResultHandler($event)\"\n></c8y-agent-chat>\n" }]
422
- }], propDecorators: { suggestions: [{
861
+ args: [{ selector: 'c8y-widget-ai-chat-section', imports: [AgentChatComponent, C8yTranslatePipe, WidgetConfigFeedbackComponent, AsyncPipe], standalone: true, template: "<c8y-widget-config-feedback>\n <div\n class=\"m-l-4 btn-ai btn-ai-hint btn-sm\"\n [title]=\"'AI code assistant' | translate\"\n >\n <span></span>\n </div>\n</c8y-widget-config-feedback>\n\n<c8y-agent-chat\n #agentChat\n [chatConfig]=\"chatConfig\"\n [suggestions]=\"agentChat.isWelcoming() ? suggestions : []\"\n [agent]=\"agent\"\n [variables]=\"variables | async\"\n (onToolResult)=\"toolResultHandler($event)\"\n [preprocessAgentMessage]=\"_componentConfig().preprocessAgentMessage\"\n [pruneMessagesForAgent]=\"_componentConfig().pruneMessagesForAgent\"\n [assistantMessageDisplayConfig]=\"_componentConfig().assistantMessageDisplayConfig || {}\"\n></c8y-agent-chat>\n" }]
862
+ }], propDecorators: { loadComponentConfig: [{
423
863
  type: Input
424
- }], agent: [{
425
- type: Input
426
- }], headline: [{
864
+ }], suggestions: [{
427
865
  type: Input
428
- }], title: [{
866
+ }], agent: [{
429
867
  type: Input
430
- }], welcomeText: [{
868
+ }], chatConfig: [{
431
869
  type: Input
432
870
  }], useContextAsVariable: [{
433
871
  type: Input
@@ -435,7 +873,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
435
873
  type: Input
436
874
  }], variables: [{
437
875
  type: Input
438
- }], loadRenderStepComponent: [{
876
+ }], onToolResult: [{
439
877
  type: Input
440
878
  }] } });
441
879
 
@@ -443,5 +881,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
443
881
  * Generated bundle index. Do not edit.
444
882
  */
445
883
 
446
- export { AgentChatComponent, AgentStepFeedbackComponent, WidgetAiChatSectionComponent };
884
+ export { AgentChatComponent, WidgetAiChatSectionComponent };
447
885
  //# sourceMappingURL=c8y-ngx-components-ai-agent-chat.mjs.map