blob: eee786c61d034c3d4f72eff9d5c11e92db68ee90 [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
Sketch🕴️9988c512025-07-31 16:45:04 +040040func (d *GetProjectConfigTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
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 {
Sketch🕴️9988c512025-07-31 16:45:04 +040043 return llm.ErrorToolOut(err)
gio30503072025-06-17 10:50:15 +000044 }
45 defer resp.Body.Close()
46 body, err := io.ReadAll(resp.Body)
47 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +040048 return llm.ErrorToolOut(err)
gio30503072025-06-17 10:50:15 +000049 }
50 if resp.StatusCode != http.StatusOK {
Sketch🕴️9988c512025-07-31 16:45:04 +040051 return llm.ErrorfToolOut("failed to get project config: %s", string(body))
gio30503072025-06-17 10:50:15 +000052 }
gio30503072025-06-17 10:50:15 +000053 output := GetProjectConfigOutput{
54 Config: string(body),
55 }
56 jsonOutput, err := json.Marshal(output)
57 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +040058 return llm.ErrorToolOut(err)
gio30503072025-06-17 10:50:15 +000059 }
Sketch🕴️9988c512025-07-31 16:45:04 +040060 return llm.ToolOut{LLMContent: llm.TextContent(string(jsonOutput))}
gio30503072025-06-17 10:50:15 +000061}
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
Sketch🕴️9988c512025-07-31 16:45:04 +0400160func (d *GetProjectEnvTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400161 resp, err := http.Get(fmt.Sprintf("%s/api/project/%s/env", d.apiBaseAddress, d.projectId))
162 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400163 return llm.ErrorToolOut(err)
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400164 }
165 defer resp.Body.Close()
166 body, err := io.ReadAll(resp.Body)
167 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400168 return llm.ErrorToolOut(err)
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400169 }
170 if resp.StatusCode != http.StatusOK {
Sketch🕴️9988c512025-07-31 16:45:04 +0400171 return llm.ErrorfToolOut("failed to get project env: %s", string(body))
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400172 }
173 var output GetProjectEnvOutput
174 if err := json.Unmarshal(body, &output); err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400175 return llm.ErrorToolOut(err)
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400176 }
177 jsonOutput, err := json.Marshal(output)
178 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400179 return llm.ErrorToolOut(err)
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400180 }
Sketch🕴️9988c512025-07-31 16:45:04 +0400181 return llm.ToolOut{LLMContent: llm.TextContent(string(jsonOutput))}
Giorgi Lekveishvili737ac872025-07-08 06:07:51 +0400182}
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
Sketch🕴️9988c512025-07-31 16:45:04 +0400227func (d *ValidateConfigTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
giofe6e7142025-06-18 08:51:23 +0000228 var input ValidateConfigInput
229 if err := json.Unmarshal(m, &input); err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400230 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000231 }
232 resp, err := http.Post(fmt.Sprintf("%s/api/validate-config", d.apiBaseAddress), "application/json", bytes.NewBuffer([]byte(input.Config)))
233 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400234 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000235 }
236 defer resp.Body.Close()
237 body, err := io.ReadAll(resp.Body)
238 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400239 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000240 }
241 if resp.StatusCode != http.StatusOK {
Sketch🕴️9988c512025-07-31 16:45:04 +0400242 return llm.ErrorfToolOut("failed to validate config: %s", string(body))
giofe6e7142025-06-18 08:51:23 +0000243 }
244 var output ValidateConfigOutput
245 if err := json.Unmarshal(body, &output); err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400246 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000247 }
248 jsonOutput, err := json.Marshal(output)
249 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400250 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000251 }
Sketch🕴️9988c512025-07-31 16:45:04 +0400252 return llm.ToolOut{LLMContent: llm.TextContent(string(jsonOutput))}
giofe6e7142025-06-18 08:51:23 +0000253}
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
Sketch🕴️9988c512025-07-31 16:45:04 +0400279func (d *DeployProjectTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
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 {
Sketch🕴️9988c512025-07-31 16:45:04 +0400285 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000286 }
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 {
Sketch🕴️9988c512025-07-31 16:45:04 +0400289 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000290 }
291 defer resp.Body.Close()
292 body, err := io.ReadAll(resp.Body)
293 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400294 return llm.ErrorToolOut(err)
giofe6e7142025-06-18 08:51:23 +0000295 }
296 if resp.StatusCode != http.StatusOK {
Sketch🕴️9988c512025-07-31 16:45:04 +0400297 return llm.ErrorfToolOut("failed to deploy project: %s", string(body))
giofe6e7142025-06-18 08:51:23 +0000298 }
Sketch🕴️9988c512025-07-31 16:45:04 +0400299 return llm.ToolOut{LLMContent: llm.TextContent(string("Project deployed successfully"))}
giofe6e7142025-06-18 08:51:23 +0000300}
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
Sketch🕴️9988c512025-07-31 16:45:04 +0400348func (d *SaveProjectTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400349 var input SaveProjectInput
350 if err := json.Unmarshal(m, &input); err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400351 return llm.ErrorToolOut(err)
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400352 }
353 req := saveProjectReq{
354 Type: "config",
355 }
356 if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400357 return llm.ErrorToolOut(err)
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400358 }
359 jsonReq, err := json.Marshal(req)
360 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400361 return llm.ErrorToolOut(err)
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400362 }
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 {
Sketch🕴️9988c512025-07-31 16:45:04 +0400365 return llm.ErrorToolOut(err)
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400366 }
367 defer resp.Body.Close()
368 body, err := io.ReadAll(resp.Body)
369 if err != nil {
Sketch🕴️9988c512025-07-31 16:45:04 +0400370 return llm.ErrorToolOut(err)
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400371 }
372 if resp.StatusCode != http.StatusOK {
Sketch🕴️9988c512025-07-31 16:45:04 +0400373 return llm.ErrorfToolOut("failed to save project: %s", string(body))
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400374 }
Sketch🕴️9988c512025-07-31 16:45:04 +0400375 return llm.ToolOut{LLMContent: llm.TextContent(string("Project saved successfully"))}
Giorgi Lekveishvili05569832025-07-06 09:16:29 +0400376}
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}