@memberjunction/ai-agents 3.4.0 → 4.0.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/dist/AgentDataPreloader.d.ts +117 -1
- package/dist/AgentDataPreloader.d.ts.map +1 -1
- package/dist/AgentDataPreloader.js +156 -58
- package/dist/AgentDataPreloader.js.map +1 -1
- package/dist/AgentRunner.d.ts +212 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +354 -103
- package/dist/AgentRunner.js.map +1 -1
- package/dist/PayloadChangeAnalyzer.d.ts +68 -0
- package/dist/PayloadChangeAnalyzer.d.ts.map +1 -1
- package/dist/PayloadChangeAnalyzer.js +68 -32
- package/dist/PayloadChangeAnalyzer.js.map +1 -1
- package/dist/PayloadFeedbackManager.d.ts +57 -1
- package/dist/PayloadFeedbackManager.d.ts.map +1 -1
- package/dist/PayloadFeedbackManager.js +66 -21
- package/dist/PayloadFeedbackManager.js.map +1 -1
- package/dist/PayloadManager.d.ts +286 -1
- package/dist/PayloadManager.d.ts.map +1 -1
- package/dist/PayloadManager.js +423 -50
- package/dist/PayloadManager.js.map +1 -1
- package/dist/__tests__/action-changes.test.d.ts +13 -0
- package/dist/__tests__/action-changes.test.d.ts.map +1 -1
- package/dist/__tests__/action-changes.test.js +57 -4
- package/dist/__tests__/action-changes.test.js.map +1 -1
- package/dist/__tests__/agent-memory-features.test.d.ts +52 -0
- package/dist/__tests__/agent-memory-features.test.d.ts.map +1 -1
- package/dist/__tests__/agent-memory-features.test.js +96 -13
- package/dist/__tests__/agent-memory-features.test.js.map +1 -1
- package/dist/__tests__/agent-type-prompt-params.test.d.ts +12 -0
- package/dist/__tests__/agent-type-prompt-params.test.d.ts.map +1 -1
- package/dist/__tests__/agent-type-prompt-params.test.js +112 -20
- package/dist/__tests__/agent-type-prompt-params.test.js.map +1 -1
- package/dist/__tests__/chat-handling-option.test.d.ts +26 -0
- package/dist/__tests__/chat-handling-option.test.d.ts.map +1 -1
- package/dist/__tests__/chat-handling-option.test.js +41 -2
- package/dist/__tests__/chat-handling-option.test.js.map +1 -1
- package/dist/agent-context-injector.d.ts +114 -0
- package/dist/agent-context-injector.d.ts.map +1 -1
- package/dist/agent-context-injector.js +138 -19
- package/dist/agent-context-injector.js.map +1 -1
- package/dist/agent-types/base-agent-type.d.ts +354 -0
- package/dist/agent-types/base-agent-type.d.ts.map +1 -1
- package/dist/agent-types/base-agent-type.js +288 -17
- package/dist/agent-types/base-agent-type.js.map +1 -1
- package/dist/agent-types/flow-agent-type.d.ts +328 -2
- package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
- package/dist/agent-types/flow-agent-type.js +503 -63
- package/dist/agent-types/flow-agent-type.js.map +1 -1
- package/dist/agent-types/index.d.ts +14 -3
- package/dist/agent-types/index.d.ts.map +1 -1
- package/dist/agent-types/index.js +14 -23
- package/dist/agent-types/index.js.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.d.ts +190 -0
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +23 -6
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +62 -0
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js +3 -2
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts +139 -2
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +207 -35
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +1344 -1
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +2282 -211
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +24 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -35
- package/dist/index.js.map +1 -1
- package/dist/memory-cleanup-agent.d.ts +53 -1
- package/dist/memory-cleanup-agent.d.ts.map +1 -1
- package/dist/memory-cleanup-agent.js +89 -21
- package/dist/memory-cleanup-agent.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +61 -1
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +260 -116
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/services/AgentEmbeddingService.d.ts +16 -0
- package/dist/services/AgentEmbeddingService.d.ts.map +1 -0
- package/dist/services/AgentEmbeddingService.js +158 -0
- package/dist/services/AgentEmbeddingService.js.map +1 -0
- package/dist/types/AgentMatchResult.d.ts +18 -0
- package/dist/types/AgentMatchResult.d.ts.map +1 -0
- package/dist/types/AgentMatchResult.js +3 -0
- package/dist/types/AgentMatchResult.js.map +1 -0
- package/dist/types/payload-operations.d.ts +51 -0
- package/dist/types/payload-operations.d.ts.map +1 -1
- package/dist/types/payload-operations.js +54 -15
- package/dist/types/payload-operations.js.map +1 -1
- package/dist/utils/ConversationMessageResolver.d.ts +79 -1
- package/dist/utils/ConversationMessageResolver.d.ts.map +1 -1
- package/dist/utils/ConversationMessageResolver.js +99 -9
- package/dist/utils/ConversationMessageResolver.js.map +1 -1
- package/package.json +20 -19
package/dist/AgentRunner.js
CHANGED
|
@@ -1,66 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AgentRunner provides a thin wrapper for executing AI agents
|
|
3
|
+
* using the MemberJunction AI Agent framework.
|
|
4
|
+
*
|
|
5
|
+
* This module handles agent instantiation using the ClassFactory pattern to ensure
|
|
6
|
+
* the correct agent subclass is used based on the agent type's DriverClass property.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/ai-agents
|
|
9
|
+
* @author MemberJunction.com
|
|
10
|
+
* @since 2.49.0
|
|
11
|
+
*/
|
|
12
|
+
import { LogError, LogStatusEx, IsVerboseLoggingEnabled, LogStatus, Metadata, RunView } from '@memberjunction/core';
|
|
13
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
14
|
+
import { AIEngine } from '@memberjunction/aiengine';
|
|
15
|
+
import { BaseAgent } from './base-agent.js';
|
|
16
|
+
/**
|
|
17
|
+
* AgentRunner provides a thin wrapper for executing AI agents.
|
|
18
|
+
*
|
|
19
|
+
* This class handles:
|
|
20
|
+
* - Loading agent type metadata to get the DriverClass
|
|
21
|
+
* - Instantiating the correct agent class using ClassFactory
|
|
22
|
+
* - Passing through to the agent's Execute method
|
|
23
|
+
*
|
|
24
|
+
* @class AgentRunner
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const runner = new AgentRunner();
|
|
28
|
+
* const result = await runner.RunAgent({
|
|
29
|
+
* agent: myAgent,
|
|
30
|
+
* conversationMessages: messages,
|
|
31
|
+
* contextUser: currentUser
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class AgentRunner {
|
|
36
|
+
/**
|
|
37
|
+
* Runs an AI agent with the specified parameters.
|
|
38
|
+
*
|
|
39
|
+
* This method acts as a thin pass-through that:
|
|
40
|
+
* 1. Loads the agent type to get the DriverClass
|
|
41
|
+
* 2. Uses ClassFactory to instantiate the correct agent class
|
|
42
|
+
* 3. Calls Execute on the agent instance and returns the result
|
|
43
|
+
*
|
|
44
|
+
* @param {ExecuteAgentParams} params - Parameters for agent execution (same as BaseAgent.Execute)
|
|
45
|
+
* @template C - The type of the agent's context as provided in the ExecuteAgentParams
|
|
46
|
+
* @template R - The type of the agent's result as returned in ExecuteAgentResult
|
|
47
|
+
* @returns {Promise<ExecuteAgentResult<T>>} The execution result (same as BaseAgent.Execute)
|
|
48
|
+
*
|
|
49
|
+
* @throws {Error} Throws if agent type loading fails or agent instantiation fails
|
|
50
|
+
*/
|
|
32
51
|
async RunAgent(params) {
|
|
33
52
|
try {
|
|
34
|
-
|
|
53
|
+
LogStatusEx({
|
|
35
54
|
message: `AgentRunner: Starting execution for agent: ${params.agent.Name} (ID: ${params.agent.ID})`,
|
|
36
55
|
verboseOnly: true,
|
|
37
|
-
isVerboseEnabled: () => params.verbose === true ||
|
|
56
|
+
isVerboseEnabled: () => params.verbose === true || IsVerboseLoggingEnabled()
|
|
38
57
|
});
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
// Ensure AIEngine is configured
|
|
59
|
+
await AIEngine.Instance.Config(false, params.contextUser);
|
|
60
|
+
// Find the agent type to get the DriverClass
|
|
61
|
+
const agentType = AIEngine.Instance.AgentTypes.find(at => at.ID === params.agent.TypeID);
|
|
41
62
|
if (!agentType) {
|
|
42
63
|
throw new Error(`Agent type not found for ID: ${params.agent.TypeID}`);
|
|
43
64
|
}
|
|
65
|
+
// Get the correct agent class using ClassFactory, prefer the agent's DriverClass if specified, otherwise fallback to Agent Type, otherwise we get BaseAgent from ClassFactory
|
|
44
66
|
const driverClass = params.agent.DriverClass || agentType.DriverClass;
|
|
45
|
-
|
|
67
|
+
LogStatusEx({
|
|
46
68
|
message: `AgentRunner: Using driver class: ${driverClass}`,
|
|
47
69
|
verboseOnly: true,
|
|
48
|
-
isVerboseEnabled: () => params.verbose === true ||
|
|
70
|
+
isVerboseEnabled: () => params.verbose === true || IsVerboseLoggingEnabled()
|
|
49
71
|
});
|
|
50
|
-
const agentInstance =
|
|
72
|
+
const agentInstance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseAgent, driverClass);
|
|
51
73
|
if (!agentInstance) {
|
|
52
74
|
throw new Error(`Failed to create agent instance for driver class: ${driverClass}`);
|
|
53
75
|
}
|
|
76
|
+
// Execute the agent and return the result directly
|
|
54
77
|
return await agentInstance.Execute(params);
|
|
55
78
|
}
|
|
56
79
|
catch (error) {
|
|
57
80
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
58
|
-
|
|
81
|
+
LogError(`AgentRunner execution failed: ${errorMessage}`, undefined, error);
|
|
82
|
+
// Re-throw the error since we can't create a proper ExecuteAgentResult without an agent run
|
|
83
|
+
// BaseAgent.Execute will handle creating a proper error result with an agent run
|
|
59
84
|
throw error;
|
|
60
85
|
}
|
|
61
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Runs an AI agent within a conversation context, handling conversation and artifact management.
|
|
89
|
+
*
|
|
90
|
+
* This method provides a complete workflow for running agents in conversations:
|
|
91
|
+
* 1. Creates or uses existing conversation
|
|
92
|
+
* 2. Creates conversation detail record for the user message
|
|
93
|
+
* 3. Executes the agent
|
|
94
|
+
* 4. Creates artifacts from the agent's payload (if configured)
|
|
95
|
+
* 5. Links artifacts to the conversation detail
|
|
96
|
+
*
|
|
97
|
+
* @param params - Core agent execution parameters
|
|
98
|
+
* @param options - Conversation-specific options
|
|
99
|
+
* @returns Promise containing the agent execution result and conversation/artifact metadata
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const runner = new AgentRunner();
|
|
104
|
+
* const result = await runner.RunAgentInConversation({
|
|
105
|
+
* agent: myAgent,
|
|
106
|
+
* conversationMessages: messages,
|
|
107
|
+
* contextUser: currentUser
|
|
108
|
+
* }, {
|
|
109
|
+
* conversationId: existingConvoId, // Optional - creates new if not provided
|
|
110
|
+
* userMessage: 'User query text',
|
|
111
|
+
* createArtifacts: true
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
62
115
|
async RunAgentInConversation(params, options) {
|
|
63
|
-
const md = new
|
|
116
|
+
const md = new Metadata();
|
|
64
117
|
const contextUser = params.contextUser;
|
|
65
118
|
if (!contextUser) {
|
|
66
119
|
throw new Error('contextUser is required for RunAgentInConversation');
|
|
@@ -70,29 +123,40 @@ class AgentRunner {
|
|
|
70
123
|
let userMessageDetailId;
|
|
71
124
|
let agentResponseDetailId;
|
|
72
125
|
let agentResponseDetail;
|
|
126
|
+
// If conversationDetailId is provided, use it (UI-created agent response detail)
|
|
73
127
|
if (options.conversationDetailId) {
|
|
74
128
|
agentResponseDetailId = options.conversationDetailId;
|
|
129
|
+
// Load the conversation detail to get the conversation ID AND keep reference for final status update
|
|
130
|
+
// This ensures backend can update Status/Message even if frontend disconnects (browser refresh)
|
|
75
131
|
agentResponseDetail = await md.GetEntityObject('Conversation Details', contextUser);
|
|
76
132
|
if (await agentResponseDetail.Load(agentResponseDetailId)) {
|
|
77
133
|
conversationId = agentResponseDetail.ConversationID;
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
LogStatus(`Using existing conversation ${conversationId} and agent response detail ${agentResponseDetailId}`);
|
|
135
|
+
// Note: In this case, we don't know the user message detail ID
|
|
136
|
+
userMessageDetailId = agentResponseDetailId; // For backward compatibility
|
|
80
137
|
}
|
|
81
138
|
else {
|
|
82
139
|
throw new Error(`Failed to load conversation detail ${agentResponseDetailId}`);
|
|
83
140
|
}
|
|
84
141
|
}
|
|
85
142
|
else {
|
|
143
|
+
// Server creates BOTH user message and agent response details
|
|
86
144
|
if (!options.userMessage) {
|
|
87
145
|
throw new Error('userMessage is required when conversationDetailId is not provided');
|
|
88
146
|
}
|
|
147
|
+
// Step 1: Get or create conversation
|
|
89
148
|
conversationId = options.conversationId || '';
|
|
90
149
|
if (!conversationId) {
|
|
91
|
-
|
|
150
|
+
LogStatus('Creating new conversation');
|
|
92
151
|
const conversation = await md.GetEntityObject('Conversations', contextUser);
|
|
152
|
+
// Smart conversation naming:
|
|
153
|
+
// 1. Use provided name if given
|
|
154
|
+
// 2. Otherwise, use AI prompt to generate a good name from user message
|
|
155
|
+
// 3. Fallback to "Chat with [AgentName]"
|
|
93
156
|
let conversationName = options.conversationName;
|
|
94
157
|
let conversationDescription = null;
|
|
95
158
|
if (!conversationName && options.userMessage) {
|
|
159
|
+
// Try to generate a name using the "Name Conversation" prompt (same as UI)
|
|
96
160
|
const nameResult = await this.GenerateConversationName(options.userMessage, contextUser);
|
|
97
161
|
if (nameResult) {
|
|
98
162
|
conversationName = nameResult.name;
|
|
@@ -103,10 +167,11 @@ class AgentRunner {
|
|
|
103
167
|
conversationName = `Chat with ${params.agent.Name}`;
|
|
104
168
|
}
|
|
105
169
|
conversation.Name = conversationName;
|
|
106
|
-
conversation.Description = conversationDescription || '';
|
|
170
|
+
conversation.Description = conversationDescription || ''; // Set description too (like UI does)
|
|
107
171
|
conversation.UserID = contextUser.ID;
|
|
108
172
|
conversation.Status = 'Available';
|
|
109
|
-
conversation.DataContextID = null;
|
|
173
|
+
conversation.DataContextID = null; // Can be set by caller if needed
|
|
174
|
+
// Link to test run if provided (for test execution traceability)
|
|
110
175
|
if (options.testRunId) {
|
|
111
176
|
conversation.TestRunID = options.testRunId;
|
|
112
177
|
}
|
|
@@ -114,15 +179,17 @@ class AgentRunner {
|
|
|
114
179
|
throw new Error('Failed to create conversation');
|
|
115
180
|
}
|
|
116
181
|
conversationId = conversation.ID;
|
|
117
|
-
|
|
182
|
+
LogStatus(`Created conversation ${conversationId}: ${conversationName}`);
|
|
118
183
|
}
|
|
119
|
-
|
|
184
|
+
// Step 2: Create conversation detail for user message
|
|
185
|
+
LogStatus('Creating conversation detail for user message');
|
|
120
186
|
const userMessageDetail = await md.GetEntityObject('Conversation Details', contextUser);
|
|
121
187
|
userMessageDetail.ConversationID = conversationId;
|
|
122
188
|
userMessageDetail.Message = options.userMessage;
|
|
123
189
|
userMessageDetail.Role = 'User';
|
|
124
190
|
userMessageDetail.UserID = contextUser.ID;
|
|
125
191
|
userMessageDetail.HiddenToUser = false;
|
|
192
|
+
// Link to test run if provided (for test execution traceability)
|
|
126
193
|
if (options.testRunId) {
|
|
127
194
|
userMessageDetail.TestRunID = options.testRunId;
|
|
128
195
|
}
|
|
@@ -130,8 +197,9 @@ class AgentRunner {
|
|
|
130
197
|
throw new Error('Failed to create user message conversation detail');
|
|
131
198
|
}
|
|
132
199
|
userMessageDetailId = userMessageDetail.ID;
|
|
133
|
-
|
|
134
|
-
|
|
200
|
+
LogStatus(`Created user message detail ${userMessageDetailId}`);
|
|
201
|
+
// Step 3: Create conversation detail for agent response (like UI does)
|
|
202
|
+
LogStatus('Creating conversation detail for agent response');
|
|
135
203
|
agentResponseDetail = await md.GetEntityObject('Conversation Details', contextUser);
|
|
136
204
|
agentResponseDetail.ConversationID = conversationId;
|
|
137
205
|
agentResponseDetail.Message = '⏳ Starting...';
|
|
@@ -139,6 +207,7 @@ class AgentRunner {
|
|
|
139
207
|
agentResponseDetail.Status = 'In-Progress';
|
|
140
208
|
agentResponseDetail.HiddenToUser = false;
|
|
141
209
|
agentResponseDetail.AgentID = params.agent.ID;
|
|
210
|
+
// Link to test run if provided (for test execution traceability)
|
|
142
211
|
if (options.testRunId) {
|
|
143
212
|
agentResponseDetail.TestRunID = options.testRunId;
|
|
144
213
|
}
|
|
@@ -146,23 +215,32 @@ class AgentRunner {
|
|
|
146
215
|
throw new Error('Failed to create agent response conversation detail');
|
|
147
216
|
}
|
|
148
217
|
agentResponseDetailId = agentResponseDetail.ID;
|
|
149
|
-
|
|
218
|
+
LogStatus(`Created agent response detail ${agentResponseDetailId}`);
|
|
150
219
|
}
|
|
151
|
-
|
|
220
|
+
// Step 4: Execute the agent with conversation context
|
|
221
|
+
LogStatus(`Executing agent ${params.agent.Name} in conversation context`);
|
|
222
|
+
// Flag to prevent late progress callbacks from overwriting final status
|
|
223
|
+
// Progress callbacks fire async and may complete AFTER agent returns
|
|
152
224
|
let agentExecutionCompleted = false;
|
|
225
|
+
// Wrap progress callback to update agent response detail
|
|
153
226
|
const originalOnProgress = params.onProgress;
|
|
154
227
|
const wrappedOnProgress = agentResponseDetail
|
|
155
228
|
? async (progress) => {
|
|
229
|
+
// Skip DB save if agent already completed - prevents race condition
|
|
230
|
+
// where late progress overwrites final Status='Complete'
|
|
156
231
|
if (agentExecutionCompleted) {
|
|
232
|
+
// Still call original callback for PubSub streaming
|
|
157
233
|
if (originalOnProgress) {
|
|
158
234
|
await originalOnProgress(progress);
|
|
159
235
|
}
|
|
160
236
|
return;
|
|
161
237
|
}
|
|
238
|
+
// Update the agent response detail with progress message
|
|
162
239
|
if (agentResponseDetail && progress.message) {
|
|
163
240
|
agentResponseDetail.Message = progress.message;
|
|
164
241
|
await agentResponseDetail.Save();
|
|
165
242
|
}
|
|
243
|
+
// Call original callback if provided
|
|
166
244
|
if (originalOnProgress) {
|
|
167
245
|
await originalOnProgress(progress);
|
|
168
246
|
}
|
|
@@ -170,19 +248,26 @@ class AgentRunner {
|
|
|
170
248
|
: originalOnProgress;
|
|
171
249
|
const modifiedParams = {
|
|
172
250
|
...params,
|
|
173
|
-
data: { ...params.data, conversationId },
|
|
251
|
+
data: { ...params.data, conversationId }, // ensure we pass along OUR conversationId
|
|
174
252
|
conversationDetailId: agentResponseDetailId,
|
|
175
253
|
onProgress: wrappedOnProgress
|
|
176
254
|
};
|
|
177
255
|
const agentResult = await this.RunAgent(modifiedParams);
|
|
256
|
+
// Mark execution as completed to stop progress saves
|
|
178
257
|
agentExecutionCompleted = true;
|
|
258
|
+
// Step 5: Update agent response detail with final result
|
|
259
|
+
// ALWAYS update status - don't rely on frontend (browser may refresh during execution)
|
|
179
260
|
if (agentResponseDetail && agentResponseDetailId) {
|
|
261
|
+
// Wait for any in-flight progress save to complete
|
|
262
|
+
// EnsureSaveComplete() resolves immediately if no save in progress
|
|
180
263
|
await agentResponseDetail.EnsureSaveComplete();
|
|
181
|
-
|
|
264
|
+
LogStatus('Updating agent response detail with final result');
|
|
265
|
+
// Reload to get any updates from agent execution
|
|
182
266
|
await agentResponseDetail.Load(agentResponseDetailId);
|
|
183
267
|
agentResponseDetail.Message = agentResult.agentRun?.Message ||
|
|
184
268
|
(agentResult.success ? '✅ Completed' : '❌ Failed');
|
|
185
269
|
agentResponseDetail.Status = agentResult.success ? 'Complete' : 'Error';
|
|
270
|
+
// Set response form and command fields
|
|
186
271
|
if (agentResult.responseForm) {
|
|
187
272
|
agentResponseDetail.ResponseForm = JSON.stringify(agentResult.responseForm);
|
|
188
273
|
}
|
|
@@ -193,20 +278,28 @@ class AgentRunner {
|
|
|
193
278
|
agentResponseDetail.AutomaticCommands = JSON.stringify(agentResult.automaticCommands);
|
|
194
279
|
}
|
|
195
280
|
await agentResponseDetail.Save();
|
|
196
|
-
|
|
281
|
+
LogStatus(`Updated agent response detail ${agentResponseDetailId} with final status: ${agentResponseDetail.Status}`);
|
|
197
282
|
}
|
|
283
|
+
// Step 6: Process artifacts if requested and agent succeeded
|
|
198
284
|
let artifactInfo;
|
|
199
|
-
const shouldCreateArtifacts = options.createArtifacts !== false;
|
|
285
|
+
const shouldCreateArtifacts = options.createArtifacts !== false; // Default true
|
|
200
286
|
if (shouldCreateArtifacts && agentResult.success && agentResult.payload) {
|
|
287
|
+
// Artifacts link to agent response detail ID
|
|
201
288
|
artifactInfo = await this.ProcessAgentArtifacts(agentResult, agentResponseDetailId, options.sourceArtifactId, contextUser);
|
|
202
289
|
}
|
|
290
|
+
// Step 7: Save media outputs to AIAgentRunMedia and create conversation attachments
|
|
203
291
|
let mediaIds = [];
|
|
204
292
|
if (agentResult.mediaOutputs && agentResult.mediaOutputs.length > 0) {
|
|
293
|
+
// Filter to only media that should be persisted (persist !== false)
|
|
205
294
|
const mediaToSave = agentResult.mediaOutputs.filter(m => m.persist !== false);
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
LogStatus(`Processing ${mediaToSave.length} of ${agentResult.mediaOutputs.length} media outputs (filtered by persist flag)`);
|
|
296
|
+
// Save to AIAgentRunMedia for permanent storage
|
|
297
|
+
mediaIds = await this.SaveAgentRunMedia(agentResult.agentRun.ID, mediaToSave, // Pass filtered array
|
|
298
|
+
contextUser);
|
|
299
|
+
// Create ConversationDetailAttachment records for UI display
|
|
208
300
|
if (agentResponseDetailId && mediaIds.length > 0) {
|
|
209
|
-
await this.CreateConversationMediaAttachments(agentResponseDetailId, mediaToSave,
|
|
301
|
+
await this.CreateConversationMediaAttachments(agentResponseDetailId, mediaToSave, // Pass same filtered array to keep indices aligned
|
|
302
|
+
mediaIds, contextUser);
|
|
210
303
|
}
|
|
211
304
|
}
|
|
212
305
|
return {
|
|
@@ -219,13 +312,28 @@ class AgentRunner {
|
|
|
219
312
|
}
|
|
220
313
|
catch (error) {
|
|
221
314
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
222
|
-
|
|
315
|
+
LogError(`RunAgentInConversation failed: ${errorMessage}`, undefined, error);
|
|
223
316
|
throw error;
|
|
224
317
|
}
|
|
225
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Gets the maximum version number for an artifact.
|
|
321
|
+
* Used when creating new versions of explicitly specified artifacts.
|
|
322
|
+
*
|
|
323
|
+
* @param artifactId - The artifact ID to query
|
|
324
|
+
* @param contextUser - The user context for the query
|
|
325
|
+
* @returns The maximum version number, or 0 if no versions exist
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* const runner = new AgentRunner();
|
|
330
|
+
* const maxVersion = await runner.GetMaxVersionForArtifact(artifactId, currentUser);
|
|
331
|
+
* const newVersionNumber = maxVersion + 1;
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
226
334
|
async GetMaxVersionForArtifact(artifactId, contextUser) {
|
|
227
335
|
try {
|
|
228
|
-
const rv = new
|
|
336
|
+
const rv = new RunView();
|
|
229
337
|
const result = await rv.RunView({
|
|
230
338
|
EntityName: 'MJ: Artifact Versions',
|
|
231
339
|
ExtraFilter: `ArtifactID='${artifactId}'`,
|
|
@@ -239,13 +347,30 @@ class AgentRunner {
|
|
|
239
347
|
return 0;
|
|
240
348
|
}
|
|
241
349
|
catch (error) {
|
|
242
|
-
|
|
350
|
+
LogError(`Error getting max version for artifact: ${error.message}`);
|
|
243
351
|
return 0;
|
|
244
352
|
}
|
|
245
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Finds the most recent artifact for a conversation detail to determine versioning.
|
|
356
|
+
* Queries the junction table to locate artifacts linked to a specific conversation message.
|
|
357
|
+
*
|
|
358
|
+
* @param conversationDetailId - The conversation detail ID to query
|
|
359
|
+
* @param contextUser - The user context for the query
|
|
360
|
+
* @returns Artifact info if exists, null if this is the first artifact for this message
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```typescript
|
|
364
|
+
* const runner = new AgentRunner();
|
|
365
|
+
* const previousArtifact = await runner.FindPreviousArtifactForMessage(detailId, currentUser);
|
|
366
|
+
* if (previousArtifact) {
|
|
367
|
+
* console.log(`Found artifact ${previousArtifact.artifactId} at version ${previousArtifact.versionNumber}`);
|
|
368
|
+
* }
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
246
371
|
async FindPreviousArtifactForMessage(conversationDetailId, contextUser) {
|
|
247
372
|
try {
|
|
248
|
-
const rv = new
|
|
373
|
+
const rv = new RunView();
|
|
249
374
|
const result = await rv.RunView({
|
|
250
375
|
EntityName: 'MJ: Conversation Detail Artifacts',
|
|
251
376
|
ExtraFilter: `ConversationDetailID='${conversationDetailId}' AND Direction='Output'`,
|
|
@@ -257,7 +382,7 @@ class AgentRunner {
|
|
|
257
382
|
return null;
|
|
258
383
|
}
|
|
259
384
|
const junction = result.Results[0];
|
|
260
|
-
const md = new
|
|
385
|
+
const md = new Metadata();
|
|
261
386
|
const version = await md.GetEntityObject('MJ: Artifact Versions', contextUser);
|
|
262
387
|
if (!(await version.Load(junction.ArtifactVersionID))) {
|
|
263
388
|
return null;
|
|
@@ -268,56 +393,98 @@ class AgentRunner {
|
|
|
268
393
|
};
|
|
269
394
|
}
|
|
270
395
|
catch (error) {
|
|
271
|
-
|
|
396
|
+
LogError(`Error finding previous artifact: ${error.message}`);
|
|
272
397
|
return null;
|
|
273
398
|
}
|
|
274
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Processes agent completion to create artifacts from the agent's payload.
|
|
402
|
+
* Handles artifact creation, versioning, and linking to conversation details.
|
|
403
|
+
*
|
|
404
|
+
* This method implements intelligent artifact versioning:
|
|
405
|
+
* 1. If sourceArtifactId is provided (explicit continuity), creates new version of that artifact
|
|
406
|
+
* 2. Otherwise, checks for previous artifacts on this conversation detail
|
|
407
|
+
* 3. If previous artifact exists, creates new version of it
|
|
408
|
+
* 4. If no previous artifact, creates entirely new artifact
|
|
409
|
+
*
|
|
410
|
+
* Respects the agent's ArtifactCreationMode configuration:
|
|
411
|
+
* - "Never": Skips artifact creation entirely
|
|
412
|
+
* - "System Only": Creates artifact with Visibility='System Only'
|
|
413
|
+
* - Other modes: Creates artifact with Visibility='Always'
|
|
414
|
+
*
|
|
415
|
+
* @param agentResult - The result from agent execution containing the payload
|
|
416
|
+
* @param conversationDetailId - The conversation detail to link the artifact to
|
|
417
|
+
* @param sourceArtifactId - Optional explicit artifact to version from (agent continuity)
|
|
418
|
+
* @param contextUser - The user context for the operation
|
|
419
|
+
* @returns Artifact metadata if created, undefined if skipped or failed
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```typescript
|
|
423
|
+
* const runner = new AgentRunner();
|
|
424
|
+
* const artifactInfo = await runner.ProcessAgentArtifacts(
|
|
425
|
+
* agentResult,
|
|
426
|
+
* conversationDetailId,
|
|
427
|
+
* sourceArtifactId, // Optional
|
|
428
|
+
* currentUser
|
|
429
|
+
* );
|
|
430
|
+
* if (artifactInfo) {
|
|
431
|
+
* console.log(`Created artifact ${artifactInfo.artifactId} version ${artifactInfo.versionNumber}`);
|
|
432
|
+
* }
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
275
435
|
async ProcessAgentArtifacts(agentResult, conversationDetailId, sourceArtifactId, contextUser) {
|
|
276
436
|
const payload = agentResult.payload;
|
|
277
437
|
const agentRun = agentResult.agentRun;
|
|
278
438
|
if (!payload || Object.keys(payload).length === 0) {
|
|
279
|
-
|
|
439
|
+
LogStatus('No payload to create artifact from');
|
|
280
440
|
return undefined;
|
|
281
441
|
}
|
|
282
|
-
|
|
283
|
-
|
|
442
|
+
// Check agent's ArtifactCreationMode
|
|
443
|
+
await AIEngine.Instance.Config(false, contextUser);
|
|
444
|
+
const agent = AIEngine.Instance.Agents.find(a => a.ID === agentRun.AgentID);
|
|
284
445
|
const creationMode = agent?.ArtifactCreationMode;
|
|
285
446
|
if (creationMode === 'Never') {
|
|
286
|
-
|
|
447
|
+
LogStatus(`Skipping artifact creation - agent "${agent?.Name}" has ArtifactCreationMode='Never'`);
|
|
287
448
|
return undefined;
|
|
288
449
|
}
|
|
289
450
|
try {
|
|
290
|
-
const md = new
|
|
451
|
+
const md = new Metadata();
|
|
291
452
|
const JSON_ARTIFACT_TYPE_ID = 'ae674c7e-ea0d-49ea-89e4-0649f5eb20d4';
|
|
453
|
+
// Determine if creating new artifact or new version
|
|
292
454
|
let artifactId;
|
|
293
455
|
let newVersionNumber;
|
|
294
456
|
let isNewArtifact = false;
|
|
457
|
+
// Priority 1: Use explicit source artifact if provided
|
|
295
458
|
if (sourceArtifactId) {
|
|
296
459
|
const maxVersion = await this.GetMaxVersionForArtifact(sourceArtifactId, contextUser);
|
|
297
460
|
artifactId = sourceArtifactId;
|
|
298
461
|
newVersionNumber = maxVersion + 1;
|
|
299
|
-
|
|
462
|
+
LogStatus(`Creating version ${newVersionNumber} of source artifact ${artifactId}`);
|
|
300
463
|
}
|
|
464
|
+
// Priority 2: Try to find previous artifact for this message
|
|
301
465
|
else {
|
|
302
466
|
const previousArtifact = await this.FindPreviousArtifactForMessage(conversationDetailId, contextUser);
|
|
303
467
|
if (previousArtifact) {
|
|
304
468
|
artifactId = previousArtifact.artifactId;
|
|
305
469
|
newVersionNumber = previousArtifact.versionNumber + 1;
|
|
306
|
-
|
|
470
|
+
LogStatus(`Creating version ${newVersionNumber} of existing artifact ${artifactId}`);
|
|
307
471
|
}
|
|
308
472
|
else {
|
|
473
|
+
// Create new artifact header
|
|
309
474
|
const artifact = await md.GetEntityObject('MJ: Artifacts', contextUser);
|
|
310
475
|
const agentName = agent?.Name || 'Agent';
|
|
311
476
|
artifact.Name = `${agentName} Payload - ${new Date().toLocaleString()}`;
|
|
312
477
|
artifact.Description = `Payload returned by ${agentName}`;
|
|
478
|
+
// Use agent's DefaultArtifactTypeID if available
|
|
313
479
|
const defaultArtifactTypeId = agent?.DefaultArtifactTypeID;
|
|
314
480
|
artifact.TypeID = defaultArtifactTypeId || JSON_ARTIFACT_TYPE_ID;
|
|
315
481
|
artifact.UserID = contextUser.ID;
|
|
316
482
|
artifact.EnvironmentID = contextUser.EnvironmentID ||
|
|
317
483
|
'F51358F3-9447-4176-B313-BF8025FD8D09';
|
|
484
|
+
// Set visibility based on agent's ArtifactCreationMode
|
|
318
485
|
if (creationMode === 'System Only') {
|
|
319
486
|
artifact.Visibility = 'System Only';
|
|
320
|
-
|
|
487
|
+
LogStatus(`Artifact marked as "System Only" per agent configuration`);
|
|
321
488
|
}
|
|
322
489
|
else {
|
|
323
490
|
artifact.Visibility = 'Always';
|
|
@@ -328,9 +495,10 @@ class AgentRunner {
|
|
|
328
495
|
artifactId = artifact.ID;
|
|
329
496
|
newVersionNumber = 1;
|
|
330
497
|
isNewArtifact = true;
|
|
331
|
-
|
|
498
|
+
LogStatus(`Created new artifact: ${artifact.Name} (${artifactId})`);
|
|
332
499
|
}
|
|
333
500
|
}
|
|
501
|
+
// Create artifact version with content
|
|
334
502
|
const version = await md.GetEntityObject('MJ: Artifact Versions', contextUser);
|
|
335
503
|
version.ArtifactID = artifactId;
|
|
336
504
|
version.VersionNumber = newVersionNumber;
|
|
@@ -339,7 +507,8 @@ class AgentRunner {
|
|
|
339
507
|
if (!(await version.Save())) {
|
|
340
508
|
throw new Error('Failed to save artifact version');
|
|
341
509
|
}
|
|
342
|
-
|
|
510
|
+
LogStatus(`Created artifact version ${newVersionNumber} (${version.ID})`);
|
|
511
|
+
// If first version of new artifact, check for extracted Name attribute
|
|
343
512
|
if (isNewArtifact && newVersionNumber === 1) {
|
|
344
513
|
const nameAttr = version.Attributes?.find((attr) => attr.StandardProperty === 'name' || attr.Name?.toLowerCase() === 'name');
|
|
345
514
|
let extractedName = nameAttr?.Value?.trim();
|
|
@@ -349,11 +518,12 @@ class AgentRunner {
|
|
|
349
518
|
if (await artifact.Load(artifactId)) {
|
|
350
519
|
artifact.Name = extractedName;
|
|
351
520
|
if (await artifact.Save()) {
|
|
352
|
-
|
|
521
|
+
LogStatus(`Updated artifact name to: ${artifact.Name}`);
|
|
353
522
|
}
|
|
354
523
|
}
|
|
355
524
|
}
|
|
356
525
|
}
|
|
526
|
+
// Create junction record linking artifact to conversation detail
|
|
357
527
|
const junction = await md.GetEntityObject('MJ: Conversation Detail Artifacts', contextUser);
|
|
358
528
|
junction.ConversationDetailID = conversationDetailId;
|
|
359
529
|
junction.ArtifactVersionID = version.ID;
|
|
@@ -361,7 +531,7 @@ class AgentRunner {
|
|
|
361
531
|
if (!(await junction.Save())) {
|
|
362
532
|
throw new Error('Failed to create artifact-message association');
|
|
363
533
|
}
|
|
364
|
-
|
|
534
|
+
LogStatus(`Linked artifact to conversation detail ${conversationDetailId}`);
|
|
365
535
|
return {
|
|
366
536
|
artifactId,
|
|
367
537
|
versionId: version.ID,
|
|
@@ -369,21 +539,35 @@ class AgentRunner {
|
|
|
369
539
|
};
|
|
370
540
|
}
|
|
371
541
|
catch (error) {
|
|
372
|
-
|
|
542
|
+
LogError(`Failed to process agent artifacts: ${error.message}`);
|
|
373
543
|
return undefined;
|
|
374
544
|
}
|
|
375
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Generates a conversation name and description using the "Name Conversation" AI prompt.
|
|
548
|
+
* Same approach as UI - uses AI to create a meaningful title and description from the first user message.
|
|
549
|
+
* Falls back to null if prompt not found or execution fails (caller will use fallback).
|
|
550
|
+
*
|
|
551
|
+
* @param userMessage - The first user message to base the name on
|
|
552
|
+
* @param contextUser - User context for prompt execution
|
|
553
|
+
* @returns Generated conversation name and description, or null if generation failed
|
|
554
|
+
* @private
|
|
555
|
+
*/
|
|
376
556
|
async GenerateConversationName(userMessage, contextUser) {
|
|
377
557
|
try {
|
|
378
|
-
|
|
379
|
-
const {
|
|
380
|
-
const {
|
|
558
|
+
// Import AIPromptRunner, AIPromptParams, and AIEngine
|
|
559
|
+
const { AIPromptRunner } = await import('@memberjunction/ai-prompts');
|
|
560
|
+
const { AIPromptParams } = await import('@memberjunction/ai-core-plus');
|
|
561
|
+
const { AIEngine } = await import('@memberjunction/aiengine');
|
|
562
|
+
// Ensure AIEngine is configured
|
|
381
563
|
await AIEngine.Instance.Config(false, contextUser);
|
|
564
|
+
// Find the "Name Conversation" prompt (same as UI)
|
|
382
565
|
const prompt = AIEngine.Instance.Prompts.find(p => p.Name === 'Name Conversation');
|
|
383
566
|
if (!prompt) {
|
|
384
|
-
|
|
567
|
+
LogStatus('Name Conversation prompt not found - using fallback naming');
|
|
385
568
|
return null;
|
|
386
569
|
}
|
|
570
|
+
// Execute the prompt using AIPromptRunner
|
|
387
571
|
const promptParams = new AIPromptParams();
|
|
388
572
|
promptParams.prompt = prompt;
|
|
389
573
|
promptParams.contextUser = contextUser;
|
|
@@ -391,12 +575,13 @@ class AgentRunner {
|
|
|
391
575
|
const runner = new AIPromptRunner();
|
|
392
576
|
const result = await runner.ExecutePrompt(promptParams);
|
|
393
577
|
if (result && result.success && result.result) {
|
|
578
|
+
// Try to parse the result as JSON to extract name and description
|
|
394
579
|
const parsed = typeof result.result === 'string'
|
|
395
580
|
? JSON.parse(result.result)
|
|
396
581
|
: result.result;
|
|
397
582
|
const { name, description } = parsed;
|
|
398
583
|
if (name) {
|
|
399
|
-
|
|
584
|
+
LogStatus(`Generated conversation name via AI: "${name}" with description: "${description || ''}"`);
|
|
400
585
|
return {
|
|
401
586
|
name,
|
|
402
587
|
description: description || ''
|
|
@@ -406,46 +591,77 @@ class AgentRunner {
|
|
|
406
591
|
return null;
|
|
407
592
|
}
|
|
408
593
|
catch (error) {
|
|
409
|
-
|
|
594
|
+
LogError(`Error generating conversation name: ${error.message}`);
|
|
410
595
|
return null;
|
|
411
596
|
}
|
|
412
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Saves media outputs to AIAgentRunMedia table for permanent storage.
|
|
600
|
+
* This creates records for each media output promoted during agent execution.
|
|
601
|
+
*
|
|
602
|
+
* Only media with `persist !== false` is saved. Media items with `persist: false`
|
|
603
|
+
* are typically intercepted binary content that was never used in the final output.
|
|
604
|
+
*
|
|
605
|
+
* @param agentRunId - The ID of the agent run
|
|
606
|
+
* @param mediaOutputs - Array of media outputs to save
|
|
607
|
+
* @param contextUser - User context for the operation
|
|
608
|
+
* @returns Array of saved AIAgentRunMedia IDs
|
|
609
|
+
* @since 3.1.0
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* const runner = new AgentRunner();
|
|
614
|
+
* const mediaIds = await runner.SaveAgentRunMedia(
|
|
615
|
+
* agentResult.agentRun.ID,
|
|
616
|
+
* agentResult.mediaOutputs,
|
|
617
|
+
* currentUser
|
|
618
|
+
* );
|
|
619
|
+
* console.log(`Saved ${mediaIds.length} media outputs`);
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
413
622
|
async SaveAgentRunMedia(agentRunId, mediaOutputs, contextUser) {
|
|
414
623
|
if (!mediaOutputs || mediaOutputs.length === 0) {
|
|
415
624
|
return [];
|
|
416
625
|
}
|
|
626
|
+
// Filter to only persist media that should be saved
|
|
627
|
+
// persist=false means intercepted but unused binary content (e.g., images not used in response)
|
|
417
628
|
const mediaToSave = mediaOutputs.filter(m => m.persist !== false);
|
|
418
629
|
if (mediaToSave.length === 0) {
|
|
419
|
-
|
|
630
|
+
LogStatus(`All ${mediaOutputs.length} media outputs have persist=false, skipping save`);
|
|
420
631
|
return [];
|
|
421
632
|
}
|
|
422
633
|
if (mediaToSave.length < mediaOutputs.length) {
|
|
423
|
-
|
|
634
|
+
LogStatus(`Filtering: ${mediaToSave.length} of ${mediaOutputs.length} media outputs will be persisted`);
|
|
424
635
|
}
|
|
425
636
|
const savedIds = [];
|
|
426
|
-
const md = new
|
|
637
|
+
const md = new Metadata();
|
|
427
638
|
try {
|
|
428
|
-
|
|
639
|
+
// Use AIEngine's cached modalities instead of a fresh DB call
|
|
640
|
+
const aiEngine = AIEngine.Instance;
|
|
429
641
|
for (let i = 0; i < mediaToSave.length; i++) {
|
|
430
642
|
const mediaOutput = mediaToSave[i];
|
|
431
643
|
try {
|
|
432
644
|
const mediaEntity = await md.GetEntityObject('MJ: AI Agent Run Medias', contextUser);
|
|
433
645
|
mediaEntity.AgentRunID = agentRunId;
|
|
646
|
+
// Link to source prompt run media if this was promoted
|
|
434
647
|
if (mediaOutput.promptRunMediaId) {
|
|
435
648
|
mediaEntity.SourcePromptRunMediaID = mediaOutput.promptRunMediaId;
|
|
436
649
|
}
|
|
650
|
+
// Get modality ID from cached engine data
|
|
437
651
|
const modality = aiEngine.GetModalityByName(mediaOutput.modality);
|
|
438
652
|
if (modality) {
|
|
439
653
|
mediaEntity.ModalityID = modality.ID;
|
|
440
654
|
}
|
|
441
655
|
else {
|
|
442
|
-
|
|
656
|
+
LogError(`Unknown modality: ${mediaOutput.modality}`);
|
|
443
657
|
continue;
|
|
444
658
|
}
|
|
445
659
|
mediaEntity.MimeType = mediaOutput.mimeType;
|
|
660
|
+
// Only store inline data if NOT from prompt run media
|
|
446
661
|
if (!mediaOutput.promptRunMediaId && mediaOutput.data) {
|
|
447
662
|
mediaEntity.InlineData = mediaOutput.data;
|
|
448
663
|
}
|
|
664
|
+
// Set dimensions if available
|
|
449
665
|
if (mediaOutput.width) {
|
|
450
666
|
mediaEntity.Width = mediaOutput.width;
|
|
451
667
|
}
|
|
@@ -455,12 +671,14 @@ class AgentRunner {
|
|
|
455
671
|
if (mediaOutput.durationSeconds) {
|
|
456
672
|
mediaEntity.DurationSeconds = mediaOutput.durationSeconds;
|
|
457
673
|
}
|
|
674
|
+
// Set label and metadata
|
|
458
675
|
if (mediaOutput.label) {
|
|
459
676
|
mediaEntity.Label = mediaOutput.label;
|
|
460
677
|
}
|
|
461
678
|
if (mediaOutput.metadata) {
|
|
462
679
|
mediaEntity.Metadata = JSON.stringify(mediaOutput.metadata);
|
|
463
680
|
}
|
|
681
|
+
// Set description if available
|
|
464
682
|
if (mediaOutput.description) {
|
|
465
683
|
mediaEntity.Description = mediaOutput.description;
|
|
466
684
|
}
|
|
@@ -468,37 +686,60 @@ class AgentRunner {
|
|
|
468
686
|
const saved = await mediaEntity.Save();
|
|
469
687
|
if (saved) {
|
|
470
688
|
savedIds.push(mediaEntity.ID);
|
|
471
|
-
|
|
689
|
+
LogStatus(`Saved AIAgentRunMedia: ${mediaEntity.ID} (${mediaOutput.modality})`);
|
|
472
690
|
}
|
|
473
691
|
else {
|
|
474
|
-
|
|
692
|
+
LogError(`Failed to save AIAgentRunMedia: ${mediaEntity.LatestResult?.Message}`);
|
|
475
693
|
}
|
|
476
694
|
}
|
|
477
695
|
catch (mediaError) {
|
|
478
|
-
|
|
696
|
+
LogError(`Error saving media output ${i}: ${mediaError.message}`);
|
|
479
697
|
}
|
|
480
698
|
}
|
|
481
|
-
|
|
699
|
+
LogStatus(`Saved ${savedIds.length} of ${mediaToSave.length} media outputs for agent run ${agentRunId}`);
|
|
482
700
|
return savedIds;
|
|
483
701
|
}
|
|
484
702
|
catch (error) {
|
|
485
|
-
|
|
703
|
+
LogError(`Error in SaveAgentRunMedia: ${error.message}`);
|
|
486
704
|
return savedIds;
|
|
487
705
|
}
|
|
488
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Creates ConversationDetailAttachment records for media outputs.
|
|
709
|
+
* This enables media to be displayed in the conversation UI.
|
|
710
|
+
*
|
|
711
|
+
* @param conversationDetailId - The conversation detail to attach media to
|
|
712
|
+
* @param mediaOutputs - Array of media outputs
|
|
713
|
+
* @param agentRunMediaIds - Corresponding AIAgentRunMedia IDs
|
|
714
|
+
* @param contextUser - User context for the operation
|
|
715
|
+
* @returns Array of created attachment IDs
|
|
716
|
+
* @since 3.1.0
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```typescript
|
|
720
|
+
* const runner = new AgentRunner();
|
|
721
|
+
* const attachmentIds = await runner.CreateConversationMediaAttachments(
|
|
722
|
+
* conversationDetailId,
|
|
723
|
+
* mediaOutputs,
|
|
724
|
+
* agentRunMediaIds,
|
|
725
|
+
* currentUser
|
|
726
|
+
* );
|
|
727
|
+
* ```
|
|
728
|
+
*/
|
|
489
729
|
async CreateConversationMediaAttachments(conversationDetailId, mediaOutputs, agentRunMediaIds, contextUser) {
|
|
490
730
|
if (!mediaOutputs || mediaOutputs.length === 0) {
|
|
491
731
|
return [];
|
|
492
732
|
}
|
|
493
733
|
const attachmentIds = [];
|
|
494
|
-
const md = new
|
|
734
|
+
const md = new Metadata();
|
|
495
735
|
try {
|
|
496
|
-
|
|
736
|
+
// Use AIEngine's cached modalities instead of a fresh DB call
|
|
737
|
+
const aiEngine = AIEngine.Instance;
|
|
497
738
|
for (let i = 0; i < mediaOutputs.length; i++) {
|
|
498
739
|
const mediaOutput = mediaOutputs[i];
|
|
499
740
|
const agentRunMediaId = agentRunMediaIds[i];
|
|
500
741
|
if (!agentRunMediaId) {
|
|
501
|
-
|
|
742
|
+
LogError(`No AIAgentRunMedia ID for media output ${i}`);
|
|
502
743
|
continue;
|
|
503
744
|
}
|
|
504
745
|
try {
|
|
@@ -506,20 +747,26 @@ class AgentRunner {
|
|
|
506
747
|
attachment.ConversationDetailID = conversationDetailId;
|
|
507
748
|
attachment.MimeType = mediaOutput.mimeType;
|
|
508
749
|
attachment.DisplayOrder = i;
|
|
750
|
+
// Get modality ID from cached engine data
|
|
509
751
|
const modality = aiEngine.GetModalityByName(mediaOutput.modality);
|
|
510
752
|
if (modality) {
|
|
511
753
|
attachment.ModalityID = modality.ID;
|
|
512
754
|
}
|
|
513
755
|
else {
|
|
514
|
-
|
|
756
|
+
LogError(`Unknown modality: ${mediaOutput.modality}`);
|
|
515
757
|
continue;
|
|
516
758
|
}
|
|
759
|
+
// Generate a filename if none provided
|
|
517
760
|
const extension = this.getMimeTypeExtension(mediaOutput.mimeType);
|
|
518
761
|
attachment.FileName = mediaOutput.label || `${mediaOutput.modality.toLowerCase()}_${i + 1}.${extension}`;
|
|
762
|
+
// Store inline data for small media
|
|
763
|
+
// In the future, consider MJStorage for large files
|
|
519
764
|
if (mediaOutput.data) {
|
|
520
765
|
attachment.InlineData = mediaOutput.data;
|
|
766
|
+
// Estimate file size from base64 length (base64 is ~33% larger than binary)
|
|
521
767
|
attachment.FileSizeBytes = Math.ceil(mediaOutput.data.length * 0.75);
|
|
522
768
|
}
|
|
769
|
+
// Set dimensions if available
|
|
523
770
|
if (mediaOutput.width) {
|
|
524
771
|
attachment.Width = mediaOutput.width;
|
|
525
772
|
}
|
|
@@ -529,30 +776,35 @@ class AgentRunner {
|
|
|
529
776
|
if (mediaOutput.durationSeconds) {
|
|
530
777
|
attachment.DurationSeconds = Math.ceil(mediaOutput.durationSeconds);
|
|
531
778
|
}
|
|
779
|
+
// Set description if available
|
|
532
780
|
if (mediaOutput.description) {
|
|
533
781
|
attachment.Description = mediaOutput.description;
|
|
534
782
|
}
|
|
535
783
|
const saved = await attachment.Save();
|
|
536
784
|
if (saved) {
|
|
537
785
|
attachmentIds.push(attachment.ID);
|
|
538
|
-
|
|
786
|
+
LogStatus(`Created ConversationDetailAttachment: ${attachment.ID} (${mediaOutput.modality})`);
|
|
539
787
|
}
|
|
540
788
|
else {
|
|
541
|
-
|
|
789
|
+
LogError(`Failed to create attachment: ${attachment.LatestResult?.Message}`);
|
|
542
790
|
}
|
|
543
791
|
}
|
|
544
792
|
catch (attachError) {
|
|
545
|
-
|
|
793
|
+
LogError(`Error creating attachment ${i}: ${attachError.message}`);
|
|
546
794
|
}
|
|
547
795
|
}
|
|
548
|
-
|
|
796
|
+
LogStatus(`Created ${attachmentIds.length} conversation attachments for detail ${conversationDetailId}`);
|
|
549
797
|
return attachmentIds;
|
|
550
798
|
}
|
|
551
799
|
catch (error) {
|
|
552
|
-
|
|
800
|
+
LogError(`Error in CreateConversationMediaAttachments: ${error.message}`);
|
|
553
801
|
return attachmentIds;
|
|
554
802
|
}
|
|
555
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Gets file extension from MIME type
|
|
806
|
+
* @private
|
|
807
|
+
*/
|
|
556
808
|
getMimeTypeExtension(mimeType) {
|
|
557
809
|
const mimeToExt = {
|
|
558
810
|
'image/png': 'png',
|
|
@@ -571,5 +823,4 @@ class AgentRunner {
|
|
|
571
823
|
return mimeToExt[mimeType.toLowerCase()] || 'bin';
|
|
572
824
|
}
|
|
573
825
|
}
|
|
574
|
-
exports.AgentRunner = AgentRunner;
|
|
575
826
|
//# sourceMappingURL=AgentRunner.js.map
|