@memberjunction/server 2.93.0 → 2.95.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.
@@ -22,7 +22,7 @@ import { LoadDataContextItemsServer } from '@memberjunction/data-context-server'
22
22
  import { LearningCycleScheduler } from '../scheduler/LearningCycleScheduler.js';
23
23
  LoadDataContextItemsServer();
24
24
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
25
- import { apiKey, baseUrl, configInfo, graphqlPort } from '../config.js';
25
+ import { apiKey, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath } from '../config.js';
26
26
  import mssql from 'mssql';
27
27
  import { registerEnumType } from 'type-graphql';
28
28
  import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
@@ -31,6 +31,109 @@ import { GetAIAPIKey } from '@memberjunction/ai';
31
31
  import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
32
32
  import { AIEngine } from '@memberjunction/aiengine';
33
33
  import { deleteAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
34
+ class ActiveConversationStreams {
35
+ static instance;
36
+ streams = new Map();
37
+ constructor() { }
38
+ static getInstance() {
39
+ if (!ActiveConversationStreams.instance) {
40
+ ActiveConversationStreams.instance = new ActiveConversationStreams();
41
+ }
42
+ return ActiveConversationStreams.instance;
43
+ }
44
+ updateStatus(conversationId, status, sessionId) {
45
+ const existing = this.streams.get(conversationId);
46
+ if (existing) {
47
+ existing.lastStatus = status;
48
+ existing.lastUpdate = new Date();
49
+ if (sessionId) {
50
+ existing.sessionIds.add(sessionId);
51
+ }
52
+ }
53
+ else {
54
+ const now = new Date();
55
+ this.streams.set(conversationId, {
56
+ lastStatus: status,
57
+ lastUpdate: now,
58
+ startTime: now,
59
+ sessionIds: sessionId ? new Set([sessionId]) : new Set()
60
+ });
61
+ }
62
+ }
63
+ getStatus(conversationId) {
64
+ const stream = this.streams.get(conversationId);
65
+ return stream ? stream.lastStatus : null;
66
+ }
67
+ getStartTime(conversationId) {
68
+ const stream = this.streams.get(conversationId);
69
+ return stream ? stream.startTime : null;
70
+ }
71
+ addSession(conversationId, sessionId) {
72
+ const stream = this.streams.get(conversationId);
73
+ if (stream) {
74
+ stream.sessionIds.add(sessionId);
75
+ }
76
+ else {
77
+ const now = new Date();
78
+ this.streams.set(conversationId, {
79
+ lastStatus: 'Processing...',
80
+ lastUpdate: now,
81
+ startTime: now,
82
+ sessionIds: new Set([sessionId])
83
+ });
84
+ }
85
+ }
86
+ removeConversation(conversationId) {
87
+ this.streams.delete(conversationId);
88
+ }
89
+ isActive(conversationId) {
90
+ const stream = this.streams.get(conversationId);
91
+ if (!stream)
92
+ return false;
93
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
94
+ return stream.lastUpdate > fiveMinutesAgo;
95
+ }
96
+ getSessionIds(conversationId) {
97
+ const stream = this.streams.get(conversationId);
98
+ return stream ? Array.from(stream.sessionIds) : [];
99
+ }
100
+ cleanupStaleStreams() {
101
+ const now = new Date();
102
+ const staleThreshold = new Date(now.getTime() - 30 * 60 * 1000);
103
+ const staleConversations = [];
104
+ this.streams.forEach((stream, conversationId) => {
105
+ if (stream.lastUpdate < staleThreshold) {
106
+ staleConversations.push(conversationId);
107
+ }
108
+ });
109
+ staleConversations.forEach(conversationId => {
110
+ this.streams.delete(conversationId);
111
+ LogStatus(`Cleaned up stale stream for conversation ${conversationId}`);
112
+ });
113
+ if (staleConversations.length > 0) {
114
+ LogStatus(`Cleaned up ${staleConversations.length} stale conversation streams`);
115
+ }
116
+ }
117
+ }
118
+ const activeStreams = ActiveConversationStreams.getInstance();
119
+ setInterval(() => {
120
+ activeStreams.cleanupStaleStreams();
121
+ }, 10 * 60 * 1000);
122
+ let ReattachConversationResponse = class ReattachConversationResponse {
123
+ lastStatusMessage;
124
+ startTime;
125
+ };
126
+ __decorate([
127
+ Field(() => String, { nullable: true }),
128
+ __metadata("design:type", String)
129
+ ], ReattachConversationResponse.prototype, "lastStatusMessage", void 0);
130
+ __decorate([
131
+ Field(() => Date, { nullable: true }),
132
+ __metadata("design:type", Date)
133
+ ], ReattachConversationResponse.prototype, "startTime", void 0);
134
+ ReattachConversationResponse = __decorate([
135
+ ObjectType()
136
+ ], ReattachConversationResponse);
34
137
  var SkipResponsePhase;
35
138
  (function (SkipResponsePhase) {
36
139
  SkipResponsePhase["ClarifyingQuestion"] = "clarifying_question";
@@ -587,7 +690,7 @@ cycle.`);
587
690
  organizationID: skipConfigInfo.orgID,
588
691
  organizationInfo: configInfo?.askSkip?.organizationInfo,
589
692
  apiKeys: this.buildSkipAPIKeys(),
590
- callingServerURL: accessToken ? `${baseUrl}:${graphqlPort}` : undefined,
693
+ callingServerURL: accessToken ? (publicUrl || `${baseUrl}:${graphqlPort}${graphqlRootPath}`) : undefined,
591
694
  callingServerAPIKey: accessToken ? apiKey : undefined,
592
695
  callingServerAccessToken: accessToken ? accessToken.Token : undefined
593
696
  };
@@ -812,6 +915,80 @@ cycle.`);
812
915
  },
813
916
  ];
814
917
  }
918
+ async ReattachToProcessingConversation(ConversationId, { userPayload }, pubSub) {
919
+ try {
920
+ const md = new Metadata();
921
+ const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
922
+ if (!user) {
923
+ LogError(`User ${userPayload.email} not found in UserCache`);
924
+ return null;
925
+ }
926
+ const convoEntity = await md.GetEntityObject('Conversations', user);
927
+ const loadResult = await convoEntity.Load(ConversationId);
928
+ if (!loadResult) {
929
+ LogError(`Could not load conversation ${ConversationId} for re-attachment`);
930
+ return null;
931
+ }
932
+ if (convoEntity.UserID !== user.ID) {
933
+ LogError(`Conversation ${ConversationId} does not belong to user ${user.Email}`);
934
+ return null;
935
+ }
936
+ if (convoEntity.Status === 'Processing') {
937
+ activeStreams.addSession(ConversationId, userPayload.sessionId);
938
+ const lastStatusMessage = activeStreams.getStatus(ConversationId) || 'Processing...';
939
+ const startTime = activeStreams.getStartTime(ConversationId);
940
+ const isStreamActive = activeStreams.isActive(ConversationId);
941
+ if (isStreamActive) {
942
+ const statusMessage = {
943
+ type: 'AskSkip',
944
+ status: 'OK',
945
+ ResponsePhase: 'Processing',
946
+ conversationID: convoEntity.ID,
947
+ message: lastStatusMessage,
948
+ };
949
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
950
+ pushStatusUpdates: {
951
+ message: JSON.stringify(statusMessage),
952
+ sessionId: userPayload.sessionId
953
+ }
954
+ });
955
+ LogStatus(`Re-attached session ${userPayload.sessionId} to active stream for conversation ${ConversationId}, last status: ${lastStatusMessage}`);
956
+ return {
957
+ lastStatusMessage,
958
+ startTime: startTime || convoEntity.__mj_UpdatedAt
959
+ };
960
+ }
961
+ else {
962
+ const statusMessage = {
963
+ type: 'AskSkip',
964
+ status: 'OK',
965
+ ResponsePhase: 'Processing',
966
+ conversationID: convoEntity.ID,
967
+ message: 'Processing...',
968
+ };
969
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
970
+ pushStatusUpdates: {
971
+ message: JSON.stringify(statusMessage),
972
+ sessionId: userPayload.sessionId
973
+ }
974
+ });
975
+ LogStatus(`Re-attached session ${userPayload.sessionId} to conversation ${ConversationId}, but stream is inactive`);
976
+ return {
977
+ lastStatusMessage: 'Processing...',
978
+ startTime: convoEntity.__mj_UpdatedAt
979
+ };
980
+ }
981
+ }
982
+ else {
983
+ LogStatus(`Conversation ${ConversationId} is not processing (Status: ${convoEntity.Status})`);
984
+ return null;
985
+ }
986
+ }
987
+ catch (error) {
988
+ LogError(`Error re-attaching to conversation: ${error}`);
989
+ return null;
990
+ }
991
+ }
815
992
  async ExecuteAskSkipAnalysisQuery(UserQuestion, ConversationId, { dataSource, userPayload }, pubSub, DataContextId, ForceEntityRefresh, StartTime) {
816
993
  const md = new Metadata();
817
994
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
@@ -819,7 +996,7 @@ cycle.`);
819
996
  throw new Error(`User ${userPayload.email} not found in UserCache`);
820
997
  const requestStartTime = StartTime || new Date();
821
998
  const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(dataSource, ConversationId, UserQuestion, user, userPayload, md, DataContextId);
822
- await this.setConversationStatus(convoEntity, 'Processing', userPayload);
999
+ await this.setConversationStatus(convoEntity, 'Processing', userPayload, pubSub);
823
1000
  const messages = await this.LoadConversationDetailsIntoSkipMessages(dataSource, convoEntity.ID, AskSkipResolver_1._maxHistoricalMessages);
824
1001
  const conversationDetailCount = 1;
825
1002
  const input = await this.buildSkipChatAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true, true, false, user, dataSource, ForceEntityRefresh === undefined ? false : ForceEntityRefresh, true);
@@ -1375,7 +1552,7 @@ cycle.`);
1375
1552
  const skipConfigInfo = configInfo.askSkip;
1376
1553
  LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${skipConfigInfo.chatURL}`);
1377
1554
  if (conversationDetailCount > 10) {
1378
- await this.setConversationStatus(convoEntity, 'Available', userPayload);
1555
+ await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
1379
1556
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1380
1557
  message: JSON.stringify({
1381
1558
  type: 'AskSkip',
@@ -1400,21 +1577,26 @@ cycle.`);
1400
1577
  response = await sendPostRequest(skipConfigInfo.chatURL, input, true, null, (message) => {
1401
1578
  LogStatus(JSON.stringify(message, null, 4));
1402
1579
  if (message.type === 'status_update') {
1403
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1404
- message: JSON.stringify({
1405
- type: 'AskSkip',
1406
- status: 'OK',
1407
- conversationID: ConversationId,
1408
- ResponsePhase: message.value.responsePhase,
1409
- message: message.value.messages[0].content,
1410
- }),
1411
- sessionId: userPayload.sessionId,
1412
- });
1580
+ const statusContent = message.value.messages[0].content;
1581
+ activeStreams.updateStatus(ConversationId, statusContent, userPayload.sessionId);
1582
+ const sessionIds = activeStreams.getSessionIds(ConversationId);
1583
+ for (const sessionId of sessionIds) {
1584
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1585
+ message: JSON.stringify({
1586
+ type: 'AskSkip',
1587
+ status: 'OK',
1588
+ conversationID: ConversationId,
1589
+ ResponsePhase: message.value.responsePhase,
1590
+ message: statusContent,
1591
+ }),
1592
+ sessionId: sessionId,
1593
+ });
1594
+ }
1413
1595
  }
1414
1596
  });
1415
1597
  }
1416
1598
  catch (error) {
1417
- await this.setConversationStatus(convoEntity, 'Available', userPayload);
1599
+ await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
1418
1600
  LogError(`Error in HandleSkipChatRequest sendPostRequest: ${error}`);
1419
1601
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1420
1602
  message: JSON.stringify({
@@ -1445,7 +1627,7 @@ cycle.`);
1445
1627
  }
1446
1628
  }
1447
1629
  else {
1448
- await this.setConversationStatus(convoEntity, 'Available', userPayload);
1630
+ await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
1449
1631
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1450
1632
  message: JSON.stringify({
1451
1633
  type: 'AskSkip',
@@ -1515,7 +1697,7 @@ cycle.`);
1515
1697
  convoDetailEntityAI.Role = 'AI';
1516
1698
  convoDetailEntityAI.HiddenToUser = false;
1517
1699
  convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
1518
- await this.setConversationStatus(convoEntity, 'Available', userPayload);
1700
+ await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
1519
1701
  if (await convoDetailEntityAI.Save()) {
1520
1702
  return {
1521
1703
  Success: true,
@@ -1782,13 +1964,38 @@ cycle.`);
1782
1964
  AIMessageConversationDetailID: convoDetailEntityAI.ID,
1783
1965
  };
1784
1966
  }
1785
- async setConversationStatus(convoEntity, status, userPayload) {
1967
+ async setConversationStatus(convoEntity, status, userPayload, pubSub) {
1786
1968
  if (convoEntity.Status !== status) {
1787
1969
  convoEntity.Status = status;
1788
1970
  const convoSaveResult = await convoEntity.Save();
1789
1971
  if (!convoSaveResult) {
1790
1972
  LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
1791
1973
  }
1974
+ else {
1975
+ if (status === 'Available') {
1976
+ activeStreams.removeConversation(convoEntity.ID);
1977
+ LogStatus(`Removed conversation ${convoEntity.ID} from active streams (status changed to Available)`);
1978
+ }
1979
+ else if (status === 'Processing') {
1980
+ activeStreams.addSession(convoEntity.ID, userPayload.sessionId);
1981
+ LogStatus(`Added session ${userPayload.sessionId} to active streams for conversation ${convoEntity.ID}`);
1982
+ }
1983
+ if (pubSub) {
1984
+ const statusMessage = {
1985
+ type: 'ConversationStatusUpdate',
1986
+ conversationID: convoEntity.ID,
1987
+ status: status,
1988
+ timestamp: new Date().toISOString()
1989
+ };
1990
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1991
+ pushStatusUpdates: {
1992
+ message: JSON.stringify(statusMessage),
1993
+ sessionId: userPayload.sessionId
1994
+ }
1995
+ });
1996
+ LogStatus(`Published conversation status update for ${convoEntity.ID}: ${status}`);
1997
+ }
1998
+ }
1792
1999
  return convoSaveResult;
1793
2000
  }
1794
2001
  return true;
@@ -1943,6 +2150,15 @@ __decorate([
1943
2150
  __metadata("design:paramtypes", [Object, PubSubEngine, String, String]),
1944
2151
  __metadata("design:returntype", Promise)
1945
2152
  ], AskSkipResolver.prototype, "ExecuteAskSkipRunScript", null);
2153
+ __decorate([
2154
+ Query(() => ReattachConversationResponse),
2155
+ __param(0, Arg('ConversationId', () => String)),
2156
+ __param(1, Ctx()),
2157
+ __param(2, PubSub()),
2158
+ __metadata("design:type", Function),
2159
+ __metadata("design:paramtypes", [String, Object, PubSubEngine]),
2160
+ __metadata("design:returntype", Promise)
2161
+ ], AskSkipResolver.prototype, "ReattachToProcessingConversation", null);
1946
2162
  __decorate([
1947
2163
  Query(() => AskSkipResultType),
1948
2164
  __param(0, Arg('UserQuestion', () => String)),