dodo: save config tool

Change-Id: Ia8c8639cda093a3776ff018c2b77e63449cbc78c
diff --git a/dodo_tools/dodo.go b/dodo_tools/dodo.go
index 41127df..fdb7c8f 100644
--- a/dodo_tools/dodo.go
+++ b/dodo_tools/dodo.go
@@ -1,8 +1,8 @@
 package dodo_tools
 
 import (
-	"context"
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -16,7 +16,6 @@
 	projectId      string
 }
 
-
 func NewGetProjectConfigTool(apiBaseAddress string, projectId string) *llm.Tool {
 	tool := &GetProjectConfigTool{
 		apiBaseAddress: apiBaseAddress,
@@ -99,11 +98,10 @@
 
 type ValidateConfigOutput struct {
 	Success bool `json:"success"`
-	Errors  any `json:"errors,omitempty"`
+	Errors  any  `json:"errors,omitempty"`
 }
 
 func (d *ValidateConfigTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
-	fmt.Printf("%s\n", string(m))
 	var input ValidateConfigInput
 	if err := json.Unmarshal(m, &input); err != nil {
 		return nil, err
@@ -171,7 +169,7 @@
 
 type DeployProjectOutput struct {
 	Success bool `json:"success"`
-	Errors  any `json:"errors,omitempty"`
+	Errors  any  `json:"errors,omitempty"`
 }
 
 type deployProjectReq struct {
@@ -187,7 +185,6 @@
 	if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
 		return nil, err
 	}
-	fmt.Printf("### %+v\n", req)
 	jsonReq, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
@@ -207,10 +204,91 @@
 	return llm.TextContent(string("Project deployed successfully")), nil
 }
 
+// Save
+
+type SaveProjectTool struct {
+	apiBaseAddress string
+	projectId      string
+}
+
+const (
+	saveProjectSchemaInputSchema = `
+{
+	"type": "object",
+	"properties": {
+		"config": {
+			"type": "string",
+			"description": "Serialized dodo-app configuration to save"
+		}
+	}
+}
+`
+)
+
+func NewSaveProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
+	tool := &SaveProjectTool{
+		apiBaseAddress: apiBaseAddress,
+		projectId:      projectId,
+	}
+
+	return &llm.Tool{
+		Name:        "dodo_save_config",
+		Description: "A tool for saving the dodo-app configuration",
+		InputSchema: llm.MustSchema(saveProjectSchemaInputSchema),
+		Run:         tool.Run,
+		EndsTurn:    true,
+	}
+}
+
+type SaveProjectInput struct {
+	Config string `json:"config"`
+}
+
+type SaveProjectOutput struct {
+	Success bool `json:"success"`
+	Errors  any  `json:"errors,omitempty"`
+}
+
+type saveProjectReq struct {
+	Type   string         `json:"type"`
+	Config map[string]any `json:"config"`
+}
+
+func (d *SaveProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+	var input SaveProjectInput
+	if err := json.Unmarshal(m, &input); err != nil {
+		return nil, err
+	}
+	req := saveProjectReq{
+		Type: "config",
+	}
+	if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
+		return nil, err
+	}
+	jsonReq, err := json.Marshal(req)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/saved", d.apiBaseAddress, d.projectId), "application/json", bytes.NewBuffer(jsonReq))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("failed to save project: %s", string(body))
+	}
+	return llm.TextContent(string("Project saved successfully")), nil
+}
+
 func NewDodoTools(apiBaseAddress string, projectId string) []*llm.Tool {
 	return []*llm.Tool{
 		NewGetProjectConfigTool(apiBaseAddress, projectId),
 		NewValidateConfigTool(apiBaseAddress),
 		NewDeployProjectTool(apiBaseAddress, projectId),
+		NewSaveProjectTool(apiBaseAddress, projectId),
 	}
-}
\ No newline at end of file
+}