From 923850313fd5bd62756cfde9d26119be42451d81 Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:35:21 -0700 Subject: [PATCH] AI Fixes (#173) Fixing a few AI-related bugs --- Taskfile.yml | 1 + frontend/app/store/services.ts | 3 + frontend/app/view/waveai.tsx | 77 +++++++++++++++--------- frontend/types/gotypes.d.ts | 11 ++++ pkg/service/blockservice/blockservice.go | 23 +++++++ pkg/wconfig/settingsconfig.go | 26 +++++--- 6 files changed, 106 insertions(+), 35 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 0b7b2b9a..f5e895f7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -145,6 +145,7 @@ tasks: - "cmd/generate/*.go" - "cmd/generatewshclient/*.go" - "pkg/service/**/*.go" + - "pkg/wconfig/**/*.go" - "pkg/wstore/*.go" - "pkg/wshrpc/**/*.go" - "pkg/tsgen/**/*.go" diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 24434bb7..8e2fcf77 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -13,6 +13,9 @@ class BlockServiceType { SaveTerminalState(arg2: string, arg3: string, arg4: string, arg5: number): Promise { return WOS.callBackendService("block", "SaveTerminalState", Array.from(arguments)) } + SaveWaveAiData(arg2: string, arg3: OpenAIPromptMessageType[]): Promise { + return WOS.callBackendService("block", "SaveWaveAiData", Array.from(arguments)) + } } export const BlockService = new BlockServiceType(); diff --git a/frontend/app/view/waveai.tsx b/frontend/app/view/waveai.tsx index d08589c5..a520ded4 100644 --- a/frontend/app/view/waveai.tsx +++ b/frontend/app/view/waveai.tsx @@ -3,7 +3,8 @@ import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; -import { WOS, atoms, globalStore } from "@/store/global"; +import { WOS, atoms, fetchWaveFile, globalStore } from "@/store/global"; +import * as services from "@/store/services"; import { WshServer } from "@/store/wshserver"; import * as jotai from "jotai"; import type { OverlayScrollbars } from "overlayscrollbars"; @@ -59,11 +60,7 @@ export class WaveAiModel implements ViewModel { return "sparkles"; // should not be hardcoded }); this.viewName = jotai.atom("Wave Ai"); - this.messagesAtom = jotai.atom( - globalStore - .get(this.blockAtom) - .meta?.history?.map((prompt: OpenAIPromptMessageType) => promptToMsg(prompt)) ?? [] - ); + this.messagesAtom = jotai.atom([]); this.addMessageAtom = jotai.atom(null, (get, set, message: ChatMessageType) => { const messages = get(this.messagesAtom); @@ -105,14 +102,35 @@ export class WaveAiModel implements ViewModel { }, 100); }, 1500); }); + this.viewText = jotai.atom((get) => { + const viewTextChildren: HeaderElem[] = [ + { + elemtype: "text", + text: get(atoms.settingsConfigAtom).ai.model, + }, + ]; + return viewTextChildren; + }); + } + + async populateMessages(): Promise { + const history = await this.fetchAiData(); + globalStore.set(this.messagesAtom, history.map(promptToMsg)); + } + + async fetchAiData(): Promise> { + const { data, fileInfo } = await fetchWaveFile(this.blockId, "aidata"); + if (!data) { + return []; + } + const history: Array = JSON.parse(new TextDecoder().decode(data)); + return history; } useWaveAi() { const [messages] = jotai.useAtom(this.messagesAtom); const [, addMessage] = jotai.useAtom(this.addMessageAtom); const [, simulateResponse] = jotai.useAtom(this.simulateAssistantResponseAtom); - const block = jotai.useAtomValue(this.blockAtom); - const metadata = block.meta; const clientId = jotai.useAtomValue(atoms.clientId); const blockId = this.blockId; @@ -125,27 +143,27 @@ export class WaveAiModel implements ViewModel { }; addMessage(newMessage); // send message to backend and get response + const settings = globalStore.get(atoms.settingsConfigAtom); const opts: OpenAIOptsType = { - model: "gpt-4o-mini", - apitoken: metadata?.apitoken as string, - maxtokens: 1000, - timeout: 10, - baseurl: metadata?.baseurl as string, + model: settings.ai.model, + apitoken: settings.ai.apitoken, + maxtokens: settings.ai.maxtokens, + timeout: settings.ai.timeoutms / 1000, + baseurl: settings.ai.baseurl, }; const newPrompt: OpenAIPromptMessageType = { role: "user", content: text, - name: (metadata?.name as string) || "user", + name: globalStore.get(atoms.settingsConfigAtom).ai.name, }; - const updatedHistory: Array = metadata?.history || []; - updatedHistory.push(newPrompt); - const beMsg: OpenAiStreamRequest = { - clientid: clientId, - opts: opts, - prompt: updatedHistory, - }; - const aiGen = WshServer.StreamWaveAiCommand(beMsg); let temp = async () => { + const history = await this.fetchAiData(); + const beMsg: OpenAiStreamRequest = { + clientid: clientId, + opts: opts, + prompt: [...history, newPrompt], + }; + const aiGen = WshServer.StreamWaveAiCommand(beMsg); let fullMsg = ""; for await (const msg of aiGen) { fullMsg += msg.text ?? ""; @@ -161,11 +179,11 @@ export class WaveAiModel implements ViewModel { role: "assistant", content: fullMsg, }; - updatedHistory.push(responsePrompt); - const writeToHistory = WshServer.SetMetaCommand({ - oref: WOS.makeORef("block", blockId), - meta: { ...metadata, history: updatedHistory }, - }); + const writeToHistory = services.BlockService.SaveWaveAiData(blockId, [ + ...history, + newPrompt, + responsePrompt, + ]); const typeResponse = simulateResponse(response); Promise.all([writeToHistory, typeResponse]); }; @@ -388,6 +406,11 @@ const WaveAi = ({ model }: { model: WaveAiModel }) => { const termFontSize: number = 14; + // a weird workaround to initialize ansynchronously + useEffect(() => { + model.populateMessages(); + }, []); + useEffect(() => { return () => { if (submitTimeoutRef.current) { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 6abd40fd..ed140114 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -5,6 +5,16 @@ declare global { + // wconfig.AiConfigType + type AiConfigType = { + baseurl: string; + apitoken: string; + name: string; + model: string; + maxtokens: number; + timeoutms: number; + }; + // wconfig.AutoUpdateOpts type AutoUpdateOpts = { enabled: boolean; @@ -266,6 +276,7 @@ declare global { type SettingsConfigType = { mimetypes: {[key: string]: MimeTypeConfigType}; term: TerminalConfigType; + ai: AiConfigType; widgets: WidgetsConfigType[]; blockheader: BlockHeaderOpts; autoupdate: AutoUpdateOpts; diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index ec4c2f70..bd3f5506 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -5,12 +5,14 @@ package blockservice import ( "context" + "encoding/json" "fmt" "time" "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/filestore" "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" + "github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -58,3 +60,24 @@ func (bs *BlockService) SaveTerminalState(ctx context.Context, blockId string, s } return nil } + +func (bs *BlockService) SaveWaveAiData(ctx context.Context, blockId string, history []wshrpc.OpenAIPromptMessageType) error { + block, err := wstore.DBMustGet[*wstore.Block](ctx, blockId) + if err != nil { + return err + } + if block.View != "waveai" { + return fmt.Errorf("invalid view type: %s", block.View) + } + historyBytes, err := json.Marshal(history) + if err != nil { + return fmt.Errorf("unable to serialize ai history: %v", err) + } + // ignore MakeFile error (already exists is ok) + filestore.WFS.MakeFile(ctx, blockId, "aidata", nil, filestore.FileOptsType{}) + err = filestore.WFS.WriteFile(ctx, blockId, "aidata", historyBytes) + if err != nil { + return fmt.Errorf("cannot save terminal state: %w", err) + } + return nil +} diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 34315850..9c7d3fb1 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -8,19 +8,12 @@ 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 { @@ -36,6 +29,15 @@ type TerminalConfigType struct { FontFamily string `json:"fontfamily,omitempty"` } +type AiConfigType struct { + BaseURL string `json:"baseurl"` + ApiToken string `json:"apitoken"` + Name string `json:"name"` + Model string `json:"model"` + MaxTokens uint32 `json:"maxtokens"` + TimeoutMs uint32 `json:"timeoutms"` +} + type MimeTypeConfigType struct { Icon string `json:"icon"` Color string `json:"color"` @@ -90,6 +92,7 @@ type WindowSettingsType struct { type SettingsConfigType struct { MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` Term TerminalConfigType `json:"term"` + Ai *AiConfigType `json:"ai"` Widgets []WidgetsConfigType `json:"widgets"` BlockHeader BlockHeaderOpts `json:"blockheader"` AutoUpdate *AutoUpdateOpts `json:"autoupdate"` @@ -165,6 +168,14 @@ func applyDefaultSettings(settings *SettingsConfigType) { } else { userName = currentUser.Username } + if settings.Ai == nil { + settings.Ai = &AiConfigType{ + Name: userName, + Model: "gpt-4o-mini", + MaxTokens: 1000, + TimeoutMs: 10 * 1000, + } + } defaultWidgets := []WidgetsConfigType{ { Icon: "files", @@ -194,7 +205,6 @@ func applyDefaultSettings(settings *SettingsConfigType) { Label: "waveai", BlockDef: wstore.BlockDef{ View: "waveai", - Meta: map[string]any{"name": userName, "baseurl": "", "apitoken": "", "history": []wshrpc.OpenAIPromptMessageType{defaultAiMessage}}, }, }, }