Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1295d51
add feature flag
mabaasit Nov 25, 2025
7a44de4
migrate prompts to compass
mabaasit Nov 25, 2025
2e2defe
Merge branch 'main' into COMPASS-10082-add-mms-prompts-and-feature-flag
mabaasit Nov 25, 2025
d524682
co-pilot feedback
mabaasit Nov 25, 2025
f9212fb
Merge branch 'COMPASS-10082-add-mms-prompts-and-feature-flag' of http…
mabaasit Nov 25, 2025
c96161b
clean up
mabaasit Dec 1, 2025
f499026
Merge branch 'main' into COMPASS-10082-add-mms-prompts-and-feature-flag
mabaasit Dec 1, 2025
f4d05a7
use edu api for gen ai
mabaasit Dec 1, 2025
1763531
clean up a bit
mabaasit Dec 1, 2025
d5e2c84
fix url for test and ensure aggregations have content
mabaasit Dec 2, 2025
0781580
tests
mabaasit Dec 3, 2025
058e950
fix error handling
mabaasit Dec 3, 2025
7d54d4d
clean up transport
mabaasit Dec 3, 2025
0aa7ff5
Merge branch 'main' of https://github.com/mongodb-js/compass into COM…
mabaasit Dec 3, 2025
309335a
changes in field name
mabaasit Dec 3, 2025
feacddc
use query parser
mabaasit Dec 3, 2025
c321983
fix check
mabaasit Dec 3, 2025
f5a34df
fix test
mabaasit Dec 3, 2025
91b17d5
clean up
mabaasit Dec 3, 2025
ea4dfdf
copilot feedback
mabaasit Dec 3, 2025
f8a4c43
fix log id
mabaasit Dec 3, 2025
8190219
fix cors issue on e2e tests
mabaasit Dec 4, 2025
c444cb9
more tests
mabaasit Dec 4, 2025
6eec8db
add type
mabaasit Dec 4, 2025
25c8089
wip
mabaasit Dec 8, 2025
9b4c9b7
fix alltext
mabaasit Dec 8, 2025
5e6d537
make expected output xml string
mabaasit Dec 9, 2025
d1b0b51
add fixtures and run gen ai eval tests
mabaasit Dec 10, 2025
1b7343f
ts fixes
mabaasit Dec 10, 2025
a1b38c5
reformat
mabaasit Dec 10, 2025
79e9910
clean up scorer
mabaasit Dec 10, 2025
821917d
Merge branch 'main' of https://github.com/mongodb-js/compass into eva…
mabaasit Dec 11, 2025
3bc7295
bootstrap
mabaasit Dec 11, 2025
eb49493
remove extra files
mabaasit Dec 11, 2025
fa1cf6f
Merge branch 'main' of https://github.com/mongodb-js/compass into eva…
mabaasit Dec 15, 2025
edc5e73
bootstrap
mabaasit Dec 15, 2025
718e8b7
fix prompts and data
mabaasit Dec 15, 2025
5244598
Merge branch 'main' of https://github.com/mongodb-js/compass into eva…
mabaasit Dec 15, 2025
534b0d8
copilot review
mabaasit Dec 15, 2025
d0568ff
reduce num of docs
mabaasit Dec 16, 2025
40ad7c9
reduce fixtures
mabaasit Dec 16, 2025
48b38ce
Merge branch 'main' of https://github.com/mongodb-js/compass into eva…
mabaasit Dec 16, 2025
6153f52
check fix
mabaasit Dec 16, 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
Prev Previous commit
Next Next commit
more tests
  • Loading branch information
mabaasit committed Dec 4, 2025
commit c444cb9fe1b0e7dbbfeb8a54b83d03f19d3e0f7b
353 changes: 353 additions & 0 deletions packages/compass-generative-ai/src/atlas-ai-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,4 +737,357 @@ describe('AtlasAiService', function () {
});
});
}

describe('with chatbot api', function () {
describe('getQueryFromUserInput and getAggregationFromUserInput', function () {
type Chunk = { type: 'text' | 'error'; content: string };
let atlasAiService: AtlasAiService;
const mockConnectionInfo = getMockConnectionInfo();

function streamChunkResponse(
readableStreamController: ReadableStreamController<any>,
chunks: Chunk[]
) {
const responseId = `resp_${Date.now()}`;
const itemId = `item_${Date.now()}`;
let sequenceNumber = 0;

const encoder = new TextEncoder();

// openai response format:
// https://github.com/vercel/ai/blob/811119c1808d7b62a4857bcad42353808cdba17c/packages/openai/src/responses/openai-responses-api.ts#L322

// Send response.created event
readableStreamController.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: 'response.created',
response: {
id: responseId,
object: 'realtime.response',
status: 'in_progress',
output: [],
usage: {
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
},
},
sequence_number: sequenceNumber++,
})}\n\n`
)
);

// Send output_item.added event
readableStreamController.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: 'response.output_item.added',
response_id: responseId,
output_index: 0,
item: {
id: itemId,
object: 'realtime.item',
type: 'message',
role: 'assistant',
content: [],
},
sequence_number: sequenceNumber++,
})}\n\n`
)
);

for (const chunk of chunks) {
if (chunk.type === 'error') {
readableStreamController.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: `error`,
response_id: responseId,
item_id: itemId,
output_index: 0,
error: {
type: 'model_error',
code: 'model_error',
message: chunk.content,
},
sequence_number: sequenceNumber++,
})}\n\n`
)
);
} else {
readableStreamController.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: 'response.output_text.delta',
response_id: responseId,
item_id: itemId,
output_index: 0,
delta: chunk.content,
sequence_number: sequenceNumber++,
})}\n\n`
)
);
}
}

const content = chunks
.filter((c) => c.type === 'text')
.map((c) => c.content)
.join('');

// Send output_item.done event
readableStreamController.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: 'response.output_item.done',
response_id: responseId,
output_index: 0,
item: {
id: itemId,
object: 'realtime.item',
type: 'message',
role: 'assistant',
content: [
{
type: 'text',
text: content,
},
],
},
sequence_number: sequenceNumber++,
})}\n\n`
)
);

// Send response.completed event
const tokenCount = Math.ceil(content.length / 4); // assume 4 chars per token
readableStreamController.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: 'response.completed',
response: {
id: responseId,
object: 'realtime.response',
status: 'completed',
output: [
{
id: itemId,
object: 'realtime.item',
type: 'message',
role: 'assistant',
content: [
{
type: 'text',
text: content,
},
],
},
],
usage: {
input_tokens: 10,
output_tokens: tokenCount,
total_tokens: 10 + tokenCount,
},
},
sequence_number: sequenceNumber++,
})}\n\n`
)
);
}

function streamableFetchMock(chunks: Chunk[]) {
const readableStream = new ReadableStream({
start(controller) {
streamChunkResponse(controller, chunks);
controller.close();
},
});
return new Response(readableStream, {
headers: { 'Content-Type': 'text/event-stream' },
});
}

beforeEach(async function () {
const mockAtlasService = new MockAtlasService();
await preferences.savePreferences({
enableChatbotEndpointForGenAI: true,
});
atlasAiService = new AtlasAiService({
apiURLPreset: 'cloud',
atlasService: mockAtlasService as any,
preferences,
logger: createNoopLogger(),
});
// Enable the AI feature
const fetchStub = sandbox.stub().resolves(
makeResponse({
features: {
GEN_AI_COMPASS: {
enabled: true,
},
},
})
);
global.fetch = fetchStub;
await atlasAiService['setupAIAccess']();
});

after(function () {
global.fetch = initialFetch;
});

const testCases = [
{
functionName: 'getQueryFromUserInput',
successResponse: {
request: [
{ type: 'text', content: 'Hello' },
{ type: 'text', content: ' world' },
{
type: 'text',
content: '. This is some non relevant text in the output',
},
{ type: 'text', content: '<filter>{test: ' },
{ type: 'text', content: '"pineapple"' },
{ type: 'text', content: '}</filter>' },
] as Chunk[],
response: {
content: {
query: {
filter: "{test:'pineapple'}",
project: null,
sort: null,
skip: null,
limit: null,
},
},
},
},
invalidModelResponse: {
request: [
{ type: 'text', content: 'Hello' },
{ type: 'text', content: ' world.' },
{ type: 'text', content: '<filter>{test: ' },
{ type: 'text', content: '"pineapple"' },
{ type: 'text', content: '}</filter>' },
{ type: 'error', content: 'Model crashed!' },
] as Chunk[],
errorMessage: 'Model crashed!',
},
},
{
functionName: 'getAggregationFromUserInput',
successResponse: {
request: [
{ type: 'text', content: 'Hello' },
{ type: 'text', content: ' world' },
{
type: 'text',
content: '. This is some non relevant text in the output',
},
{ type: 'text', content: '<aggregation>[{$count: ' },
{ type: 'text', content: '"pineapple"' },
{ type: 'text', content: '}]</aggregation>' },
] as Chunk[],
response: {
content: {
aggregation: {
pipeline: "[{$count:'pineapple'}]",
},
},
},
},
invalidModelResponse: {
request: [
{ type: 'text', content: 'Hello' },
{ type: 'text', content: ' world.' },
{ type: 'text', content: '<aggregation>[{test: ' },
{ type: 'text', content: '"pineapple"' },
{ type: 'text', content: '}]</aggregation>' },
{ type: 'error', content: 'Model crashed!' },
] as Chunk[],
errorMessage: 'Model crashed!',
},
},
] as const;

for (const {
functionName,
successResponse,
invalidModelResponse,
} of testCases) {
describe(functionName, function () {
it('makes a post request with the user input to the endpoint in the environment', async function () {
const fetchStub = sandbox
.stub()
.resolves(streamableFetchMock(successResponse.request));
global.fetch = fetchStub;

const input = {
userInput: 'test',
signal: new AbortController().signal,
collectionName: 'jam',
databaseName: 'peanut',
schema: { _id: { types: [{ bsonType: 'ObjectId' }] } },
sampleDocuments: [
{ _id: new ObjectId('642d766b7300158b1f22e972') },
],
requestId: 'abc',
};

const res = await atlasAiService[functionName](
input as any,
mockConnectionInfo
);

expect(fetchStub).to.have.been.calledOnce;

const { args } = fetchStub.firstCall;
const requestBody = JSON.parse(args[1].body as string);

expect(requestBody.model).to.equal('mongodb-chat-latest');
expect(requestBody.store).to.equal(false);
expect(requestBody.instructions).to.be.a('string');
expect(requestBody.input).to.be.an('array');

const { role, content } = requestBody.input[0];
expect(role).to.equal('user');
expect(content[0].text).to.include(
`Database name: "${input.databaseName}"`
);
expect(content[0].text).to.include(
`Collection name: "${input.collectionName}"`
);
expect(res).to.deep.eq(successResponse.response);
});

it('should throw an error when the stream contains an error chunk', async function () {
const fetchStub = sandbox
.stub()
.resolves(streamableFetchMock(invalidModelResponse.request));
global.fetch = fetchStub;

try {
await atlasAiService[functionName](
{
userInput: 'test',
collectionName: 'test',
databaseName: 'peanut',
requestId: 'abc',
signal: new AbortController().signal,
},
mockConnectionInfo
);
expect.fail(`Expected ${functionName} to throw`);
} catch (err) {
expect((err as Error).message).to.match(
new RegExp(invalidModelResponse.errorMessage, 'i')
);
}
});
});
}
});
});
});
Loading
Loading