Skip to content

Commit fb10a06

Browse files
kylecarbsjohnstcn
authored andcommitted
Initial chat schema
1 parent 50695b7 commit fb10a06

40 files changed

+1920
-3
lines changed

cli/server.go

+48
Original file line numberDiff line numberDiff line change
@@ -2621,6 +2621,54 @@ func redirectHTTPToHTTPSDeprecation(ctx context.Context, logger slog.Logger, inv
26212621
}
26222622
}
26232623

2624+
func ReadAIProvidersFromEnv(environ []string) ([]codersdk.AIProviderConfig, error) {
2625+
// The index numbers must be in-order.
2626+
sort.Strings(environ)
2627+
2628+
var providers []codersdk.AIProviderConfig
2629+
for _, v := range serpent.ParseEnviron(environ, "CODER_AI_PROVIDER_") {
2630+
tokens := strings.SplitN(v.Name, "_", 2)
2631+
if len(tokens) != 2 {
2632+
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
2633+
}
2634+
2635+
providerNum, err := strconv.Atoi(tokens[0])
2636+
if err != nil {
2637+
return nil, xerrors.Errorf("parse number: %s", v.Name)
2638+
}
2639+
2640+
var provider codersdk.AIProviderConfig
2641+
switch {
2642+
case len(providers) < providerNum:
2643+
return nil, xerrors.Errorf(
2644+
"provider num %v skipped: %s",
2645+
len(providers),
2646+
v.Name,
2647+
)
2648+
case len(providers) == providerNum:
2649+
// At the next next provider.
2650+
providers = append(providers, provider)
2651+
case len(providers) == providerNum+1:
2652+
// At the current provider.
2653+
provider = providers[providerNum]
2654+
}
2655+
2656+
key := tokens[1]
2657+
switch key {
2658+
case "TYPE":
2659+
provider.Type = v.Value
2660+
case "API_KEY":
2661+
provider.APIKey = v.Value
2662+
case "BASE_URL":
2663+
provider.BaseURL = v.Value
2664+
case "MODELS":
2665+
provider.Models = strings.Split(v.Value, " ")
2666+
}
2667+
providers[providerNum] = provider
2668+
}
2669+
return providers, nil
2670+
}
2671+
26242672
// ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with
26252673
// the viper CLI.
26262674
func ReadExternalAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) {

cli/testdata/TestProvisioners_Golden/list.golden

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
1+
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
22
00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== default-provisioner v0.0.0-devel map[owner: scope:organization] built-in idle <nil> <nil> 00000000-0000-0000-bbbb-000000000001 succeeded Coder
33
00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 map[foo:bar owner: scope:organization] built-in busy 00000000-0000-0000-bbbb-000000000002 running <nil> <nil> Coder
44
00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 map[owner: scope:organization] built-in offline <nil> <nil> 00000000-0000-0000-bbbb-000000000003 succeeded Coder
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
1+
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
22
====[timestamp]===== ====[timestamp]===== built-in test v0.0.0-devel idle map[owner: scope:organization]

cli/testdata/server-config.yaml.golden

+4
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,10 @@ client:
519519
# Support links to display in the top right drop down menu.
520520
# (default: <unset>, type: struct[[]codersdk.LinkConfig])
521521
supportLinks: []
522+
# Configure AI providers.
523+
# (default: <unset>, type: struct[codersdk.AIConfig])
524+
ai:
525+
providers: []
522526
# External Authentication providers.
523527
# (default: <unset>, type: struct[[]codersdk.ExternalAuthConfig])
524528
externalAuthProviders: []

coderd/ai/ai.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package ai
2+
3+
import (
4+
"context"
5+
6+
"github.com/kylecarbs/aisdk-go"
7+
)
8+
9+
type Provider func(ctx context.Context, messages []aisdk.Message) (aisdk.DataStream, error)

coderd/apidoc/docs.go

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/chat.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package coderd
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"time"
7+
8+
"github.com/coder/coder/v2/coderd/database"
9+
"github.com/coder/coder/v2/coderd/database/db2sdk"
10+
"github.com/coder/coder/v2/coderd/httpapi"
11+
"github.com/coder/coder/v2/coderd/httpmw"
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/google/uuid"
14+
"github.com/kylecarbs/aisdk-go"
15+
)
16+
17+
// postChats creates a new chat.
18+
//
19+
// @Summary Create a chat
20+
// @ID post-chat
21+
// @Security CoderSessionToken
22+
// @Produce json
23+
// @Tags Chat
24+
// @Success 201 {object} codersdk.Chat
25+
// @Router /chats [post]
26+
func (api *API) postChats(w http.ResponseWriter, r *http.Request) {
27+
apiKey := httpmw.APIKey(r)
28+
ctx := r.Context()
29+
30+
chat, err := api.Database.InsertChat(ctx, database.InsertChatParams{
31+
ID: uuid.New(),
32+
OwnerID: apiKey.UserID,
33+
CreatedAt: time.Now(),
34+
UpdatedAt: time.Now(),
35+
Title: "New Chat",
36+
})
37+
if err != nil {
38+
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
39+
Message: "Failed to create chat",
40+
Detail: err.Error(),
41+
})
42+
return
43+
}
44+
45+
httpapi.Write(ctx, w, http.StatusCreated, db2sdk.Chat(chat))
46+
}
47+
48+
// listChats lists all chats for a user.
49+
//
50+
// @Summary List chats
51+
// @ID list-chats
52+
// @Security CoderSessionToken
53+
// @Produce json
54+
// @Tags Chat
55+
// @Success 200 {array} codersdk.Chat
56+
// @Router /chats [get]
57+
func (api *API) listChats(w http.ResponseWriter, r *http.Request) {
58+
apiKey := httpmw.APIKey(r)
59+
ctx := r.Context()
60+
61+
chats, err := api.Database.GetChatsByOwnerID(ctx, apiKey.UserID)
62+
if err != nil {
63+
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
64+
Message: "Failed to list chats",
65+
Detail: err.Error(),
66+
})
67+
}
68+
69+
httpapi.Write(ctx, w, http.StatusOK, db2sdk.Chats(chats))
70+
}
71+
72+
// chat returns a chat by ID.
73+
//
74+
// @Summary Get a chat
75+
// @ID get-chat
76+
// @Security CoderSessionToken
77+
// @Produce json
78+
// @Tags Chat
79+
// @Success 200 {object} codersdk.Chat
80+
// @Router /chats/{chat} [get]
81+
func (api *API) chat(w http.ResponseWriter, r *http.Request) {
82+
ctx := r.Context()
83+
chat := httpmw.ChatParam(r)
84+
httpapi.Write(ctx, w, http.StatusOK, db2sdk.Chat(chat))
85+
}
86+
87+
// chatMessages returns the messages of a chat.
88+
//
89+
// @Summary Get chat messages
90+
// @ID get-chat-messages
91+
// @Security CoderSessionToken
92+
// @Produce json
93+
// @Tags Chat
94+
// @Success 200 {array} aisdk.Message
95+
// @Router /chats/{chat}/messages [get]
96+
func (api *API) chatMessages(w http.ResponseWriter, r *http.Request) {
97+
ctx := r.Context()
98+
chat := httpmw.ChatParam(r)
99+
rawMessages, err := api.Database.GetChatMessagesByChatID(ctx, chat.ID)
100+
if err != nil {
101+
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
102+
Message: "Failed to get chat messages",
103+
Detail: err.Error(),
104+
})
105+
}
106+
messages := make([]aisdk.Message, len(rawMessages))
107+
for i, message := range rawMessages {
108+
var msg aisdk.Message
109+
err = json.Unmarshal(message.Content, &msg)
110+
if err != nil {
111+
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
112+
Message: "Failed to unmarshal chat message",
113+
Detail: err.Error(),
114+
})
115+
}
116+
messages[i] = msg
117+
}
118+
119+
httpapi.Write(ctx, w, http.StatusOK, messages)
120+
}
121+
122+
func (api *API) postChatMessage(w http.ResponseWriter, r *http.Request) {
123+
ctx := r.Context()
124+
chat := httpmw.ChatParam(r)
125+
var message aisdk.Message
126+
err := json.NewDecoder(r.Body).Decode(&message)
127+
if err != nil {
128+
httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{
129+
Message: "Failed to decode chat message",
130+
Detail: err.Error(),
131+
})
132+
}
133+
134+
var stream aisdk.DataStream
135+
stream.WithToolCalling(func(toolCall aisdk.ToolCall) any {
136+
return nil
137+
})
138+
}

coderd/coderd.go

+10
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,16 @@ func New(options *Options) *API {
998998
r.Get("/{fileID}", api.fileByID)
999999
r.Post("/", api.postFile)
10001000
})
1001+
r.Route("/chats", func(r chi.Router) {
1002+
r.Use(apiKeyMiddleware)
1003+
r.Get("/", api.listChats)
1004+
r.Post("/", api.postChats)
1005+
r.Route("/{chat}", func(r chi.Router) {
1006+
r.Use(httpmw.ExtractChatParam(options.Database))
1007+
r.Get("/", api.chat)
1008+
r.Get("/messages", api.chatMessages)
1009+
})
1010+
})
10011011
r.Route("/external-auth", func(r chi.Router) {
10021012
r.Use(
10031013
apiKeyMiddleware,

0 commit comments

Comments
 (0)