diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..3cc2f79d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "hmruntime", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/hmruntime", // adjust this if the path is different + "env": { + "ENV": "dev", + "AWS_REGION":"us-west-2", + "AWS_PROFILE":"hm-cp-staging", + "AWS_SDK_LOAD_CONFIG": "true" + } + } + ] +} diff --git a/hmruntime/.gitignore b/hmruntime/.gitignore index f7a62604..aaff457a 100644 --- a/hmruntime/.gitignore +++ b/hmruntime/.gitignore @@ -1 +1,2 @@ hmruntime +.env diff --git a/hmruntime/aws/secrets_manager.go b/hmruntime/aws/secrets_manager.go new file mode 100644 index 00000000..510b3b11 --- /dev/null +++ b/hmruntime/aws/secrets_manager.go @@ -0,0 +1,16 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/secretsmanager" +) + +func GetSecretManagerSession() (*secretsmanager.SecretsManager, error) { + sess, err := session.NewSession() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + return svc, nil +} diff --git a/hmruntime/config/config.go b/hmruntime/config/config.go new file mode 100644 index 00000000..be2e114c --- /dev/null +++ b/hmruntime/config/config.go @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Hypermode, Inc. and Contributors + * + * 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 config + +import ( + "encoding/json" + "fmt" + "os" +) + +type Config struct { + ClerkUsername string `json:"clerkUsername"` + ClerkFrontendURL string `json:"clerkFrontendURL"` + ConsoleURL string `json:"consoleURL"` +} + +func GetAppConfiguration(env string) (*Config, error) { + var config Config + + const configPath = "config/%s.json" + + path := fmt.Sprintf(configPath, env) + + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open config: %w", err) + } + defer file.Close() + + if err := json.NewDecoder(file).Decode(&config); err != nil { + return nil, fmt.Errorf("parse config: %w", err) + } + + return &config, nil +} diff --git a/hmruntime/config/dev.json b/hmruntime/config/dev.json new file mode 100644 index 00000000..e77f204f --- /dev/null +++ b/hmruntime/config/dev.json @@ -0,0 +1,5 @@ +{ + "clerkUsername": "user_2YJwZdjhRY9AMZxExUWGIDpvvwd", + "clerkFrontendURL": "https://subtle-guinea-29.clerk.accounts.dev", + "consoleURL": "http://localhost:8071/graphql" +} diff --git a/hmruntime/config/prod.json b/hmruntime/config/prod.json new file mode 100644 index 00000000..9b4269b3 --- /dev/null +++ b/hmruntime/config/prod.json @@ -0,0 +1,5 @@ +{ + "clerkUsername": "user_2ZPJ5bfF4OoHpMOi6YN2zMJ5JtC", + "clerkFrontendURL": "https://clerk.admin-hypermode.com", + "consoleURL": "https://api.admin-hypermode.com/graphql" +} diff --git a/hmruntime/config/stg.json b/hmruntime/config/stg.json new file mode 100644 index 00000000..9b5e3d48 --- /dev/null +++ b/hmruntime/config/stg.json @@ -0,0 +1,5 @@ +{ + "clerkUsername": "user_2YJwZdjhRY9AMZxExUWGIDpvvwd", + "clerkFrontendURL": "https://subtle-guinea-29.clerk.accounts.dev", + "consoleURL": "https://api.admin-hypermode-stage.com/graphql" +} diff --git a/hmruntime/console/clerk.go b/hmruntime/console/clerk.go new file mode 100644 index 00000000..e6ac4082 --- /dev/null +++ b/hmruntime/console/clerk.go @@ -0,0 +1,86 @@ +package console + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type ClerkAPI struct { + UserID string + BearerToken string + ClerkFrontendAPIURL string + JWT string +} + +type SignInResponse struct { + Token string `json:"token"` +} + +type ClientSession struct { + LastActiveToken LastActiveToken `json:"last_active_token"` +} + +type LastActiveToken struct { + JWT string `json:"jwt"` +} + +func (c *ClerkAPI) Login() error { + // First POST request to get the token + tokenURL := "https://api.clerk.com/v1/sign_in_tokens" + tokenPayload := map[string]interface{}{ + "user_id": c.UserID, + "expires_in_seconds": 2592000, + } + jsonData, _ := json.Marshal(tokenPayload) + + req, err := http.NewRequest("POST", tokenURL, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.BearerToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var signInResponse SignInResponse + err = json.Unmarshal(body, &signInResponse) + if err != nil { + return err + } + token := signInResponse.Token + + // Second POST request to get the JWT + signInURL := fmt.Sprintf("%s/v1/client/sign_ins?_is_native=true", c.ClerkFrontendAPIURL) + payload := fmt.Sprintf("strategy=ticket&ticket=%s", token) + + req, err = http.NewRequest("POST", signInURL, bytes.NewBufferString(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err = client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, _ = io.ReadAll(resp.Body) + var session ClientSession + err = json.Unmarshal(body, &session) + if err != nil { + return err + } + c.JWT = session.LastActiveToken.JWT + + return nil +} diff --git a/hmruntime/console/hm_console.go b/hmruntime/console/hm_console.go new file mode 100644 index 00000000..cfbe601d --- /dev/null +++ b/hmruntime/console/hm_console.go @@ -0,0 +1,60 @@ +package console + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type ModelSpec struct { + ID string `json:"id"` + ModelType string `json:"modelType"` + Endpoint string `json:"endpoint"` +} + +func GetModelEndpoint(url, id, jwt string) (string, error) { + + payload := []byte(fmt.Sprintf(`{"query":"query GetModelSpec {\n getModelSpec(id: \"%v\") {\n id\n modelType\n endpoint\n }\n}","variables":{}}`, id)) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return "", fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Authorization", jwt) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %w", err) + } + + // Create an instance of the ModelSpec struct + var spec ModelSpec + + // Unmarshal the JSON data into the ModelSpec struct + err = json.Unmarshal(body, &spec) + if err != nil { + return "", fmt.Errorf("error unmarshaling response body: %w", err) + } + + if spec.ID != id { + return "", fmt.Errorf("error: ID does not match") + } + + if spec.ModelType != "classifier" { + return "", fmt.Errorf("error: model type does not match") + } + + return spec.Endpoint, nil + +} diff --git a/hmruntime/go.mod b/hmruntime/go.mod index 7c6a16c6..7a6a92e9 100644 --- a/hmruntime/go.mod +++ b/hmruntime/go.mod @@ -9,8 +9,11 @@ require ( github.com/tetratelabs/wazero v1.5.0 ) +require github.com/jmespath/go-jmespath v0.4.0 // indirect + require ( github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/aws/aws-sdk-go v1.49.9 github.com/sergi/go-diff v1.3.1 // indirect github.com/stretchr/testify v1.8.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/hmruntime/go.sum b/hmruntime/go.sum index 26e7bfcd..f48eff4b 100644 --- a/hmruntime/go.sum +++ b/hmruntime/go.sum @@ -5,6 +5,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/aws/aws-sdk-go v1.49.9 h1:4xoyi707rsifB1yMsd5vGbAH21aBzwpL3gNRMSmjIyc= +github.com/aws/aws-sdk-go v1.49.9/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -14,6 +16,9 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -35,6 +40,7 @@ golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGm gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/hmruntime/hostfns.go b/hmruntime/hostfns.go index 96f2b016..eaed64e6 100644 --- a/hmruntime/hostfns.go +++ b/hmruntime/hostfns.go @@ -1,10 +1,19 @@ package main import ( + "bytes" "context" + "encoding/json" "fmt" + "hmruntime/aws" + "hmruntime/config" + "hmruntime/console" + "io" "log" + "net/http" + "os" + "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/tetratelabs/wazero" wasm "github.com/tetratelabs/wazero/api" ) @@ -17,6 +26,7 @@ func instantiateHostFunctions(ctx context.Context, runtime wazero.Runtime) error // Each host function should get a line here: b.NewFunctionBuilder().WithFunc(hostExecuteDQL).Export("executeDQL") b.NewFunctionBuilder().WithFunc(hostExecuteGQL).Export("executeGQL") + b.NewFunctionBuilder().WithFunc(hostInvokeClassifier).Export("invokeClassifier") _, err := b.Instantiate(ctx) if err != nil { @@ -59,3 +69,117 @@ func hostExecuteGQL(ctx context.Context, mod wasm.Module, pStmt uint32) uint32 { return writeString(ctx, mod, string(r)) } + +type ClassifierResult struct { + Label string `json:"label"` + Confidence float64 `json:"confidence"` + Probabilities []ClassifierLabel `json:"probabilities"` +} +type ClassifierLabel struct { + Label string `json:"label"` + Probability float64 `json:"probability"` +} +type ClassifierResponse struct { + Uid ClassifierResult `json:"uid"` +} + +func hostInvokeClassifier(ctx context.Context, mod wasm.Module, modelId uint32, psentence uint32) uint32 { + mem := mod.Memory() + model, err := readString(mem, modelId) + if err != nil { + log.Println("error reading model id from wasm memory:", err) + return 0 + } + fmt.Println("reading model name from wasm memory:", model) + sentence, err := readString(mem, psentence) + if err != nil { + log.Println("error reading sentence string from wasm memory:", err) + return 0 + } + + // appConfig, err := config.GetAppConfiguration(os.Getenv("ENV")) + appConfig, err := config.GetAppConfiguration("dev") + + if err != nil { + log.Println("error reading app config:", err) + return 0 + } + + c := &console.ClerkAPI{ + UserID: appConfig.ClerkUsername, + BearerToken: os.Getenv("CLERK_AUTH_API_KEY"), + ClerkFrontendAPIURL: appConfig.ClerkFrontendURL, + } + + if err := c.Login(); err != nil { + log.Println("error logging in to Clerk:", err) + return 0 + } + + fmt.Println("JWT:", c.JWT) + + // get the model endpoint + endpoint, err := console.GetModelEndpoint(appConfig.ConsoleURL, model, c.JWT) + if err != nil { + log.Println("error getting model endpoint:", err) + return 0 + } + + // POST to embedding service + postBody, _ := json.Marshal(map[string]string{ + "uid": sentence, + }) + requestBody := bytes.NewBuffer(postBody) + //Leverage Go's HTTP Post function to make request + + req, err := http.NewRequest( + http.MethodPost, + endpoint, + requestBody) + if err != nil { + log.Println("error buidling request:", err) + return 0 + } + svc, err := aws.GetSecretManagerSession() + if err != nil { + log.Println("error getting secret manager session:", err) + return 0 + } + secretValue, err := svc.GetSecretValue(&secretsmanager.GetSecretValueInput{ + SecretId: &endpoint, + }) + if err != nil { + log.Println("error getting secret:", err) + return 0 + } + if secretValue.SecretString == nil { + log.Println("secret string was empty") + return 0 + } + + modelKey := *secretValue.SecretString + + req.Header.Set("x-api-key", modelKey) + resp, err := http.DefaultClient.Do(req) + //Handle Error + if err != nil { + log.Printf("An Error Occured %v", err) + return 0 + } + defer resp.Body.Close() + //Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("An Error Occured %v", err) + return 0 + } + + // snippet only + var result ClassifierResponse + if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer + fmt.Println("Can not unmarshal JSON") + } + str, _ := json.Marshal(result.Uid) + // return a string + return writeString(ctx, mod, string(str)) +} diff --git a/plugins/as/hypermode-as/assembly/hypermode.ts b/plugins/as/hypermode-as/assembly/hypermode.ts index d71f98cd..e4436958 100644 --- a/plugins/as/hypermode-as/assembly/hypermode.ts +++ b/plugins/as/hypermode-as/assembly/hypermode.ts @@ -2,3 +2,4 @@ export declare function executeDQL(statement: string, isMutation: bool): string; export declare function executeGQL(statement: string): string; +export declare function invokeClassifier(modelId: string, sentence: string): string; diff --git a/plugins/as/hypermode-as/assembly/index.ts b/plugins/as/hypermode-as/assembly/index.ts index 7862d321..8a966445 100644 --- a/plugins/as/hypermode-as/assembly/index.ts +++ b/plugins/as/hypermode-as/assembly/index.ts @@ -13,7 +13,8 @@ export abstract class dql { } private static execute(query: string, isMutation: bool): DQLResponse { - const response = host.executeDQL(query, isMutation); + let response = host.executeDQL(query, isMutation); + response = response.replace("@groupby","groupby"); console.log(response); return JSON.parse>(response); } @@ -22,6 +23,33 @@ export abstract class dql { export abstract class graphql { static execute(statement: string): GQLResponse { const response = host.executeGQL(statement); + console.log(`graphql ${response}`) return JSON.parse>(response); } } + +export function invokeClassifier(modelId: string, text: string): ClassificationResult { + + // Preferablly this would return `GQLResponse`, + // but we're blocked by https://github.com/JairusSW/as-json/issues/53 + console.log("Invoking invokeClassifier") + const response = host.invokeClassifier(modelId, text); + console.log(`response ${response}`) + const result = JSON.parse(response) + + return result; +} +// @ts-ignore +@json +export class ClassificationProbability { // must be defined in the library + label!: string; + probability!: f32; +}; + +// @ts-ignore +@json +export class ClassificationResult { // must be defined in the library + label: string = ""; + confidence: f32 = 0.0; + probabilities!: ClassificationProbability[] +};