-
Notifications
You must be signed in to change notification settings - Fork 394
Expand file tree
/
Copy pathmain.go
More file actions
174 lines (160 loc) · 4.76 KB
/
Copy pathmain.go
File metadata and controls
174 lines (160 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// A very, very basic chat client for `docker agent serve chat`.
//
// PR #2510 (`feat: add docker agent serve chat command`) exposes any
// docker-agent agent through an OpenAI-compatible HTTP server. The whole
// point of that feature is that any tool already speaking OpenAI's
// /v1/chat/completions protocol can drive a docker-agent agent without
// custom integration. This example demonstrates exactly that: it uses the
// official github.com/openai/openai-go SDK, only repointed at the local
// chat server, to run an interactive REPL against an agent.
//
// Prerequisites:
//
// # Start an agent in chat mode (in another terminal):
// ./bin/docker-agent serve chat ./examples/42.yaml
// # It listens on http://127.0.0.1:8083 by default.
//
// Then run this client:
//
// go run ./examples/chat
// # or, to pin a specific agent in a multi-agent team:
// go run ./examples/chat -model root
// # or, to point at a different server:
// go run ./examples/chat -base http://127.0.0.1:9090/v1
//
// Type a message and press <Enter>. Type "exit" (or send EOF with ^D) to
// quit.
package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
)
func main() {
baseURL := flag.String("base", "http://127.0.0.1:8083/v1", "Base URL of the docker-agent chat server")
model := flag.String("model", "", "Agent name to talk to (defaults to the team's default agent)")
stream := flag.Bool("stream", true, "Stream the agent's response token-by-token")
flag.Parse()
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
err := run(ctx, *baseURL, *model, *stream)
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
log.Fatal(err)
}
}
func run(ctx context.Context, baseURL, model string, stream bool) error {
// The chat server doesn't validate API keys, but the OpenAI SDK
// requires *some* string to be passed.
client := openai.NewClient(
option.WithBaseURL(baseURL),
option.WithAPIKey("not-needed"),
)
// Ask the server which agents are exposed and pick a default model
// when the user didn't pin one. This also doubles as a connectivity
// check.
if model == "" {
picked, err := pickDefaultModel(ctx, &client)
if err != nil {
return fmt.Errorf("listing models: %w", err)
}
model = picked
}
fmt.Printf("Connected to %s — chatting with %q. Type \"exit\" to quit.\n", baseURL, model)
// History keeps the conversation going across turns. The chat server
// is stateless: it builds a fresh session per request from whatever
// messages the client sends, so it's the client's job to remember.
var history []openai.ChatCompletionMessageParamUnion
in := bufio.NewScanner(os.Stdin)
in.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for {
fmt.Print("\n> ")
if !in.Scan() {
if err := in.Err(); err != nil {
return err
}
fmt.Println()
return nil // EOF
}
userInput := strings.TrimSpace(in.Text())
if userInput == "" {
continue
}
if userInput == "exit" || userInput == "quit" {
return nil
}
history = append(history, openai.UserMessage(userInput))
reply, err := chat(ctx, &client, model, history, stream)
if err != nil {
return err
}
history = append(history, openai.AssistantMessage(reply))
}
}
// pickDefaultModel queries /v1/models and returns the first agent name
// the server advertises.
func pickDefaultModel(ctx context.Context, client *openai.Client) (string, error) {
page, err := client.Models.List(ctx)
if err != nil {
return "", err
}
if len(page.Data) == 0 {
return "", errors.New("server exposes no models")
}
return page.Data[0].ID, nil
}
// chat sends the conversation to the server, prints the assistant's reply
// to stdout (streaming if requested) and returns the final assembled
// content so the caller can append it to the history.
func chat(
ctx context.Context,
client *openai.Client,
model string,
history []openai.ChatCompletionMessageParamUnion,
stream bool,
) (string, error) {
params := openai.ChatCompletionNewParams{
Model: model,
Messages: history,
}
if !stream {
resp, err := client.Chat.Completions.New(ctx, params)
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", errors.New("server returned no choices")
}
content := resp.Choices[0].Message.Content
fmt.Println(content)
return content, nil
}
s := client.Chat.Completions.NewStreaming(ctx, params)
var b strings.Builder
for s.Next() {
chunk := s.Current()
if len(chunk.Choices) == 0 {
continue
}
delta := chunk.Choices[0].Delta.Content
if delta == "" {
continue
}
fmt.Print(delta)
b.WriteString(delta)
}
if err := s.Err(); err != nil && !errors.Is(err, io.EOF) {
return "", err
}
fmt.Println()
return b.String(), nil
}