diff --git a/api/pkg/controller/handlers.go b/api/pkg/controller/handlers.go index c90e94c40b..17fca5d994 100644 --- a/api/pkg/controller/handlers.go +++ b/api/pkg/controller/handlers.go @@ -1,12 +1,15 @@ package controller import ( + "context" + "errors" + "github.com/lukemarsden/helix/api/pkg/store" "github.com/lukemarsden/helix/api/pkg/types" ) func (c *Controller) GetStatus(ctx types.RequestContext) (types.UserStatus, error) { - balanceTransfers, err := c.Options.Store.GetBalanceTransfers(ctx.Ctx, store.GetBalanceTransfersQuery{ + balanceTransfers, err := c.Options.Store.GetBalanceTransfers(ctx.Ctx, store.OwnerQuery{ Owner: ctx.Owner, OwnerType: ctx.OwnerType, }) @@ -26,8 +29,63 @@ func (c *Controller) GetStatus(ctx types.RequestContext) (types.UserStatus, erro } func (c *Controller) GetTransactions(ctx types.RequestContext) ([]*types.BalanceTransfer, error) { - return c.Options.Store.GetBalanceTransfers(ctx.Ctx, store.GetBalanceTransfersQuery{ + return c.Options.Store.GetBalanceTransfers(ctx.Ctx, store.OwnerQuery{ Owner: ctx.Owner, OwnerType: ctx.OwnerType, }) } +func (c *Controller) CreateAPIKey(ctx types.RequestContext, name string) (string, error) { + apiKey, err := c.Options.Store.CreateAPIKey(ctx.Ctx, store.OwnerQuery{ + Owner: ctx.Owner, + OwnerType: ctx.OwnerType, + }, name) + if err != nil { + return "", err + } + return apiKey, nil +} + +func (c *Controller) GetAPIKeys(ctx types.RequestContext) ([]*types.ApiKey, error) { + apiKeys, err := c.Options.Store.GetAPIKeys(ctx.Ctx, store.OwnerQuery{ + Owner: ctx.Owner, + OwnerType: ctx.OwnerType, + }) + if err != nil { + return nil, err + } + if apiKeys == nil { + _, err := c.CreateAPIKey(ctx, "default") + if err != nil { + return nil, err + } + return c.GetAPIKeys(ctx) + } + return apiKeys, nil +} + +func (c *Controller) DeleteAPIKey(ctx types.RequestContext, apiKey string) error { + fetchedApiKey, err := c.Options.Store.CheckAPIKey(ctx.Ctx, apiKey) + if err != nil { + return err + } + if fetchedApiKey == nil { + return errors.New("no such key") + } + // only the owner of an api key can delete it + if fetchedApiKey.Owner != ctx.Owner || fetchedApiKey.OwnerType != ctx.OwnerType { + return errors.New("unauthorized") + } + err = c.Options.Store.DeleteAPIKey(ctx.Ctx, *fetchedApiKey) + if err != nil { + return err + } + return nil +} + +func (c *Controller) CheckAPIKey(ctx context.Context, apiKey string) (*types.ApiKey, error) { + key, err := c.Options.Store.CheckAPIKey(ctx, apiKey) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/api/pkg/server/handlers.go b/api/pkg/server/handlers.go index ee066ec038..3b95f3a5df 100644 --- a/api/pkg/server/handlers.go +++ b/api/pkg/server/handlers.go @@ -520,3 +520,38 @@ func (apiServer *HelixAPIServer) respondRunnerSession(res http.ResponseWriter, r } return taskResponse, nil } + +func (apiServer *HelixAPIServer) createAPIKey(res http.ResponseWriter, req *http.Request) (string, error) { + name := req.URL.Query().Get("name") + apiKey, err := apiServer.Controller.CreateAPIKey(apiServer.getRequestContext(req), name) + if err != nil { + return "", err + } + return apiKey, nil +} + +func (apiServer *HelixAPIServer) getAPIKeys(res http.ResponseWriter, req *http.Request) ([]*types.ApiKey, error) { + apiKeys, err := apiServer.Controller.GetAPIKeys(apiServer.getRequestContext(req)) + if err != nil { + return nil, err + } + return apiKeys, nil +} + +func (apiServer *HelixAPIServer) deleteAPIKey(res http.ResponseWriter, req *http.Request) (string, error) { + apiKey := req.URL.Query().Get("key") + err := apiServer.Controller.DeleteAPIKey(apiServer.getRequestContext(req), apiKey) + if err != nil { + return "", err + } + return "", nil +} + +func (apiServer *HelixAPIServer) checkAPIKey(res http.ResponseWriter, req *http.Request) (*types.ApiKey, error) { + apiKey := req.URL.Query().Get("key") + key, err := apiServer.Controller.CheckAPIKey(apiServer.getRequestContext(req).Ctx, apiKey) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/api/pkg/server/keycloak.go b/api/pkg/server/keycloak.go index 755a77e1d3..445b87e410 100644 --- a/api/pkg/server/keycloak.go +++ b/api/pkg/server/keycloak.go @@ -8,6 +8,8 @@ import ( gocloak "github.com/Nerzal/gocloak/v13" jwt "github.com/golang-jwt/jwt/v4" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/types" ) const CLIENT_ID = "api" @@ -36,16 +38,37 @@ func newKeycloak(options ServerOptions) *keycloak { type keyCloakMiddleware struct { keycloak *keycloak options ServerOptions + store store.Store } -func newMiddleware(keycloak *keycloak, options ServerOptions) *keyCloakMiddleware { - return &keyCloakMiddleware{keycloak: keycloak, options: options} +func newMiddleware(keycloak *keycloak, options ServerOptions, store store.Store) *keyCloakMiddleware { + return &keyCloakMiddleware{keycloak: keycloak, options: options, store: store} } func extractBearerToken(token string) string { return strings.Replace(token, "Bearer ", "", 1) } +func (auth *keyCloakMiddleware) maybeOwnerFromRequest(r *http.Request) (*types.ApiKey, error) { + // in case the request is authenticated with an lp- token, rather than a + // keycloak JWT, return the owner. Returns nil if it's not an lp- token. + token := r.Header.Get("Authorization") + token = extractBearerToken(token) + + if strings.HasPrefix(token, "lp-") { + if owner, err := auth.store.CheckAPIKey(r.Context(), token); err != nil { + return nil, fmt.Errorf("error checking API key: %s", err.Error()) + } else if owner == nil { + // user claimed to provide lp- token, but it was invalid + return nil, fmt.Errorf("invalid API key") + } else { + return owner, nil + } + } + // user didn't claim token was an lp token, so fallback to keycloak + return nil, nil +} + func (auth *keyCloakMiddleware) jwtFromRequest(r *http.Request) (*jwt.Token, error) { // try to extract Authorization parameter from the HTTP header token := r.Header.Get("Authorization") @@ -107,12 +130,24 @@ func getRequestUser(req *http.Request) string { func (auth *keyCloakMiddleware) verifyToken(next http.Handler) http.Handler { f := func(w http.ResponseWriter, r *http.Request) { - token, err := auth.jwtFromRequest(r) + maybeOwner, err := auth.maybeOwnerFromRequest(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } - r = r.WithContext(setRequestUser(r.Context(), getUserIdFromJWT(token))) + if maybeOwner == nil { + // check keycloak JWT + token, err := auth.jwtFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + r = r.WithContext(setRequestUser(r.Context(), getUserIdFromJWT(token))) + next.ServeHTTP(w, r) + return + } + // successful api_key auth + r = r.WithContext(setRequestUser(r.Context(), maybeOwner.Owner)) next.ServeHTTP(w, r) } diff --git a/api/pkg/server/server.go b/api/pkg/server/server.go index 493f3c34b2..3905ee51ba 100644 --- a/api/pkg/server/server.go +++ b/api/pkg/server/server.go @@ -73,7 +73,7 @@ func (apiServer *HelixAPIServer) ListenAndServe(ctx context.Context, cm *system. }).Subrouter() keycloak := newKeycloak(apiServer.Options) - keyCloakMiddleware := newMiddleware(keycloak, apiServer.Options) + keyCloakMiddleware := newMiddleware(keycloak, apiServer.Options, apiServer.Store) authRouter.Use(keyCloakMiddleware.verifyToken) authRouter.HandleFunc("/status", Wrapper(apiServer.status)).Methods("GET") @@ -87,6 +87,11 @@ func (apiServer *HelixAPIServer) ListenAndServe(ctx context.Context, cm *system. authRouter.HandleFunc("/filestore/rename", Wrapper(apiServer.filestoreRename)).Methods("PUT") authRouter.HandleFunc("/filestore/delete", Wrapper(apiServer.filestoreDelete)).Methods("DELETE") + authRouter.HandleFunc("/api_keys", Wrapper(apiServer.createAPIKey)).Methods("POST") + authRouter.HandleFunc("/api_keys", Wrapper(apiServer.getAPIKeys)).Methods("GET") + authRouter.HandleFunc("/api_keys", Wrapper(apiServer.deleteAPIKey)).Methods("DELETE") + authRouter.HandleFunc("/api_keys/check", Wrapper(apiServer.checkAPIKey)).Methods("GET") + if apiServer.Options.LocalFilestorePath != "" { fileServer := http.FileServer(http.Dir(apiServer.Options.LocalFilestorePath)) subrouter.PathPrefix("/filestore/viewer/").Handler(http.StripPrefix("/api/v1/filestore/viewer/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/api/pkg/store/migrations/0003_api_key.down.sql b/api/pkg/store/migrations/0003_api_key.down.sql new file mode 100644 index 0000000000..ded91d06c8 --- /dev/null +++ b/api/pkg/store/migrations/0003_api_key.down.sql @@ -0,0 +1 @@ +drop table api_key; \ No newline at end of file diff --git a/api/pkg/store/migrations/0003_api_key.up.sql b/api/pkg/store/migrations/0003_api_key.up.sql new file mode 100644 index 0000000000..446ae13239 --- /dev/null +++ b/api/pkg/store/migrations/0003_api_key.up.sql @@ -0,0 +1,7 @@ +-- TODO: add created_at +create table api_key ( + owner varchar(255) NOT NULL, + owner_type varchar(255) NOT NULL, + key varchar(255) PRIMARY KEY, + name varchar(255) NOT NULL +); \ No newline at end of file diff --git a/api/pkg/store/postgres.go b/api/pkg/store/postgres.go index 875cbfac72..194e70ddd9 100644 --- a/api/pkg/store/postgres.go +++ b/api/pkg/store/postgres.go @@ -2,7 +2,9 @@ package store import ( "context" + "crypto/rand" "embed" + "encoding/base64" "encoding/json" "fmt" @@ -244,7 +246,7 @@ func (d *PostgresStore) UpdateSession( func (d *PostgresStore) GetBalanceTransfers( ctx context.Context, - query GetBalanceTransfersQuery, + query OwnerQuery, ) ([]*types.BalanceTransfer, error) { d.mtx.RLock() defer d.mtx.RUnlock() @@ -346,6 +348,127 @@ values ($1, $2, $3, $4, $5, $6)` return nil } +func (d *PostgresStore) CreateAPIKey(ctx context.Context, owner OwnerQuery, name string) (string, error) { + d.mtx.Lock() + defer d.mtx.Unlock() + + // Generate a new API key + key, err := generateAPIKey() + if err != nil { + return "", err + } + + // Insert the new API key into the database + sqlStatement := ` +insert into api_key (owner, owner_type, key, name) +values ($1, $2, $3, $4) +returning key +` + var id string + err = d.db.QueryRow( + sqlStatement, + owner.Owner, + owner.OwnerType, + key, + name, + ).Scan(&id) + if err != nil { + return "", err + } + + return id, nil +} + +func generateAPIKey() (string, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return "", err + } + return "lp-" + base64.URLEncoding.EncodeToString(key), nil +} + +func (d *PostgresStore) GetAPIKeys(ctx context.Context, query OwnerQuery) ([]*types.ApiKey, error) { + d.mtx.RLock() + defer d.mtx.RUnlock() + var apiKeys []*types.ApiKey + sqlStatement := ` +select + key, + owner, + owner_type +from + api_key +where + owner = $1 and owner_type = $2 +` + rows, err := d.db.Query( + sqlStatement, + query.Owner, + query.OwnerType, + ) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var apiKey types.ApiKey + err := rows.Scan( + &apiKey.Key, + &apiKey.Owner, + &apiKey.OwnerType, + ) + if err != nil { + return nil, err + } + apiKeys = append(apiKeys, &apiKey) + } + err = rows.Err() + if err != nil { + return nil, err + } + return apiKeys, nil +} + +func (d *PostgresStore) DeleteAPIKey(ctx context.Context, apiKey types.ApiKey) error { + d.mtx.Lock() + defer d.mtx.Unlock() + sqlStatement := ` +delete from api_key where key = $1 and owner = $2 and owner_type = $3 +` + _, err := d.db.Exec( + sqlStatement, + apiKey.Key, + apiKey.Owner, + apiKey.OwnerType, + ) + return err +} + +func (d *PostgresStore) CheckAPIKey(ctx context.Context, apiKey string) (*types.ApiKey, error) { + d.mtx.RLock() + defer d.mtx.RUnlock() + var key types.ApiKey + sqlStatement := ` +select + key, owner, owner_type +from + api_key +where + key = $1 +` + row := d.db.QueryRow(sqlStatement, apiKey) + err := row.Scan(&key.Key, &key.Owner, &key.OwnerType) + if err != nil { + if err == sql.ErrNoRows { + // not an error, but not a valid api key either + return nil, nil + } + return nil, err + } + return &key, nil +} + // Compile-time interface check: var _ Store = (*PostgresStore)(nil) diff --git a/api/pkg/store/types.go b/api/pkg/store/types.go index d72f79117b..1c0fc5f593 100644 --- a/api/pkg/store/types.go +++ b/api/pkg/store/types.go @@ -11,22 +11,38 @@ type GetBalanceTransfersQuery struct { OwnerType types.OwnerType `json:"owner_type"` } +type GetJobsQuery struct { + Owner string `json:"owner"` + OwnerType types.OwnerType `json:"owner_type"` +} + +type OwnerQuery struct { + Owner string `json:"owner"` + OwnerType types.OwnerType `json:"owner_type"` +} + type GetSessionsQuery struct { Owner string `json:"owner"` OwnerType types.OwnerType `json:"owner_type"` } type Store interface { - // balance transfers - GetBalanceTransfers(ctx context.Context, query GetBalanceTransfersQuery) ([]*types.BalanceTransfer, error) - CreateBalanceTransfer(ctx context.Context, balanceTransfer types.BalanceTransfer) error - // sessions GetSession(ctx context.Context, id string) (*types.Session, error) GetSessions(ctx context.Context, query GetSessionsQuery) ([]*types.Session, error) CreateSession(ctx context.Context, session types.Session) (*types.Session, error) UpdateSession(ctx context.Context, session types.Session) (*types.Session, error) DeleteSession(ctx context.Context, id string) (*types.Session, error) + + // balance transfers + GetBalanceTransfers(ctx context.Context, query OwnerQuery) ([]*types.BalanceTransfer, error) + CreateBalanceTransfer(ctx context.Context, balanceTransfer types.BalanceTransfer) error + + // api keys + CreateAPIKey(ctx context.Context, owner OwnerQuery, name string) (string, error) + GetAPIKeys(ctx context.Context, query OwnerQuery) ([]*types.ApiKey, error) + DeleteAPIKey(ctx context.Context, apiKey types.ApiKey) error + CheckAPIKey(ctx context.Context, apiKey string) (*types.ApiKey, error) } type StoreOptions struct { diff --git a/api/pkg/types/types.go b/api/pkg/types/types.go index 5b6cff0602..acd4408924 100644 --- a/api/pkg/types/types.go +++ b/api/pkg/types/types.go @@ -90,6 +90,13 @@ type SessionFilter struct { Reject []SessionFilterModel `json:"reject"` } +type ApiKey struct { + Owner string `json:"owner"` + OwnerType OwnerType `json:"owner_type"` + Key string `json:"key"` + Name string `json:"name"` +} + // passed between the api server and the controller type RequestContext struct { Ctx context.Context diff --git a/docker-compose.yaml b/docker-compose.yaml index 7a5ea55205..de4648f8ef 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,10 @@ services: - NOXY_KEYCLOAK_FRONT=/auth - NOXY_KEYCLOAK_HOST=keycloak - NOXY_KEYCLOAK_PORT=8080 + - NOXY_GRADIO_FRONT=/gradio + - NOXY_GRADIO_HOST=gradio + - NOXY_GRADIO_PORT=7860 + - NOXY_GRADIO_WS=1 postgres: image: postgres:12.13-alpine restart: always @@ -74,6 +78,25 @@ services: - ./go.mod:/app/go.mod - ./go.sum:/app/go.sum - ./api:/app/api + gradio: + ports: + - 7860:7860 + build: + context: gradio + dockerfile: Dockerfile + restart: always + # TODO: in production, maybre remove --reload? + entrypoint: uvicorn main:app --reload --host 0.0.0.0 --port 7860 + # uvicorn will auto-reload, or you can docker-compose restart gradio when + # changing the code + # (you'll still need to docker-compose build gradio when changing the + # requirements, or do things with exec/pip) + volumes: + - ./gradio:/app + + # TODO: mount the same data directory api server is using for filestore to + # gradio so it can just read files from there, including lilypad results... + frontend: ports: - 8081:8081 diff --git a/frontend/assets/img/cowsay.png b/frontend/assets/img/cowsay.png new file mode 100644 index 0000000000..830d595640 Binary files /dev/null and b/frontend/assets/img/cowsay.png differ diff --git a/frontend/assets/img/cowsay.xcf b/frontend/assets/img/cowsay.xcf new file mode 100644 index 0000000000..ddd6759159 Binary files /dev/null and b/frontend/assets/img/cowsay.xcf differ diff --git a/frontend/assets/img/mistral7b.jpeg b/frontend/assets/img/mistral7b.jpeg new file mode 100644 index 0000000000..3f861de403 Binary files /dev/null and b/frontend/assets/img/mistral7b.jpeg differ diff --git a/frontend/assets/img/sdxl.jpeg b/frontend/assets/img/sdxl.jpeg new file mode 100644 index 0000000000..a4619538d6 Binary files /dev/null and b/frontend/assets/img/sdxl.jpeg differ diff --git a/frontend/assets/img/sdxl.xcf b/frontend/assets/img/sdxl.xcf new file mode 100644 index 0000000000..7dd01d48e8 Binary files /dev/null and b/frontend/assets/img/sdxl.xcf differ diff --git a/frontend/src/contexts/account.tsx b/frontend/src/contexts/account.tsx index 5bb680feee..05e8e00925 100644 --- a/frontend/src/contexts/account.tsx +++ b/frontend/src/contexts/account.tsx @@ -13,6 +13,7 @@ import { IUser, IBalanceTransfer, ISession, + IApiKey, } from '../types' const REALM = 'helix' @@ -26,6 +27,7 @@ export interface IAccountContext { transactions: IBalanceTransfer[], sessions: ISession[], loadSessions: () => void, + apiKeys: IApiKey[], onLogin: () => void, onLogout: () => void, } @@ -36,6 +38,7 @@ export const AccountContext = createContext({ sessions: [], transactions: [], loadSessions: () => {}, + apiKeys: [], onLogin: () => {}, onLogout: () => {}, }) @@ -50,6 +53,7 @@ export const useAccountContext = (): IAccountContext => { const [ credits, setCredits ] = useState(0) const [ transactions, setTransactions ] = useState([]) const [ sessions, setSessions ] = useState([]) + const [ apiKeys, setApiKeys ] = useState([]) const keycloak = useMemo(() => { return new Keycloak({ @@ -76,16 +80,25 @@ export const useAccountContext = (): IAccountContext => { if(!statusResult) return setCredits(statusResult.credits) }, []) + + const loadApiKeys = useCallback(async () => { + const result = await api.get('/api/v1/api_keys') + if(!result) return + setApiKeys(result) + }, []) + const loadAll = useCallback(async () => { await bluebird.all([ loadSessions(), loadTransactions(), loadStatus(), + loadApiKeys(), ]) }, [ loadTransactions, loadStatus, + loadApiKeys, ]) const onLogin = useCallback(() => { @@ -184,6 +197,7 @@ export const useAccountContext = (): IAccountContext => { sessions, transactions, loadSessions, + apiKeys, onLogin, onLogout, }), [ @@ -193,6 +207,7 @@ export const useAccountContext = (): IAccountContext => { sessions, transactions, loadSessions, + apiKeys, onLogin, onLogout, ]) diff --git a/frontend/src/pages/Account.tsx b/frontend/src/pages/Account.tsx index d6fab3dd68..e80afd50d9 100644 --- a/frontend/src/pages/Account.tsx +++ b/frontend/src/pages/Account.tsx @@ -1,20 +1,47 @@ import React, { FC } from 'react' import Box from '@mui/material/Box' +import axios from 'axios' import useAccount from '../hooks/useAccount' -import DataGridWithFilters from '../components/datagrid/DataGridWithFilters' +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction' +import Typography from '@mui/material/Typography' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' +import IconButton from '@mui/material/IconButton' +import DeleteIcon from '@mui/icons-material/Delete' const Account: FC = () => { const account = useAccount() if(!account.user) return null + const handleDeleteApiKey = async (key: string) => { + try { + await axios.delete(`/api/v1/api_keys?key=${key}`) + } catch (error) { + console.error(error) + } + } + return ( - - account page - - } - /> + + API Keys + + {account.apiKeys.map((apiKey) => ( + + + + handleDeleteApiKey(apiKey.key)} + > + + + + + ))} + + ) } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index bcf5611591..05d258bfa4 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -206,5 +206,7 @@ const Dashboard: FC = () => { ); } +// TODO: replace iframe above with a gradio-embed javascript lib, otherwise it's +// iframe-in-iframe, and scroll bars are undisablable export default Dashboard \ No newline at end of file diff --git a/frontend/src/pages/Layout.tsx b/frontend/src/pages/Layout.tsx index 48e5840251..ec7787f7a7 100644 --- a/frontend/src/pages/Layout.tsx +++ b/frontend/src/pages/Layout.tsx @@ -77,6 +77,7 @@ const AppBar = styled(MuiAppBar, { const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( ({ theme, open }) => ({ '& .MuiDrawer-paper': { + backgroundColor: "#f8f8f8", position: 'relative', whiteSpace: 'nowrap', width: drawerWidth, @@ -486,7 +487,7 @@ const Layout: FC = ({ account.onLogin() }} > - Login + Login/Register ) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 8ab6010d29..4f1c6b7bef 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -25,6 +25,13 @@ export interface IBalanceTransfer { export type IOwnerType = 'user' | 'system' | 'org'; +export interface IApiKey { + owner: string; + owner_type: string; + key: string; + name: string; +} + export interface IFileStoreBreadcrumb { path: string, title: string, diff --git a/gradio/Dockerfile b/gradio/Dockerfile new file mode 100644 index 0000000000..0ed491ef62 --- /dev/null +++ b/gradio/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3 +RUN mkdir /app +WORKDIR /app +ADD . /app +RUN pip install -r requirements.txt \ No newline at end of file diff --git a/gradio/main.py b/gradio/main.py new file mode 100644 index 0000000000..48f910dee1 --- /dev/null +++ b/gradio/main.py @@ -0,0 +1,54 @@ +from fastapi import FastAPI +import gradio as gr + +# TODO: implement multiple pages within the app as separate gradio apps within +# this python process + +# must match path nginx/noxy is proxying to (see docker-compose.yml) +CUSTOM_PATH = "/gradio" + +app = FastAPI() + +# should never access this route directly +@app.get("/") +def read_main(): + return {"message": "here be dragons"} + +def cowsay(message, request: gr.Request): + return "Hello " + message + "! " + str(dict(request.query_params)) + +def alternatingly_agree(message, history): + if len(history) % 2 == 0: + return f"Yes, I do think that '{message}'" + else: + return "I don't think so" + +# TODO: show the API call made to LilySaaS API in the UI, so users can see +# easily how to recreate it + +APPS = { + "cowsay": + gr.Interface( + fn=cowsay, + inputs=gr.Textbox(lines=2, placeholder="What would you like the cow to say?"), + outputs="text", + allow_flagging="never", + css="footer {visibility: hidden}" + ), + "sdxl": + gr.Interface( + fn=cowsay, + inputs=gr.Textbox(lines=2, placeholder="Enter prompt for SDXL"), + outputs="image", + allow_flagging="never", + css="footer {visibility: hidden}" + ), + "mistral7b": + gr.ChatInterface(alternatingly_agree, + css="footer {visibility: hidden}" + ), +} + +for (app_name, gradio_app) in APPS.items(): + print("mounting app", app_name, "->", gradio_app) + app.mount(CUSTOM_PATH+"/"+app_name, gr.routes.App.create_app(gradio_app)) \ No newline at end of file diff --git a/gradio/requirements.txt b/gradio/requirements.txt new file mode 100644 index 0000000000..5c5b76773a --- /dev/null +++ b/gradio/requirements.txt @@ -0,0 +1 @@ +gradio==3.50.2 \ No newline at end of file