Skip to content

Commit

Permalink
Allow for user feedback (#15)
Browse files Browse the repository at this point in the history
This adds a UI for users to provide feedback on the Qiskit Code Assistant as well as specific prompts.

Included features
- Feedback status bar icon: When clicked opens a feedback dialog for general feedback on the extension or model. The text field is immediately focused and and supports multiline input.
- Prompt specific feedback: Once a prompt response has been received a feedback button is added to the Notebook cell toolbar. When clicked a feedback dialog connected to the most recently received prompt response is opened. The prompt response does not necessarily need to have been in the current cell, or even file. 
- After feedback is successfully sent to the service a toast notification is triggered
  • Loading branch information
ajbozarth authored Nov 7, 2024
1 parent 98d89d5 commit a3b4a17
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 6 deletions.
16 changes: 16 additions & 0 deletions qiskit_code_assistant_jupyterlab/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,21 @@ def post(self, id):
self.finish(json.dumps(r.json()))


class FeedbackHandler(APIHandler):
@tornado.web.authenticated
def post(self):
url = url_path_join(runtime_configs["service_url"], "feedback")

try:
r = requests.post(url, headers=get_header(), json=self.get_json_body())
r.raise_for_status()
except requests.exceptions.HTTPError as err:
self.set_status(err.response.status_code)
self.finish(json.dumps(err.response.json()))
else:
self.finish(json.dumps(r.json()))


def setup_handlers(web_app):
host_pattern = ".*$"
id_regex = r"(?P<id>[\w\-]+)"
Expand All @@ -197,6 +212,7 @@ def setup_handlers(web_app):
(f"{base_url}/disclaimer/{id_regex}/acceptance", DisclaimerAcceptanceHandler),
(f"{base_url}/model/{id_regex}/prompt", PromptHandler),
(f"{base_url}/prompt/{id_regex}/acceptance", PromptAcceptanceHandler),
(f"{base_url}/feedback", FeedbackHandler),
]
web_app.add_handlers(host_pattern, handlers)
init_token()
9 changes: 9 additions & 0 deletions schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
"selector": ".jp-mod-completer-enabled"
}
],
"jupyter.lab.toolbars": {
"Cell": [
{
"name": "prompt-feedback",
"command": "qiskit-code-assistant:prompt-feedback",
"rank": 0
}
]
},
"title": "Qiskit Code Assistant",
"description": "Qiskit Code Assistant settings.",
"type": "object",
Expand Down
27 changes: 26 additions & 1 deletion src/QiskitCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { JupyterFrontEnd } from '@jupyterlab/application';
import {
CompletionHandler,
ICompletionContext,
Expand All @@ -32,6 +33,11 @@ import { Widget } from '@lumino/widgets';
import { postModelPromptAccept } from './service/api';
import { autoComplete } from './service/autocomplete';
import { qiskitIcon } from './utils/icons';
import { ICompletionReturn } from './utils/schema';

const FEEDBACK_COMMAND = 'qiskit-code-assistant:prompt-feedback';

export let lastPrompt: ICompletionReturn | undefined = undefined;

function getInputText(text: string, widget: Widget): string {
const cellsContents: string[] = [];
Expand Down Expand Up @@ -62,11 +68,16 @@ export class QiskitCompletionProvider implements ICompletionProvider {
readonly rank: number = 1100;

settings: ISettingRegistry.ISettings;
app: JupyterFrontEnd;
prompt_id: string = '';
results: string[] = [];

constructor(options: { settings: ISettingRegistry.ISettings }) {
constructor(options: {
settings: ISettingRegistry.ISettings;
app: JupyterFrontEnd;
}) {
this.settings = options.settings;
this.app = options.app;
}

async fetch(
Expand All @@ -78,6 +89,10 @@ export class QiskitCompletionProvider implements ICompletionProvider {
return autoComplete(text).then(results => {
this.prompt_id = results.prompt_id;
this.results = results.items;
if (this.prompt_id) {
lastPrompt = results;
this.app.commands.notifyCommandChanged(FEEDBACK_COMMAND);
}
return {
start: request.offset,
end: request.offset,
Expand Down Expand Up @@ -113,6 +128,7 @@ export class QiskitInlineCompletionProvider
readonly identifier: string = 'qiskit-code-assistant-inline-completer';
readonly name: string = 'Qiskit Code Assistant';

app: JupyterFrontEnd;
prompt_id: string = '';
schema: ISettingRegistry.IProperty = {
default: {
Expand All @@ -121,6 +137,10 @@ export class QiskitInlineCompletionProvider
}
};

constructor(options: { app: JupyterFrontEnd }) {
this.app = options.app;
}

async fetch(
request: CompletionHandler.IRequest,
context: IInlineCompletionContext
Expand All @@ -134,6 +154,11 @@ export class QiskitInlineCompletionProvider

return autoComplete(text).then(results => {
this.prompt_id = results.prompt_id;
if (this.prompt_id) {
lastPrompt = results;
this.app.commands.notifyCommandChanged(FEEDBACK_COMMAND);
}

return {
items: results.items.map((item: string): IInlineCompletionItem => {
return { insertText: item };
Expand Down
27 changes: 25 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ import {
} from '@jupyterlab/application';
import { ICommandPalette } from '@jupyterlab/apputils';
import { ICompletionProviderManager } from '@jupyterlab/completer';
import { INotebookTracker } from '@jupyterlab/notebook';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { IStatusBar } from '@jupyterlab/statusbar';

import { StatusBarWidget } from './StatusBarWidget';
import {
lastPrompt,
QiskitCompletionProvider,
QiskitInlineCompletionProvider
} from './QiskitCompletionProvider';
import { postServiceUrl } from './service/api';
import { getFeedbackStatusBarWidget, getFeedback } from './service/feedback';
import { refreshModelsList } from './service/modelHandler';
import { updateAPIToken } from './service/token';
import { feedbackIcon } from './utils/icons';

const EXTENSION_ID = 'qiskit-code-assistant-jupyterlab';

Expand All @@ -39,6 +43,7 @@ namespace CommandIDs {
export const selectCompleterNotebook = 'completer:select-notebook';
export const selectCompleterFile = 'completer:select-file';
export const updateApiToken = 'qiskit-code-assistant:set-api-token';
export const promptFeedback = 'qiskit-code-assistant:prompt-feedback';
}

/**
Expand All @@ -50,13 +55,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
autoStart: true,
requires: [
ICompletionProviderManager,
INotebookTracker,
ICommandPalette,
ISettingRegistry,
IStatusBar
],
activate: async (
app: JupyterFrontEnd,
completionProviderManager: ICompletionProviderManager,
notebookTracker: INotebookTracker,
palette: ICommandPalette,
settingRegistry: ISettingRegistry,
statusBar: IStatusBar
Expand All @@ -71,11 +78,16 @@ const plugin: JupyterFrontEndPlugin<void> = {
postServiceUrl(settings.composite['serviceUrl'] as string)
);

const provider = new QiskitCompletionProvider({ settings });
const inlineProvider = new QiskitInlineCompletionProvider();
const provider = new QiskitCompletionProvider({ settings, app });
const inlineProvider = new QiskitInlineCompletionProvider({ app });
completionProviderManager.registerProvider(provider);
completionProviderManager.registerInlineProvider(inlineProvider);

statusBar.registerStatusItem(EXTENSION_ID + ':feedback', {
item: getFeedbackStatusBarWidget(),
align: 'left'
});

const statusBarWidget = new StatusBarWidget();
statusBar.registerStatusItem(EXTENSION_ID + ':statusbar', {
item: statusBarWidget,
Expand All @@ -86,6 +98,17 @@ const plugin: JupyterFrontEndPlugin<void> = {
console.error('Failed initial load of models list', reason);
});

app.commands.addCommand(CommandIDs.promptFeedback, {
label: 'Give feedback for the Qiskit Code Assistant',
icon: feedbackIcon,
execute: () => getFeedback(),
isEnabled: () => lastPrompt !== undefined,
isVisible: () =>
['code', 'markdown'].includes(
notebookTracker.activeCell?.model.type || ''
) && lastPrompt !== undefined
});

app.commands.addCommand(CommandIDs.updateApiToken, {
label: 'Qiskit Code Assistant: Set IBM Quantum API token',
execute: () => updateAPIToken()
Expand Down
35 changes: 35 additions & 0 deletions src/service/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Notification } from '@jupyterlab/apputils';

import { requestAPI } from '../utils/handler';
import {
IFeedbackResponse,
IModelDisclaimer,
IModelInfo,
IModelPromptResponse,
Expand Down Expand Up @@ -229,3 +230,37 @@ export async function postModelPromptAccept(
}
});
}

// POST /feedback
export async function postFeedback(
model_id?: string,
prompt_id?: string,
positive_feedback?: boolean,
comment?: string,
input?: string,
output?: string
): Promise<IFeedbackResponse> {
return await requestAPI('feedback', {
method: 'POST',
body: JSON.stringify({
model_id,
prompt_id,
positive_feedback,
comment,
input,
output
})
}).then(async response => {
if (response.ok) {
return await response.json();
} else {
notifyInvalid(response);
console.error(
'Error sending feedback',
response.status,
response.statusText
);
throw Error(response.statusText);
}
});
}
9 changes: 7 additions & 2 deletions src/service/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ async function promptPromise(
response.results.map(results => items.push(results.generated_text));
return {
items,
prompt_id: response.prompt_id
prompt_id: response.prompt_id,
input: requestText
};
}
);
}

export async function autoComplete(text: string): Promise<ICompletionReturn> {
const emptyReturn: ICompletionReturn = { items: [], prompt_id: '' };
const emptyReturn: ICompletionReturn = {
items: [],
prompt_id: '',
input: ''
};

return await checkAPIToken()
.then(async () => {
Expand Down
Loading

0 comments on commit a3b4a17

Please sign in to comment.