diff --git a/README.md b/README.md index 6cdd05b..aa9869a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,28 @@ The typical use case is as a RAG component: Create embeddings for your text coll ## Quick Start +### 0. Discover the API + +```bash +# Get the service manifest with all available endpoints +curl http://localhost:8880/ +# or +curl http://localhost:8880/v1 + +# Response includes: +# - Service name, version, and description (customizable via env variables) +# - Available API versions +# - Authentication schemes +# - Complete list of endpoints with descriptions +``` + +**Customize the manifest**: Set environment variables in your `.env` file: +```bash +SERVICE_API_NAME="My Custom API" +SERVICE_API_DESCRIPTION="My custom description" +SERVICE_API_DOC_URL="https://my-docs.example.com" +``` + ### 1. Start with Docker ```bash @@ -46,6 +68,11 @@ The typical use case is as a RAG component: Create embeddings for your text coll # Start services (includes PostgreSQL with pgvector) docker-compose up -d +# Discover available API endpoints (at root or versioned endpoint) +curl http://localhost:8880/ +# or +curl http://localhost:8880/v1 + # Access the API documentation curl http://localhost:8880/docs ``` diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 3ed7bba..3589c6c 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -9,6 +9,7 @@ import ( "net/http" "github.com/jackc/pgx/v5/pgxpool" + "github.com/mpilhlt/embapi/internal/models" huma "github.com/danielgtaylor/huma/v2" ) @@ -17,14 +18,16 @@ type contextKey string // Context keys const ( - PoolKey = contextKey("dbPool") - KeyGenKey = contextKey("keyGen") + PoolKey = contextKey("dbPool") + KeyGenKey = contextKey("keyGen") + OptionsKey = contextKey("options") ) // Error responses var ( - ErrPoolNotFound = errors.New("database connection pool not found in context") - ErrKeyGenNotFound = errors.New("key generator not found in context") + ErrPoolNotFound = errors.New("database connection pool not found in context") + ErrKeyGenNotFound = errors.New("key generator not found in context") + ErrOptionsNotFound = errors.New("options not found in context") ) // The type definitions and functions that follow are used to @@ -45,8 +48,13 @@ func (s StandardKeyGen) RandomKey(len int) (string, error) { } // AddRoutes adds all the routes to the API -func AddRoutes(pool *pgxpool.Pool, keyGen RandomKeyGenerator, api huma.API) error { - err := RegisterUsersRoutes(pool, keyGen, api) +func AddRoutes(pool *pgxpool.Pool, keyGen RandomKeyGenerator, api huma.API, options *models.Options) error { + err := RegisterManifestRoutes(pool, api, options) + if err != nil { + fmt.Printf(" Unable to register Manifest routes: %v\n", err) + return err + } + err = RegisterUsersRoutes(pool, keyGen, api) if err != nil { fmt.Printf(" Unable to register Users routes: %v\n", err) return err @@ -111,6 +119,17 @@ func addKeyGenToContext[I any, O any](keyGen RandomKeyGenerator, next func(conte } } +// Middleware to add the options to the context +func addOptionsToContext[I any, O any](options *models.Options, next func(context.Context, *I) (*O, error)) func(context.Context, *I) (*O, error) { + return func(ctx context.Context, input *I) (*O, error) { + if options == nil { + return nil, fmt.Errorf("provided options is nil") + } + ctx = context.WithValue(ctx, OptionsKey, options) + return next(ctx, input) + } +} + // Get the database connection pool from the context // (exported helper function so that blackbox testing can access it) func GetDBPool(ctx context.Context) (*pgxpool.Pool, error) { @@ -130,3 +149,13 @@ func GetKeyGen(ctx context.Context) (RandomKeyGenerator, error) { } return keyGen, nil } + +// Get the options from the context +// (exported helper function so that blackbox testing can access it) +func GetOptions(ctx context.Context) (*models.Options, error) { + options, ok := ctx.Value(OptionsKey).(*models.Options) + if !ok { + return nil, huma.NewError(http.StatusInternalServerError, ErrOptionsNotFound.Error()) + } + return options, nil +} diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index f9666dd..b91351b 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -181,7 +181,7 @@ func startTestServer(t *testing.T, pool *pgxpool.Pool, keyGen handlers.RandomKey */ // Create a new router & API - config := huma.DefaultConfig("DHaMPS Vector Database API", "0.0.1") + config := huma.DefaultConfig(models.APIName, models.APIVersion) config.Components.SecuritySchemes = auth.Config router := http.NewServeMux() api := humago.New(router, config) @@ -191,7 +191,7 @@ func startTestServer(t *testing.T, pool *pgxpool.Pool, keyGen handlers.RandomKey api.UseMiddleware(auth.EmbAPIKeyReaderAuth(api, pool, &options)) api.UseMiddleware(auth.AuthTermination(api, &options)) - err := handlers.AddRoutes(pool, keyGen, api) + err := handlers.AddRoutes(pool, keyGen, api, &options) if err != nil { fmt.Printf("Unable to add routes to API: %v", err) return err, func() {} diff --git a/internal/handlers/manifest.go b/internal/handlers/manifest.go new file mode 100644 index 0000000..6d9a02f --- /dev/null +++ b/internal/handlers/manifest.go @@ -0,0 +1,283 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/mpilhlt/embapi/internal/models" + + "github.com/danielgtaylor/huma/v2" + "github.com/jackc/pgx/v5/pgxpool" +) + +// getManifestFunc returns the service manifest +func getManifestFunc(ctx context.Context, input *models.GetManifestRequest) (*models.GetManifestResponse, error) { + // Get options from context + options, err := GetOptions(ctx) + if err != nil { + return nil, err + } + + // Use configured values or fall back to defaults + apiName := models.APIName + if options.APIName != "" { + apiName = options.APIName + } + + apiDescription := models.APIDescription + if options.APIDescription != "" { + apiDescription = options.APIDescription + } + + apiDocURL := "https://mpilhlt.github.io/embapi/" + if options.APIDocURL != "" { + apiDocURL = options.APIDocURL + } + + // Build the manifest + // Note: The endpoint list is manually maintained to provide comprehensive API documentation. + // While it could be generated dynamically from registered routes, the manual approach ensures + // accurate descriptions and proper grouping, and allows filtering of internal/test endpoints. + manifest := models.ServiceManifest{ + Name: apiName, + Versions: []string{"v1"}, + Description: apiDescription, + Documentation: apiDocURL, + ServiceVersion: models.APIVersion, + Authentication: map[string]interface{}{ + "adminAuth": map[string]interface{}{ + "type": "http", + "scheme": "bearer", + "description": "Admin API key for administrative operations", + }, + "ownerAuth": map[string]interface{}{ + "type": "http", + "scheme": "bearer", + "description": "Owner API key for resource management", + }, + "editorAuth": map[string]interface{}{ + "type": "http", + "scheme": "bearer", + "description": "Editor API key for editing shared resources", + }, + "readerAuth": map[string]interface{}{ + "type": "http", + "scheme": "bearer", + "description": "Reader API key for read-only access to shared resources", + }, + }, + Endpoints: []models.EndpointInfo{ + // Users + { + Path: "/v1/users", + Methods: []string{"GET", "POST"}, + Description: "Manage users", + Tags: []string{"users"}, + }, + { + Path: "/v1/users/{user_handle}", + Methods: []string{"GET", "PUT", "DELETE"}, + Description: "Manage a specific user", + Tags: []string{"users"}, + }, + // Projects + { + Path: "/v1/projects/{user_handle}", + Methods: []string{"GET", "POST"}, + Description: "List or create projects for a user", + Tags: []string{"projects"}, + }, + { + Path: "/v1/projects/{user_handle}/{project_handle}", + Methods: []string{"GET", "PUT", "DELETE"}, + Description: "Manage a specific project", + Tags: []string{"projects"}, + }, + { + Path: "/v1/projects/{user_handle}/{project_handle}/share", + Methods: []string{"GET", "POST"}, + Description: "Manage project sharing", + Tags: []string{"projects", "sharing"}, + }, + { + Path: "/v1/projects/{user_handle}/{project_handle}/share/{unshare_with_handle}", + Methods: []string{"DELETE"}, + Description: "Remove sharing access to a project", + Tags: []string{"projects", "sharing"}, + }, + { + Path: "/v1/projects/{user_handle}/{project_handle}/shared-with", + Methods: []string{"GET"}, + Description: "List users with whom a project is shared", + Tags: []string{"projects", "sharing"}, + }, + { + Path: "/v1/projects/{user_handle}/{project_handle}/transfer-ownership", + Methods: []string{"POST"}, + Description: "Transfer ownership of a project to another user", + Tags: []string{"projects"}, + }, + // Embeddings + { + Path: "/v1/embeddings/{user_handle}/{project_handle}", + Methods: []string{"GET", "POST", "DELETE"}, + Description: "Manage embeddings in a project", + Tags: []string{"embeddings"}, + }, + { + Path: "/v1/embeddings/{user_handle}/{project_handle}/{text_id}", + Methods: []string{"GET", "PUT", "DELETE"}, + Description: "Manage a specific embedding by text ID", + Tags: []string{"embeddings"}, + }, + // Similarity Search + { + Path: "/v1/similars/{user_handle}/{project_handle}/{text_id}", + Methods: []string{"GET"}, + Description: "Find similar documents by text ID", + Tags: []string{"similarity"}, + }, + { + Path: "/v1/similars/{user_handle}/{project_handle}", + Methods: []string{"POST"}, + Description: "Find similar documents by posting a vector", + Tags: []string{"similarity"}, + }, + // LLM Service Definitions + { + Path: "/v1/llm-definitions/{user_handle}", + Methods: []string{"GET", "POST"}, + Description: "List or create LLM service definitions", + Tags: []string{"llm-services"}, + }, + { + Path: "/v1/llm-definitions/{user_handle}/{definition_handle}", + Methods: []string{"GET", "PUT", "DELETE"}, + Description: "Manage a specific LLM service definition", + Tags: []string{"llm-services"}, + }, + { + Path: "/v1/llm-definitions/{user_handle}/{definition_handle}/share", + Methods: []string{"POST"}, + Description: "Share an LLM service definition with other users", + Tags: []string{"llm-services", "sharing"}, + }, + { + Path: "/v1/llm-definitions/{user_handle}/{definition_handle}/share/{unshare_with_handle}", + Methods: []string{"DELETE"}, + Description: "Remove sharing access to an LLM service definition", + Tags: []string{"llm-services", "sharing"}, + }, + { + Path: "/v1/llm-definitions/{user_handle}/{definition_handle}/shared-with", + Methods: []string{"GET"}, + Description: "List users with whom an LLM service definition is shared", + Tags: []string{"llm-services", "sharing"}, + }, + // LLM Service Instances + { + Path: "/v1/llm-instances/{user_handle}", + Methods: []string{"GET", "POST"}, + Description: "List or create LLM service instances", + Tags: []string{"llm-services"}, + }, + { + Path: "/v1/llm-instances/{user_handle}/from-definition", + Methods: []string{"POST"}, + Description: "Create an LLM service instance from a definition", + Tags: []string{"llm-services"}, + }, + { + Path: "/v1/llm-instances/{user_handle}/{instance_handle}", + Methods: []string{"GET", "PUT", "DELETE"}, + Description: "Manage a specific LLM service instance", + Tags: []string{"llm-services"}, + }, + { + Path: "/v1/llm-instances/{user_handle}/{instance_handle}/share", + Methods: []string{"POST"}, + Description: "Share an LLM service instance with other users", + Tags: []string{"llm-services", "sharing"}, + }, + { + Path: "/v1/llm-instances/{user_handle}/{instance_handle}/share/{unshare_with_handle}", + Methods: []string{"DELETE"}, + Description: "Remove sharing access to an LLM service instance", + Tags: []string{"llm-services", "sharing"}, + }, + { + Path: "/v1/llm-instances/{user_handle}/{instance_handle}/shared-with", + Methods: []string{"GET"}, + Description: "List users with whom an LLM service instance is shared", + Tags: []string{"llm-services", "sharing"}, + }, + // API Standards + { + Path: "/v1/api-standards", + Methods: []string{"GET", "POST"}, + Description: "List or create API standards", + Tags: []string{"api-standards"}, + }, + { + Path: "/v1/api-standards/{api_standard_handle}", + Methods: []string{"GET", "PUT", "DELETE"}, + Description: "Manage a specific API standard", + Tags: []string{"api-standards"}, + }, + // Admin + { + Path: "/v1/admin/footgun", + Methods: []string{"GET"}, + Description: "Reset the database (admin only, for testing)", + Tags: []string{"admin"}, + }, + { + Path: "/v1/admin/sanity-check", + Methods: []string{"GET"}, + Description: "Perform a sanity check on the database", + Tags: []string{"admin"}, + }, + }, + } + + // Build the response + response := &models.GetManifestResponse{} + response.Body = manifest + return response, nil +} + +// RegisterManifestRoutes registers the manifest routes with the API +func RegisterManifestRoutes(pool *pgxpool.Pool, api huma.API, options *models.Options) error { + // Define huma.Operations for the manifest endpoint + // This endpoint is public and doesn't require authentication + // We register at both / (root) and /v1 (versioned root) for discoverability + getManifestRootOp := huma.Operation{ + OperationID: "getManifestRoot", + Method: http.MethodGet, + Path: "/{$}", + Summary: "Get service manifest describing the API", + Description: "Returns a service manifest with metadata about the API and a list of available endpoints", + Security: []map[string][]string{}, + Tags: []string{"public", "manifest"}, + } + + getManifestV1Op := huma.Operation{ + OperationID: "getManifestV1", + Method: http.MethodGet, + Path: "/v1", + Summary: "Get service manifest for API version 1", + Description: "Returns a service manifest with metadata about API version 1 and a list of available endpoints", + Security: []map[string][]string{}, + Tags: []string{"public", "manifest"}, + } + + // Create a handler wrapper that includes both pool and options in context + handler := func(ctx context.Context, input *models.GetManifestRequest) (*models.GetManifestResponse, error) { + return getManifestFunc(ctx, input) + } + + // Register the routes with both pool and options in context + huma.Register(api, getManifestRootOp, addOptionsToContext(options, addPoolToContext(pool, handler))) + huma.Register(api, getManifestV1Op, addOptionsToContext(options, addPoolToContext(pool, handler))) + return nil +} diff --git a/internal/handlers/manifest_test.go b/internal/handlers/manifest_test.go new file mode 100644 index 0000000..bd589ee --- /dev/null +++ b/internal/handlers/manifest_test.go @@ -0,0 +1,137 @@ +package handlers_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/mpilhlt/embapi/internal/handlers" + "github.com/mpilhlt/embapi/internal/models" + "github.com/stretchr/testify/assert" +) + +func TestManifestFunc(t *testing.T) { + // Get the database connection pool from package variable + pool := connPool + + // Start the server + err, shutDownServer := startTestServer(t, pool, handlers.StandardKeyGen{}) + assert.NoError(t, err) + + fmt.Printf("\nRunning manifest tests ...\n\n") + + // Define test cases + tt := []struct { + name string + method string + requestPath string + expectStatus int + }{ + { + name: "Get manifest at root", + method: http.MethodGet, + requestPath: "/", + expectStatus: http.StatusOK, + }, + { + name: "Get manifest at /v1", + method: http.MethodGet, + requestPath: "/v1", + expectStatus: http.StatusOK, + }, + } + + for _, v := range tt { + t.Run(v.name, func(t *testing.T) { + requestURL := fmt.Sprintf("http://%v:%d%v", options.Host, options.Port, v.requestPath) + req, err := http.NewRequest(v.method, requestURL, nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("Error sending request: %v\n", err) + } + defer resp.Body.Close() + + if resp.StatusCode != v.expectStatus { + t.Errorf("Expected status code %d, got %s\n", v.expectStatus, resp.Status) + } else { + t.Logf("Expected status code %d, got %s\n", v.expectStatus, resp.Status) + } + + respBody, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + // Verify the response structure + var manifest models.ServiceManifest + err = json.Unmarshal(respBody, &manifest) + assert.NoError(t, err) + + // Check required fields + assert.NotEmpty(t, manifest.Name, "name field should not be empty") + assert.NotEmpty(t, manifest.Versions, "versions field should not be empty") + assert.Contains(t, manifest.Versions, "v1", "versions should contain v1") + + // Check optional fields are present + assert.NotEmpty(t, manifest.Description, "description field should be present") + assert.NotEmpty(t, manifest.Documentation, "documentation field should be present") + assert.Equal(t, "https://mpilhlt.github.io/embapi/", manifest.Documentation, "documentation URL should match") + assert.NotEmpty(t, manifest.ServiceVersion, "serviceVersion field should be present") + assert.NotEmpty(t, manifest.Authentication, "authentication field should be present") + + // Check endpoints + assert.NotEmpty(t, manifest.Endpoints, "endpoints field should not be empty") + + // Verify some key endpoints are present + foundUsers := false + foundProjects := false + foundEmbeddings := false + foundSimilars := false + + for _, endpoint := range manifest.Endpoints { + if endpoint.Path == "/v1/users" { + foundUsers = true + assert.Contains(t, endpoint.Methods, "GET") + assert.Contains(t, endpoint.Methods, "POST") + } + if endpoint.Path == "/v1/projects/{user_handle}/{project_handle}" { + foundProjects = true + } + if endpoint.Path == "/v1/embeddings/{user_handle}/{project_handle}" { + foundEmbeddings = true + } + if endpoint.Path == "/v1/similars/{user_handle}/{project_handle}/{text_id}" { + foundSimilars = true + } + } + + assert.True(t, foundUsers, "users endpoint should be in the manifest") + assert.True(t, foundProjects, "projects endpoint should be in the manifest") + assert.True(t, foundEmbeddings, "embeddings endpoint should be in the manifest") + assert.True(t, foundSimilars, "similars endpoint should be in the manifest") + + t.Logf("Manifest contains %d endpoints\n", len(manifest.Endpoints)) + }) + } + + // Cleanup + t.Cleanup(func() { + fmt.Print("\n\nRunning cleanup ...\n\n") + + requestURL := fmt.Sprintf("http://%s:%d/v1/admin/footgun", options.Host, options.Port) + req, err := http.NewRequest(http.MethodGet, requestURL, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+options.AdminKey) + _, err = http.DefaultClient.Do(req) + if err != nil && err.Error() != "no rows in result set" { + t.Fatalf("Error sending request: %v\n", err) + } + assert.NoError(t, err) + + fmt.Print("Shutting down server\n\n") + shutDownServer() + }) + + fmt.Printf("\n") +} diff --git a/internal/models/manifest.go b/internal/models/manifest.go new file mode 100644 index 0000000..4a8ec84 --- /dev/null +++ b/internal/models/manifest.go @@ -0,0 +1,36 @@ +package models + +import ( + "net/http" +) + +// ServiceManifest represents the service metadata and available endpoints +type ServiceManifest struct { + Name string `json:"name" doc:"Human-readable name of the service"` + Versions []string `json:"versions" doc:"Supported API version numbers"` + Description string `json:"description,omitempty" doc:"Short human-readable description of the service"` + Documentation string `json:"documentation,omitempty" doc:"Link to more elaborate human-readable description of the API"` + Logo string `json:"logo,omitempty" doc:"Link to a square image which can be used as the service's logo"` + ServiceVersion string `json:"serviceVersion,omitempty" doc:"Version of the software exposing this service"` + Authentication map[string]interface{} `json:"authentication,omitempty" doc:"JSON object describing the authentication offered according to the OpenAPI spec"` + BatchSize int `json:"batchSize,omitempty" doc:"Batch size for batch operations"` + Endpoints []EndpointInfo `json:"endpoints" doc:"List of available endpoints"` +} + +// EndpointInfo represents information about an available endpoint +type EndpointInfo struct { + Path string `json:"path" doc:"URI path or URI template (RFC6570) for the endpoint"` + Methods []string `json:"methods" doc:"HTTP methods supported by this endpoint"` + Description string `json:"description,omitempty" doc:"Brief description of the endpoint"` + Tags []string `json:"tags,omitempty" doc:"Tags categorizing this endpoint"` +} + +// GetManifestRequest is the request for getting the service manifest +type GetManifestRequest struct { +} + +// GetManifestResponse is the response containing the service manifest +type GetManifestResponse struct { + Header http.Header `json:"header,omitempty" doc:"Response headers"` + Body ServiceManifest `json:"manifest" doc:"Service manifest"` +} diff --git a/internal/models/options.go b/internal/models/options.go index dddfba5..6b06231 100644 --- a/internal/models/options.go +++ b/internal/models/options.go @@ -1,15 +1,25 @@ package models +// Version constants for the API +const ( + APIVersion = "0.0.1" + APIName = "DHaMPS Vector Database API" + APIDescription = "PostgreSQL-backed vector database with pgvector support, providing a RESTful API for managing embeddings in Retrieval Augmented Generation (RAG) workflows" +) + // Options for the CLI. type Options struct { - Debug bool ` env:"SERVICE_DEBUG" doc:"Enable debug logging" short:"d" default:"true"` - AuthVerbose bool `name:"auth-verbose" env:"SERVICE_AUTH_VERBOSE" doc:"Enable verbose authentication logging" default:"false"` - Host string ` env:"SERVICE_HOST" doc:"Hostname to listen on" default:"localhost"` - Port int ` env:"SERVICE_PORT" doc:"Port to listen on" short:"p" default:"8880"` - DBHost string `name:"db-host" env:"SERVICE_DBHOST" doc:"Database hostname" default:"localhost"` - DBPort int `name:"db-port" env:"SERVICE_DBPORT" doc:"Database port" default:"5432"` - DBUser string `name:"db-user" env:"SERVICE_DBUSER" doc:"Database username" default:"postgres"` - DBPassword string `name:"db-password" env:"SERVICE_DBPASSWORD" doc:"Database password" default:"password"` - DBName string `name:"db-name" env:"SERVICE_DBNAME" doc:"Database name" default:"postgres"` - AdminKey string `name:"admin-key" env:"SERVICE_ADMINKEY" doc:"Admin API key"` + Debug bool ` env:"SERVICE_DEBUG" doc:"Enable debug logging" short:"d" default:"true"` + AuthVerbose bool `name:"auth-verbose" env:"SERVICE_AUTH_VERBOSE" doc:"Enable verbose authentication logging" default:"false"` + Host string ` env:"SERVICE_HOST" doc:"Hostname to listen on" default:"localhost"` + Port int ` env:"SERVICE_PORT" doc:"Port to listen on" short:"p" default:"8880"` + DBHost string `name:"db-host" env:"SERVICE_DBHOST" doc:"Database hostname" default:"localhost"` + DBPort int `name:"db-port" env:"SERVICE_DBPORT" doc:"Database port" default:"5432"` + DBUser string `name:"db-user" env:"SERVICE_DBUSER" doc:"Database username" default:"postgres"` + DBPassword string `name:"db-password" env:"SERVICE_DBPASSWORD" doc:"Database password" default:"password"` + DBName string `name:"db-name" env:"SERVICE_DBNAME" doc:"Database name" default:"postgres"` + AdminKey string `name:"admin-key" env:"SERVICE_ADMINKEY" doc:"Admin API key"` + APIName string `name:"api-name" env:"SERVICE_API_NAME" doc:"Service name for API manifest"` + APIDescription string `name:"api-description" env:"SERVICE_API_DESCRIPTION" doc:"Service description for API manifest"` + APIDocURL string `name:"api-doc-url" env:"SERVICE_API_DOC_URL" doc:"Documentation URL for API manifest"` } diff --git a/main.go b/main.go index d85dc7b..595cfac 100644 --- a/main.go +++ b/main.go @@ -68,6 +68,15 @@ func main() { if os.Getenv("SERVICE_ADMINKEY") != "" { options.AdminKey = os.Getenv("SERVICE_ADMINKEY") } + if os.Getenv("SERVICE_API_NAME") != "" { + options.APIName = os.Getenv("SERVICE_API_NAME") + } + if os.Getenv("SERVICE_API_DESCRIPTION") != "" { + options.APIDescription = os.Getenv("SERVICE_API_DESCRIPTION") + } + if os.Getenv("SERVICE_API_DOC_URL") != "" { + options.APIDocURL = os.Getenv("SERVICE_API_DOC_URL") + } println() println("=== Starting DH@MPS Vector Database ...") @@ -93,7 +102,7 @@ func main() { keyGen := handlers.StandardKeyGen{} // Create a new router & API - config := huma.DefaultConfig("DHaMPS Vector Database API", "0.0.1") + config := huma.DefaultConfig(models.APIName, models.APIVersion) config.Components.SecuritySchemes = auth.Config router := http.NewServeMux() @@ -118,7 +127,7 @@ func main() { api.UseMiddleware(auth.AuthTermination(api, options)) // Add routes to the API - err = handlers.AddRoutes(pool, keyGen, api) + err = handlers.AddRoutes(pool, keyGen, api, options) if err != nil { fmt.Printf(" Unable to add routes: %v\n", err) os.Exit(1) diff --git a/template.env b/template.env index b9f8b98..4fd2053 100644 --- a/template.env +++ b/template.env @@ -11,6 +11,13 @@ SERVICE_DBPASSWORD=postgres SERVICE_DBNAME=postgres SERVICE_ADMINKEY=Ch4ngeM3! +# Service manifest configuration (optional) +# These values appear in the API manifest at GET / and GET /v1 +# If not set, defaults will be used +# SERVICE_API_NAME="DHaMPS Vector Database API" +# SERVICE_API_DESCRIPTION="PostgreSQL-backed vector database with pgvector support, providing a RESTful API for managing embeddings in Retrieval Augmented Generation (RAG) workflows" +# SERVICE_API_DOC_URL="https://mpilhlt.github.io/embapi/" + # Encryption key for API keys in LLM service instances (required for API key encryption) # Must be a secure random string, at least 32 characters recommended # Example: openssl rand -hex 32