diff --git a/cli/init.go b/cli/init.go index cab3dc2e8..6a279d6ba 100644 --- a/cli/init.go +++ b/cli/init.go @@ -28,6 +28,7 @@ import ( ) var name string +var sfnType string // initCmd represents the init command var initCmd = &cobra.Command{ @@ -48,7 +49,22 @@ var initCmd = &cobra.Command{ name = strings.ReplaceAll(name, " ", "_") // create app.go fname := filepath.Join(name, defaultSFNSourceFile) - contentTmpl := golang.InitTmpl + // sfn content template + var contentTmpl, testTmpl []byte + // sfn type + switch sfnType { + case "llm": + contentTmpl = golang.InitLLMTmpl + testTmpl = golang.InitLLMTestTmpl + case "normal": + contentTmpl = golang.InitTmpl + testTmpl = golang.InitTestTmpl + default: + log.WarningStatusEvent(os.Stdout, "The type of Stream Function is not supported, use the default type: llm") + contentTmpl = golang.InitLLMTmpl + testTmpl = golang.InitLLMTestTmpl + + } if err := file.PutContents(fname, contentTmpl); err != nil { log.FailureStatusEvent(os.Stdout, "Write stream function into app.go file failure with the error: %v", err) return @@ -56,7 +72,7 @@ var initCmd = &cobra.Command{ // create app_test.go testName := filepath.Join(name, defaultSFNTestSourceFile) - if err := file.PutContents(testName, golang.InitTestTmpl); err != nil { + if err := file.PutContents(testName, testTmpl); err != nil { log.FailureStatusEvent(os.Stdout, "Write unittest tmpl into app_test.go file failure with the error: %v", err) return } @@ -79,4 +95,5 @@ func init() { rootCmd.AddCommand(initCmd) initCmd.Flags().StringVarP(&name, "name", "n", "", "The name of Stream Function") + initCmd.Flags().StringVarP(&sfnType, "type", "t", "llm", "The type of Stream Function, support normal and llm") } diff --git a/cli/serverless/golang/template.go b/cli/serverless/golang/template.go index d1767dd0b..64384a501 100644 --- a/cli/serverless/golang/template.go +++ b/cli/serverless/golang/template.go @@ -12,9 +12,15 @@ var MainFuncTmpl []byte //go:embed templates/init.tmpl var InitTmpl []byte +//go:embed templates/init_llm.tmpl +var InitLLMTmpl []byte + //go:embed templates/init_test.tmpl var InitTestTmpl []byte +//go:embed templates/init_llm_test.tmpl +var InitLLMTestTmpl []byte + //go:embed templates/wasi_main.tmpl var WasiMainFuncTmpl []byte diff --git a/cli/serverless/golang/templates/init.tmpl b/cli/serverless/golang/templates/init.tmpl index 1c81aeb50..aa24478b8 100644 --- a/cli/serverless/golang/templates/init.tmpl +++ b/cli/serverless/golang/templates/init.tmpl @@ -2,7 +2,7 @@ package main import ( "fmt" - "log/slog" + "strings" "github.com/yomorun/yomo/serverless" ) @@ -16,55 +16,25 @@ func Init() error { return nil } -// Description outlines the functionality for the LLM Function Calling feature. -// It provides a detailed description of the function's purpose, essential for -// integration with LLM Function Calling. The presence of this function and its -// return value make the function discoverable and callable within the LLM -// ecosystem. For more information on Function Calling, refer to the OpenAI -// documentation at: https://platform.openai.com/docs/guides/function-calling -func Description() string { - return `Get current weather for a given city. If no city is provided, you - should ask to clarify the city. If the city name is given, you should - convert the city name to Latitude and Longitude geo coordinates, keeping - Latitude and Longitude in decimal format.` -} - -// InputSchema defines the argument structure for LLM Function Calling. It -// utilizes jsonschema tags to detail the definition. For jsonschema in Go, -// see https://github.com/invopop/jsonschema. -func InputSchema() any { - return &LLMArguments{} -} - -// LLMArguments defines the arguments for the LLM Function Calling. These -// arguments are combined to form a prompt automatically. -type LLMArguments struct { - City string `json:"city" jsonschema:"description=The city name to get the weather for,required"` - Latitude float64 `json:"latitude" jsonschema:"description=The latitude of the city, in decimal format, range should be in (-90, 90)"` - Longitude float64 `json:"longitude" jsonschema:"description=The longitude of the city, in decimal format, range should be in (-180, 180)"` -} - // DataTags specifies the data tags to which this serverless function // subscribes, essential for data reception. Upon receiving data with these // tags, the Handler function is triggered. func DataTags() []uint32 { - return []uint32{0x30} + return []uint32{0x33} } // Handler orchestrates the core processing logic of this function. -// - ctx.ReadLLMArguments() parses LLM Function Calling Arguments (skip if none). -// - ctx.WriteLLMResult() sends the retrieval result back to LLM. // - ctx.Tag() identifies the tag of the incoming data. // - ctx.Data() accesses the raw data. // - ctx.Write() forwards processed data downstream. func Handler(ctx serverless.Context) { - var p LLMArguments - // deserilize the arguments from llm tool_call response - ctx.ReadLLMArguments(&p) - - // invoke the open weather map api and return the result back to LLM - result := fmt.Sprintf("The current weather in %s (%f,%f) is sunny", p.City, p.Latitude, p.Longitude) - ctx.WriteLLMResult(result) - - slog.Info("get-weather", "city", p.City, "rag", result) + data := ctx.Data() + fmt.Printf("<< sfn received[%d Bytes]: %s\n", len(data), data) + output := strings.ToUpper(string(data)) + err := ctx.Write(0x34, []byte(output)) + if err != nil { + fmt.Printf(">> sfn write error: %v\n", err) + return + } + fmt.Printf(">> sfn written[%d Bytes]: %s\n", len(output), output) } diff --git a/cli/serverless/golang/templates/init_llm.tmpl b/cli/serverless/golang/templates/init_llm.tmpl new file mode 100644 index 000000000..1c81aeb50 --- /dev/null +++ b/cli/serverless/golang/templates/init_llm.tmpl @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "log/slog" + + "github.com/yomorun/yomo/serverless" +) + +// Init is an optional function invoked during the initialization phase of the +// sfn instance. It's designed for setup tasks like global variable +// initialization, establishing database connections, or loading models into +// GPU memory. If initialization fails, the sfn instance will halt and terminate. +// This function can be omitted if no initialization tasks are needed. +func Init() error { + return nil +} + +// Description outlines the functionality for the LLM Function Calling feature. +// It provides a detailed description of the function's purpose, essential for +// integration with LLM Function Calling. The presence of this function and its +// return value make the function discoverable and callable within the LLM +// ecosystem. For more information on Function Calling, refer to the OpenAI +// documentation at: https://platform.openai.com/docs/guides/function-calling +func Description() string { + return `Get current weather for a given city. If no city is provided, you + should ask to clarify the city. If the city name is given, you should + convert the city name to Latitude and Longitude geo coordinates, keeping + Latitude and Longitude in decimal format.` +} + +// InputSchema defines the argument structure for LLM Function Calling. It +// utilizes jsonschema tags to detail the definition. For jsonschema in Go, +// see https://github.com/invopop/jsonschema. +func InputSchema() any { + return &LLMArguments{} +} + +// LLMArguments defines the arguments for the LLM Function Calling. These +// arguments are combined to form a prompt automatically. +type LLMArguments struct { + City string `json:"city" jsonschema:"description=The city name to get the weather for,required"` + Latitude float64 `json:"latitude" jsonschema:"description=The latitude of the city, in decimal format, range should be in (-90, 90)"` + Longitude float64 `json:"longitude" jsonschema:"description=The longitude of the city, in decimal format, range should be in (-180, 180)"` +} + +// DataTags specifies the data tags to which this serverless function +// subscribes, essential for data reception. Upon receiving data with these +// tags, the Handler function is triggered. +func DataTags() []uint32 { + return []uint32{0x30} +} + +// Handler orchestrates the core processing logic of this function. +// - ctx.ReadLLMArguments() parses LLM Function Calling Arguments (skip if none). +// - ctx.WriteLLMResult() sends the retrieval result back to LLM. +// - ctx.Tag() identifies the tag of the incoming data. +// - ctx.Data() accesses the raw data. +// - ctx.Write() forwards processed data downstream. +func Handler(ctx serverless.Context) { + var p LLMArguments + // deserilize the arguments from llm tool_call response + ctx.ReadLLMArguments(&p) + + // invoke the open weather map api and return the result back to LLM + result := fmt.Sprintf("The current weather in %s (%f,%f) is sunny", p.City, p.Latitude, p.Longitude) + ctx.WriteLLMResult(result) + + slog.Info("get-weather", "city", p.City, "rag", result) +} diff --git a/cli/serverless/golang/templates/init_llm_test.tmpl b/cli/serverless/golang/templates/init_llm_test.tmpl new file mode 100644 index 000000000..8ad577bfd --- /dev/null +++ b/cli/serverless/golang/templates/init_llm_test.tmpl @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "reflect" + "testing" + + "github.com/yomorun/yomo/ai" + "github.com/yomorun/yomo/serverless/mock" +) + +func TestHandler(t *testing.T) { + tests := []struct { + name string + ctx *mock.MockContext + // want is the expected data and tag that be written by ctx.Write + want []mock.WriteRecord + }{ + { + name: "get weather", + ctx: mock.NewMockContext([]byte(`{"arguments":"{\"city\":\"New York\",\"latitude\":40.7128,\"longitude\":-74.0060}"}`), 0x33), + want: []mock.WriteRecord{ + {Data: []byte(`{"result":"The current weather in New York (40.712800,-74.006000) is sunny","arguments":"{\"city\":\"New York\",\"latitude\":40.7128,\"longitude\":-74.0060}","is_ok":true}`), Tag: ai.ReducerTag}, + }, + }, + // TODO: add more test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Handler(tt.ctx) + got := tt.ctx.RecordsWritten() + + fmt.Println(string(got[0].Data)) + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("TestHandler got: %v, want: %v", got, tt.want) + } + }) + } +} diff --git a/cli/serverless/golang/templates/init_test.tmpl b/cli/serverless/golang/templates/init_test.tmpl index 8ad577bfd..5cec7d053 100644 --- a/cli/serverless/golang/templates/init_test.tmpl +++ b/cli/serverless/golang/templates/init_test.tmpl @@ -5,7 +5,6 @@ import ( "reflect" "testing" - "github.com/yomorun/yomo/ai" "github.com/yomorun/yomo/serverless/mock" ) @@ -17,10 +16,10 @@ func TestHandler(t *testing.T) { want []mock.WriteRecord }{ { - name: "get weather", - ctx: mock.NewMockContext([]byte(`{"arguments":"{\"city\":\"New York\",\"latitude\":40.7128,\"longitude\":-74.0060}"}`), 0x33), + name: "upper", + ctx: mock.NewMockContext([]byte("hello"), 0x33), want: []mock.WriteRecord{ - {Data: []byte(`{"result":"The current weather in New York (40.712800,-74.006000) is sunny","arguments":"{\"city\":\"New York\",\"latitude\":40.7128,\"longitude\":-74.0060}","is_ok":true}`), Tag: ai.ReducerTag}, + {Data: []byte("HELLO"), Tag: 0x34}, }, }, // TODO: add more test cases.