@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.
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +9 -6
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +69 -54
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +24 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +67 -11
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +5 -0
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +234 -18
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/package.json +39 -39
- package/src/config.ts +2 -0
- package/src/generated/generated.ts +49 -40
- package/src/generic/RunViewResolver.ts +67 -8
- package/src/resolvers/AskSkipResolver.ts +289 -18
|
@@ -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
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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)),
|