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