Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Host function(s) for calling AI models #38

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "hmruntime",
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
}
]
}
1 change: 1 addition & 0 deletions hmruntime/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
hmruntime
.env
16 changes: 16 additions & 0 deletions hmruntime/aws/secrets_manager.go
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 48 additions & 0 deletions hmruntime/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2023 Hypermode, Inc. and Contributors
*
* Licensed under the Apache License, Version 2.0 (the License);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not be licensing our code under Apache 2 license. We can stick with just Copyright 2023 Hypermode, Inc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The runtime Go code is internal closed-source. Not OSS licensed.

* 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it'd be better to put these configs as env variables instead of in a json file?

The main concern I have is if there's ever a need to change one of the configs, we'll need to change this in the repo and then re-create the image, instead of just changing the env variables directly.

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
}
5 changes: 5 additions & 0 deletions hmruntime/config/dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"clerkUsername": "user_2YJwZdjhRY9AMZxExUWGIDpvvwd",
"clerkFrontendURL": "https://subtle-guinea-29.clerk.accounts.dev",
"consoleURL": "http://localhost:8071/graphql"
}
5 changes: 5 additions & 0 deletions hmruntime/config/prod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"clerkUsername": "user_2ZPJ5bfF4OoHpMOi6YN2zMJ5JtC",
"clerkFrontendURL": "https://clerk.admin-hypermode.com",
"consoleURL": "https://api.admin-hypermode.com/graphql"
}
5 changes: 5 additions & 0 deletions hmruntime/config/stg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"clerkUsername": "user_2YJwZdjhRY9AMZxExUWGIDpvvwd",
"clerkFrontendURL": "https://subtle-guinea-29.clerk.accounts.dev",
"consoleURL": "https://api.admin-hypermode-stage.com/graphql"
}
86 changes: 86 additions & 0 deletions hmruntime/console/clerk.go
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +30 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly - this is using Clerk, with a Clerk user ID coming from a json file, to get a JWT token for authenticating to the Dgraph console database. That's way too complex, and not an ideal strategy. We should have a service-to-service authentication flow that does not require using Clerk. Clerk-generated JWT tokens are for user-based authentication, not for services.

60 changes: 60 additions & 0 deletions hmruntime/console/hm_console.go
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can encode the payload this way instead using maps and then json.Marshal, which I think is cleaner than using fmt.Sprintf.

query := `query GetModelSpec {...}`
variables := map[string]interface{}{"id": id}

body, err := json.Marshal(map[string]interface{}{
	"query":     query,
	"variables": variables,
})
// handle err

req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. But it's not just about clean code. sprintf isn't appropriate for encoding parameters that are strings because it will fail if the string contains a quotation mark or other character that needs to be escaped. While unlikely for an id, it's still a good practice to avoid injection attacks.


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{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's set a Timeout for the http client. We can maybe have a global httpClient variable with the timeout, so we don't have to set the timeout on every request.

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" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's cleaner to have "classifier" as a const instead on top of this file, along with other ModelTypes we have or will add in the future.

return "", fmt.Errorf("error: model type does not match")
}

return spec.Endpoint, nil

}
3 changes: 3 additions & 0 deletions hmruntime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +12 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go mod tidy should reorganize these better to group direct and indirect dependencies.

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
Expand Down
6 changes: 6 additions & 0 deletions hmruntime/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
Loading
Loading