From fcb51c3335f24667f9caa0d3918d425c2253e808 Mon Sep 17 00:00:00 2001 From: "Chris Van Pelt (CVP)" Date: Thu, 19 Dec 2024 16:00:22 -0800 Subject: [PATCH] feat(weave): Add mods page and menu item only for wandb admins (#3279) * Basic UI for mods * Pass along purl for wild demo purposes * Mod demo updates * Wire up secret setting, add mod visual indicator * gradient similar to streamlit * Added secret types * Fix mutation types * Use the wandb api host for our iframe * Wire up backend host * Make mods page scroll properly * Only show mods to admins * Fix TSC errors * Fix project sidebar deps * Fix bungled merge * Add grace period for startup * Try some debugging, increase retries * Fix formatting * Fix trailing newline * Enabling container logging * Use wget instead of curl as we dont have it in the container anymore * Fix shadow variable lint error --- .github/workflows/test.yaml | 16 +- wb_schema.gql | 21 ++ weave-js/src/common/hooks/useSecrets.ts | 121 ++++++ .../components/FancyPage/useProjectSidebar.ts | 27 +- .../PagePanelComponents/Home/Browse3.tsx | 18 + .../Home/Browse3/pages/ModsPage.tsx | 353 ++++++++++++++++++ weave-js/src/config.ts | 3 +- 7 files changed, 550 insertions(+), 9 deletions(-) create mode 100644 weave-js/src/common/hooks/useSecrets.ts create mode 100644 weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ModsPage.tsx diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 295b6ef89227..ab0a55fb384c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -81,11 +81,16 @@ jobs: env: CI: 1 WANDB_ENABLE_TEST_CONTAINER: true + LOGGING_ENABLED: true ports: - '8080:8080' - '8083:8083' - '9015:9015' - options: --health-cmd "curl --fail http://localhost:8080/healthz || exit 1" --health-interval=5s --health-timeout=3s + options: >- + --health-cmd "wget -q -O /dev/null http://localhost:8080/healthz || exit 1" + --health-interval=5s + --health-timeout=3s + --health-start-period=10s outputs: tests_should_run: ${{ steps.test_check.outputs.tests_should_run }} steps: @@ -254,11 +259,16 @@ jobs: env: CI: 1 WANDB_ENABLE_TEST_CONTAINER: true + LOGGING_ENABLED: true ports: - '8080:8080' - '8083:8083' - '9015:9015' - options: --health-cmd "curl --fail http://localhost:8080/healthz || exit 1" --health-interval=5s --health-timeout=3s + options: >- + --health-cmd "wget -q -O /dev/null http://localhost:8080/healthz || exit 1" + --health-interval=5s + --health-timeout=3s + --health-start-period=10s weave_clickhouse: image: clickhouse/clickhouse-server ports: @@ -267,6 +277,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Enable debug logging + run: echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV - name: Set up Python ${{ matrix.python-version-major }}.${{ matrix.python-version-minor }} uses: actions/setup-python@v5 with: diff --git a/wb_schema.gql b/wb_schema.gql index f326ab4d4872..13152c785475 100644 --- a/wb_schema.gql +++ b/wb_schema.gql @@ -104,10 +104,24 @@ type UpdateUserPayload { clientMutationId: String } +input InsertSecretInput { + entityName: String! + secretName: String! + @constraints(max: 255, pattern: "^[A-Za-z_][A-Za-z0-9_]*$") + secretValue: String! + clientMutationId: String +} + +type InsertSecretPayload { + success: Boolean! + clientMutationId: String +} + type Mutation { updateUser(input: UpdateUserInput!): UpdateUserPayload @audit deleteView(input: DeleteViewInput!): DeleteViewPayload upsertView(input: UpsertViewInput!): UpsertViewPayload @audit + insertSecret(input: InsertSecretInput!): InsertSecretPayload updateArtifactSequence( input: UpdateArtifactSequenceInput! ): UpdateArtifactCollectionPayload @@ -275,6 +289,12 @@ type RowType { row: JSON! } +type Secret { + entityId: Int! + name: String! + createdAt: DateTime! +} + type Entity implements Node { id: ID! name: String! @@ -296,6 +316,7 @@ type Entity implements Node { filters: JSONString collectionTypes: [ArtifactCollectionType!] ): ArtifactCollectionConnection + secrets: [Secret!]! } type EntityConnection { diff --git a/weave-js/src/common/hooks/useSecrets.ts b/weave-js/src/common/hooks/useSecrets.ts new file mode 100644 index 000000000000..8446be55dd73 --- /dev/null +++ b/weave-js/src/common/hooks/useSecrets.ts @@ -0,0 +1,121 @@ +/** + * This is a GraphQL approach to querying viewer information. + * There is a query engine based approach in useViewerUserInfo.ts. + */ + +import { + gql, + TypedDocumentNode, + useApolloClient, + useMutation, +} from '@apollo/client'; +import {useEffect, useState} from 'react'; + +const SECRETS_QUERY = gql` + query secrets($entityName: String!) { + entity(name: $entityName) { + id + secrets { + entityId + name + createdAt + } + } + } +`; + +const SECRETS_MUTATION = gql` + mutation insertSecret( + $entityName: String! + $secretName: String! + $secretValue: String! + ) { + insertSecret( + input: { + entityName: $entityName + secretName: $secretName + secretValue: $secretValue + } + ) { + success + } + } +` as TypedDocumentNode; + +type SecretResponseLoading = { + loading: true; + entityId: string; + secrets: string[]; +}; +type SecretResponseSuccess = { + loading: false; + entityId: string; + secrets: string[]; +}; +type SecretResponse = SecretResponseLoading | SecretResponseSuccess; + +export const useSecrets = ({ + entityName, +}: { + entityName: string; +}): SecretResponse => { + const [response, setResponse] = useState({ + loading: true, + entityId: '', + secrets: [], + }); + + const apolloClient = useApolloClient(); + + useEffect(() => { + let mounted = true; + apolloClient + .query({query: SECRETS_QUERY as any, variables: {entityName}}) + .then(result => { + if (!mounted) { + return; + } + const secretPayloads = result.data.entity?.secrets ?? []; + if (!secretPayloads) { + setResponse({ + loading: false, + entityId: '', + secrets: [], + }); + return; + } + const secrets = secretPayloads.map((secret: any) => secret.name).sort(); + setResponse({ + loading: false, + entityId: result.data.entity?.id ?? '', + secrets, + }); + }); + return () => { + mounted = false; + }; + }, [apolloClient, entityName]); + + return response; +}; + +interface InsertSecretResponse { + insertSecret: { + success: boolean; + }; +} + +type InsertSecretVariables = { + entityName: string; + secretName: string; + secretValue: string; +}; + +export const useInsertSecret = () => { + const [insertSecret] = useMutation< + InsertSecretResponse, + InsertSecretVariables + >(SECRETS_MUTATION); + + return insertSecret; +}; diff --git a/weave-js/src/components/FancyPage/useProjectSidebar.ts b/weave-js/src/components/FancyPage/useProjectSidebar.ts index 19dce4215df7..2bd8c5635d55 100644 --- a/weave-js/src/components/FancyPage/useProjectSidebar.ts +++ b/weave-js/src/components/FancyPage/useProjectSidebar.ts @@ -11,7 +11,8 @@ export const useProjectSidebar = ( hasWeaveData: boolean, hasTraceBackend: boolean = true, hasModelsAccess: boolean = true, - isLaunchActive: boolean = false + isLaunchActive: boolean = false, + isWandbAdmin: boolean = false ): FancyPageSidebarItem[] => { // Should show models sidebar items if we have models data or if we don't have a trace backend let showModelsSidebarItems = hasModelsData || !hasTraceBackend; @@ -34,6 +35,14 @@ export const useProjectSidebar = ( const isShowAll = isNoSidebarItems || isBothSidebarItems; return useMemo(() => { + const weaveOnlyMenu = [ + 'weave/leaderboards', + 'weave/operations', + 'weave/objects', + ]; + if (isWandbAdmin) { + weaveOnlyMenu.push('weave/mods'); + } const allItems = isLoading ? [] : [ @@ -178,6 +187,14 @@ export const useProjectSidebar = ( isShown: isWeaveOnly, iconName: IconNames.TypeNumberAlt, }, + { + type: 'button' as const, + name: 'Mods', + slug: 'weave/mods', + isShown: false, // Only shown in overflow menu + isDisabled: !isWandbAdmin, + iconName: IconNames.LayoutGrid, + }, { type: 'button' as const, name: 'Leaders', @@ -205,7 +222,7 @@ export const useProjectSidebar = ( type: 'menuPlaceholder' as const, key: 'moreWeaveOnly', isShown: isWeaveOnly, - menu: ['weave/leaderboards', 'weave/operations', 'weave/objects'], + menu: weaveOnlyMenu, }, { type: 'menuPlaceholder' as const, @@ -216,10 +233,7 @@ export const useProjectSidebar = ( 'weave/models', 'weave/datasets', 'weave/scorers', - 'weave/leaderboards', - 'weave/operations', - 'weave/objects', - ], + ].concat(weaveOnlyMenu), }, ]; @@ -252,5 +266,6 @@ export const useProjectSidebar = ( isModelsOnly, showWeaveSidebarItems, isLaunchActive, + isWandbAdmin, ]); }; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx index 761fd5369309..c0192ddbe9f6 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx @@ -71,6 +71,7 @@ import {SimplePageLayoutContext} from './Browse3/pages/common/SimplePageLayout'; import {CompareEvaluationsPage} from './Browse3/pages/CompareEvaluationsPage/CompareEvaluationsPage'; import {LeaderboardListingPage} from './Browse3/pages/LeaderboardPage/LeaderboardListingPage'; import {LeaderboardPage} from './Browse3/pages/LeaderboardPage/LeaderboardPage'; +import {ModsPage} from './Browse3/pages/ModsPage'; import {ObjectPage} from './Browse3/pages/ObjectPage'; import {ObjectVersionPage} from './Browse3/pages/ObjectVersionPage'; import { @@ -146,6 +147,7 @@ const tabOptions = [ 'leaderboards', 'boards', 'tables', + 'mods', 'scorers', ]; const tabs = tabOptions.join('|'); @@ -484,6 +486,11 @@ const Browse3ProjectRoot: FC<{ + {/* MODS */} + + + {/* PLAYGROUND */} { return ; }; +const ModsPageBinding = () => { + const params = useParamsDecoded(); + return ( + + ); +}; + const TablesPageBinding = () => { const params = useParamsDecoded(); diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ModsPage.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ModsPage.tsx new file mode 100644 index 000000000000..05d6da871c02 --- /dev/null +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ModsPage.tsx @@ -0,0 +1,353 @@ +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Drawer from '@mui/material/Drawer'; +import Grid from '@mui/material/Grid2'; +import TextField from '@mui/material/TextField'; +import { + useInsertSecret, + useSecrets, +} from '@wandb/weave/common/hooks/useSecrets'; +import {TargetBlank} from '@wandb/weave/common/util/links'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {useHistory} from 'react-router'; +import {Link} from 'react-router-dom'; + +import {SimplePageLayout} from './common/SimplePageLayout'; + +type Mod = { + id: string; + name: string; + description: string; + secrets: string[]; +}; + +type ModCategoryType = 'Labeling' | 'Analysis' | 'Demos'; + +type ModCategories = { + [key in ModCategoryType]: Mod[]; +}; + +const modCats: ModCategories = { + Labeling: [ + { + id: 'labeling/html', + name: 'HTML Labeler', + description: 'Label generated HTML against your own criteria', + secrets: ['WANDB_API_KEY', 'OPENAI_API_KEY'], + }, + ], + Analysis: [ + { + id: 'embedding-classifier', + name: 'Embedding Classifier', + description: + 'Classify your traces by embedding them and have an LLM label the clusters', + secrets: ['WANDB_API_KEY', 'OPENAI_API_KEY'], + }, + { + id: 'cost-dashboard', + name: 'Cost Dashboard', + description: 'A dashboard showing your project LLM costs over time', + secrets: ['WANDB_API_KEY'], + }, + ], + Demos: [ + { + id: 'welcome', + name: 'Welcome', + description: 'A simple welcome mod', + secrets: ['WANDB_API_KEY'], + }, + { + id: 'openui', + name: 'OpenUI', + description: 'Generate UIs from images or text descriptions', + secrets: ['WANDB_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY'], + }, + { + id: 'gist', + name: 'Gist', + description: 'Load a gist that contains a streamlit app.py file', + secrets: ['WANDB_API_KEY'], + }, + ], +}; + +const ModCategory: React.FC<{ + category: ModCategoryType; + mods: Mod[]; + entity: string; + project: string; +}> = ({category, mods, entity, project}) => { + return ( + +
+ {category} +
+ +
+ ); +}; + +const ModCards: React.FC<{mods: Mod[]; entity: string; project: string}> = ({ + mods, + entity, + project, +}) => { + const searchParams = new URLSearchParams(window.location.search); + const [gistId, setGistId] = useState(''); + + const purl = + searchParams.get('purl') || + (gistId !== '' ? encodeURIComponent(`pkg:gist/${gistId}`) : ''); + return ( + + {mods.map(mod => ( + + + +
{mod.name}
+

{mod.description}

+
+ + {mod.id === 'gist' && ( + setGistId(e.target.value)} + /> + )} + + +
+
+ ))} +
+ ); +}; + +const ModFrame: React.FC<{entity: string; project: string; modId: string}> = ({ + entity, + project, + modId, +}) => { + const searchParams = new URLSearchParams(window.location.search); + const purl = searchParams.get('purl'); + return ( +