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
diff --git a/loop/agent.go b/loop/agent.go
index d1875bf..186bd1a 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -1416,6 +1416,9 @@
}()
browserTools = bTools
+ // TODO(gio): get these from the config
+ dodoTools := dodo_tools.NewDodoTools(os.Getenv("DODO_API_BASE_ADDR"), os.Getenv("DODO_PROJECT_ID"))
+
convo.Tools = []*llm.Tool{
bashTool.Tool(),
claudetool.Keyword,
@@ -1426,9 +1429,9 @@
makeDoneTool(a.codereview),
a.codereview.Tool(),
claudetool.AboutSketch,
- dodo_tools.NewGetProjectConfigTool(),
}
convo.Tools = append(convo.Tools, browserTools...)
+ convo.Tools = append(convo.Tools, dodoTools...)
// Add MCP tools if configured
if len(a.config.MCPServers) > 0 {
diff --git a/loop/agent_system_prompt.txt b/loop/agent_system_prompt.txt
index c572752..99b3145 100644
--- a/loop/agent_system_prompt.txt
+++ b/loop/agent_system_prompt.txt
@@ -74,17 +74,24 @@
<dodo>
You are developing application on top of the dodo. dodo is a Platform As A Service infrastructure, allowing users to define their services and infrastructure needs.
-dodo infrastucture is configuration driven. Read <dodo-schema> section which provides JSON schema definition of the configuration.
-Use dodo tools to interact with dodo. Following two tools are implemented:
-1. dodo_get_project_config: Gets the current state of the application configuration. It requires two input fields: apiBaseAddress represents dodo API base address, and projectId which represents current dodo project ID you are working on. Use DODO_API_BASE_ADDR and DODO_PROJECT_ID env variables respectively.
-2. dodo_update_project_config: not implemented yet.
+dodo infrastucture is configuration driven, refer to <dodo-schema> section for configuration JSONSchema definition.
+Use following tools to interact with dodo:
+1. dodo_get_project_config: Gets the current state of the application configuration.
+2. dodo_validate_config: Takes dodo-app configuration and validates it. Returned result is a JSON object with boolean success field and optional errors array field.
+3. dodo_deploy_project: Takes new configuration and deployes it.
You might want to use dodo tools in following scenarios:
1. User explicitely asks to get the current configuration.
2. User explicitely asks to add new infrastructure pieces or modify existing ones.
3. User asks you to implement new feature which requires new infrastucture piece.
-When you are not sure about the modification you've made in the dodo app configuration, ask user for confirmation.
+When making changes in the dodo-app configuration, make sure it is valid before presenting your changes to user or sending it to dodo API for deployment.
+When validating inspect success and error fields and fix all errors. Use the todo_read and todo_write tools to organize and track your dodo-app configuration changes.
+
+Always pretty print dodo-app config before presenting it to the user.
+
+When you are not sure about the modification you've made in the dodo-app configuration, ask user for confirmation. When requesting confirmation, be very clear about it and formulate request as a question. Again, be very clear and terse when asking questions to the user.
+Use multiplechoice tool with Yes/No options when asking user confirmation.
</dodo>
<dodo-schema>
@@ -327,7 +334,7 @@
"properties": {
"name": {
"type": "string",
- "description": "Name of the port (e.g., 'http', 'grpc')."
+ "description": "Name of the port (e.g., 'http', 'grpc'). Port value will be available to the service at runtime as a DODO_PORT_<NAME> environment variable, where <NAME> is uppercased port name."
},
"value": {
"type": "number",
diff --git a/sketch b/sketch
new file mode 100755
index 0000000..df3099f
--- /dev/null
+++ b/sketch
Binary files differ