@yrpri/api 9.0.82 → 9.0.83
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/active-citizen/email_templates/notifications/general_user_notification/html.ejs +20 -11
- package/active-citizen/email_templates/notifications/general_user_notification/text.ejs +8 -5
- package/active-citizen/engine/analytics/utils.cjs +11 -2
- package/agents/assistants/modes/tools/agentTools.js +7 -1
- package/agents/controllers/policySynthAgents.js +66 -1
- package/agents/managers/emailInvitesManager.js +55 -0
- package/agents/managers/emailTemplateRenderer.js +362 -0
- package/agents/managers/notificationAgentQueueManager.js +32 -19
- package/agents/managers/subscriptionManager.js +2 -0
- package/agents/models/testData/updateAgentWorkflowConfiguration.js +19 -7
- package/controllers/groups.cjs +3 -24
- package/package.json +1 -1
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
<% if (!skipHeaderAndFooter) { %>
|
|
2
|
+
<%- include('../header.html.ejs') %>
|
|
3
|
+
<h2 style="font-weight: 600;line-height: 1.1em;margin: 0;font-size: 14pt;padding-top: 8px;">
|
|
4
|
+
<%- header %>
|
|
5
|
+
</h2>
|
|
6
|
+
<% } %>
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
<% if (!skipHeaderAndFooter) { %>
|
|
9
|
+
<p style="margin-top: 8px;margin-bottom: 8px;">
|
|
10
|
+
<%- content %>
|
|
11
|
+
</p>
|
|
12
|
+
<% } %>
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
<% if (skipHeaderAndFooter) { %>
|
|
15
|
+
<%- content %>
|
|
16
|
+
<% } %>
|
|
10
17
|
|
|
11
|
-
<p style="margin-top: 5px;padding-top: 8px;">
|
|
12
|
-
<%- link %>
|
|
13
|
-
</p>
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
<% if (!skipHeaderAndFooter) { %>
|
|
20
|
+
<p style="margin-top: 5px;padding-top: 8px;">
|
|
21
|
+
<%- link %>
|
|
22
|
+
</p>
|
|
23
|
+
<%- include('../footer.html.ejs') %>
|
|
24
|
+
<% } %>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
<%- include('../header.text.ejs') %>
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
<% if (!skipHeaderAndFooter) { %>
|
|
3
|
+
<%- include('../header.text.ejs') %>
|
|
4
|
+
<%- header %>
|
|
5
|
+
<% } %>
|
|
4
6
|
|
|
5
7
|
<%- content %>
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<%- include('../footer.text.ejs') %>
|
|
9
|
+
<% if (!skipHeaderAndFooter) { %>
|
|
10
|
+
<%- link %>
|
|
11
|
+
<%- include('../footer.text.ejs') %>
|
|
12
|
+
<% } %>
|
|
@@ -107,8 +107,17 @@ const importPost = (post, done) => {
|
|
|
107
107
|
for (let i = 0; i < answers.length; i += 1) {
|
|
108
108
|
if (answers[i]) {
|
|
109
109
|
if (answers[i].value) {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try {
|
|
111
|
+
description += " ";
|
|
112
|
+
description += answers[i].value.trim();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
description += answers[i].value;
|
|
116
|
+
console.warn(`Error trimming answer to description: ${answers[i].value}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.error(`No value for answer in adding to description: ${answers[i]}`);
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
}
|
|
@@ -403,7 +403,13 @@ export class AgentTools extends BaseAssistantTools {
|
|
|
403
403
|
}
|
|
404
404
|
async renderAgentRunWidget(agent, run) {
|
|
405
405
|
const subscription = await this.assistant.getCurrentSubscription();
|
|
406
|
-
const
|
|
406
|
+
const workflowCopy = JSON.parse(JSON.stringify(run.workflow));
|
|
407
|
+
if (workflowCopy.steps) {
|
|
408
|
+
workflowCopy.steps.forEach((step) => {
|
|
409
|
+
step.emailInstructions = "";
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const workflowBase64 = btoa(JSON.stringify(workflowCopy));
|
|
407
413
|
return `<yp-agent-run-widget
|
|
408
414
|
agentProductId="${agent.id}"
|
|
409
415
|
runId="${run.id}"
|
|
@@ -592,6 +592,63 @@ export class PolicySynthAgentsController {
|
|
|
592
592
|
else {
|
|
593
593
|
console.log("Test o1 1712 models already exist");
|
|
594
594
|
}
|
|
595
|
+
const geminiProModel = await PsAiModel.findOne({
|
|
596
|
+
where: {
|
|
597
|
+
name: "Gemini 1.5 Pro 2",
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
if (!geminiProModel) {
|
|
601
|
+
const geminiProConfig = {
|
|
602
|
+
type: PsAiModelType.Text,
|
|
603
|
+
modelSize: PsAiModelSize.Medium,
|
|
604
|
+
provider: "google",
|
|
605
|
+
prices: {
|
|
606
|
+
costInTokensPerMillion: 1.25,
|
|
607
|
+
costOutTokensPerMillion: 5.0,
|
|
608
|
+
currency: "USD",
|
|
609
|
+
},
|
|
610
|
+
maxTokensOut: 8192,
|
|
611
|
+
defaultTemperature: 0.0,
|
|
612
|
+
model: "gemini-1.5-pro-002",
|
|
613
|
+
active: true,
|
|
614
|
+
};
|
|
615
|
+
const geminiPro = await PsAiModel.create({
|
|
616
|
+
name: "Gemini 1.5 Pro 2",
|
|
617
|
+
organization_id: 1,
|
|
618
|
+
user_id: userId,
|
|
619
|
+
configuration: geminiProConfig,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
console.log("Gemini 1.5 Pro 2 models already exist");
|
|
624
|
+
}
|
|
625
|
+
const geminiPro15FlashModel = await PsAiModel.findOne({
|
|
626
|
+
where: {
|
|
627
|
+
name: "Gemini 1.5 Flash 2",
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
if (!geminiPro15FlashModel) {
|
|
631
|
+
const geminiPro15FlashConfig = {
|
|
632
|
+
type: PsAiModelType.Text,
|
|
633
|
+
modelSize: PsAiModelSize.Small,
|
|
634
|
+
provider: "google",
|
|
635
|
+
prices: {
|
|
636
|
+
costInTokensPerMillion: 0.075,
|
|
637
|
+
costOutTokensPerMillion: 0.3,
|
|
638
|
+
currency: "USD",
|
|
639
|
+
},
|
|
640
|
+
maxTokensOut: 8192,
|
|
641
|
+
defaultTemperature: 0.0,
|
|
642
|
+
model: "gemini-1.5-flash-002",
|
|
643
|
+
active: true,
|
|
644
|
+
};
|
|
645
|
+
const geminiPro15Flash = await PsAiModel.create({
|
|
646
|
+
name: "Gemini 1.5 Flash 2",
|
|
647
|
+
organization_id: 1,
|
|
648
|
+
user_id: userId,
|
|
649
|
+
configuration: geminiPro15FlashConfig,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
595
652
|
}
|
|
596
653
|
initializeRoutes() {
|
|
597
654
|
this.router.get("/:groupId", auth.can("view group"), this.getAgent);
|
|
@@ -631,7 +688,8 @@ PolicySynthAgentsController.setupApiKeysForGroup = async (group) => {
|
|
|
631
688
|
const anthropicSonnet = await findLatestActiveModel("Anthropic Sonnet 3.5");
|
|
632
689
|
const openAiGpt4 = await findLatestActiveModel("GPT-4o");
|
|
633
690
|
const openAiGpt4Mini = await findLatestActiveModel("GPT-4o Mini");
|
|
634
|
-
const geminiPro = await findLatestActiveModel("Gemini 1.5 Pro");
|
|
691
|
+
const geminiPro = await findLatestActiveModel("Gemini 1.5 Pro 2");
|
|
692
|
+
const geminiPro15Flash = await findLatestActiveModel("Gemini 1.5 Flash 2");
|
|
635
693
|
const openAio1Preview = await findLatestActiveModel("o1 Preview");
|
|
636
694
|
const openAio1Mini = await findLatestActiveModel("o1 Mini");
|
|
637
695
|
const openAio11712 = await findLatestActiveModel("o1 24");
|
|
@@ -672,6 +730,12 @@ PolicySynthAgentsController.setupApiKeysForGroup = async (group) => {
|
|
|
672
730
|
apiKey: process.env.GEMINI_API_KEY,
|
|
673
731
|
});
|
|
674
732
|
}
|
|
733
|
+
if (geminiPro15Flash && process.env.GEMINI_API_KEY) {
|
|
734
|
+
groupAccessConfig.push({
|
|
735
|
+
aiModelId: geminiPro15Flash.id,
|
|
736
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
675
739
|
if (openAio11712 && process.env.OPENAI_API_KEY) {
|
|
676
740
|
groupAccessConfig.push({
|
|
677
741
|
aiModelId: openAio11712.id,
|
|
@@ -679,5 +743,6 @@ PolicySynthAgentsController.setupApiKeysForGroup = async (group) => {
|
|
|
679
743
|
});
|
|
680
744
|
}
|
|
681
745
|
group.set("private_access_configuration", groupAccessConfig);
|
|
746
|
+
group.changed("private_access_configuration", true);
|
|
682
747
|
await group.save();
|
|
683
748
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// AgentNotificationEmailService.ts
|
|
2
|
+
import models from "../../models/index.cjs";
|
|
3
|
+
import { EmailTemplateRenderer } from "./emailTemplateRenderer.js";
|
|
4
|
+
import queue from "../../active-citizen/workers/queue.cjs";
|
|
5
|
+
import { NotificationAgentQueueManager } from "./notificationAgentQueueManager.js";
|
|
6
|
+
const dbModels = models;
|
|
7
|
+
const Group = dbModels.Group;
|
|
8
|
+
const User = dbModels.User;
|
|
9
|
+
const Community = dbModels.Community;
|
|
10
|
+
const Domain = dbModels.Domain;
|
|
11
|
+
export class AgentInviteManager {
|
|
12
|
+
/**
|
|
13
|
+
* Send a notification email to group admins about the current workflow step or completion.
|
|
14
|
+
*/
|
|
15
|
+
static async sendInviteEmail(link, agentRunId, groupId, user) {
|
|
16
|
+
try {
|
|
17
|
+
const agentRun = await NotificationAgentQueueManager.getAgentRun(agentRunId);
|
|
18
|
+
if (!agentRun) {
|
|
19
|
+
console.error("Agent run not found");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const subject = `${agentRun?.Subscription?.Plan?.AgentProduct?.name} - ${agentRun?.workflow?.steps[agentRun?.workflow?.currentStepIndex]?.shortName} invite`;
|
|
23
|
+
// 2. Extract bundleId and userId if needed
|
|
24
|
+
const bundleId = agentRun?.Subscription?.Plan?.AgentProduct?.AgentBundles?.[0]?.id || 1;
|
|
25
|
+
const group = (await Group.findOne({
|
|
26
|
+
where: { id: groupId },
|
|
27
|
+
include: [
|
|
28
|
+
{
|
|
29
|
+
model: Community,
|
|
30
|
+
attributes: ["id", "name"],
|
|
31
|
+
include: [{ model: Domain, attributes: ["id", "name", "domain_name"] }],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
}));
|
|
35
|
+
const emailContent = EmailTemplateRenderer.renderEmail("", user.name, agentRun.Subscription?.Plan?.AgentProduct?.name || "", agentRun.workflow, link, "https://evoly.ai/is/amplifier/img/amplifier-logo.png", "https://evoly.ai/is/amplifier/img/evoly-bw-logo.png", "https://evoly.ai/", "© 2024 Evoly ehf, Vegmuli 8, 108, Reykjavik, Iceland");
|
|
36
|
+
queue.add("send-one-email", {
|
|
37
|
+
subject: subject,
|
|
38
|
+
template: "general_user_notification",
|
|
39
|
+
user: user,
|
|
40
|
+
domain: group.Community?.Domain,
|
|
41
|
+
group: group,
|
|
42
|
+
object: {},
|
|
43
|
+
header: "",
|
|
44
|
+
content: emailContent,
|
|
45
|
+
link: link,
|
|
46
|
+
skipHeaderAndFooter: true,
|
|
47
|
+
}, { priority: "high" });
|
|
48
|
+
console.log("AgentInviteManager: Email enqueued successfully");
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error("AgentInviteManager: Error sending notification email", error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A flexible renderer for "Competitor Agent" (or any) workflow emails.
|
|
3
|
+
*
|
|
4
|
+
* - The CTA block (button + instructions) is inserted *within* the loop,
|
|
5
|
+
* right after the step that is currently active (currentStepIndex).
|
|
6
|
+
* - Branding (logo, brand name, footer text, etc.) are all passed in,
|
|
7
|
+
* so they are not hardcoded in the template.
|
|
8
|
+
*/
|
|
9
|
+
export class EmailTemplateRenderer {
|
|
10
|
+
/**
|
|
11
|
+
* Render the dynamic email with steps, highlighting the current step,
|
|
12
|
+
* and inserting the CTA block immediately after the current step only.
|
|
13
|
+
*
|
|
14
|
+
* @param recipientName - e.g. "Petur".
|
|
15
|
+
* @param invitedBy - e.g. "Robert Bjarnason".
|
|
16
|
+
* @param agentName - e.g. "Competitor Agent".
|
|
17
|
+
* @param workflow - The workflow with steps + currentStepIndex.
|
|
18
|
+
* @param ctaLink - Destination URL when clicking the CTA button.
|
|
19
|
+
* @param brandLogoUrl - URL to your brand’s logo.
|
|
20
|
+
* @param brandName - Display name of the brand (e.g., "amplifier powered by Evoly").
|
|
21
|
+
* @param footerText - Footer text, e.g. "© 2024 Evoly ehf, Vegmuli 8..."
|
|
22
|
+
* @param footerEmailSettingsLink - Link to "Change email settings".
|
|
23
|
+
*/
|
|
24
|
+
static renderEmail(recipientName, invitedBy, agentName, workflow, ctaLink, brandLogoUrl, brandName, footerText, footerEmailSettingsLink) {
|
|
25
|
+
// Identify the current step
|
|
26
|
+
const currentStepIndex = workflow.currentStepIndex;
|
|
27
|
+
const currentStep = workflow.steps[currentStepIndex] || {};
|
|
28
|
+
// Pull out CTA text + instructions from the current step
|
|
29
|
+
const ctaText = currentStep.emailCallForAction || "Continue";
|
|
30
|
+
const instructionsText = currentStep.emailInstructions || "";
|
|
31
|
+
// We'll build the steps as HTML in a single array, so that we can:
|
|
32
|
+
// - Render each step
|
|
33
|
+
// - If it's the current step, we insert the CTA block right afterward
|
|
34
|
+
const stepsHtmlArray = [];
|
|
35
|
+
workflow.steps.forEach((step, idx) => {
|
|
36
|
+
const isActive = idx === currentStepIndex;
|
|
37
|
+
// Bubble color
|
|
38
|
+
const bubbleBg = isActive ? "#e74c3c" : "#eeeeee";
|
|
39
|
+
const bubbleTextColor = isActive ? "#ffffff" : "#666666";
|
|
40
|
+
// Step text color
|
|
41
|
+
const stepTextColor = isActive ? "#1d211c" : "rgba(29, 33, 28, 0.5)";
|
|
42
|
+
// Single step row
|
|
43
|
+
stepsHtmlArray.push(`
|
|
44
|
+
<tr>
|
|
45
|
+
<td
|
|
46
|
+
style="
|
|
47
|
+
width: 25px;
|
|
48
|
+
text-align: center;
|
|
49
|
+
vertical-align: top;
|
|
50
|
+
"
|
|
51
|
+
>
|
|
52
|
+
<table
|
|
53
|
+
role="presentation"
|
|
54
|
+
style="
|
|
55
|
+
width: 25px;
|
|
56
|
+
height: 25px;
|
|
57
|
+
border-spacing: 0;
|
|
58
|
+
border-collapse: collapse;
|
|
59
|
+
margin-top: 2px;
|
|
60
|
+
"
|
|
61
|
+
>
|
|
62
|
+
<tr>
|
|
63
|
+
<td
|
|
64
|
+
style="
|
|
65
|
+
width: 25px;
|
|
66
|
+
height: 25px;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
background-color: ${bubbleBg};
|
|
69
|
+
color: ${bubbleTextColor};
|
|
70
|
+
font-weight: 500;
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
text-align: center;
|
|
73
|
+
line-height: 25px;
|
|
74
|
+
font-family: 'Prompt', Arial, sans-serif;
|
|
75
|
+
"
|
|
76
|
+
>
|
|
77
|
+
${idx + 1}
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
</table>
|
|
81
|
+
</td>
|
|
82
|
+
<td
|
|
83
|
+
style="
|
|
84
|
+
padding: 0 0 0 16px;
|
|
85
|
+
color: #1d211c;
|
|
86
|
+
font-weight: normal;
|
|
87
|
+
vertical-align: middle;
|
|
88
|
+
"
|
|
89
|
+
>
|
|
90
|
+
<strong style="color:${stepTextColor}">
|
|
91
|
+
${step.shortName}
|
|
92
|
+
</strong>:
|
|
93
|
+
<span style="color:${stepTextColor}">
|
|
94
|
+
${step.shortDescription}
|
|
95
|
+
</span>
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
<tr><td style="height: 8px; line-height: 8px; font-size: 0"></td></tr>
|
|
99
|
+
`);
|
|
100
|
+
// If this step is the current step, we drop in the CTA block immediately after:
|
|
101
|
+
if (isActive) {
|
|
102
|
+
stepsHtmlArray.push(`
|
|
103
|
+
<!-- CTA/instructions block for the current step only -->
|
|
104
|
+
<tr>
|
|
105
|
+
<td colspan="2">
|
|
106
|
+
<table
|
|
107
|
+
role="presentation"
|
|
108
|
+
style="
|
|
109
|
+
width: 100%;
|
|
110
|
+
border-spacing: 0;
|
|
111
|
+
border-collapse: collapse;
|
|
112
|
+
background-color: rgba(255, 220, 47, 0.1);
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
padding: 150px;
|
|
115
|
+
text-align: center;
|
|
116
|
+
"
|
|
117
|
+
>
|
|
118
|
+
<!-- Button Section -->
|
|
119
|
+
<tr>
|
|
120
|
+
<td align="center" style="padding: 36px 0 0 0">
|
|
121
|
+
<table
|
|
122
|
+
role="presentation"
|
|
123
|
+
style="
|
|
124
|
+
border-spacing: 0;
|
|
125
|
+
border-collapse: collapse;
|
|
126
|
+
margin: 0 auto;
|
|
127
|
+
"
|
|
128
|
+
>
|
|
129
|
+
<tr>
|
|
130
|
+
<td
|
|
131
|
+
style="
|
|
132
|
+
background-color: #ffdc2f;
|
|
133
|
+
padding: 10px 20px;
|
|
134
|
+
border-radius: 50px;
|
|
135
|
+
text-align: center;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
font-size: 14px;
|
|
138
|
+
font-family: 'Prompt', Arial, sans-serif;
|
|
139
|
+
"
|
|
140
|
+
>
|
|
141
|
+
<a
|
|
142
|
+
href="${ctaLink}"
|
|
143
|
+
style="
|
|
144
|
+
text-decoration: none;
|
|
145
|
+
color: #1d211c;
|
|
146
|
+
display: inline-block;
|
|
147
|
+
"
|
|
148
|
+
>
|
|
149
|
+
${ctaText}
|
|
150
|
+
</a>
|
|
151
|
+
</td>
|
|
152
|
+
</tr>
|
|
153
|
+
</table>
|
|
154
|
+
</td>
|
|
155
|
+
</tr>
|
|
156
|
+
|
|
157
|
+
<!-- Instructions below the button -->
|
|
158
|
+
<tr>
|
|
159
|
+
<td
|
|
160
|
+
style="
|
|
161
|
+
padding: 22px 0 36px 0;
|
|
162
|
+
color: rgba(29, 33, 28, 1);
|
|
163
|
+
font-weight: normal;
|
|
164
|
+
line-height: 1.5;
|
|
165
|
+
text-align: center;
|
|
166
|
+
"
|
|
167
|
+
>
|
|
168
|
+
<table
|
|
169
|
+
role="presentation"
|
|
170
|
+
style="
|
|
171
|
+
width: 100%;
|
|
172
|
+
border-spacing: 0;
|
|
173
|
+
border-collapse: collapse;
|
|
174
|
+
"
|
|
175
|
+
>
|
|
176
|
+
<tr>
|
|
177
|
+
<td
|
|
178
|
+
style="
|
|
179
|
+
max-width: 540px;
|
|
180
|
+
margin: 0 auto;
|
|
181
|
+
text-align: center;
|
|
182
|
+
display: inline-block;
|
|
183
|
+
line-height: 1.5;
|
|
184
|
+
color: rgba(29, 33, 28, 0.5);
|
|
185
|
+
font-size: 15px;
|
|
186
|
+
padding: 0 20px;
|
|
187
|
+
"
|
|
188
|
+
>
|
|
189
|
+
${instructionsText}
|
|
190
|
+
</td>
|
|
191
|
+
</tr>
|
|
192
|
+
</table>
|
|
193
|
+
</td>
|
|
194
|
+
</tr>
|
|
195
|
+
</table>
|
|
196
|
+
</td>
|
|
197
|
+
</tr>
|
|
198
|
+
<tr><td style="height: 16px; line-height: 16px; font-size: 0"></td></tr>
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// Join the array into one big HTML string
|
|
203
|
+
const stepsHtml = stepsHtmlArray.join("");
|
|
204
|
+
// Now wrap that in the main email layout, using the brand parameters
|
|
205
|
+
const outputHtml = `
|
|
206
|
+
<!DOCTYPE html>
|
|
207
|
+
<html lang="en">
|
|
208
|
+
<head>
|
|
209
|
+
<meta charset="UTF-8" />
|
|
210
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
211
|
+
<link
|
|
212
|
+
href="https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
|
213
|
+
rel="stylesheet"
|
|
214
|
+
/>
|
|
215
|
+
<link
|
|
216
|
+
href="https://fonts.googleapis.com/css2?family=Prompt:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
|
217
|
+
rel="stylesheet"
|
|
218
|
+
/>
|
|
219
|
+
<title>Email Notification</title>
|
|
220
|
+
</head>
|
|
221
|
+
<body
|
|
222
|
+
style="
|
|
223
|
+
margin: 0;
|
|
224
|
+
padding: 0;
|
|
225
|
+
background-color: #ffffff;
|
|
226
|
+
font-family: 'Poppins', Arial, sans-serif;
|
|
227
|
+
font-size: 15px;
|
|
228
|
+
color: #1d211c;
|
|
229
|
+
"
|
|
230
|
+
>
|
|
231
|
+
<table role="presentation" style="border-spacing: 0; width: 100%;">
|
|
232
|
+
<tr>
|
|
233
|
+
<td align="center">
|
|
234
|
+
<table
|
|
235
|
+
style="
|
|
236
|
+
max-width: 680px;
|
|
237
|
+
margin: 0 auto;
|
|
238
|
+
border: 1px solid rgba(29, 33, 28, 0.25);
|
|
239
|
+
padding: 0 30px 0 30px;
|
|
240
|
+
"
|
|
241
|
+
role="presentation"
|
|
242
|
+
>
|
|
243
|
+
<!-- BRANDING / LOGO -->
|
|
244
|
+
<tr>
|
|
245
|
+
<td style="padding-top: 40px; text-align: left">
|
|
246
|
+
<img
|
|
247
|
+
src="${brandLogoUrl}"
|
|
248
|
+
alt="${brandName} logo"
|
|
249
|
+
width="120"
|
|
250
|
+
style="border: 0; display: block"
|
|
251
|
+
/>
|
|
252
|
+
</td>
|
|
253
|
+
</tr>
|
|
254
|
+
|
|
255
|
+
<!-- SALUTATION -->
|
|
256
|
+
<tr>
|
|
257
|
+
<td style="padding: 24px 0 0 0">
|
|
258
|
+
<div style="line-height: 1.5; margin-bottom: 20px">
|
|
259
|
+
Hi ${recipientName ? recipientName : "there"},
|
|
260
|
+
</div>
|
|
261
|
+
</td>
|
|
262
|
+
</tr>
|
|
263
|
+
|
|
264
|
+
${invitedBy
|
|
265
|
+
? `
|
|
266
|
+
<!-- INVITE LINE -->
|
|
267
|
+
<tr>
|
|
268
|
+
<td style="padding: 20px 0 0 0">
|
|
269
|
+
<div style="line-height: 1.5; margin-bottom: 20px">
|
|
270
|
+
<strong>${invitedBy}</strong> invited you to participate
|
|
271
|
+
in the <strong>${agentName}</strong> workflow.
|
|
272
|
+
</div>
|
|
273
|
+
</td>
|
|
274
|
+
</tr>
|
|
275
|
+
`
|
|
276
|
+
: `
|
|
277
|
+
<tr>
|
|
278
|
+
<td style="padding: 20px 0 0 0">
|
|
279
|
+
<div style="line-height: 1.5; margin-bottom: 20px">
|
|
280
|
+
The next step in the <strong>${agentName}</strong> workflow is ready.
|
|
281
|
+
</div>
|
|
282
|
+
</td>
|
|
283
|
+
</tr>`}
|
|
284
|
+
|
|
285
|
+
<!-- STEPS + CTA (inserted in the loop) -->
|
|
286
|
+
<tr>
|
|
287
|
+
<td>
|
|
288
|
+
<table align="left" style="width: auto">
|
|
289
|
+
${stepsHtml}
|
|
290
|
+
</table>
|
|
291
|
+
</td>
|
|
292
|
+
</tr>
|
|
293
|
+
|
|
294
|
+
<!-- SPACER -->
|
|
295
|
+
<tr>
|
|
296
|
+
<td style="height: 60px; line-height: 60px; font-size: 0">
|
|
297
|
+
|
|
298
|
+
</td>
|
|
299
|
+
</tr>
|
|
300
|
+
|
|
301
|
+
<!-- FOOTER with brand text -->
|
|
302
|
+
<tr>
|
|
303
|
+
<td align="center" style="padding: 30px 0; text-align: center">
|
|
304
|
+
<table
|
|
305
|
+
role="presentation"
|
|
306
|
+
style="
|
|
307
|
+
width: 100%;
|
|
308
|
+
border-spacing: 0;
|
|
309
|
+
border-collapse: collapse;
|
|
310
|
+
color: rgba(29, 33, 28, 0.5);
|
|
311
|
+
"
|
|
312
|
+
>
|
|
313
|
+
<tr>
|
|
314
|
+
<td style="padding-bottom: 12px">
|
|
315
|
+
<!-- If you want a simpler brand name or a second logo,
|
|
316
|
+
place it here. Or remove if not needed. -->
|
|
317
|
+
<img
|
|
318
|
+
src="${brandLogoUrl}"
|
|
319
|
+
alt="${brandName}"
|
|
320
|
+
width="80"
|
|
321
|
+
style="
|
|
322
|
+
display: block;
|
|
323
|
+
margin: 0 auto;
|
|
324
|
+
border: 0;
|
|
325
|
+
opacity: 0.5;
|
|
326
|
+
"
|
|
327
|
+
/>
|
|
328
|
+
</td>
|
|
329
|
+
</tr>
|
|
330
|
+
<tr>
|
|
331
|
+
<td style="font-size: 12px; padding-bottom: 6px">
|
|
332
|
+
${footerText}
|
|
333
|
+
</td>
|
|
334
|
+
</tr>
|
|
335
|
+
<tr>
|
|
336
|
+
<td>
|
|
337
|
+
<a
|
|
338
|
+
href="${footerEmailSettingsLink}"
|
|
339
|
+
style="
|
|
340
|
+
font-size: 12px;
|
|
341
|
+
text-decoration: underline;
|
|
342
|
+
color: inherit;
|
|
343
|
+
"
|
|
344
|
+
>
|
|
345
|
+
Change email settings
|
|
346
|
+
</a>
|
|
347
|
+
</td>
|
|
348
|
+
</tr>
|
|
349
|
+
</table>
|
|
350
|
+
</td>
|
|
351
|
+
</tr>
|
|
352
|
+
|
|
353
|
+
</table>
|
|
354
|
+
</td>
|
|
355
|
+
</tr>
|
|
356
|
+
</table>
|
|
357
|
+
</body>
|
|
358
|
+
</html>
|
|
359
|
+
`;
|
|
360
|
+
return outputHtml;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -9,6 +9,7 @@ import { YpAgentProduct } from "../models/agentProduct.js";
|
|
|
9
9
|
import { YpSubscription } from "../models/subscription.js";
|
|
10
10
|
import { YpAgentProductBundle } from "../models/agentProductBundle.js";
|
|
11
11
|
import queue from "../../active-citizen/workers/queue.cjs";
|
|
12
|
+
import { EmailTemplateRenderer } from "./emailTemplateRenderer.js";
|
|
12
13
|
const dbModels = models;
|
|
13
14
|
const Group = dbModels.Group;
|
|
14
15
|
const User = dbModels.User;
|
|
@@ -38,14 +39,19 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
38
39
|
else {
|
|
39
40
|
console.error(`NotificationAgentQueueManager: WebSocket client with ID ${wsClientId} not found`);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
if (updatedWorkflow) {
|
|
43
|
+
await this.sendNotificationEmail(agent, agentRun, updatedWorkflow);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error("NotificationAgentQueueManager: No updated workflow found");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async sendNotificationEmail(agent, agentRun, updatedWorkflow) {
|
|
42
50
|
// Send email notification
|
|
43
|
-
const subject = `${agentRun.Subscription?.Plan?.AgentProduct?.name} - ${
|
|
44
|
-
const content = `Next workflow step is ready: ${currentWorkflowStep?.description}`;
|
|
51
|
+
const subject = `${agentRun.Subscription?.Plan?.AgentProduct?.name} - ${updatedWorkflow?.steps[updatedWorkflow?.currentStepIndex]?.shortName} ready`;
|
|
45
52
|
const bundleId = agentRun.Subscription?.Plan?.AgentProduct?.AgentBundles?.[0]?.id || 1;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
async sendNotificationEmail(agent, subject, content, bundleId) {
|
|
53
|
+
const currentWorkflowStep = updatedWorkflow?.steps[updatedWorkflow?.currentStepIndex];
|
|
54
|
+
const userId = agentRun.Subscription?.user_id;
|
|
49
55
|
const group = (await Group.findOne({
|
|
50
56
|
where: { id: agent.group_id },
|
|
51
57
|
include: [
|
|
@@ -61,14 +67,13 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
61
67
|
}));
|
|
62
68
|
const link = `https://app.${group.Community?.Domain?.domain_name}/agent_bundle/${bundleId}?needsLogin=true`;
|
|
63
69
|
if (group && group.GroupAdmins && group.GroupAdmins.length > 0) {
|
|
64
|
-
|
|
65
|
-
const emailContent =
|
|
66
|
-
<div>
|
|
67
|
-
<h1>${subject}</h1>
|
|
68
|
-
<p>${content}</p>
|
|
69
|
-
</div>
|
|
70
|
-
`;
|
|
70
|
+
let admins = group.GroupAdmins.filter((admin) => admin.email);
|
|
71
|
+
const emailContent = EmailTemplateRenderer.renderEmail("", "", agentRun.Subscription?.Plan?.AgentProduct?.name || "", updatedWorkflow, link, "https://evoly.ai/is/amplifier/img/amplifier-logo.png", "https://evoly.ai/is/amplifier/img/evoly-bw-logo.png", "https://evoly.ai/", "© 2024 Evoly ehf, Vegmuli 8, 108, Reykjavik, Iceland");
|
|
71
72
|
const MAX_ADMIN_EMAILS = 25;
|
|
73
|
+
// Deduplicate admins using Set
|
|
74
|
+
admins = Array.from(new Set(admins.map((admin) => admin.email)))
|
|
75
|
+
.map((email) => admins.find((admin) => admin.email === email))
|
|
76
|
+
.filter((admin) => admin !== undefined);
|
|
72
77
|
for (let u = 0; u < Math.min(admins.length, MAX_ADMIN_EMAILS); u++) {
|
|
73
78
|
queue.add("send-one-email", {
|
|
74
79
|
subject: subject,
|
|
@@ -80,6 +85,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
80
85
|
header: "",
|
|
81
86
|
content: emailContent,
|
|
82
87
|
link: link,
|
|
88
|
+
skipHeaderAndFooter: true,
|
|
83
89
|
}, { priority: "high" });
|
|
84
90
|
}
|
|
85
91
|
}
|
|
@@ -129,6 +135,13 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
129
135
|
// Get the agent run record
|
|
130
136
|
const agentRun = await YpAgentProductRun.findByPk(agentRunId, {
|
|
131
137
|
attributes: ["id", "workflow", "status"],
|
|
138
|
+
include: [
|
|
139
|
+
{
|
|
140
|
+
model: YpSubscription,
|
|
141
|
+
as: "Subscription",
|
|
142
|
+
attributes: ["id", "user_id"],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
132
145
|
});
|
|
133
146
|
if (!agentRun || !agentRun.workflow) {
|
|
134
147
|
console.error(`NotificationAgentQueueManager: Agent run ${agentRunId} or its workflow not found`);
|
|
@@ -178,7 +191,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
178
191
|
console.error(`NotificationAgentQueueManager: Error in advanceWorkflowStepOrCompleteAgentRun:`, error);
|
|
179
192
|
}
|
|
180
193
|
}
|
|
181
|
-
async getAgentRun(agentRunId) {
|
|
194
|
+
static async getAgentRun(agentRunId) {
|
|
182
195
|
return await YpAgentProductRun.findOne({
|
|
183
196
|
where: { id: agentRunId },
|
|
184
197
|
attributes: ["id", "status", "workflow"],
|
|
@@ -186,7 +199,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
186
199
|
{
|
|
187
200
|
model: YpSubscription,
|
|
188
201
|
as: "Subscription",
|
|
189
|
-
attributes: ["id"],
|
|
202
|
+
attributes: ["id", "user_id"],
|
|
190
203
|
include: [
|
|
191
204
|
{
|
|
192
205
|
model: YpSubscriptionPlan,
|
|
@@ -250,7 +263,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
250
263
|
const agent = await PsAgent.findByPk(agentId, {
|
|
251
264
|
include: [{ model: PsAgentClass, as: "Class" }],
|
|
252
265
|
});
|
|
253
|
-
const agentRun = await
|
|
266
|
+
const agentRun = await NotificationAgentQueueManager.getAgentRun(agentRunId);
|
|
254
267
|
if (!agentRun) {
|
|
255
268
|
console.error(`NotificationAgentQueueManager: Agent run with ID ${agentRunId} not found.`);
|
|
256
269
|
return;
|
|
@@ -306,7 +319,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
306
319
|
const agent = await PsAgent.findByPk(agentId, {
|
|
307
320
|
include: [{ model: PsAgentClass, as: "Class" }],
|
|
308
321
|
});
|
|
309
|
-
const agentRun = await
|
|
322
|
+
const agentRun = await NotificationAgentQueueManager.getAgentRun(agentRunId);
|
|
310
323
|
if (agent && agentRun) {
|
|
311
324
|
// Send notification email
|
|
312
325
|
//TODO: Fix this, the agent run should not be marked as failed if the job failed
|
|
@@ -389,7 +402,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
389
402
|
console.error(`NotificationAgentQueueManager: Agent or Agent Class not found for agent ${agentId}`);
|
|
390
403
|
return undefined;
|
|
391
404
|
}
|
|
392
|
-
const agentRun = await
|
|
405
|
+
const agentRun = await NotificationAgentQueueManager.getAgentRun(agentRunId);
|
|
393
406
|
if (!agentRun) {
|
|
394
407
|
console.error(`NotificationAgentQueueManager: Agent run with ID ${agentRunId} not found.`);
|
|
395
408
|
return undefined;
|
|
@@ -420,7 +433,7 @@ export class NotificationAgentQueueManager extends AgentQueueManager {
|
|
|
420
433
|
if (!agent || !agent.Class) {
|
|
421
434
|
throw Error(`NotificationAgentQueueManager: Agent or Agent Class not found for agent ${agentId}`);
|
|
422
435
|
}
|
|
423
|
-
const agentRun = await
|
|
436
|
+
const agentRun = await NotificationAgentQueueManager.getAgentRun(agentRunId);
|
|
424
437
|
if (!agentRun) {
|
|
425
438
|
console.error(`NotificationAgentQueueManager: Agent run with ID ${agentRunId} not found.`);
|
|
426
439
|
return false;
|
|
@@ -253,6 +253,8 @@ export class SubscriptionManager {
|
|
|
253
253
|
}
|
|
254
254
|
// Update the workflow group configuration
|
|
255
255
|
workflowGroup.configuration.agents.topLevelAgentId = clonedTopLevelAgent.id;
|
|
256
|
+
console.log(`Set top level agent id to ${clonedTopLevelAgent.id} for workflow group ${workflowGroup.id}`);
|
|
257
|
+
workflowGroup.changed('configuration', true);
|
|
256
258
|
await workflowGroup.save();
|
|
257
259
|
if (agentProduct.configuration.structuredAnswersOverride) {
|
|
258
260
|
for (const override of agentProduct.configuration
|
|
@@ -16,13 +16,15 @@ async function setupAgentProductsConfiguration() {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "Competitor Analysis People Review",
|
|
19
|
-
shortName: "
|
|
20
|
-
description: "People
|
|
21
|
-
shortDescription: "People
|
|
19
|
+
shortName: "Vet top 10 competitors",
|
|
20
|
+
description: "People review to vet top 10 key competitors",
|
|
21
|
+
shortDescription: "People review to vet top 10 key competitors",
|
|
22
22
|
agentClassUuid: "a1b2c3d4-e5f6-c7c8-a9c0-c1225354f516",
|
|
23
23
|
type: "engagmentFromOutputConnector",
|
|
24
24
|
stepBackgroundColor: "#e74c3c",
|
|
25
25
|
stepTextColor: "#ffffff",
|
|
26
|
+
emailCallForAction: "Start Vetting Competitors",
|
|
27
|
+
emailInstructions: "Use up 👍 and down 👎 thumbs to vet competitors. You can choose as many as you want but <b>only top 10 will go forward</b>, to the next round!"
|
|
26
28
|
},
|
|
27
29
|
{
|
|
28
30
|
name: "Competitor Analysis Detailed Search",
|
|
@@ -35,14 +37,16 @@ async function setupAgentProductsConfiguration() {
|
|
|
35
37
|
stepTextColor: "#ffffff",
|
|
36
38
|
},
|
|
37
39
|
{
|
|
38
|
-
name: "Competitor Analysis
|
|
39
|
-
shortName: "
|
|
40
|
-
description: "
|
|
41
|
-
shortDescription: "
|
|
40
|
+
name: "Competitor Analysis Comment on Key Competitors",
|
|
41
|
+
shortName: "Competitors comments",
|
|
42
|
+
description: "Comment on key competitors before creating report",
|
|
43
|
+
shortDescription: "Comment on key competitors before report",
|
|
42
44
|
agentClassUuid: "c6e99ac4-e5f6-c7c1-a1c0-c1ab53c4ff16",
|
|
43
45
|
type: "engagmentFromOutputConnector",
|
|
44
46
|
stepBackgroundColor: "#2ecc71",
|
|
45
47
|
stepTextColor: "#ffffff",
|
|
48
|
+
emailCallForAction: "Comment On Key Competitors",
|
|
49
|
+
emailInstructions: "View the details for key competitors and add your insights before the final report is created."
|
|
46
50
|
},
|
|
47
51
|
{
|
|
48
52
|
name: "Competitors Report",
|
|
@@ -53,6 +57,8 @@ async function setupAgentProductsConfiguration() {
|
|
|
53
57
|
type: "agentOps",
|
|
54
58
|
stepBackgroundColor: "#d486da",
|
|
55
59
|
stepTextColor: "#ffffff",
|
|
60
|
+
emailCallForAction: "View the Competitor Report",
|
|
61
|
+
emailInstructions: "View the final report on the state of the market based on the competitors analysis."
|
|
56
62
|
},
|
|
57
63
|
],
|
|
58
64
|
};
|
|
@@ -95,6 +101,8 @@ async function setupAgentProductsConfiguration() {
|
|
|
95
101
|
type: "engagmentFromOutputConnector",
|
|
96
102
|
stepBackgroundColor: "#e74c3c",
|
|
97
103
|
stepTextColor: "#ffffff",
|
|
104
|
+
emailCallForAction: "Vet Investors",
|
|
105
|
+
emailInstructions: "Use up 👍 and down 👎 thumbs to vet investors. You can choose as many as you want but <b>only top 10 will go forward</b>, to the next round!"
|
|
98
106
|
},
|
|
99
107
|
{
|
|
100
108
|
name: "Funding Agent Detailed Search",
|
|
@@ -115,6 +123,8 @@ async function setupAgentProductsConfiguration() {
|
|
|
115
123
|
type: "engagmentFromOutputConnector",
|
|
116
124
|
stepBackgroundColor: "#2ecc71",
|
|
117
125
|
stepTextColor: "#ffffff",
|
|
126
|
+
emailCallForAction: "Comment On Key Investors",
|
|
127
|
+
emailInstructions: "View the details for key investors and add your insights before the final report is created."
|
|
118
128
|
},
|
|
119
129
|
{
|
|
120
130
|
name: "Funding Agent Report",
|
|
@@ -125,6 +135,8 @@ async function setupAgentProductsConfiguration() {
|
|
|
125
135
|
type: "agentOps",
|
|
126
136
|
stepBackgroundColor: "#d486da",
|
|
127
137
|
stepTextColor: "#ffffff",
|
|
138
|
+
emailCallForAction: "View the Funding Report",
|
|
139
|
+
emailInstructions: "View the final report on investors and funding opportunities."
|
|
128
140
|
},
|
|
129
141
|
],
|
|
130
142
|
};
|
package/controllers/groups.cjs
CHANGED
|
@@ -1004,6 +1004,7 @@ router.post("/:groupId/sendEmailInvitesForAnons", auth.can("edit group"), async
|
|
|
1004
1004
|
invalidEmails: emailArray.filter((email) => !validEmails.includes(email)),
|
|
1005
1005
|
});
|
|
1006
1006
|
}
|
|
1007
|
+
const { AgentInviteManager } = await import("../agents/managers/emailInvitesManager.js");
|
|
1007
1008
|
for (const email of validEmails) {
|
|
1008
1009
|
const token = crypto.randomBytes(20).toString("hex");
|
|
1009
1010
|
const invite = await models.Invite.create({
|
|
@@ -1013,30 +1014,8 @@ router.post("/:groupId/sendEmailInvitesForAnons", auth.can("edit group"), async
|
|
|
1013
1014
|
community_id: group.community_id,
|
|
1014
1015
|
from_user_id: req.user.id,
|
|
1015
1016
|
});
|
|
1016
|
-
const invite_link =
|
|
1017
|
-
|
|
1018
|
-
models.AcActivity.inviteCreated({
|
|
1019
|
-
email: email,
|
|
1020
|
-
user_id: null,
|
|
1021
|
-
sender_user_id: req.user.id,
|
|
1022
|
-
sender_name: req.user.name,
|
|
1023
|
-
group_id: group.id,
|
|
1024
|
-
community_id: group.community_id,
|
|
1025
|
-
domain_id: req.ypDomain.id,
|
|
1026
|
-
invite_id: invite.id,
|
|
1027
|
-
invite_link: invite_link,
|
|
1028
|
-
invite_type: models.Invite.INVITE_TO_COMMUNITY_AND_GROUP_AS_ANON,
|
|
1029
|
-
token: token,
|
|
1030
|
-
}, function (error) {
|
|
1031
|
-
if (error) {
|
|
1032
|
-
reject(error);
|
|
1033
|
-
}
|
|
1034
|
-
else {
|
|
1035
|
-
resolve();
|
|
1036
|
-
}
|
|
1037
|
-
});
|
|
1038
|
-
});
|
|
1039
|
-
await createActivityPromise;
|
|
1017
|
+
const invite_link = `https://app.${req.ypDomain.domain_name}/group/${group.id}?anonInvite=1&token=${token}&forAgentBundle=1`;
|
|
1018
|
+
await AgentInviteManager.sendInviteEmail(invite_link, req.body.agentRunId, group.id, req.user);
|
|
1040
1019
|
log.info("Invite Created", {
|
|
1041
1020
|
email,
|
|
1042
1021
|
inviteId: invite.id,
|