Skip to content
This repository was archived by the owner on Sep 29, 2025. It is now read-only.

Commit c734096

Browse files
mongodbenBen Perlmutter
andauthored
(EAI-626): Send slack message on user comment to feedback channel (#601)
* add braintrust to core * Update packages/mongodb-chatbot-server/src/routes/conversations/addMessageToConversation.ts * working server logging instrumentation * checkpoint * target more recent version to use es2023 * update lib to latest * update tracing * lets see * braintrust tracing in test * no trace in test * tracing tests * up timeout on flaky test * chatbot staging * add basic LLM as a judge tracing * tracing w/ LLM as a judge * add judge vars to drone * standardize custom tracing * additional clean up * remove unused import * clean up * implement NL feedback * add user message rating eval * remove unused tests * trace correct route * remove console.log * remove trace * working comment eval, mid refactor * comment sentiment eval * Get started * working, pretty slack integration * post to slack * add env vars --------- Co-authored-by: Ben Perlmutter <mongodben@mongodb.com>
1 parent d5a8e72 commit c734096

File tree

10 files changed

+954
-22
lines changed

10 files changed

+954
-22
lines changed

package-lock.json

Lines changed: 671 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/chatbot-server-mongodb-public/environments/production.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ env:
1616
JUDGE_LLM: "gpt-4o-mini"
1717
JUDGE_EMBEDDING_MODEL: "text-embedding-3-small"
1818
# TODO:(EAI-644) add prod tracing
19+
# SLACK_COMMENT_CONVERSATION_ID: "C07A68V63EH"
1920
# BRAINTRUST_CHATBOT_TRACING_PROJECT_NAME: "chatbot-responses-prod"
2021

2122
envSecrets:
@@ -24,6 +25,7 @@ envSecrets:
2425
OPENAI_API_KEY: docs-chatbot-prod
2526
# TODO:(EAI-644) add prod tracing
2627
# BRAINTRUST_TRACING_API_KEY: docs-chatbot-prod
28+
# SLACK_BOT_TOKEN: docs-chatbot-prod
2729

2830
ingress:
2931
enabled: true

packages/chatbot-server-mongodb-public/environments/staging.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ env:
1616
BRAINTRUST_CHATBOT_TRACING_PROJECT_NAME: "chatbot-responses-staging"
1717
JUDGE_LLM: "gpt-4o-mini"
1818
JUDGE_EMBEDDING_MODEL: "text-embedding-3-small"
19+
SLACK_COMMENT_CONVERSATION_ID: "C08AUU9M1AL"
1920

2021
envSecrets:
2122
MONGODB_CONNECTION_URI: docs-chatbot-staging
2223
OPENAI_ENDPOINT: docs-chatbot-staging
2324
OPENAI_API_KEY: docs-chatbot-staging
2425
BRAINTRUST_TRACING_API_KEY: docs-chatbot-staging
26+
SLACK_BOT_TOKEN: docs-chatbot-staging
2527

2628
ingress:
2729
enabled: true

packages/chatbot-server-mongodb-public/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"evaluate": "braintrust eval"
2727
},
2828
"dependencies": {
29+
"@slack/web-api": "^7.8.0",
2930
"common-tags": "^1.8.2",
3031
"cookie-parser": "^1.4.6",
3132
"dotenv": "^16.0.3",
@@ -34,6 +35,8 @@
3435
"mongodb-chatbot-verified-answers": "*",
3536
"mongodb-rag-core": "*",
3637
"pm2": "^5.3.0",
38+
"slack-block-builder": "^2.8.0",
39+
"slackify-markdown": "^4.4.0",
3740
"zod": "^3.23.8",
3841
"zod-to-json-schema": "^3.23.2"
3942
},

packages/chatbot-server-mongodb-public/src/config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ const llmAsAJudgeConfig = {
224224
},
225225
};
226226

227+
const { SLACK_BOT_TOKEN, SLACK_COMMENT_CONVERSATION_ID } = process.env;
228+
227229
export const config: AppConfig = {
228230
conversationsRouterConfig: {
229231
llm,
@@ -244,7 +246,15 @@ export const config: AppConfig = {
244246
rateMessageUpdateTrace: makeRateMessageUpdateTrace(llmAsAJudgeConfig),
245247
commentMessageUpdateTrace: makeCommentMessageUpdateTrace(
246248
openAiClient,
247-
JUDGE_LLM
249+
JUDGE_LLM,
250+
SLACK_BOT_TOKEN !== undefined &&
251+
SLACK_COMMENT_CONVERSATION_ID !== undefined
252+
? {
253+
token: SLACK_BOT_TOKEN,
254+
conversationId: SLACK_COMMENT_CONVERSATION_ID,
255+
llmAsAJudge: llmAsAJudgeConfig,
256+
}
257+
: undefined
248258
),
249259
generateUserPrompt,
250260
systemPrompt,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Message } from "mongodb-rag-core";
2+
import { ObjectId } from "mongodb-rag-core/mongodb";
3+
import { strict as assert } from "assert";
4+
5+
export function extractSampleMessages({
6+
messages,
7+
targetMessageId,
8+
maxNumMessagesBefore = 5,
9+
maxNumMessagesAfter = 2,
10+
}: {
11+
messages: Message[];
12+
targetMessageId: ObjectId;
13+
maxNumMessagesBefore?: number;
14+
maxNumMessagesAfter?: number;
15+
}) {
16+
const commentIdx = messages.findLastIndex((message) =>
17+
message.id.equals(targetMessageId)
18+
);
19+
assert(
20+
commentIdx !== -1,
21+
`Comment for message with ID ${targetMessageId.toHexString()} not found in messages with a comment.`
22+
);
23+
const sampleMessagesStartIndex = Math.max(
24+
0,
25+
commentIdx - maxNumMessagesBefore
26+
);
27+
const sampleMessagesEndIndex = Math.min(
28+
messages.length,
29+
commentIdx + maxNumMessagesAfter
30+
);
31+
const sampleMessages = messages.slice(
32+
sampleMessagesStartIndex,
33+
sampleMessagesEndIndex
34+
);
35+
const targetMessageIndex = sampleMessages.findIndex((message) =>
36+
message.id.equals(targetMessageId)
37+
);
38+
return {
39+
sampleMessages,
40+
targetMessageIndex,
41+
};
42+
}

packages/chatbot-server-mongodb-public/src/tracing/mongoDbChatbotCommentSentiment.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { AssistantMessage, DbMessage, Message } from "mongodb-rag-core";
1+
import { Message } from "mongodb-rag-core";
22
import { ObjectId } from "mongodb-rag-core/mongodb";
33
import { OpenAI } from "mongodb-rag-core/openai";
44
import { z } from "zod";
55
import { zodToJsonSchema } from "zod-to-json-schema";
6-
import { strict as assert } from "assert";
76
import { wrapTraced } from "mongodb-rag-core/braintrust";
7+
import { extractSampleMessages } from "./extractSampleMessages";
88

99
export interface CommentSentimentParams {
1010
judgeLlm: string;
@@ -104,29 +104,16 @@ function makeUserMessage(
104104
messages: Message[],
105105
messageWithCommentId: ObjectId
106106
): OpenAI.Chat.Completions.ChatCompletionUserMessageParam {
107-
const commentIdx = messages.findLastIndex(
108-
(message): message is DbMessage<AssistantMessage> =>
109-
message.id.equals(messageWithCommentId) && "userComment" in message
110-
);
111-
assert(
112-
commentIdx !== -1,
113-
`Comment for message with ID ${messageWithCommentId.toHexString()} not found in messages with a comment.`
114-
);
115-
const sampleMessagesStartIndex = Math.max(0, commentIdx - 5);
116-
const sampleMessagesEndIndex = Math.min(messages.length, commentIdx + 2);
117-
const sampleMessages = messages.slice(
118-
sampleMessagesStartIndex,
119-
sampleMessagesEndIndex
120-
);
121-
const sampleMessagesTargetCommentIdx = sampleMessages.findIndex((message) =>
122-
message.id.equals(messageWithCommentId)
123-
);
107+
const { sampleMessages, targetMessageIndex } = extractSampleMessages({
108+
messages,
109+
targetMessageId: messageWithCommentId,
110+
});
124111
let transcript = "Conversation Transcript:\n\n";
125112
sampleMessages.forEach((message, i) => {
126113
transcript += `${formatRole(message.role)}:\n ${message.content}\n`;
127114
if (message.role === "assistant" && typeof message.rating === "boolean") {
128115
transcript += `\nUser rating: ${message.rating}\n`;
129-
if (i === sampleMessagesTargetCommentIdx) {
116+
if (i === targetMessageIndex) {
130117
transcript += `User comment to analyze: ${message.userComment}\n`;
131118
}
132119
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ObjectId } from "mongodb-rag-core/mongodb";
2+
import { postCommentToSlack } from "./postCommentToSlack";
3+
import "dotenv/config";
4+
describe.skip("postCommentToSlack", () => {
5+
it("should post message to slack", async () => {
6+
const id = new ObjectId();
7+
await postCommentToSlack({
8+
slackToken: process.env.SLACK_BOT_TOKEN!,
9+
slackConversationId: process.env.SLACK_COMMENT_CONVERSATION_ID!,
10+
conversation: {
11+
_id: new ObjectId(),
12+
createdAt: new Date(),
13+
messages: [
14+
{
15+
role: "user",
16+
content: "hey",
17+
id: new ObjectId(),
18+
createdAt: new Date(),
19+
},
20+
{
21+
role: "assistant",
22+
content: "hello",
23+
rating: true,
24+
userComment: "good",
25+
id,
26+
createdAt: new Date(),
27+
references: [
28+
{
29+
title: "title",
30+
url: "https://example.com",
31+
},
32+
],
33+
},
34+
],
35+
},
36+
messageWithCommentId: id,
37+
llmAsAJudge: {
38+
judgeEmbeddingModel: process.env.JUDGE_EMBEDDING_MODEL!,
39+
judgeModel: process.env.JUDGE_LLM!,
40+
openAiConfig: {
41+
azureOpenAi: {
42+
apiKey: process.env.OPENAI_API_KEY!,
43+
endpoint: process.env.OPENAI_ENDPOINT!,
44+
apiVersion: process.env.OPENAI_API_VERSION!,
45+
},
46+
},
47+
},
48+
});
49+
});
50+
});
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { AssistantMessage, Conversation } from "mongodb-rag-core";
2+
import { ObjectId } from "mongodb-rag-core/mongodb";
3+
import { WebClient } from "@slack/web-api";
4+
import { Blocks, Message as BuilderMessage, Md } from "slack-block-builder";
5+
import { extractTracingData } from "./extractTracingData";
6+
import { strict as assert } from "assert";
7+
import slackify from "slackify-markdown";
8+
import { getLlmAsAJudgeScores, LlmAsAJudge } from "./getLlmAsAJudgeScores";
9+
10+
export interface PostCommentToSlackParams {
11+
slackToken: string;
12+
slackConversationId: string;
13+
conversation: Conversation;
14+
messageWithCommentId: ObjectId;
15+
llmAsAJudge: LlmAsAJudge;
16+
}
17+
export async function postCommentToSlack({
18+
slackToken,
19+
slackConversationId,
20+
conversation,
21+
messageWithCommentId,
22+
llmAsAJudge,
23+
}: PostCommentToSlackParams) {
24+
const client = new WebClient(slackToken);
25+
const builder = await makeSlackMessageText(
26+
conversation,
27+
messageWithCommentId,
28+
slackConversationId,
29+
llmAsAJudge
30+
);
31+
const res = await client.chat.postMessage({
32+
...builder,
33+
unfurl_links: false,
34+
unfurl_media: false,
35+
});
36+
return res;
37+
}
38+
39+
async function makeSlackMessageText(
40+
conversation: Conversation,
41+
messageWithCommentId: ObjectId,
42+
slackConversationId: string,
43+
llmAsAJudge: LlmAsAJudge
44+
) {
45+
const tracingData = extractTracingData(
46+
conversation.messages,
47+
messageWithCommentId
48+
);
49+
const {
50+
isVerifiedAnswer,
51+
llmDoesNotKnow,
52+
rejectQuery,
53+
tags,
54+
numRetrievedChunks,
55+
assistantMessage,
56+
userMessage,
57+
} = tracingData;
58+
59+
assert(assistantMessage, "Assistant message not found");
60+
assert(userMessage, "User message not found");
61+
const { rating, userComment } = extractFeedback(assistantMessage);
62+
assert(rating, "Rating not found");
63+
assert(userComment, "User comment not found");
64+
65+
const scores = await getLlmAsAJudgeScores(llmAsAJudge, tracingData);
66+
67+
const feedbackMd = `${Md.bold(
68+
rating === true ? "👍 Positive Feedback" : "👎 Negative Feedback"
69+
)}
70+
71+
${Md.bold("User Comment:")}
72+
${userComment}`;
73+
74+
const messagesMd = `${Md.blockquote(
75+
`${Md.bold(Md.emoji("bust_in_silhouette") + ` User`)}`
76+
)}
77+
78+
${slackify(fixNewLines(userMessage.content))}
79+
80+
${`${Md.blockquote(Md.bold(Md.emoji("robot_face") + ` Assistant`))}`}
81+
82+
${slackify(fixNewLines(assistantMessage.content))}`;
83+
84+
const referencesMd =
85+
Md.bold("References:") +
86+
"\n" +
87+
(assistantMessage.references && assistantMessage?.references.length > 0
88+
? `${assistantMessage.references
89+
.map((ref) => {
90+
return `${Md.listBullet(Md.link(ref.url, ref.title))}`;
91+
})
92+
.join("\n")}`
93+
: "No References");
94+
95+
const messageMetadataMd = `${Md.bold("Metadata:")}
96+
97+
${Md.listBullet(`Verified Answer: ${isVerifiedAnswer ? "Yes" : "No"}`)}
98+
${Md.listBullet(`LLM Does Not Know: ${llmDoesNotKnow ? "Yes" : "No"}`)}
99+
${Md.listBullet(`Rejected Query: ${rejectQuery ? "Yes" : "No"}`)}
100+
${Md.listBullet(`Number of Retrieved Chunks: ${numRetrievedChunks}`)}
101+
${Md.listBullet(`Tags: ${tags.map(Md.codeInline).join(", ")}`)}`;
102+
103+
const idMetadataMd = `Conversation ID: ${Md.codeInline(
104+
conversation._id.toHexString()
105+
)}
106+
Message ID/ Braintrust Trace ID: ${Md.codeInline(
107+
messageWithCommentId.toHexString()
108+
)}`;
109+
110+
const scoresMd = `${Md.bold("Scores:")}
111+
${
112+
scores
113+
? Object.entries(scores)
114+
.map(([label, score]) => {
115+
return `${Md.listBullet(`${Md.bold(label)}: ${score}`)}`;
116+
})
117+
.join("\n")
118+
: "No LLM-as-Judge Scores"
119+
}`;
120+
121+
return BuilderMessage({
122+
channel: slackConversationId,
123+
text: "User Feedback",
124+
})
125+
.blocks(
126+
Blocks.Section({ text: feedbackMd }),
127+
Blocks.Divider(),
128+
Blocks.Section({ text: messagesMd }),
129+
Blocks.Section({ text: referencesMd }),
130+
Blocks.Divider(),
131+
Blocks.Section({ text: scoresMd }),
132+
Blocks.Divider(),
133+
Blocks.Section({ text: messageMetadataMd }),
134+
Blocks.Divider(),
135+
Blocks.Section({ text: idMetadataMd })
136+
)
137+
.asUser()
138+
.buildToObject();
139+
}
140+
141+
function fixNewLines(text: string) {
142+
return text.replaceAll("\\n", "\n");
143+
}
144+
145+
function extractFeedback(assistantMessage: AssistantMessage) {
146+
return {
147+
rating: assistantMessage.rating,
148+
userComment: assistantMessage.userComment,
149+
};
150+
}

0 commit comments

Comments
 (0)