Skip to content

Commit

Permalink
Merge pull request #20 from concord-consortium/188650126-non-develope…
Browse files Browse the repository at this point in the history
…r-model-training

feat: user can select assistant id when in dev mode (PT-188650126)
  • Loading branch information
emcelroy authored Jan 2, 2025
2 parents 863ce39 + e9de612 commit d56ed82
Show file tree
Hide file tree
Showing 24 changed files with 277 additions and 168 deletions.
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,10 @@ Default configuration setting values are defined in the `app-config.json` file.
Settings related to accessibility in the UI:
- **`keyboardShortcut`** (string): Custom keystroke for placing focus in the main text input field (e.g., `ctrl+?`).

### Assistant

- **`assistant`** (Object)
Settings to configure the AI assistant:
- **`assistantId`** (string): The unique ID of an existing assistant to use.
- **`instructions`** (string): Instructions to use when creating new assistants (e.g., `You are helpful data analysis partner.`).
- **`modelName`** (string): The name of the model the assistant should use (e.g., `gpt-4o-mini`).
- **`useExisting`** (boolean): Whether to use an existing assistant.
### AssistantId

- **`assistantId`** (string)
The unique ID of an existing assistant to use, or "mock" for a mocked assistant.

### Dimensions

Expand Down
4 changes: 3 additions & 1 deletion cypress/e2e/workspace.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
context("Test the overall app", () => {
it("renders without crashing", () => {
cy.visit("/");
cy.get("body").should("contain", "Loading...");
cy.get("body").should("contain", "DAVAI");
cy.get("[data-testid=chat-transcript]").should("exist");
cy.get("[data-testid=chat-input]").should("exist");
});
});
7 changes: 1 addition & 6 deletions src/app-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
"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
},
"assistantId": "asst_xmAX5oxByssXrkBymMbcsVEm",
"dimensions": {
"height": 680,
"width": 380
Expand Down
7 changes: 5 additions & 2 deletions src/components/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "openai/shims/node";
import React from "react";
import { render, screen } from "@testing-library/react";
import { App } from "./App";
Expand All @@ -6,7 +7,7 @@ import { MockAppConfigProvider } from "../test-utils/app-config-provider";

jest.mock("../hooks/use-assistant-store", () => ({
useAssistantStore: jest.fn(() => ({
initialize: jest.fn(),
initializeAssistant: jest.fn(),
transcriptStore: {
messages: [],
addMessage: jest.fn(),
Expand All @@ -29,6 +30,8 @@ describe("test load app", () => {
<App />
</MockAppConfigProvider>
);
expect(screen.getByText("Loading...")).toBeDefined();
expect(screen.getByText("DAVAI")).toBeDefined();
expect(screen.getByTestId("chat-transcript")).toBeDefined();
expect(screen.getByTestId("chat-input")).toBeDefined();
});
});
32 changes: 17 additions & 15 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ export const App = observer(() => {
useEffect(() => {
initializePlugin({pluginName: kPluginName, version: kVersion, dimensions});
selectSelf();
assistantStore.initialize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
assistantStore.initializeAssistant();
}, [assistantStore, appConfig.assistantId]);

const handleFocusShortcut = () => {
selectSelf();
};
Expand Down Expand Up @@ -89,24 +92,23 @@ export const App = observer(() => {
return true;
};

const handleMockAssistant = async () => {
if (!appConfig.isAssistantMocked) {
// If we switch to a mocked assistant, we delete the current thread and clear the transcript.
// First make sure the user is OK with that.
const threadDeleted = await handleDeleteThread();
if (!threadDeleted) return;
const handleSelectAssistant = async (id: string) => {
// If we switch assistants, we delete the current thread and clear the transcript.
// First make sure the user is OK with that.
const threadDeleted = await handleDeleteThread();
if (!threadDeleted) return;

if (id === "mock") {
transcriptStore.clearTranscript();
transcriptStore.addMessage(DAVAI_SPEAKER, {content: GREETING});
appConfig.toggleMockAssistant();
} else {
appConfig.toggleMockAssistant();
appConfig.setMockAssistant(true);
appConfig.setAssistantId(id);
return;
}
};

if (!assistantStore.assistant) {
return <div>Loading...</div>;
}
appConfig.setMockAssistant(false);
appConfig.setAssistantId(id);
};

return (
<div className="App">
Expand Down Expand Up @@ -170,7 +172,7 @@ export const App = observer(() => {
assistantStore={assistantStore}
onCreateThread={handleCreateThread}
onDeleteThread={handleDeleteThread}
onMockAssistant={handleMockAssistant}
onSelectAssistant={handleSelectAssistant}
/>
</>
}
Expand Down
7 changes: 6 additions & 1 deletion src/components/developer-options.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@
font-size: .75rem;
font-weight: normal;
line-height: 1.4;
margin: 0 0 0 5px;
margin: 0 10px;
padding: 0;
user-select: none;
white-space: nowrap;
}

select {
margin: 0 10px 10px;
padding: 7px 10px;
}
}
48 changes: 28 additions & 20 deletions src/components/developer-options.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import "openai/shims/node";
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";

import { DeveloperOptionsComponent } from "./developer-options";
import { AssistantModel } from "../models/assistant-model";
import { ChatTranscriptModel } from "../models/chat-transcript-model";
import { MockAppConfigProvider } from "../test-utils/app-config-provider";
import { mockAppConfig } from "../test-utils/mock-app-config";
import { MockOpenAiConnectionProvider } from "../test-utils/openai-connection-provider";

const mockTranscriptStore = ChatTranscriptModel.create({
messages: [
Expand All @@ -20,13 +21,14 @@ const mockTranscriptStore = ChatTranscriptModel.create({
});

const mockAssistantStore = AssistantModel.create({
apiConnection: {
apiKey: "abc123",
dangerouslyAllowBrowser: true
},
assistant: {},
assistantId: "asst_abc123",
instructions: "This is just a test",
modelName: "test-model",
thread: {},
transcriptStore: mockTranscriptStore,
useExistingAssistant: true,
});

jest.mock("../models/app-config-model", () => ({
Expand All @@ -39,35 +41,41 @@ jest.mock("../models/app-config-model", () => ({
describe("test developer options component", () => {
const onCreateThread = jest.fn();
const onDeleteThread = jest.fn();
const onMockAssistant = jest.fn();
const onSelectAssistant = jest.fn();

const WrapperComponent = () => {
return (
<MockAppConfigProvider>
<DeveloperOptionsComponent
assistantStore={mockAssistantStore}
onCreateThread={onCreateThread}
onDeleteThread={onDeleteThread}
onMockAssistant={onMockAssistant}
/>
<MockOpenAiConnectionProvider>
<DeveloperOptionsComponent
assistantStore={mockAssistantStore}
onCreateThread={onCreateThread}
onDeleteThread={onDeleteThread}
onSelectAssistant={onSelectAssistant}
/>
</MockOpenAiConnectionProvider>
</MockAppConfigProvider>
);
};

it("renders a developer options component with mock assistant checkbox and thread buttons", () => {
it("renders a developer options component with mock assistant checkbox and thread buttons", async () => {
render(<WrapperComponent />);

const developerOptions = screen.getByTestId("developer-options");
expect(developerOptions).toBeInTheDocument();

const mockAssistantCheckbox = screen.getByTestId("mock-assistant-checkbox");
expect(mockAssistantCheckbox).toBeInTheDocument();
expect(mockAssistantCheckbox).toHaveAttribute("type", "checkbox");
expect(mockAssistantCheckbox).toHaveProperty("checked", false);
const mockAssistantCheckboxLabel = screen.getByTestId("mock-assistant-checkbox-label");
expect(mockAssistantCheckboxLabel).toHaveTextContent("Use Mock Assistant");
fireEvent.click(mockAssistantCheckbox);
expect(onMockAssistant).toHaveBeenCalledTimes(1);
const selectAssistantOptionLabel = screen.getByTestId("assistant-select-label");
expect(selectAssistantOptionLabel).toHaveTextContent("Select an Assistant");
const selectAssistantOption = screen.getByTestId("assistant-select");
expect(selectAssistantOption).toBeInTheDocument();
await waitFor(() => {
expect(selectAssistantOption).toHaveValue("asst_abc123");
});
await waitFor(() => {
expect(selectAssistantOption).toHaveTextContent("Jest Mock Assistant");
});
fireEvent.change(selectAssistantOption, { target: { value: "mock" } });
expect(onSelectAssistant).toHaveBeenCalledTimes(1);

const deleteThreadButton = screen.getByTestId("delete-thread-button");
expect(deleteThreadButton).toBeInTheDocument();
Expand Down
67 changes: 53 additions & 14 deletions src/components/developer-options.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,80 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { OpenAI } from "openai";
import { observer } from "mobx-react-lite";
import { AssistantModelType } from "../models/assistant-model";
import { useAppConfigContext } from "../hooks/use-app-config-context";
import { useOpenAIContext } from "../hooks/use-openai-context";

import "./developer-options.scss";

interface IProps {
assistantStore: AssistantModelType;
onCreateThread: () => void;
onDeleteThread: () => void;
onMockAssistant: () => void;
onSelectAssistant: (id: string) => void;
}

export const DeveloperOptionsComponent = observer(function DeveloperOptions({assistantStore, onCreateThread, onDeleteThread, onMockAssistant}: IProps) {
export const DeveloperOptionsComponent = observer(function DeveloperOptions({assistantStore, onCreateThread, onDeleteThread, onSelectAssistant}: IProps) {
const appConfig = useAppConfigContext();
const apiConnection = useOpenAIContext();
const selectedAssistant = assistantStore.assistantId ? assistantStore.assistantId : "mock";
const [assistantOptions, setAssistantOptions] = useState<Map<string, string>>();

useEffect(() => {
const fetchAssistants = async () => {
try {
const res = await apiConnection.beta.assistants.list();
const assistants = new Map();
res.data.map((assistant: OpenAI.Beta.Assistant) => {
const assistantName = assistant.name || assistant.id;
assistants.set(assistant.id, assistantName);
});
setAssistantOptions(assistants);
} catch (err) {
console.error(err);
}
};

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

const handleSelectAssistant = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = e.target.value;
onSelectAssistant(id);
};

return (
<div className="developer-options" data-testid="developer-options">
<label htmlFor="mock-assistant-checkbox" data-testid="mock-assistant-checkbox-label">
<input
checked={appConfig.isAssistantMocked}
data-testid="mock-assistant-checkbox"
id="mock-assistant-checkbox"
type="checkbox"
onChange={onMockAssistant}
/>
Use Mock Assistant
<label htmlFor="assistant-select" data-testid="assistant-select-label">
Select an Assistant
</label>
<select
id="assistant-select"
data-testid="assistant-select"
value={selectedAssistant}
onChange={handleSelectAssistant}
>
<option value="mock">Mock Assistant</option>
{Array.from(assistantOptions?.entries() || []).map(([assistantId, assistantName]) => (
<option
aria-selected={assistantStore.assistantId === assistantId}
key={assistantId}
value={assistantId}
>
{assistantName}
</option>
))}
</select>
<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
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ 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.`;

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,15 +1,17 @@
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 => {
const defaultConfig = appConfigJson as AppConfig;
const urlParamMode = getUrlParam("mode");
const assistantId = getUrlParam("assistantId");
const configOverrides: Partial<AppConfig> = {
mode: isAppMode(urlParamMode) ? urlParamMode : defaultConfig.mode
mode: isAppMode(urlParamMode) ? urlParamMode : defaultConfig.mode,
assistantId: assistantId || defaultConfig.assistantId,
};

return {
Expand All @@ -21,5 +23,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/openai-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>
);
};
Loading

0 comments on commit d56ed82

Please sign in to comment.