blob: 1bc20c5fe9bf39fae1dc67491a131d74f7240008 [file] [log] [blame]
gio30503072025-06-17 10:50:15 +00001package dodo_tools
2
3import (
giofe6e7142025-06-18 08:51:23 +00004 "bytes"
Giorgi Lekveishvili05569832025-07-06 09:16:29 +04005 "context"
gio30503072025-06-17 10:50:15 +00006 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10
11 "sketch.dev/llm"
12)
13
14type GetProjectConfigTool struct {
giofe6e7142025-06-18 08:51:23 +000015 apiBaseAddress string
16 projectId string
gio30503072025-06-17 10:50:15 +000017}
18
giofe6e7142025-06-18 08:51:23 +000019func NewGetProjectConfigTool(apiBaseAddress string, projectId string) *llm.Tool {
20 tool := &GetProjectConfigTool{
21 apiBaseAddress: apiBaseAddress,
22 projectId: projectId,
23 }
gio30503072025-06-17 10:50:15 +000024 return &llm.Tool{
25 Name: "dodo_get_project_config",
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +040026 Description: "A tool for getting current state of the infrastructure configuration of a dodo project. You might want to use dodo_get_project_env tool to resolve ingress domains.",
giofe6e7142025-06-18 08:51:23 +000027 InputSchema: llm.EmptySchema(),
gio30503072025-06-17 10:50:15 +000028 Run: tool.Run,
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +040029 EndsTurn: false,
gio30503072025-06-17 10:50:15 +000030 }
31}
32
33type GetProjectConfigInput struct {
gio30503072025-06-17 10:50:15 +000034}
35
36type GetProjectConfigOutput struct {
37 Config string `json:"config"`
38}
39
40func (d *GetProjectConfigTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
giofe6e7142025-06-18 08:51:23 +000041 resp, err := http.Get(fmt.Sprintf("%s/api/project/%s/config", d.apiBaseAddress, d.projectId))
gio30503072025-06-17 10:50:15 +000042 if err != nil {
43 return nil, err
44 }
45 defer resp.Body.Close()
46 body, err := io.ReadAll(resp.Body)
47 if err != nil {
48 return nil, err
49 }
50 if resp.StatusCode != http.StatusOK {
51 return nil, fmt.Errorf("failed to get project config: %s", string(body))
52 }
gio30503072025-06-17 10:50:15 +000053 output := GetProjectConfigOutput{
54 Config: string(body),
55 }
56 jsonOutput, err := json.Marshal(output)
57 if err != nil {
58 return nil, err
59 }
60 return llm.TextContent(string(jsonOutput)), nil
61}
giofe6e7142025-06-18 08:51:23 +000062
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +040063// Env
64
65type GetProjectEnvTool struct {
66 apiBaseAddress string
67 projectId string
68}
69
70func NewGetProjectEnvTool(apiBaseAddress string, projectId string) *llm.Tool {
71 tool := &GetProjectEnvTool{
72 apiBaseAddress: apiBaseAddress,
73 projectId: projectId,
74 }
75 return &llm.Tool{
76 Name: "dodo_get_project_env",
77 Description: `A tool for getting GitHub PAT (Personal Access Token) and available networks of the dodo project. Use if you need to:
781. Interact with GitHub and do not have PAT cached.
792. Want to resolve network domains of dodo service ingresses.
803. Access application wide environment variables.
81
82Returns JSON object conforming following schema which you can cache:
83
84{
85 "type": "object",
86 "properties": {
87 "githubToken": {
88 "type": "string",
89 "description": "Github PAT (Personal Access Token)"
90 },
91 "networks": {
92 "type": "array",
93 "description": "List of available networks",
94 "items": {
95 "type": "object",
96 "required": ["name", "domain", "hasAuth"],
97 "properties": {
98 "name": {
99 "type": "string",
100 "description": "The name of the network"
101 },
102 "domain": {
103 "type": "string",
104 "description": "The domain of the network"
105 },
106 "hasAuth": {
107 "type": "boolean",
108 "description": "Represents if services published on this network can use built-in authorization"
109 }
110 }
111 }
112 },
113 "envVars": {
114 "type": "array",
115 "description": "List of application wide environment variables",
116 "items": {
117 "type": "object",
118 "required": ["name", "value"],
119 "properties": {
120 "name": {
121 "type": "string",
122 "description": "The name of the environment variable"
123 },
124 "value": {
125 "type": "string",
126 "description": "The value of the environment variable"
127 }
128 }
129 }
130 }
131 }
132}
133`,
134 InputSchema: llm.EmptySchema(),
135 Run: tool.Run,
136 EndsTurn: false,
137 }
138}
139
140type GetProjectEnvInput struct {
141}
142
143type Network struct {
144 Name string `json:"name"`
145 Domain string `json:"domain"`
146 HasAuth bool `json:"hasAuth"`
147}
148
149type GetProjectEnvOutput struct {
150 GithubToken string `json:"githubToken,omitempty"`
151 Networks []Network `json:"networks,omitempty"`
152}
153
154func (d *GetProjectEnvTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
155 resp, err := http.Get(fmt.Sprintf("%s/api/project/%s/env", d.apiBaseAddress, d.projectId))
156 if err != nil {
157 return nil, err
158 }
159 defer resp.Body.Close()
160 body, err := io.ReadAll(resp.Body)
161 if err != nil {
162 return nil, err
163 }
164 if resp.StatusCode != http.StatusOK {
165 return nil, fmt.Errorf("failed to get project env: %s", string(body))
166 }
167 var output GetProjectEnvOutput
168 if err := json.Unmarshal(body, &output); err != nil {
169 return nil, err
170 }
171 jsonOutput, err := json.Marshal(output)
172 if err != nil {
173 return nil, err
174 }
175 return llm.TextContent(string(jsonOutput)), nil
176}
177
178// Validate
179
giofe6e7142025-06-18 08:51:23 +0000180type ValidateConfigTool struct {
181 apiBaseAddress string
182}
183
184const (
185 validateConfigSchemaInputSchema = `
186{
187 "type": "object",
188 "properties": {
189 "config": {
190 "type": "string",
191 "description": "The dodo-app configuration to validate"
192 }
193 },
194 "required": ["config"]
195}
196`
197)
198
199func NewValidateConfigTool(apiBaseAddress string) *llm.Tool {
200 tool := &ValidateConfigTool{
201 apiBaseAddress: apiBaseAddress,
202 }
203 return &llm.Tool{
204 Name: "dodo_validate_config",
205 Description: "A tool for validating the dodo-app configuration",
206 InputSchema: llm.MustSchema(validateConfigSchemaInputSchema),
207 Run: tool.Run,
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400208 EndsTurn: false,
giofe6e7142025-06-18 08:51:23 +0000209 }
210}
211
212type ValidateConfigInput struct {
213 Config string `json:"config"`
214}
215
216type ValidateConfigOutput struct {
217 Success bool `json:"success"`
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400218 Errors any `json:"errors,omitempty"`
giofe6e7142025-06-18 08:51:23 +0000219}
220
221func (d *ValidateConfigTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
giofe6e7142025-06-18 08:51:23 +0000222 var input ValidateConfigInput
223 if err := json.Unmarshal(m, &input); err != nil {
224 return nil, err
225 }
226 resp, err := http.Post(fmt.Sprintf("%s/api/validate-config", d.apiBaseAddress), "application/json", bytes.NewBuffer([]byte(input.Config)))
227 if err != nil {
228 return nil, err
229 }
230 defer resp.Body.Close()
231 body, err := io.ReadAll(resp.Body)
232 if err != nil {
233 return nil, err
234 }
235 if resp.StatusCode != http.StatusOK {
236 return nil, fmt.Errorf("failed to validate config: %s", string(body))
237 }
238 var output ValidateConfigOutput
239 if err := json.Unmarshal(body, &output); err != nil {
240 return nil, err
241 }
242 jsonOutput, err := json.Marshal(output)
243 if err != nil {
244 return nil, err
245 }
246 return llm.TextContent(string(jsonOutput)), nil
247}
248
249type DeployProjectTool struct {
250 apiBaseAddress string
251 projectId string
252}
253
giofe6e7142025-06-18 08:51:23 +0000254func NewDeployProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
255 tool := &DeployProjectTool{
256 apiBaseAddress: apiBaseAddress,
257 projectId: projectId,
258 }
259
260 return &llm.Tool{
261 Name: "dodo_deploy_project",
262 Description: "A tool for deploying the dodo-app configuration",
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400263 InputSchema: llm.EmptySchema(),
giofe6e7142025-06-18 08:51:23 +0000264 Run: tool.Run,
265 EndsTurn: true,
266 }
267}
268
giofe6e7142025-06-18 08:51:23 +0000269type deployProjectReq struct {
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400270 Type string `json:"type"`
giofe6e7142025-06-18 08:51:23 +0000271}
272
273func (d *DeployProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400274 req := deployProjectReq{
275 Type: "draft",
giofe6e7142025-06-18 08:51:23 +0000276 }
giofe6e7142025-06-18 08:51:23 +0000277 jsonReq, err := json.Marshal(req)
278 if err != nil {
279 return nil, err
280 }
281 resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/deploy", d.apiBaseAddress, d.projectId), "application/json", bytes.NewBuffer(jsonReq))
282 if err != nil {
283 return nil, err
284 }
285 defer resp.Body.Close()
286 body, err := io.ReadAll(resp.Body)
287 if err != nil {
288 return nil, err
289 }
290 if resp.StatusCode != http.StatusOK {
291 return nil, fmt.Errorf("failed to deploy project: %s", string(body))
292 }
293 return llm.TextContent(string("Project deployed successfully")), nil
294}
295
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400296// Save
297
298type SaveProjectTool struct {
299 apiBaseAddress string
300 projectId string
301}
302
303const (
304 saveProjectSchemaInputSchema = `
305{
306 "type": "object",
307 "properties": {
Giorgi Lekveishvili49b7b0a2025-07-07 13:52:35 +0400308 "config": {
309 "type": "string",
310 "description": "Serialized dodo-app configuration to save"
311 }
312 },
313 "required": ["config"]
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400314}
315`
316)
317
318func NewSaveProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
319 tool := &SaveProjectTool{
320 apiBaseAddress: apiBaseAddress,
321 projectId: projectId,
322 }
323
324 return &llm.Tool{
325 Name: "dodo_save_config",
326 Description: "A tool for saving the dodo-app configuration",
327 InputSchema: llm.MustSchema(saveProjectSchemaInputSchema),
328 Run: tool.Run,
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400329 EndsTurn: false,
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400330 }
331}
332
333type SaveProjectInput struct {
334 Config string `json:"config"`
335}
336
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400337type saveProjectReq struct {
338 Type string `json:"type"`
339 Config map[string]any `json:"config"`
340}
341
342func (d *SaveProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
343 var input SaveProjectInput
344 if err := json.Unmarshal(m, &input); err != nil {
345 return nil, err
346 }
347 req := saveProjectReq{
348 Type: "config",
349 }
350 if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
351 return nil, err
352 }
353 jsonReq, err := json.Marshal(req)
354 if err != nil {
355 return nil, err
356 }
357 resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/saved", d.apiBaseAddress, d.projectId), "application/json", bytes.NewBuffer(jsonReq))
358 if err != nil {
359 return nil, err
360 }
361 defer resp.Body.Close()
362 body, err := io.ReadAll(resp.Body)
363 if err != nil {
364 return nil, err
365 }
366 if resp.StatusCode != http.StatusOK {
367 return nil, fmt.Errorf("failed to save project: %s", string(body))
368 }
369 return llm.TextContent(string("Project saved successfully")), nil
370}
371
giofe6e7142025-06-18 08:51:23 +0000372func NewDodoTools(apiBaseAddress string, projectId string) []*llm.Tool {
373 return []*llm.Tool{
374 NewGetProjectConfigTool(apiBaseAddress, projectId),
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400375 NewGetProjectEnvTool(apiBaseAddress, projectId),
giofe6e7142025-06-18 08:51:23 +0000376 NewValidateConfigTool(apiBaseAddress),
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400377 NewSaveProjectTool(apiBaseAddress, projectId),
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400378 NewDeployProjectTool(apiBaseAddress, projectId),
giofe6e7142025-06-18 08:51:23 +0000379 }
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400380}