@memberjunction/ai-agents 5.23.0 → 5.25.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/README.md +32 -0
- package/dist/AgentRunner.d.ts +58 -3
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +348 -62
- package/dist/AgentRunner.js.map +1 -1
- package/dist/base-agent.d.ts +48 -3
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +260 -67
- package/dist/base-agent.js.map +1 -1
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -332,6 +332,38 @@ New code should import these directly from `@memberjunction/ai-reranker`.
|
|
|
332
332
|
- `@memberjunction/aiengine` -- AIEngine for metadata and vector search
|
|
333
333
|
- `@memberjunction/ai-core-plus` -- Shared types (ExecuteAgentParams, ExecuteAgentResult)
|
|
334
334
|
- `@memberjunction/ai-engine-base` -- Base metadata cache and permissions
|
|
335
|
+
## Storage Account Resolution
|
|
336
|
+
|
|
337
|
+
When agents create file-based artifacts (PDF, Excel, Word), the system resolves which `FileStorageAccount` to use via a hierarchical chain. The first non-null value wins:
|
|
338
|
+
|
|
339
|
+
| Priority | Source | Field |
|
|
340
|
+
|----------|--------|-------|
|
|
341
|
+
| 1 (highest) | Runtime | `ExecuteAgentParams.override.storageAccountId` |
|
|
342
|
+
| 2 | Agent | `AIAgent.DefaultStorageAccountID` |
|
|
343
|
+
| 3 | Category tree | `AIAgentCategory.DefaultStorageAccountID` (walks up `ParentID`) |
|
|
344
|
+
| 4 (lowest) | Agent Type | `AIAgentType.DefaultStorageAccountID` |
|
|
345
|
+
| Fallback | System | Single active account (if only one exists) |
|
|
346
|
+
|
|
347
|
+
### How it works
|
|
348
|
+
|
|
349
|
+
- `BaseAgent.getStorageAccountID(params)` implements the resolution logic. It is `protected` so subclasses can override it for custom routing.
|
|
350
|
+
- The resolved ID is stored in `ExecuteAgentResult.resolvedStorageAccountId` and passed to `AgentRunner.ProcessFileArtifacts()` for upload routing.
|
|
351
|
+
- `AgentRunner.uploadBase64ToStorage()` uses `FileStorageEngine.Instance.GetAccountWithProvider()` to get the account + provider, then `initializeDriverWithAccountCredentials()` for proper OAuth credential handling.
|
|
352
|
+
|
|
353
|
+
### Startup validation
|
|
354
|
+
|
|
355
|
+
`AIEngine.validateStorageAccountDefaults()` runs at server startup. If 2+ active storage accounts exist but agent types lack a `DefaultStorageAccountID`, it auto-assigns the highest-priority account and logs a prominent warning.
|
|
356
|
+
|
|
357
|
+
### Configuration
|
|
358
|
+
|
|
359
|
+
Set `DefaultStorageAccountID` at any level via the admin UI or metadata sync:
|
|
360
|
+
- **Agent Type** -- broadest default (e.g., all Loop agents → Dropbox)
|
|
361
|
+
- **Agent Category** -- business-domain default (e.g., Marketing → Box, Finance → SharePoint)
|
|
362
|
+
- **Agent** -- per-agent override
|
|
363
|
+
- **Runtime** -- `ExecuteAgentParams.override.storageAccountId` for programmatic callers
|
|
364
|
+
|
|
365
|
+
## Dependencies
|
|
366
|
+
|
|
335
367
|
- `@memberjunction/ai` -- Core AI abstractions
|
|
336
368
|
- `@memberjunction/ai-reranker` -- Two-stage retrieval reranking
|
|
337
369
|
- `@memberjunction/actions` -- Server-side action execution
|
package/dist/AgentRunner.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @since 2.49.0
|
|
11
11
|
*/
|
|
12
12
|
import { UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
13
|
-
import { ExecuteAgentResult, ExecuteAgentParams, MediaOutput } from '@memberjunction/ai-core-plus';
|
|
13
|
+
import { ExecuteAgentResult, ExecuteAgentParams, MediaOutput, FileOutputRef } from '@memberjunction/ai-core-plus';
|
|
14
14
|
/**
|
|
15
15
|
* AgentRunner provides a thin wrapper for executing AI agents.
|
|
16
16
|
*
|
|
@@ -123,7 +123,7 @@ export declare class AgentRunner {
|
|
|
123
123
|
* const newVersionNumber = maxVersion + 1;
|
|
124
124
|
* ```
|
|
125
125
|
*/
|
|
126
|
-
GetMaxVersionForArtifact(artifactId: string, contextUser: UserInfo): Promise<number>;
|
|
126
|
+
GetMaxVersionForArtifact(artifactId: string, contextUser: UserInfo, provider?: IMetadataProvider): Promise<number>;
|
|
127
127
|
/**
|
|
128
128
|
* Checks whether the serialized content for a new artifact version is identical to the
|
|
129
129
|
* latest existing version of the same artifact, using SHA-256 content hashing.
|
|
@@ -139,7 +139,7 @@ export declare class AgentRunner {
|
|
|
139
139
|
* @param contextUser - User context for the RunView query
|
|
140
140
|
* @returns The existing version's ID if content is identical, or `null` if a new version should be created
|
|
141
141
|
*/
|
|
142
|
-
protected CheckForDuplicateVersion(artifactId: string, candidateContent: string, latestVersionNumber: number, contextUser: UserInfo): Promise<string | null>;
|
|
142
|
+
protected CheckForDuplicateVersion(artifactId: string, candidateContent: string, latestVersionNumber: number, contextUser: UserInfo, provider?: IMetadataProvider): Promise<string | null>;
|
|
143
143
|
/**
|
|
144
144
|
* Creates a `ConversationDetailArtifact` junction record linking an artifact version
|
|
145
145
|
* to a conversation detail, then returns the standard artifact result tuple.
|
|
@@ -280,6 +280,61 @@ export declare class AgentRunner {
|
|
|
280
280
|
* ```
|
|
281
281
|
*/
|
|
282
282
|
CreateConversationMediaAttachments(conversationDetailId: string, mediaOutputs: MediaOutput[], agentRunMediaIds: string[], contextUser: UserInfo, provider?: IMetadataProvider): Promise<string[]>;
|
|
283
|
+
/**
|
|
284
|
+
* Creates MJ: Artifact records for file outputs collected during agent execution.
|
|
285
|
+
* Reads directly from `ExecuteAgentResult.fileOutputs` — no DB query needed.
|
|
286
|
+
*
|
|
287
|
+
* Called automatically by RunAgentInConversation after the agent completes.
|
|
288
|
+
*
|
|
289
|
+
* @param fileOutputs - File outputs collected by BaseAgent during action execution
|
|
290
|
+
* @param conversationDetailId - The conversation detail to link artifacts to
|
|
291
|
+
* @param contextUser - User context for DB operations
|
|
292
|
+
* @param resolvedStorageAccountId - Pre-resolved FileStorageAccount ID from the agent's
|
|
293
|
+
* hierarchical resolution chain (Runtime → Agent → Category → Type → fallback).
|
|
294
|
+
* When provided, uploads use this specific account instead of picking the first active one.
|
|
295
|
+
*/
|
|
296
|
+
ProcessFileArtifacts(fileOutputs: FileOutputRef[], conversationDetailId: string, contextUser: UserInfo, resolvedStorageAccountId?: string, provider?: IMetadataProvider): Promise<void>;
|
|
297
|
+
/**
|
|
298
|
+
* Re-processes file artifacts from a historical agent run by querying its persisted
|
|
299
|
+
* action step OutputData. Use this to recover artifacts for runs that completed before
|
|
300
|
+
* ProcessFileArtifacts was introduced, or for debugging.
|
|
301
|
+
*
|
|
302
|
+
* @param agentRunId - The agent run whose action steps to inspect
|
|
303
|
+
* @param conversationDetailId - The conversation detail to link artifacts to
|
|
304
|
+
* @param contextUser - User context for DB operations
|
|
305
|
+
*/
|
|
306
|
+
ReprocessRunFileArtifacts(agentRunId: string, conversationDetailId: string, contextUser: UserInfo, provider?: IMetadataProvider): Promise<void>;
|
|
307
|
+
/** Loads all completed action steps for a given agent run (read-only, narrow fields). */
|
|
308
|
+
private loadActionStepsForRun;
|
|
309
|
+
/**
|
|
310
|
+
* Parses persisted OutputData JSON from an action step and extracts file output metadata.
|
|
311
|
+
* Detection is shape-based (looks for objects with fileName + mimeType + fileData/fileId),
|
|
312
|
+
* not name-based — works regardless of what the action named its output parameter.
|
|
313
|
+
*
|
|
314
|
+
* Used only by the historical reprocessing path — live runs use BaseAgent's detectFileOutputs.
|
|
315
|
+
*/
|
|
316
|
+
private parseStepFileOutputs;
|
|
317
|
+
/** Uploads or resolves a single file output and creates the artifact records.
|
|
318
|
+
* Falls back to inline base64 artifact if storage is unavailable or upload fails. */
|
|
319
|
+
private processFileOutput;
|
|
320
|
+
/**
|
|
321
|
+
* Uploads base64-encoded file content to MJStorage and creates an MJ: Files record.
|
|
322
|
+
* Returns the new MJ: Files record ID.
|
|
323
|
+
*
|
|
324
|
+
* @param resolvedStorageAccountId - Pre-resolved account from the agent's storage resolution chain.
|
|
325
|
+
* When provided, uploads to this specific account. Otherwise falls back to the first active account.
|
|
326
|
+
*/
|
|
327
|
+
private uploadBase64ToStorage;
|
|
328
|
+
/**
|
|
329
|
+
* Shared logic for creating an artifact + version + conversation detail link in a single
|
|
330
|
+
* transaction. The caller provides a callback to set the version-specific fields (file-backed
|
|
331
|
+
* vs. inline content).
|
|
332
|
+
*/
|
|
333
|
+
private createArtifactWithVersion;
|
|
334
|
+
/** Creates a file-backed artifact (version references a FileID in MJStorage). */
|
|
335
|
+
private createFileArtifact;
|
|
336
|
+
/** Creates an inline artifact (version stores base64 data URL directly, no MJStorage). */
|
|
337
|
+
private createInlineFileArtifact;
|
|
283
338
|
/**
|
|
284
339
|
* Gets file extension from MIME type
|
|
285
340
|
* @private
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentRunner.d.ts","sourceRoot":"","sources":["../src/AgentRunner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAgF,QAAQ,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"AgentRunner.d.ts","sourceRoot":"","sources":["../src/AgentRunner.ts"],"names":[],"mappings":"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"}
|
package/dist/AgentRunner.js
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
* @since 2.49.0
|
|
11
11
|
*/
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
|
-
import { LogError, LogStatusEx, IsVerboseLoggingEnabled, LogStatus, Metadata, RunView } from '@memberjunction/core';
|
|
13
|
+
import { LogError, LogStatusEx, IsVerboseLoggingEnabled, LogStatus, Metadata, RunView, ProviderType } from '@memberjunction/core';
|
|
14
14
|
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
15
15
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
16
|
+
import { ParseFileOutputRef } from '@memberjunction/ai-core-plus';
|
|
16
17
|
import { BaseAgent } from './base-agent.js';
|
|
18
|
+
import { ArtifactMetadataEngine } from '@memberjunction/core-entities';
|
|
19
|
+
import { FileStorageEngine } from '@memberjunction/storage';
|
|
17
20
|
/**
|
|
18
21
|
* AgentRunner provides a thin wrapper for executing AI agents.
|
|
19
22
|
*
|
|
@@ -131,18 +134,39 @@ export class AgentRunner {
|
|
|
131
134
|
// If conversationDetailId is provided, use it (UI-created agent response detail)
|
|
132
135
|
if (options.conversationDetailId) {
|
|
133
136
|
agentResponseDetailId = options.conversationDetailId;
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
// LATENCY OPTIMIZATION (Opt #2): When conversationId is pre-resolved by the caller
|
|
138
|
+
// (e.g., the resolver already loaded the ConversationDetail to build history), we
|
|
139
|
+
// skip the redundant DB load that was ONLY needed to extract ConversationID.
|
|
140
|
+
//
|
|
141
|
+
// We still must load the ConversationDetail entity object because it serves double
|
|
142
|
+
// duty: (a) progress callback updates its Message field during execution, and
|
|
143
|
+
// (b) step 5 sets the final Status/Message/ResponseForm after the agent completes.
|
|
144
|
+
// What we save here is the serial dependency: previously we had to wait for the
|
|
145
|
+
// Load to complete before we even knew conversationId. Now we know conversationId
|
|
146
|
+
// immediately and can resolve the entity object's load in parallel with other setup.
|
|
147
|
+
if (options.conversationId) {
|
|
148
|
+
conversationId = options.conversationId;
|
|
149
|
+
LogStatus(`Using pre-resolved conversation ${conversationId} and agent response detail ${agentResponseDetailId}`);
|
|
142
150
|
}
|
|
143
151
|
else {
|
|
144
|
-
|
|
152
|
+
// Fallback for callers that don't provide conversationId (backward compatibility).
|
|
153
|
+
// This path still works but incurs the extra DB round-trip to extract conversationId.
|
|
154
|
+
const tempDetail = await md.GetEntityObject('MJ: Conversation Details', contextUser);
|
|
155
|
+
if (await tempDetail.Load(agentResponseDetailId)) {
|
|
156
|
+
conversationId = tempDetail.ConversationID;
|
|
157
|
+
LogStatus(`Using existing conversation ${conversationId} and agent response detail ${agentResponseDetailId}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw new Error(`Failed to load conversation detail ${agentResponseDetailId}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Load the entity object for progress updates and final status Save at step 5.
|
|
164
|
+
// This is needed regardless of whether conversationId was pre-resolved.
|
|
165
|
+
agentResponseDetail = await md.GetEntityObject('MJ: Conversation Details', contextUser);
|
|
166
|
+
if (!await agentResponseDetail.Load(agentResponseDetailId)) {
|
|
167
|
+
throw new Error(`Failed to load conversation detail entity for status updates: ${agentResponseDetailId}`);
|
|
145
168
|
}
|
|
169
|
+
userMessageDetailId = agentResponseDetailId; // For backward compatibility
|
|
146
170
|
}
|
|
147
171
|
else {
|
|
148
172
|
// Server creates BOTH user message and agent response details
|
|
@@ -260,55 +284,90 @@ export class AgentRunner {
|
|
|
260
284
|
const agentResult = await this.RunAgent(modifiedParams);
|
|
261
285
|
// Mark execution as completed to stop progress saves
|
|
262
286
|
agentExecutionCompleted = true;
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
// LATENCY OPTIMIZATION (Opt #5): Steps 5, 6, and 7 are now parallelized.
|
|
288
|
+
//
|
|
289
|
+
// Previously these ran sequentially: save final status → process artifacts → save media.
|
|
290
|
+
// This added their individual latencies together (~130ms for steps 6+7, plus ~80ms for
|
|
291
|
+
// step 5's EnsureSaveComplete + Load + Save cycle).
|
|
292
|
+
//
|
|
293
|
+
// These operations are safe to parallelize because:
|
|
294
|
+
// - Step 5 (status update) writes to the ConversationDetail record (Status, Message fields)
|
|
295
|
+
// - Step 6 (artifacts) creates new Artifact/ArtifactVersion records and a junction record
|
|
296
|
+
// linking to the ConversationDetail — it only needs the agentResponseDetailId (string),
|
|
297
|
+
// not the entity object, and doesn't read/write the same fields as step 5
|
|
298
|
+
// - Step 7 (media) creates new AIAgentRunMedia and ConversationDetailAttachment records —
|
|
299
|
+
// entirely separate from steps 5 and 6
|
|
300
|
+
//
|
|
301
|
+
// IMPORTANT: All three MUST complete before the resolver publishes the 'complete' event,
|
|
302
|
+
// because the client reloads all conversation data from the DB when it receives that event.
|
|
303
|
+
// If any write hasn't flushed yet, the client would see stale data. Promise.all guarantees
|
|
304
|
+
// all three finish before we return.
|
|
305
|
+
// Step 5: Update agent response detail with final result (async)
|
|
306
|
+
const updateDetailPromise = (async () => {
|
|
307
|
+
if (agentResponseDetail && agentResponseDetailId) {
|
|
308
|
+
// Wait for any in-flight progress save to complete
|
|
309
|
+
// EnsureSaveComplete() resolves immediately if no save in progress
|
|
310
|
+
await agentResponseDetail.EnsureSaveComplete();
|
|
311
|
+
LogStatus('Updating agent response detail with final result');
|
|
312
|
+
// Reload to get any updates from agent execution
|
|
313
|
+
await agentResponseDetail.Load(agentResponseDetailId);
|
|
314
|
+
agentResponseDetail.Message = agentResult.agentRun?.Message ||
|
|
315
|
+
(agentResult.success
|
|
316
|
+
? '✅ Completed'
|
|
317
|
+
: agentResult.agentRun?.ErrorMessage || '❌ Failed');
|
|
318
|
+
agentResponseDetail.Status = agentResult.success ? 'Complete' : 'Error';
|
|
319
|
+
// Set response form and command fields
|
|
320
|
+
if (agentResult.responseForm) {
|
|
321
|
+
agentResponseDetail.ResponseForm = JSON.stringify(agentResult.responseForm);
|
|
322
|
+
}
|
|
323
|
+
if (agentResult.actionableCommands && agentResult.actionableCommands.length > 0) {
|
|
324
|
+
agentResponseDetail.ActionableCommands = JSON.stringify(agentResult.actionableCommands);
|
|
325
|
+
}
|
|
326
|
+
if (agentResult.automaticCommands && agentResult.automaticCommands.length > 0) {
|
|
327
|
+
agentResponseDetail.AutomaticCommands = JSON.stringify(agentResult.automaticCommands);
|
|
328
|
+
}
|
|
329
|
+
await agentResponseDetail.Save();
|
|
330
|
+
LogStatus(`Updated agent response detail ${agentResponseDetailId} with final status: ${agentResponseDetail.Status}`);
|
|
280
331
|
}
|
|
281
|
-
|
|
282
|
-
|
|
332
|
+
})();
|
|
333
|
+
// Step 6: Process artifacts if requested and agent succeeded (async)
|
|
334
|
+
const processArtifactsPromise = (async () => {
|
|
335
|
+
const shouldCreateArtifacts = options.createArtifacts !== false; // Default true
|
|
336
|
+
if (shouldCreateArtifacts && agentResult.success && agentResult.payload) {
|
|
337
|
+
return this.ProcessAgentArtifacts(agentResult, agentResponseDetailId, options.sourceArtifactId, contextUser, md);
|
|
283
338
|
}
|
|
284
|
-
|
|
285
|
-
|
|
339
|
+
return undefined;
|
|
340
|
+
})();
|
|
341
|
+
// Step 6b: Process file artifacts produced by file-generation actions (async)
|
|
342
|
+
const processFileArtifactsPromise = (async () => {
|
|
343
|
+
if (agentResult.success && agentResponseDetailId && agentResult.fileOutputs?.length) {
|
|
344
|
+
await this.ProcessFileArtifacts(agentResult.fileOutputs, agentResponseDetailId, contextUser, agentResult.resolvedStorageAccountId, md);
|
|
286
345
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// Filter to only media that should be persisted (persist !== false)
|
|
301
|
-
const mediaToSave = agentResult.mediaOutputs.filter(m => m.persist !== false);
|
|
302
|
-
LogStatus(`Processing ${mediaToSave.length} of ${agentResult.mediaOutputs.length} media outputs (filtered by persist flag)`);
|
|
303
|
-
// Save to AIAgentRunMedia for permanent storage
|
|
304
|
-
mediaIds = await this.SaveAgentRunMedia(agentResult.agentRun.ID, mediaToSave, // Pass filtered array
|
|
305
|
-
contextUser, md);
|
|
306
|
-
// Create ConversationDetailAttachment records for UI display
|
|
307
|
-
if (agentResponseDetailId && mediaIds.length > 0) {
|
|
308
|
-
await this.CreateConversationMediaAttachments(agentResponseDetailId, mediaToSave, // Pass same filtered array to keep indices aligned
|
|
309
|
-
mediaIds, contextUser, md);
|
|
346
|
+
})();
|
|
347
|
+
// Step 7: Save media outputs to AIAgentRunMedia and create conversation attachments (async)
|
|
348
|
+
const saveMediaPromise = (async () => {
|
|
349
|
+
if (agentResult.mediaOutputs && agentResult.mediaOutputs.length > 0) {
|
|
350
|
+
const mediaToSave = agentResult.mediaOutputs.filter(m => m.persist !== false);
|
|
351
|
+
LogStatus(`Processing ${mediaToSave.length} of ${agentResult.mediaOutputs.length} media outputs (filtered by persist flag)`);
|
|
352
|
+
// Save to AIAgentRunMedia for permanent storage
|
|
353
|
+
const ids = await this.SaveAgentRunMedia(agentResult.agentRun.ID, mediaToSave, contextUser, md);
|
|
354
|
+
// Create ConversationDetailAttachment records for UI display
|
|
355
|
+
if (agentResponseDetailId && ids.length > 0) {
|
|
356
|
+
await this.CreateConversationMediaAttachments(agentResponseDetailId, mediaToSave, ids, contextUser, md);
|
|
357
|
+
}
|
|
358
|
+
return ids;
|
|
310
359
|
}
|
|
311
|
-
|
|
360
|
+
return [];
|
|
361
|
+
})();
|
|
362
|
+
// Wait for all three post-execution operations to complete before returning.
|
|
363
|
+
// The resolver publishes the 'complete' event after this returns, so the client
|
|
364
|
+
// is guaranteed to see all DB writes when it reloads.
|
|
365
|
+
const [, artifactInfo] = await Promise.all([
|
|
366
|
+
updateDetailPromise,
|
|
367
|
+
processArtifactsPromise,
|
|
368
|
+
processFileArtifactsPromise,
|
|
369
|
+
saveMediaPromise
|
|
370
|
+
]);
|
|
312
371
|
return {
|
|
313
372
|
agentResult,
|
|
314
373
|
conversationId,
|
|
@@ -338,9 +397,9 @@ export class AgentRunner {
|
|
|
338
397
|
* const newVersionNumber = maxVersion + 1;
|
|
339
398
|
* ```
|
|
340
399
|
*/
|
|
341
|
-
async GetMaxVersionForArtifact(artifactId, contextUser) {
|
|
400
|
+
async GetMaxVersionForArtifact(artifactId, contextUser, provider) {
|
|
342
401
|
try {
|
|
343
|
-
const rv =
|
|
402
|
+
const rv = RunView.FromMetadataProvider(provider || this._provider);
|
|
344
403
|
const result = await rv.RunView({
|
|
345
404
|
EntityName: 'MJ: Artifact Versions',
|
|
346
405
|
ExtraFilter: `ArtifactID='${artifactId}'`,
|
|
@@ -373,9 +432,9 @@ export class AgentRunner {
|
|
|
373
432
|
* @param contextUser - User context for the RunView query
|
|
374
433
|
* @returns The existing version's ID if content is identical, or `null` if a new version should be created
|
|
375
434
|
*/
|
|
376
|
-
async CheckForDuplicateVersion(artifactId, candidateContent, latestVersionNumber, contextUser) {
|
|
435
|
+
async CheckForDuplicateVersion(artifactId, candidateContent, latestVersionNumber, contextUser, provider) {
|
|
377
436
|
const candidateHash = createHash('sha256').update(candidateContent, 'utf8').digest('hex');
|
|
378
|
-
const rv =
|
|
437
|
+
const rv = RunView.FromMetadataProvider(provider || this._provider);
|
|
379
438
|
const result = await rv.RunView({
|
|
380
439
|
EntityName: 'MJ: Artifact Versions',
|
|
381
440
|
ExtraFilter: `ArtifactID='${artifactId}' AND VersionNumber=${latestVersionNumber}`,
|
|
@@ -433,7 +492,7 @@ export class AgentRunner {
|
|
|
433
492
|
*/
|
|
434
493
|
async FindPreviousArtifactForMessage(conversationDetailId, contextUser, provider) {
|
|
435
494
|
try {
|
|
436
|
-
const rv =
|
|
495
|
+
const rv = RunView.FromMetadataProvider(provider || this._provider);
|
|
437
496
|
const result = await rv.RunView({
|
|
438
497
|
EntityName: 'MJ: Conversation Detail Artifacts',
|
|
439
498
|
ExtraFilter: `ConversationDetailID='${conversationDetailId}' AND Direction='Output'`,
|
|
@@ -519,7 +578,7 @@ export class AgentRunner {
|
|
|
519
578
|
let isNewArtifact = false;
|
|
520
579
|
// Priority 1: Use explicit source artifact if provided
|
|
521
580
|
if (sourceArtifactId) {
|
|
522
|
-
const maxVersion = await this.GetMaxVersionForArtifact(sourceArtifactId, contextUser);
|
|
581
|
+
const maxVersion = await this.GetMaxVersionForArtifact(sourceArtifactId, contextUser, provider);
|
|
523
582
|
artifactId = sourceArtifactId;
|
|
524
583
|
newVersionNumber = maxVersion + 1;
|
|
525
584
|
LogStatus(`Creating version ${newVersionNumber} of source artifact ${artifactId}`);
|
|
@@ -565,7 +624,7 @@ export class AgentRunner {
|
|
|
565
624
|
const serializedContent = JSON.stringify(payload, null, 2);
|
|
566
625
|
// Skip version creation if content is identical to the latest version
|
|
567
626
|
if (!isNewArtifact && newVersionNumber > 1) {
|
|
568
|
-
const existingVersionId = await this.CheckForDuplicateVersion(artifactId, serializedContent, newVersionNumber - 1, contextUser);
|
|
627
|
+
const existingVersionId = await this.CheckForDuplicateVersion(artifactId, serializedContent, newVersionNumber - 1, contextUser, provider);
|
|
569
628
|
if (existingVersionId) {
|
|
570
629
|
console.debug(`Skipping duplicate artifact version — content identical to version ${newVersionNumber - 1}`);
|
|
571
630
|
return undefined;
|
|
@@ -863,6 +922,233 @@ export class AgentRunner {
|
|
|
863
922
|
return attachmentIds;
|
|
864
923
|
}
|
|
865
924
|
}
|
|
925
|
+
// ── File artifact processing ───────────────────────────────────────────────
|
|
926
|
+
/**
|
|
927
|
+
* Creates MJ: Artifact records for file outputs collected during agent execution.
|
|
928
|
+
* Reads directly from `ExecuteAgentResult.fileOutputs` — no DB query needed.
|
|
929
|
+
*
|
|
930
|
+
* Called automatically by RunAgentInConversation after the agent completes.
|
|
931
|
+
*
|
|
932
|
+
* @param fileOutputs - File outputs collected by BaseAgent during action execution
|
|
933
|
+
* @param conversationDetailId - The conversation detail to link artifacts to
|
|
934
|
+
* @param contextUser - User context for DB operations
|
|
935
|
+
* @param resolvedStorageAccountId - Pre-resolved FileStorageAccount ID from the agent's
|
|
936
|
+
* hierarchical resolution chain (Runtime → Agent → Category → Type → fallback).
|
|
937
|
+
* When provided, uploads use this specific account instead of picking the first active one.
|
|
938
|
+
*/
|
|
939
|
+
async ProcessFileArtifacts(fileOutputs, conversationDetailId, contextUser, resolvedStorageAccountId, provider) {
|
|
940
|
+
if (fileOutputs.length === 0)
|
|
941
|
+
return;
|
|
942
|
+
const md = provider || this._provider;
|
|
943
|
+
// Hoist engine configs before parallel processing
|
|
944
|
+
await Promise.all([
|
|
945
|
+
ArtifactMetadataEngine.Instance.Config(false, contextUser),
|
|
946
|
+
FileStorageEngine.Instance.Config(false, contextUser),
|
|
947
|
+
]);
|
|
948
|
+
await Promise.all(fileOutputs.map(fo => this.processFileOutput(fo, conversationDetailId, contextUser, resolvedStorageAccountId, md)));
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Re-processes file artifacts from a historical agent run by querying its persisted
|
|
952
|
+
* action step OutputData. Use this to recover artifacts for runs that completed before
|
|
953
|
+
* ProcessFileArtifacts was introduced, or for debugging.
|
|
954
|
+
*
|
|
955
|
+
* @param agentRunId - The agent run whose action steps to inspect
|
|
956
|
+
* @param conversationDetailId - The conversation detail to link artifacts to
|
|
957
|
+
* @param contextUser - User context for DB operations
|
|
958
|
+
*/
|
|
959
|
+
async ReprocessRunFileArtifacts(agentRunId, conversationDetailId, contextUser, provider) {
|
|
960
|
+
const md = provider || this._provider;
|
|
961
|
+
const steps = await this.loadActionStepsForRun(agentRunId, contextUser, md);
|
|
962
|
+
if (steps.length === 0)
|
|
963
|
+
return;
|
|
964
|
+
const fileOutputs = steps.flatMap(step => this.parseStepFileOutputs(step));
|
|
965
|
+
await this.ProcessFileArtifacts(fileOutputs, conversationDetailId, contextUser, undefined, md);
|
|
966
|
+
}
|
|
967
|
+
/** Loads all completed action steps for a given agent run (read-only, narrow fields). */
|
|
968
|
+
async loadActionStepsForRun(agentRunId, contextUser, provider) {
|
|
969
|
+
const rv = RunView.FromMetadataProvider(provider || this._provider);
|
|
970
|
+
const result = await rv.RunView({
|
|
971
|
+
EntityName: 'MJ: AI Agent Run Steps',
|
|
972
|
+
ExtraFilter: `AgentRunID='${agentRunId}' AND StepType='Actions' AND Status='Completed'`,
|
|
973
|
+
Fields: ['ID', 'OutputData'],
|
|
974
|
+
ResultType: 'simple'
|
|
975
|
+
}, contextUser);
|
|
976
|
+
return result.Success ? (result.Results ?? []) : [];
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Parses persisted OutputData JSON from an action step and extracts file output metadata.
|
|
980
|
+
* Detection is shape-based (looks for objects with fileName + mimeType + fileData/fileId),
|
|
981
|
+
* not name-based — works regardless of what the action named its output parameter.
|
|
982
|
+
*
|
|
983
|
+
* Used only by the historical reprocessing path — live runs use BaseAgent's detectFileOutputs.
|
|
984
|
+
*/
|
|
985
|
+
parseStepFileOutputs(step) {
|
|
986
|
+
if (!step.OutputData)
|
|
987
|
+
return [];
|
|
988
|
+
let outputData;
|
|
989
|
+
try {
|
|
990
|
+
outputData = JSON.parse(step.OutputData);
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return [];
|
|
994
|
+
}
|
|
995
|
+
const params = outputData.actionResult?.parameters;
|
|
996
|
+
if (!params)
|
|
997
|
+
return [];
|
|
998
|
+
const results = [];
|
|
999
|
+
for (const param of params) {
|
|
1000
|
+
if (param.Value == null)
|
|
1001
|
+
continue;
|
|
1002
|
+
const ref = ParseFileOutputRef(param.Value);
|
|
1003
|
+
if (ref)
|
|
1004
|
+
results.push(ref);
|
|
1005
|
+
}
|
|
1006
|
+
return results;
|
|
1007
|
+
}
|
|
1008
|
+
/** Uploads or resolves a single file output and creates the artifact records.
|
|
1009
|
+
* Falls back to inline base64 artifact if storage is unavailable or upload fails. */
|
|
1010
|
+
async processFileOutput(fo, conversationDetailId, contextUser, resolvedStorageAccountId, provider) {
|
|
1011
|
+
try {
|
|
1012
|
+
if (fo.fileId) {
|
|
1013
|
+
// File already in storage — create file-backed artifact
|
|
1014
|
+
await this.createFileArtifact(fo.fileId, fo.mimeType, fo.fileName, fo.sizeBytes, conversationDetailId, contextUser, provider);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
// Check if any storage accounts are configured
|
|
1018
|
+
const hasStorage = FileStorageEngine.Instance.HasStorageAccounts;
|
|
1019
|
+
if (!hasStorage) {
|
|
1020
|
+
// No storage configured — go straight to inline artifact
|
|
1021
|
+
LogStatus(`ProcessFileArtifacts: no storage accounts configured for "${fo.fileName}", creating inline artifact`);
|
|
1022
|
+
await this.createInlineFileArtifact(fo.fileData, fo.mimeType, fo.fileName, fo.sizeBytes, conversationDetailId, contextUser, provider);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
// Try to upload to storage
|
|
1026
|
+
try {
|
|
1027
|
+
const fileId = await this.uploadBase64ToStorage(fo.fileData, fo.fileName, fo.mimeType, contextUser, resolvedStorageAccountId, provider);
|
|
1028
|
+
await this.createFileArtifact(fileId, fo.mimeType, fo.fileName, fo.sizeBytes, conversationDetailId, contextUser, provider);
|
|
1029
|
+
}
|
|
1030
|
+
catch (storageError) {
|
|
1031
|
+
// Upload failed — fall back to inline artifact
|
|
1032
|
+
LogStatus(`ProcessFileArtifacts: storage upload failed for "${fo.fileName}", creating inline artifact: ${storageError.message}`);
|
|
1033
|
+
await this.createInlineFileArtifact(fo.fileData, fo.mimeType, fo.fileName, fo.sizeBytes, conversationDetailId, contextUser, provider);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
LogError(`ProcessFileArtifacts: failed for "${fo.fileName}": ${error.message}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Uploads base64-encoded file content to MJStorage and creates an MJ: Files record.
|
|
1042
|
+
* Returns the new MJ: Files record ID.
|
|
1043
|
+
*
|
|
1044
|
+
* @param resolvedStorageAccountId - Pre-resolved account from the agent's storage resolution chain.
|
|
1045
|
+
* When provided, uploads to this specific account. Otherwise falls back to the first active account.
|
|
1046
|
+
*/
|
|
1047
|
+
async uploadBase64ToStorage(base64Data, fileName, mimeType, contextUser, resolvedStorageAccountId, provider) {
|
|
1048
|
+
const result = await FileStorageEngine.Instance.UploadFile({
|
|
1049
|
+
content: Buffer.from(base64Data, 'base64'),
|
|
1050
|
+
fileName,
|
|
1051
|
+
mimeType,
|
|
1052
|
+
contextUser,
|
|
1053
|
+
storageAccountId: resolvedStorageAccountId,
|
|
1054
|
+
provider
|
|
1055
|
+
});
|
|
1056
|
+
return result.FileID;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Shared logic for creating an artifact + version + conversation detail link in a single
|
|
1060
|
+
* transaction. The caller provides a callback to set the version-specific fields (file-backed
|
|
1061
|
+
* vs. inline content).
|
|
1062
|
+
*/
|
|
1063
|
+
async createArtifactWithVersion(params) {
|
|
1064
|
+
const { mimeType, fileName, sizeBytes, conversationDetailId, contextUser, provider, setVersionFields, label } = params;
|
|
1065
|
+
// Resolve the artifact type by MIME type; fall back to the built-in JSON type
|
|
1066
|
+
const JSON_ARTIFACT_TYPE_ID = 'ae674c7e-ea0d-49ea-89e4-0649f5eb20d4';
|
|
1067
|
+
const artifactType = ArtifactMetadataEngine.Instance.GetArtifactTypeByMimeType(mimeType);
|
|
1068
|
+
if (!artifactType) {
|
|
1069
|
+
LogStatus(`ProcessFileArtifacts: no ArtifactType found for MIME ${mimeType}, using JSON fallback`);
|
|
1070
|
+
}
|
|
1071
|
+
const artifactTypeId = artifactType?.ID ?? JSON_ARTIFACT_TYPE_ID;
|
|
1072
|
+
// Use direct provider transaction (BeginTransaction/CommitTransaction) instead of
|
|
1073
|
+
// TransactionGroup. This ensures saves execute immediately within the SQL transaction,
|
|
1074
|
+
// so server-side entity hooks (e.g. ArtifactVersion.ExtractAndSaveAttributes) can
|
|
1075
|
+
// Load() parent records that were saved earlier in the same transaction.
|
|
1076
|
+
const dbProvider = provider;
|
|
1077
|
+
const useTransaction = dbProvider.ProviderType === ProviderType.Database;
|
|
1078
|
+
if (useTransaction) {
|
|
1079
|
+
await dbProvider.BeginTransaction();
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
// Create the artifact header
|
|
1083
|
+
const artifact = await provider.GetEntityObject('MJ: Artifacts', contextUser);
|
|
1084
|
+
artifact.Name = fileName;
|
|
1085
|
+
artifact.TypeID = artifactTypeId;
|
|
1086
|
+
artifact.UserID = contextUser.ID;
|
|
1087
|
+
artifact.Visibility = 'Always';
|
|
1088
|
+
if (!(await artifact.Save())) {
|
|
1089
|
+
throw new Error(`Failed to save artifact for ${label}: ${fileName}`);
|
|
1090
|
+
}
|
|
1091
|
+
// Create the artifact version — shared fields first, then caller-specific fields
|
|
1092
|
+
const version = await provider.GetEntityObject('MJ: Artifact Versions', contextUser);
|
|
1093
|
+
version.ArtifactID = artifact.ID;
|
|
1094
|
+
version.VersionNumber = 1;
|
|
1095
|
+
version.MimeType = mimeType;
|
|
1096
|
+
version.FileName = fileName;
|
|
1097
|
+
version.UserID = contextUser.ID;
|
|
1098
|
+
if (sizeBytes !== undefined) {
|
|
1099
|
+
version.ContentSizeBytes = sizeBytes;
|
|
1100
|
+
}
|
|
1101
|
+
setVersionFields(version);
|
|
1102
|
+
if (!(await version.Save())) {
|
|
1103
|
+
throw new Error(`Failed to save artifact version for ${label}: ${fileName}`);
|
|
1104
|
+
}
|
|
1105
|
+
// Link the artifact version to the conversation detail
|
|
1106
|
+
const junction = await provider.GetEntityObject('MJ: Conversation Detail Artifacts', contextUser);
|
|
1107
|
+
junction.ConversationDetailID = conversationDetailId;
|
|
1108
|
+
junction.ArtifactVersionID = version.ID;
|
|
1109
|
+
junction.Direction = 'Output';
|
|
1110
|
+
if (!(await junction.Save())) {
|
|
1111
|
+
throw new Error(`Failed to link ${label} artifact to conversation detail: ${conversationDetailId}`);
|
|
1112
|
+
}
|
|
1113
|
+
if (useTransaction) {
|
|
1114
|
+
await dbProvider.CommitTransaction();
|
|
1115
|
+
}
|
|
1116
|
+
LogStatus(`Created ${label} artifact: ${fileName} (${mimeType}) → artifact ${artifact.ID}, version ${version.ID}`);
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
if (useTransaction) {
|
|
1120
|
+
try {
|
|
1121
|
+
await dbProvider.RollbackTransaction();
|
|
1122
|
+
}
|
|
1123
|
+
catch (rollbackError) {
|
|
1124
|
+
LogError(`Failed to rollback ${label} artifact transaction: ${rollbackError}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/** Creates a file-backed artifact (version references a FileID in MJStorage). */
|
|
1131
|
+
async createFileArtifact(fileId, mimeType, fileName, sizeBytes, conversationDetailId, contextUser, provider) {
|
|
1132
|
+
await this.createArtifactWithVersion({
|
|
1133
|
+
mimeType, fileName, sizeBytes, conversationDetailId, contextUser, provider,
|
|
1134
|
+
label: 'file',
|
|
1135
|
+
setVersionFields: (version) => {
|
|
1136
|
+
version.ContentMode = 'File';
|
|
1137
|
+
version.FileID = fileId;
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/** Creates an inline artifact (version stores base64 data URL directly, no MJStorage). */
|
|
1142
|
+
async createInlineFileArtifact(base64Data, mimeType, fileName, sizeBytes, conversationDetailId, contextUser, provider) {
|
|
1143
|
+
await this.createArtifactWithVersion({
|
|
1144
|
+
mimeType, fileName, sizeBytes, conversationDetailId, contextUser, provider,
|
|
1145
|
+
label: 'inline file',
|
|
1146
|
+
setVersionFields: (version) => {
|
|
1147
|
+
version.ContentMode = 'Text';
|
|
1148
|
+
version.Content = `data:${mimeType};base64,${base64Data}`;
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
866
1152
|
/**
|
|
867
1153
|
* Gets file extension from MIME type
|
|
868
1154
|
* @private
|