From fa73d7509d895e1b1bcd8d5ddb887d852eadd653 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Tue, 6 Aug 2024 17:16:49 +0200 Subject: [PATCH] discard tool use; more timestamps --- .../components/Assistant/Model/Assistant.tsx | 2 +- .../Assistant/Model/Conversation.ts | 51 ++++--- .../app/components/Assistant/UI/Chat/Chat.tsx | 133 ++++++++++++------ 3 files changed, 125 insertions(+), 61 deletions(-) diff --git a/catalog/app/components/Assistant/Model/Assistant.tsx b/catalog/app/components/Assistant/Model/Assistant.tsx index 95359573490..3fdc8269752 100644 --- a/catalog/app/components/Assistant/Model/Assistant.tsx +++ b/catalog/app/components/Assistant/Model/Assistant.tsx @@ -30,7 +30,7 @@ function useConstructAssistantAPI() { ) const [state, dispatch] = Actor.useActorLayer( Conversation.ConversationActor, - Eff.Effect.succeed(Conversation.init), + Conversation.init, layerEff, ) diff --git a/catalog/app/components/Assistant/Model/Conversation.ts b/catalog/app/components/Assistant/Model/Conversation.ts index b16c0e8e291..1ea6ecd7c76 100644 --- a/catalog/app/components/Assistant/Model/Conversation.ts +++ b/catalog/app/components/Assistant/Model/Conversation.ts @@ -15,7 +15,7 @@ const MODULE = 'Conversation' // TODO: make this a globally available service? const genId = Eff.Effect.sync(uuid.v4) -interface ToolCall { +export interface ToolCall { readonly name: string readonly input: Record readonly fiber: Eff.Fiber.RuntimeFiber @@ -49,12 +49,16 @@ export type Event = Eff.Data.TaggedEnum<{ // eslint-disable-next-line @typescript-eslint/no-redeclare export const Event = Eff.Data.taggedEnum() +interface StateBase { + readonly events: Event[] + readonly timestamp: Date +} + export type State = Eff.Data.TaggedEnum<{ /** * Waiting for user input */ - Idle: { - readonly events: Event[] + Idle: StateBase & { readonly error: Eff.Option.Option<{ message: string details: string @@ -64,16 +68,14 @@ export type State = Eff.Data.TaggedEnum<{ /** * Waiting for assistant (LLM) to respond */ - WaitingForAssistant: { - readonly events: Event[] + WaitingForAssistant: StateBase & { readonly requestFiber: Eff.Fiber.RuntimeFiber } /** * Tool use in progress */ - ToolUse: { - readonly events: Event[] + ToolUse: StateBase & { // TODO: use HashMap? readonly calls: Record // readonly retries: number @@ -112,9 +114,12 @@ export type Action = Eff.Data.TaggedEnum<{ // eslint-disable-next-line @typescript-eslint/no-redeclare export const Action = Eff.Data.taggedEnum() -export const init = State.Idle({ - events: [], - error: Eff.Option.none(), +export const init = Eff.Effect.gen(function* () { + return State.Idle({ + timestamp: new Date(yield* Eff.Clock.currentTimeMillis), + events: [], + error: Eff.Option.none(), + }) }) const llmRequest = (events: Event[]) => @@ -153,9 +158,10 @@ export const ConversationActor = Eff.Effect.succeed( Idle: { Ask: (state, action, dispatch) => Eff.Effect.gen(function* () { + const timestamp = new Date(yield* Eff.Clock.currentTimeMillis) const event = Event.Message({ id: yield* genId, - timestamp: new Date(yield* Eff.Clock.currentTimeMillis), + timestamp, role: 'user', content: Content.text(action.content), }) @@ -167,10 +173,13 @@ export const ConversationActor = Eff.Effect.succeed( (r) => Eff.Effect.succeed(Action.LLMResponse(r)), (error) => Eff.Effect.succeed(Action.LLMError({ error })), ) - return State.WaitingForAssistant({ events, requestFiber }) + return State.WaitingForAssistant({ events, timestamp, requestFiber }) }), Clear: () => - Eff.Effect.succeed(State.Idle({ events: [], error: Eff.Option.none() })), + Eff.Effect.gen(function* () { + const timestamp = new Date(yield* Eff.Clock.currentTimeMillis) + return State.Idle({ events: [], timestamp, error: Eff.Option.none() }) + }), Discard: (state, { id }) => Eff.Effect.succeed({ ...state, @@ -184,6 +193,7 @@ export const ConversationActor = Eff.Effect.succeed( Eff.Effect.gen(function* () { return State.Idle({ events: state.events, + timestamp: new Date(yield* Eff.Clock.currentTimeMillis), error: Eff.Option.some({ message: 'Error while interacting with LLM. Please try again.', details: `${error}`, @@ -210,7 +220,8 @@ export const ConversationActor = Eff.Effect.succeed( ) } - if (!toolUses.length) return State.Idle({ events, error: Eff.Option.none() }) + if (!toolUses.length) + return State.Idle({ events, timestamp, error: Eff.Option.none() }) const ctxService = yield* Context.ConversationContext const { tools } = yield* ctxService.context @@ -229,7 +240,7 @@ export const ConversationActor = Eff.Effect.succeed( } } - return State.ToolUse({ events, calls }) + return State.ToolUse({ events, timestamp: state.timestamp, calls }) }), Abort: (state) => Eff.Effect.gen(function* () { @@ -259,7 +270,8 @@ export const ConversationActor = Eff.Effect.succeed( events = events.concat(event) } - if (Object.keys(calls).length) return State.ToolUse({ events, calls }) + if (Object.keys(calls).length) + return State.ToolUse({ events, timestamp: state.timestamp, calls }) // all calls completed, send results back to LLM const requestFiber = yield* Actor.forkRequest( @@ -268,7 +280,12 @@ export const ConversationActor = Eff.Effect.succeed( (r) => Eff.Effect.succeed(Action.LLMResponse(r)), (error) => Eff.Effect.succeed(Action.LLMError({ error })), ) - return State.WaitingForAssistant({ events, requestFiber }) + return State.WaitingForAssistant({ + events, + + timestamp: new Date(yield* Eff.Clock.currentTimeMillis), + requestFiber, + }) }), Abort: (state) => Eff.Effect.gen(function* () { diff --git a/catalog/app/components/Assistant/UI/Chat/Chat.tsx b/catalog/app/components/Assistant/UI/Chat/Chat.tsx index 93570eeaf13..adf8f123436 100644 --- a/catalog/app/components/Assistant/UI/Chat/Chat.tsx +++ b/catalog/app/components/Assistant/UI/Chat/Chat.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import * as M from '@material-ui/core' // import * as Lab from '@material-ui/lab' +import JsonDisplay from 'components/JsonDisplay' import Markdown from 'components/Markdown' import * as Model from '../../Model' @@ -53,6 +54,7 @@ const useMessageContainerStyles = M.makeStyles((t) => ({ contents: { ...t.typography.body2, color: t.palette.text.primary, + maxWidth: 'calc(50vw - 112px)', padding: `${t.spacing(1.5)}px`, }, footer: { @@ -131,19 +133,26 @@ function MessageAction({ children, onClick }: MessageActionProps) { type ConversationDispatch = (action: Model.Conversation.Action) => void -interface MessageSharedProps { +interface ConversationDispatchProps { dispatch: ConversationDispatch +} + +interface ConversationStateProps { state: Model.Conversation.State['_tag'] } -function Message({ +type MessageEventProps = ConversationDispatchProps & + ConversationStateProps & + ReturnType + +function MessageEvent({ state, id, timestamp, dispatch, role, content, -}: MessageSharedProps & ReturnType) { +}: MessageEventProps) { const discard = React.useMemo( () => state === 'Idle' ? () => dispatch(Model.Conversation.Action.Discard({ id })) : null, @@ -152,7 +161,7 @@ function Message({ return ( {discard && discard}} + actions={discard && discard} timestamp={timestamp} > {Model.Content.MessageContentBlock.$match(content, { @@ -164,44 +173,79 @@ function Message({ ) } -type ToolUseEvent = ReturnType -interface ToolUseProps - extends MessageSharedProps, - Omit { - result?: ToolUseEvent['result'] +type ToolUseEventProps = ConversationDispatchProps & + ConversationStateProps & + ReturnType + +function ToolUseEvent({ + state, + id, + timestamp, + toolUseId, + name, + input, + result, + dispatch, +}: ToolUseEventProps) { + const discard = React.useMemo( + () => + state === 'Idle' ? () => dispatch(Model.Conversation.Action.Discard({ id })) : null, + [dispatch, id, state], + ) + const details = React.useMemo( + () => ({ toolUseId, input, result }), + [toolUseId, input, result], + ) + // FIXME: JsonDisplay expansion doesn't work + return ( + discard} + > + + Tool Use: {name} ({result.status}) + + + {/* @ts-expect-error */} + + + + ) } -function ToolUse({ - // id, - // timestamp, +interface ToolUseStateProps + extends ConversationDispatchProps, + Model.Conversation.ToolCall { + toolUseId: string + timestamp: Date +} + +function ToolUseState({ + timestamp, toolUseId, name, input, - result, // dispatch, -}: ToolUseProps) { - const details = ( - <> - Tool Use ID: {toolUseId} -
- Tool Name: {name} -
- Input: -
{JSON.stringify(input, null, 2)}
- {!!result && ( - <> - Result: -
{JSON.stringify(result, null, 2)}
- - )} - + dispatch, +}: ToolUseStateProps) { + const abort = React.useCallback( + () => dispatch(Model.Conversation.Action.Abort()), + [dispatch], ) + const details = React.useMemo(() => ({ toolUseId, input }), [toolUseId, input]) return ( - - - - Tool Use: {name} ({result?.status ?? 'in progress'}) - - + abort} + > + + Tool Use: {name} (in progress) + + + {/* @ts-expect-error */} + + ) } @@ -319,7 +363,7 @@ export default function Chat({ state, dispatch }: ChatProps) { .map( Model.Conversation.Event.$match({ Message: (event) => ( - ), ToolUse: (event) => ( - Eff.Option.match(s.error, { onSome: (e) => ( - + Error occurred: {e.message} {e.details} @@ -349,14 +393,17 @@ export default function Chat({ state, dispatch }: ChatProps) { ), onNone: () => null, }), - WaitingForAssistant: () => ( - Processing... + WaitingForAssistant: (s) => ( + // TODO: abort + + Processing... + ), - ToolUse: ({ calls }) => + ToolUse: ({ calls, timestamp }) => Object.entries(calls).map(([id, call]) => ( -