blob: 2d113cbe633b90a7eb3d1d26d59f0b9e75ea233e [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
Giorgi Lekveishvilib9532942025-07-20 14:48:37 +0400149type EnvVar struct {
150 Name string `json:"name"`
151 Value string `json:"value"`
152}
153
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400154type GetProjectEnvOutput struct {
155 GithubToken string `json:"githubToken,omitempty"`
156 Networks []Network `json:"networks,omitempty"`
Giorgi Lekveishvilib9532942025-07-20 14:48:37 +0400157 EnvVars []EnvVar `json:"envVars"`
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400158}
159
160func (d *GetProjectEnvTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
161 resp, err := http.Get(fmt.Sprintf("%s/api/project/%s/env", d.apiBaseAddress, d.projectId))
162 if err != nil {
163 return nil, err
164 }
165 defer resp.Body.Close()
166 body, err := io.ReadAll(resp.Body)
167 if err != nil {
168 return nil, err
169 }
170 if resp.StatusCode != http.StatusOK {
171 return nil, fmt.Errorf("failed to get project env: %s", string(body))
172 }
173 var output GetProjectEnvOutput
174 if err := json.Unmarshal(body, &output); err != nil {
175 return nil, err
176 }
177 jsonOutput, err := json.Marshal(output)
178 if err != nil {
179 return nil, err
180 }
181 return llm.TextContent(string(jsonOutput)), nil
182}
183
184// Validate
185
giofe6e7142025-06-18 08:51:23 +0000186type ValidateConfigTool struct {
187 apiBaseAddress string
188}
189
190const (
191 validateConfigSchemaInputSchema = `
192{
193 "type": "object",
194 "properties": {
195 "config": {
196 "type": "string",
197 "description": "The dodo-app configuration to validate"
198 }
199 },
200 "required": ["config"]
201}
202`
203)
204
205func NewValidateConfigTool(apiBaseAddress string) *llm.Tool {
206 tool := &ValidateConfigTool{
207 apiBaseAddress: apiBaseAddress,
208 }
209 return &llm.Tool{
210 Name: "dodo_validate_config",
211 Description: "A tool for validating the dodo-app configuration",
212 InputSchema: llm.MustSchema(validateConfigSchemaInputSchema),
213 Run: tool.Run,
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400214 EndsTurn: false,
giofe6e7142025-06-18 08:51:23 +0000215 }
216}
217
218type ValidateConfigInput struct {
219 Config string `json:"config"`
220}
221
222type ValidateConfigOutput struct {
223 Success bool `json:"success"`
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400224 Errors any `json:"errors,omitempty"`
giofe6e7142025-06-18 08:51:23 +0000225}
226
227func (d *ValidateConfigTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
giofe6e7142025-06-18 08:51:23 +0000228 var input ValidateConfigInput
229 if err := json.Unmarshal(m, &input); err != nil {
230 return nil, err
231 }
232 resp, err := http.Post(fmt.Sprintf("%s/api/validate-config", d.apiBaseAddress), "application/json", bytes.NewBuffer([]byte(input.Config)))
233 if err != nil {
234 return nil, err
235 }
236 defer resp.Body.Close()
237 body, err := io.ReadAll(resp.Body)
238 if err != nil {
239 return nil, err
240 }
241 if resp.StatusCode != http.StatusOK {
242 return nil, fmt.Errorf("failed to validate config: %s", string(body))
243 }
244 var output ValidateConfigOutput
245 if err := json.Unmarshal(body, &output); err != nil {
246 return nil, err
247 }
248 jsonOutput, err := json.Marshal(output)
249 if err != nil {
250 return nil, err
251 }
252 return llm.TextContent(string(jsonOutput)), nil
253}
254
255type DeployProjectTool struct {
256 apiBaseAddress string
257 projectId string
258}
259
giofe6e7142025-06-18 08:51:23 +0000260func NewDeployProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
261 tool := &DeployProjectTool{
262 apiBaseAddress: apiBaseAddress,
263 projectId: projectId,
264 }
265
266 return &llm.Tool{
267 Name: "dodo_deploy_project",
268 Description: "A tool for deploying the dodo-app configuration",
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400269 InputSchema: llm.EmptySchema(),
giofe6e7142025-06-18 08:51:23 +0000270 Run: tool.Run,
271 EndsTurn: true,
272 }
273}
274
giofe6e7142025-06-18 08:51:23 +0000275type deployProjectReq struct {
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400276 Type string `json:"type"`
giofe6e7142025-06-18 08:51:23 +0000277}
278
279func (d *DeployProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400280 req := deployProjectReq{
281 Type: "draft",
giofe6e7142025-06-18 08:51:23 +0000282 }
giofe6e7142025-06-18 08:51:23 +0000283 jsonReq, err := json.Marshal(req)
284 if err != nil {
285 return nil, err
286 }
287 resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/deploy", d.apiBaseAddress, d.projectId), "application/json", bytes.NewBuffer(jsonReq))
288 if err != nil {
289 return nil, err
290 }
291 defer resp.Body.Close()
292 body, err := io.ReadAll(resp.Body)
293 if err != nil {
294 return nil, err
295 }
296 if resp.StatusCode != http.StatusOK {
297 return nil, fmt.Errorf("failed to deploy project: %s", string(body))
298 }
299 return llm.TextContent(string("Project deployed successfully")), nil
300}
301
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400302// Save
303
304type SaveProjectTool struct {
305 apiBaseAddress string
306 projectId string
307}
308
309const (
310 saveProjectSchemaInputSchema = `
311{
312 "type": "object",
313 "properties": {
Giorgi Lekveishvili49b7b0a2025-07-07 13:52:35 +0400314 "config": {
315 "type": "string",
316 "description": "Serialized dodo-app configuration to save"
317 }
318 },
319 "required": ["config"]
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400320}
321`
322)
323
324func NewSaveProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
325 tool := &SaveProjectTool{
326 apiBaseAddress: apiBaseAddress,
327 projectId: projectId,
328 }
329
330 return &llm.Tool{
331 Name: "dodo_save_config",
332 Description: "A tool for saving the dodo-app configuration",
333 InputSchema: llm.MustSchema(saveProjectSchemaInputSchema),
334 Run: tool.Run,
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400335 EndsTurn: false,
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400336 }
337}
338
339type SaveProjectInput struct {
340 Config string `json:"config"`
341}
342
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400343type saveProjectReq struct {
344 Type string `json:"type"`
345 Config map[string]any `json:"config"`
346}
347
348func (d *SaveProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
349 var input SaveProjectInput
350 if err := json.Unmarshal(m, &input); err != nil {
351 return nil, err
352 }
353 req := saveProjectReq{
354 Type: "config",
355 }
356 if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
357 return nil, err
358 }
359 jsonReq, err := json.Marshal(req)
360 if err != nil {
361 return nil, err
362 }
363 resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/saved", d.apiBaseAddress, d.projectId), "application/json", bytes.NewBuffer(jsonReq))
364 if err != nil {
365 return nil, err
366 }
367 defer resp.Body.Close()
368 body, err := io.ReadAll(resp.Body)
369 if err != nil {
370 return nil, err
371 }
372 if resp.StatusCode != http.StatusOK {
373 return nil, fmt.Errorf("failed to save project: %s", string(body))
374 }
375 return llm.TextContent(string("Project saved successfully")), nil
376}
377
giofe6e7142025-06-18 08:51:23 +0000378func NewDodoTools(apiBaseAddress string, projectId string) []*llm.Tool {
379 return []*llm.Tool{
380 NewGetProjectConfigTool(apiBaseAddress, projectId),
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400381 NewGetProjectEnvTool(apiBaseAddress, projectId),
giofe6e7142025-06-18 08:51:23 +0000382 NewValidateConfigTool(apiBaseAddress),
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400383 NewSaveProjectTool(apiBaseAddress, projectId),
Giorgi Lekveishvilif9d7fc52025-07-07 07:09:42 +0400384 NewDeployProjectTool(apiBaseAddress, projectId),
giofe6e7142025-06-18 08:51:23 +0000385 }
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400386}