2023-11-25 17:55:18 -07:00
package cli
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
2023-11-25 23:35:22 -07:00
"strings"
2023-11-25 17:55:18 -07:00
openai "github.com/sashabaranov/go-openai"
)
type FunctionResult struct {
Message string ` json:"message" `
Result any ` json:"result,omitempty" `
}
type FunctionParameter struct {
Type string ` json:"type" ` // "string", "integer", "boolean"
Description string ` json:"description" `
Enum [ ] string ` json:"enum,omitempty" `
}
type FunctionParameters struct {
Type string ` json:"type" ` // "object"
Properties map [ string ] FunctionParameter ` json:"properties" `
Required [ ] string ` json:"required,omitempty" ` // required function parameter names
}
type AvailableTool struct {
openai . Tool
// The tool's implementation. Returns a string, as tool call results
// are treated as normal messages with string contents.
Impl func ( arguments map [ string ] interface { } ) ( string , error )
}
2023-11-26 08:52:00 -07:00
const (
READ_DIR_DESCRIPTION = ` Return the contents of the CWD ( current working directory ) .
2023-11-25 17:55:18 -07:00
Results are returned as JSON in the following format :
{
2023-11-25 23:37:54 -07:00
"message" : "success" , // if successful, or a different message indicating failure
2023-11-26 00:32:28 -07:00
// result may be an empty array if there are no files in the directory
2023-11-25 17:55:18 -07:00
"result" : [
2023-11-25 23:37:54 -07:00
{ "name" : "a_file" , "type" : "file" , "size" : 123 } ,
{ "name" : "a_directory/" , "type" : "dir" , "size" : 11 } ,
2023-11-25 17:55:18 -07:00
... // more files or directories
]
}
2023-11-25 23:37:54 -07:00
For files , size represents the size ( in bytes ) of the file .
2023-11-26 08:52:00 -07:00
For directories , size represents the number of entries in that directory . `
READ_FILE_DESCRIPTION = ` Read the contents of a text file relative to the current working directory .
Each line of the file is prefixed with its line number and a tabs ( \ t ) to make
it make it easier to see which lines to change for other modifications .
Example :
{
"message" : "success" , // if successful, or a different message indicating failure
"result" : "1\tthe contents\n2\tof the file\n"
} `
WRITE_FILE_DESCRIPTION = ` Write the provided contents to a file relative to the current working directory .
Result is returned as JSON in the following format :
{
"message" : "success" , // if successful, or a different message indicating failure
} `
2023-11-26 10:33:33 -07:00
FILE_INSERT_LINES = ` Insert lines into a file. `
2023-11-26 08:52:00 -07:00
2023-11-26 10:33:33 -07:00
FILE_REPLACE_LINES = ` Replace an (inclusive) range of lines within a file. `
2023-11-26 08:52:00 -07:00
2023-11-26 10:33:33 -07:00
FILE_REMOVE_LINES = ` Remove an (inclusive) range of lines from a file. `
2023-11-26 08:52:00 -07:00
)
var AvailableTools = map [ string ] AvailableTool {
"read_dir" : {
Tool : openai . Tool { Type : "function" , Function : openai . FunctionDefinition {
Name : "read_dir" ,
Description : READ_DIR_DESCRIPTION ,
2023-11-25 17:55:18 -07:00
Parameters : FunctionParameters {
Type : "object" ,
Properties : map [ string ] FunctionParameter {
"relative_dir" : {
Type : "string" ,
Description : "If set, read the contents of a directory relative to the current one." ,
} ,
} ,
} ,
} } ,
Impl : func ( args map [ string ] interface { } ) ( string , error ) {
var relativeDir string
tmp , ok := args [ "relative_dir" ]
if ok {
relativeDir , ok = tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid relative_dir in function arguments: %v" , tmp )
}
}
return ReadDir ( relativeDir ) , nil
} ,
} ,
2023-11-26 00:32:28 -07:00
"read_file" : {
Tool : openai . Tool { Type : "function" , Function : openai . FunctionDefinition {
2023-11-26 08:52:00 -07:00
Name : "read_file" ,
Description : READ_FILE_DESCRIPTION ,
2023-11-26 00:32:28 -07:00
Parameters : FunctionParameters {
Type : "object" ,
Properties : map [ string ] FunctionParameter {
"path" : {
Type : "string" ,
Description : "Path to a file within the current working directory to read." ,
} ,
} ,
2023-11-26 03:43:25 -07:00
Required : [ ] string { "path" } ,
2023-11-26 00:32:28 -07:00
} ,
} } ,
Impl : func ( args map [ string ] interface { } ) ( string , error ) {
tmp , ok := args [ "path" ]
if ! ok {
return "" , fmt . Errorf ( "Path parameter to read_file was not included." )
}
path , ok := tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid path in function arguments: %v" , tmp )
}
return ReadFile ( path ) , nil
} ,
} ,
"write_file" : {
Tool : openai . Tool { Type : "function" , Function : openai . FunctionDefinition {
2023-11-26 08:52:00 -07:00
Name : "write_file" ,
Description : WRITE_FILE_DESCRIPTION ,
2023-11-26 00:32:28 -07:00
Parameters : FunctionParameters {
Type : "object" ,
Properties : map [ string ] FunctionParameter {
"path" : {
Type : "string" ,
Description : "Path to a file within the current working directory to write to." ,
} ,
"content" : {
Type : "string" ,
Description : "The content to write to the file. Overwrites any existing content!" ,
} ,
} ,
2023-11-26 03:43:25 -07:00
Required : [ ] string { "path" , "content" } ,
2023-11-26 00:32:28 -07:00
} ,
} } ,
Impl : func ( args map [ string ] interface { } ) ( string , error ) {
tmp , ok := args [ "path" ]
if ! ok {
return "" , fmt . Errorf ( "Path parameter to write_file was not included." )
}
path , ok := tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid path in function arguments: %v" , tmp )
}
tmp , ok = args [ "content" ]
if ! ok {
return "" , fmt . Errorf ( "Content parameter to write_file was not included." )
}
content , ok := tmp . ( string )
if ! ok {
2023-11-26 03:43:25 -07:00
return "" , fmt . Errorf ( "Invalid content in function arguments: %v" , tmp )
2023-11-26 00:32:28 -07:00
}
return WriteFile ( path , content ) , nil
} ,
} ,
2023-11-26 10:33:33 -07:00
"file_insert_lines" : {
2023-11-26 03:51:06 -07:00
Tool : openai . Tool { Type : "function" , Function : openai . FunctionDefinition {
2023-11-26 10:33:33 -07:00
Name : "file_insert_lines" ,
Description : FILE_INSERT_LINES ,
2023-11-26 03:51:06 -07:00
Parameters : FunctionParameters {
Type : "object" ,
Properties : map [ string ] FunctionParameter {
"path" : {
Type : "string" ,
Description : "Path of the file to be modified, relative to the current working directory." ,
} ,
2023-11-26 10:33:33 -07:00
"start_line" : {
Type : "integer" ,
Description : ` Which line to begin inserting lines at (existing line will be moved to after content). ` ,
} ,
"content" : {
2023-11-26 03:51:06 -07:00
Type : "string" ,
2023-11-26 10:33:33 -07:00
Description : ` The content to insert. ` ,
} ,
} ,
Required : [ ] string { "path" , "start" , "content" } ,
} ,
} } ,
Impl : func ( args map [ string ] interface { } ) ( string , error ) {
tmp , ok := args [ "path" ]
if ! ok {
return "" , fmt . Errorf ( "path parameter to write_file was not included." )
}
path , ok := tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid path in function arguments: %v" , tmp )
}
var start_line int
tmp , ok = args [ "start_line" ]
if ok {
tmp , ok := tmp . ( float64 )
if ! ok {
return "" , fmt . Errorf ( "Invalid start_line in function arguments: %v" , tmp )
}
start_line = int ( tmp )
}
var content string
tmp , ok = args [ "content" ]
if ok {
content , ok = tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid content in function arguments: %v" , tmp )
}
}
return FileInsertLines ( path , start_line , content ) , nil
} ,
} ,
"file_replace_lines" : {
Tool : openai . Tool { Type : "function" , Function : openai . FunctionDefinition {
Name : "file_replace_lines" ,
Description : FILE_REPLACE_LINES ,
Parameters : FunctionParameters {
Type : "object" ,
Properties : map [ string ] FunctionParameter {
"path" : {
Type : "string" ,
Description : "Path of the file to be modified, relative to the current working directory." ,
2023-11-26 03:51:06 -07:00
} ,
"start_line" : {
Type : "integer" ,
2023-11-26 10:33:33 -07:00
Description : ` Line number which specifies the start of the replacement range (inclusive, this line will be replaced) ` ,
2023-11-26 03:51:06 -07:00
} ,
"end_line" : {
Type : "integer" ,
2023-11-26 10:33:33 -07:00
Description : ` Line number which specifies the end of the replacement range (inclusive, this line will be replaced). If unset, will be set to the end of the file. ` ,
2023-11-26 03:51:06 -07:00
} ,
"content" : {
Type : "string" ,
2023-11-26 10:33:33 -07:00
Description : ` Content which will replace specified range. ` ,
2023-11-26 03:51:06 -07:00
} ,
} ,
2023-11-26 10:33:33 -07:00
Required : [ ] string { "path" , "start_line" , "content" } ,
2023-11-26 03:51:06 -07:00
} ,
} } ,
Impl : func ( args map [ string ] interface { } ) ( string , error ) {
tmp , ok := args [ "path" ]
if ! ok {
return "" , fmt . Errorf ( "path parameter to write_file was not included." )
}
path , ok := tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid path in function arguments: %v" , tmp )
}
var start_line int
tmp , ok = args [ "start_line" ]
if ok {
tmp , ok := tmp . ( float64 )
if ! ok {
return "" , fmt . Errorf ( "Invalid start_line in function arguments: %v" , tmp )
}
start_line = int ( tmp )
}
var end_line int
tmp , ok = args [ "end_line" ]
if ok {
tmp , ok := tmp . ( float64 )
if ! ok {
return "" , fmt . Errorf ( "Invalid end_line in function arguments: %v" , tmp )
}
end_line = int ( tmp )
}
var content string
tmp , ok = args [ "content" ]
if ok {
content , ok = tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid content in function arguments: %v" , tmp )
}
}
2023-11-26 10:33:33 -07:00
return FileReplaceLines ( path , start_line , end_line , content ) , nil
} ,
} ,
"file_remove_lines" : {
Tool : openai . Tool { Type : "function" , Function : openai . FunctionDefinition {
Name : "file_remove_lines" ,
Description : FILE_REMOVE_LINES ,
Parameters : FunctionParameters {
Type : "object" ,
Properties : map [ string ] FunctionParameter {
"path" : {
Type : "string" ,
Description : "Path of the file to be modified, relative to the current working directory." ,
} ,
"start_line" : {
Type : "integer" ,
Description : ` Line number which specifies the start of the removal range (inclusive, this line will be removed) ` ,
} ,
"end_line" : {
Type : "integer" ,
Description : ` Line number which specifies the end of the removal range (inclusive, this line will be removed). ` ,
} ,
} ,
Required : [ ] string { "path" , "start_line" , "end_line" } ,
} ,
} } ,
Impl : func ( args map [ string ] interface { } ) ( string , error ) {
tmp , ok := args [ "path" ]
if ! ok {
return "" , fmt . Errorf ( "path parameter to write_file was not included." )
}
path , ok := tmp . ( string )
if ! ok {
return "" , fmt . Errorf ( "Invalid path in function arguments: %v" , tmp )
}
var start_line int
tmp , ok = args [ "start_line" ]
if ok {
tmp , ok := tmp . ( float64 )
if ! ok {
return "" , fmt . Errorf ( "Invalid start_line in function arguments: %v" , tmp )
}
start_line = int ( tmp )
}
var end_line int
tmp , ok = args [ "end_line" ]
if ok {
tmp , ok := tmp . ( float64 )
if ! ok {
return "" , fmt . Errorf ( "Invalid end_line in function arguments: %v" , tmp )
}
end_line = int ( tmp )
}
return FileRemoveLines ( path , start_line , end_line ) , nil
2023-11-26 03:51:06 -07:00
} ,
} ,
2023-11-25 17:55:18 -07:00
}
func resultToJson ( result FunctionResult ) string {
if result . Message == "" {
// When message not supplied, assume success
result . Message = "success"
}
jsonBytes , err := json . Marshal ( result )
if err != nil {
fmt . Printf ( "Could not marshal FunctionResult to JSON: %v\n" , err )
}
return string ( jsonBytes )
}
// ExecuteToolCalls handles the execution of all tool_calls provided, and
// returns their results formatted as []Message(s) with role: 'tool' and.
func ExecuteToolCalls ( toolCalls [ ] openai . ToolCall ) ( [ ] Message , error ) {
var toolResults [ ] Message
for _ , toolCall := range toolCalls {
if toolCall . Type != "function" {
// unsupported tool type
continue
}
tool , ok := AvailableTools [ toolCall . Function . Name ]
if ! ok {
return nil , fmt . Errorf ( "Requested tool '%s' does not exist. Hallucination?" , toolCall . Function . Name )
}
var functionArgs map [ string ] interface { }
err := json . Unmarshal ( [ ] byte ( toolCall . Function . Arguments ) , & functionArgs )
if err != nil {
return nil , fmt . Errorf ( "Could not unmarshal tool arguments. Malformed JSON? Error: %v" , err )
}
// TODO: ability to silence this
fmt . Fprintf ( os . Stderr , "INFO: Executing tool '%s' with args %s\n" , toolCall . Function . Name , toolCall . Function . Arguments )
// Execute the tool
toolResult , err := tool . Impl ( functionArgs )
if err != nil {
// This can happen if the model missed or supplied invalid tool args
return nil , fmt . Errorf ( "Tool '%s' error: %v\n" , toolCall . Function . Name , err )
}
toolResults = append ( toolResults , Message {
Role : "tool" ,
OriginalContent : toolResult ,
ToolCallID : sql . NullString { String : toolCall . ID , Valid : true } ,
// name is not required since the introduction of ToolCallID
// hypothesis: by setting it, we inform the model of what a
// function's purpose was if future requests omit the function
// definition
} )
}
return toolResults , nil
}
2023-11-25 23:35:22 -07:00
// isPathContained attempts to verify whether `path` is the same as or
// contained within `directory`. It is overly cautious, returning false even if
// `path` IS contained within `directory`, but the two paths use different
// casing, and we happen to be on a case-insensitive filesystem.
// This is ultimately to attempt to stop an LLM from going outside of where I
// tell it to. Additional layers of security should be considered.. run in a
// VM/container.
func isPathContained ( directory string , path string ) ( bool , error ) {
// Clean and resolve symlinks for both paths
2023-11-26 00:32:28 -07:00
path , err := filepath . Abs ( path )
2023-11-25 23:35:22 -07:00
if err != nil {
return false , err
}
2023-11-26 00:32:28 -07:00
// check if path exists
_ , err = os . Stat ( path )
2023-11-25 23:35:22 -07:00
if err != nil {
2023-11-26 00:32:28 -07:00
if ! os . IsNotExist ( err ) {
return false , fmt . Errorf ( "Could not stat path: %v" , err )
}
} else {
path , err = filepath . EvalSymlinks ( path )
if err != nil {
return false , err
}
2023-11-25 23:35:22 -07:00
}
2023-11-26 00:32:28 -07:00
directory , err = filepath . Abs ( directory )
2023-11-25 23:35:22 -07:00
if err != nil {
return false , err
}
2023-11-26 00:32:28 -07:00
directory , err = filepath . EvalSymlinks ( directory )
2023-11-25 23:35:22 -07:00
if err != nil {
return false , err
}
// Case insensitive checks
2023-11-26 00:32:28 -07:00
if ! strings . EqualFold ( path , directory ) &&
! strings . HasPrefix ( strings . ToLower ( path ) , strings . ToLower ( directory ) + string ( os . PathSeparator ) ) {
2023-11-25 23:35:22 -07:00
return false , nil
}
return true , nil
}
func isPathWithinCWD ( path string ) ( bool , * FunctionResult ) {
cwd , err := os . Getwd ( )
if err != nil {
return false , & FunctionResult { Message : "Failed to determine current working directory" }
}
if ok , err := isPathContained ( cwd , path ) ; ! ok {
if err != nil {
return false , & FunctionResult { Message : fmt . Sprintf ( "Could not determine whether path '%s' is within the current working directory: %s" , path , err . Error ( ) ) }
}
return false , & FunctionResult { Message : fmt . Sprintf ( "Path '%s' is not within the current working directory" , path ) }
}
return true , nil
}
2023-11-25 17:55:18 -07:00
func ReadDir ( path string ) string {
2023-11-25 23:35:22 -07:00
// TODO(?): implement whitelist - list of directories which model is allowed to work in
if path == "" {
path = "."
}
ok , res := isPathWithinCWD ( path )
if ! ok {
return resultToJson ( * res )
}
files , err := os . ReadDir ( path )
2023-11-25 17:55:18 -07:00
if err != nil {
return resultToJson ( FunctionResult {
Message : err . Error ( ) ,
} )
}
var dirContents [ ] map [ string ] interface { }
for _ , f := range files {
info , _ := f . Info ( )
2023-11-25 23:37:54 -07:00
name := f . Name ( )
2023-11-26 00:32:28 -07:00
if strings . HasPrefix ( name , "." ) {
// skip hidden files
continue
}
2023-11-25 23:37:54 -07:00
entryType := "file"
size := info . Size ( )
2023-11-25 17:55:18 -07:00
if info . IsDir ( ) {
2023-11-25 23:37:54 -07:00
name += "/"
entryType = "dir"
2023-11-25 17:55:18 -07:00
subdirfiles , _ := os . ReadDir ( filepath . Join ( "." , path , info . Name ( ) ) )
2023-11-25 23:37:54 -07:00
size = int64 ( len ( subdirfiles ) )
2023-11-25 17:55:18 -07:00
}
dirContents = append ( dirContents , map [ string ] interface { } {
2023-11-25 23:37:54 -07:00
"name" : name ,
"type" : entryType ,
"size" : size ,
2023-11-25 17:55:18 -07:00
} )
}
return resultToJson ( FunctionResult { Result : dirContents } )
}
2023-11-26 00:32:28 -07:00
func ReadFile ( path string ) string {
ok , res := isPathWithinCWD ( path )
if ! ok {
return resultToJson ( * res )
}
data , err := os . ReadFile ( path )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not read path: %s" , err . Error ( ) ) } )
}
2023-11-26 03:43:47 -07:00
lines := strings . Split ( string ( data ) , "\n" )
content := strings . Builder { }
for i , line := range lines {
content . WriteString ( fmt . Sprintf ( "%d\t%s\n" , i + 1 , line ) )
}
2023-11-26 00:32:28 -07:00
return resultToJson ( FunctionResult {
2023-11-26 03:43:47 -07:00
Result : content . String ( ) ,
2023-11-26 00:32:28 -07:00
} )
}
func WriteFile ( path string , content string ) string {
ok , res := isPathWithinCWD ( path )
if ! ok {
return resultToJson ( * res )
}
err := os . WriteFile ( path , [ ] byte ( content ) , 0644 )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not write to path: %s" , err . Error ( ) ) } )
}
return resultToJson ( FunctionResult { } )
}
2023-11-26 03:51:06 -07:00
2023-11-26 10:33:33 -07:00
func FileInsertLines ( path string , startLine int , content string ) string {
2023-11-26 03:51:06 -07:00
ok , res := isPathWithinCWD ( path )
if ! ok {
return resultToJson ( * res )
}
// Read the existing file's content
data , err := os . ReadFile ( path )
if err != nil {
if ! os . IsNotExist ( err ) {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not read path: %s" , err . Error ( ) ) } )
}
_ , err = os . Create ( path )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not create new file: %s" , err . Error ( ) ) } )
}
data = [ ] byte { }
}
2023-11-26 10:33:33 -07:00
if startLine < 1 {
return resultToJson ( FunctionResult { Message : "start_line cannot be less than 1" } )
2023-11-26 03:51:06 -07:00
}
lines := strings . Split ( string ( data ) , "\n" )
contentLines := strings . Split ( strings . Trim ( content , "\n" ) , "\n" )
2023-11-26 10:33:33 -07:00
before := lines [ : startLine - 1 ]
after := lines [ startLine - 1 : ]
lines = append ( before , append ( contentLines , after ... ) ... )
newContent := strings . Join ( lines , "\n" )
// Join the lines and write back to the file
err = os . WriteFile ( path , [ ] byte ( newContent ) , 0644 )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not write to path: %s" , err . Error ( ) ) } )
}
return resultToJson ( FunctionResult { Result : newContent } )
}
func FileReplaceLines ( path string , startLine int , endLine int , content string ) string {
ok , res := isPathWithinCWD ( path )
if ! ok {
return resultToJson ( * res )
}
// Read the existing file's content
data , err := os . ReadFile ( path )
if err != nil {
if ! os . IsNotExist ( err ) {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not read path: %s" , err . Error ( ) ) } )
2023-11-26 03:51:06 -07:00
}
2023-11-26 10:33:33 -07:00
_ , err = os . Create ( path )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not create new file: %s" , err . Error ( ) ) } )
}
data = [ ] byte { }
}
if startLine < 1 {
return resultToJson ( FunctionResult { Message : "start_line cannot be less than 1" } )
}
lines := strings . Split ( string ( data ) , "\n" )
contentLines := strings . Split ( strings . Trim ( content , "\n" ) , "\n" )
if endLine == 0 || endLine > len ( lines ) {
endLine = len ( lines )
}
before := lines [ : startLine - 1 ]
after := lines [ endLine : ]
lines = append ( before , append ( contentLines , after ... ) ... )
newContent := strings . Join ( lines , "\n" )
// Join the lines and write back to the file
err = os . WriteFile ( path , [ ] byte ( newContent ) , 0644 )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not write to path: %s" , err . Error ( ) ) } )
}
return resultToJson ( FunctionResult { Result : newContent } )
}
2023-11-26 03:51:06 -07:00
2023-11-26 10:33:33 -07:00
func FileRemoveLines ( path string , startLine int , endLine int ) string {
ok , res := isPathWithinCWD ( path )
if ! ok {
return resultToJson ( * res )
}
// Read the existing file's content
data , err := os . ReadFile ( path )
if err != nil {
if ! os . IsNotExist ( err ) {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not read path: %s" , err . Error ( ) ) } )
2023-11-26 03:51:06 -07:00
}
2023-11-26 10:33:33 -07:00
_ , err = os . Create ( path )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not create new file: %s" , err . Error ( ) ) } )
2023-11-26 03:51:06 -07:00
}
2023-11-26 10:33:33 -07:00
data = [ ] byte { }
}
if startLine < 1 {
return resultToJson ( FunctionResult { Message : "start_line cannot be less than 1" } )
}
lines := strings . Split ( string ( data ) , "\n" )
if endLine == 0 || endLine > len ( lines ) {
endLine = len ( lines )
2023-11-26 03:51:06 -07:00
}
2023-11-26 10:33:33 -07:00
lines = append ( lines [ : startLine - 1 ] , lines [ endLine : ] ... )
2023-11-26 03:51:06 -07:00
newContent := strings . Join ( lines , "\n" )
// Join the lines and write back to the file
err = os . WriteFile ( path , [ ] byte ( newContent ) , 0644 )
if err != nil {
return resultToJson ( FunctionResult { Message : fmt . Sprintf ( "Could not write to path: %s" , err . Error ( ) ) } )
}
return resultToJson ( FunctionResult { Result : newContent } )
}