├── DFCI-GPT4DFCI-Backend ├── app │ ├── __init__.py │ ├── utils.py │ ├── models.py │ └── main.py ├── tests │ ├── test_add_messages.py │ ├── test_get_convo.py │ ├── test_get_messages.py │ ├── test_create_completion.py │ ├── conftest.py │ ├── test_update_convo.py │ ├── test_get_convos.py │ └── test_create_convo.py ├── gunicorn.conf.py ├── Dockerfile ├── ocr-pipeline.yaml ├── pyproject.toml └── README.md ├── DFCI-GPT4DFCI ├── src │ ├── vite-env.d.ts │ ├── components │ │ ├── Layout │ │ │ ├── index.ts │ │ │ ├── LayoutDrawer.tsx │ │ │ └── Layout.tsx │ │ ├── Sidebar │ │ │ ├── index.ts │ │ │ └── Sidebar.tsx │ │ ├── ConvoList │ │ │ ├── index.ts │ │ │ ├── ConvoList.tsx │ │ │ └── ConvoListItem.tsx │ │ ├── ModelSelect │ │ │ ├── index.ts │ │ │ └── ModelSelect.tsx │ │ ├── OptionsMenu │ │ │ ├── index.ts │ │ │ ├── AboutModal.tsx │ │ │ └── OptionsMenu.tsx │ │ ├── UserProfile │ │ │ ├── index.ts │ │ │ └── UserProfile.tsx │ │ ├── ConvoDisplay │ │ │ ├── index.ts │ │ │ └── ConvoDisplay.tsx │ │ ├── LandingDisplay │ │ │ ├── index.ts │ │ │ ├── dfci-logo.jpg │ │ │ └── LandingDisplay.tsx │ │ ├── MessageBubble │ │ │ ├── index.ts │ │ │ └── MessageBubble.tsx │ │ ├── ExpandableInput │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ └── ExpandableInput.tsx │ │ └── ConfirmationDialog │ │ │ ├── index.ts │ │ │ └── ConfirmationDialog.tsx │ ├── utils.ts │ ├── theme.ts │ ├── hooks │ │ ├── useAutoScrollToBottom.ts │ │ ├── useConvos.ts │ │ └── useAPI.ts │ ├── main.tsx │ ├── services │ │ ├── UserService.ts │ │ ├── AssistantService.ts │ │ └── StorageService.ts │ ├── models │ │ └── index.ts │ ├── assets │ │ └── react.svg │ └── App.tsx ├── public │ └── favicon.ico ├── tsconfig.node.json ├── index.html ├── tests │ ├── App.spec.ts │ ├── ConvoList.spec.ts │ ├── ExpandableInput.ts │ └── mocks │ │ ├── UserService.ts │ │ ├── AssistantService.ts │ │ └── StorageService.ts ├── vite.config.ts ├── tsconfig.json ├── README.md ├── package.json └── playwright.config.ts ├── GPT4DFCI User Technical Training └── GPT4DFCI Training v204.pdf ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── DFCI-GPT4DFCI-infra ├── README.md └── infra-pulumi-alpha.py ├── CITATION.cff ├── README.md └── LICENSE /DFCI-GPT4DFCI-Backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/test_add_messages.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | import Layout from "./Layout"; 2 | 3 | export default Layout; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | import Sidebar from "./Sidebar"; 2 | 3 | export default Sidebar; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ConvoList/index.ts: -------------------------------------------------------------------------------- 1 | import ConvoList from "./ConvoList"; 2 | 3 | export default ConvoList; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dana-Farber-AIOS/GPT4DFCI/HEAD/DFCI-GPT4DFCI/public/favicon.ico -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ModelSelect/index.ts: -------------------------------------------------------------------------------- 1 | import ModelSelect from "./ModelSelect"; 2 | 3 | export default ModelSelect; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/OptionsMenu/index.ts: -------------------------------------------------------------------------------- 1 | import OptionsMenu from "./OptionsMenu"; 2 | 3 | export default OptionsMenu; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/UserProfile/index.ts: -------------------------------------------------------------------------------- 1 | import UserProfile from "./UserProfile"; 2 | 3 | export default UserProfile; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ConvoDisplay/index.ts: -------------------------------------------------------------------------------- 1 | import ConvoDisplay from "./ConvoDisplay"; 2 | 3 | export default ConvoDisplay; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/LandingDisplay/index.ts: -------------------------------------------------------------------------------- 1 | import LandingDisplay from "./LandingDisplay"; 2 | 3 | export default LandingDisplay; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/MessageBubble/index.ts: -------------------------------------------------------------------------------- 1 | import MessageBubble from "./MessageBubble"; 2 | 3 | export default MessageBubble; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ExpandableInput/index.ts: -------------------------------------------------------------------------------- 1 | import ExpandableInput from "./ExpandableInput"; 2 | 3 | export default ExpandableInput; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ConfirmationDialog/index.ts: -------------------------------------------------------------------------------- 1 | import ConfirmationDialog from "./ConfirmationDialog"; 2 | 3 | export default ConfirmationDialog; 4 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/LandingDisplay/dfci-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dana-Farber-AIOS/GPT4DFCI/HEAD/DFCI-GPT4DFCI/src/components/LandingDisplay/dfci-logo.jpg -------------------------------------------------------------------------------- /GPT4DFCI User Technical Training/GPT4DFCI Training v204.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dana-Farber-AIOS/GPT4DFCI/HEAD/GPT4DFCI User Technical Training/GPT4DFCI Training v204.pdf -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | # Gunicorn configuration file 2 | import multiprocessing 3 | 4 | max_requests = 1000 5 | max_requests_jitter = 50 6 | 7 | log_file = "-" 8 | 9 | bind = "0.0.0.0:3100" 10 | 11 | worker_class = "uvicorn.workers.UvicornWorker" 12 | workers = (multiprocessing.cpu_count() * 2) + 1 13 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ExpandableInput/utils.ts: -------------------------------------------------------------------------------- 1 | export const numLineBreaks = (text: string) => { 2 | const pattern = /\n|\r\n|\r/g; 3 | const matches = text.match(pattern) || []; 4 | 5 | return matches.length; 6 | }; 7 | 8 | export const numRows = (text: string, maxRows: number) => { 9 | return Math.min(Math.max(numLineBreaks(text) + 1, 1), maxRows); 10 | }; 11 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GPT4DFCI 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/utils.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type Newable = { new (...args: any[]): T }; 3 | 4 | export const throwResponseError = (response: Response) => { 5 | if (!response.ok) { 6 | throw new Error( 7 | `${response.statusText} (status: ${response.status}, type: ${response.type})` 8 | ); 9 | } 10 | return response.json() as Promise; 11 | }; 12 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM python:3.10 4 | 5 | WORKDIR /code 6 | 7 | COPY pyproject.toml poetry.lock . 8 | 9 | RUN pip install "poetry==1.6.1" 10 | RUN poetry --version 11 | RUN poetry export --without-hashes --format=requirements.txt > requirements.txt 12 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 13 | 14 | COPY . . 15 | 16 | EXPOSE 3100 17 | 18 | CMD ["gunicorn", "app.main:app"] 19 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/theme.ts: -------------------------------------------------------------------------------- 1 | import "@fontsource/inter/400.css"; 2 | import "@fontsource/inter/500.css"; 3 | import "@fontsource/inter/600.css"; 4 | import "@fontsource/inter/700.css"; 5 | 6 | import { extendTheme } from "@chakra-ui/react"; 7 | 8 | const colors = {}; 9 | const fonts = { 10 | heading: `'Inter', sans-serif`, 11 | body: `'Inter', sans-serif`, 12 | }; 13 | 14 | const theme = extendTheme({ colors, fonts }); 15 | 16 | export default theme; 17 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tests/App.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("page has GPT4DFCI in title", async ({ page }) => { 4 | await page.goto("/"); 5 | 6 | await expect(page).toHaveTitle(/GPT4DFCI/); 7 | }); 8 | 9 | test("landing display appears on page load", async ({ page }) => { 10 | await page.goto("/"); 11 | 12 | const LandingDisplay = await page.getByTestId("landing-display"); 13 | 14 | await expect(LandingDisplay).toBeVisible(); 15 | }); 16 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | "@": path.resolve(__dirname, "./src"), 11 | }, 12 | }, 13 | build: { 14 | outDir: "build", 15 | }, 16 | server: { 17 | watch: { 18 | usePolling: true, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/app/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | 4 | 5 | def generate_uuid(): 6 | return str(uuid.uuid4()) 7 | 8 | 9 | def generate_timestamp(): 10 | return datetime.datetime.utcnow().isoformat() 11 | 12 | 13 | def is_valid_uuid(to_check: str, version: int = 4): 14 | # reference: https://stackoverflow.com/a/33245493 15 | try: 16 | uuid_obj = uuid.UUID(to_check, version=version) 17 | 18 | except ValueError: 19 | return False 20 | 21 | return str(uuid_obj) == to_check 22 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/hooks/useAutoScrollToBottom.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect, useRef } from "react"; 2 | 3 | const useAutoScrollToBottom = (dependencies: DependencyList) => { 4 | const ref = useRef(null); 5 | const scrollToBottom = () => { 6 | ref.current?.scrollIntoView({ behavior: "smooth" }); 7 | }; 8 | 9 | /* eslint-disable react-hooks/exhaustive-deps */ 10 | useEffect(() => { 11 | scrollToBottom(); 12 | }, dependencies); 13 | 14 | return ref; 15 | }; 16 | 17 | export default useAutoScrollToBottom; 18 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import { ChakraProvider } from "@chakra-ui/react"; 5 | import theme from "./theme.ts"; 6 | 7 | /* Reference for cssVarsRoot=":root" 8 | https://github.com/chakra-ui/chakra-ui/issues/6253 */ 9 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/app/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal, Optional 2 | from pydantic import BaseModel 3 | 4 | 5 | class Message(BaseModel): 6 | id: str 7 | text: str 8 | sender: Literal["user", "assistant"] 9 | timestamp: str 10 | status: Literal["success", "error"] 11 | statusMessage: Optional[str] = None 12 | 13 | 14 | class Convo(BaseModel): 15 | id: Optional[str] 16 | userId: str 17 | title: str 18 | model: str 19 | timestamp: Optional[str] = None 20 | isArchived: bool = False 21 | 22 | 23 | class ChatCompletion(BaseModel): 24 | id: str 25 | object: str 26 | created: int 27 | model: str 28 | choices: List[dict] 29 | usage: dict 30 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/ocr-pipeline.yaml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main # Change this to your branch name 3 | 4 | pr: 5 | - main 6 | 7 | jobs: 8 | - job: BuildAndPush 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | steps: 12 | - checkout: self 13 | 14 | # - task: Docker@2 15 | # inputs: 16 | # containerRegistry: '' 17 | # repository: 'myapp' 18 | # command: 'buildAndPush' 19 | # Dockerfile: '**/Dockerfile' 20 | # tags: '$(Build.BuildId)' 21 | - task: Docker@2 22 | inputs: 23 | containerRegistry: ${{ secrets.AZURE_CONTAINER_REGISTRY }} 24 | repository: ${{ secrets.AZURE_CONTAINER_REPOSITORY }} 25 | command: 'buildAndPush' 26 | Dockerfile: '**/Dockerfile' 27 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "dfci-GPT4DFCI-backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Your Name "] 6 | readme = "README.md" 7 | packages = [{include = "app"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | azure-cosmos = "^4.5.0" 12 | azure-identity = "^1.14.0" 13 | fastapi = "^0.101.1" 14 | uvicorn = {extras = ["standard"], version = "^0.23.2"} 15 | python-dotenv = "^1.0.0" 16 | gunicorn = "^21.2.0" 17 | openai = "^0.27.8" 18 | mypy = "^1.5.1" 19 | 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | ruff = "^0.0.284" 23 | black = "^23.7.0" 24 | pytest = "^7.4.0" 25 | httpx = "^0.24.1" 26 | 27 | 28 | [build-system] 29 | requires = ["poetry-core"] 30 | build-backend = "poetry.core.masonry.api" 31 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* For alias */ 24 | "paths": { 25 | "@/*": ["./src/*", "./dist/*"] 26 | } 27 | }, 28 | "include": ["src", "tests"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ModelSelect/ModelSelect.tsx: -------------------------------------------------------------------------------- 1 | import { GPTModel } from "@/models"; 2 | import { Select, SelectProps } from "@chakra-ui/react"; 3 | 4 | interface ModelSelectProps extends Omit { 5 | value: GPTModel; 6 | options: GPTModel[]; 7 | onChange: (model: GPTModel) => void; 8 | } 9 | 10 | const ModelSelect = ({ 11 | value, 12 | options, 13 | onChange, 14 | ...props 15 | }: ModelSelectProps): JSX.Element => { 16 | return ( 17 | 29 | ); 30 | }; 31 | 32 | export default ModelSelect; 33 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tests/ConvoList.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("new list item is created when prompt is submitted", async ({ page }) => { 4 | await page.goto("/"); 5 | 6 | const countBefore = await page 7 | .getByTestId("convo-list") 8 | .getByRole("listitem") 9 | .count(); 10 | 11 | console.log(`Count before ${countBefore}`); 12 | const PromptInput = await page.getByTestId("prompt-input"); 13 | 14 | await PromptInput.click(); 15 | await PromptInput.fill("Hello, world"); 16 | await PromptInput.press("Enter"); 17 | 18 | const countAfter = await page 19 | .getByTestId("convo-list") 20 | .getByRole("listitem") 21 | .count(); 22 | 23 | await expect(countAfter).toBe(countBefore + 1); 24 | }); 25 | 26 | // test("archive list item works", async ({ page }) => { 27 | // }); 28 | 29 | // test("delete list item works", async ({ page }) => { 30 | // }); 31 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/OptionsMenu/AboutModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalBody, 4 | ModalCloseButton, 5 | ModalContent, 6 | ModalFooter, 7 | ModalHeader, 8 | ModalOverlay, 9 | UseDisclosureReturn, 10 | } from "@chakra-ui/react"; 11 | 12 | interface AboutModalProps { 13 | disclosure: UseDisclosureReturn; 14 | } 15 | 16 | const AboutModal = ({ disclosure }: AboutModalProps): JSX.Element => { 17 | const { isOpen, onClose } = disclosure; 18 | 19 | return ( 20 | 21 | 22 | 23 | About GPT4DFCI 24 | 25 | (Content goes here.) 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default AboutModal; 33 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/README.md: -------------------------------------------------------------------------------- 1 | # DFCI-GPT4DFCI-Backend 2 | 3 | ## Development setup 4 | 5 | 1. Install [Poetry](https://python-poetry.org/docs/) 6 | 2. Run `poetry install` in the project root 7 | 3. Run `poetry shell` to activate the virtual environment (also creates a sub-shell) 8 | 4. Run `uvnicorn main:app --reload` to start the local server 9 | 5. If the command was successful, http://127.0.0.1:8000/docs should show a Swagger/OpenAPI docs page 10 | 11 | ## Build notes 12 | https://learn.microsoft.com/en-us/azure/developer/python/tutorial-containerize-simple-web-app-for-app-service?tabs=web-app-fastapi 13 | 14 | * The `gunicorn.conf.py` file is from this tutorial. 15 | * Startup command is `gunicorn main:app` 16 | 17 | **How to export requirements.txt (if needed for build step)** 18 | ``` 19 | poetry export --without-hashes --format=requirements.txt > requirements.txt 20 | ``` 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/UserProfile/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@/models"; 2 | import { Avatar, Flex, Text } from "@chakra-ui/react"; 3 | 4 | interface UserSettingsProps { 5 | user?: User; 6 | } 7 | 8 | const UserSettings = ({ user }: UserSettingsProps): JSX.Element => { 9 | return ( 10 | 19 | 20 | 21 | 22 | {user ? `${user?.displayName}` : "Loading user..."} 23 | 24 | 25 | {user?.email} 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default UserSettings; 33 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/hooks/useConvos.ts: -------------------------------------------------------------------------------- 1 | import { ConversationMetadata } from "@/models"; 2 | import { useState } from "react"; 3 | 4 | /** Hook for editing "convos" state */ 5 | const useConvos = () => { 6 | const [convos, setConvos] = useState([]); 7 | 8 | const createConvo = (convo: ConversationMetadata) => { 9 | setConvos([...convos, convo]); 10 | }; 11 | 12 | const editTitle = (convo: ConversationMetadata, title: string) => { 13 | const index = convos.findIndex((c) => c.id === convo.id); 14 | 15 | const edited = [...convos]; 16 | edited[index].title = title; 17 | 18 | setConvos(edited); 19 | }; 20 | 21 | const archiveConvo = (convo: ConversationMetadata) => { 22 | setConvos(convos.filter((_convo) => _convo.id !== convo.id)); 23 | }; 24 | 25 | const archiveAllConvos = () => { 26 | setConvos([]); 27 | }; 28 | 29 | return { 30 | convos, 31 | createConvo, 32 | editTitle, 33 | archiveConvo, 34 | archiveAllConvos, 35 | setConvos, 36 | }; 37 | }; 38 | 39 | export default useConvos; 40 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/test_get_convo.py: -------------------------------------------------------------------------------- 1 | from azure.cosmos import ContainerProxy 2 | from fastapi.testclient import TestClient 3 | from app.utils import generate_uuid 4 | 5 | 6 | def test_correct_convo_is_returned(client: TestClient): 7 | container: ContainerProxy = client.app.state.cosmos_container 8 | 9 | convo = { 10 | "id": "does-not-matter", 11 | "userId": "does-not-matter", 12 | "title": "does-not-matter", 13 | "model": "does-not-matter", 14 | "timestamp": "does-not-matter", 15 | "isArchived": False, 16 | # omitting messages for convenience 17 | } 18 | 19 | container.create_item(convo) 20 | 21 | response = client.get( 22 | f"/convos/{convo['id']}", headers={"user-id": convo["userId"]} 23 | ) 24 | 25 | assert response.status_code == 200 26 | assert response.json() == convo 27 | 28 | 29 | def test_error_is_thrown_when_no_convo_exists(client: TestClient): 30 | user_id = generate_uuid() 31 | 32 | response_get = client.get( 33 | "/convos/id-that-does-not-exist", headers={"user-id": user_id} 34 | ) 35 | 36 | assert response_get.status_code == 404 37 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tests/ExpandableInput.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("enter clears input", async ({ page }) => { 4 | await page.goto("/"); 5 | 6 | const PromptInput = await page.getByTestId("prompt-input"); 7 | 8 | await PromptInput.click(); 9 | await PromptInput.fill("Hello, world"); 10 | await PromptInput.press("Enter"); 11 | 12 | await expect(PromptInput).toBeEmpty(); 13 | }); 14 | 15 | test("shift enter creates a new line", async ({ page }) => { 16 | await page.goto("/"); 17 | 18 | const PromptInput = await page.getByTestId("prompt-input"); 19 | 20 | await PromptInput.click(); 21 | await PromptInput.fill("Hello, world"); 22 | await PromptInput.press("Shift+Enter"); 23 | 24 | await expect(PromptInput).toHaveText("Hello, world\n"); 25 | }); 26 | 27 | test("new line expands input", async ({ page }) => { 28 | await page.goto("/"); 29 | 30 | const PromptInput = await page.getByTestId("prompt-input"); 31 | 32 | await PromptInput.click(); 33 | await PromptInput.fill("Hello, world"); 34 | await PromptInput.press("Shift+Enter"); 35 | 36 | await expect(PromptInput).toHaveJSProperty("rows", 2); 37 | }); 38 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tests/mocks/UserService.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@/models"; 2 | import { UserService } from "@/services/UserService"; 3 | 4 | export class MockUserService implements UserService { 5 | getUser(): Promise { 6 | const user: User = { 7 | token: "abcdef", 8 | displayName: "xxx", 9 | email: "xxx@DFCI.HARVARD.EDU", 10 | }; 11 | 12 | const msToSleep = 0; 13 | return new Promise((resolve) => setTimeout(resolve, msToSleep)).then( 14 | () => user 15 | ); 16 | } 17 | } 18 | 19 | export class UserServiceWithDelay implements UserService { 20 | getUser(): Promise { 21 | const user: User = { 22 | token: "", 23 | displayName: "xxx", 24 | email: "xxx@DFCI.HARVARD.EDU", 25 | }; 26 | 27 | const msToSleep = 1000; 28 | return new Promise((resolve) => setTimeout(resolve, msToSleep)).then( 29 | () => user 30 | ); 31 | } 32 | } 33 | 34 | export class UserServiceWithError implements UserService { 35 | getUser(): Promise { 36 | return new Promise((_, reject) => reject("Error fetching user.")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@/models"; 2 | 3 | export interface UserService { 4 | getUser(): Promise; 5 | } 6 | 7 | export class AADUserService implements UserService { 8 | async getUser(): Promise { 9 | const getUserData = (response: any) => { 10 | const data = response[0]; 11 | 12 | let claims = data["user_claims"]; 13 | 14 | // reformat 15 | claims = claims.reduce((result: any, claim: any) => { 16 | result[claim["typ"]] = claim["val"]; 17 | 18 | return result; 19 | }, {}); 20 | 21 | const user = { 22 | token: data["access_token"], 23 | displayName: claims["name"], 24 | email: claims["preferred_username"], 25 | }; 26 | 27 | return user; 28 | }; 29 | // https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-user-identities#access-user-claims-using-the-api 30 | const response = fetch("/.auth/me") 31 | .then((response) => response.json()) 32 | .then(getUserData); 33 | 34 | return response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/tests/mocks/AssistantService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssistantService, 3 | GetCompletionRequest, 4 | } from "@/services/AssistantService"; 5 | 6 | export class MockAssistantService implements AssistantService { 7 | getCompletion(request: GetCompletionRequest) { 8 | if (request.messages.length <= 0) { 9 | throw Error("Request must include at least one message."); 10 | } 11 | 12 | const response = `This is a response to a chat with ${request.messages.length} messages.`; 13 | const msToSleep = 1000; 14 | return new Promise((resolve) => setTimeout(resolve, msToSleep)).then( 15 | () => response 16 | ); 17 | } 18 | } 19 | 20 | export class MockAssistantServiceError implements AssistantService { 21 | getCompletion(request: GetCompletionRequest) { 22 | if (request.messages.length <= 0) { 23 | throw Error("Request must include at least one message."); 24 | } 25 | 26 | const msToSleep = 1000; 27 | return new Promise((resolve) => setTimeout(resolve, msToSleep)).then( 28 | () => { 29 | throw Error("This is a fake error."); 30 | } 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/README.md: -------------------------------------------------------------------------------- 1 | # GPT4DFCI Front-end Code 2 | 3 | ## Stack 4 | 5 | - **Language:** [TypeScript](https://www.typescriptlang.org/) 6 | - **Package managment:** [Yarn](https://yarnpkg.com/) 7 | - **Build tool:** [Vite](https://vitejs.dev/) 8 | - **Frontend library:** [React](https://react.dev/) 9 | - **UI component library:** [Chakra UI](https://chakra-ui.com/) 10 | - **Icons:** [Lucide Icons](https://lucide.dev/) 11 | - **Linting and formatting:** [ESLint](https://eslint.org/), [Prettier](https://prettier.io/) 12 | - **Testing:** [Playwright](https://playwright.dev/) 13 | 14 | ## Development setup 15 | 16 | To install dependencies: 17 | 18 | ``` 19 | yarn 20 | ``` 21 | 22 | To run dev server: 23 | 24 | ``` 25 | yarn dev 26 | ``` 27 | 28 | ## Testing 29 | 30 | To run tests: 31 | 32 | ``` 33 | yarn playwright test 34 | ``` 35 | 36 | ## Type Checking 37 | 38 | To run type checks: 39 | 40 | ``` 41 | yarn tsc 42 | ``` 43 | 44 | ## Linting and Formatting 45 | 46 | To run linter: 47 | 48 | ``` 49 | yarn lint 50 | ``` 51 | 52 | To run formatter: 53 | 54 | ``` 55 | yarn prettier --write . 56 | ``` 57 | 58 | 59 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ConvoDisplay/ConvoDisplay.tsx: -------------------------------------------------------------------------------- 1 | import useAutoScrollToBottom from "@/hooks/useAutoScrollToBottom"; 2 | import { Conversation } from "@/models"; 3 | import { Box, Flex } from "@chakra-ui/react"; 4 | import MessageBubble from "@/components/MessageBubble"; 5 | import { motion } from "framer-motion"; 6 | 7 | interface ConvoDisplayProps { 8 | convo: Conversation; 9 | } 10 | 11 | const ConvoDisplay = ({ convo }: ConvoDisplayProps): JSX.Element => { 12 | // automatically scroll to bottom on new message 13 | const endOfMessages = useAutoScrollToBottom([convo.messages.length]); 14 | return ( 15 | 16 | {convo.messages.map((message) => ( 17 | 25 | 26 | 27 | ))} 28 |
29 | 30 | ); 31 | }; 32 | 33 | export default ConvoDisplay; 34 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/test_get_messages.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from azure.cosmos import ContainerProxy 3 | from fastapi.testclient import TestClient 4 | from app.models import Message 5 | 6 | 7 | def test_correct_messages_are_fetched(client: TestClient): 8 | container: ContainerProxy = client.app.state.cosmos_container 9 | 10 | convo = { 11 | "id": "does-not-matter", 12 | "userId": "does-not-matter", 13 | "title": "does-not-matter", 14 | "model": "does-not-matter", 15 | "timestamp": "does-not-matter", 16 | "isArchived": False, 17 | "messages": [ 18 | { 19 | "id": "does-not-matter", 20 | "text": "first message", 21 | "sender": "user", 22 | "timestamp": "does-not-matter", 23 | "status": "success", 24 | "statusMessage": None, 25 | } 26 | ], 27 | } 28 | 29 | container.create_item(convo) 30 | 31 | response = client.get( 32 | f"/convos/{convo['id']}/messages", 33 | headers={"user-id": convo["userId"]}, 34 | ) 35 | 36 | assert response.status_code == 200 37 | 38 | messages: List[Message] = response.json() 39 | 40 | assert messages == convo["messages"] 41 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/services/AssistantService.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "@/models"; 2 | import { throwResponseError } from "@/utils"; 3 | 4 | export interface GetCompletionRequest { 5 | messages: Message[]; 6 | deployment_name: string; 7 | } 8 | 9 | export interface AssistantService { 10 | getCompletion(request: GetCompletionRequest): Promise; 11 | } 12 | 13 | export class OpenAIAssistantService implements AssistantService { 14 | async getCompletion(request: GetCompletionRequest): Promise { 15 | const endpoint = `${GPT4DFCI_API_ENDPOINT}?deployment_name=${request.deployment_name}`; 16 | const options = { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json", 20 | "Ocp-Apim-Subscription-Key": ${OCP_APIM_SUBSCRIPTION_KEY}, 21 | }, 22 | body: JSON.stringify(request.messages), 23 | }; 24 | return fetch(endpoint, options) 25 | .then( 26 | throwResponseError<{ 27 | choices: { message: { content: string } }[]; 28 | }> 29 | ) 30 | .then((response) => { 31 | return response.choices[0].message.content; 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/Layout/LayoutDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | BoxProps, 4 | Drawer, 5 | DrawerCloseButton, 6 | DrawerContent, 7 | DrawerOverlay, 8 | IconButton, 9 | useDisclosure, 10 | } from "@chakra-ui/react"; 11 | import { Menu } from "lucide-react"; 12 | import { useRef } from "react"; 13 | 14 | const LayoutDrawer = ({ children, ...props }: BoxProps): JSX.Element => { 15 | const { isOpen, onOpen, onClose } = useDisclosure(); 16 | const buttonRef = useRef(null); 17 | 18 | return ( 19 | 20 | } 24 | aria-label="menu-toggle" 25 | variant="ghost" 26 | /> 27 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default LayoutDrawer; 44 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-infra/README.md: -------------------------------------------------------------------------------- 1 | # GPT4DFCI Infrastructure 2 | 3 | This is alpha code provided as an example. What we ended up doing for v1.0 is to have 3 subscriptions; in the lowest one we would create manually Azure resources, configure them in detail, and export complete Azure ARM Template. These ARM files were then used to deploy in the other two subscriptions (one dedicated to testing and the other to production). The following code is based on the Pulumi-Azure interface and it aims at automating this process. 4 | 5 | ## Prerequisites 6 | 7 | ### Create a project folder 8 | 9 | ### Login into Azure 10 | 11 | `az login` 12 | 13 | ### Login into pulumi 14 | 15 | `pulumi login` 16 | 17 | ### Crete a new pulumi-azure project 18 | 19 | `pulumi new azure-python` 20 | 21 | ### Install required libraries 22 | 23 | `pip install pulumi pulumi_azure pulumi_azure_native` 24 | 25 | ## Run the deployment 26 | 27 | ### Replace `__main__.py` content with [`infra-pulumi-alpha.py`](./infra-pulumi-alpha.py) content 28 | 29 | ### Bring up the infra 30 | 31 | `pulumi up` 32 | 33 | ## Toubleshooting 34 | 35 | ### Check the infra status 36 | 37 | `pulumi stack` 38 | 39 | ### Take down the infra 40 | 41 | `pulumi destroy` 42 | 43 | ![image](https://github.com/Dana-Farber-AIOS/GPT4DFCI/assets/25375373/413e1af9-576f-44a8-9cc4-1fffb53d7c2c) 44 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dfci-GPT4DFCI", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^2.7.0", 14 | "@emotion/react": "^11.11.1", 15 | "@emotion/styled": "^11.11.0", 16 | "@fontsource-variable/inter": "^5.0.3", 17 | "@fontsource/inter": "^5.0.3", 18 | "framer-motion": "^10.12.16", 19 | "lucide-react": "^0.244.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0" 22 | }, 23 | "devDependencies": { 24 | "@playwright/test": "^1.35.1", 25 | "@types/node": "^20.2.5", 26 | "@types/react": "^18.0.37", 27 | "@types/react-dom": "^18.0.11", 28 | "@typescript-eslint/eslint-plugin": "^5.59.0", 29 | "@typescript-eslint/parser": "^5.59.0", 30 | "@vitejs/plugin-react": "^4.0.0", 31 | "eslint": "^8.38.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.3.4", 34 | "prettier": "^2.8.8", 35 | "typescript": "^5.0.2", 36 | "vite": "^4.3.9" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/test_create_completion.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | 4 | def test_create_completion_returns_dict_with_expected_keys(client: TestClient): 5 | response = client.post( 6 | "/completion", 7 | params={"deployment_name": "gpt-35-turbo-0613"}, 8 | json=[ 9 | { 10 | "id": "", 11 | "text": "Tell me a joke about ChatGPT.", 12 | "sender": "user", 13 | "timestamp": "", 14 | "status": "success", 15 | "statusMessage": "", 16 | } 17 | ], 18 | ) 19 | 20 | assert response.status_code == 200 21 | 22 | result = response.json() 23 | 24 | assert list(result.keys()) == [ 25 | "id", 26 | "object", 27 | "created", 28 | "model", 29 | "choices", 30 | "usage", 31 | ] 32 | 33 | 34 | def test_missing_deployment_name_throws_error(client: TestClient): 35 | response = client.post( 36 | "/completion", 37 | json=[ 38 | { 39 | "id": "", 40 | "text": "Tell me a joke about ChatGPT.", 41 | "sender": "user", 42 | "timestamp": "", 43 | "status": "success", 44 | "statusMessage": "", 45 | } 46 | ], 47 | ) 48 | 49 | assert response.status_code == 422 50 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from azure.cosmos import CosmosClient, PartitionKey 4 | from dotenv import dotenv_values 5 | from fastapi.testclient import TestClient 6 | import pytest 7 | from app.main import app 8 | 9 | config = dotenv_values(".env.test") 10 | 11 | AZURE_COSMOSDB_ENDPOINT = config["AZURE_COSMOSDB_ENDPOINT"] 12 | AZURE_COSMOSDB_KEY = config["AZURE_COSMOSDB_KEY"] 13 | AZURE_COSMOSDB_DATABASE = config["AZURE_COSMOSDB_DATABASE"] 14 | AZURE_COSMOSDB_CONTAINER = config["AZURE_COSMOSDB_CONTAINER"] 15 | 16 | logging.info( 17 | f""" 18 | Using Cosmos config: 19 | Endpoint: {AZURE_COSMOSDB_ENDPOINT} 20 | Database: {AZURE_COSMOSDB_DATABASE} 21 | Container: {AZURE_COSMOSDB_CONTAINER} 22 | """, 23 | ) 24 | 25 | 26 | @pytest.fixture 27 | def test_container(): 28 | try: 29 | db = CosmosClient( 30 | url=AZURE_COSMOSDB_ENDPOINT, 31 | credential=AZURE_COSMOSDB_KEY, 32 | ).get_database_client(database=AZURE_COSMOSDB_DATABASE) 33 | 34 | partition_key = PartitionKey(path="/userId") 35 | container = db.create_container_if_not_exists( 36 | id=AZURE_COSMOSDB_CONTAINER, 37 | partition_key=partition_key, 38 | ) 39 | 40 | yield container 41 | 42 | finally: 43 | db.delete_container(container) 44 | 45 | 46 | @pytest.fixture 47 | def client(test_container): 48 | app.state.cosmos_container = test_container 49 | return TestClient(app) 50 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/LandingDisplay/LandingDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Flex, 4 | Image, 5 | ListItem, 6 | Text, 7 | UnorderedList, 8 | } from "@chakra-ui/react"; 9 | import Logo from "./dfci-logo.jpg"; 10 | 11 | const LandingDisplay = (): JSX.Element => { 12 | return ( 13 | 20 | DFCI Logo 21 | 22 | GPT4DFCI 23 | 24 | 35 | Welcome to GPT4DFCI, the DFCI instance of GPT. You may use PHI 36 | and PII in here provided: 37 | 44 | Some condition 45 | Some condition 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default LandingDisplay; 53 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/test_update_convo.py: -------------------------------------------------------------------------------- 1 | from azure.cosmos import ContainerProxy 2 | from fastapi.testclient import TestClient 3 | 4 | 5 | def test_title_can_be_updated(client: TestClient): 6 | container: ContainerProxy = client.app.state.cosmos_container 7 | 8 | convo = { 9 | "id": "does-not-matter", 10 | "userId": "does-not-matter", 11 | "title": "does-not-matter", 12 | "model": "does-not-matter", 13 | "timestamp": "does-not-matter", 14 | "isArchived": False, 15 | "messages": [], 16 | } 17 | 18 | container.create_item(convo) 19 | 20 | update = { 21 | "id": "does-not-matter", 22 | "userId": "does-not-matter", 23 | "title": "should-not-matter", 24 | "model": "does-not-matter", 25 | "timestamp": "does-not-matter", 26 | "isArchived": False, 27 | "messages": [], 28 | } 29 | 30 | response = client.patch( 31 | f"/convos/{convo['id']}", headers={"user-id": convo["userId"]}, json=update 32 | ) 33 | 34 | assert response.status_code == 200 35 | 36 | # try and fetch updated convo 37 | query = "SELECT * FROM convos c where \ 38 | c.id = @convoId and \ 39 | c.userId = @userId" 40 | 41 | params: list[dict[str, object]] = [ 42 | {"name": "@convoId", "value": convo["id"]}, 43 | {"name": "@userId", "value": convo["userId"]}, 44 | ] 45 | 46 | results = container.query_items( 47 | query=query, 48 | parameters=params, 49 | enable_cross_partition_query=False, 50 | ) 51 | 52 | results = list(results) 53 | 54 | assert len(results) == 1 55 | assert results[0]["title"] == update["title"] 56 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/OptionsMenu/OptionsMenu.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Icon, 3 | IconButton, 4 | Menu, 5 | MenuButton, 6 | MenuItem, 7 | MenuList, 8 | useDisclosure, 9 | } from "@chakra-ui/react"; 10 | import { Download, Info, MoreVertical } from "lucide-react"; 11 | import AboutModal from "./AboutModal"; 12 | 13 | interface OptionsMenuProps { 14 | archive: { 15 | onSubmit: () => void; 16 | isDisabled: boolean; 17 | }; 18 | export: { 19 | onSubmit: () => void; 20 | }; 21 | } 22 | 23 | const OptionsMenu = (props: OptionsMenuProps): JSX.Element => { 24 | const aboutDisclosure = useDisclosure(); 25 | 26 | return ( 27 | <> 28 | 29 | } 33 | size="md" 34 | color="gray" 35 | /> 36 | 37 | } 39 | onClick={aboutDisclosure.onOpen} 40 | > 41 | About 42 | 43 | } 45 | onClick={props.export.onSubmit} 46 | isDisabled 47 | > 48 | Export data (coming soon) 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default OptionsMenu; 58 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "Citation for this repository" 3 | authors: 4 | - family-names: Umeton 5 | given-names: Renato 6 | - family-names: Kwok 7 | given-names: Anne 8 | - family-names: Maurya 9 | given-names: Rahul 10 | - family-names: Leco 11 | given-names: Domenic 12 | - family-names: Lenane 13 | given-names: Naomi 14 | - family-names: Willcox 15 | given-names: Jennifer 16 | - family-names: Abel 17 | given-names: Gregory 18 | - family-names: Tolikas 19 | given-names: Mary 20 | - family-names: Johnson 21 | given-names: Jason 22 | title: "GPT-4 in a Cancer Center: Institute-Wide Deployment Challenges and Lessons Learned" 23 | date-released: 2024-03-15 24 | doi: 10.1056/AIcs2300191 25 | url: https://github.com/Dana-Farber-AIOS/GPT4DFCI 26 | preferred-citation: 27 | type: article 28 | authors: 29 | - family-names: Umeton 30 | given-names: Renato 31 | - family-names: Kwok 32 | given-names: Anne 33 | - family-names: Maurya 34 | given-names: Rahul 35 | - family-names: Leco 36 | given-names: Domenic 37 | - family-names: Lenane 38 | given-names: Naomi 39 | - family-names: Willcox 40 | given-names: Jennifer 41 | - family-names: Abel 42 | given-names: Gregory 43 | - family-names: Tolikas 44 | given-names: Mary 45 | - family-names: Johnson 46 | given-names: Jason 47 | doi: 10.1056/AIcs2300191 48 | journal: "NEJM AI" 49 | publisher: Massachusetts Medical Society 50 | month: 3 51 | year: 2024 52 | issue: 4 53 | volume: 1 54 | start: 10 55 | title: "GPT-4 in a Cancer Center: Institute-Wide Deployment Challenges and Lessons Learned" 56 | url: https://ai.nejm.org/doi/full/10.1056/AIcs2300191 57 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | token: string; 3 | displayName: string; 4 | email: string; 5 | } 6 | 7 | export interface Message { 8 | id: string; 9 | conversationId: string; 10 | sender: Sender; 11 | text: string; 12 | timestamp: Date; 13 | status: MessageStatus; 14 | statusMessage?: string; 15 | } 16 | 17 | export enum MessageStatus { 18 | Success = "success", 19 | Error = "error", 20 | } 21 | 22 | export interface Conversation { 23 | id: string; 24 | messages: Message[]; 25 | title: string; 26 | timestamp: Date; 27 | isArchived: boolean; 28 | model: GPTModel; 29 | } 30 | 31 | export type ConversationMetadata = Omit; 32 | 33 | export enum Sender { 34 | User = "user", 35 | Assistant = "assistant", 36 | } 37 | 38 | export enum GPTModel { 39 | GPT35Turbo = "gpt-3.5-turbo", 40 | GPT4 = "gpt-4", 41 | GPT4turbo = "gpt-4-turbo", 42 | } 43 | 44 | export const DEFAULT_MODEL = GPTModel.GPT35Turbo; 45 | 46 | export const modelColorScheme = (model: GPTModel) => { 47 | switch (model) { 48 | case GPTModel.GPT35Turbo: 49 | return "green"; 50 | case GPTModel.GPT4: 51 | return "blue"; 52 | case GPTModel.GPT4turbo: 53 | return "blue"; 54 | default: 55 | return "gray"; 56 | } 57 | }; 58 | 59 | export const modelDeployment = (model: GPTModel) => { 60 | switch (model) { 61 | case GPTModel.GPT35Turbo: 62 | return "gpt-35-turbo-0613"; 63 | case GPTModel.GPT4: 64 | return "gpt-4model"; 65 | case GPTModel.GPT4turbo: 66 | return "gpt-4tmodel"; 67 | 68 | default: 69 | return ""; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI-Backend/tests/test_get_convos.py: -------------------------------------------------------------------------------- 1 | from azure.cosmos import ContainerProxy 2 | from fastapi.testclient import TestClient 3 | from app.utils import generate_uuid 4 | 5 | 6 | def test_multiple_convos_are_retrieved(client: TestClient): 7 | user_id = generate_uuid() 8 | 9 | container: ContainerProxy = client.app.state.cosmos_container 10 | 11 | convo_a = { 12 | "id": "convo_a", 13 | "userId": user_id, 14 | "title": "convo_a", 15 | "model": "does-not-matter", 16 | "timestamp": "does-not-matter", 17 | "isArchived": False, 18 | # omitting messages for convenience 19 | } 20 | 21 | convo_b = { 22 | "id": "convo_b", 23 | "userId": user_id, 24 | "title": "convo_b", 25 | "model": "does-not-matter", 26 | "timestamp": "does-not-matter", 27 | "isArchived": False, 28 | # omitting messages for convenience 29 | } 30 | 31 | container.create_item(convo_a) 32 | container.create_item(convo_b) 33 | 34 | response = client.get("/convos", headers={"user-id": user_id}) 35 | 36 | assert response.status_code == 200 37 | assert response.json() == [ 38 | convo_a, 39 | convo_b, 40 | ] 41 | 42 | 43 | def test_nonexistent_user_returns_empty_list(client: TestClient): 44 | user_id = generate_uuid() 45 | 46 | response = client.get("/convos", headers={"user-id": user_id}) 47 | 48 | assert response.status_code == 200 49 | assert response.json() == [] 50 | 51 | 52 | def test_empty_user_id_throws_error(client): 53 | response = client.get("/convos", headers={"user-id": ""}) 54 | 55 | assert response.status_code == 400 56 | 57 | 58 | def test_missing_user_id_throws_error(client): 59 | response = client.get("/convos") 60 | 61 | assert response.status_code == 422 62 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ConvoList/ConvoList.tsx: -------------------------------------------------------------------------------- 1 | import { ConversationMetadata } from "@/models"; 2 | import ConvoListItem from "./ConvoListItem"; 3 | import { Box, Flex } from "@chakra-ui/react"; 4 | import { motion } from "framer-motion"; 5 | 6 | interface ConvoListProps { 7 | convos: ConversationMetadata[]; 8 | activeConvo: ConversationMetadata; 9 | onSelect: (convo: ConversationMetadata) => void; 10 | onEdit: (convo: ConversationMetadata, newTitle: string) => void; 11 | onArchive: (convo: ConversationMetadata) => void; 12 | } 13 | 14 | const ConvoList = ({ 15 | convos, 16 | activeConvo, 17 | onSelect, 18 | onEdit, 19 | onArchive, 20 | }: ConvoListProps): JSX.Element => { 21 | const isActive = (convo: ConversationMetadata) => 22 | activeConvo !== undefined && convo.id === activeConvo.id; 23 | return ( 24 | 25 | {convos 26 | .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) 27 | .map((convo) => ( 28 | 35 | 42 | 43 | ))} 44 | 45 | ); 46 | }; 47 | 48 | export default ConvoList; 49 | -------------------------------------------------------------------------------- /DFCI-GPT4DFCI/src/components/ExpandableInput/ExpandableInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Textarea, TextareaProps } from "@chakra-ui/react"; 3 | import { numRows } from "./utils"; 4 | 5 | interface ExpandableInputProps extends TextareaProps { 6 | maxRows: number; 7 | isSubmitting?: boolean; 8 | } 9 | 10 | const ExpandableInput = ({ 11 | isSubmitting = false, 12 | maxRows, 13 | ...props 14 | }: ExpandableInputProps) => { 15 | const [height, setHeight] = useState(1); 16 | const [value, setValue] = useState(""); 17 | 18 | const onChange = (event: React.ChangeEvent) => { 19 | setValue(event.target.value); 20 | setHeight(numRows(event.target.value, maxRows)); 21 | 22 | if (props.onChange) { 23 | props.onChange(event); 24 | } 25 | }; 26 | 27 | // Submit on Enter (or Numpad Enter), but allow linebreak on Shift + Enter 28 | const onKeyDown = (event: React.KeyboardEvent) => { 29 | if ( 30 | !event.shiftKey && 31 | (event.key === "Enter" || event.key === "Numpad Enter") 32 | ) { 33 | event.preventDefault(); 34 | onSubmit(event); 35 | } 36 | }; 37 | 38 | const onSubmit = (event: React.FormEvent) => { 39 | if (!isSubmitting && value.length > 0) { 40 | if (props.onSubmit) { 41 | props.onSubmit(event); 42 | } 43 | setValue(""); 44 | setHeight(1); 45 | } 46 | }; 47 | 48 | return ( 49 |