@memberjunction/server 2.36.1 → 2.37.1

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.
@@ -1,5 +1,5 @@
1
- import { Arg, Ctx, Field, Int, Mutation, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
- import { LogError, LogStatus, Metadata, KeyValuePair, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, AllMetadataArrays } from '@memberjunction/core';
1
+ import { Arg, Ctx, Field, Mutation, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
+ import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo } from '@memberjunction/core';
3
3
  import { AppContext, UserPayload, MJ_SERVER_EVENT_CODE } from '../types.js';
4
4
  import { BehaviorSubject } from 'rxjs';
5
5
  import { take } from 'rxjs/operators';
@@ -32,6 +32,9 @@ import {
32
32
  SkipConversation,
33
33
  SkipAPIArtifact,
34
34
  SkipAPIAgentRequest,
35
+ SkipAPIArtifactRequest,
36
+ SkipAPIArtifactType,
37
+ SkipAPIArtifactVersion,
35
38
  } from '@memberjunction/skip-types';
36
39
 
37
40
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
@@ -40,6 +43,9 @@ import {
40
43
  AIAgentLearningCycleEntity,
41
44
  AIAgentNoteEntity,
42
45
  AIAgentRequestEntity,
46
+ ArtifactTypeEntity,
47
+ ConversationArtifactEntity,
48
+ ConversationArtifactVersionEntity,
43
49
  ConversationDetailEntity,
44
50
  ConversationEntity,
45
51
  DataContextEntity,
@@ -47,7 +53,7 @@ import {
47
53
  UserNotificationEntity,
48
54
  } from '@memberjunction/core-entities';
49
55
  import { DataSource } from 'typeorm';
50
- import { ___skipAPIOrgId, ___skipAPIurl, ___skipLearningAPIurl, ___skipLearningCycleIntervalInMinutes, apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
56
+ import { ___skipAPIOrgId, ___skipAPIurl, ___skipLearningAPIurl, ___skipLearningCycleIntervalInMinutes, ___skipRunLearningCycles, apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
51
57
 
52
58
  import { registerEnumType } from 'type-graphql';
53
59
  import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
@@ -56,6 +62,8 @@ import { GetAIAPIKey } from '@memberjunction/ai';
56
62
  import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
57
63
  import { AIAgentEntityExtended, AIEngine } from '@memberjunction/aiengine';
58
64
  import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
65
+ import e from 'express';
66
+ import { Skip } from '@graphql-tools/utils';
59
67
 
60
68
  enum SkipResponsePhase {
61
69
  ClarifyingQuestion = 'clarifying_question',
@@ -155,6 +163,23 @@ export class StopLearningCycleResultType {
155
163
  CycleDetails: CycleDetailsType;
156
164
  }
157
165
 
166
+ /**
167
+ * Internally used type
168
+ */
169
+ type BaseSkipRequest = {
170
+ entities: SkipEntityInfo[],
171
+ queries: SkipQueryInfo[],
172
+ notes: SkipAPIAgentNote[],
173
+ noteTypes: SkipAPIAgentNoteType[],
174
+ requests: SkipAPIAgentRequest[],
175
+ accessToken: GetDataAccessToken,
176
+ organizationID: string,
177
+ organizationInfo: any,
178
+ apiKeys: SkipAPIRequestAPIKey[],
179
+ callingServerURL: string,
180
+ callingServerAPIKey: string,
181
+ callingServerAccessToken: string
182
+ }
158
183
  @Resolver(AskSkipResultType)
159
184
  export class AskSkipResolver {
160
185
  private static _defaultNewChatName = 'New Chat';
@@ -162,14 +187,23 @@ export class AskSkipResolver {
162
187
  // Static initializer that runs when the class is loaded - initializes the learning cycle scheduler
163
188
  static {
164
189
  try {
165
- LogStatus('Initializing Skip AI Learning Cycle Scheduler');
166
-
167
190
  // Set up event listener for server initialization
168
191
  const eventListener = MJGlobal.Instance.GetEventListener(true);
169
192
  eventListener.subscribe(event => {
170
193
  // Filter for our server's setup complete event
171
194
  if (event.eventCode === MJ_SERVER_EVENT_CODE && event.args?.type === 'setupComplete') {
172
195
  try {
196
+ if (___skipRunLearningCycles !== 'Y') {
197
+ LogStatus('Skip AI Learning cycle scheduler not started: Disabled in configuration');
198
+ return;
199
+ }
200
+
201
+ // Check if we have a valid endpoint when cycles are enabled
202
+ if (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
203
+ LogError('Skip AI Learning cycle scheduler not started: Learning cycles are enabled but no API endpoint is configured');
204
+ return;
205
+ }
206
+
173
207
  const dataSources = event.args.dataSources;
174
208
  if (dataSources && dataSources.length > 0) {
175
209
  // Initialize the scheduler
@@ -181,7 +215,6 @@ export class AskSkipResolver {
181
215
  // Default is 60 minutes, if the interval is not set in the config, use 60 minutes
182
216
  const interval = ___skipLearningCycleIntervalInMinutes ?? 60;
183
217
  scheduler.start(interval);
184
- LogStatus(`📅 Skip AI Learning cycle scheduler started with ${interval} minute interval`);
185
218
  } else {
186
219
  LogError('Cannot initialize Skip learning cycle scheduler: No data sources available');
187
220
  }
@@ -190,8 +223,6 @@ export class AskSkipResolver {
190
223
  }
191
224
  }
192
225
  });
193
-
194
- LogStatus('Skip AI Learning Cycle Scheduler initialization listener registered');
195
226
  } catch (error) {
196
227
  // Handle any errors from the static initializer
197
228
  LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
@@ -283,6 +314,30 @@ export class AskSkipResolver {
283
314
  @Ctx() { dataSource, userPayload }: AppContext,
284
315
  @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
285
316
  ) {
317
+ // First check if learning cycles are enabled in configuration
318
+ if (___skipRunLearningCycles !== 'Y') {
319
+ return {
320
+ success: false,
321
+ error: 'Learning cycles are disabled in configuration',
322
+ elapsedTime: 0,
323
+ noteChanges: [],
324
+ queryChanges: [],
325
+ requestChanges: []
326
+ };
327
+ }
328
+
329
+ // Check if we have a valid endpoint when cycles are enabled
330
+ if (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
331
+ return {
332
+ success: false,
333
+ error: 'Learning cycle API endpoint is not configured',
334
+ elapsedTime: 0,
335
+ noteChanges: [],
336
+ queryChanges: [],
337
+ requestChanges: []
338
+ };
339
+ }
340
+
286
341
  const startTime = new Date();
287
342
  // First, get the user from the cache
288
343
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
@@ -308,8 +363,6 @@ export class AskSkipResolver {
308
363
  };
309
364
  }
310
365
 
311
- LogStatus(`Starting learning cycle for AI agent Skip`);
312
-
313
366
  // Get the Skip agent ID
314
367
  const md = new Metadata();
315
368
  const skipAgent = AIEngine.Instance.GetAgentByName('Skip');
@@ -342,7 +395,7 @@ export class AskSkipResolver {
342
395
  try {
343
396
  // Build the request to Skip learning API
344
397
  LogStatus(`Building Skip Learning API request`);
345
- const input = await this.buildSkipLearningAPIRequest(learningCycleId, lastCompleteLearningCycleDate, true, true, true, true, dataSource, user, ForceEntityRefresh || false);
398
+ const input = await this.buildSkipLearningAPIRequest(learningCycleId, lastCompleteLearningCycleDate, true, true, true, false, dataSource, user, ForceEntityRefresh || false);
346
399
 
347
400
  // Make the API request
348
401
  const response = await this.handleSimpleSkipLearningPostRequest(input, user, learningCycleId, agentID);
@@ -625,7 +678,7 @@ cycle.`);
625
678
  forceEntitiesRefresh: boolean = false,
626
679
  includeCallBackKeyAndAccessToken: boolean = false,
627
680
  additionalTokenInfo: any = {}
628
- ) {
681
+ ): Promise<BaseSkipRequest> {
629
682
 
630
683
  const entities = includeEntities ? await this.BuildSkipEntities(dataSource, forceEntitiesRefresh) : [];
631
684
  const queries = includeQueries ? this.BuildSkipQueries() : [];
@@ -657,7 +710,7 @@ cycle.`);
657
710
  noteTypes,
658
711
  requests,
659
712
  accessToken,
660
- organizationId: ___skipAPIOrgId,
713
+ organizationID: ___skipAPIOrgId,
661
714
  organizationInfo: configInfo?.askSkip?.organizationInfo,
662
715
  apiKeys: this.buildSkipAPIKeys(),
663
716
  callingServerURL: accessToken ? `${baseUrl}:${graphqlPort}` : undefined,
@@ -698,7 +751,7 @@ cycle.`);
698
751
 
699
752
  // Create the learning-specific request object
700
753
  const input: SkipAPILearningCycleRequest = {
701
- organizationId: baseRequest.organizationId,
754
+ organizationId: baseRequest.organizationID,
702
755
  organizationInfo: baseRequest.organizationInfo,
703
756
  learningCycleId,
704
757
  lastLearningCycleDate,
@@ -858,23 +911,101 @@ cycle.`);
858
911
  additionalTokenInfo
859
912
  );
860
913
 
914
+ const artifacts: SkipAPIArtifact[] = await this.buildSkipAPIArtifacts(contextUser, dataSource, conversationId);
915
+
861
916
  // Create the chat-specific request object
862
917
  const input: SkipAPIRequest = {
918
+ ...baseRequest,
863
919
  messages,
864
920
  conversationID: conversationId.toString(),
865
921
  dataContext: <DataContext>CopyScalarsAndArrays(dataContext), // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
866
- organizationID: baseRequest.organizationId,
867
922
  requestPhase,
868
- entities: baseRequest.entities,
869
- queries: baseRequest.queries,
870
- notes: baseRequest.notes,
871
- noteTypes: baseRequest.noteTypes,
872
- apiKeys: baseRequest.apiKeys,
923
+ artifacts: artifacts
873
924
  };
874
925
 
875
926
  return input;
876
927
  }
877
928
 
929
+ /**
930
+ * Builds up an array of SkipAPIArtifact types to send across information about the artifacts associated with this particular
931
+ * conversation.
932
+ * @param contextUser
933
+ * @param dataSource
934
+ * @param conversationId
935
+ * @returns
936
+ */
937
+ protected async buildSkipAPIArtifacts(contextUser: UserInfo, dataSource: DataSource, conversationId: string): Promise<SkipAPIArtifact[]> {
938
+ const md = new Metadata();
939
+ const ei = md.EntityByName('MJ: Conversation Artifacts');
940
+ const rv = new RunView();
941
+ const results = await rv.RunViews([
942
+ {
943
+ EntityName: "MJ: Conversation Artifacts",
944
+ ExtraFilter: `ConversationID='${conversationId}'`, // get artifacts linked to this convo
945
+ OrderBy: "__mj_CreatedAt"
946
+ },
947
+ {
948
+ EntityName: "MJ: Artifact Types", // get all artifact types
949
+ OrderBy: "Name"
950
+ },
951
+ {
952
+ EntityName: "MJ: Conversation Artifact Versions",
953
+ ExtraFilter: `ConversationArtifactID IN (SELECT ID FROM [${ei.SchemaName}].[${ei.BaseView}] WHERE ConversationID='${conversationId}')`,
954
+ OrderBy: 'ConversationArtifactID, __mj_CreatedAt'
955
+ }
956
+ ], contextUser);
957
+ if (results && results.length > 0 && results.every((r) => r.Success)) {
958
+ const types: SkipAPIArtifactType[] = results[1].Results.map((a: ArtifactTypeEntity) => {
959
+ const retVal: SkipAPIArtifactType = {
960
+ id: a.ID,
961
+ name: a.Name,
962
+ description: a.Description,
963
+ contentType: a.ContentType,
964
+ enabled: a.IsEnabled,
965
+ createdAt: a.__mj_CreatedAt,
966
+ updatedAt: a.__mj_UpdatedAt
967
+ }
968
+ return retVal;
969
+ });
970
+ const allConvoArtifacts = results[0].Results.map((a: ConversationArtifactEntity) => {
971
+ const rawVersions: ConversationArtifactVersionEntity[] = results[2].Results as ConversationArtifactVersionEntity[];
972
+ const thisArtifactsVersions = rawVersions.filter(rv => rv.ConversationArtifactID === a.ID);
973
+ const versionsForThisArtifact: SkipAPIArtifactVersion[] = thisArtifactsVersions.map((v: ConversationArtifactVersionEntity) => {
974
+ const versionRetVal: SkipAPIArtifactVersion = {
975
+ id: v.ID,
976
+ artifactId: v.ConversationArtifactID,
977
+ version: v.Version,
978
+ configuration: v.Configuration,
979
+ content: v.Content,
980
+ comments: v.Comments,
981
+ createdAt: v.__mj_CreatedAt,
982
+ updatedAt: v.__mj_UpdatedAt
983
+ };
984
+ return versionRetVal;
985
+ });
986
+ const artifactRetVal: SkipAPIArtifact = {
987
+ id: a.ID,
988
+ name: a.Name,
989
+ description: a.Description,
990
+ comments: a.Comments,
991
+ sharingScope: a.SharingScope as 'None' |'SpecificUsers' |'Everyone' |'Public',
992
+ versions: versionsForThisArtifact,
993
+ conversationId: a.ConversationID,
994
+ artifactType: types.find((t => t.id === a.ArtifactTypeID)),
995
+ createdAt: a.__mj_CreatedAt,
996
+ updatedAt: a.__mj_UpdatedAt
997
+ };
998
+ return artifactRetVal;
999
+ });
1000
+
1001
+ return allConvoArtifacts;
1002
+ }
1003
+ else {
1004
+ return [];
1005
+ }
1006
+ }
1007
+
1008
+
878
1009
  /**
879
1010
  * Executes a script in the context of a data context and returns the results
880
1011
  * @param pubSub
@@ -1452,7 +1583,6 @@ cycle.`);
1452
1583
  convoDetailEntity.Message = UserQuestion;
1453
1584
  convoDetailEntity.Role = 'User';
1454
1585
  convoDetailEntity.HiddenToUser = false;
1455
- convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
1456
1586
  let convoDetailSaveResult: boolean = await convoDetailEntity.Save();
1457
1587
  if (!convoDetailSaveResult) {
1458
1588
  LogError(`Error saving conversation detail entity for user message: ${UserQuestion}`, undefined, convoDetailEntity.LatestResult);
@@ -1782,7 +1912,8 @@ cycle.`);
1782
1912
  user,
1783
1913
  convoEntity,
1784
1914
  pubSub,
1785
- userPayload
1915
+ userPayload,
1916
+ dataSource
1786
1917
  );
1787
1918
  const response: AskSkipResultType = {
1788
1919
  Success: true,
@@ -2012,11 +2143,66 @@ cycle.`);
2012
2143
  user: UserInfo,
2013
2144
  convoEntity: ConversationEntity,
2014
2145
  pubSub: PubSubEngine,
2015
- userPayload: UserPayload
2146
+ userPayload: UserPayload,
2147
+ dataSource: DataSource
2016
2148
  ): Promise<{ AIMessageConversationDetailID: string }> {
2017
2149
  const sTitle = apiResponse.reportTitle;
2018
2150
  const sResult = JSON.stringify(apiResponse);
2019
2151
 
2152
+ // first up, let's see if Skip asked us to create an artifact or add a new version to an existing artifact, or NOT
2153
+ // use artifacts at all...
2154
+ let artifactId: string = null;
2155
+ let artifactVersionId: string = null;
2156
+
2157
+ if (apiResponse.artifactRequest?.action === 'new_artifact' || apiResponse.artifactRequest?.action === 'new_artifact_version') {
2158
+ // Skip has requested that we create a new artifact or add a new version to an existing artifact
2159
+ artifactId = apiResponse.artifactRequest.artifactId; // will only be populated if action == new_artifact_version
2160
+ let newVersion: number = 0;
2161
+ if (apiResponse.artifactRequest?.action === 'new_artifact') {
2162
+ const artifactEntity = await md.GetEntityObject<ConversationArtifactEntity>('MJ: Convesration Artifacts', user);
2163
+ // create the new artifact here
2164
+ artifactEntity.NewRecord();
2165
+ artifactEntity.ConversationID = convoEntity.ID;
2166
+ artifactEntity.Name = apiResponse.artifactRequest.name;
2167
+ artifactEntity.Description = apiResponse.artifactRequest.description;
2168
+ if (await artifactEntity.Save()) {
2169
+ // saved, grab the new ID
2170
+ artifactId = artifactEntity.ID;
2171
+ }
2172
+ else {
2173
+ LogError(`Error saving artifact entity for conversation: ${convoEntity.ID}`, undefined, artifactEntity.LatestResult);
2174
+ }
2175
+ newVersion = 1;
2176
+ }
2177
+ else {
2178
+ // we are updating an existing artifact with a new vesrion so we need to get the old max version and increment it
2179
+ const ei = md.EntityByName("MJ: Convesration Artifacts");
2180
+ const sSQL = `SELECT ISNULL(MAX(Version),0) AS MaxVersion FROM [${ei.SchemaName}].[${ei.BaseView}] WHERE ID = '${artifactId}'`;
2181
+ const result = await dataSource.query(sSQL);
2182
+ if (result && result.length > 0) {
2183
+ newVersion = result[0].MaxVersion + 1;
2184
+ } else {
2185
+ LogError(`Error getting max version for artifact ID: ${artifactId}`, undefined, result);
2186
+ }
2187
+ }
2188
+ if (artifactId && newVersion > 0) {
2189
+ // only do this if we were provided an artifact ID or we saved a new one above successfully
2190
+ const artifactVersionEntity = await md.GetEntityObject<ConversationArtifactVersionEntity>('MJ: Conversation Artifact Versions', user);
2191
+ // create the new artifact version here
2192
+ artifactVersionEntity.NewRecord();
2193
+ artifactVersionEntity.ConversationArtifactID = artifactId;
2194
+ artifactVersionEntity.Version = newVersion;
2195
+ artifactVersionEntity.Configuration = sResult; // store the full response here
2196
+ if (await artifactVersionEntity.Save()) {
2197
+ // success saving the new version, set the artifactVersionId
2198
+ artifactVersionId = artifactVersionEntity.ID;
2199
+ }
2200
+ else {
2201
+ LogError(`Error saving Artifact Version record`)
2202
+ }
2203
+ }
2204
+ }
2205
+
2020
2206
  // Create a conversation detail record for the Skip response
2021
2207
  const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
2022
2208
  convoDetailEntityAI.NewRecord();
@@ -2024,7 +2210,13 @@ cycle.`);
2024
2210
  convoDetailEntityAI.Message = sResult;
2025
2211
  convoDetailEntityAI.Role = 'AI';
2026
2212
  convoDetailEntityAI.HiddenToUser = false;
2027
- convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
2213
+ if (artifactId && artifactId.length > 0) {
2214
+ // bind the new convo detail record to the artifact + version for this response
2215
+ convoDetailEntityAI.ArtifactID = artifactId;
2216
+ if (artifactVersionId && artifactVersionId.length > 0) {
2217
+ convoDetailEntityAI.ArtifactVersionID = artifactVersionId;
2218
+ }
2219
+ }
2028
2220
  const convoDetailSaveResult: boolean = await convoDetailEntityAI.Save();
2029
2221
  if (!convoDetailSaveResult) {
2030
2222
  LogError(`Error saving conversation detail entity for AI message: ${sResult}`, undefined, convoDetailEntityAI.LatestResult);
@@ -2070,7 +2262,7 @@ cycle.`);
2070
2262
  // Save the data context items...
2071
2263
  // FOR NOW, we don't want to store the data in the database, we will just load it from the data context when we need it
2072
2264
  // we need a better strategy to persist because the cost of storage and retrieval/parsing is higher than just running the query again in many/most cases
2073
- dataContext.SaveItems(user, false);
2265
+ await dataContext.SaveItems(user, false);
2074
2266
 
2075
2267
  // send a UI update trhough pub-sub
2076
2268
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
@@ -2121,6 +2313,22 @@ cycle.`);
2121
2313
  try {
2122
2314
  LogStatus('Manual execution of Skip learning cycle requested via API');
2123
2315
 
2316
+ // First check if learning cycles are enabled in configuration
2317
+ if (___skipRunLearningCycles !== 'Y') {
2318
+ return {
2319
+ Success: false,
2320
+ Message: 'Learning cycles are disabled in configuration'
2321
+ };
2322
+ }
2323
+
2324
+ // Check if we have a valid endpoint when cycles are enabled
2325
+ if (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
2326
+ return {
2327
+ Success: false,
2328
+ Message: 'Learning cycle API endpoint is not configured'
2329
+ };
2330
+ }
2331
+
2124
2332
  // Use the organization ID from config if not provided
2125
2333
  const orgId = OrganizationId || ___skipAPIOrgId;
2126
2334
 
@@ -39,22 +39,21 @@ export class LearningCycleScheduler extends BaseSingleton<LearningCycleScheduler
39
39
  /**
40
40
  * Start the scheduler with the specified interval in minutes
41
41
  * @param intervalMinutes The interval in minutes between runs
42
+ * @param skipLearningAPIurl The URL for the learning cycle API endpoint
42
43
  */
43
44
  public start(intervalMinutes: number = 60): void {
44
-
45
- // start learning cycle immediately upon the server start
46
- this.runLearningCycle()
47
- .catch(error => LogError(`Error in initial learning cycle: ${error}`));
48
-
49
- const intervalMs = intervalMinutes * 60 * 1000;
50
-
51
45
  LogStatus(`Starting learning cycle scheduler with interval of ${intervalMinutes} minutes`);
52
46
 
53
- // Schedule the recurring task
47
+ // Set up the interval for recurring calls
48
+ const intervalMs = intervalMinutes * 60 * 1000;
54
49
  this.intervalId = setInterval(() => {
55
50
  this.runLearningCycle()
56
51
  .catch(error => LogError(`Error in scheduled learning cycle: ${error}`));
57
52
  }, intervalMs);
53
+
54
+ // Start learning cycle immediately upon the server start
55
+ this.runLearningCycle()
56
+ .catch(error => LogError(`Error in initial learning cycle: ${error}`));
58
57
  }
59
58
 
60
59
  /**
@@ -76,8 +75,6 @@ export class LearningCycleScheduler extends BaseSingleton<LearningCycleScheduler
76
75
  const startTime = new Date();
77
76
 
78
77
  try {
79
- LogStatus('Starting scheduled learning cycle execution');
80
-
81
78
  // Make sure we have data sources
82
79
  if (!this.dataSources || this.dataSources.length === 0) {
83
80
  throw new Error('No data sources available for the learning cycle');