diff --git a/catalog/CHANGELOG.md b/catalog/CHANGELOG.md index 159c9776ffb..e1f34f44485 100644 --- a/catalog/CHANGELOG.md +++ b/catalog/CHANGELOG.md @@ -17,6 +17,9 @@ where verb is one of ## Changes +- [Changed] Qurator: propagate error messages from Bedrock ([#4192](https://github.com/quiltdata/quilt/pull/4192)) +- [Added] Qurator Developer Tools ([#4192](https://github.com/quiltdata/quilt/pull/4192)) +- [Changed] JsonDisplay: handle dates and functions ([#4192](https://github.com/quiltdata/quilt/pull/4192)) - [Fixed] Keep default Intercom launcher closed when closing Package Dialog ([#4244](https://github.com/quiltdata/quilt/pull/4244)) - [Fixed] Handle invalid bucket name in `ui.sourceBuckets` in bucket config ([#4242](https://github.com/quiltdata/quilt/pull/4242)) - [Added] Preview Markdown while editing ([#4153](https://github.com/quiltdata/quilt/pull/4153)) diff --git a/catalog/app/components/Assistant/Model/Assistant.tsx b/catalog/app/components/Assistant/Model/Assistant.tsx index a8eb8538acd..74667acf28c 100644 --- a/catalog/app/components/Assistant/Model/Assistant.tsx +++ b/catalog/app/components/Assistant/Model/Assistant.tsx @@ -62,7 +62,8 @@ function useConstructAssistantAPI() { } } -type AssistantAPI = ReturnType +export type AssistantAPI = ReturnType +export type { AssistantAPI as API } const Ctx = React.createContext(null) diff --git a/catalog/app/components/Assistant/Model/Bedrock.ts b/catalog/app/components/Assistant/Model/Bedrock.ts index d8b438b4ff4..11501f73697 100644 --- a/catalog/app/components/Assistant/Model/Bedrock.ts +++ b/catalog/app/components/Assistant/Model/Bedrock.ts @@ -1,3 +1,4 @@ +import type * as AWSSDK from 'aws-sdk' import BedrockRuntime from 'aws-sdk/clients/bedrockruntime' import * as Eff from 'effect' @@ -110,6 +111,10 @@ const toolConfigToBedrock = ( }), }) +function isAWSError(e: any): e is AWSSDK.AWSError { + return e.code !== undefined && e.message !== undefined +} + // a layer providing the service over aws.bedrock export function LLMBedrock(bedrock: BedrockRuntime) { const converse = (prompt: LLM.Prompt, opts?: LLM.Options) => @@ -127,21 +132,28 @@ export function LLMBedrock(bedrock: BedrockRuntime) { opts, ], })( - Eff.Effect.tryPromise(() => - bedrock - .converse({ - modelId: MODEL_ID, - system: [{ text: prompt.system }], - messages: messagesToBedrock(prompt.messages), - toolConfig: prompt.toolConfig && toolConfigToBedrock(prompt.toolConfig), - ...opts, - }) - .promise() - .then((backendResponse) => ({ - backendResponse, - content: mapContent(backendResponse.output.message?.content), - })), - ), + Eff.Effect.tryPromise({ + try: () => + bedrock + .converse({ + modelId: MODEL_ID, + system: [{ text: prompt.system }], + messages: messagesToBedrock(prompt.messages), + toolConfig: prompt.toolConfig && toolConfigToBedrock(prompt.toolConfig), + ...opts, + }) + .promise() + .then((backendResponse) => ({ + backendResponse, + content: mapContent(backendResponse.output.message?.content), + })), + catch: (e) => + new LLM.LLMError({ + message: isAWSError(e) + ? `Bedrock error (${e.code}): ${e.message}` + : `Unexpected error: ${e}`, + }), + }), ) return Eff.Layer.succeed(LLM.LLM, { converse }) diff --git a/catalog/app/components/Assistant/Model/Content.ts b/catalog/app/components/Assistant/Model/Content.ts index ab401450aa5..9c45cde3ee3 100644 --- a/catalog/app/components/Assistant/Model/Content.ts +++ b/catalog/app/components/Assistant/Model/Content.ts @@ -104,4 +104,4 @@ export type PromptMessageContentBlock = Eff.Data.TaggedEnum<{ export const PromptMessageContentBlock = Eff.Data.taggedEnum() export const text = (first: string, ...rest: string[]) => - PromptMessageContentBlock.Text({ text: [first, ...rest].join('') }) + PromptMessageContentBlock.Text({ text: [first, ...rest].join('\n') }) diff --git a/catalog/app/components/Assistant/Model/Conversation.ts b/catalog/app/components/Assistant/Model/Conversation.ts index 48e9c47c47a..50a7ee8053e 100644 --- a/catalog/app/components/Assistant/Model/Conversation.ts +++ b/catalog/app/components/Assistant/Model/Conversation.ts @@ -98,7 +98,7 @@ export type Action = Eff.Data.TaggedEnum<{ readonly content: string } LLMError: { - readonly error: Eff.Cause.UnknownException + readonly error: LLM.LLMError } LLMResponse: { readonly content: Exclude[] @@ -144,8 +144,8 @@ const llmRequest = (events: Event[]) => const response = yield* llm.converse(prompt) if (Eff.Option.isNone(response.content)) { - return yield* new Eff.Cause.UnknownException( - new Error('No content in LLM response'), + return yield* Eff.Effect.fail( + new LLM.LLMError({ message: 'No content in LLM response' }), ) } @@ -193,8 +193,8 @@ export const ConversationActor = Eff.Effect.succeed( WaitingForAssistant: { LLMError: ({ events }, { error }) => idle(events, { - message: 'Error while interacting with LLM. Please try again.', - details: `${error}`, + message: 'Error while interacting with LLM.', + details: error.message, }), LLMResponse: (state, { content, toolUses }, dispatch) => Eff.Effect.gen(function* () { @@ -360,7 +360,7 @@ to your advantage. Use GitHub Flavored Markdown syntax for formatting when appropriate. ` -const constructPrompt = ( +export const constructPrompt = ( events: Event[], context: Context.ContextShape, ): Eff.Effect.Effect => diff --git a/catalog/app/components/Assistant/Model/LLM.ts b/catalog/app/components/Assistant/Model/LLM.ts index be093502fc8..98be8ad3b26 100644 --- a/catalog/app/components/Assistant/Model/LLM.ts +++ b/catalog/app/components/Assistant/Model/LLM.ts @@ -57,6 +57,14 @@ interface ConverseResponse { backendResponse: BedrockRuntime.ConverseResponse } +export class LLMError { + message: string + + constructor({ message }: { message: string }) { + this.message = message + } +} + // a service export class LLM extends Eff.Context.Tag('LLM')< LLM, @@ -64,6 +72,6 @@ export class LLM extends Eff.Context.Tag('LLM')< converse: ( prompt: Prompt, opts?: Options, - ) => Eff.Effect.Effect + ) => Eff.Effect.Effect } >() {} diff --git a/catalog/app/components/Assistant/UI/Chat/Chat.tsx b/catalog/app/components/Assistant/UI/Chat/Chat.tsx index 24d03e92cd2..0b58b93acc6 100644 --- a/catalog/app/components/Assistant/UI/Chat/Chat.tsx +++ b/catalog/app/components/Assistant/UI/Chat/Chat.tsx @@ -9,6 +9,7 @@ import usePrevious from 'utils/usePrevious' import * as Model from '../../Model' +import DevTools from './DevTools' import Input from './Input' const BG = { @@ -145,10 +146,8 @@ function MessageAction({ children, onClick }: MessageActionProps) { ) } -type ConversationDispatch = (action: Model.Conversation.Action) => void - interface ConversationDispatchProps { - dispatch: ConversationDispatch + dispatch: Model.Assistant.API['dispatch'] } interface ConversationStateProps { @@ -281,13 +280,85 @@ function WaitingState({ timestamp, dispatch }: WaitingStateProps) { ) } -const useChatStyles = M.makeStyles((t) => ({ +interface MenuProps { + state: Model.Assistant.API['state'] + dispatch: Model.Assistant.API['dispatch'] + onToggleDevTools: () => void + devToolsOpen: boolean + className?: string +} + +function Menu({ state, dispatch, devToolsOpen, onToggleDevTools, className }: MenuProps) { + const [menuOpen, setMenuOpen] = React.useState(null) + + const isIdle = state._tag === 'Idle' + + const toggleMenu = React.useCallback( + (e: React.BaseSyntheticEvent) => + setMenuOpen((prev) => (prev ? null : e.currentTarget)), + [setMenuOpen], + ) + const closeMenu = React.useCallback(() => setMenuOpen(null), [setMenuOpen]) + + const startNewSession = React.useCallback(() => { + if (isIdle) dispatch(Model.Conversation.Action.Clear()) + closeMenu() + }, [closeMenu, isIdle, dispatch]) + + const showDevTools = React.useCallback(() => { + onToggleDevTools() + closeMenu() + }, [closeMenu, onToggleDevTools]) + + return ( + <> + + + menu + + + + + + close + + + + + + New session + + Developer Tools + + + ) +} + +const useStyles = M.makeStyles((t) => ({ chat: { display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden', }, + menu: { + position: 'absolute', + right: t.spacing(1), + top: t.spacing(1), + zIndex: 1, + }, + devTools: { + height: '50%', + }, historyContainer: { flexGrow: 1, overflowY: 'auto', @@ -313,15 +384,15 @@ const useChatStyles = M.makeStyles((t) => ({ })) interface ChatProps { - state: Model.Conversation.State - dispatch: ConversationDispatch + state: Model.Assistant.API['state'] + dispatch: Model.Assistant.API['dispatch'] } export default function Chat({ state, dispatch }: ChatProps) { - const classes = useChatStyles() + const classes = useStyles() const scrollRef = React.useRef(null) - const inputDisabled = state._tag != 'Idle' + const inputDisabled = state._tag !== 'Idle' const stateFingerprint = `${state._tag}:${state.timestamp.getTime()}` @@ -341,8 +412,27 @@ export default function Chat({ state, dispatch }: ChatProps) { [dispatch], ) + const [devToolsOpen, setDevToolsOpen] = React.useState(false) + + const toggleDevTools = React.useCallback( + () => setDevToolsOpen((prev) => !prev), + [setDevToolsOpen], + ) + return (
+ + + + + +
@@ -375,8 +465,9 @@ export default function Chat({ state, dispatch }: ChatProps) { Eff.Option.match(s.error, { onSome: (e) => ( - Error occurred: {e.message} -
{e.details}
+ {e.message} +
+ {e.details}
// TODO: retry / discard ), diff --git a/catalog/app/components/Assistant/UI/Chat/DevTools.tsx b/catalog/app/components/Assistant/UI/Chat/DevTools.tsx new file mode 100644 index 00000000000..d7ed7e5501c --- /dev/null +++ b/catalog/app/components/Assistant/UI/Chat/DevTools.tsx @@ -0,0 +1,61 @@ +import * as Eff from 'effect' +import * as React from 'react' +import * as M from '@material-ui/core' + +import JsonDisplay from 'components/JsonDisplay' + +import * as Model from '../../Model' + +const useStyles = M.makeStyles((t) => ({ + root: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + heading: { + ...t.typography.h5, + borderBottom: `1px solid ${t.palette.divider}`, + lineHeight: '64px', + paddingLeft: t.spacing(2), + }, + contents: { + flexGrow: 1, + overflow: 'auto', + }, + json: { + margin: t.spacing(2, 0), + padding: t.spacing(0, 2), + }, +})) + +interface DevToolsProps { + state: Model.Assistant.API['state'] +} + +export default function DevTools({ state }: DevToolsProps) { + const classes = useStyles() + + const context = Model.Context.useAggregatedContext() + + const prompt = React.useMemo( + () => + Eff.Effect.runSync( + Model.Conversation.constructPrompt( + state.events.filter((e) => !e.discarded), + context, + ), + ), + [state, context], + ) + + return ( +
+

Qurator Developer Tools

+
+ + + +
+
+ ) +} diff --git a/catalog/app/components/JsonDisplay/JsonDisplay.tsx b/catalog/app/components/JsonDisplay/JsonDisplay.tsx index 1e7af7b2fbf..74b56e3e692 100644 --- a/catalog/app/components/JsonDisplay/JsonDisplay.tsx +++ b/catalog/app/components/JsonDisplay/JsonDisplay.tsx @@ -123,7 +123,14 @@ function getHref(v: string) { } function NonStringValue({ value }: { value: PrimitiveValue }) { - return
{`${value}`}
+ const formatted = React.useMemo(() => { + if (value instanceof Date) return `Date(${value.toISOString()})` + if (typeof value === 'function') { + return `Function(${(value as Function).name || 'anonymous'})` + } + return `${value}` + }, [value]) + return
{formatted}
} function S3UrlValue({ href, children }: React.PropsWithChildren<{ href: string }>) { diff --git a/catalog/package-lock.json b/catalog/package-lock.json index 5d48b7d2d2e..008085ba6b9 100644 --- a/catalog/package-lock.json +++ b/catalog/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^6.0.4", - "@effect/platform": "^0.67.0", - "@effect/schema": "^0.75.0", + "@effect/platform": "^0.67.1", + "@effect/schema": "^0.75.5", "@finos/perspective": "^1.9.4", "@finos/perspective-viewer": "^1.9.4", "@finos/perspective-viewer-d3fc": "^1.9.4", @@ -36,7 +36,7 @@ "dedent": "^1.5.1", "dompurify": "^3.1.6", "echarts": "^5.5.0", - "effect": "^3.9.0", + "effect": "^3.10.19", "final-form": "^4.20.9", "fontfaceobserver": "^2.3.0", "fp-ts": "^2.16.1", @@ -1515,27 +1515,27 @@ } }, "node_modules/@effect/platform": { - "version": "0.67.0", - "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.67.0.tgz", - "integrity": "sha512-zozI6oNlycWFjAJcxhdLO0j4R7DoNkEITrHHnecTaGevUJUXh0CUuAthEBnQ4Z2T1rTySl937ZybdvYSdexReg==", + "version": "0.67.1", + "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.67.1.tgz", + "integrity": "sha512-UxbrJPSxjrbFR9m4zbMzoc4kEfwQLEDlJKCpP6d3mHCFskUweYq/vWowYm4+87CjEfEtFoVYpsn/+bE0eK421g==", "dependencies": { "find-my-way-ts": "^0.1.5", "multipasta": "^0.2.5" }, "peerDependencies": { - "@effect/schema": "^0.75.0", - "effect": "^3.9.0" + "@effect/schema": "^0.75.1", + "effect": "^3.9.1" } }, "node_modules/@effect/schema": { - "version": "0.75.0", - "resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.75.0.tgz", - "integrity": "sha512-Bx1sx5UjFQXaWiEKmkS0B/HnauEpdtTo0VoCZL4eNtAUgXfL3q2O/VLFv8idXFg43WhO7WlcYaHRjVxoIgw3DQ==", + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.75.5.tgz", + "integrity": "sha512-TQInulTVCuF+9EIbJpyLP6dvxbQJMphrnRqgexm/Ze39rSjfhJuufF7XvU3SxTgg3HnL7B/kpORTJbHhlE6thw==", "dependencies": { "fast-check": "^3.21.0" }, "peerDependencies": { - "effect": "^3.9.0" + "effect": "^3.9.2" } }, "node_modules/@emotion/hash": { @@ -8164,9 +8164,12 @@ "dev": true }, "node_modules/effect": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.9.0.tgz", - "integrity": "sha512-KnbQNgPxd74cAHvAf0C7qv3+KLeuEVz9jUflCrB3gzVXrJQ0WTQzGF2DGg3frma1dFnPp1J4zJBonUVreAm1BQ==" + "version": "3.10.19", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.10.19.tgz", + "integrity": "sha512-Bc+unZVpHQ/0QkydshNk97OjDXT17Y1M4rBjDZaEPjD6YmlcZxhadEo325OfdPQKWCKEHTdRGtX8/bfQ0RLTIw==", + "dependencies": { + "fast-check": "^3.21.0" + } }, "node_modules/electron-to-chromium": { "version": "1.4.665", @@ -9507,9 +9510,9 @@ } }, "node_modules/fast-check": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.22.0.tgz", - "integrity": "sha512-8HKz3qXqnHYp/VCNn2qfjHdAdcI8zcSqOyX64GOMukp7SL2bfzfeDKjSd+UyECtejccaZv3LcvZTm9YDD22iCQ==", + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.1.tgz", + "integrity": "sha512-u/MudsoQEgBUZgR5N1v87vEgybeVYus9VnDVaIkxkkGP2jt54naghQ3PCQHJiogS8U/GavZCUPFfx3Xkp+NaHw==", "funding": [ { "type": "individual", @@ -21267,18 +21270,18 @@ "dev": true }, "@effect/platform": { - "version": "0.67.0", - "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.67.0.tgz", - "integrity": "sha512-zozI6oNlycWFjAJcxhdLO0j4R7DoNkEITrHHnecTaGevUJUXh0CUuAthEBnQ4Z2T1rTySl937ZybdvYSdexReg==", + "version": "0.67.1", + "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.67.1.tgz", + "integrity": "sha512-UxbrJPSxjrbFR9m4zbMzoc4kEfwQLEDlJKCpP6d3mHCFskUweYq/vWowYm4+87CjEfEtFoVYpsn/+bE0eK421g==", "requires": { "find-my-way-ts": "^0.1.5", "multipasta": "^0.2.5" } }, "@effect/schema": { - "version": "0.75.0", - "resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.75.0.tgz", - "integrity": "sha512-Bx1sx5UjFQXaWiEKmkS0B/HnauEpdtTo0VoCZL4eNtAUgXfL3q2O/VLFv8idXFg43WhO7WlcYaHRjVxoIgw3DQ==", + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.75.5.tgz", + "integrity": "sha512-TQInulTVCuF+9EIbJpyLP6dvxbQJMphrnRqgexm/Ze39rSjfhJuufF7XvU3SxTgg3HnL7B/kpORTJbHhlE6thw==", "requires": { "fast-check": "^3.21.0" } @@ -26540,9 +26543,12 @@ "dev": true }, "effect": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.9.0.tgz", - "integrity": "sha512-KnbQNgPxd74cAHvAf0C7qv3+KLeuEVz9jUflCrB3gzVXrJQ0WTQzGF2DGg3frma1dFnPp1J4zJBonUVreAm1BQ==" + "version": "3.10.19", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.10.19.tgz", + "integrity": "sha512-Bc+unZVpHQ/0QkydshNk97OjDXT17Y1M4rBjDZaEPjD6YmlcZxhadEo325OfdPQKWCKEHTdRGtX8/bfQ0RLTIw==", + "requires": { + "fast-check": "^3.21.0" + } }, "electron-to-chromium": { "version": "1.4.665", @@ -27538,9 +27544,9 @@ "dev": true }, "fast-check": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.22.0.tgz", - "integrity": "sha512-8HKz3qXqnHYp/VCNn2qfjHdAdcI8zcSqOyX64GOMukp7SL2bfzfeDKjSd+UyECtejccaZv3LcvZTm9YDD22iCQ==", + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.1.tgz", + "integrity": "sha512-u/MudsoQEgBUZgR5N1v87vEgybeVYus9VnDVaIkxkkGP2jt54naghQ3PCQHJiogS8U/GavZCUPFfx3Xkp+NaHw==", "requires": { "pure-rand": "^6.1.0" } diff --git a/catalog/package.json b/catalog/package.json index a504bcfb439..2d25a9f0b4d 100644 --- a/catalog/package.json +++ b/catalog/package.json @@ -47,8 +47,8 @@ }, "dependencies": { "@braintree/sanitize-url": "^6.0.4", - "@effect/platform": "^0.67.0", - "@effect/schema": "^0.75.0", + "@effect/platform": "^0.67.1", + "@effect/schema": "^0.75.5", "@finos/perspective": "^1.9.4", "@finos/perspective-viewer": "^1.9.4", "@finos/perspective-viewer-d3fc": "^1.9.4", @@ -73,7 +73,7 @@ "dedent": "^1.5.1", "dompurify": "^3.1.6", "echarts": "^5.5.0", - "effect": "^3.9.0", + "effect": "^3.10.19", "final-form": "^4.20.9", "fontfaceobserver": "^2.3.0", "fp-ts": "^2.16.1",