Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
abialemuel committed Oct 4, 2024
1 parent 2d4a3b5 commit 30328c0
Show file tree
Hide file tree
Showing 53 changed files with 3,350 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
test
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AI Proxy Service

## Description

The AI Proxy Service is a backend solution designed to securely manage and relay requests to various large language models (LLMs), including GPT-4 via the OpenAI API, while providing the flexibility to integrate other models that follow the OpenAI API standard format. This enables users to easily switch between different LLM providers, whether they are hosted externally or run locally, without major changes to client applications.

The service is stateless and integrates with Single Sign-On (SSO) using Microsoft Entra ID and Google OAuth 2.0 for secure users authentication. It also supports server-side requests from other backend services through secret key-based authentication, making it easy to integrate AI capabilities securely into other systems.

In addition to providing access to multiple AI models, the service enhances their functionality by addressing common limitations like context management, token efficiency, and cost control. Features such as token usage tracking and conversation summarization ensure that tokens are used optimally, reducing costs and improving performance.

## Features
- **Stateless Architecture**: Ensures scalability and performance by maintaining no internal state between requests.
- **SSO Authentication**: Integrates with Microsoft Entra ID and Google OAuth 2.0 to authenticate users securely.
- **Secret Key-based Authentication**: Allows other backend services to securely access GPT-4 using secret key-based authentication.
- **Token Limitations**: Implements token consumption tracking over a specified period, ensuring cost control and preventing overuse.
- **Token Efficiency with Summarization**: The service periodically summarizes after every n conversations to maintain token efficiency. This allows for a streamlined conversation history, reducing the amount of context needed while still retaining relevant information.
- **Model Flexibility**: Supports multiple AI models that adhere to the OpenAI API standard format, providing flexibility to switch between different LLM providers.
- **Context Awareness**: Manages and maintains conversational context across multiple requests by storing relevant information in a separate datastore, simulating long-term memory for conversations.
- **Logging and Monitoring**: Logs requests, responses, and usage metrics, with integrations for observability platforms.
- **Error Handling**: Graceful error handling with clear error messages and status codes.
- **Extensible**: Designed for easy extension with new authentication providers or features.

## Prerequisites

- Go 1.20+
- Git
- Docker


## Installation

1. Clone the repository:

```sh
git clone <repository_url>
cd monitoring-app
```

2. Install the dependencies:

```sh
go mod tidy
```

## Configuration

- **Main Configuration**: `config.yaml`

### `config.yaml`

This file contains the main configuration for the service, including logging levels, APM (Application Performance Monitoring), SSO, Datastore settings.


## AI Proxy SAD
![image.png](./docs/img/Architecture-Diagram.png)
52 changes: 52 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
app:
name: "proxy-service"
port: 8080
version: "1.0.0"
env: "dev"
tribe: "tribe"
ui:
host: "http://localhost:3000"
log:
level: "info"
format: "json"
redis:
host: "redis"
port: 6379
password: "redis"
db: 0
mongo:
host: "mongo"
port: 27017
username: "admin"
password: "admin"
db: "proxy-service"
apm:
enabled: true
host: "jaeger"
port: 4317
rate: 1
microsoftOauth:
tenantID: "your-tenant-id"
clientID: "your-client-id"
clientSecret: "your-client-secret"
redirectURL: "your-redirect-url"
googleOauth:
clientID: "your-client-id"
clientSecret: "your-client-secret"
redirectURL: "your-redirect-url"
openAI:
host: "http://localhost:1234"
path: "/v1/chat/completions"
apiKey: "your-api-key"
tokenLifetime: 3600
tokenLimit: 10000
services:
- tribe: "tribeA"
name: "code-review"
username: "user"
password: "password"
- tribe: "tribeB"
name: "chatbot"
username: "user"
password: "password"

147 changes: 147 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package config

import (
"fmt"
"reflect"
"strings"

"github.com/go-playground/validator/v10"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)

type Config interface {
Init(configPath string) error
Get() *MainConfig
}

type config struct {
Config *MainConfig
}

func New() Config {
return &config{
Config: &MainConfig{},
}
}

func (c *config) Init(configPath string) error {
// Load environment variables from .env file
if err := godotenv.Load(); err != nil {
// Log the error if needed, but continue to load other configurations
}

if err := c.load(c.Config, ".", configPath); err != nil {
return err
}
err := validator.New().Struct(c.Config)
if err != nil {
return err
}

return nil
}

func (c *config) Get() *MainConfig {
return c.Config
}

func (c *config) load(cfg *MainConfig, path string, configPath string) error {
// Set default values
viper.SetDefault("log.level", "info")

viper.AddConfigPath(path)
if configPath != "" {
viper.SetConfigFile(configPath)
}
viper.SetConfigType("yaml")
viper.SetEnvKeyReplacer(strings.NewReplacer(`.`, `_`))
viper.AutomaticEnv()

// Read the config file
if err := viper.ReadInConfig(); err != nil && configPath != "" {
return err
}

// Unmarshal the config into the struct
if err := viper.Unmarshal(cfg); err != nil {
return err
}

// Populate struct from environment variables using reflection
if err := c.populateFromEnv(cfg); err != nil {
return err
}

return nil
}

// populateFromEnv populates struct fields from environment variables if the `env` tag is present
func (c *config) populateFromEnv(cfg any) error {
val := reflect.ValueOf(cfg)

// Ensure that the value is a pointer to a struct
if val.Kind() != reflect.Ptr {
return fmt.Errorf("expected a pointer but got %v", val.Kind())
}

if val.Elem().Kind() != reflect.Struct {
return fmt.Errorf("expected a pointer to struct but got %v", val.Elem().Kind())
}

val = val.Elem() // Dereference the pointer to get to the struct
typ := val.Type() // Get the struct type

return c.populate(val, typ)
}

// populate recursively populates struct fields from environment variables if the `env` tag is present
func (c *config) populate(val reflect.Value, typ reflect.Type) error {
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
structField := typ.Field(i)

// Get the 'env' tag
envTag := structField.Tag.Get("env")
if envTag != "" {
// Get the environment variable value from Viper
envValue := viper.GetString(envTag)
if envValue != "" {
// Set the field based on its type
switch field.Kind() {
case reflect.Ptr:
if field.Type().Elem().Kind() == reflect.String {
field.Set(reflect.ValueOf(&envValue)) // set *string
} else if field.Type().Elem().Kind() == reflect.Int {
// Convert string to int before setting if it's an integer field
var intVal int
fmt.Sscanf(envValue, "%d", &intVal)
field.Set(reflect.ValueOf(&intVal)) // set *int
}
case reflect.String:
field.SetString(envValue) // set string
case reflect.Int:
// Convert string to int before setting if it's an integer field
var intVal int
fmt.Sscanf(envValue, "%d", &intVal)
field.SetInt(int64(intVal)) // set int
case reflect.Bool:
// Convert string to bool before setting if it's a boolean field
var boolVal bool
fmt.Sscanf(envValue, "%t", &boolVal)
field.SetBool(boolVal) // set bool
}
}
}

// Check if the field is a struct
if field.Kind() == reflect.Struct {
// Call populate recursively for nested structs
if err := c.populate(field, field.Type()); err != nil {
return err
}
}
}

return nil
}
49 changes: 49 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package config_test

import (
"path/filepath"
"runtime"
"testing"

config "github.com/abialemuel/AI-Proxy-Service/config"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)

type Suite struct {
cnf config.Config
suite.Suite
}

func (c *Suite) SetupSuite() {
c.cnf = config.New()
}

func (c *Suite) TestConfig() {
c.Run("Wrong init file path", func() {
err := c.cnf.Init("wrongPath")
assert.NotNil(c.T(), err)
})

c.Run("Failed config validation", func() {
_, filename, _, _ := runtime.Caller(0)
err := c.cnf.Init(filepath.Clean(filepath.Join(filepath.Dir(filename), "mocks/config.wrong.validate.yaml")))
assert.NotNil(c.T(), err)
})

c.Run("Correct init file path", func() {
_, filename, _, _ := runtime.Caller(0)
err := c.cnf.Init(filepath.Clean(filepath.Join(filepath.Dir(filename), "mocks/config.yaml")))
assert.Nil(c.T(), err)
})

c.Run("Get must be not nil", func() {
assert.NotNil(c.T(), c.cnf.Get())
})

}

func TestConfig(t *testing.T) {
suite.Run(t, &Suite{})
}
6 changes: 6 additions & 0 deletions config/mocks/config.wrong.validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# just sample config file for config testing
APM:
enabled: true
host: "otel-collector"
port: 4317
rate: 1
15 changes: 15 additions & 0 deletions config/mocks/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# just sample config file for config testing
App:
name: "proxy-service"
version: "1.0.0"
env: "dev"
tribe: "dpe"
log:
level: "info"
format: "json"

APM:
enabled: true
host: "otel-collector"
port: 4317
rate: 1
Loading

0 comments on commit 30328c0

Please sign in to comment.