Skip to content

Commit 045405a

Browse files
authored
new wsh ai command (#1257)
1 parent 2c055b5 commit 045405a

7 files changed

Lines changed: 350 additions & 90 deletions

File tree

cmd/wsh/cmd/wshcmd-ai.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2024, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"io"
9+
"os"
10+
"strings"
11+
12+
"github.com/spf13/cobra"
13+
"github.com/wavetermdev/waveterm/pkg/waveobj"
14+
"github.com/wavetermdev/waveterm/pkg/wshrpc"
15+
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
16+
"github.com/wavetermdev/waveterm/pkg/wshutil"
17+
)
18+
19+
var aiCmd = &cobra.Command{
20+
Use: "ai [-] [message...]",
21+
Short: "Send a message to an AI block",
22+
Args: cobra.MinimumNArgs(1),
23+
Run: aiRun,
24+
PreRunE: preRunSetupRpcClient,
25+
DisableFlagsInUseLine: true,
26+
}
27+
28+
var aiFileFlags []string
29+
30+
func init() {
31+
rootCmd.AddCommand(aiCmd)
32+
aiCmd.Flags().StringArrayVarP(&aiFileFlags, "file", "f", nil, "attach file content (use '-' for stdin)")
33+
}
34+
35+
func aiRun(cmd *cobra.Command, args []string) {
36+
// Then in aiRun, we'd need logic like:
37+
var stdinUsed bool
38+
var message strings.Builder
39+
40+
// Handle file attachments first
41+
for _, file := range aiFileFlags {
42+
if file == "-" {
43+
if stdinUsed {
44+
WriteStderr("[error] stdin (-) can only be used once\n")
45+
return
46+
}
47+
stdinUsed = true
48+
data, err := io.ReadAll(os.Stdin)
49+
if err != nil {
50+
WriteStderr("[error] reading from stdin: %v\n", err)
51+
return
52+
}
53+
message.WriteString("Content from stdin:\n")
54+
message.Write(data)
55+
} else {
56+
data, err := os.ReadFile(file)
57+
if err != nil {
58+
WriteStderr("[error] reading file %s: %v\n", file, err)
59+
return
60+
}
61+
message.WriteString(fmt.Sprintf("Content of %s:\n", file))
62+
message.Write(data)
63+
}
64+
message.WriteString("\n\n---------\n\n")
65+
}
66+
67+
// Default to "waveai" block
68+
isDefaultBlock := blockArg == "" || blockArg == "this"
69+
if isDefaultBlock {
70+
blockArg = "view@waveai"
71+
}
72+
73+
fullORef, err := resolveSimpleId(blockArg)
74+
if err != nil && isDefaultBlock {
75+
// Create new AI block if default block doesn't exist
76+
data := &wshrpc.CommandCreateBlockData{
77+
BlockDef: &waveobj.BlockDef{
78+
Meta: map[string]interface{}{
79+
waveobj.MetaKey_View: "waveai",
80+
},
81+
},
82+
}
83+
84+
newORef, err := wshclient.CreateBlockCommand(RpcClient, *data, &wshrpc.RpcOpts{Timeout: 2000})
85+
if err != nil {
86+
WriteStderr("[error] creating AI block: %v\n", err)
87+
return
88+
}
89+
fullORef = &newORef
90+
// Wait for the block's route to be available
91+
gotRoute, err := wshclient.WaitForRouteCommand(RpcClient, wshrpc.CommandWaitForRouteData{
92+
RouteId: wshutil.MakeFeBlockRouteId(fullORef.OID),
93+
WaitMs: 4000,
94+
}, &wshrpc.RpcOpts{Timeout: 5000})
95+
if err != nil {
96+
WriteStderr("[error] waiting for AI block: %v\n", err)
97+
return
98+
}
99+
if !gotRoute {
100+
WriteStderr("[error] AI block route could not be established\n")
101+
return
102+
}
103+
} else if err != nil {
104+
WriteStderr("[error] resolving block: %v\n", err)
105+
return
106+
}
107+
108+
// Create the route for this block
109+
route := wshutil.MakeFeBlockRouteId(fullORef.OID)
110+
111+
// Then handle main message
112+
if args[0] == "-" {
113+
if stdinUsed {
114+
WriteStderr("[error] stdin (-) can only be used once\n")
115+
return
116+
}
117+
data, err := io.ReadAll(os.Stdin)
118+
if err != nil {
119+
WriteStderr("[error] reading from stdin: %v\n", err)
120+
return
121+
}
122+
message.Write(data)
123+
} else {
124+
message.WriteString(strings.Join(args, " "))
125+
}
126+
127+
if message.Len() == 0 {
128+
WriteStderr("[error] message is empty\n")
129+
return
130+
}
131+
if message.Len() > 10*1024 {
132+
WriteStderr("[error] current max message size is 10k\n")
133+
return
134+
}
135+
136+
messageData := wshrpc.AiMessageData{
137+
Message: message.String(),
138+
}
139+
err = wshclient.AiSendMessageCommand(RpcClient, messageData, &wshrpc.RpcOpts{
140+
Route: route,
141+
Timeout: 2000,
142+
})
143+
if err != nil {
144+
WriteStderr("[error] sending message: %v\n", err)
145+
return
146+
}
147+
}

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { WshClient } from "./wshclient";
77

88
// WshServerCommandToDeclMap
99
class RpcApiType {
10+
// command "aisendmessage" [call]
11+
AiSendMessageCommand(client: WshClient, data: AiMessageData, opts?: RpcOpts): Promise<void> {
12+
return client.wshRpcCall("aisendmessage", data, opts);
13+
}
14+
1015
// command "authenticate" [call]
1116
AuthenticateCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<CommandAuthenticateRtnData> {
1217
return client.wshRpcCall("authenticate", data, opts);

0 commit comments

Comments
 (0)