From 8476e6f95cab7d9eb79c27efa31ec0e56ea9e728 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 31 Jul 2024 17:32:30 +0200 Subject: [PATCH] expose route/location info to the assistant --- .../components/Assistant/Model/Assistant.tsx | 2 + .../Assistant/Model/GlobalContext/index.ts | 49 ++++++ .../__snapshots__/navigate.spec.ts.snap | 75 ++++++++++ .../components/Assistant/Model/navigation.ts | 139 ++++++++++++++++-- .../containers/Search/AssistantContext.tsx | 120 +-------------- catalog/app/utils/Navigation.ts | 2 +- 6 files changed, 252 insertions(+), 135 deletions(-) create mode 100644 catalog/app/components/Assistant/Model/GlobalContext/index.ts diff --git a/catalog/app/components/Assistant/Model/Assistant.tsx b/catalog/app/components/Assistant/Model/Assistant.tsx index f969fad3c73..747a255180e 100644 --- a/catalog/app/components/Assistant/Model/Assistant.tsx +++ b/catalog/app/components/Assistant/Model/Assistant.tsx @@ -9,6 +9,7 @@ import * as Actor from 'utils/Actor' import * as Bedrock from './Bedrock' import * as Context from './Context' import * as Conversation from './Conversation' +import * as GlobalContext from './GlobalContext' import * as GlobalTools from './GlobalTools' function usePassThru(val: T) { @@ -36,6 +37,7 @@ function useConstructAssistantAPI() { Context.usePushContext({ tools: GlobalTools.use(), + messages: GlobalContext.use(), }) // XXX: move this to actor state? diff --git a/catalog/app/components/Assistant/Model/GlobalContext/index.ts b/catalog/app/components/Assistant/Model/GlobalContext/index.ts new file mode 100644 index 00000000000..08d72db365a --- /dev/null +++ b/catalog/app/components/Assistant/Model/GlobalContext/index.ts @@ -0,0 +1,49 @@ +import * as React from 'react' + +import { useCurrentRoute } from '../navigation' + +function useRouteContext() { + const { match, loc } = useCurrentRoute() + + const description = React.useMemo(() => { + if (!match) return '' + const params = match.decoded?.params + ? ` + + ${JSON.stringify(match.decoded.params, null, 2)} + +` + : '' + return ` + + Name: "${match.descriptor.name}" + + ${match.descriptor.description} + + ${params} + +` + }, [match]) + + const msg = React.useMemo( + () => + ` + + + ${JSON.stringify(loc, null, 2)} + + ${description} + Refer to "navigate" tool schema for navigable routes and their parameters. + +`, + [description, loc], + ) + + return msg +} + +export function useGlobalContext() { + return [useRouteContext()] +} + +export { useGlobalContext as use } diff --git a/catalog/app/components/Assistant/Model/GlobalTools/__snapshots__/navigate.spec.ts.snap b/catalog/app/components/Assistant/Model/GlobalTools/__snapshots__/navigate.spec.ts.snap index 5aaeee04016..a431058574c 100644 --- a/catalog/app/components/Assistant/Model/GlobalTools/__snapshots__/navigate.spec.ts.snap +++ b/catalog/app/components/Assistant/Model/GlobalTools/__snapshots__/navigate.spec.ts.snap @@ -213,6 +213,31 @@ exports[`components/Assistant/Model/GlobalTools/navigate NavigateSchema produced ], "type": "object", }, + { + "additionalProperties": false, + "description": "Installation page", + "properties": { + "name": { + "const": "install", + }, + "params": { + "$id": "/schemas/{}", + "oneOf": [ + { + "type": "object", + }, + { + "type": "array", + }, + ], + }, + }, + "required": [ + "name", + "params", + ], + "type": "object", + }, { "additionalProperties": false, "description": "Search page", @@ -578,6 +603,56 @@ exports[`components/Assistant/Model/GlobalTools/navigate NavigateSchema produced ], "type": "object", }, + { + "additionalProperties": false, + "description": "TBD", + "properties": { + "name": { + "const": "activate", + }, + "params": { + "$id": "/schemas/{}", + "oneOf": [ + { + "type": "object", + }, + { + "type": "array", + }, + ], + }, + }, + "required": [ + "name", + "params", + ], + "type": "object", + }, + { + "additionalProperties": false, + "description": "Bucket root page", + "properties": { + "name": { + "const": "bucket", + }, + "params": { + "$id": "/schemas/{}", + "oneOf": [ + { + "type": "object", + }, + { + "type": "array", + }, + ], + }, + }, + "required": [ + "name", + "params", + ], + "type": "object", + }, ], }, }, diff --git a/catalog/app/components/Assistant/Model/navigation.ts b/catalog/app/components/Assistant/Model/navigation.ts index 6d9e2057461..eac16fe2546 100644 --- a/catalog/app/components/Assistant/Model/navigation.ts +++ b/catalog/app/components/Assistant/Model/navigation.ts @@ -1,4 +1,5 @@ import * as Eff from 'effect' +import * as React from 'react' import * as RR from 'react-router-dom' import { Schema as S } from '@effect/schema' @@ -9,21 +10,90 @@ import * as Nav from 'utils/Navigation' const MODULE = 'Assistant/Model/navigation' -const home = Nav.makeRoute({ - name: 'home', - path: ROUTES.home.path, - description: 'Home page', - // searchParams: S.Struct({ - // // XXX: passing this param doesn't actually work bc of how it's implemented in - // // website/pages/Landing/Buckets/Buckets.js - // q: SearchParamLastOpt.annotations({ - // title: 'bucket filter query', - // description: 'filter buckets in the bucket grid', - // }), - // }), -}) - -const routeList = [home, search] as const +// the routes are in the order of matching +const routeList = [ + Nav.makeRoute({ + name: 'home', + path: ROUTES.home.path, + description: 'Home page', + // searchParams: S.Struct({ + // // XXX: passing this param doesn't actually work bc of how it's implemented in + // // website/pages/Landing/Buckets/Buckets.js + // q: SearchParamLastOpt.annotations({ + // title: 'bucket filter query', + // description: 'filter buckets in the bucket grid', + // }), + // }), + }), + Nav.makeRoute({ + name: 'install', + path: ROUTES.install.path, + description: 'Installation page', + }), + search, + Nav.makeRoute({ + name: 'activate', + path: ROUTES.activate.path, + description: 'TBD', + }), + // + // + // + // + // + // + // + // + // + // + // + // {(cfg.passwordAuth === true || cfg.ssoAuth === true) && ( + // + // + // + // )} + // {!!cfg.passwordAuth && ( + // + // + // + // )} + // {!!cfg.passwordAuth && ( + // + // + // + // )} + // + // + // + // + // + // + // + // + // + // {cfg.mode === 'OPEN' && ( + // // XXX: show profile in all modes? + // + // + // + // )} + // + // + // + // + // + // + // + // + // + Nav.makeRoute({ + name: 'bucket', + path: ROUTES.bucketRoot.path, + description: 'Bucket root page', + // XXX: this should hold the bucket name and subroute info (e.g. package vs file view) + }), +] as const + type KnownRoute = (typeof routeList)[number] type KnownRouteMap = { [K in KnownRoute['name']]: Extract @@ -51,3 +121,42 @@ export const navigate = (route: NavigableRoute, history: History) => Eff.Effect.andThen((loc) => Eff.Effect.sync(() => history.push(loc))), ), ) + +interface Match { + descriptor: KnownRoute + decoded: NavigableRoute | null +} + +const matchLocation = (loc: typeof Nav.Location.Type): Match | null => + Eff.pipe( + Eff.Array.findFirst(routeList, (route) => + RR.matchPath(loc.pathname, { path: route.path, exact: true }) + ? Eff.Option.some(route) + : Eff.Option.none(), + ), + Eff.Option.map((descriptor) => ({ + descriptor, + decoded: Eff.pipe( + loc, + // @ts-expect-error + S.decodeOption(descriptor.paramsSchema), + Eff.Option.map((params) => ({ name: descriptor.name, params }) as NavigableRoute), + Eff.Option.getOrNull, + ), + })), + Eff.Option.getOrNull, + ) + +interface LocationInfo { + loc: typeof Nav.Location.Type + match: Match | null +} + +export function useCurrentRoute(): LocationInfo { + const loc = RR.useLocation() + const match = React.useMemo( + () => matchLocation({ pathname: loc.pathname, search: loc.search, hash: '' }), + [loc.pathname, loc.search], + ) + return { match, loc } +} diff --git a/catalog/app/containers/Search/AssistantContext.tsx b/catalog/app/containers/Search/AssistantContext.tsx index 626f4f98353..cc0c74f5bb2 100644 --- a/catalog/app/containers/Search/AssistantContext.tsx +++ b/catalog/app/containers/Search/AssistantContext.tsx @@ -3,60 +3,11 @@ import * as React from 'react' import { Schema as S } from '@effect/schema' import * as Assistant from 'components/Assistant' -// import * as Model from 'model' import { runtime } from 'utils/Effect' import useConstant from 'utils/useConstant' import * as SearchUIModel from './model' -const RESULT_TYPE_LABELS = { - [SearchUIModel.ResultType.QuiltPackage]: 'Quilt Packages', - [SearchUIModel.ResultType.S3Object]: 'S3 Objects', -} - -const intro = (model: SearchUIModel.SearchUIModel) => { - let lines: string[] = [] - lines.push(`You see the Quilt Catalog's search page with the following parameters:`) - lines.push('') - lines.push(`- result type: ${RESULT_TYPE_LABELS[model.state.resultType]}`) - lines.push(`- result order: ${model.state.order}`) - lines.push( - model.state.searchString - ? `- search string: ${model.state.searchString}` - : '- search string is empty', - ) - lines.push( - model.state.buckets.length - ? `- in buckets: ${model.state.buckets.join(', ')}` - : '- in all buckets', - ) - lines.push('') - lines.push('Prefer using local tools over global') // XXX - return lines.join('\n') -} - -function useMessages(model: SearchUIModel.SearchUIModel) { - return [intro(model)] -} - -// const RefineSearchSchema = S.Struct({ -// searchString: S.optional(S.String).annotations({ -// description: 'set search string', -// }), -// order: S.optional(S.Enums(Model.GQLTypes.SearchResultOrder)).annotations({ -// description: 'set result order', -// }), -// resultType: S.optional(S.Enums(SearchUIModel.ResultType)).annotations({ -// description: 'set result type', -// }), -// buckets: S.optional(S.Array(S.String)).annotations({ -// description: 'select buckets to search in (keep empty to search in all buckets)', -// }), -// }).annotations({ -// description: -// 'Refine current search by adjusting search parameters. Dont provide a parameter to keep it as is', -// }) - const GetResultsSchema = S.Struct({ dummy: S.optional(S.String).annotations({ description: 'not used', @@ -148,7 +99,6 @@ function useGetResults(model: SearchUIModel.SearchUIModel) { GetResultsSchema, () => Eff.Effect.gen(function* () { - yield* Eff.Console.debug('tool: get search results') // wait til results are Some const lastOpt = yield* ref.changes.pipe( Eff.Stream.takeUntil((x) => Eff.Option.isSome(x)), @@ -170,78 +120,10 @@ function useGetResults(model: SearchUIModel.SearchUIModel) { ) } -const withPrefix = >(prefix: string, obj: T) => - Object.entries(obj).reduce((acc, [k, v]) => ({ ...acc, [prefix + k]: v }), {}) - -function useTools(model: SearchUIModel.SearchUIModel) { - // const { - // updateUrlState, - // // setSearchString, - // // setOrder, - // // setResultType, - // // setBuckets, - // // - // // activateObjectsFilter, - // // deactivateObjectsFilter, - // // setObjectsFilter, - // // - // // activatePackagesFilter, - // // deactivatePackagesFilter, - // // setPackagesFilter, - // // - // // activatePackagesMetaFilter, - // // deactivatePackagesMetaFilter, - // // setPackagesMetaFilter, - // // - // // clearFilters, - // // reset, - // } = model.actions - - return withPrefix('catalog_search_', { - // refine: Assistant.Model.Tool.useMakeTool( - // RefineSearchSchema, - // (params) => - // Eff.Effect.gen(function* () { - // yield* Eff.Effect.sync(() => - // updateUrlState((s) => ({ ...s, ...(params as any) })), - // ) - // return Eff.Option.some( - // Assistant.Model.Tool.Result({ - // status: 'success', - // content: [ - // Assistant.Model.Content.text( - // 'Search parameters updated. Use catalog_search_getResults tool to get the search results.', - // ), - // ], - // }), - // ) - // }), - // [updateUrlState], - // ), - // - // activateObjectsFilter, - // deactivateObjectsFilter, - // setObjectsFilter, - // - // activatePackagesFilter, - // deactivatePackagesFilter, - // setPackagesFilter, - // - // activatePackagesMetaFilter, - // deactivatePackagesMetaFilter, - // setPackagesMetaFilter, - // - // clearFilters, - // reset, - getResults: useGetResults(model), - }) -} - export default function AssistantContext() { const model = SearchUIModel.use() Assistant.Context.usePushContext({ - tools: useTools(model), - messages: useMessages(model), + tools: { getSearchResults: useGetResults(model) }, }) return null } diff --git a/catalog/app/utils/Navigation.ts b/catalog/app/utils/Navigation.ts index f2c9edbc72a..afe592c72bf 100644 --- a/catalog/app/utils/Navigation.ts +++ b/catalog/app/utils/Navigation.ts @@ -6,7 +6,7 @@ import { Schema as S, ParseResult } from '@effect/schema' import { JsonRecord } from 'utils/types' // XXX: make into a class? -const Location = S.Struct({ +export const Location = S.Struct({ pathname: S.String, search: S.String, hash: S.String,