From 4494dc25cbd00c61225f8fe5c75087310d0b7937 Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:21:44 -0700 Subject: [PATCH] AI Port Context (#170) This brings over the AI context from the previous app. In particular, it makes it so each block has its own context that persists after the app is reloaded. Note that this does not provide the app with the cli-specific context from the previous app. --- frontend/app/view/waveai.tsx | 66 ++++++++++++++++++++++------------- pkg/waveai/waveai.go | 16 ++++++--- pkg/wconfig/settingsconfig.go | 9 ++++- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/frontend/app/view/waveai.tsx b/frontend/app/view/waveai.tsx index e22ff372b..d08589c58 100644 --- a/frontend/app/view/waveai.tsx +++ b/frontend/app/view/waveai.tsx @@ -3,7 +3,7 @@ import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; -import { WOS, atoms } from "@/store/global"; +import { WOS, atoms, globalStore } from "@/store/global"; import { WshServer } from "@/store/wshserver"; import * as jotai from "jotai"; import type { OverlayScrollbars } from "overlayscrollbars"; @@ -25,19 +25,20 @@ interface ChatMessageType { const outline = "2px solid var(--accent-color)"; -const defaultMessage: ChatMessageType = { - id: uuidv4(), - user: "assistant", - text: `

Hello, how may I help you with this command?
-(Cmd-Shift-Space: open/close, Ctrl+L: clear chat buffer, Up/Down: select code blocks, Enter: to copy a selected code block to the command input)

`, - isAssistant: true, -}; - interface ChatItemProps { chatItem: ChatMessageType; itemCount: number; } +function promptToMsg(prompt: OpenAIPromptMessageType): ChatMessageType { + return { + id: uuidv4(), + user: prompt.role, + text: prompt.content, + isAssistant: prompt.role == "assistant", + }; +} + export class WaveAiModel implements ViewModel { blockId: string; blockAtom: jotai.Atom; @@ -49,7 +50,7 @@ export class WaveAiModel implements ViewModel { messagesAtom: jotai.PrimitiveAtom>; addMessageAtom: jotai.WritableAtom; updateLastMessageAtom: jotai.WritableAtom; - simulateAssistantResponseAtom: jotai.WritableAtom; + simulateAssistantResponseAtom: jotai.WritableAtom>; constructor(blockId: string) { this.blockId = blockId; @@ -57,8 +58,12 @@ export class WaveAiModel implements ViewModel { this.viewIcon = jotai.atom((get) => { return "sparkles"; // should not be hardcoded }); - this.viewName = jotai.atom("Ai"); - this.messagesAtom = jotai.atom([defaultMessage]); + this.viewName = jotai.atom("Wave Ai"); + this.messagesAtom = jotai.atom( + globalStore + .get(this.blockAtom) + .meta?.history?.map((prompt: OpenAIPromptMessageType) => promptToMsg(prompt)) ?? [] + ); this.addMessageAtom = jotai.atom(null, (get, set, message: ChatMessageType) => { const messages = get(this.messagesAtom); @@ -73,7 +78,7 @@ export class WaveAiModel implements ViewModel { set(this.messagesAtom, [...messages.slice(0, -1), updatedMessage]); } }); - this.simulateAssistantResponseAtom = jotai.atom(null, (get, set, userMessage: ChatMessageType) => { + this.simulateAssistantResponseAtom = jotai.atom(null, async (get, set, userMessage: ChatMessageType) => { const typingMessage: ChatMessageType = { id: uuidv4(), user: "assistant", @@ -106,8 +111,10 @@ export class WaveAiModel implements ViewModel { const [messages] = jotai.useAtom(this.messagesAtom); const [, addMessage] = jotai.useAtom(this.addMessageAtom); const [, simulateResponse] = jotai.useAtom(this.simulateAssistantResponseAtom); - const metadata = jotai.useAtomValue(this.blockAtom).meta; + const block = jotai.useAtomValue(this.blockAtom); + const metadata = block.meta; const clientId = jotai.useAtomValue(atoms.clientId); + const blockId = this.blockId; const sendMessage = (text: string, user: string = "user") => { const newMessage: ChatMessageType = { @@ -119,23 +126,23 @@ export class WaveAiModel implements ViewModel { addMessage(newMessage); // send message to backend and get response const opts: OpenAIOptsType = { - model: "gpt-3.5-turbo", + model: "gpt-4o-mini", apitoken: metadata?.apitoken as string, maxtokens: 1000, timeout: 10, baseurl: metadata?.baseurl as string, }; - const prompt: Array = [ - { - role: "user", - content: text, - name: (metadata?.name as string) || "user", - }, - ]; + const newPrompt: OpenAIPromptMessageType = { + role: "user", + content: text, + name: (metadata?.name as string) || "user", + }; + const updatedHistory: Array = metadata?.history || []; + updatedHistory.push(newPrompt); const beMsg: OpenAiStreamRequest = { clientid: clientId, opts: opts, - prompt: prompt, + prompt: updatedHistory, }; const aiGen = WshServer.StreamWaveAiCommand(beMsg); let temp = async () => { @@ -149,7 +156,18 @@ export class WaveAiModel implements ViewModel { text: fullMsg, isAssistant: true, }; - simulateResponse(response); + + const responsePrompt: OpenAIPromptMessageType = { + role: "assistant", + content: fullMsg, + }; + updatedHistory.push(responsePrompt); + const writeToHistory = WshServer.SetMetaCommand({ + oref: WOS.makeORef("block", blockId), + meta: { ...metadata, history: updatedHistory }, + }); + const typeResponse = simulateResponse(response); + Promise.all([writeToHistory, typeResponse]); }; temp(); }; diff --git a/pkg/waveai/waveai.go b/pkg/waveai/waveai.go index 1a49e0c50..3800b9c8c 100644 --- a/pkg/waveai/waveai.go +++ b/pkg/waveai/waveai.go @@ -51,6 +51,16 @@ type OpenAICloudReqPacketType struct { MaxChoices int `json:"maxchoices,omitempty"` } +type OpenAIOptsType struct { + Model string `json:"model"` + APIToken string `json:"apitoken"` + BaseURL string `json:"baseurl,omitempty"` + MaxTokens int `json:"maxtokens,omitempty"` + MaxChoices int `json:"maxchoices,omitempty"` + Timeout int `json:"timeout,omitempty"` + BlockId string `json:"blockid"` +} + func MakeOpenAICloudReqPacket() *OpenAICloudReqPacketType { return &OpenAICloudReqPacketType{ Type: OpenAICloudReqStr, @@ -71,7 +81,7 @@ func GetWSEndpoint() string { } const DefaultMaxTokens = 1000 -const DefaultModel = "gpt-3.5-turbo" +const DefaultModel = "gpt-4o-mini" const DefaultStreamChanSize = 10 const PCloudWSEndpoint = "wss://wsapi.waveterm.dev/" const PCloudWSEndpointVarName = "PCLOUD_WS_ENDPOINT" @@ -101,7 +111,6 @@ func ConvertPrompt(prompt []wshrpc.OpenAIPromptMessageType) []openaiapi.ChatComp func RunCloudCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] { rtn := make(chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]) go func() { - log.Printf("start: %v", request) defer close(rtn) if request.Opts == nil { rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("no openai opts found")} @@ -139,7 +148,6 @@ func RunCloudCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRe return } for { - log.Printf("loop") _, socketMessage, err := conn.ReadMessage() if err == io.EOF { break @@ -173,7 +181,6 @@ func RunCloudCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRe func RunLocalCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] { rtn := make(chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]) go func() { - log.Printf("start2: %v", request) defer close(rtn) if request.Opts == nil { rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("no openai opts found")} @@ -208,7 +215,6 @@ func RunLocalCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRe } sentHeader := false for { - log.Printf("loop2") streamResp, err := apiResp.Recv() if err == io.EOF { break diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 5d7c4c9d3..343158504 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -8,12 +8,19 @@ import ( "path/filepath" "github.com/wavetermdev/thenextwave/pkg/wavebase" + "github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wstore" ) const termThemesDir = "terminal-themes" const settingsFile = "settings.json" +var defaultAiMessage = wshrpc.OpenAIPromptMessageType{ + Role: "assistant", + Content: `

Hello, how may I help you?
+(Cmd-Shift-Space: open/close, Ctrl+L: clear chat buffer, Up/Down: select code blocks, Enter: to copy a selected code block to the command input)

`, +} + var settingsAbsPath = filepath.Join(configDirAbsPath, settingsFile) type WidgetsConfigType struct { @@ -187,7 +194,7 @@ func applyDefaultSettings(settings *SettingsConfigType) { Label: "waveai", BlockDef: wstore.BlockDef{ View: "waveai", - Meta: map[string]any{"name": userName, "baseurl": "", "apitoken": ""}, + Meta: map[string]any{"name": userName, "baseurl": "", "apitoken": "", "history": []wshrpc.OpenAIPromptMessageType{defaultAiMessage}}, }, }, }