@memberjunction/ai-agents 5.11.0 → 5.13.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 +12 -0
- package/dist/AgentDataPreloader.js +1 -1
- package/dist/AgentDataPreloader.js.map +1 -1
- package/dist/MJAIAgentRequestEntityServer.d.ts +45 -0
- package/dist/MJAIAgentRequestEntityServer.d.ts.map +1 -0
- package/dist/MJAIAgentRequestEntityServer.js +210 -0
- package/dist/MJAIAgentRequestEntityServer.js.map +1 -0
- package/dist/base-agent.d.ts +67 -0
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +269 -2
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/base-agent.js
CHANGED
|
@@ -19,7 +19,7 @@ import { ActionEngineServer } from '@memberjunction/actions';
|
|
|
19
19
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
20
20
|
import { AgentContextInjector } from './agent-context-injector.js';
|
|
21
21
|
import { RerankerService } from '@memberjunction/ai-reranker';
|
|
22
|
-
import { AIPromptParams, ChildPromptParam, ConversationUtility } from '@memberjunction/ai-core-plus';
|
|
22
|
+
import { AIPromptParams, ChildPromptParam, ConversationUtility, parseAssignmentStrategy } from '@memberjunction/ai-core-plus';
|
|
23
23
|
import { AgentRunner } from './AgentRunner.js';
|
|
24
24
|
import { PayloadManager } from './PayloadManager.js';
|
|
25
25
|
import { AgentDataPreloader } from './AgentDataPreloader.js';
|
|
@@ -104,6 +104,13 @@ export class BaseAgent {
|
|
|
104
104
|
* @private
|
|
105
105
|
*/
|
|
106
106
|
this._agentRun = null;
|
|
107
|
+
/**
|
|
108
|
+
* Stores the ID of an AIAgentRequest created when a Chat step fires.
|
|
109
|
+
* Populated by executeChatStep(), returned in ExecuteAgentResult.feedbackRequestId.
|
|
110
|
+
* Only set for root agents (depth 0), not sub-agents.
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
this._feedbackRequestId = null;
|
|
107
114
|
/**
|
|
108
115
|
* Agent hierarchy for display purposes (e.g., ["Marketing Agent", "Copywriter Agent"]).
|
|
109
116
|
* Tracked separately as it's display-only and doesn't need persistence.
|
|
@@ -192,6 +199,22 @@ export class BaseAgent {
|
|
|
192
199
|
* Storage for injected notes and examples to include in result
|
|
193
200
|
*/
|
|
194
201
|
this._injectedMemory = { notes: [], examples: [] };
|
|
202
|
+
/**
|
|
203
|
+
* Walks up the agent's category hierarchy looking for an AssignmentStrategy.
|
|
204
|
+
* Loads categories via RunView and caches them for the duration of this run.
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
this._categoryCache = null;
|
|
208
|
+
/**
|
|
209
|
+
* Determines the request type ID based on the Chat step's context.
|
|
210
|
+
* - If the responseForm has only approve/reject-style buttons, uses "Approval"
|
|
211
|
+
* - If there's a responseForm with fields, uses "Information"
|
|
212
|
+
* - Otherwise defaults to "Information"
|
|
213
|
+
*
|
|
214
|
+
* Caches the request type lookup for the duration of this agent execution.
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
this._requestTypeCache = null;
|
|
195
218
|
}
|
|
196
219
|
/**
|
|
197
220
|
* Maximum allowed validation retries before forcing failure.
|
|
@@ -4402,6 +4425,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4402
4425
|
await this.finalizeStepEntity(stepEntity, promptResult.success, promptResult.success ? undefined : promptResult.errorMessage, outputData);
|
|
4403
4426
|
// Return based on next step
|
|
4404
4427
|
if (updatedNextStep.step === 'Chat') {
|
|
4428
|
+
// For root agents, create a persistent AIAgentRequest so the request is
|
|
4429
|
+
// tracked in the dashboard and can be responded to outside a conversation.
|
|
4430
|
+
// This is done here because Chat decisions from executePromptStep terminate
|
|
4431
|
+
// immediately and never reach executeChatStep in the main loop.
|
|
4432
|
+
if (this._depth === 0) {
|
|
4433
|
+
await this.createFeedbackRequest(params, stepEntity, updatedNextStep);
|
|
4434
|
+
}
|
|
4405
4435
|
return { ...updatedNextStep, terminate: true };
|
|
4406
4436
|
}
|
|
4407
4437
|
else if (updatedNextStep.step === 'Success' || updatedNextStep.step === 'Failed') {
|
|
@@ -5505,6 +5535,20 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5505
5535
|
content: resultsMessage,
|
|
5506
5536
|
metadata: metadata
|
|
5507
5537
|
});
|
|
5538
|
+
// Surface substantive action result messages as a separate instruction message.
|
|
5539
|
+
// When action messages contain multi-line directives (e.g., "YOUR NEXT ACTION — pick one"),
|
|
5540
|
+
// they get buried inside the JSON results structure above. LLMs treat JSON-wrapped text as
|
|
5541
|
+
// data to acknowledge, not instructions to follow. By re-surfacing these messages as a
|
|
5542
|
+
// standalone user message, the LLM reads them as explicit instructions.
|
|
5543
|
+
const substantiveMessages = actionSummaries
|
|
5544
|
+
.filter(a => a.success && a.message && a.message.includes('\n'))
|
|
5545
|
+
.map(a => a.message);
|
|
5546
|
+
if (substantiveMessages.length > 0) {
|
|
5547
|
+
params.conversationMessages.push({
|
|
5548
|
+
role: 'user',
|
|
5549
|
+
content: `IMPORTANT — Follow these instructions from the action results:\n\n${substantiveMessages.join('\n\n')}`
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5508
5552
|
}
|
|
5509
5553
|
// Call agent type's post-processing for actions
|
|
5510
5554
|
let finalPayload = currentPayload;
|
|
@@ -5567,12 +5611,22 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5567
5611
|
* Executes a chat step - these should bubble up to the user for interaction.
|
|
5568
5612
|
* Chat steps are terminal and indicate the agent needs user input.
|
|
5569
5613
|
*
|
|
5614
|
+
* For root agents (depth 0), this also creates a persistent AIAgentRequest row
|
|
5615
|
+
* so the request is tracked even when the agent isn't running in a conversation.
|
|
5616
|
+
* The feedbackRequestId is returned in ExecuteAgentResult for callers to use
|
|
5617
|
+
* (e.g., sending notifications, syncing conversation responses).
|
|
5618
|
+
*
|
|
5570
5619
|
* @private
|
|
5571
5620
|
*/
|
|
5572
5621
|
async executeChatStep(params, previousDecision) {
|
|
5573
5622
|
const stepEntity = await this.createStepEntity({ stepType: 'Chat', stepName: 'User Interaction', contextUser: params.contextUser });
|
|
5574
5623
|
// Chat steps are successful - they indicate a need for user interaction
|
|
5575
5624
|
await this.finalizeStepEntity(stepEntity, true);
|
|
5625
|
+
// For root agents, create a persistent AIAgentRequest so the request is
|
|
5626
|
+
// tracked in the dashboard and can be responded to outside a conversation.
|
|
5627
|
+
if (this._depth === 0) {
|
|
5628
|
+
await this.createFeedbackRequest(params, stepEntity, previousDecision);
|
|
5629
|
+
}
|
|
5576
5630
|
return {
|
|
5577
5631
|
step: 'Chat',
|
|
5578
5632
|
terminate: true,
|
|
@@ -5587,6 +5641,214 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5587
5641
|
automaticCommands: previousDecision.automaticCommands
|
|
5588
5642
|
};
|
|
5589
5643
|
}
|
|
5644
|
+
/**
|
|
5645
|
+
* Creates a persistent AIAgentRequest row when a Chat step fires.
|
|
5646
|
+
* Resolves the target user via the assignment chain:
|
|
5647
|
+
* 1. contextUser (explicit caller)
|
|
5648
|
+
* 2. AgentRun.UserID (run initiator)
|
|
5649
|
+
* 3. Conversation.UserID (conversation fallback)
|
|
5650
|
+
* 4. Agent.OwnerUserID (agent owner)
|
|
5651
|
+
* 5. null (system-level, visible to admins)
|
|
5652
|
+
*
|
|
5653
|
+
* @private
|
|
5654
|
+
*/
|
|
5655
|
+
async createFeedbackRequest(params, stepEntity, previousDecision) {
|
|
5656
|
+
try {
|
|
5657
|
+
const requestTypeId = await this.resolveRequestTypeId(previousDecision, params.contextUser);
|
|
5658
|
+
const resolvedStrategy = await this.resolveAssignmentStrategy(params, requestTypeId);
|
|
5659
|
+
const requestForUserId = this.resolveUserFromStrategy(resolvedStrategy, params);
|
|
5660
|
+
const priority = resolvedStrategy?.priority ?? 50;
|
|
5661
|
+
const expirationMinutes = resolvedStrategy?.expirationMinutes;
|
|
5662
|
+
const request = await this._metadata.GetEntityObject('MJ: AI Agent Requests', params.contextUser);
|
|
5663
|
+
request.NewRecord();
|
|
5664
|
+
request.AgentID = params.agent.ID;
|
|
5665
|
+
request.RequestedAt = new Date();
|
|
5666
|
+
request.RequestForUserID = requestForUserId;
|
|
5667
|
+
request.Status = 'Requested';
|
|
5668
|
+
request.Request = previousDecision.message || 'Agent needs user input';
|
|
5669
|
+
request.RequestTypeID = requestTypeId;
|
|
5670
|
+
request.ResponseSchema = previousDecision.responseForm
|
|
5671
|
+
? JSON.stringify(previousDecision.responseForm)
|
|
5672
|
+
: null;
|
|
5673
|
+
request.Priority = priority;
|
|
5674
|
+
request.OriginatingAgentRunID = this._agentRun?.ID || null;
|
|
5675
|
+
request.OriginatingAgentRunStepID = stepEntity.ID;
|
|
5676
|
+
if (expirationMinutes != null && expirationMinutes > 0) {
|
|
5677
|
+
request.ExpiresAt = new Date(Date.now() + expirationMinutes * 60_000);
|
|
5678
|
+
}
|
|
5679
|
+
const saved = await request.Save();
|
|
5680
|
+
if (saved) {
|
|
5681
|
+
this._feedbackRequestId = request.ID;
|
|
5682
|
+
this.logStatus(`📋 Created feedback request ${request.ID} for user ${requestForUserId || '(system)'}`, true, params);
|
|
5683
|
+
}
|
|
5684
|
+
else {
|
|
5685
|
+
LogError(`Failed to save AIAgentRequest for agent ${params.agent.Name}`);
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
catch (error) {
|
|
5689
|
+
// Don't let request creation failure break the agent execution
|
|
5690
|
+
LogError(`Error creating feedback request: ${error.message}`);
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
/**
|
|
5694
|
+
* Walks the assignment strategy resolution chain (bottom-up, first-non-null wins):
|
|
5695
|
+
* 1. ExecuteAgentParams.assignmentStrategy (per-invocation)
|
|
5696
|
+
* 2. Agent Type's AssignmentStrategy
|
|
5697
|
+
* 3. Agent's Category AssignmentStrategy (walks up ParentID tree)
|
|
5698
|
+
* 4. Request Type's DefaultAssignmentStrategy
|
|
5699
|
+
* 5. Fallback: null (caller uses contextUser + warning)
|
|
5700
|
+
* @private
|
|
5701
|
+
*/
|
|
5702
|
+
async resolveAssignmentStrategy(params, requestTypeId) {
|
|
5703
|
+
// 1. Per-invocation override (highest precedence)
|
|
5704
|
+
if (params.assignmentStrategy) {
|
|
5705
|
+
return params.assignmentStrategy;
|
|
5706
|
+
}
|
|
5707
|
+
// 2. Agent Type's AssignmentStrategy
|
|
5708
|
+
const agentType = AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, params.agent.TypeID));
|
|
5709
|
+
const typeStrategy = parseAssignmentStrategy(agentType?.AssignmentStrategy ?? null);
|
|
5710
|
+
if (typeStrategy) {
|
|
5711
|
+
return typeStrategy;
|
|
5712
|
+
}
|
|
5713
|
+
// 3. Agent's Category AssignmentStrategy (walk up ParentID tree)
|
|
5714
|
+
const categoryStrategy = await this.resolveCategoryAssignmentStrategy(params);
|
|
5715
|
+
if (categoryStrategy) {
|
|
5716
|
+
return categoryStrategy;
|
|
5717
|
+
}
|
|
5718
|
+
// 4. Request Type's DefaultAssignmentStrategy
|
|
5719
|
+
if (requestTypeId && this._requestTypeCache) {
|
|
5720
|
+
const requestType = this._requestTypeCache.find(t => UUIDsEqual(t.ID, requestTypeId));
|
|
5721
|
+
if (requestType) {
|
|
5722
|
+
const rtStrategy = parseAssignmentStrategy(requestType.DefaultAssignmentStrategy);
|
|
5723
|
+
if (rtStrategy) {
|
|
5724
|
+
return rtStrategy;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
return null;
|
|
5729
|
+
}
|
|
5730
|
+
async resolveCategoryAssignmentStrategy(params) {
|
|
5731
|
+
const categoryId = params.agent.CategoryID;
|
|
5732
|
+
if (!categoryId)
|
|
5733
|
+
return null;
|
|
5734
|
+
try {
|
|
5735
|
+
// Load all categories if not cached
|
|
5736
|
+
if (!this._categoryCache) {
|
|
5737
|
+
const rv = new RunView();
|
|
5738
|
+
const result = await rv.RunView({
|
|
5739
|
+
EntityName: 'MJ: AI Agent Categories',
|
|
5740
|
+
Fields: ['ID', 'ParentID', 'AssignmentStrategy'],
|
|
5741
|
+
ResultType: 'simple'
|
|
5742
|
+
}, params.contextUser);
|
|
5743
|
+
this._categoryCache = result.Success ? result.Results : [];
|
|
5744
|
+
}
|
|
5745
|
+
// Walk up the tree from the agent's category to the root
|
|
5746
|
+
let currentId = categoryId;
|
|
5747
|
+
const visited = new Set(); // prevent infinite loops
|
|
5748
|
+
while (currentId && !visited.has(currentId)) {
|
|
5749
|
+
visited.add(currentId);
|
|
5750
|
+
const cat = this._categoryCache.find(c => UUIDsEqual(c.ID, currentId));
|
|
5751
|
+
if (!cat)
|
|
5752
|
+
break;
|
|
5753
|
+
const strategy = parseAssignmentStrategy(cat.AssignmentStrategy);
|
|
5754
|
+
if (strategy)
|
|
5755
|
+
return strategy;
|
|
5756
|
+
currentId = cat.ParentID;
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
catch (error) {
|
|
5760
|
+
LogError(`Error resolving category assignment strategy: ${error.message}`);
|
|
5761
|
+
}
|
|
5762
|
+
return null;
|
|
5763
|
+
}
|
|
5764
|
+
/**
|
|
5765
|
+
* Resolves the target user ID from a resolved assignment strategy.
|
|
5766
|
+
* For simple strategies (RunUser, AgentOwner, SpecificUser), resolves immediately.
|
|
5767
|
+
* For List/SharedInbox strategies, delegates to list-based resolution.
|
|
5768
|
+
* Falls back to contextUser with a warning if no strategy is provided.
|
|
5769
|
+
* @private
|
|
5770
|
+
*/
|
|
5771
|
+
resolveUserFromStrategy(strategy, params) {
|
|
5772
|
+
if (!strategy) {
|
|
5773
|
+
// No strategy resolved anywhere — fall back to contextUser + warning
|
|
5774
|
+
if (params.contextUser?.ID) {
|
|
5775
|
+
LogStatus(`⚠️ No assignment strategy configured for agent ${params.agent.Name}; defaulting to context user`);
|
|
5776
|
+
return params.contextUser.ID;
|
|
5777
|
+
}
|
|
5778
|
+
LogStatus(`⚠️ No assignment strategy and no context user for agent ${params.agent.Name}; request will be unassigned`);
|
|
5779
|
+
return null;
|
|
5780
|
+
}
|
|
5781
|
+
switch (strategy.type) {
|
|
5782
|
+
case 'RunUser':
|
|
5783
|
+
return params.contextUser?.ID
|
|
5784
|
+
?? this._agentRun?.UserID
|
|
5785
|
+
?? null;
|
|
5786
|
+
case 'AgentOwner':
|
|
5787
|
+
return params.agent.OwnerUserID ?? null;
|
|
5788
|
+
case 'SpecificUser':
|
|
5789
|
+
return strategy.userID ?? null;
|
|
5790
|
+
case 'List':
|
|
5791
|
+
// List-based resolution (RoundRobin, LeastBusy, Random) requires async DB lookups.
|
|
5792
|
+
// For now, fall back to contextUser. Full list resolution is a future enhancement
|
|
5793
|
+
// that will query ListDetail records and track assignment state.
|
|
5794
|
+
LogStatus(`ℹ️ List-based assignment strategy configured but not yet implemented; defaulting to context user`);
|
|
5795
|
+
return params.contextUser?.ID ?? null;
|
|
5796
|
+
case 'SharedInbox':
|
|
5797
|
+
// SharedInbox means "unassigned — anyone in the list can claim it"
|
|
5798
|
+
return null;
|
|
5799
|
+
default:
|
|
5800
|
+
return params.contextUser?.ID ?? null;
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
async resolveRequestTypeId(previousDecision, contextUser) {
|
|
5804
|
+
try {
|
|
5805
|
+
// Load request types if not cached
|
|
5806
|
+
if (!this._requestTypeCache) {
|
|
5807
|
+
const rv = new RunView();
|
|
5808
|
+
const result = await rv.RunView({
|
|
5809
|
+
EntityName: 'MJ: AI Agent Request Types',
|
|
5810
|
+
ResultType: 'entity_object'
|
|
5811
|
+
}, contextUser);
|
|
5812
|
+
this._requestTypeCache = result.Success ? result.Results : [];
|
|
5813
|
+
}
|
|
5814
|
+
// Determine type name based on responseForm content
|
|
5815
|
+
let typeName = 'Information'; // default
|
|
5816
|
+
if (previousDecision.responseForm) {
|
|
5817
|
+
typeName = this.detectRequestTypeName(previousDecision.responseForm);
|
|
5818
|
+
}
|
|
5819
|
+
const matchedType = this._requestTypeCache.find(t => t.Name === typeName);
|
|
5820
|
+
return matchedType?.ID || null;
|
|
5821
|
+
}
|
|
5822
|
+
catch (error) {
|
|
5823
|
+
LogError(`Error resolving request type: ${error.message}`);
|
|
5824
|
+
return null;
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
/**
|
|
5828
|
+
* Inspects an AgentResponseForm to determine the best request type name.
|
|
5829
|
+
* Returns "Approval" if the form is a simple two-option approve/reject pattern,
|
|
5830
|
+
* otherwise returns "Information".
|
|
5831
|
+
* @private
|
|
5832
|
+
*/
|
|
5833
|
+
detectRequestTypeName(form) {
|
|
5834
|
+
const q = form.questions?.[0];
|
|
5835
|
+
if (!q || form.questions.length !== 1) {
|
|
5836
|
+
return 'Information';
|
|
5837
|
+
}
|
|
5838
|
+
const qType = q.type;
|
|
5839
|
+
if ((qType.type === 'buttongroup' || qType.type === 'radio') && 'options' in qType) {
|
|
5840
|
+
const opts = qType.options;
|
|
5841
|
+
if (opts.length === 2) {
|
|
5842
|
+
const labels = opts.map(o => o.label.toLowerCase());
|
|
5843
|
+
const hasPositive = labels.some(l => l.includes('approv') || l.includes('yes') || l.includes('accept'));
|
|
5844
|
+
const hasNegative = labels.some(l => l.includes('reject') || l.includes('no') || l.includes('deny'));
|
|
5845
|
+
if (hasPositive && hasNegative) {
|
|
5846
|
+
return 'Approval';
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
return 'Information';
|
|
5851
|
+
}
|
|
5590
5852
|
/**
|
|
5591
5853
|
* Executes a ForEach loop with actual for loop
|
|
5592
5854
|
* @private
|
|
@@ -6178,6 +6440,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6178
6440
|
// set status to Failed
|
|
6179
6441
|
this._agentRun.Status = 'Failed';
|
|
6180
6442
|
}
|
|
6443
|
+
else if (finalStep.step === 'Chat') {
|
|
6444
|
+
// Chat steps mean the agent is waiting for human input
|
|
6445
|
+
this._agentRun.Status = 'AwaitingFeedback';
|
|
6446
|
+
}
|
|
6181
6447
|
else {
|
|
6182
6448
|
this._agentRun.Status = 'Completed';
|
|
6183
6449
|
}
|
|
@@ -6216,7 +6482,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6216
6482
|
memoryContext: this._injectedMemory.notes.length > 0 || this._injectedMemory.examples.length > 0
|
|
6217
6483
|
? this._injectedMemory
|
|
6218
6484
|
: undefined,
|
|
6219
|
-
mediaOutputs: this._mediaOutputs.length > 0 ? this._mediaOutputs : undefined
|
|
6485
|
+
mediaOutputs: this._mediaOutputs.length > 0 ? this._mediaOutputs : undefined,
|
|
6486
|
+
feedbackRequestId: this._feedbackRequestId || undefined
|
|
6220
6487
|
};
|
|
6221
6488
|
}
|
|
6222
6489
|
/**
|