diff --git a/src/app-config.json b/src/app-config.json index 2072454..27cfad3 100644 --- a/src/app-config.json +++ b/src/app-config.json @@ -4,7 +4,7 @@ "keyboard_shortcut": "ctrl+?" }, "assistant": { - "existing_assistant_id": "asst_Af8jrKYOFP4MxA9nse61yFBq", + "existing_assistant_id": "asst_xmAX5oxByssXrkBymMbcsVEm", "instructions": "You are DAVAI, an Data Analysis through Voice and Artificial Intelligence partner. You are an intermediary for a user who is blind who wants to interact with data tables in a data analysis app named CODAP.", "model": "gpt-4o-mini", "use_existing": true diff --git a/src/models/assistant-model.ts b/src/models/assistant-model.ts index 570ff64..c81b022 100644 --- a/src/models/assistant-model.ts +++ b/src/models/assistant-model.ts @@ -2,11 +2,10 @@ import { types, flow } from "mobx-state-tree"; import { getTools, initLlmConnection } from "../utils/llm-utils"; import { ChatTranscriptModel, transcriptStore } from "./chat-transcript-model"; import { Message } from "openai/resources/beta/threads/messages"; -import { getAttributeList, getDataContext, getListOfDataContexts } from "../utils/codap-api-helpers"; import { DAVAI_SPEAKER, DEBUG_SPEAKER } from "../constants"; -import { createGraph } from "../utils/codap-utils"; import { formatMessage } from "../utils/utils"; import appConfigJson from "../app-config.json"; +import { codapInterface } from "@concord-consortium/codap-plugin-api"; export const AssistantModel = types .model("AssistantModel", { @@ -90,17 +89,29 @@ export const AssistantModel = types const pollRunState: (currentRunId: string) => Promise = flow(function* (currentRunId) { let runState = yield davai.beta.threads.runs.retrieve(self.thread.id, currentRunId); transcriptStore.addMessage(DEBUG_SPEAKER, { - description: "Polling run state", - content: formatMessage(runState), + description: "Run state status", + content: formatMessage(runState.status), }); - while (runState.status !== "completed" && runState.status !== "requires_action") { + const errorStates = ["failed", "cancelled", "incomplete"]; + + while (runState.status !== "completed" && runState.status !== "requires_action" && !errorStates.includes(runState.status)) { yield new Promise((resolve) => setTimeout(resolve, 2000)); runState = yield davai.beta.threads.runs.retrieve(self.thread.id, currentRunId); transcriptStore.addMessage(DEBUG_SPEAKER, { - description: "Polling run state", + description: "Run state status", + content: formatMessage(runState.status), + }); + } + + if (errorStates.includes(runState.status)) { + transcriptStore.addMessage(DEBUG_SPEAKER, { + description: "Run failed", content: formatMessage(runState), }); + transcriptStore.addMessage(DAVAI_SPEAKER, { + content: "I'm sorry, I encountered an error. Please try again.", + }); } if (runState.status === "requires_action") { @@ -139,34 +150,19 @@ export const AssistantModel = types try { const toolOutputs = runState.required_action?.submit_tool_outputs.tool_calls ? yield Promise.all( - runState.required_action.submit_tool_outputs.tool_calls.map(async (toolCall: any) => { - if (toolCall.function.name === "get_data_contexts") { - const dataContextList = await getListOfDataContexts(); - const { requestMessage, ...codapResponse } = dataContextList; - transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Request sent to CODAP", content: formatMessage(requestMessage) }); - transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Response from CODAP", content: formatMessage(codapResponse) }); - return { tool_call_id: toolCall.id, output: JSON.stringify(dataContextList) }; - } else if (toolCall.function.name === "get_attributes") { - const { dataset } = JSON.parse(toolCall.function.arguments); - // getting the root collection won't always work. what if a user wants the attributes - // in the Mammals dataset but there is a hierarchy? - const rootCollection = (await getDataContext(dataset)).values.collections[0]; - const attributeListRes = await getAttributeList(dataset, rootCollection.name); - const { requestMessage, ...codapResponse } = attributeListRes; - transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Request sent to CODAP", content: formatMessage(requestMessage) }); - transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Response from CODAP", content: formatMessage(codapResponse) }); - return { tool_call_id: toolCall.id, output: JSON.stringify(attributeListRes) }; - } else if (toolCall.function.name === "create_graph") { - const { dataset, name, xAttribute, yAttribute } = JSON.parse(toolCall.function.arguments); - const { requestMessage, ...codapResponse} = await createGraph(dataset, name, xAttribute, yAttribute); - transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Request sent to CODAP", content: formatMessage(requestMessage) }); - transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Response from CODAP", content: formatMessage(codapResponse) }); - return { tool_call_id: toolCall.id, output: "Graph created." }; + runState.required_action.submit_tool_outputs.tool_calls.map(flow(function* (toolCall: any) { + if (toolCall.function.name === "create_request") { + const { action, resource, values } = JSON.parse(toolCall.function.arguments); + const request = { action, resource, values }; + transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Request sent to CODAP", content: formatMessage(request) }); + const res = yield codapInterface.sendRequest(request); + transcriptStore.addMessage(DEBUG_SPEAKER, { description: "Response from CODAP", content: formatMessage(res) }); + return { tool_call_id: toolCall.id, output: JSON.stringify(res) }; } else { return { tool_call_id: toolCall.id, output: "Tool call not recognized." }; } }) - ) + )) : []; if (toolOutputs) { diff --git a/src/utils/codap-api-helpers.ts b/src/utils/codap-api-helpers.ts deleted file mode 100644 index 15385f0..0000000 --- a/src/utils/codap-api-helpers.ts +++ /dev/null @@ -1,362 +0,0 @@ -import {ClientHandler, codapInterface} from "@concord-consortium/codap-plugin-api"; -import { Action, Attribute, CodapItemValues, CodapItem } from "../types"; -export interface IDimensions { - width: number; - height: number; -} - -export interface IInitializePlugin { - pluginName: string; - version: string; - dimensions: IDimensions; -} -export interface IResult { - success: boolean; - values: any; -} - -////////////// internal helper functions ////////////// - -const ctxStr = (contextName: string) => `dataContext[${contextName}]`; -const collStr = (collectionName: string) => `collection[${collectionName}]`; - -const createMessage = (action: string, resource: string, values?: any) => { - return { - action, - resource, - values - }; -}; - -const sendMessage = async (action: Action, resource: string, values?: CodapItemValues) => { - const message = createMessage(action, resource, values); - const result = await codapInterface.sendRequest(message) as unknown as IResult; - return {...result, requestMessage: message}; -}; - -////////////// public API ////////////// - -export const initializePlugin = async (options: IInitializePlugin) => { - const { pluginName, version, dimensions } = options; - const interfaceConfig = { - name: pluginName, - version, - dimensions - }; - return await codapInterface.init(interfaceConfig); -}; - -////////////// component functions ////////////// - -export const createTable = async (dataContext: string, datasetName?: string) => { - const values: CodapItemValues = { - type: "caseTable", - dataContext - }; - if (datasetName) { - values.name = datasetName; - } - return sendMessage("create", "component", values); -}; - -// Selects this component. In CODAP this will bring this component to the front. -export const selectSelf = () => { - - const selectComponent = async function (id: number) { - return codapInterface.sendRequest({ - action: "notify", - resource: `component[${id}]`, - values: {request: "select"} - }, (result: IResult) => { - if (!result.success) { - // eslint-disable-next-line no-console - console.log("selectSelf failed"); - } - }); - }; - - codapInterface.sendRequest({action: "get", resource: "interactiveFrame"}, (result: IResult) => { - if (result.success) { - return selectComponent(result.values.id); - } - }); -}; - -export const addComponentListener = (callback: ClientHandler) => { - codapInterface.on("notify", "component", callback); -}; - -////////////// data context functions ////////////// - -export const getListOfDataContexts = () => { - return sendMessage("get", "dataContextList"); -}; - -export const getDataContext = (dataContextName: string) => { - return sendMessage("get", ctxStr(dataContextName)); -}; - -export const createDataContext = (dataContextName: string) => { - return sendMessage("create", "dataContext", {name: dataContextName}); -}; - -export const createDataContextFromURL = (url: string) => { - return sendMessage("create", "dataContextFromURL", {"URL": url}); -}; - -export const addDataContextsListListener = (callback: ClientHandler) => { - codapInterface.on("notify", "documentChangeNotice", callback); -}; - -export const addDataContextChangeListener = (dataContextName: string, callback: ClientHandler) => { - codapInterface.on("notify", `dataContextChangeNotice[${dataContextName}]`, callback); -}; - -////////////// collection functions ////////////// - -export const getCollectionList = (dataContextName: string) => { - return sendMessage("get", `${ctxStr(dataContextName)}.collectionList`); -}; - -export const getCollection = (dataContextName: string, collectionName: string) => { - return sendMessage("get", `${ctxStr(dataContextName)}.${collStr(collectionName)}`); -}; - -export const createParentCollection = (dataContextName: string, collectionName: string, attrs?: Attribute[]) => { - const resource = `${ctxStr(dataContextName)}.collection`; - - const values: CodapItemValues = { - "name": collectionName, - "title": collectionName, - "parent": "_root_" - }; - - if (attrs) { - values.attrs = attrs; - } - - return sendMessage("create", resource, values); -}; - -export const createChildCollection = (dataContextName: string, collectionName: string, parentCollectionName: string, attrs?: Attribute[]) => { - const resource = `${ctxStr(dataContextName)}.collection`; - - const values: CodapItemValues = { - "name": collectionName, - "title": collectionName, - "parent": parentCollectionName - }; - - if (attrs) { - values.attrs = attrs; - } - - return sendMessage("create", resource, values); -}; - -export const createNewCollection = (dataContextName: string, collectionName: string, attrs?: Attribute[]) => { - const resource = `${ctxStr(dataContextName)}.collection`; - - const values: CodapItemValues = { - "name": collectionName, - "title": collectionName, - }; - - if (attrs) { - values.attrs = attrs; - } - - return sendMessage("create", resource, values); -}; - -export const ensureUniqueCollectionName = async (dataContextName: string, collectionName: string, index: number): Promise => { - index = index || 0; - const uniqueName = `${collectionName}${index !== 0 ? index : ""}`; - const getCollMessage = { - "action": "get", - "resource": `${ctxStr(dataContextName)}.collection[${uniqueName}]` - }; - - const result: IResult = await new Promise((resolve) => { - codapInterface.sendRequest(getCollMessage, (res: IResult) => { - resolve(res); - }); - }); - - if (result.success) { - // guard against runaway loops - if (index >= 100) { - return undefined; - } - return ensureUniqueCollectionName(dataContextName, collectionName, index + 1); - } else { - return uniqueName; - } -}; - -////////////// attribute functions ////////////// - -export const getAttribute = (dataContextName: string, collectionName: string, attributeName: string) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.attribute[${attributeName}]`; - return sendMessage("get", resource); -}; - -export const getAttributeList = (dataContextName: string, collectionName: string) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.attributeList`; - return sendMessage("get", resource); -}; - -export const createNewAttribute = (dataContextName: string, collectionName: string, attributeName: string) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.attribute`; - const values: CodapItemValues = { - "name": attributeName, - "title": attributeName, - }; - return sendMessage("create", resource, values); -}; - -export const updateAttribute = (dataContextName: string, collectionName: string, attributeName: string, attribute: Attribute, values: CodapItemValues) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.attribute[${attributeName}]`; - return sendMessage("update", resource, values); -}; - -export const updateAttributePosition = (dataContextName: string, collectionName: string, attrName: string, newPosition: number) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.attributeLocation[${attrName}]`; - return sendMessage("update", resource, { - "collection": collectionName, - "position": newPosition - }); -}; - -export const createCollectionFromAttribute = (dataContextName: string, oldCollectionName: string, attr: Attribute, parent: number|string) => { - // check if a collection for the attribute already exists - const getCollectionMessage = createMessage("get", `${ctxStr(dataContextName)}.${collStr(attr.name)}`); - - return codapInterface.sendRequest(getCollectionMessage, async (result: IResult) => { - // since you can't "re-parent" collections we need to create a temp top level collection, move the attribute, - // and then check if CODAP deleted the old collection as it became empty and if so rename the new collection - const moveCollection = result.success && (result.values.attrs.length === 1 || attr.name === oldCollectionName); - const newCollectionName = moveCollection ? await ensureUniqueCollectionName(dataContextName, attr.name, 0) : attr.name; - if (newCollectionName === undefined) { - return; - } - const _parent = parent === "root" ? "_root_" : parent; - const createCollectionRequest = createMessage("create", `${ctxStr(dataContextName)}.collection`, { - "name": newCollectionName, - "title": newCollectionName, - parent: _parent, - }); - - return codapInterface.sendRequest(createCollectionRequest, (createCollResult: IResult) => { - if (createCollResult.success) { - const moveAttributeRequest = createMessage("update", `${ctxStr(dataContextName)}.${collStr(oldCollectionName)}.attributeLocation[${attr.name}]`, { - "collection": newCollectionName, - "position": 0 - }); - return codapInterface.sendRequest(moveAttributeRequest); - } - }); - }); -}; - -////////////// case functions ////////////// - -export const getCaseCount = (dataContextName: string, collectionName: string) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.caseCount`; - return sendMessage("get", resource); -}; - -export const getCaseByIndex = (dataContextName: string, collectionName: string, index: number) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.caseByIndex[${index}]`; - return sendMessage("get", resource); -}; - -export const getCaseByID = (dataContextName: string, caseID: number | string) => { - const resource = `${ctxStr(dataContextName)}.caseByID[${caseID}]`; - return sendMessage("get", resource); -}; - -export const getCaseBySearch = (dataContextName: string, collectionName: string, search: string) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.caseSearch[${search}]`; - return sendMessage("get", resource); -}; - -export const getCaseByFormulaSearch = (dataContextName: string, collectionName: string, search: string) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.caseFormulaSearch[${search}]`; - return sendMessage("get", resource); -}; - -export const createSingleOrParentCase = (dataContextName: string, collectionName: string, values: Array) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.case`; - return sendMessage("create", resource, values); -}; - -export const createChildCase = (dataContextName: string, collectionName: string, parentCaseID: number|string, values: CodapItemValues) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.case`; - const valuesWithParent = [ - { - parent: parentCaseID, - values - } - ]; - return sendMessage("create", resource, valuesWithParent); -}; - -export const updateCaseById = (dataContextName: string, caseID: number|string, values: CodapItemValues) => { - const resource = `${ctxStr(dataContextName)}.caseByID[${caseID}]`; - const updateValues = { - values - }; - return sendMessage("update", resource, updateValues); -}; - -export const updateCases = (dataContextName: string, collectionName: string, values: CodapItem[]) => { - const resource = `${ctxStr(dataContextName)}.${collStr(collectionName)}.case`; - return sendMessage("update", resource, values); -}; - -export const selectCases = (dataContextName: string, caseIds: Array) => { - return sendMessage("create", `${ctxStr(dataContextName)}.selectionList`, caseIds); -}; - -////////////// item functions ////////////// - -export const getItemCount = (dataContextName: string) => { - return sendMessage("get", `${ctxStr(dataContextName)}.itemCount`); -}; - -export const getAllItems = (dataContextName: string) =>{ - return sendMessage("get", `${ctxStr(dataContextName)}.itemSearch[*]`); -}; - -export const getItemByID = (dataContextName: string, itemID: number | string) => { - return sendMessage("get", `${ctxStr(dataContextName)}.itemByID[${itemID}]`); -}; - -export const getItemByIndex = (dataContextName: string, index: number) => { - return sendMessage("get", `${ctxStr(dataContextName)}.item[${index}]`); -}; - -export const getItemByCaseID = (dataContextName: string, caseID: number | string) => { - return sendMessage("get", `${ctxStr(dataContextName)}.itemByCaseID[${caseID}]`); -}; - -export const getItemBySearch = (dataContextName: string, search: string) => { - return sendMessage("get", `${ctxStr(dataContextName)}.itemSearch[${search}]`); -}; - -export const createItems = (dataContextName: string, items: Array) => { - return sendMessage("create", `${ctxStr(dataContextName)}.item`, items); -}; - -export const updateItemByID = (dataContextName: string, itemID: number | string, values: CodapItemValues) => { - return sendMessage("update", `${ctxStr(dataContextName)}.itemByID[${itemID}]`, values); -}; - -export const updateItemByIndex = (dataContextName: string, index: number, values: CodapItemValues) => { - return sendMessage("update", `${ctxStr(dataContextName)}.item[${index}]`, values); -}; - -export const updateItemByCaseID = (dataContextName: string, caseID: number | string, values: CodapItemValues) => { - return sendMessage("update", `${ctxStr(dataContextName)}.itemByCaseID[${caseID}]`, values); -}; diff --git a/src/utils/codap-utils.ts b/src/utils/codap-utils.ts deleted file mode 100644 index 70d3fa2..0000000 --- a/src/utils/codap-utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { codapInterface, getAttribute } from "@concord-consortium/codap-plugin-api"; -import { IResult } from "./codap-api-helpers"; - -export const getCodapAttribute = async (dataContextName: string, collectionName: string, attributeName: string) => { - const attribute = await getAttribute(dataContextName, collectionName, attributeName); - return attribute; -}; - -export const createGraph = async (dataContextName: string, graphName: string, xAttribute: string, yAttribute: string) => { - const graph = { - "action": "create", - "resource": "component", - "values": { - "type": "graph", - "name": graphName, - "dataContext": dataContextName, - "xAttributeName": xAttribute, - "yAttributeName": yAttribute - } - }; - - const res = await codapInterface.sendRequest(graph) as any as IResult; - return { requestMessage: graph, ...res }; -}; diff --git a/src/utils/openai-utils.ts b/src/utils/openai-utils.ts index 674c1eb..2d3d6d4 100644 --- a/src/utils/openai-utils.ts +++ b/src/utils/openai-utils.ts @@ -15,58 +15,31 @@ export const openAiTools: AssistantTool[] = [ { type: "function", function: { - name: "get_attributes", - description: "Get a list of all attributes in a dataset", - strict: true, + name: "create_request", + description: "Create a request to get data from CODAP", + strict: false, parameters: { type: "object", properties: { - dataset: { + action: { type: "string", - description: "The specified dataset containing attributes" - } - }, - additionalProperties: false, - required: [ - "dataset" - ] - } - } - }, - { - type: "function", - function: { - name: "create_graph", - description: "Create a graph tile in CODAP", - strict: true, - parameters: { - type: "object", - properties: { - dataset: { - type: "string", - description: "The name of the dataset to which the attributes belong" - }, - name: { - type: "string", - description: "A name for the graph" - }, - xAttribute: { - type: "string", - description: "The x-axis attribute" + description: "The action to perform" }, - yAttribute: { + resource: { type: "string", - description: "The y-axis attribute" + description: "The resource to act upon" }, + values: { + type: "object", + description: "The values to pass to the action" + } }, additionalProperties: false, required: [ - "dataset", - "name", - "xAttribute", - "yAttribute" + "action", + "resource" ] } } - } + }, ];