Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 27 additions & 31 deletions internal/tool/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,50 +229,46 @@ func (t *CodeGenTool) Description() string {
return "Generate common code patterns from templates. Supports Go, Python, and TypeScript templates for handlers, tests, middleware, CRUD operations, and more."
}

func (t *CodeGenTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"generate", "list", "preview", "suggest"},
"description": "Action to perform",
},
"template": map[string]interface{}{
"type": "string",
"description": "Template name (e.g., go-handler, py-fastapi-endpoint)",
},
"variables": map[string]interface{}{
"type": "object",
"description": "Template variables as key-value pairs",
},
"language": map[string]interface{}{
"type": "string",
"description": "Filter templates by language (go, python, typescript)",
},
"description": map[string]interface{}{
"type": "string",
"description": "Natural language description for template suggestion",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (t *CodeGenTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"generate", "list", "preview", "suggest"}, Description: "Action to perform"},
"template": {Type: "string", Description: "Template name (e.g., go-handler, py-fastapi-endpoint)"},
"variables": {Type: "object", Description: "Template variables as key-value pairs"},
"language": {Type: "string", Description: "Filter templates by language (go, python, typescript)"},
"description": {Type: "string", Description: "Natural language description for template suggestion"},
},
"required": []string{"action"},
Required: []string{"action"},
}
}

type codeGenInput struct {
func (t *CodeGenTool) Parameters() map[string]interface{} {
return codeGenSchema.ToJSONSchema()
}

// codeGenSchema is the single source of truth for CodeGen's input schema.
var codeGenSchema = (&CodeGenTool{}).Schema()

// CodeGenInput is the typed input for CodeGenTool.
type CodeGenInput struct {
Action string `json:"action"`
Template string `json:"template"`
Variables map[string]string `json:"variables"`
Language string `json:"language"`
Description string `json:"description"`
}

// codeGenInput is an unexported alias for backward compatibility with tests.
type codeGenInput = CodeGenInput

func (t *CodeGenTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var in codeGenInput
if err := json.Unmarshal(input, &in); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
in, err := DecodeInput[CodeGenInput]("CodeGen", input)
if err != nil {
return "", err
}

switch in.Action {
case "generate":
if in.Template == "" {
Expand Down
97 changes: 97 additions & 0 deletions internal/tool/schema_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -994,3 +994,100 @@ func TestRefactorSchemaProvider(t *testing.T) {
t.Fatalf("required = %v, want [action file]", RefactorTool{}.Parameters()["required"])
}
}

func TestSQLSchemaProvider(t *testing.T) {
var _ SchemaProvider = SQLTool{}
props := schemaProps(t, SQLTool{}.Parameters())
enum, ok := props["driver"].(map[string]interface{})["enum"].([]interface{})
if !ok || len(enum) != 3 || enum[0] != "sqlite" || enum[2] != "mysql" {
t.Fatalf("driver enum = %v, want 3 options", props["driver"])
}
req, _ := SQLTool{}.Parameters()["required"].([]string)
if len(req) != 2 || req[0] != "dsn" || req[1] != "query" {
t.Fatalf("required = %v, want [dsn query]", SQLTool{}.Parameters()["required"])
}
}

func TestStructuredEditSchemaProvider(t *testing.T) {
var _ SchemaProvider = StructuredEditTool{}
props := schemaProps(t, StructuredEditTool{}.Parameters())
if props["path"].(map[string]interface{})["type"] != "string" {
t.Fatal("path type wrong")
}
blocks := props["blocks"].(map[string]interface{})
if blocks["type"] != "array" {
t.Fatalf("blocks type = %v, want array", blocks["type"])
}
items := blocks["items"].(map[string]interface{})
itemReq, ok := items["required"].([]string)
if !ok || len(itemReq) != 2 || itemReq[0] != "search" || itemReq[1] != "replace" {
t.Fatalf("items required = %v, want [search replace]", items["required"])
}
req, _ := StructuredEditTool{}.Parameters()["required"].([]string)
if len(req) != 2 || req[0] != "path" || req[1] != "blocks" {
t.Fatalf("required = %v, want [path blocks]", StructuredEditTool{}.Parameters()["required"])
}
}

func TestVerifyPlanExecutionSchemaProvider(t *testing.T) {
var _ SchemaProvider = VerifyPlanExecutionTool{}
props := schemaProps(t, VerifyPlanExecutionTool{}.Parameters())
steps := props["plan_steps"].(map[string]interface{})
if steps["type"] != "array" {
t.Fatalf("plan_steps type = %v, want array", steps["type"])
}
itemProps := steps["items"].(map[string]interface{})["properties"].(map[string]interface{})
if itemProps["description"].(map[string]interface{})["type"] != "string" {
t.Fatal("description type wrong")
}
if itemProps["expected"].(map[string]interface{})["type"] != "string" {
t.Fatal("expected type wrong")
}
req, _ := VerifyPlanExecutionTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "plan_steps" {
t.Fatalf("required = %v, want [plan_steps]", VerifyPlanExecutionTool{}.Parameters()["required"])
}
}

func TestWorkflowSchemaProvider(t *testing.T) {
var _ SchemaProvider = WorkflowTool{}
props := schemaProps(t, WorkflowTool{}.Parameters())
if props["workflow"].(map[string]interface{})["type"] != "string" {
t.Fatal("workflow type wrong")
}
if props["args"].(map[string]interface{})["type"] != "object" {
t.Fatal("args type wrong")
}
req, _ := WorkflowTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "workflow" {
t.Fatalf("required = %v, want [workflow]", WorkflowTool{}.Parameters()["required"])
}
}

func TestCodeGenSchemaProvider(t *testing.T) {
var inter SchemaProvider = (&CodeGenTool{})
_ = inter
props := schemaProps(t, (&CodeGenTool{}).Parameters())
enum, ok := props["action"].(map[string]interface{})["enum"].([]interface{})
if !ok || len(enum) != 4 || enum[0] != "generate" || enum[3] != "suggest" {
t.Fatalf("action enum = %v, want 4 options", props["action"])
}
req, _ := (&CodeGenTool{}).Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "action" {
t.Fatalf("required = %v, want [action]", (&CodeGenTool{}).Parameters()["required"])
}
}

func TestTicketComplianceSchemaProvider(t *testing.T) {
var inter SchemaProvider = (&TicketComplianceTool{})
_ = inter
props := schemaProps(t, (&TicketComplianceTool{}).Parameters())
enum, ok := props["ticket_source"].(map[string]interface{})["enum"].([]interface{})
if !ok || len(enum) != 3 || enum[0] != "github" || enum[2] != "linear" {
t.Fatalf("ticket_source enum = %v, want 3 options", props["ticket_source"])
}
req, _ := (&TicketComplianceTool{}).Parameters()["required"].([]string)
if len(req) != 2 || req[0] != "ticket_content" || req[1] != "diff" {
t.Fatalf("required = %v, want [ticket_content diff]", (&TicketComplianceTool{}).Parameters()["required"])
}
}
66 changes: 30 additions & 36 deletions internal/tool/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ import (
// dialect light up without further changes here.
type SQLTool struct{}

// SQLInput is the typed input for SQLTool.
type SQLInput struct {
Driver string `json:"driver"`
DSN string `json:"dsn"`
Query string `json:"query"`
AllowWrite bool `json:"allow_write"`
MaxRows int `json:"max_rows"`
}

func (SQLTool) Name() string { return "SQL" }
func (SQLTool) Aliases() []string { return []string{"sql", "sql_query"} }

Expand All @@ -40,38 +49,29 @@ func (SQLTool) Description() string {
// can prompt when appropriate.
func (SQLTool) RiskLevel() string { return "medium" }

func (SQLTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"driver": map[string]interface{}{
"type": "string",
"description": "Database dialect: one of sqlite, postgres, mysql. Defaults to sqlite.",
"enum": []string{"sqlite", "postgres", "mysql"},
},
"dsn": map[string]interface{}{
"type": "string",
"description": "Data source name / connection string. For sqlite " +
"this is the file path (or \":memory:\").",
},
"query": map[string]interface{}{
"type": "string",
"description": "The SQL statement to execute.",
},
"allow_write": map[string]interface{}{
"type": "boolean",
"description": "Allow destructive / mutating statements. When false " +
"(the default) only read-only queries are permitted.",
},
"max_rows": map[string]interface{}{
"type": "integer",
"description": "Maximum number of rows to return (default 100).",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (SQLTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"driver": {Type: "string", Enum: []interface{}{"sqlite", "postgres", "mysql"}, Description: "Database dialect: one of sqlite, postgres, mysql. Defaults to sqlite."},
"dsn": {Type: "string", Description: `Data source name / connection string. For sqlite this is the file path (or ":memory:").`},
"query": {Type: "string", Description: "The SQL statement to execute."},
"allow_write": {Type: "boolean", Description: "Allow destructive / mutating statements. When false (the default) only read-only queries are permitted."},
"max_rows": {Type: "integer", Description: "Maximum number of rows to return (default 100)."},
},
"required": []string{"dsn", "query"},
Required: []string{"dsn", "query"},
}
}

func (SQLTool) Parameters() map[string]interface{} {
return sqlSchema.ToJSONSchema()
}

// sqlSchema is the single source of truth for SQL's input schema.
var sqlSchema = SQLTool{}.Schema()

// driverName maps a user-facing dialect to its database/sql driver name. The
// bool reports whether rho bundles a driver for that dialect.
func sqlDriverName(dialect string) (driver string, bundled bool, err error) {
Expand Down Expand Up @@ -250,14 +250,8 @@ func firstSQLKeyword(query string) string {
}

func (t SQLTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Driver string `json:"driver"`
DSN string `json:"dsn"`
Query string `json:"query"`
AllowWrite bool `json:"allow_write"`
MaxRows int `json:"max_rows"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[SQLInput]("SQL", input)
if err != nil {
return "", err
}
if strings.TrimSpace(p.DSN) == "" {
Expand Down
64 changes: 32 additions & 32 deletions internal/tool/structured_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,42 @@ func (StructuredEditTool) Description() string {
return "Apply search-and-replace edits to a file. Provide one or more SEARCH/REPLACE blocks. Each block finds exact text and replaces it. The SEARCH text must match the file contents exactly, including whitespace."
}

func (StructuredEditTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "Path to the file to edit",
},
"blocks": map[string]interface{}{
"type": "array",
"description": "List of SEARCH/REPLACE blocks",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"search": map[string]interface{}{
"type": "string",
"description": "Exact text to find. Must match the file exactly, including whitespace.",
},
"replace": map[string]interface{}{
"type": "string",
"description": "Text to replace the search text with.",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (StructuredEditTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"path": {Type: "string", Description: "Path to the file to edit"},
"blocks": {
Type: "array",
Description: "List of SEARCH/REPLACE blocks",
Items: &SchemaProperty{
Type: "object",
Properties: map[string]SchemaProperty{
"search": {Type: "string", Description: "Exact text to find. Must match the file exactly, including whitespace."},
"replace": {Type: "string", Description: "Text to replace the search text with."},
},
"required": []string{"search", "replace"},
Required: []string{"search", "replace"},
},
},
"auto_format": map[string]interface{}{
"type": "boolean",
"description": "If true, ignore insignificant whitespace differences when matching (default: false)",
},
"auto_format": {Type: "boolean", Description: "If true, ignore insignificant whitespace differences when matching (default: false)"},
},
"required": []string{"path", "blocks"},
Required: []string{"path", "blocks"},
}
}

func (StructuredEditTool) Parameters() map[string]interface{} {
return structuredEditSchema.ToJSONSchema()
}

// structuredEditSchema is the single source of truth for StructuredEdit's input schema.
var structuredEditSchema = StructuredEditTool{}.Schema()

// StructuredEditInput is the typed input for StructuredEditTool.
// Aliased from the legacy unexported name.
type StructuredEditInput = structuredEditInput

type structuredEditInput struct {
Path string `json:"path"`
Blocks []searchReplaceBlock `json:"blocks"`
Expand All @@ -68,11 +69,10 @@ type searchReplaceBlock struct {
}

func (s StructuredEditTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p structuredEditInput
if err := json.Unmarshal(input, &p); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
p, err := DecodeInput[structuredEditInput]("StructuredEdit", input)
if err != nil {
return "", err
}

if p.Path == "" {
return "", fmt.Errorf("path is required")
}
Expand Down
Loading
Loading