From 6da641411a4983aa2ad42c521dcceed29dc1c403 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 8 Jul 2024 16:27:30 +0200 Subject: [PATCH] tool for getting search results --- .../components/Assistant/Model/GlobalTools.ts | 2 +- .../app/components/Assistant/UI/Chat/Chat.tsx | 2 +- .../containers/Search/AssistantContext.tsx | 154 +++++++++++++++--- 3 files changed, 129 insertions(+), 29 deletions(-) diff --git a/catalog/app/components/Assistant/Model/GlobalTools.ts b/catalog/app/components/Assistant/Model/GlobalTools.ts index 2602a2e92c1..3289b8248ea 100644 --- a/catalog/app/components/Assistant/Model/GlobalTools.ts +++ b/catalog/app/components/Assistant/Model/GlobalTools.ts @@ -48,7 +48,7 @@ function useStartSearch() { status: 'success', content: [ Content.ToolResultContentBlock.Text({ - text: `navigating to the search page and starting the search session`, + text: 'Navigating to the search page and starting the search session. Use catalog_search_getResults tool to get the search results.', }), ], }), diff --git a/catalog/app/components/Assistant/UI/Chat/Chat.tsx b/catalog/app/components/Assistant/UI/Chat/Chat.tsx index bc6f227fb62..c0d6d404bd2 100644 --- a/catalog/app/components/Assistant/UI/Chat/Chat.tsx +++ b/catalog/app/components/Assistant/UI/Chat/Chat.tsx @@ -235,7 +235,7 @@ export default function Chat({ state, dispatch }: ChatProps) {
- Hi! I'm Qurator. How can I help you? + Hi! I'm Qurator, your AI assistant. How can I help you? {state.events.map( Model.Conversation.Event.$match({ diff --git a/catalog/app/containers/Search/AssistantContext.tsx b/catalog/app/containers/Search/AssistantContext.tsx index 1345fd4f9cb..69ca9c7f7da 100644 --- a/catalog/app/containers/Search/AssistantContext.tsx +++ b/catalog/app/containers/Search/AssistantContext.tsx @@ -1,9 +1,10 @@ import * as Eff from 'effect' +import * as React from 'react' import { Schema as S } from '@effect/schema' import * as Assistant from 'components/Assistant' import * as Model from 'model' -import assertNever from 'utils/assertNever' +import useConstant from 'utils/useConstant' import * as SearchUIModel from './model' @@ -33,32 +34,8 @@ const intro = (model: SearchUIModel.SearchUIModel) => { return lines.join('\n') } -const results = (model: SearchUIModel.SearchUIModel) => { - const r = model.firstPageQuery - switch (r._tag) { - case 'fetching': - return 'Search results are still loading...' - case 'error': - return `Search request failed with error:\n${JSON.stringify(r.error)}` - case 'data': - switch (r.data.__typename) { - case 'InvalidInput': - return `Search request failed with error:\n${JSON.stringify(r.data)}` - case 'EmptySearchResultSet': - return 'Search request returned no results' - case 'ObjectsSearchResultSet': - case 'PackagesSearchResultSet': - return `Search request returned ${r.data.stats.total} results` - default: - assertNever(r.data) - } - default: - assertNever(r) - } -} - function useMessages(model: SearchUIModel.SearchUIModel) { - return [intro(model), results(model)] + return [intro(model)] } const RefineSearchSchema = S.Struct({ @@ -79,6 +56,119 @@ const RefineSearchSchema = S.Struct({ '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', + }), +}).annotations({ + description: 'Get current search results', +}) + +function useGetResults(model: SearchUIModel.SearchUIModel) { + const result: Eff.Option.Option = React.useMemo( + () => + Eff.Match.value(model.firstPageQuery).pipe( + Eff.Match.tag('fetching', () => Eff.Option.none()), + Eff.Match.tag('error', (r) => + Eff.Option.some( + Assistant.Model.Tool.Result({ + status: 'error', + content: [ + Assistant.Model.Content.text( + `Search request failed with error:\n${JSON.stringify(r.error)}`, + ), + ], + }), + ), + ), + Eff.Match.tag('data', (r) => + Eff.Match.value(r.data).pipe( + Eff.Match.when({ __typename: 'InvalidInput' }, (data) => + Eff.Option.some( + Assistant.Model.Tool.Result({ + status: 'error', + content: [ + Assistant.Model.Content.text( + `Search request failed with error:\n${JSON.stringify(data)}`, + ), + ], + }), + ), + ), + Eff.Match.when({ __typename: 'EmptySearchResultSet' }, () => + Eff.Option.some( + Assistant.Model.Tool.Result({ + status: 'success', + content: [ + Assistant.Model.Content.text('Search request returned no results'), + ], + }), + ), + ), + Eff.Match.orElse((data) => { + // 'ObjectsSearchResultSet' + // 'PackagesSearchResultSet' + const label = + data.__typename === 'ObjectsSearchResultSet' ? 'objects' : 'packages' + return Eff.Option.some( + Assistant.Model.Tool.Result({ + status: 'success', + content: [ + Assistant.Model.Content.text( + `Found ${data.stats.total} ${label}. First page follows:`, + ), + ...data.firstPage.hits.map((hit, i) => + Assistant.Model.Content.text( + `\n${JSON.stringify( + hit, + null, + 2, + )}\n`, + ), + ), + ], + }), + ) + }), + ), + ), + Eff.Match.exhaustive, + ), + [model.firstPageQuery], + ) + + const ref = useConstant(() => Eff.Effect.runSync(Eff.SubscriptionRef.make(result))) + + React.useEffect(() => { + Eff.Effect.runFork(Eff.SubscriptionRef.set(ref, result)) + }, [result, ref]) + + return Assistant.Model.Tool.useMakeTool( + 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)), + Eff.Stream.runLast, + ) + const last = Eff.Option.flatten(lastOpt) + // should be some + const res = Eff.Option.match(last, { + onSome: (value) => value, + onNone: () => + Assistant.Model.Tool.Result({ + status: 'error', + content: [Assistant.Model.Content.text('Got empty results')], + }), + }) + return Eff.Option.some(res) + }), + [ref], + ) +} + const withPrefix = >(prefix: string, obj: T) => Object.entries(obj).reduce((acc, [k, v]) => ({ ...acc, [prefix + k]: v }), {}) @@ -114,7 +204,16 @@ function useTools(model: SearchUIModel.SearchUIModel) { yield* Eff.Effect.sync(() => updateUrlState((s) => ({ ...s, ...(params as any) })), ) - return Eff.Option.none() + 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], ), @@ -133,6 +232,7 @@ function useTools(model: SearchUIModel.SearchUIModel) { // // clearFilters, // reset, + getResults: useGetResults(model), }) }