Skip to content

(EAI-1159) Implement middleware in new contentRouter #816

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: search_content_route
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9664693
Create MongoDbSearchResultsStore, add limit to DefaultFindContent and…
mmeigs Jul 2, 2025
f35d9a7
Implement saveSearchResult, create MongoDbSearchResultsStore.test.ts
mmeigs Jul 3, 2025
b07d9d9
lint, format
mmeigs Jul 3, 2025
3a1060b
Check entire returned document in MongoDbSearchResultsStore.test.ts
mmeigs Jul 3, 2025
f62e36c
Create ResultChunk type and zod check
mmeigs Jul 3, 2025
b6f9aec
Correct usage of limit in makeDefaultFindContent
mmeigs Jul 7, 2025
1c10dfc
PR feedback: cast badSearchResultRecord as any
mmeigs Jul 7, 2025
3a9b937
Starting structure of searchContent
mmeigs Jul 7, 2025
f479ea0
Use unknown instead of any for ResultChunk additional metadata
mmeigs Jul 8, 2025
fe1d015
Merge branch 'add_search_results_store' into expose_search_endpoint
mmeigs Jul 8, 2025
4df3c20
Add searchContent test file, broaden QueryFilter & MongoDbAtlasVector…
mmeigs Jul 8, 2025
baad5bd
Work on clarity of comments in contentRouter
mmeigs Jul 8, 2025
b3ad758
PR feedback: Combine describe blocks in MongoDbSearchResultsStore.tes…
mmeigs Jul 8, 2025
d2b9375
Merge branch 'add_search_results_store' into expose_search_endpoint
mmeigs Jul 8, 2025
370bd86
Use generics on middleware: requireRequestOrigin, requireValidIpAddress
mmeigs Jul 8, 2025
a9d1910
Structure out contentRouter test file
mmeigs Jul 8, 2025
dd3c316
Combine describes
mmeigs Jul 8, 2025
969e3b6
Merge branch 'add_search_results_store' into expose_search_endpoint
mmeigs Jul 8, 2025
5ebefb5
Clean
mmeigs Jul 8, 2025
647144f
makeFindContentWithMongoDbMetadata
mmeigs Jul 9, 2025
c65e8e6
config.test.ts
mmeigs Jul 9, 2025
a7919c1
Merge branch 'search_content_route' into expose_search_endpoint
mmeigs Jul 9, 2025
147aea0
Clean
mmeigs Jul 9, 2025
5dabbf6
PR feedback
mmeigs Jul 10, 2025
6f377f5
Correct types
mmeigs Jul 10, 2025
31710a4
Correct test
mmeigs Jul 10, 2025
310a607
Merge branch 'search_content_route' into expose_search_endpoint
mmeigs Jul 10, 2025
fd66498
Fix test return type
mmeigs Jul 11, 2025
2f1df27
lint
mmeigs Jul 11, 2025
8efc4c6
Revert move of classifyMongoDbProgrammingLanguageAndProduct, jest nee…
mmeigs Jul 11, 2025
2c9a21b
Created addCustomData.ts, generics, use in both contentRouter and con…
mmeigs Jul 11, 2025
ff75594
Clean
mmeigs Jul 11, 2025
19ac7d4
Remove unnecessary tests and comments
mmeigs Jul 11, 2025
3446476
Merge branch 'expose_search_endpoint' into implement_search_middleware
mmeigs Jul 11, 2025
cadae06
Added custom middleware to contentRouter, used in searchContent route…
mmeigs Jul 14, 2025
c8c38ca
Merge branch 'search_content_route' into implement_search_middleware
mmeigs Jul 14, 2025
25e2c5c
Add customData to db...
mmeigs Jul 14, 2025
db98f56
Clean: allow undefined customData value
mmeigs Jul 14, 2025
d435d8f
Alter types for createConversationsMiddlewareReq
mmeigs Jul 14, 2025
3a58520
Rerun tests
mmeigs Jul 15, 2025
896194c
Merge branch 'search_content_route' into implement_search_middleware
mmeigs Jul 15, 2025
2812d4e
PR feedback
mmeigs Jul 16, 2025
9e9c44d
Add Locals types to middleware invocations
mmeigs Jul 16, 2025
4db7567
Lint, fix trace name, remove unnecessary import
mmeigs Jul 16, 2025
690b895
(EAI-972) Add extra braintrust tracing to searchContent route (#822)
mmeigs Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions packages/chatbot-server-mongodb-public/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {
requireRequestOrigin,
AddCustomDataFunc,
makeDefaultFindVerifiedAnswer,
defaultCreateConversationCustomData,
defaultAddMessageToConversationCustomData,
makeVerifiedAnswerGenerateResponse,
addDefaultCustomData,
ConversationsRouterLocals,
SearchContentRouterLocals,
} from "mongodb-chatbot-server";
import cookieParser from "cookie-parser";
import { blockGetRequests } from "./middleware/blockGetRequests";
Expand Down Expand Up @@ -269,7 +270,7 @@ export const generateResponse = wrapTraced(

export const createConversationCustomDataWithAuthUser: AddCustomDataFunc =
async (req, res) => {
const customData = await defaultCreateConversationCustomData(req, res);
const customData = await addDefaultCustomData(req, res);
if (req.cookies.auth_user) {
customData.authUser = req.cookies.auth_user;
}
Expand Down Expand Up @@ -318,12 +319,13 @@ export const config: AppConfig = {
classifierModel: languageModel,
}),
searchResultsStore,
middleware: [requireValidIpAddress<SearchContentRouterLocals>(), requireRequestOrigin<SearchContentRouterLocals>()],
},
conversationsRouterConfig: {
middleware: [
blockGetRequests,
requireValidIpAddress(),
requireRequestOrigin(),
requireValidIpAddress<ConversationsRouterLocals>(),
requireRequestOrigin<ConversationsRouterLocals>(),
useSegmentIds(),
redactConnectionUri(),
cookieParser(),
Expand All @@ -332,10 +334,7 @@ export const config: AppConfig = {
? createConversationCustomDataWithAuthUser
: undefined,
addMessageToConversationCustomData: async (req, res) => {
const defaultCustomData = await defaultAddMessageToConversationCustomData(
req,
res
);
const defaultCustomData = await addDefaultCustomData(req, res);
const customData = {
...defaultCustomData,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const makeFindContentWithMongoDbMetadata = ({
return res;
},
{
name: "makeFindContentWithMongoDbMetadata",
name: "findContentWithMongoDbMetadata",
}
);
return wrappedFindContent;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { RequestHandler } from "express";
import { ParamsDictionary } from "express-serve-static-core";
import { ParsedQs } from "qs";
import { getRequestId, logRequest, sendErrorResponse } from "../utils";
import { ConversationsMiddleware } from "../routes/conversations/conversationsRouter";

export const CUSTOM_REQUEST_ORIGIN_HEADER = "X-Request-Origin";

export function requireRequestOrigin(): ConversationsMiddleware {
export function requireRequestOrigin<
Locals extends Record<string, any>
>(): RequestHandler<ParamsDictionary, unknown, unknown, ParsedQs, Locals> {
return (req, res, next) => {
const reqId = getRequestId(req);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createConversationsMiddlewareReq,
createConversationsMiddlewareRes,
} from "../test/middlewareTestHelpers";
import { ConversationsRouterLocals } from "../routes";

const baseReq = {
body: { message: "Hello, world!" },
Expand All @@ -18,7 +19,7 @@ describe("requireValidIpAddress", () => {
const res = createConversationsMiddlewareRes();
const next = jest.fn();

const middleware = requireValidIpAddress();
const middleware = requireValidIpAddress<ConversationsRouterLocals>();
req.body = baseReq.body;
req.params = baseReq.params;
req.query = baseReq.query;
Expand All @@ -39,7 +40,7 @@ describe("requireValidIpAddress", () => {
const next = jest.fn();

const invalidIpAddress = "not-an-ip-address";
const middleware = requireValidIpAddress();
const middleware = requireValidIpAddress<ConversationsRouterLocals>();
req.body = baseReq.body;
req.params = baseReq.params;
req.query = baseReq.query;
Expand All @@ -59,7 +60,7 @@ describe("requireValidIpAddress", () => {
const res = createConversationsMiddlewareRes();
const next = jest.fn();

const middleware = requireValidIpAddress();
const middleware = requireValidIpAddress<ConversationsRouterLocals>();
req.body = baseReq.body;
req.params = baseReq.params;
req.query = baseReq.query;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { RequestHandler } from "express";
import { ParamsDictionary } from "express-serve-static-core";
import { ParsedQs } from "qs";
import { getRequestId, logRequest, sendErrorResponse } from "../utils";
import { ConversationsMiddleware } from "../routes/conversations/conversationsRouter";
import { isValidIp } from "../routes/conversations/utils";

export function requireValidIpAddress(): ConversationsMiddleware {
export function requireValidIpAddress<
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to chris' comment, if the generics aren't ever being used here (which they dont seem to be), lets remove them

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the usage of the generics to all the invocations of these middlewares! Otherwise, the function definitions would need an unknown for the locals values.

Locals extends Record<string, any>
>(): RequestHandler<ParamsDictionary, unknown, unknown, ParsedQs, Locals> {
return (req, res, next) => {
const reqId = getRequestId(req);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { getRequestId, logRequest, sendErrorResponse } from "../utils";

export const SomeExpressRequest = z.object({
headers: z.object({}).optional(),
params: z.object({}).optional(),
params: z.object({}),
query: z.object({}).optional(),
body: z.object({}).optional(),
});

function generateZodErrorMessage(error: ZodError) {
export function generateZodErrorMessage(error: ZodError) {
return generateErrorMessage(error.issues, {
delimiter: {
error: "\n",
Expand Down
94 changes: 94 additions & 0 deletions packages/mongodb-chatbot-server/src/processors/addCustomData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Request, Response } from "express";

export type RequestCustomData = Record<string, unknown> | undefined;

/**
Function to add custom data to the {@link Conversation} or content search Request persisted to the database.
Has access to the Express.js request and response plus the values
from the {@link Response.locals} object.
*/
export type AddCustomDataFunc = (
request: Request,
response: Response
) => Promise<RequestCustomData>;

const addIpToCustomData: AddCustomDataFunc = async (req) =>
req.ip
? {
ip: req.ip,
}
: undefined;

const addOriginToCustomData: AddCustomDataFunc = async (_, res) =>
res.locals.customData.origin
? {
origin: res.locals.customData.origin,
}
: undefined;

export const originCodes = [
"LEARN",
"DEVELOPER",
"DOCS",
"DOTCOM",
"GEMINI_CODE_ASSIST",
"VSCODE",
"OTHER",
] as const;

export type OriginCode = (typeof originCodes)[number];

interface OriginRule {
regex: RegExp;
code: OriginCode;
}

const ORIGIN_RULES: OriginRule[] = [
{ regex: /learn\.mongodb\.com/, code: "LEARN" },
{ regex: /mongodb\.com\/developer/, code: "DEVELOPER" },
{ regex: /mongodb\.com\/docs/, code: "DOCS" },
{ regex: /mongodb\.com\//, code: "DOTCOM" },
{ regex: /google-gemini-code-assist/, code: "GEMINI_CODE_ASSIST" },
{ regex: /vscode-mongodb-copilot/, code: "VSCODE" },
];

function getOriginCode(origin: string): OriginCode {
for (const rule of ORIGIN_RULES) {
if (rule.regex.test(origin)) {
return rule.code;
}
}
return "OTHER";
}

const addOriginCodeToCustomData: AddCustomDataFunc = async (_, res) => {
const origin = res.locals.customData.origin;
return typeof origin === "string" && origin.length > 0
? {
originCode: getOriginCode(origin),
}
: undefined;
};

const addUserAgentToCustomData: AddCustomDataFunc = async (req) =>
req.headers["user-agent"]
? {
userAgent: req.headers["user-agent"],
}
: undefined;

export type AddDefinedCustomDataFunc = (
...args: Parameters<AddCustomDataFunc>
) => Promise<Exclude<RequestCustomData, undefined>>;

export const addDefaultCustomData: AddDefinedCustomDataFunc = async (
req,
res
) => {
return {
...(await addIpToCustomData(req, res)),
...(await addOriginToCustomData(req, res)),
...(await addOriginCodeToCustomData(req, res)),
...(await addUserAgentToCustomData(req, res)),
};
};
1 change: 1 addition & 0 deletions packages/mongodb-chatbot-server/src/processors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from "./InputGuardrail";
export * from "./makeVerifiedAnswerGenerateResponse";
export * from "./includeChunksForMaxTokensPossible";
export * from "./GenerateResponse";
export * from "./addCustomData";
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Express } from "express";
import request from "supertest";
import { makeTestApp } from "../../test/testHelpers";
import type { MakeContentRouterParams } from "./contentRouter";
import type {
FindContentFunc,
MongoDbSearchResultsStore,
} from "mongodb-rag-core";
import type {
MakeContentRouterParams,
SearchContentMiddleware,
} from "./contentRouter";
import { makeTestApp } from "../../test/testHelpers";

// Minimal in-memory mock for SearchResultsStore for testing purposes
const mockSearchResultsStore: MongoDbSearchResultsStore = {
Expand All @@ -23,8 +27,7 @@ const findContentMock = jest.fn().mockResolvedValue({
queryEmbedding: [],
}) satisfies FindContentFunc;

// Helper to build contentRouterConfig for the test app
function makeContentRouterConfig(
function makeMockContentRouterConfig(
overrides: Partial<MakeContentRouterParams> = {}
) {
return {
Expand All @@ -35,20 +38,57 @@ function makeContentRouterConfig(
}

describe("contentRouter", () => {
const ipAddress = "127.0.0.1";
const searchEndpoint = "/api/v1/content/search";

it("should call custom middleware if provided", async () => {
const mockMiddleware = jest.fn((_req, _res, next) => next());
const { app, origin } = await makeTestApp({
contentRouterConfig: makeContentRouterConfig({
contentRouterConfig: makeMockContentRouterConfig({
middleware: [mockMiddleware],
}),
});
await request(app)
.post(searchEndpoint)
.set("req-id", "test-req-id")
.set("Origin", origin)
.send({ query: "mongodb" });
await createContentReq({ app, origin, query: "mongodb" });
expect(mockMiddleware).toHaveBeenCalled();
});

test("should use route middleware customData", async () => {
const middleware1: SearchContentMiddleware = (_, res, next) => {
res.locals.customData.middleware1 = true;
next();
};
let called = false;
const middleware2: SearchContentMiddleware = (_, res, next) => {
expect(res.locals.customData.middleware1).toBe(true);
called = true;
next();
};
const { app, origin } = await makeTestApp({
contentRouterConfig: makeMockContentRouterConfig({
middleware: [middleware1, middleware2],
}),
});
await createContentReq({ app, origin, query: "What is aggregation?" });
expect(called).toBe(true);
});

/**
Helper function to create a new content request
*/
async function createContentReq({
app,
origin,
query,
}: {
app: Express;
origin: string;
query: string;
}) {
const createContentRes = await request(app)
.post(searchEndpoint)
.set("X-FORWARDED-FOR", ipAddress)
.set("Origin", origin)
.send({ query });
return createContentRes;
}
});
Loading