diff --git a/explorer/assistant/models.py b/explorer/assistant/models.py index 21c6db6d..a2158fbb 100644 --- a/explorer/assistant/models.py +++ b/explorer/assistant/models.py @@ -1,5 +1,6 @@ from django.db import models from django.conf import settings +from explorer.ee.db_connections.models import DatabaseConnection class PromptLog(models.Model): @@ -19,3 +20,17 @@ class Meta: duration = models.FloatField(blank=True, null=True) # seconds model = models.CharField(blank=True, max_length=128, default="") error = models.TextField(blank=True, null=True) + + +class TableDescription(models.Model): + + class Meta: + app_label = "explorer" + unique_together = ("connection", "table_name") + + connection = models.ForeignKey(to=DatabaseConnection, on_delete=models.CASCADE) + table_name = models.CharField(max_length=512) + description = models.TextField() + + def __str__(self): + return f"{self.connection.alias} - {self.table_name}" diff --git a/explorer/assistant/urls.py b/explorer/assistant/urls.py new file mode 100644 index 00000000..ece81fbd --- /dev/null +++ b/explorer/assistant/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from explorer.assistant.views import (TableDescriptionListView, + TableDescriptionCreateView, + TableDescriptionUpdateView, + TableDescriptionDeleteView, + AssistantHelpView) + +assistant_urls = [ + path("assistant/", AssistantHelpView.as_view(), name="assistant"), + path("table-descriptions/", TableDescriptionListView.as_view(), name="table_description_list"), + path("table-descriptions/new/", TableDescriptionCreateView.as_view(), name="table_description_create"), + path("table-descriptions//update/", TableDescriptionUpdateView.as_view(), name="table_description_update"), + path("table-descriptions//delete/", TableDescriptionDeleteView.as_view(), name="table_description_delete"), +] diff --git a/explorer/assistant/utils.py b/explorer/assistant/utils.py index 3dd3cdfb..940cd5ed 100644 --- a/explorer/assistant/utils.py +++ b/explorer/assistant/utils.py @@ -6,7 +6,7 @@ OPENAI_MODEL = app_settings.EXPLORER_ASSISTANT_MODEL["name"] ROW_SAMPLE_SIZE = 2 -MAX_FIELD_SAMPLE_SIZE = 500 # characters +MAX_FIELD_SAMPLE_SIZE = 200 # characters def openai_client(): @@ -42,7 +42,7 @@ def tables_from_schema_info(db_connection, table_names): def sample_rows_from_tables(connection, table_names): ret = "" for table_name in table_names: - ret += f"SAMPLE FROM TABLE {table_name}:\n" + ret += f"SAMPLE FROM TABLE '{table_name}':\n" ret += format_rows_from_table( sample_rows_from_table(connection, table_name) ) + "\n\n" @@ -95,17 +95,6 @@ def format_rows_from_table(rows): return ret -def get_table_names_from_query(sql): - from sql_metadata import Parser - if sql: - try: - parsed = Parser(sql) - return parsed.tables - except ValueError: - return [] - return [] - - def num_tokens_from_string(string: str) -> int: """Returns the number of tokens in a text string.""" import tiktoken @@ -123,10 +112,18 @@ def fits_in_window(string: str) -> bool: return num_tokens_from_string(string) < (app_settings.EXPLORER_ASSISTANT_MODEL["max_tokens"] * 0.95) +def build_system_prompt(flavor): + bsp = ExplorerValue.objects.get_item(ExplorerValue.ASSISTANT_SYSTEM_PROMPT).value + bsp += f"""\n\nYou are an expert at writing SQL, specifically for {flavor}, and account for the nuances + of this dialect of SQL.""" + return bsp + + def build_prompt(db_connection, assistant_request, included_tables, query_error=None, sql=None): - user_prompt = "" djc = db_connection.as_django_connection() - user_prompt += f"## Database Vendor / SQL Flavor is {djc.vendor}\n\n" + sp = build_system_prompt(djc.vendor) + + user_prompt = f"## Database Type is {djc.vendor}\n\n" if query_error: user_prompt += f"## Query Error ##\n\n{query_error}\n\n" @@ -134,19 +131,18 @@ def build_prompt(db_connection, assistant_request, included_tables, query_error= if sql: user_prompt += f"## Existing SQL ##\n\n{sql}\n\n" - results_sample = sample_rows_from_tables(djc, - included_tables) + results_sample = sample_rows_from_tables(djc, included_tables) + # If it's too large with sampling, then provide *just* the structure if fits_in_window(user_prompt + results_sample): user_prompt += f"## Table Structure with Sampled Data ##\n\n{results_sample}\n\n" - else: # If it's too large with sampling, then provide *just* the structure - table_struct = tables_from_schema_info(db_connection, - included_tables) + else: + table_struct = tables_from_schema_info(db_connection, included_tables) user_prompt += f"## Table Structure ##\n\n{table_struct}\n\n" user_prompt += f"## User's Request to Assistant ##\n\n{assistant_request}\n\n" prompt = { - "system": ExplorerValue.objects.get_item(ExplorerValue.ASSISTANT_SYSTEM_PROMPT).value, + "system": sp, "user": user_prompt } return prompt diff --git a/explorer/assistant/views.py b/explorer/assistant/views.py index f5d6f2aa..4eacda69 100644 --- a/explorer/assistant/views.py +++ b/explorer/assistant/views.py @@ -1,6 +1,10 @@ from django.http import JsonResponse from django.views import View from django.utils import timezone +from django.views.generic import ListView, CreateView, UpdateView, DeleteView +from django.urls import reverse_lazy +from .models import TableDescription + import json from explorer.telemetry import Stat, StatNames @@ -8,7 +12,6 @@ from explorer.assistant.models import PromptLog from explorer.assistant.utils import ( do_req, extract_response, - get_table_names_from_query, build_prompt ) @@ -16,8 +19,7 @@ def run_assistant(request_data, user): sql = request_data.get("sql") - extra_tables = request_data.get("selected_tables", []) - included_tables = get_table_names_from_query(sql) + extra_tables + included_tables = request_data.get("selected_tables", []) connection_id = request_data.get("connection_id") try: @@ -67,3 +69,29 @@ def post(self, request, *args, **kwargs): return JsonResponse(response_data) except json.JSONDecodeError: return JsonResponse({"status": "error", "message": "Invalid JSON"}, status=400) + + +class TableDescriptionListView(ListView): + model = TableDescription + template_name = "assistant/table_description_list.html" + context_object_name = "table_descriptions" + + +class TableDescriptionCreateView(CreateView): + model = TableDescription + template_name = "assistant/table_description_form.html" + fields = ["connection", "table_name", "description"] + success_url = reverse_lazy("table_description_list") + + +class TableDescriptionUpdateView(UpdateView): + model = TableDescription + template_name = "assistant/table_description_form.html" + fields = ["connection", "table_name", "description"] + success_url = reverse_lazy("table_description_list") + + +class TableDescriptionDeleteView(DeleteView): + model = TableDescription + template_name = "assistant/table_description_confirm_delete.html" + success_url = reverse_lazy("table_description_list") diff --git a/explorer/migrations/0026_tabledescription.py b/explorer/migrations/0026_tabledescription.py new file mode 100644 index 00000000..5f44139c --- /dev/null +++ b/explorer/migrations/0026_tabledescription.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.4 on 2024-08-19 14:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('explorer', '0025_remove_query_connection_remove_querylog_connection_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='TableDescription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('table_name', models.CharField(max_length=512)), + ('description', models.TextField()), + ('connection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='explorer.databaseconnection')), + ], + options={ + 'unique_together': {('connection', 'table_name')}, + }, + ), + ] diff --git a/explorer/src/js/assistant.js b/explorer/src/js/assistant.js index 9e7c4690..abf0ead6 100644 --- a/explorer/src/js/assistant.js +++ b/explorer/src/js/assistant.js @@ -1,9 +1,9 @@ import {getCsrfToken} from "./csrf"; import { marked } from "marked"; import DOMPurify from "dompurify"; -import * as bootstrap from 'bootstrap'; -import List from "list.js"; +import * as bootstrap from "bootstrap"; import { SchemaSvc, getConnElement } from "./schemaService" +import Choices from "choices.js" function getErrorMessage() { const errorElement = document.querySelector('.alert-danger.db-error'); @@ -13,50 +13,50 @@ function getErrorMessage() { function setupTableList() { SchemaSvc.get().then(schema => { const keys = Object.keys(schema); + const selectElement = document.createElement('select'); + selectElement.className = 'js-choice'; + selectElement.toggleAttribute('multiple'); + selectElement.toggleAttribute('data-trigger'); + + keys.forEach((key) => { + const option = document.createElement('option'); + option.value = key; + option.textContent = key; + selectElement.appendChild(option); + }); + const tableList = document.getElementById('table-list'); tableList.innerHTML = ''; - - keys.forEach((key, index) => { - const div = document.createElement('div'); - div.className = 'form-check'; - - const input = document.createElement('input'); - input.className = 'form-check-input table-checkbox'; - input.type = 'checkbox'; - input.value = key; - input.id = 'flexCheckDefault' + index; - - const label = document.createElement('label'); - label.className = 'form-check-label'; - label.setAttribute('for', input.id); - label.textContent = key; - - div.appendChild(input); - div.appendChild(label); - tableList.appendChild(div); + tableList.appendChild(selectElement); + + const choices = new Choices('.js-choice', { + removeItemButton: true, + searchEnabled: true, + shouldSort: false, + placeholder: true, + placeholderValue: 'Relevant tables', + position: 'bottom' }); - let options = { - valueNames: ['form-check-label'], - }; - - new List('additional_table_container', options); - const selectAllButton = document.getElementById('select_all_button'); - const checkboxes = document.querySelectorAll('.table-checkbox'); - - let selectState = 'all'; - - selectAllButton.innerHTML = 'Select All'; - selectAllButton.addEventListener('click', (e) => { e.preventDefault(); - const isSelectingAll = selectState === 'all'; - checkboxes.forEach((checkbox) => { - checkbox.checked = isSelectingAll; + choices.setChoiceByValue(keys); + }); + + const deselectAllButton = document.getElementById('deselect_all_button'); + deselectAllButton.addEventListener('click', (e) => { + e.preventDefault(); + keys.forEach(k => { + choices.removeActiveItemsByValue(k); }); - selectState = isSelectingAll ? 'none' : 'all'; - selectAllButton.innerHTML = isSelectingAll ? 'Deselect All' : 'Select All'; + }); + + document.addEventListener('docChanged', (e) => { + const textContent = window.editor.state.doc.toString(); + const textWords = new Set(textContent.split(/\s+/)); + const hasKeys = keys.filter(key => textWords.has(key)); + choices.setChoiceByValue(hasKeys); }); }) .catch(error => { @@ -64,6 +64,7 @@ function setupTableList() { }); } + export function setUpAssistant(expand = false) { getConnElement().addEventListener('change', setupTableList); @@ -71,13 +72,13 @@ export function setUpAssistant(expand = false) { const error = getErrorMessage(); - if(expand || error) { + if (expand || error) { const myCollapseElement = document.getElementById('assistant_collapse'); const bsCollapse = new bootstrap.Collapse(myCollapseElement, { - toggle: false + toggle: false }); bsCollapse.show(); - if(error) { + if (error) { document.getElementById('id_error_help_message').classList.remove('d-none'); } } @@ -85,7 +86,7 @@ export function setUpAssistant(expand = false) { const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); - document.getElementById('id_assistant_input').addEventListener('keydown', function(event) { + document.getElementById('id_assistant_input').addEventListener('keydown', function (event) { if ((event.ctrlKey || event.metaKey) && (event.key === 'Enter')) { event.preventDefault(); submitAssistantAsk(); diff --git a/explorer/src/js/codemirror-config.js b/explorer/src/js/codemirror-config.js index a4539e49..1a1e38a0 100644 --- a/explorer/src/js/codemirror-config.js +++ b/explorer/src/js/codemirror-config.js @@ -14,9 +14,14 @@ import { Prec } from "@codemirror/state"; import {sql} from "@codemirror/lang-sql"; import { SchemaSvc } from "./schemaService" +let debounceTimeout; + let updateListenerExtension = EditorView.updateListener.of((update) => { if (update.docChanged) { - document.dispatchEvent(new CustomEvent('docChanged', {})); + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(() => { + document.dispatchEvent(new CustomEvent('docChanged', {})); + }, 500); } }); diff --git a/explorer/src/scss/assistant.scss b/explorer/src/scss/assistant.scss index fbb2c625..1da3d53b 100644 --- a/explorer/src/scss/assistant.scss +++ b/explorer/src/scss/assistant.scss @@ -14,11 +14,6 @@ cursor: pointer; } -#additional_table_container { - overflow-y: auto; - max-height: 10rem; -} - #assistant_input_parent { max-height: 120px; overflow: hidden; diff --git a/explorer/src/scss/styles.scss b/explorer/src/scss/styles.scss index cece367d..9558b24f 100644 --- a/explorer/src/scss/styles.scss +++ b/explorer/src/scss/styles.scss @@ -10,3 +10,4 @@ $bootstrap-icons-font-dir: "../../../node_modules/bootstrap-icons/font/fonts"; @import "assistant"; @import "pivot.css"; +@import "choices.js/public/assets/styles/choices.css"; diff --git a/explorer/templates/assistant/table_description_confirm_delete.html b/explorer/templates/assistant/table_description_confirm_delete.html new file mode 100644 index 00000000..07ebed5c --- /dev/null +++ b/explorer/templates/assistant/table_description_confirm_delete.html @@ -0,0 +1,14 @@ +{% extends "explorer/base.html" %} + +{% block sql_explorer_content %} +
+

Confirm Delete

+

Are you sure you want to delete the table description for "{{ object.table_name }}" in {{ object.connection.alias }}?

+
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} + diff --git a/explorer/templates/assistant/table_description_form.html b/explorer/templates/assistant/table_description_form.html new file mode 100644 index 00000000..c99069aa --- /dev/null +++ b/explorer/templates/assistant/table_description_form.html @@ -0,0 +1,13 @@ +{% extends "explorer/base.html" %} + +{% block sql_explorer_content %} +
+

{% if form.instance.pk %}Edit{% else %}Create{% endif %} Table Description

+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+
+{% endblock %} \ No newline at end of file diff --git a/explorer/templates/assistant/table_description_list.html b/explorer/templates/assistant/table_description_list.html new file mode 100644 index 00000000..131a2b90 --- /dev/null +++ b/explorer/templates/assistant/table_description_list.html @@ -0,0 +1,31 @@ +{% extends "explorer/base.html" %} + +{% block sql_explorer_content %} +
+

Table Descriptions

+ Create New + + + + + + + + + + + {% for table_description in table_descriptions %} + + + + + + + {% endfor %} + +
ConnectionTable NameDescriptionActions
{{ table_description.connection.alias }}{{ table_description.table_name }}{{ table_description.description|truncatewords:20 }} + Edit + Delete +
+
+{% endblock %} diff --git a/explorer/templates/explorer/assistant.html b/explorer/templates/explorer/assistant.html index beb4deba..9b0f27d3 100644 --- a/explorer/templates/explorer/assistant.html +++ b/explorer/templates/explorer/assistant.html @@ -10,9 +10,9 @@
-
+
@@ -20,20 +20,23 @@
- - - (?) - - +
+
+ + +
+
+
+
-
+
diff --git a/explorer/tests/test_assistant.py b/explorer/tests/test_assistant.py index c0a7d6c1..b37981ff 100644 --- a/explorer/tests/test_assistant.py +++ b/explorer/tests/test_assistant.py @@ -219,18 +219,6 @@ def test_format_rows_from_table(self): ret = format_rows_from_table(d) self.assertEqual(ret, "col1 | col2\n" + "-" * 50 + "\nval1 | val2\n") - def test_parsing_tables_from_query(self): - from explorer.assistant.utils import get_table_names_from_query - sql = "SELECT * FROM explorer_query" - ret = get_table_names_from_query(sql) - self.assertEqual(ret, ["explorer_query"]) - - def test_parsing_tables_from_no_tables(self): - from explorer.assistant.utils import get_table_names_from_query - sql = "select 1;" - ret = get_table_names_from_query(sql) - self.assertEqual(ret, []) - def test_schema_info_from_table_names(self): from explorer.assistant.utils import tables_from_schema_info ret = tables_from_schema_info(default_db_connection(), ["explorer_query"]) diff --git a/explorer/urls.py b/explorer/urls.py index 36fec7bb..c612ba5c 100644 --- a/explorer/urls.py +++ b/explorer/urls.py @@ -6,7 +6,7 @@ ListQueryView, PlayQueryView, QueryFavoritesView, QueryFavoriteView, QueryView, SchemaJsonView, SchemaView, StreamQueryView, format_sql ) -from explorer.assistant.views import AssistantHelpView +from explorer.assistant.urls import assistant_urls urlpatterns = [ path( @@ -43,7 +43,7 @@ path("favorites/", QueryFavoritesView.as_view(), name="query_favorites"), path("favorite/", QueryFavoriteView.as_view(), name="query_favorite"), path("", ListQueryView.as_view(), name="explorer_index"), - path("assistant/", AssistantHelpView.as_view(), name="assistant"), ] +urlpatterns += assistant_urls urlpatterns += ee_urls diff --git a/package-lock.json b/package-lock.json index 1cbcab11..03b0fd4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@codemirror/language-data": "^6.3.1", "bootstrap": "^5.0.1", "bootstrap-icons": "^1.11.2", + "choices.js": "^10.2.0", "codemirror": "^6.0.1", "cookiejs": "^2.1.3", "dompurify": "^3.0.7", @@ -24,6 +25,20 @@ "vite": "^5.0.13", "vite-plugin-copy": "^0.1.6", "vite-plugin-static-copy": "^1.0.5" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.18.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@codemirror/autocomplete": { @@ -981,13 +996,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz", - "integrity": "sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1112,6 +1126,16 @@ "node": ">=8" } }, + "node_modules/choices.js": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/choices.js/-/choices.js-10.2.0.tgz", + "integrity": "sha512-8PKy6wq7BMjNwDTZwr3+Zry6G2+opJaAJDDA/j3yxvqSCnvkKe7ZIFfIyOhoc7htIWFhsfzF9tJpGUATcpUtPg==", + "dependencies": { + "deepmerge": "^4.2.2", + "fuse.js": "^6.6.2", + "redux": "^4.2.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1166,6 +1190,14 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dompurify": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.7.tgz", @@ -1274,6 +1306,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "engines": { + "node": ">=10" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1506,6 +1546,19 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -1545,6 +1598,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz", + "integrity": "sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index c7e89cad..53833c52 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@codemirror/language-data": "^6.3.1", "bootstrap": "^5.0.1", "bootstrap-icons": "^1.11.2", + "choices.js": "^10.2.0", "codemirror": "^6.0.1", "cookiejs": "^2.1.3", "dompurify": "^3.0.7", diff --git a/requirements/extra/assistant.txt b/requirements/extra/assistant.txt index c5ae0910..a04e4fa4 100644 --- a/requirements/extra/assistant.txt +++ b/requirements/extra/assistant.txt @@ -1,3 +1,2 @@ openai>=1.6.1 -sql_metadata>=2.10 tiktoken>=0.7