Skip to content

Commit

Permalink
feat: Add Copilot as an LLM backend within Navie
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinbyrne committed Oct 15, 2024
1 parent b7a12bb commit f161686
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 14 deletions.
9 changes: 9 additions & 0 deletions src/services/navieConfigurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export default function navieConfigurationService(context: vscode.ExtensionConte
);
}

export async function openAIApiKeyEquals(
extensionContext: vscode.ExtensionContext,
key: string | undefined
): Promise<boolean> {
const { secrets } = extensionContext;
const storedKey = await secrets.get(OPENAI_API_KEY);
return key === storedKey;
}

export async function setOpenAIApiKey(
extensionContext: vscode.ExtensionContext,
key: string | undefined
Expand Down
7 changes: 7 additions & 0 deletions src/services/processWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class ProcessWatcher implements vscode.Disposable {

protected _onError: vscode.EventEmitter<Error> = new vscode.EventEmitter<Error>();
protected _onAbort: vscode.EventEmitter<Error> = new vscode.EventEmitter<Error>();
protected _onRestart: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();

protected shouldRun = false;
protected hasAborted = false;
Expand All @@ -101,6 +102,11 @@ export class ProcessWatcher implements vscode.Disposable {
return this._onAbort.event;
}

// Fired when the process is restarted.
public get onRestart(): vscode.Event<void> {
return this._onRestart.event;
}

public get id(): ProcessId {
return this.options.id;
}
Expand Down Expand Up @@ -169,6 +175,7 @@ export class ProcessWatcher implements vscode.Disposable {
}

async restart(): Promise<void> {
this._onRestart.fire();
await this.stop();
await this.start();
}
Expand Down
33 changes: 26 additions & 7 deletions src/services/rpcProcessService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import assert from 'assert';
import { DEBUG_EXCEPTION, Telemetry } from '../telemetry';
import ErrorCode from '../telemetry/definitions/errorCodes';
import AssetService, { AssetIdentifier } from '../assets/assetService';
import { setOpenAIApiKey } from './navieConfigurationService';
import { openAIApiKeyEquals, setOpenAIApiKey } from './navieConfigurationService';

export type RpcConnect = (port: number) => Client;

Expand Down Expand Up @@ -81,6 +81,10 @@ export default class RpcProcessService implements Disposable {
);
}

get onRestart(): vscode.Event<void> {
return this.processWatcher.onRestart;
}

// Provides some internal state access, primarily for testing purposes.
public get state(): RpcProcessServiceState {
return {
Expand Down Expand Up @@ -236,14 +240,28 @@ export default class RpcProcessService implements Disposable {
}

async updateSettings(settings: RpcSettings): Promise<void> {
let shouldRestart = false;

if (settings.useCopilot !== undefined) {
await vscode.workspace
.getConfiguration('appMap.navie')
.update('useVSCodeLM', settings.useCopilot, true);
const config = vscode.workspace.getConfiguration('appMap.navie');
const key = 'useVSCodeLM';

await config.update(key, settings.useCopilot, true);

const otherSettings = config.inspect(key);
if (otherSettings?.workspaceValue !== undefined) {
await config.update(key, undefined, undefined);
}

shouldRestart = true;
}

if (settings.openAIApiKey) {
await setOpenAIApiKey(this.context, settings.openAIApiKey);
if (Object.hasOwnProperty.call(settings, 'openAIApiKey')) {
const sameKey = await openAIApiKeyEquals(this.context, settings.openAIApiKey);
if (!sameKey) {
await setOpenAIApiKey(this.context, settings.openAIApiKey);
shouldRestart = true;
}
}

if (settings.env) {
Expand All @@ -252,8 +270,9 @@ export default class RpcProcessService implements Disposable {
env[k] = v;
});
await vscode.workspace.getConfiguration('appMap').update('commandLineEnvironment', env, true);
shouldRestart = true;
}

this.debouncedRestart();
if (shouldRestart) this.restart();
}
}
10 changes: 7 additions & 3 deletions src/webviews/chatSearchWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,12 @@ export default class ChatSearchWebview {
}),
this.rpcService.onRpcPortChange(() => {
panel.webview.postMessage({
type: 'settings-updated',
type: 'navie-restarted',
});
}),
this.rpcService.onRestart(() => {
panel.webview.postMessage({
type: 'navie-restarting',
});
})
);
Expand Down Expand Up @@ -275,8 +280,7 @@ export default class ChatSearchWebview {
case 'own-key':
this.rpcService.updateSettings({
useCopilot: false,
openAIApiKey:
(await vscode.window.showInputBox({ placeHolder: 'OpenAI API Key' })) ?? '',
openAIApiKey: await vscode.window.showInputBox({ placeHolder: 'OpenAI API Key' }),
env: {
OPENAI_BASE_URL: undefined,
},
Expand Down
35 changes: 34 additions & 1 deletion test/unit/mock/vscode/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,42 @@ export const EVENTS = {
onDidChangeConfiguration: listener(),
};

class Configuration extends Map<string, unknown> {
get(key: string, defaultValue?: unknown): unknown {
return super.get(key) ?? defaultValue;
}

inspect(key: string): { workspaceValue?: unknown } | undefined {
return {};
}

update(key: string, value: unknown, target?: unknown): Promise<void> {
if (value === undefined || value === null) {
this.delete(key);
} else {
let filteredValue = value;
if (typeof value === 'object') {
// Undefined/null values are deleted
filteredValue = Object.entries(value).reduce((acc, [k, v]) => {
if (v !== undefined && v !== null) acc[k] = v;
return acc;
}, {});
}
this.set(key, filteredValue);
}
return Promise.resolve();
}
}

const configs = new Map<string, Configuration>();

export default {
fs,
getConfiguration: () => new Map<string, unknown>(),
getConfiguration: (key: string) => {
let config = configs.get(key);
if (!config) configs.set(key, (config = new Configuration()));
return config;
},
workspaceFolders: [],
onDidChangeConfiguration: EVENTS.onDidChangeConfiguration,
onDidChangeWorkspaceFolders: EVENTS.onDidChangeWorkspaceFolders,
Expand Down
44 changes: 44 additions & 0 deletions test/unit/services/rpcProcessService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import RpcProcessService, { RpcProcessServiceState } from '../../../src/services
import MockExtensionContext from '../../mocks/mockExtensionContext';
import EventEmitter from '../mock/vscode/EventEmitter';
import { waitFor } from '../../waitFor';
import vscode from '../mock/vscode';

chai.use(chaiAsPromised);

Expand Down Expand Up @@ -189,4 +190,47 @@ describe('RpcProcessService', () => {
await waitFor(`Expecting debounced restart`, () => restartSpy.calledOnce);
});
});

describe('updateSettings', () => {
it('does not restart if no settings are changed', async () => {
const restartSpy = sinon.spy(rpcService, 'restart');
await rpcService.updateSettings({});
expect(restartSpy.called).to.be.false;
});

it('restarts if the useCopilot setting is changed', async () => {
const restartSpy = sinon.spy(rpcService, 'restart');
await rpcService.updateSettings({ useCopilot: true });
expect(restartSpy.called).to.be.true;
});

it('restarts if the openAIApiKey setting is changed', async () => {
const restartSpy = sinon.spy(rpcService, 'restart');
await rpcService.updateSettings({ openAIApiKey: 'new-key' });
expect(restartSpy.called).to.be.true;
});

it('does not restart if the openAIApiKey setting is the same', async () => {
extensionContext.secrets.store('openai.api_key', 'old-key');
const restartSpy = sinon.spy(rpcService, 'restart');
await rpcService.updateSettings({ openAIApiKey: 'old-key' });
expect(restartSpy.called).to.be.false;
});

it('restarts if the env setting is changed', async () => {
const restartSpy = sinon.spy(rpcService, 'restart');
await rpcService.updateSettings({ env: { foo: 'bar' } });
expect(restartSpy.called).to.be.true;
});

it('properly sets env values', async () => {
vscode.workspace
.getConfiguration('appMap')
.update('commandLineEnvironment', { foo: 'bar', bar: 'baz' }, true);
await rpcService.updateSettings({ env: { foo: undefined, baz: 'qux' } });
const env = vscode.workspace.getConfiguration('appMap').get('commandLineEnvironment');
const x = vscode.workspace;
expect(env).to.deep.equal({ bar: 'baz', baz: 'qux' });
});
});
});
10 changes: 7 additions & 3 deletions web/src/chatSearchView.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ export default function mountChatSearchView() {
});
});

messages.on('settings-updated', () => {
app.$refs.ui.loadNavieConfig();
});
messages
.on('navie-restarting', () => {
app.$refs.ui.onNavieRestarting();
})
.on('navie-restarted', () => {
app.$refs.ui.loadNavieConfig();
});

app.$on('open-install-instructions', () => {
vscode.postMessage({ command: 'open-install-instructions' });
Expand Down

0 comments on commit f161686

Please sign in to comment.