dodo: implement validate and deploy tools

Change-Id: I026cff45159dd1e6fa6a9bcef7c843564bcf9c8b
diff --git a/dodo_tools/dodo.go b/dodo_tools/dodo.go
index a5228de..41127df 100644
--- a/dodo_tools/dodo.go
+++ b/dodo_tools/dodo.go
@@ -2,6 +2,7 @@
 
 import (
 	"context"
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -11,41 +12,26 @@
 )
 
 type GetProjectConfigTool struct {
+	apiBaseAddress string
+	projectId      string
 }
 
-const (
-	getProjectSchemaInputSchema = `
-{
-	"type": "object",
-	"properties": {
-		"apiBaseAddress": {
-			"type": "string",
-			"description": "The base address of the dodo API"
-		},
-		"projectId": {
-			"type": "string",
-			"description": "The ID of the dodo project to get infrastructure configuration."
-		}
-	},
-	"required": ["apiBaseAddress", "projectId"]
-}
-`
-)
 
-func NewGetProjectConfigTool() *llm.Tool {
-	tool := &GetProjectConfigTool{}
+func NewGetProjectConfigTool(apiBaseAddress string, projectId string) *llm.Tool {
+	tool := &GetProjectConfigTool{
+		apiBaseAddress: apiBaseAddress,
+		projectId:      projectId,
+	}
 	return &llm.Tool{
 		Name:        "dodo_get_project_config",
 		Description: "A tool for getting current state of the infrastructure configuration of a dodo project",
-		InputSchema: llm.MustSchema(getProjectSchemaInputSchema),
+		InputSchema: llm.EmptySchema(),
 		Run:         tool.Run,
 		EndsTurn:    true,
 	}
 }
 
 type GetProjectConfigInput struct {
-	ApiBaseAddress string `json:"apiBaseAddress"`
-	ProjectId      string `json:"projectId"`
 }
 
 type GetProjectConfigOutput struct {
@@ -53,11 +39,7 @@
 }
 
 func (d *GetProjectConfigTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
-	var input GetProjectConfigInput
-	if err := json.Unmarshal(m, &input); err != nil {
-		return nil, err
-	}
-	resp, err := http.Get(fmt.Sprintf("%s/api/project/%s/config", input.ApiBaseAddress, input.ProjectId))
+	resp, err := http.Get(fmt.Sprintf("%s/api/project/%s/config", d.apiBaseAddress, d.projectId))
 	if err != nil {
 		return nil, err
 	}
@@ -69,10 +51,6 @@
 	if resp.StatusCode != http.StatusOK {
 		return nil, fmt.Errorf("failed to get project config: %s", string(body))
 	}
-	var config map[string]interface{}
-	if err := json.Unmarshal(body, &config); err != nil {
-		return nil, fmt.Errorf("got invalid project config: %s %s", err, string(body))
-	}
 	output := GetProjectConfigOutput{
 		Config: string(body),
 	}
@@ -82,3 +60,157 @@
 	}
 	return llm.TextContent(string(jsonOutput)), nil
 }
+
+type ValidateConfigTool struct {
+	apiBaseAddress string
+}
+
+const (
+	validateConfigSchemaInputSchema = `
+{
+	"type": "object",
+	"properties": {
+		"config": {
+			"type": "string",
+			"description": "The dodo-app configuration to validate"
+		}
+	},
+	"required": ["config"]
+}
+`
+)
+
+func NewValidateConfigTool(apiBaseAddress string) *llm.Tool {
+	tool := &ValidateConfigTool{
+		apiBaseAddress: apiBaseAddress,
+	}
+	return &llm.Tool{
+		Name:        "dodo_validate_config",
+		Description: "A tool for validating the dodo-app configuration",
+		InputSchema: llm.MustSchema(validateConfigSchemaInputSchema),
+		Run:         tool.Run,
+		EndsTurn:    true,
+	}
+}
+
+type ValidateConfigInput struct {
+	Config string `json:"config"`
+}
+
+type ValidateConfigOutput struct {
+	Success bool `json:"success"`
+	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
+	}
+	resp, err := http.Post(fmt.Sprintf("%s/api/validate-config", d.apiBaseAddress), "application/json", bytes.NewBuffer([]byte(input.Config)))
+	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 validate config: %s", string(body))
+	}
+	var output ValidateConfigOutput
+	if err := json.Unmarshal(body, &output); err != nil {
+		return nil, err
+	}
+	jsonOutput, err := json.Marshal(output)
+	if err != nil {
+		return nil, err
+	}
+	return llm.TextContent(string(jsonOutput)), nil
+}
+
+type DeployProjectTool struct {
+	apiBaseAddress string
+	projectId      string
+}
+
+const (
+	deployProjectSchemaInputSchema = `
+{
+	"type": "object",
+	"properties": {
+		"config": {
+			"type": "string",
+			"description": "Serialized dodo-app configuration to deploy"
+		}
+	}
+}
+`
+)
+
+func NewDeployProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
+	tool := &DeployProjectTool{
+		apiBaseAddress: apiBaseAddress,
+		projectId:      projectId,
+	}
+
+	return &llm.Tool{
+		Name:        "dodo_deploy_project",
+		Description: "A tool for deploying the dodo-app configuration",
+		InputSchema: llm.MustSchema(deployProjectSchemaInputSchema),
+		Run:         tool.Run,
+		EndsTurn:    true,
+	}
+}
+
+type DeployProjectInput struct {
+	Config string `json:"config"`
+}
+
+type DeployProjectOutput struct {
+	Success bool `json:"success"`
+	Errors  any `json:"errors,omitempty"`
+}
+
+type deployProjectReq struct {
+	Config map[string]any `json:"config"`
+}
+
+func (d *DeployProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+	var input DeployProjectInput
+	if err := json.Unmarshal(m, &input); err != nil {
+		return nil, err
+	}
+	req := deployProjectReq{}
+	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
+	}
+	resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/deploy", 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 deploy project: %s", string(body))
+	}
+	return llm.TextContent(string("Project deployed successfully")), nil
+}
+
+func NewDodoTools(apiBaseAddress string, projectId string) []*llm.Tool {
+	return []*llm.Tool{
+		NewGetProjectConfigTool(apiBaseAddress, projectId),
+		NewValidateConfigTool(apiBaseAddress),
+		NewDeployProjectTool(apiBaseAddress, projectId),
+	}
+}
\ No newline at end of file