Skip to content

Instantly share code, notes, and snippets.

@Cdaprod
Last active October 29, 2024 16:38
Show Gist options
  • Save Cdaprod/3b5f65b88eb21ee0a988bbcee8ffd0f3 to your computer and use it in GitHub Desktop.
Save Cdaprod/3b5f65b88eb21ee0a988bbcee8ffd0f3 to your computer and use it in GitHub Desktop.
Building a robust microservice registry AI assistant tailored to Go-based system. Here we adapt an [initial LangGraph and Graphiti-based approach](https://github.com/getzep/graphiti/blob/main/examples/langgraph-agent/agent.ipynb) to fit within a Go ecosystem, leveraging alternative technologies and methodologies to achieve similar functionality.

Building a robust microservice registry AI assistant tailored to Go-based system. Here we adapt an initial LangGraph and Graphiti-based approach to fit within a Go ecosystem, leveraging alternative technologies and methodologies to achieve similar functionality.

Objective

  • Goal: Develop an AI assistant for your GitHub/Docker microservice registry service using Go, without relying on Neo4j.
  • Key Components:
    • Data Storage: Replace Neo4j/Graphiti with a suitable alternative (e.g., PostgreSQL).
    • AI Integration: Utilize OpenAI's API or similar services to handle natural language processing.
    • API Development: Build RESTful or GraphQL APIs in Go to interact with the AI assistant.
    • State Management: Implement mechanisms to maintain conversation state and persist interactions.

Overview of the Approach

  1. Choose an Alternative Data Storage Solution: Opt for a relational database like PostgreSQL.
  2. Implement Data Models in Go: Define your data structures and interact with the database using GORM.
  3. Implement Search Functionality: Utilize PostgreSQL's full-text search or integrate Elasticsearch for advanced querying.
  4. Integrate AI Capabilities: Use OpenAI's API to handle natural language queries and generate responses.
  5. Build the AI Assistant Logic: Create functions to process user queries, interact with the database, and generate responses.
  6. Develop an API for Interaction: Use a web framework like Gin to create endpoints for user interactions.
  7. Ensure Robustness: Incorporate concurrency management, error handling, security measures, and testing.
  8. Deploy and Monitor: Set up deployment pipelines and monitoring tools to maintain system health.

Let's dive into each step in detail!


1. Choose an Alternative Data Storage Solution

Given your requirements and the need to exclude Neo4j, PostgreSQL is an excellent choice due to its robustness, support for complex queries, and full-text search capabilities.

Why PostgreSQL?

  • ACID Compliance: Ensures reliable transactions.
  • Full-Text Search: Facilitates efficient searching across large text datasets.
  • Extensibility: Supports various extensions for additional functionalities.
  • Community and Support: Extensive documentation and active community.

Installation

Ensure PostgreSQL is installed and running on your system. You can download it from PostgreSQL Downloads.

Create a Database

# Access PostgreSQL shell
psql -U postgres

# Create a new database
CREATE DATABASE registry_db;

# Create a new user with a password
CREATE USER registry_user WITH PASSWORD 'your_secure_password';

# Grant privileges
GRANT ALL PRIVILEGES ON DATABASE registry_db TO registry_user;

# Exit the shell
\q

2. Implement Data Models in Go

Use GORM, a popular ORM library for Go, to interact with PostgreSQL.

Install GORM and PostgreSQL Driver

go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

Define Data Structures

package models

import (
	"time"
	
	"gorm.io/gorm"
)

// RegistryItem represents an individual repository in the registry.
type RegistryItem struct {
	ID            string         `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
	Name          string         `gorm:"not null"`
	Type          string         `gorm:"not null"`
	Status        string         `gorm:"not null"`
	Path          string         `gorm:"not null"`
	CreatedAt     time.Time      `gorm:"autoCreateTime"`
	LastUpdated   time.Time      `gorm:"autoUpdateTime"`
	Enabled       bool           `gorm:"default:true"`
	HasDockerfile bool           `gorm:"default:false"`
	Metadata      RegistryMetadata `gorm:"type:jsonb"`
}

// RegistryMetadata holds additional configuration details.
type RegistryMetadata struct {
	Description      string               `json:"description"`
	Owner            string               `json:"owner"`
	LastUpdated      time.Time            `json:"last_updated"`
	Tags             []string             `json:"tags"`
	GitHub           GitHubMetadata       `json:"github"`
	Docker           DockerMetadata       `json:"docker"`
	CustomProperties CustomPropertiesData `json:"custom_properties"`
}

// GitHubMetadata holds GitHub-related information.
type GitHubMetadata struct {
	Stars         int                    `json:"stars"`
	Forks         int                    `json:"forks"`
	Issues        int                    `json:"issues"`
	PullRequests  PullRequestMetadata    `json:"pull_requests"`
	LatestCommit  string                 `json:"latest_commit"`
	License       string                 `json:"license"`
	Topics        []string               `json:"topics"`
}

// PullRequestMetadata holds pull request details.
type PullRequestMetadata struct {
	Open   int `json:"open"`
	Closed int `json:"closed"`
}

// DockerMetadata holds Docker-related information.
type DockerMetadata struct {
	Dockerfile      DockerfileMetadata       `json:"dockerfile"`
	DockerCompose   DockerComposeMetadata    `json:"docker_compose"`
}

// DockerfileMetadata holds Dockerfile details.
type DockerfileMetadata struct {
	ExposedPorts []string `json:"exposed_ports"`
	EnvVars      []string `json:"env_vars"`
	Cmd          string   `json:"cmd"`
	Entrypoint   string   `json:"entrypoint"`
	Volumes      []string `json:"volumes"`
}

// DockerComposeMetadata holds Docker Compose details.
type DockerComposeMetadata struct {
	Services  []string            `json:"services"`
	Ports     map[string][]string `json:"ports"`
	Volumes   map[string][]string `json:"volumes"`
	EnvVars   map[string][]string `json:"env_vars"`
	Command   map[string]string   `json:"command"`
}

// CustomPropertiesData holds custom properties.
type CustomPropertiesData struct {
	DeployEnvironment string   `json:"deploy_environment"`
	MonitoringEnabled bool     `json:"monitoring_enabled"`
	AutoScale         bool     `json:"auto_scale"`
	Service           string   `json:"service"`
	App               string   `json:"app"`
	Image             string   `json:"image"`
	Ports             []string `json:"ports"`
	Volumes           []string `json:"volumes"`
	Network           string   `json:"network"`
	Domain            string   `json:"domain"`
}

Initialize the Database Connection

package database

import (
	"fmt"
	"log"
	"os"

	"your_project/models"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func InitDB() *gorm.DB {
	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
		os.Getenv("DB_HOST"),
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASSWORD"),
		os.Getenv("DB_NAME"),
		os.Getenv("DB_PORT"),
	)

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatalf("failed to connect to database: %v", err)
	}

	// Enable UUID extension
	db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";")

	// Migrate the schema
	err = db.AutoMigrate(&models.RegistryItem{})
	if err != nil {
		log.Fatalf("failed to migrate database: %v", err)
	}

	return db
}

Environment Variables

Ensure you have the following environment variables set (e.g., in a .env file):

DB_HOST=localhost
DB_USER=registry_user
DB_PASSWORD=your_secure_password
DB_NAME=registry_db
DB_PORT=5432
OPENAI_API_KEY=your_openai_api_key

Load Environment Variables

Use the godotenv package to load environment variables.

go get github.com/joho/godotenv
package main

import (
	"log"

	"github.com/joho/godotenv"
	"your_project/database"
)

func main() {
	// Load environment variables
	err := godotenv.Load()
	if err != nil {
		log.Println("No .env file found. Using environment variables.")
	}

	// Initialize database
	db := database.InitDB()

	// Continue with setting up the server...
	_ = db
}

3. Implement Search Functionality

To enable efficient searching, you can utilize PostgreSQL's Full-Text Search or integrate Elasticsearch for more advanced querying.

Option 1: PostgreSQL Full-Text Search

Add a Search Index

CREATE INDEX registryitem_search_idx ON registry_items USING GIN(to_tsvector('english', name || ' ' || type || ' ' || status || ' ' || metadata::text));

Implement Search in Go

package repository

import (
	"your_project/models"

	"gorm.io/gorm"
)

func SearchRegistryItems(db *gorm.DB, query string) ([]models.RegistryItem, error) {
	var items []models.RegistryItem
	searchQuery := "to_tsvector('english', name || ' ' || type || ' ' || status || ' ' || metadata::text) @@ plainto_tsquery(?)"
	err := db.Where(searchQuery, query).Find(&items).Error
	return items, err
}

Option 2: Integrate Elasticsearch

For more advanced search capabilities, Elasticsearch is a powerful tool.

Installation

Download and install Elasticsearch from Elasticsearch Downloads.

Install Go Elasticsearch Client

go get github.com/elastic/go-elasticsearch/v8

Initialize Elasticsearch Client

package elasticsearch

import (
	"log"
	"os"

	"github.com/elastic/go-elasticsearch/v8"
)

func InitElasticsearch() *elasticsearch.Client {
	cfg := elasticsearch.Config{
		Addresses: []string{
			os.Getenv("ELASTICSEARCH_URL"), // e.g., "http://localhost:9200"
		},
	}
	es, err := elasticsearch.NewClient(cfg)
	if err != nil {
		log.Fatalf("Error creating Elasticsearch client: %s", err)
	}
	return es
}

Indexing Data

Implement functions to index your RegistryItem data into Elasticsearch.

package repository

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"

	"your_project/models"

	"github.com/elastic/go-elasticsearch/v8"
	"github.com/elastic/go-elasticsearch/v8/esapi"
)

type ElasticRepository struct {
	Client *elasticsearch.Client
	Index  string
}

func NewElasticRepository(client *elasticsearch.Client, index string) *ElasticRepository {
	return &ElasticRepository{
		Client: client,
		Index:  index,
	}
}

func (er *ElasticRepository) IndexRegistryItem(item models.RegistryItem) error {
	data, err := json.Marshal(item)
	if err != nil {
		return err
	}

	req := esapi.IndexRequest{
		Index:      er.Index,
		DocumentID: item.ID,
		Body:       bytes.NewReader(data),
		Refresh:    "true",
	}

	res, err := req.Do(context.Background(), er.Client)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.IsError() {
		return fmt.Errorf("error indexing document ID=%s", item.ID)
	}

	return nil
}

func (er *ElasticRepository) SearchRegistryItems(query string) ([]models.RegistryItem, error) {
	var r map[string]interface{}
	var items []models.RegistryItem

	searchBody := map[string]interface{}{
		"query": map[string]interface{}{
			"multi_match": map[string]interface{}{
				"query":  query,
				"fields": []string{"name", "type", "status", "metadata.description"},
			},
		},
	}

	data, err := json.Marshal(searchBody)
	if err != nil {
		return items, err
	}

	res, err := er.Client.Search(
		er.Client.Search.WithContext(context.Background()),
		er.Client.Search.WithIndex(er.Index),
		er.Client.Search.WithBody(bytes.NewReader(data)),
		er.Client.Search.WithTrackTotalHits(true),
		er.Client.Search.WithPretty(),
	)
	if err != nil {
		return items, err
	}
	defer res.Body.Close()

	if res.IsError() {
		return items, fmt.Errorf("error searching: %s", res.String())
	}

	if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
		return items, err
	}

	for _, hit := range r["hits"].(map[string]interface{})["hits"].([]interface{}) {
		source := hit.(map[string]interface{})["_source"]
		sourceBytes, _ := json.Marshal(source)
		var item models.RegistryItem
		if err := json.Unmarshal(sourceBytes, &item); err != nil {
			log.Println("Error unmarshaling search hit:", err)
			continue
		}
		items = append(items, item)
	}

	return items, nil
}

Usage

package main

import (
	"log"

	"your_project/database"
	"your_project/models"
	"your_project/repository"

	"github.com/elastic/go-elasticsearch/v8"
)

func main() {
	// Initialize database
	db := database.InitDB()

	// Initialize Elasticsearch
	esClient, err := elasticsearch.NewDefaultClient()
	if err != nil {
		log.Fatalf("Error creating the client: %s", err)
	}
	esRepo := repository.NewElasticRepository(esClient, "registry_items")

	// Example: Index a RegistryItem
	item := models.RegistryItem{
		ID:            "8fedb80d-c984-436b-9281-e19302c97734",
		Name:          "Cdaprod/_middleware-infrastructure",
		Type:          "service",
		Status:        "active",
		Path:          "https://github.com/Cdaprod/_middleware-infrastructure",
		CreatedAt:     time.Now(),
		LastUpdated:   time.Now(),
		Enabled:       true,
		HasDockerfile: true,
		Metadata: models.RegistryMetadata{
			Description: "Auto-generated configuration for Cdaprod/_middleware-infrastructure",
			Owner:       "Cdaprod",
			LastUpdated: time.Now(),
			Tags:        []string{},
			GitHub: models.GitHubMetadata{
				Stars:        1,
				Forks:        0,
				Issues:       0,
				PullRequests: models.PullRequestMetadata{Open: 0, Closed: 0},
				LatestCommit: "2024-09-30T17:24:07Z",
				License:      "NOASSERTION",
				Topics:       []string{},
			},
			Docker: models.DockerMetadata{
				Dockerfile: models.DockerfileMetadata{
					ExposedPorts: []string{"8080"},
					EnvVars:      []string{},
					Cmd:          "",
					Entrypoint:   "[/main]",
					Volumes:      []string{},
				},
				DockerCompose: models.DockerComposeMetadata{
					Services: []string{"middleware-infrastructure"},
					Ports: map[string][]string{
						"middleware-infrastructure": {"8080"},
					},
					Volumes: map[string][]string{
						"middleware-infrastructure": {""},
					},
					EnvVars: map[string][]string{
						"middleware-infrastructure": {"GO_ENV", "PORT"},
					},
					Command: map[string]string{
						"middleware-infrastructure": "",
					},
				},
			},
			CustomProperties: models.CustomPropertiesData{
				DeployEnvironment: "",
				MonitoringEnabled: false,
				AutoScale:         false,
				Service:           "",
				App:               "_middleware-infrastructure",
				Image:             "Cdaprod/_middleware-infrastructure:9badff3",
				Ports:             []string{"8080"},
				Volumes:           []string{},
				Network:           "project-domain-namespace",
				Domain:            "_middleware-infrastructure.Cdaprod.dev",
			},
		},
	}

	// Save to PostgreSQL
	if err := db.Create(&item).Error; err != nil {
		log.Fatalf("Error saving item to DB: %v", err)
	}

	// Index in Elasticsearch
	if err := esRepo.IndexRegistryItem(item); err != nil {
		log.Fatalf("Error indexing item in Elasticsearch: %v", err)
	}

	log.Println("RegistryItem successfully saved and indexed.")
}

4. Integrate AI Capabilities

Leverage OpenAI's GPT models to handle natural language queries. Use a Go client library to interact with the OpenAI API.

Install OpenAI Go Client

We'll use the sashabaranov/go-openai client.

go get github.com/sashabaranov/go-openai

Initialize OpenAI Client

package ai

import (
	"context"
	"log"
	"os"

	openai "github.com/sashabaranov/go-openai"
)

type OpenAIClient struct {
	Client *openai.Client
}

func NewOpenAIClient() *OpenAIClient {
	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		log.Fatal("OPENAI_API_KEY is not set")
	}
	client := openai.NewClient(apiKey)
	return &OpenAIClient{Client: client}
}

func (c *OpenAIClient) GetAIResponse(messages []openai.ChatCompletionMessage) (string, error) {
	resp, err := c.Client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
		Model:    openai.GPT3Dot5Turbo,
		Messages: messages,
	})
	if err != nil {
		return "", err
	}

	if len(resp.Choices) == 0 {
		return "", nil
	}

	return resp.Choices[0].Message.Content, nil
}

Define the Chatbot Logic

package chatbot

import (
	"log"
	"time"

	"your_project/models"
	"your_project/repository"

	openai "github.com/sashabaranov/go-openai"
)

type Chatbot struct {
	AIClient *ai.OpenAIClient
	DB       *gorm.DB
	ESRepo   *repository.ElasticRepository
}

func NewChatbot(aiClient *ai.OpenAIClient, db *gorm.DB, esRepo *repository.ElasticRepository) *Chatbot {
	return &Chatbot{
		AIClient: aiClient,
		DB:       db,
		ESRepo:   esRepo,
	}
}

func (cb *Chatbot) HandleUserQuery(userName string, userNodeUUID string, query string) (string, error) {
	// Step 1: Use AI to interpret the query and extract intent
	intent, err := cb.extractIntent(query)
	if err != nil {
		return "I'm sorry, I couldn't understand your request.", err
	}

	// Step 2: Based on intent, interact with the database or Elasticsearch
	var response string
	switch intent.Action {
	case "GetStatus":
		item, err := cb.DB.First(&models.RegistryItem{}, "id = ?", intent.ServiceID).Error
		if err != nil {
			return "Service not found.", err
		}
		response = fmt.Sprintf("The status of %s is %s.", item.Name, item.Status)
	case "SearchServices":
		items, err := cb.ESRepo.SearchRegistryItems(intent.Query)
		if err != nil {
			return "Error searching for services.", err
		}
		response = cb.formatSearchResults(items)
	default:
		response = "I'm sorry, I didn't understand your request."
	}

	// Step 3: Log the interaction
	err = cb.logInteraction(userName, query, response)
	if err != nil {
		log.Println("Error logging interaction:", err)
	}

	return response, nil
}

type Intent struct {
	Action    string
	ServiceID string
	Query     string
}

func (cb *Chatbot) extractIntent(query string) (Intent, error) {
	messages := []openai.ChatCompletionMessage{
		{
			Role:    openai.ChatMessageRoleSystem,
			Content: "You are an assistant that helps users interact with a microservice registry. Extract the intent from the user's query.",
		},
		{
			Role:    openai.ChatMessageRoleUser,
			Content: query,
		},
	}

	response, err := cb.AIClient.GetAIResponse(messages)
	if err != nil {
		return Intent{}, err
	}

	// Assume the AI returns a JSON object with Action, ServiceID, and Query
	var intent Intent
	err = json.Unmarshal([]byte(response), &intent)
	if err != nil {
		return Intent{}, err
	}

	return intent, nil
}

func (cb *Chatbot) formatSearchResults(items []models.RegistryItem) string {
	if len(items) == 0 {
		return "No services found matching your query."
	}

	result := "Here are the services I found:\n"
	for _, item := range items {
		result += fmt.Sprintf("- %s (Status: %s)\n", item.Name, item.Status)
	}
	return result
}

func (cb *Chatbot) logInteraction(userName, userQuery, botResponse string) error {
	// Implement logging logic, e.g., save to the database
	interaction := models.Interaction{
		UserName:    userName,
		UserQuery:   userQuery,
		BotResponse: botResponse,
		Timestamp:   time.Now(),
	}
	return cb.DB.Create(&interaction).Error
}

Define the Interaction Model

package models

import "time"

// Interaction logs user queries and bot responses.
type Interaction struct {
	ID          uint      `gorm:"primaryKey"`
	UserName    string    `gorm:"not null"`
	UserQuery   string    `gorm:"not null"`
	BotResponse string    `gorm:"not null"`
	Timestamp   time.Time `gorm:"autoCreateTime"`
}

Migration

Ensure the Interaction model is migrated.

// In database/init.go after migrating RegistryItem
db.AutoMigrate(&models.Interaction{})

5. Build the AI Assistant Logic

Now, integrate the chatbot logic with your API to handle incoming user queries.

Define API Endpoints Using Gin

Install Gin

go get -u github.com/gin-gonic/gin

Set Up the Server

package main

import (
	"log"

	"your_project/ai"
	"your_project/chatbot"
	"your_project/database"
	"your_project/repository"

	"github.com/gin-gonic/gin"
	"github.com/joho/godotenv"
	"gorm.io/gorm"
)

func main() {
	// Load environment variables
	err := godotenv.Load()
	if err != nil {
		log.Println("No .env file found. Using environment variables.")
	}

	// Initialize database
	db := database.InitDB()

	// Initialize Elasticsearch
	esRepo := repository.NewElasticRepository(repository.InitElasticsearch(), "registry_items")

	// Initialize AI Client
	aiClient := ai.NewOpenAIClient()

	// Initialize Chatbot
	cb := chatbot.NewChatbot(aiClient, db, esRepo)

	// Set up Gin router
	router := gin.Default()

	// Define routes
	router.POST("/query", func(c *gin.Context) {
		var req struct {
			UserName string `json:"user_name" binding:"required"`
			Query    string `json:"query" binding:"required"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(400, gin.H{"error": err.Error()})
			return
		}

		// Handle user query
		response, err := cb.HandleUserQuery(req.UserName, "", req.Query) // userNodeUUID can be managed as needed
		if err != nil {
			c.JSON(500, gin.H{"error": "Internal server error."})
			return
		}

		c.JSON(200, gin.H{"response": response})
	})

	// Start server
	if err := router.Run(":8080"); err != nil {
		log.Fatalf("Failed to run server: %v", err)
	}
}

Example Request

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
    "user_name": "alice",
    "query": "What is the status of the _middleware-infrastructure service?"
}'

Example Response

{
  "response": "The status of Cdaprod/_middleware-infrastructure is active."
}

6. Ensure Robustness

To build a strong and reliable system, implement the following best practices:

Concurrency and Thread Safety

  • GORM: GORM's DB instance is safe for concurrent use by multiple goroutines.
  • Elasticsearch Client: The go-elasticsearch client is also safe for concurrent use.

Error Handling

  • Graceful Degradation: Provide meaningful error messages to users.
  • Logging: Use structured logging (e.g., with logrus or zap) for better log management.

Example with Structured Logging Using Logrus

go get github.com/sirupsen/logrus
package main

import (
	"github.com/sirupsen/logrus"
)

var log = logrus.New()

func main() {
	// Set log format
	log.SetFormatter(&logrus.JSONFormatter{})

	// Set log level
	log.SetLevel(logrus.InfoLevel)

	// Use log instead of standard log
	log.Info("Starting server...")
}

Security

  • Input Sanitization: Prevent SQL injection by using parameterized queries (handled by GORM).
  • API Authentication: Implement authentication mechanisms (e.g., API keys, OAuth) to secure your endpoints.
  • Environment Variables: Securely manage API keys and sensitive data, avoiding hardcoding them.

Testing

  • Unit Tests: Write tests for individual functions and methods.
  • Integration Tests: Test interactions between components (e.g., API endpoints with the database).
  • Mocking: Use mocking libraries to simulate external dependencies.

Example Unit Test for Chatbot

package chatbot

import (
	"testing"

	"your_project/ai"
	"your_project/repository"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"gorm.io/gorm"
)

// MockAIClient is a mock of OpenAIClient
type MockAIClient struct {
	mock.Mock
}

func (m *MockAIClient) GetAIResponse(messages []openai.ChatCompletionMessage) (string, error) {
	args := m.Called(messages)
	return args.String(0), args.Error(1)
}

func TestHandleUserQuery(t *testing.T) {
	// Setup mocks
	mockAI := new(MockAIClient)
	mockAI.On("GetAIResponse", mock.Anything).Return(`{"Action":"GetStatus","ServiceID":"8fedb80d-c984-436b-9281-e19302c97734","Query":""}`, nil)

	// Mock DB and Elasticsearch
	var db *gorm.DB // Initialize a test DB or mock as needed
	esRepo := &repository.ElasticRepository{} // Mock as needed

	// Initialize Chatbot
	cb := &Chatbot{
		AIClient: mockAI,
		DB:       db,
		ESRepo:   esRepo,
	}

	// Execute
	response, err := cb.HandleUserQuery("alice", "", "What is the status of the _middleware-infrastructure service?")

	// Assert
	assert.Nil(t, err)
	assert.Equal(t, "The status of Cdaprod/_middleware-infrastructure is active.", response)

	// Verify
	mockAI.AssertExpectations(t)
}

Logging and Monitoring

  • Logging: Implement comprehensive logging to track system behavior and troubleshoot issues.
  • Monitoring: Use tools like Prometheus and Grafana to monitor application performance and health.
  • Alerts: Set up alerts for critical issues (e.g., system downtime, high error rates).

Integrate Prometheus for Metrics

go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
	// Existing setup...

	// Add Prometheus metrics endpoint
	router.GET("/metrics", gin.WrapH(promhttp.Handler()))

	// Start server
	if err := router.Run(":8080"); err != nil {
		log.Fatalf("Failed to run server: %v", err)
	}
}

7. Develop an API for Interaction

You've already set up a basic API endpoint (/query). To enhance functionality, consider adding more endpoints for additional operations like adding new registry items, updating services, etc.

Example: Add New Registry Item

router.POST("/add", func(c *gin.Context) {
	var item models.RegistryItem
	if err := c.ShouldBindJSON(&item); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// Save to PostgreSQL
	if err := db.Create(&item).Error; err != nil {
		c.JSON(500, gin.H{"error": "Failed to add item."})
		return
	}

	// Index in Elasticsearch
	if err := esRepo.IndexRegistryItem(item); err != nil {
		c.JSON(500, gin.H{"error": "Failed to index item."})
		return
	}

	c.JSON(200, gin.H{"status": "Item added successfully."})
})

Example Request

curl -X POST http://localhost:8080/add \
-H "Content-Type: application/json" \
-d '{
    "id": "8fedb80d-c984-436b-9281-e19302c97734",
    "name": "Cdaprod/_middleware-infrastructure",
    "type": "service",
    "status": "active",
    "path": "https://github.com/Cdaprod/_middleware-infrastructure",
    "enabled": true,
    "has_dockerfile": true,
    "metadata": {
        "description": "Auto-generated configuration for Cdaprod/_middleware-infrastructure",
        "owner": "Cdaprod",
        "last_updated": "2024-10-15T16:23:59Z",
        "tags": [],
        "github": {
            "stars": 1,
            "forks": 0,
            "issues": 0,
            "pull_requests": {
                "open": 0,
                "closed": 0
            },
            "latest_commit": "2024-09-30T17:24:07Z",
            "license": "NOASSERTION",
            "topics": []
        },
        "docker": {
            "dockerfile": {
                "exposed_ports": ["8080"],
                "env_vars": [],
                "cmd": "",
                "entrypoint": "[/main]",
                "volumes": []
            },
            "docker_compose": {
                "services": ["middleware-infrastructure"],
                "ports": {
                    "middleware-infrastructure": ["8080"]
                },
                "volumes": {
                    "middleware-infrastructure": [""]
                },
                "env_vars": {
                    "middleware-infrastructure": ["GO_ENV", "PORT"]
                },
                "command": {
                    "middleware-infrastructure": ""
                }
            }
        },
        "custom_properties": {
            "deploy_environment": null,
            "monitoring_enabled": null,
            "auto_scale": null,
            "service": null,
            "app": "_middleware-infrastructure",
            "image": "Cdaprod/_middleware-infrastructure:9badff3",
            "ports": ["8080"],
            "volumes": [],
            "network": "project-domain-namespace",
            "domain": "_middleware-infrastructure.Cdaprod.dev"
        }
    }
}'

Example Response

{
  "status": "Item added successfully."
}

8. Continuous Improvement

To ensure your system remains robust and evolves with your needs, consider the following:

Data Refreshing

  • Automated Updates: Implement scheduled tasks to refresh data from GitHub repositories, Docker registries, etc.
  • Webhooks: Use GitHub and Docker webhooks to receive real-time updates and reflect changes in your registry.

Example: GitHub Webhook Handler

router.POST("/webhook/github", func(c *gin.Context) {
	var payload github.WebhookPayload
	if err := c.ShouldBindJSON(&payload); err != nil {
		c.JSON(400, gin.H{"error": "Invalid payload"})
		return
	}

	// Process the payload and update the registry
	go processGitHubWebhook(payload)

	c.JSON(200, gin.H{"status": "Webhook received"})
})

func processGitHubWebhook(payload github.WebhookPayload) {
	// Implement logic to update registry based on the webhook
}

Caching

  • Redis: Integrate Redis to cache frequent queries and reduce database load.
  • In-Memory Caching: Use Go's in-memory structures or libraries like groupcache for caching.

Example: Integrate Redis for Caching

go get github.com/go-redis/redis/v8
package cache

import (
	"context"
	"log"
	"os"
	"time"

	"github.com/go-redis/redis/v8"
)

type RedisClient struct {
	Client *redis.Client
}

func InitRedis() *RedisClient {
	rdb := redis.NewClient(&redis.Options{
		Addr:     os.Getenv("REDIS_ADDR"), // e.g., "localhost:6379"
		Password: os.Getenv("REDIS_PASSWORD"), // no password set
		DB:       0,  // use default DB
	})

	// Ping to test connection
	_, err := rdb.Ping(context.Background()).Result()
	if err != nil {
		log.Fatalf("Failed to connect to Redis: %v", err)
	}

	return &RedisClient{Client: rdb}
}

func (rc *RedisClient) Get(key string) (string, error) {
	return rc.Client.Get(context.Background(), key).Result()
}

func (rc *RedisClient) Set(key string, value string, expiration time.Duration) error {
	return rc.Client.Set(context.Background(), key, value, expiration).Err()
}

Usage in Chatbot

func (cb *Chatbot) HandleUserQuery(userName string, userNodeUUID string, query string) (string, error) {
	cacheKey := fmt.Sprintf("query:%s", query)
	// Check cache first
	cachedResponse, err := cacheClient.Get(cacheKey)
	if err == nil && cachedResponse != "" {
		return cachedResponse, nil
	}

	// Proceed with handling the query
	response, err := cb.processQuery(userName, userNodeUUID, query)
	if err != nil {
		return response, err
	}

	// Cache the response
	cacheClient.Set(cacheKey, response, 10*time.Minute)

	return response, nil
}

Scalability

  • Load Balancing: Use tools like NGINX or HAProxy to distribute traffic across multiple instances.
  • Containerization: Containerize your application using Docker for easy deployment and scaling.
  • Orchestration: Utilize Kubernetes for managing containerized applications at scale.

Feedback Loop

  • User Feedback: Implement mechanisms to collect user feedback on AI responses to continuously improve the system.
  • Analytics: Analyze interaction logs to identify common queries and areas for improvement.

9. Deployment and Monitoring

Deploy your application in a production environment and set up monitoring to ensure its reliability.

Containerization with Docker

Create a Dockerfile

# Start from the official Golang image
FROM golang:1.20-alpine AS builder

# Set environment variables
ENV GO111MODULE=on
ENV CGO_ENABLED=0

# Set working directory
WORKDIR /app

# Copy go.mod and go.sum
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy the source code
COPY . .

# Build the application
RUN go build -o registry-app .

# Start a new stage from scratch
FROM alpine:latest

# Set working directory
WORKDIR /root/

# Copy the binary from the builder
COPY --from=builder /app/registry-app .

# Copy any necessary files (e.g., config files)
COPY --from=builder /app/.env .

# Expose port
EXPOSE 8080

# Run the executable
CMD ["./registry-app"]

Build and Run the Docker Image

# Build the Docker image
docker build -t registry-app:latest .

# Run the Docker container
docker run -d -p 8080:8080 --env-file .env registry-app:latest

Orchestration with Kubernetes

Create Deployment and Service Manifests

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: registry-app
  template:
    metadata:
      labels:
        app: registry-app
    spec:
      containers:
      - name: registry-app
        image: registry-app:latest
        ports:
        - containerPort: 8080
        envFrom:
        - configMapRef:
            name: registry-app-config
        - secretRef:
            name: registry-app-secrets

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: registry-app-service
spec:
  selector:
    app: registry-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: LoadBalancer

Deploy to Kubernetes

# Apply ConfigMaps and Secrets first
kubectl apply -f configmap.yaml
kubectl apply -f secrets.yaml

# Deploy the application
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

Monitoring with Prometheus and Grafana

Set Up Prometheus

Deploy Prometheus in your Kubernetes cluster and configure it to scrape metrics from your application.

Set Up Grafana

Deploy Grafana and configure it to visualize metrics from Prometheus.

Integrate Prometheus Metrics

Use the promhttp handler in your Gin server to expose metrics.

import (
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

// Define metrics
var (
	queryCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "user_queries_total",
			Help: "Total number of user queries received",
		},
		[]string{"action"},
	)
)

func init() {
	// Register metrics
	prometheus.MustRegister(queryCounter)
}

func main() {
	// Existing setup...

	// Define routes
	router.POST("/query", func(c *gin.Context) {
		// Increment counter based on action
		// Assume action is extracted from intent
		queryCounter.WithLabelValues("handle_query").Inc()

		// Existing query handling...
	})

	// Expose metrics
	router.GET("/metrics", gin.WrapH(promhttp.Handler()))

	// Start server
	if err := router.Run(":8080"); err != nil {
		log.Fatalf("Failed to run server: %v", err)
	}
}

10. Sample Interaction Flow

User Input

{
    "user_name": "alice",
    "query": "What is the status of the _middleware-infrastructure service?"
}

Assistant Processing

  1. Receive Request: The /query endpoint receives the user's request.
  2. Extract Intent: The chatbot uses OpenAI's API to parse the intent, identifying that the user wants to know the status of a specific service.
  3. Database Query: It queries PostgreSQL to retrieve the status of _middleware-infrastructure.
  4. Generate Response: Formats the response based on the retrieved data.
  5. Log Interaction: Saves the interaction details in the interactions table.
  6. Respond to User: Sends back the status information to the user.

Assistant Response

{
    "response": "The status of Cdaprod/_middleware-infrastructure is active."
}

11. Continuous Integration and Deployment (CI/CD)

Automate your build, test, and deployment processes using CI/CD pipelines.

Example with GitHub Actions

.github/workflows/ci-cd.yaml

name: CI/CD Pipeline

on:
  push:
    branches:
      - main

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - name: Checkout Code
      uses: actions/checkout@v2

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.20

    - name: Install Dependencies
      run: go mod download

    - name: Run Tests
      run: go test ./...

    - name: Build Docker Image
      run: docker build -t registry-app:latest .

    - name: Push to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
    
    - name: Push Image
      run: docker push registry-app:latest

  deploy:

    needs: build
    runs-on: ubuntu-latest

    steps:
    - name: Deploy to Kubernetes
      uses: azure/setup-kubectl@v1
      with:
        version: 'v1.20.0'
    - run: kubectl apply -f deployment.yaml
    - run: kubectl apply -f service.yaml

Secrets Configuration

Ensure you set the necessary secrets (DOCKER_USERNAME, DOCKER_PASSWORD, etc.) in your GitHub repository settings.


12. Additional Enhancements

To further strengthen your AI assistant and registry system, consider implementing the following:

Authentication and Authorization

  • API Security: Use JWT tokens or OAuth2 for securing your API endpoints.
  • Role-Based Access Control (RBAC): Define roles and permissions to control access to different functionalities.

Example: JWT Authentication Middleware

package middleware

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/dgrijalva/jwt-go"
)

func AuthMiddleware(secret string) gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
			c.Abort()
			return
		}

		tokenString := strings.TrimPrefix(authHeader, "Bearer ")
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			// Validate the algorithm
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method")
			}
			return []byte(secret), nil
		})

		if err != nil || !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
			c.Abort()
			return
		}

		c.Next()
	}
}

Apply Middleware to Routes

router.Use(middleware.AuthMiddleware(os.Getenv("JWT_SECRET")))

Rate Limiting

Prevent abuse by limiting the number of requests a user can make in a given time frame.

Implement Rate Limiting Using Gin Middleware

package middleware

import (
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/time/rate"
)

func RateLimiter(r rate.Limit, b int) gin.HandlerFunc {
	limiter := rate.NewLimiter(r, b)

	return func(c *gin.Context) {
		if !limiter.Allow() {
			c.JSON(429, gin.H{"error": "Too many requests"})
			c.Abort()
			return
		}
		c.Next()
	}
}

Apply Rate Limiting to Routes

router.POST("/query", middleware.RateLimiter(1, 5), func(c *gin.Context) {
	// Handler code
})

Data Validation

Ensure that incoming data conforms to expected formats to prevent errors and security vulnerabilities.

Example Using Gin's Binding and Validation

type AddRegistryItemRequest struct {
	ID            string                 `json:"id" binding:"required,uuid"`
	Name          string                 `json:"name" binding:"required"`
	Type          string                 `json:"type" binding:"required"`
	Status        string                 `json:"status" binding:"required"`
	Path          string                 `json:"path" binding:"required,url"`
	Enabled       bool                   `json:"enabled"`
	HasDockerfile bool                   `json:"has_dockerfile"`
	Metadata      models.RegistryMetadata `json:"metadata" binding:"required"`
}

router.POST("/add", func(c *gin.Context) {
	var req AddRegistryItemRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// Convert request to RegistryItem
	item := models.RegistryItem{
		ID:            req.ID,
		Name:          req.Name,
		Type:          req.Type,
		Status:        req.Status,
		Path:          req.Path,
		Enabled:       req.Enabled,
		HasDockerfile: req.HasDockerfile,
		Metadata:      req.Metadata,
		CreatedAt:     time.Now(),
		LastUpdated:   time.Now(),
	}

	// Save to PostgreSQL and Elasticsearch...
})

Internationalization (i18n)

Support multiple languages to cater to a diverse user base.

Example: Integrate with a Translation Service

You can integrate with services like Google Translate API or Microsoft Translator to translate user queries and AI responses.

func (cb *Chatbot) TranslateText(text, targetLang string) (string, error) {
	// Implement translation logic using an external API
}

Conclusion

By following this comprehensive guide, you've successfully adapted the LangGraph and Graphiti-based AI assistant to a Go-centric architecture without relying on Neo4j. Your system now leverages PostgreSQL for data storage, Elasticsearch for advanced search capabilities, and OpenAI's API for AI-driven responses. Additionally, you've incorporated best practices for robustness, scalability, security, and continuous improvement.

Next Steps

  1. Prototype and Testing: Implement the core functionalities and test them thoroughly.
  2. User Interface: Develop a frontend interface (e.g., web dashboard) to interact with the AI assistant.
  3. Advanced AI Features: Enhance the AI assistant with more sophisticated natural language understanding and context management.
  4. Documentation: Create comprehensive documentation for your system to facilitate maintenance and onboarding.
  5. Feedback Mechanism: Implement ways to collect and analyze user feedback to continuously improve the assistant.

Feel free to reach out if you need further assistance or have specific questions regarding any part of the implementation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment