mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 16:38:15 -05:00
feat: stub basic control plane functionality (#9)
Stub's out some basic control plane functionality. This also required setting up some Source and Tools initialization.
This commit is contained in:
@@ -116,7 +116,10 @@ func run(cmd *Command) error {
|
||||
}
|
||||
|
||||
// run server
|
||||
s := server.NewServer(cmd.cfg)
|
||||
s, err := server.NewServer(cmd.cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Toolbox failed to start with the following error: %w", err)
|
||||
}
|
||||
err = s.ListenAndServe(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Toolbox crashed with the following error: %w", err)
|
||||
|
||||
@@ -196,6 +196,7 @@ func TestParseToolFile(t *testing.T) {
|
||||
`,
|
||||
wantSources: sources.Configs{
|
||||
"my-pg-instance": sources.CloudSQLPgConfig{
|
||||
Name: "my-pg-instance",
|
||||
Kind: sources.CloudSQLPgKind,
|
||||
Project: "my-project",
|
||||
Region: "my-region",
|
||||
@@ -205,6 +206,7 @@ func TestParseToolFile(t *testing.T) {
|
||||
},
|
||||
wantTools: tools.Configs{
|
||||
"example_tool": tools.CloudSQLPgGenericConfig{
|
||||
Name: "example_tool",
|
||||
Kind: tools.CloudSQLPgSQLGenericKind,
|
||||
Source: "my-pg-instance",
|
||||
Description: "some description",
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,12 +4,14 @@ go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,6 +1,10 @@
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
||||
61
internal/server/api.go
Normal file
61
internal/server/api.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// apiRouter creates a router that represents the routes under /api
|
||||
func apiRouter(s *Server) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Get("/toolset/{toolsetName}", toolsetHandler(s))
|
||||
|
||||
// TODO: make this POST
|
||||
r.Get("/tool/{toolName}", toolHandler(s))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func toolsetHandler(s *Server) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
toolsetName := chi.URLParam(r, "toolsetName")
|
||||
_, _ = w.Write([]byte(fmt.Sprintf("Stub for toolset %s manifest!", toolsetName)))
|
||||
}
|
||||
}
|
||||
|
||||
func toolHandler(s *Server) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
toolName := chi.URLParam(r, "toolName")
|
||||
tool, ok := s.tools[toolName]
|
||||
if !ok {
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(fmt.Sprintf("Tool %q does not exist", toolName)))
|
||||
return
|
||||
}
|
||||
|
||||
res, err := tool.Invoke()
|
||||
if err != nil {
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(fmt.Sprintf("Tool Result: %s", res)))
|
||||
}
|
||||
}
|
||||
@@ -24,28 +24,60 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
// Server contains info for running an instance of Toolbox. Should be instantiated with NewServer().
|
||||
type Server struct {
|
||||
conf Config
|
||||
router chi.Router
|
||||
conf Config
|
||||
root chi.Router
|
||||
|
||||
sources map[string]sources.Source
|
||||
tools map[string]tools.Tool
|
||||
}
|
||||
|
||||
// NewServer returns a Server object based on provided Config.
|
||||
func NewServer(cfg Config) *Server {
|
||||
func NewServer(cfg Config) (*Server, error) {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("🧰 Hello world! 🧰"))
|
||||
})
|
||||
|
||||
s := &Server{
|
||||
conf: cfg,
|
||||
router: r,
|
||||
// initalize and validate the sources
|
||||
sources := make(map[string]sources.Source)
|
||||
for name, sc := range cfg.SourceConfigs {
|
||||
s, err := sc.Initialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to initialize tool %s: %w", name, err)
|
||||
}
|
||||
sources[name] = s
|
||||
}
|
||||
return s
|
||||
fmt.Printf("Initalized %d sources.\n", len(sources))
|
||||
|
||||
// initalize and validate the tools
|
||||
tools := make(map[string]tools.Tool)
|
||||
for name, tc := range cfg.ToolConfigs {
|
||||
t, err := tc.Initialize(sources)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to initialize tool %s: %w", name, err)
|
||||
}
|
||||
tools[name] = t
|
||||
}
|
||||
fmt.Printf("Initalized %d tools.\n", len(tools))
|
||||
|
||||
s := &Server{
|
||||
conf: cfg,
|
||||
root: r,
|
||||
sources: sources,
|
||||
tools: tools,
|
||||
}
|
||||
r.Mount("/api", apiRouter(s))
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ListenAndServe starts an HTTP server for the given Server instance.
|
||||
@@ -60,5 +92,5 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to open listener for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return http.Serve(l, s.router)
|
||||
return http.Serve(l, s.root)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ func TestServe(t *testing.T) {
|
||||
Address: addr,
|
||||
Port: port,
|
||||
}
|
||||
s := server.NewServer(cfg)
|
||||
s, err := server.NewServer(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable initialize server!")
|
||||
}
|
||||
|
||||
// start server in background
|
||||
errCh := make(chan error)
|
||||
|
||||
@@ -20,6 +20,7 @@ const CloudSQLPgKind string = "cloud-sql-postgres"
|
||||
var _ Config = CloudSQLPgConfig{}
|
||||
|
||||
type CloudSQLPgConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Project string `yaml:"project"`
|
||||
Region string `yaml:"region"`
|
||||
@@ -30,3 +31,18 @@ type CloudSQLPgConfig struct {
|
||||
func (r CloudSQLPgConfig) sourceKind() string {
|
||||
return CloudSQLPgKind
|
||||
}
|
||||
|
||||
func (r CloudSQLPgConfig) Initialize() (Source, error) {
|
||||
s := CloudSQLPgSource{
|
||||
Name: r.Name,
|
||||
Kind: CloudSQLPgKind,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var _ Source = CloudSQLPgSource{}
|
||||
|
||||
type CloudSQLPgSource struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ func TestParseFromYaml(t *testing.T) {
|
||||
`,
|
||||
want: sources.Configs{
|
||||
"my-pg-instance": sources.CloudSQLPgConfig{
|
||||
Name: "my-pg-instance",
|
||||
Kind: sources.CloudSQLPgKind,
|
||||
Project: "my-project",
|
||||
Region: "my-region",
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
type Config interface {
|
||||
sourceKind() string
|
||||
Initialize() (Source, error)
|
||||
}
|
||||
|
||||
// validate interface
|
||||
@@ -48,7 +49,7 @@ func (c *Configs) UnmarshalYAML(node *yaml.Node) error {
|
||||
}
|
||||
switch k.Kind {
|
||||
case CloudSQLPgKind:
|
||||
actual := CloudSQLPgConfig{}
|
||||
actual := CloudSQLPgConfig{Name: name}
|
||||
if err := n.Decode(&actual); err != nil {
|
||||
return fmt.Errorf("unable to parse as %q: %w", k.Kind, err)
|
||||
}
|
||||
@@ -60,3 +61,6 @@ func (c *Configs) UnmarshalYAML(node *yaml.Node) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Source interface {
|
||||
}
|
||||
|
||||
@@ -14,12 +14,19 @@
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
)
|
||||
|
||||
const CloudSQLPgSQLGenericKind string = "cloud-sql-postgres-generic"
|
||||
|
||||
// validate interface
|
||||
var _ Config = CloudSQLPgGenericConfig{}
|
||||
|
||||
type CloudSQLPgGenericConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Source string `yaml:"source"`
|
||||
Description string `yaml:"description"`
|
||||
@@ -30,3 +37,38 @@ type CloudSQLPgGenericConfig struct {
|
||||
func (r CloudSQLPgGenericConfig) toolKind() string {
|
||||
return CloudSQLPgSQLGenericKind
|
||||
}
|
||||
|
||||
func (r CloudSQLPgGenericConfig) Initialize(srcs map[string]sources.Source) (Tool, error) {
|
||||
// verify source exists
|
||||
rawS, ok := srcs[r.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No source named %q configured!", r.Source)
|
||||
}
|
||||
|
||||
// verify the source is the right kind
|
||||
s, ok := rawS.(sources.CloudSQLPgSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Sources for %q tools must be of kind %q!", CloudSQLPgSQLGenericKind, sources.CloudSQLPgKind)
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
t := CloudSQLPgGenericTool{
|
||||
Name: r.Name,
|
||||
Kind: CloudSQLPgSQLGenericKind,
|
||||
Source: s,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ Tool = CloudSQLPgGenericTool{}
|
||||
|
||||
type CloudSQLPgGenericTool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Source sources.CloudSQLPgSource
|
||||
}
|
||||
|
||||
func (t CloudSQLPgGenericTool) Invoke() (string, error) {
|
||||
return fmt.Sprintf("Stub tool call for %q!", t.Name), nil
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func TestParseFromYaml(t *testing.T) {
|
||||
`,
|
||||
want: tools.Configs{
|
||||
"example_tool": tools.CloudSQLPgGenericConfig{
|
||||
Name: "example_tool",
|
||||
Kind: tools.CloudSQLPgSQLGenericKind,
|
||||
Source: "my-pg-instance",
|
||||
Description: "some description",
|
||||
|
||||
@@ -17,16 +17,18 @@ package tools
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// SourceConfigs is a type used to allow unmarshal of the data source config map
|
||||
type Configs map[string]Config
|
||||
|
||||
type Config interface {
|
||||
toolKind() string
|
||||
Initialize(map[string]sources.Source) (Tool, error)
|
||||
}
|
||||
|
||||
// SourceConfigs is a type used to allow unmarshal of the data source config map
|
||||
type Configs map[string]Config
|
||||
|
||||
// validate interface
|
||||
var _ yaml.Unmarshaler = &Configs{}
|
||||
|
||||
@@ -48,7 +50,7 @@ func (c *Configs) UnmarshalYAML(node *yaml.Node) error {
|
||||
}
|
||||
switch k.Kind {
|
||||
case CloudSQLPgSQLGenericKind:
|
||||
actual := CloudSQLPgGenericConfig{}
|
||||
actual := CloudSQLPgGenericConfig{Name: name}
|
||||
if err := n.Decode(&actual); err != nil {
|
||||
return fmt.Errorf("unable to parse as %q: %w", k.Kind, err)
|
||||
}
|
||||
@@ -67,3 +69,7 @@ type Parameter struct {
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
}
|
||||
|
||||
type Tool interface {
|
||||
Invoke() (string, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user