Skip to content

Commit 2a5f7cd

Browse files
committed
chore: basic structure
1 parent 8c66df9 commit 2a5f7cd

File tree

7 files changed

+191
-42
lines changed

7 files changed

+191
-42
lines changed

snippets/ai/auth/atlas-ai-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type AIFeatureEnablement = {
3535
};
3636
};
3737

38-
type AIQuery = {
38+
export type AIQuery = {
3939
content: {
4040
query: Record<
4141
'filter' | 'project' | 'collation' | 'sort' | 'skip' | 'limit',

snippets/ai/auth/atlas-service.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { log } from '../logger';
12
import { AuthService } from './auth-service';
23
import type { AtlasServiceConfig } from './util';
34
import { throwIfAborted } from './util';
@@ -62,13 +63,13 @@ export class AtlasService {
6263
throw new Error('Network traffic is not allowed');
6364
}
6465
throwIfAborted(init?.signal as AbortSignal);
65-
const authHeaders = this.authService.getAuthHeaders();
66+
const authHeaders = await this.authService.getAuthHeaders();
6667
const finalHeaders = {
6768
...authHeaders,
6869
...this.options?.defaultHeaders,
6970
...init?.headers,
7071
};
71-
console.log('AtlasService: Making a fetch', {
72+
log.debug('AtlasService: Making a fetch', {
7273
url,
7374
headers: finalHeaders,
7475
method: init?.method || 'GET',
@@ -82,7 +83,7 @@ export class AtlasService {
8283
res.headers.forEach((value, key) => {
8384
responseHeadersObj[key] = value;
8485
});
85-
console.log('AtlasService: Received API response', {
86+
log.debug('AtlasService: Received API response', {
8687
url,
8788
status: res.status,
8889
statusText: res.statusText,
@@ -91,7 +92,7 @@ export class AtlasService {
9192
await throwIfNotOk(res);
9293
return res;
9394
} catch (err) {
94-
console.log('AtlasService: Fetch errored', {
95+
log.error('AtlasService: Fetch errored', {
9596
url,
9697
error: err instanceof Error ? err.message : err,
9798
stack: err instanceof Error ? err.stack : undefined,
@@ -104,7 +105,7 @@ export class AtlasService {
104105
init?: RequestInit,
105106
): Promise<Response> {
106107
const authHeaders = await this.authService.getAuthHeaders();
107-
console.log('AtlasService: Authenticated fetch', { url, authHeaders });
108+
log.debug('AtlasService: Authenticated fetch', { url, authHeaders });
108109
const res = await this.fetch(url, {
109110
...init,
110111
headers: {

snippets/ai/auth/auth-service.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,11 @@ import { EventEmitter } from 'events';
66
import { createMongoDBOIDCPlugin } from '@mongodb-js/oidc-plugin';
77
import { oidcServerRequestHandler } from '@mongodb-js/devtools-connect';
88
import { Server } from 'http';
9-
10-
class OIDCLogger extends EventEmitter {
11-
debug(...args: unknown[]) {
12-
console.debug(...args);
13-
this.emit('debug', ...args);
14-
}
15-
16-
info(...args: unknown[]) {
17-
console.info(...args);
18-
this.emit('info', ...args);
19-
}
20-
21-
error(...args: unknown[]) {
22-
console.error(...args);
23-
this.emit('error', ...args);
24-
}
25-
}
9+
import { log } from '../logger';
2610

2711
const redirectRequestHandler = oidcServerRequestHandler.bind(null, {
28-
productName: 'Compass',
29-
productDocsLink: 'https://www.mongodb.com/docs/compass',
12+
productName: 'mongosh AI Suite',
13+
productDocsLink: 'https://www.mongodb.com/docs/mongosh',
3014
});
3115

3216
export class AuthService extends AtlasAuthService {
@@ -51,7 +35,7 @@ export class AuthService extends AtlasAuthService {
5135
const { openBrowser, ...atlasConfig } = config;
5236
this.config = atlasConfig;
5337
this.openBrowser = openBrowser;
54-
console.log('Initializing AuthService with config:', {
38+
log.debug('Initializing AuthService with config:', {
5539
...this.config,
5640
});
5741

@@ -73,7 +57,7 @@ export class AuthService extends AtlasAuthService {
7357
await this.openBrowser(url);
7458
},
7559
allowedFlows: ['auth-code', 'device-auth'],
76-
logger: new OIDCLogger(),
60+
logger: log,
7761
});
7862
}
7963

@@ -148,7 +132,7 @@ export class AuthService extends AtlasAuthService {
148132
)
149133
).accessToken;
150134

151-
console.log('token', this.token);
135+
log.debug('token', this.token);
152136
return {
153137
primaryEmail: 'test@example.com',
154138
sub: 'test',

snippets/ai/decorators.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { output } from "./helpers";
2+
import { createLoadingAnimation } from "./helpers";
3+
4+
export function aiCommand<T extends Function>(
5+
value: T,
6+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7+
context: ClassMethodDecoratorContext
8+
): T & { isDirectShellCommand: true } {
9+
const wrappedFunction = function(this: any, ...args: any[]) {
10+
// Combine all arguments into a single string
11+
const combinedString = args.join(' ');
12+
// Call the original function with the combined string
13+
return value.call(this, combinedString);
14+
} as unknown as T; // Cast the wrapped function to match the original type
15+
return Object.assign(wrappedFunction, { isDirectShellCommand: true } as const);
16+
}
17+
18+
export function withLoadingAnimation(message: string = 'Loading...') {
19+
return function<T extends Function>(
20+
value: T,
21+
context: ClassMethodDecoratorContext
22+
): T {
23+
const wrappedFunction = async function(this: any, ...args: any[]) {
24+
const signal = AbortSignal.timeout(10000);
25+
const loadingAnimation = createLoadingAnimation({signal, message});
26+
loadingAnimation.start();
27+
28+
try {
29+
const result = await value.call(this, ...args);
30+
return result;
31+
} finally {
32+
loadingAnimation.stop();
33+
}
34+
} as unknown as T;
35+
return wrappedFunction;
36+
};
37+
}

snippets/ai/helpers.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { AIQuery } from "./auth/atlas-ai-service";
2+
3+
export function output(text: string) {
4+
process.stdout.write(`${text}`);
5+
}
6+
7+
export function setInput(text: string) {
8+
process.stdin.unshift(text);
9+
}
10+
export function createLoadingAnimation({signal, message = 'Loading'}: {signal: AbortSignal, message?: string}): {
11+
start: (message?: string) => void;
12+
stop: () => void;
13+
} {
14+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
15+
let i = 0;
16+
let interval: NodeJS.Timeout | null = null;
17+
18+
return {
19+
start() {
20+
interval = setInterval(() => {
21+
const frame = frames[i = ++i % frames.length];
22+
process.stdout.write(`\r${frame} ${message}`);
23+
}, 80);
24+
25+
signal.addEventListener('abort', () => {
26+
if (interval) {
27+
clearInterval(interval);
28+
process.stdout.write('\r\x1b[K'); // Clear the line
29+
}
30+
}, { once: true });
31+
},
32+
stop() {
33+
if (interval) {
34+
clearInterval(interval);
35+
process.stdout.write('\r\x1b[K'); // Clear the line
36+
}
37+
}
38+
};
39+
}
40+
41+
export class MongoshCommandBuilder {
42+
createMongoShellQuery(params: AIQuery['content']): string {
43+
const {filter, project, collation, sort, skip, limit} = params.query;
44+
45+
return `db.collection.find(
46+
${filter},
47+
${project ? `{ projection: ${project} }` : '{}'}
48+
)${collation ? `.collation(${collation})` : ''}${sort ? `.sort(${sort})` : ''}${skip ? `.skip(${skip})` : ''}${limit ? `.limit(${limit})` : ''}`
49+
};
50+
}

snippets/ai/index.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { AuthService } from './auth/auth-service';
22
import { AtlasService } from './auth/atlas-service';
33
import { AtlasAiService } from './auth/atlas-ai-service';
44
import { config } from './auth/util';
5+
import { createLoadingAnimation, MongoshCommandBuilder, output, setInput } from './helpers';
56
import open from 'open';
7+
import { aiCommand, withLoadingAnimation } from './decorators';
68

79
const authService = new AuthService({
810
...config['atlas'],
911
openBrowser: async (url: string) => {
10-
console.log('\nOpening authentication page in your default browser...');
12+
output('Opening authentication page in your default browser...');
1113
await open(url);
1214
},
1315
});
@@ -21,18 +23,53 @@ const aiService = new AtlasAiService({
2123
apiURLPreset: 'admin-api',
2224
});
2325

24-
module.exports = (globalThis: any) => {
25-
globalThis.ai = {
26-
login: async () => {
27-
await authService.signIn();
28-
},
29-
explain: async (code: string) => {
26+
const mongoshCommandBuilder = new MongoshCommandBuilder();
27+
28+
class AI {
29+
constructor(private readonly context: any, private readonly aiService: AtlasAiService) {
30+
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
31+
.filter(name => {
32+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), name);
33+
return descriptor && typeof descriptor.value === 'function' && name !== 'constructor';
34+
});
35+
console.log('Class methods:', methods);
36+
37+
// for all methods, wrap them with the wrapFunction method
38+
for (const methodName of methods) {
39+
const method = (this as any)[methodName];
40+
if (typeof method === 'function' && method.isDirectShellCommand) {
41+
this.wrapFunction(methodName, method);
42+
}
43+
}
44+
}
45+
46+
wrapFunction(name: string, fn: Function) {
47+
const wrapperFn = (...args: string[]) => {
48+
return Object.assign(fn(...args), {
49+
[Symbol.for('@@mongosh.syntheticPromise')]: true,
50+
});
51+
};
52+
wrapperFn.isDirectShellCommand = true;
53+
wrapperFn.returnsPromise = true;
54+
55+
const instanceState = this.context.db._mongo._instanceState;
56+
57+
(instanceState as any).shellApi[`ai.${name}`] = (instanceState as any).context[`ai.${name}`] = wrapperFn;
58+
instanceState.registerPlugin(this);
59+
}
60+
61+
@aiCommand
62+
async query(code: string) {
63+
const signal = AbortSignal.timeout(10000);
64+
const loadingAnimation = createLoadingAnimation({signal, message: 'Generating query...'});
65+
loadingAnimation.start();
66+
3067
const result = await aiService.getQueryFromUserInput(
3168
{
3269
userInput: code,
3370
databaseName: 'test',
3471
collectionName: 'test',
35-
signal: AbortSignal.timeout(10000),
72+
signal,
3673
requestId: 'test',
3774
},
3875
{
@@ -42,10 +79,22 @@ module.exports = (globalThis: any) => {
4279
id: '1234',
4380
},
4481
);
45-
return JSON.stringify(result.content.query);
46-
},
47-
ask: async (question: string) => {
48-
return 'test';
49-
},
50-
};
82+
loadingAnimation.stop();
83+
84+
const query = mongoshCommandBuilder.createMongoShellQuery(result.content);
85+
setInput(query);
86+
}
87+
88+
@aiCommand
89+
@withLoadingAnimation('Generating help...')
90+
async help(...args: string[]) {
91+
await new Promise(resolve => setTimeout(resolve, 1000));
92+
}
93+
}
94+
95+
96+
module.exports = (globalThis: any) => {
97+
globalThis.ai = new AI(globalThis, aiService);
5198
};
99+
100+

snippets/ai/logger.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import EventEmitter from "events";
2+
3+
const IS_DEBUG = process.env.DEBUG === 'true';
4+
5+
class Logger extends EventEmitter {
6+
debug(...args: unknown[]) {
7+
if (IS_DEBUG) {
8+
console.debug(...args);
9+
this.emit('debug', ...args);
10+
}
11+
}
12+
13+
info(...args: unknown[]) {
14+
if (IS_DEBUG) {
15+
console.info(...args);
16+
this.emit('info', ...args);
17+
}
18+
}
19+
20+
error(...args: unknown[]) {
21+
if (IS_DEBUG) {
22+
console.error(...args);
23+
this.emit('error', ...args);
24+
}
25+
}
26+
}
27+
28+
export const log = new Logger();

0 commit comments

Comments
 (0)