Skip to content

Commit

Permalink
checkpoint: user can select assistant id when in dev mode.
Browse files Browse the repository at this point in the history
  • Loading branch information
lublagg committed Dec 20, 2024
1 parent 863ce39 commit e05ff31
Show file tree
Hide file tree
Showing 17 changed files with 137 additions and 108 deletions.
6 changes: 0 additions & 6 deletions src/app-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
"accessibility": {
"keyboardShortcut": "ctrl+?"
},
"assistant": {
"assistantId": "asst_xmAX5oxByssXrkBymMbcsVEm",
"instructions": "You are DAVAI, a 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.",
"modelName": "gpt-4o-mini",
"useExisting": true
},
"dimensions": {
"height": 680,
"width": 380
Expand Down
13 changes: 6 additions & 7 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { observer } from "mobx-react-lite";
import { initializePlugin, selectSelf } from "@concord-consortium/codap-plugin-api";
import { useAppConfigContext } from "../hooks/use-app-config-context";
import { useAssistantStore } from "../hooks/use-assistant-store";
import { useOpenAIContext } from "../hooks/use-open-ai-context";

Check warning on line 6 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

'useOpenAIContext' is defined but never used

Check warning on line 6 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

'useOpenAIContext' is defined but never used
import { ChatInputComponent } from "./chat-input";
import { ChatTranscriptComponent } from "./chat-transcript";
import { ReadAloudMenu } from "./readaloud-menu";
import { KeyboardShortcutControls } from "./keyboard-shortcut-controls";
import { DAVAI_SPEAKER, GREETING, USER_SPEAKER } from "../constants";
import { DAVAI_SPEAKER, defaultAssistantId, GREETING, USER_SPEAKER } from "../constants";
import { DeveloperOptionsComponent } from "./developer-options";
import { getUrlParam } from "../utils/utils";

Expand All @@ -34,7 +35,9 @@ export const App = observer(() => {
useEffect(() => {
initializePlugin({pluginName: kPluginName, version: kVersion, dimensions});
selectSelf();
assistantStore.initialize();
if (!isDevMode) {
assistantStore.initializeAssistant(defaultAssistantId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down Expand Up @@ -104,10 +107,6 @@ export const App = observer(() => {
}
};

if (!assistantStore.assistant) {
return <div>Loading...</div>;
}

return (
<div className="App">
<header>
Expand Down Expand Up @@ -142,7 +141,7 @@ export const App = observer(() => {
</div>
}
<ChatInputComponent
disabled={!assistantStore.thread && !appConfig.isAssistantMocked}
disabled={!assistantStore.assistant || !appConfig.isAssistantMocked}
keyboardShortcutEnabled={keyboardShortcutEnabled}
shortcutKeys={keyboardShortcutKeys}
onSubmit={handleChatInputSubmit}
Expand Down
44 changes: 41 additions & 3 deletions src/components/developer-options.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react";
import React, { SyntheticEvent, useEffect, useState } from "react";

Check warning on line 1 in src/components/developer-options.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

'SyntheticEvent' is defined but never used

Check warning on line 1 in src/components/developer-options.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

'SyntheticEvent' is defined but never used
import { observer } from "mobx-react-lite";
import { AssistantModelType } from "../models/assistant-model";
import { useAppConfigContext } from "../hooks/use-app-config-context";

import "./developer-options.scss";
import { useOpenAIContext } from "../hooks/use-open-ai-context";

interface IProps {
assistantStore: AssistantModelType;
Expand All @@ -14,9 +15,46 @@ interface IProps {

export const DeveloperOptionsComponent = observer(function DeveloperOptions({assistantStore, onCreateThread, onDeleteThread, onMockAssistant}: IProps) {
const appConfig = useAppConfigContext();
const apiConnection = useOpenAIContext();
const [assistantOptions, setAssistantOptioms] = useState<string[]>();

useEffect(() => {
const fetchAssistants = async () => {
try {
const res = await apiConnection.beta.assistants.list();
const assistantIds = res.data.map(asst => asst.id);
setAssistantOptioms(assistantIds);
} catch (err) {
console.error(err);
}
};

fetchAssistants();
}, [apiConnection.beta.assistants]);

const handleSetSelectedAssistant = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = e.target.value;
assistantStore.initializeAssistant(id);
};

return (
<div className="developer-options" data-testid="developer-options">
<label htmlFor="mock-assistant-checkbox" data-testid="mock-assistant-checkbox-label">
<select
value={assistantStore.assistantId ? assistantStore.assistantId : "default"}
onChange={handleSetSelectedAssistant}
>
<option value="default" disabled>
-- Select an assistant --
</option>
{assistantOptions?.map((id) => {
return (
<option aria-selected={assistantStore.assistantId === id} key={id}>
{id}
</option>
);
})}
</select>
<input
checked={appConfig.isAssistantMocked}
data-testid="mock-assistant-checkbox"
Expand All @@ -28,14 +66,14 @@ export const DeveloperOptionsComponent = observer(function DeveloperOptions({ass
</label>
<button
data-testid="delete-thread-button"
disabled={!assistantStore.thread}
disabled={!assistantStore.assistant || !assistantStore.thread}
onClick={onDeleteThread}
>
Delete Thread
</button>
<button
data-testid="new-thread-button"
disabled={assistantStore.thread || appConfig.isAssistantMocked}
disabled={!assistantStore.assistant || assistantStore.thread || appConfig.isAssistantMocked}
onClick={onCreateThread}
>
New Thread
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const DAVAI_SPEAKER = "DAVAI";
export const USER_SPEAKER = "User";

export const GREETING = `Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.`;

export const defaultAssistantId = "asst_xmAX5oxByssXrkBymMbcsVEm";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext } from "react";
import { AppConfigModelType } from "./models/app-config-model";
import { AppConfigModelType } from "../models/app-config-model";

export const AppConfigContext = createContext<AppConfigModelType | undefined>(undefined);
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import { AppConfig, isAppMode } from "./types";
import appConfigJson from "./app-config.json";
import { AppConfigModel, AppConfigModelSnapshot } from "./models/app-config-model";
import { getUrlParam } from "./utils/utils";
import { AppConfig, isAppMode } from "../types";
import appConfigJson from "../app-config.json";
import { AppConfigModel, AppConfigModelSnapshot } from "../models/app-config-model";
import { getUrlParam } from "../utils/utils";
import { AppConfigContext } from "./app-config-context";

export const loadAppConfig = (): AppConfig => {
Expand All @@ -21,5 +21,9 @@ export const loadAppConfig = (): AppConfig => {
export const AppConfigProvider = ({ children }: { children: React.ReactNode }) => {
const appConfigSnapshot = loadAppConfig() as AppConfigModelSnapshot;
const appConfig = AppConfigModel.create(appConfigSnapshot);
return <AppConfigContext.Provider value={appConfig}>{children}</AppConfigContext.Provider>;
return (
<AppConfigContext.Provider value={appConfig}>
{children}
</AppConfigContext.Provider>
);
};
23 changes: 23 additions & 0 deletions src/contexts/open-ai-connection-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { createContext } from "react";
import { OpenAI } from "openai";

export const createNewConnection = () => {
return new OpenAI({
apiKey: process.env.REACT_APP_OPENAI_API_KEY || "fake-key",
baseURL: process.env.REACT_APP_OPENAI_BASE_URL,
dangerouslyAllowBrowser: true,
organization: "org-jbU1egKECzYlQI73HMMi7EOZ",
project: "proj_VsykADfoZHvqcOJUHyVAYoDG",
});
};

export const OpenAIConnectionContext = createContext<OpenAI|undefined>(undefined);

export const OpenAIConnectionProvider = ({ children }: {children: React.ReactNode}) => {
const apiConnection = createNewConnection();
return (
<OpenAIConnectionContext.Provider value={apiConnection}>
{children}
</OpenAIConnectionContext.Provider>
)

Check warning on line 22 in src/contexts/open-ai-connection-provider.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Missing semicolon

Check warning on line 22 in src/contexts/open-ai-connection-provider.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Missing semicolon
}

Check warning on line 23 in src/contexts/open-ai-connection-provider.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Newline required at end of file but not found

Check warning on line 23 in src/contexts/open-ai-connection-provider.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Missing semicolon

Check warning on line 23 in src/contexts/open-ai-connection-provider.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Newline required at end of file but not found

Check warning on line 23 in src/contexts/open-ai-connection-provider.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Missing semicolon
2 changes: 1 addition & 1 deletion src/hooks/use-app-config-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from "react";
import { AppConfigModelType } from "../models/app-config-model";
import { AppConfigContext } from "../app-config-context";
import { AppConfigContext } from "../contexts/app-config-context";

export const useAppConfigContext = (): AppConfigModelType => {
const context = useContext(AppConfigContext);
Expand Down
15 changes: 4 additions & 11 deletions src/hooks/use-assistant-store.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { useMemo } from "react";
import { useAppConfigContext } from "./use-app-config-context";
import { AssistantModel } from "../models/assistant-model";
import { useChatTranscriptStore } from "./use-chat-transcript-store";
import { useOpenAIContext } from "./use-open-ai-context";

export const useAssistantStore = () => {
const appConfig = useAppConfigContext();
const apiConnection = useOpenAIContext();
const transcriptStore = useChatTranscriptStore();
const { assistantId, instructions, modelName, useExisting } = appConfig.assistant;
const assistantStore = useMemo(() => {
return AssistantModel.create({
assistantId,
modelName,
instructions,
transcriptStore,
useExisting,
});
}, [assistantId, instructions, modelName, transcriptStore, useExisting]);
return AssistantModel.create({transcriptStore, apiConnection});
}, [transcriptStore, apiConnection]);

return assistantStore;
};
11 changes: 11 additions & 0 deletions src/hooks/use-open-ai-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContext } from "react";
import { OpenAI } from "openai";
import { OpenAIConnectionContext } from "../contexts/open-ai-connection-provider";

export const useOpenAIContext = (): OpenAI => {
const context = useContext(OpenAIConnectionContext);
if (!context) {
throw new Error("useOpenAIContext must be used within a OpenAIConnectionContext.Provider");
}
return context;
};
7 changes: 5 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./components/App";
import { AppConfigProvider } from "./app-config-provider";
import { AppConfigProvider } from "./contexts/app-config-provider";
import { OpenAIConnectionProvider } from "./contexts/open-ai-connection-provider";

import "./index.scss";

Expand All @@ -15,7 +16,9 @@ if (container) {

root.render(
<AppConfigProvider>
<App />
<OpenAIConnectionProvider>
<App />
</OpenAIConnectionProvider>
</AppConfigProvider>
);
}
21 changes: 5 additions & 16 deletions src/models/app-config-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,23 @@ import { AppMode, AppModeValues } from "../types";
/**
* AppConfigModel encapsulates the application's configuration settings.
* It includes properties and methods for managing accessibility, AI assistant settings, and the application's mode.
*
*
* @property {Object} accessibility - Settings related to accessibility in the UI.
* @property {string} accessibility.keyboardShortcut - Custom keystroke for placing focus in the main text input field (e.g., `ctrl+?`).
*
* @property {Object} assistant - Settings to configure the AI assistant.
* @property {string} assistant.assistantId - The unique ID of an existing assistant to use.
* @property {string} assistant.instructions - Instructions to use when creating new assistants (e.g., `You are helpful data analysis partner.`).
* @property {string} assistant.modelName - The name of the model the assistant should use (e.g., `gpt-4o-mini`).
* @property {boolean} assistant.useExisting - Whether to use an existing assistant.
*
*
*
* @property {Object} dimensions - Dimensions of the application's component within CODAP.
* @property {number} dimensions.width - The width of the application (in pixels).
* @property {number} dimensions.height - The height of the application (in pixels).
*
*
* @property {boolean|null} mockAssistant - A flag indicating whether to mock AI interactions. (optional).
*
*
* @property {"development"|"production"|"test"} mode - The mode in which the application runs.
*/
export const AppConfigModel = types.model("AppConfigModel", {
accessibility: types.model({
keyboardShortcut: types.string,
}),
assistant: types.model({
assistantId: types.string,
instructions: types.string,
modelName: types.string,
useExisting: types.boolean,
}),
dimensions: types.model({
width: types.number,
height: types.number,
Expand Down
56 changes: 27 additions & 29 deletions src/models/assistant-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,44 @@ import { Message } from "openai/resources/beta/threads/messages";
import { codapInterface } from "@concord-consortium/codap-plugin-api";
import { DAVAI_SPEAKER, DEBUG_SPEAKER } from "../constants";
import { formatJsonMessage } from "../utils/utils";
import { getTools, initLlmConnection } from "../utils/llm-utils";
import { ChatTranscriptModel } from "./chat-transcript-model";
import { requestThreadDeletion } from "../utils/openai-utils";
import { ChatTranscriptModel } from "./chat-transcript-model";
import { OpenAI } from "openai";

const OpenAIType = types.custom({
name: "OpenAIType",
fromSnapshot(snapshot: OpenAI) {
return new OpenAI({ apiKey: snapshot.apiKey });
},
toSnapshot() {
return undefined; // OpenAI instance is non-serializable
},
isTargetType(value) {
return value instanceof OpenAI;
},
getValidationMessage() {
return "";
},
});

/**
* AssistantModel encapsulates the AI assistant and its interactions with the user.
* It includes properties and methods for configuring the assistant, handling chat interactions, and maintaining the assistant's
* thread and transcript.
*
* @property {Object|null} assistant - The assistant object, or `null` if not initialized.
* @property {string} assistantId - The unique ID of the assistant being used.
* @property {string} instructions - Instructions provided when creating or configuring a new assistant.
* @property {string} modelName - The identifier for the assistant's model (e.g., "gpt-4o-mini").
* @property {Object|null} apiConnection - The API connection object for interacting with the assistant, or `null` if not connected.
* @property {string} assistantId - The unique ID of the assistant being used, or `null` if not initialized.
* @property {Object} apiConnection - The API connection object for interacting with the assistant
* @property {Object|null} thread - The assistant's thread used for the current chat, or `null` if no thread is active.
* @property {ChatTranscriptModel} transcriptStore - The assistant's chat transcript store for recording and managing chat messages.
* @property {boolean} useExisting - A flag indicating whether to use an existing assistant (`true`) or create a new one (`false`).
*/
export const AssistantModel = types
.model("AssistantModel", {
apiConnection: OpenAIType,
assistant: types.maybe(types.frozen()),
assistantId: types.string,
instructions: types.string,
modelName: types.string,
apiConnection: types.maybe(types.frozen()),
assistantId: types.maybe(types.string),
thread: types.maybe(types.frozen()),
transcriptStore: ChatTranscriptModel,
useExisting: true,
})
.volatile(() => ({
isLoadingResponse: false,
Expand All @@ -46,24 +56,12 @@ export const AssistantModel = types
}, 1000);
}
}))
.actions((self) => ({
afterCreate(){
self.apiConnection = initLlmConnection();
}
}))
.actions((self) => {
const initialize = flow(function* () {
const initializeAssistant = flow(function* (id: string) {
try {
const tools = getTools();

const davaiAssistant = self.useExisting && self.assistantId
? yield self.apiConnection.beta.assistants.retrieve(self.assistantId)
: yield self.apiConnection.beta.assistants.create({instructions: self.instructions, model: self.modelName, tools });

if (!self.useExisting) {
self.assistantId = davaiAssistant.id;
}
self.assistant = davaiAssistant;
if (!self.apiConnection) throw new Error("API connection is not initialized");
self.assistantId = id;
self.assistant = yield self.apiConnection.beta.assistants.retrieve(id);
self.thread = yield self.apiConnection.beta.threads.create();
self.transcriptStore.addMessage(DEBUG_SPEAKER, {
description: "You are chatting with assistant",
Expand Down Expand Up @@ -235,7 +233,7 @@ export const AssistantModel = types
}
});

return { createThread, deleteThread, initialize, handleMessageSubmit };
return { createThread, deleteThread, initializeAssistant, handleMessageSubmit };
});

export interface AssistantModelType extends Instance<typeof AssistantModel> {}
Loading

0 comments on commit e05ff31

Please sign in to comment.