Back to Blog

Golang을 사용하여 Agora 대화형 AI 서비스를 구축하세요.

대화형 AI는 사람들이 인공지능과 상호작용하는 방식을 혁신하고 있습니다. 사용자는 텍스트 프롬프트를 세심하게 작성하는 대신 AI 에이전트와 자연스럽고 실시간 음성 대화를 나눌 수 있습니다. 이는 직관적이고 효율적인 상호작용의 새로운 기회를 열어줍니다.

많은 개발자들이 텍스트 기반 에이전트를 위해 맞춤형 LLM 워크플로우를 구축하는 데 상당한 시간을 투자해 왔습니다. Agora의 대화형 AI 엔진은 기존 워크플로우를 Agora 채널에 연결하여 현재 AI 인프라를 포기하지 않고도 실시간 음성 대화를 가능하게 합니다.

이 가이드에서는 사용자와 Agora의 대화형 AI 사이의 연결을 처리하는 Go 서버를 구축하는 방법을 단계별로 안내합니다. 완료 시에는 애플리케이션에 음성 기반 AI 대화를 지원할 수 있는 생산 환경에 적합한 백엔드를 확보하게 됩니다.

필수 조건

시작하기 전에 다음을 확인하세요:

프로젝트 설정

필요한 의존성을 설치하여 Golang 프로젝트를 설정해 보겠습니다. 먼저 새로운 디렉토리를 생성하고 Go 모듈을 초기화합니다:

mkdir agora-convo-ai-go-servercd agora-convo-ai-go-servergo mod init github.com/AgoraIO-Community/convo-ai-go-server

다음으로 서버에 필요한 주요 의존성을 추가합니다:

go get github.com/gin-gonic/gingo get github.com/joho/godotenvgo get github.com/AgoraIO-Community/go-tokenbuilder

초기 디렉토리 구조를 생성하고, 가이드를 진행하면서 필요한 파일을 이 디렉토리에 추가해 나갈 것입니다.

mkdir -p convoai token_service http_headers validationtouch .env

프로젝트 디렉토리는 다음과 같은 구조를 갖게 됩니다:

├── convoai/├── token_service/├── http_headers/├── validation/├── .env├── go.mod├── go.sum

서버 진입점

먼저 서버의 진입점이 될 주요 애플리케이션 파일을 설정합니다. 환경 변수를 로드하고 구성 설정을 수행한 후 적절한 미들웨어와 라우트를 설정하여 라우터를 초기화합니다.

main.go 파일을 생성합니다:

touch main.go
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

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

func loadConfig() (*convoai.ConvoAIConfig, error) {
	config := &convoai.ConvoAIConfig{
		// Agora Configuration
		AppID:          os.Getenv("AGORA_APP_ID"),
		AppCertificate: os.Getenv("AGORA_APP_CERTIFICATE"),
		CustomerID:     os.Getenv("AGORA_CUSTOMER_ID"),
		CustomerSecret: os.Getenv("AGORA_CUSTOMER_SECRET"),
		BaseURL:        os.Getenv("AGORA_CONVO_AI_BASE_URL"),
		AgentUID:       os.Getenv("AGENT_UID"),

		// LLM Configuration
		LLMModel: os.Getenv("LLM_MODEL"),
		LLMURL:   os.Getenv("LLM_URL"),
		LLMToken: os.Getenv("LLM_TOKEN"),

		// TTS Configuration
		TTSVendor: os.Getenv("TTS_VENDOR"),
	}

	// Microsoft TTS Configuration
	if msKey := os.Getenv("MICROSOFT_TTS_KEY"); msKey != "" {
		config.MicrosoftTTS = &convoai.MicrosoftTTSConfig{
			Key:       msKey,
			Region:    os.Getenv("MICROSOFT_TTS_REGION"),
			VoiceName: os.Getenv("MICROSOFT_TTS_VOICE_NAME"),
			Rate:      os.Getenv("MICROSOFT_TTS_RATE"),
			Volume:    os.Getenv("MICROSOFT_TTS_VOLUME"),
		}
	}

	// ElevenLabs TTS Configuration
	if elKey := os.Getenv("ELEVENLABS_API_KEY"); elKey != "" {
		config.ElevenLabsTTS = &convoai.ElevenLabsTTSConfig{
			Key:     elKey,
			VoiceID: os.Getenv("ELEVENLABS_VOICE_ID"),
			ModelID: os.Getenv("ELEVENLABS_MODEL_ID"),
		}
	}

	// Modalities Configuration
	config.InputModalities = os.Getenv("INPUT_MODALITIES")
	config.OutputModalities = os.Getenv("OUTPUT_MODALITIES")

	return config, nil
}

func setupServer() *http.Server {
	log.Println("Starting setupServer")
	if err := godotenv.Load(); err != nil {
		log.Println("Warning: Error loading .env file. Using existing environment variables.")
	}

	// Load configuration
	config, err := loadConfig()
	if err != nil {
		log.Fatal("Failed to load configuration:", err)
	}

        // TODO: Validate environment configuration

	// Server Configuration
	serverPort := os.Getenv("PORT")
	if serverPort == "" {
		serverPort = "8080"
	}

	// CORS Configuration
	corsAllowOrigin := os.Getenv("CORS_ALLOW_ORIGIN")

	// Set up router with headers
	router := gin.Default()
	//TODO: Register headers

	// TODO: Initialize services & register routes

	// Register healthcheck route
	router.GET("/ping", Ping)

	// Configure and start the HTTP server
	server := &http.Server{
		Addr:    ":" + serverPort,
		Handler: router,
	}

	log.Println("Server setup completed")
	log.Println("- listening on port", serverPort)
	return server
}

func main() {
	server := setupServer()

	// Start the server in a separate goroutine to handle graceful shutdown.
	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}

	}()

	// Prepare to handle graceful shutdown.
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)

	// Wait for a shutdown signal.
	<-quit
	log.Println("Shutting down server...")

	// Attempt to gracefully shutdown the server with a timeout of 5 seconds.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := server.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown:", err)
	}

	log.Println("Server exiting")
}

// Ping is a handler function that serves as a basic health check endpoint.
func Ping(c *gin.Context) {
	c.JSON(200, gin.H{
		"message": "pong",
	})
}
참고: 환경 변수에서 PORT를 로드하고 있으며, .env 파일에 설정되지 않은 경우 기본값으로 8080으로 설정됩니다.

기본 Go 서버를 테스트하려면 다음을 실행하세요:

go run main.go

콘솔에 “Server setup completed”와 “- listening on port 8080”이 표시되어야 합니다.

브라우저에서 http://localhost:8080/ping로 이동하여 서버가 정상적으로 작동하는지 확인할 수 있습니다. 응답으로 {“message”: “pong”}가 표시되어야 합니다.

curl을 사용하여 서버를 테스트하려면 다음 명령어를 실행하세요:

curl http://localhost:8080/ping

응답으로 {“message”: “pong”}가 표시되어야 합니다.

형식 정의

다음으로 ConvoAI 서비스에 필요한 형식을 정의해 보겠습니다. convoai-types.go라는 파일을convoai디렉토리에 생성합니다.

touch convoai/convoai-types.go

다음 형식을 추가합니다:

package convoai

// InviteAgentRequest represents the request body for inviting an AI agent
type InviteAgentRequest struct {
	RequesterID      string   `json:"requester_id"`
	ChannelName      string   `json:"channel_name"`
	RtcCodec         *int     `json:"rtc_codec,omitempty"`
	InputModalities  []string `json:"input_modalities,omitempty"`
	OutputModalities []string `json:"output_modalities,omitempty"`
}

// RemoveAgentRequest represents the request body for removing an AI agent
type RemoveAgentRequest struct {
	AgentID string `json:"agent_id"`
}

// TTSVendor represents the text-to-speech vendor type
type TTSVendor string

const (
	TTSVendorMicrosoft  TTSVendor = "microsoft"
	TTSVendorElevenLabs TTSVendor = "elevenlabs"
)

// TTSConfig represents the text-to-speech configuration
type TTSConfig struct {
	Vendor TTSVendor   `json:"vendor"`
	Params interface{} `json:"params"`
}

// AgoraStartRequest represents the request to start a conversation
type AgoraStartRequest struct {
	Name       string     `json:"name"`
	Properties Properties `json:"properties"`
}

// Properties represents the configuration properties for the conversation
type Properties struct {
	Channel          string    `json:"channel"`
	Token            string    `json:"token"`
	AgentRtcUID      string    `json:"agent_rtc_uid"`
	RemoteRtcUIDs    []string  `json:"remote_rtc_uids"`
	EnableStringUID  bool      `json:"enable_string_uid"`
	IdleTimeout      int       `json:"idle_timeout"`
	ASR              ASR       `json:"asr"`
	LLM              LLM       `json:"llm"`
	TTS              TTSConfig `json:"tts"`
	VAD              VAD       `json:"vad"`
	AdvancedFeatures Features  `json:"advanced_features"`
}

// ASR represents the Automatic Speech Recognition configuration
type ASR struct {
	Language string `json:"language"`
	Task     string `json:"task"`
}

// LLM represents the Language Learning Model configuration
type LLM struct {
	URL              string          `json:"url"`
	APIKey           string          `json:"api_key"`
	SystemMessages   []SystemMessage `json:"system_messages"`
	GreetingMessage  string          `json:"greeting_message"`
	FailureMessage   string          `json:"failure_message"`
	MaxHistory       int             `json:"max_history"`
	Params           LLMParams       `json:"params"`
	InputModalities  []string        `json:"input_modalities"`
	OutputModalities []string        `json:"output_modalities"`
}

// SystemMessage represents a system message in the conversation
type SystemMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

// LLMParams represents the parameters for the Language Learning Model
type LLMParams struct {
	Model       string  `json:"model"`
	MaxTokens   int     `json:"max_tokens"`
	Temperature float64 `json:"temperature"`
	TopP        float64 `json:"top_p"`
}

// VAD represents the Voice Activity Detection configuration
type VAD struct {
	SilenceDurationMS   int     `json:"silence_duration_ms"`
	SpeechDurationMS    int     `json:"speech_duration_ms"`
	Threshold           float64 `json:"threshold"`
	InterruptDurationMS int     `json:"interrupt_duration_ms"`
	PrefixPaddingMS     int     `json:"prefix_padding_ms"`
}

// Features represents advanced features configuration
type Features struct {
	EnableAIVAD bool `json:"enable_aivad"`
	EnableBHVS  bool `json:"enable_bhvs"`
}

// InviteAgentResponse represents the response for an agent invitation
type InviteAgentResponse struct {
	AgentID  string `json:"agent_id"`
	CreateTS int64  `json:"create_ts"`
	Status   string `json:"status"`
}

// RemoveAgentResponse represents the response for an agent removal
type RemoveAgentResponse struct {
	Success bool   `json:"success"`
	AgentID string `json:"agent_id"`
}

// ConvoAIConfig holds all configuration for the ConvoAI service
type ConvoAIConfig struct {
	// Agora Configuration
	AppID          string
	AppCertificate string
	CustomerID     string
	CustomerSecret string
	BaseURL        string
	AgentUID       string

	// LLM Configuration
	LLMModel string
	LLMURL   string
	LLMToken string

	// TTS Configuration
	TTSVendor     string
	MicrosoftTTS  *MicrosoftTTSConfig
	ElevenLabsTTS *ElevenLabsTTSConfig

	// Modalities Configuration
	InputModalities  string
	OutputModalities string
}

// MicrosoftTTSConfig holds Microsoft TTS specific configuration
type MicrosoftTTSConfig struct {
	Key       string `json:"key"`
	Region    string `json:"region"`
	VoiceName string `json:"voice_name"`
	Rate      string `json:"rate"`
	Volume    string `json:"volume"`
}

// ElevenLabsTTSConfig holds ElevenLabs TTS specific configuration
type ElevenLabsTTSConfig struct {
	Key     string `json:"key"`
	VoiceID string `json:"voice_id"`
	ModelID string `json:"model_id"`
}

이 새로운 유형들은 다음 단계에서 조립할 모든 구성 요소에 대한 이해를 제공합니다. 고객 요청을 받아 AgoraStartRequest를 구성하고 Agora의 대화형 AI 엔진으로 전송합니다. Agora의 Convo AI 엔진은 에이전트를 대화로 추가합니다.

ConvoAI 서비스

유형이 정의되었으니, 대화에서 에이전트를 초대하고 제거하는 에이전트 경로를 구현해 보겠습니다.

convoai-service.go 파일을 생성합니다:

touch convoai/convoai-service.go

gin과 agora-token 라이브러리를 import합니다. 에이전트 토큰을 생성해야 하기 때문입니다. 그 다음 에이전트 경로를 등록하고 설정합니다. 이 함수들은 요청을 해당 핸들러로 전달하기 전에 유효성 검사를 수행합니다.

package convoai

import (
	"net/http"

	"github.com/AgoraIO-Community/convo-ai-go-server/token_service"
	"github.com/gin-gonic/gin"
)

// ConvoAIService handles AI conversation functionality
type ConvoAIService struct {
	config       *ConvoAIConfig
	tokenService *token_service.TokenService
}

// NewConvoAIService creates a new ConvoAIService instance
func NewConvoAIService(config *ConvoAIConfig, tokenService *token_service.TokenService) *ConvoAIService {
	return &ConvoAIService{
		config:       config,
		tokenService: tokenService,
	}
}

// Register the ConvoAI service routes
func (s *ConvoAIService) RegisterRoutes(router *gin.Engine) {
	agent := router.Group("/agent")
	agent.POST("/invite", s.InviteAgent)
	agent.POST("/remove", s.RemoveAgent)
}

// InviteAgent handles the agent invitation request
func (s *ConvoAIService) InviteAgent(c *gin.Context) {
	var req InviteAgentRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Validate the request
	if err := s.validateInviteRequest(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Call the handler
	response, err := s.HandleInviteAgent(req)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, response)
}

// RemoveAgent handles the agent removal request
func (s *ConvoAIService) RemoveAgent(c *gin.Context) {
	var req RemoveAgentRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Validate the request
	if err := s.validateRemoveRequest(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Call the handler
	response, err := s.HandleRemoveAgent(req)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, response)
}

에이전트 초대 핸들러

다음으로, 여러 핵심 작업을 처리해야 하는 초대 핸들러를 구현합니다:

  • AI 에이전트가 RTC 채널에 액세스하기 위한 토큰을 생성합니다.
  • 텍스트-투-스피치(Microsoft 또는 ElevenLabs)를 구성합니다.
  • AI 에이전트의 프롬프트 및 인사 메시지를 정의합니다.
  • 대화 흐름을 제어하는 음성 활동 감지(VAD)를 구성합니다.
  • Agora의 Conversational AI 엔진에 시작 요청을 전송합니다.
  • Agora의 Convo AI 엔진 응답에서 AgentID를 포함하여 클라이언트에 응답을 반환합니다.

convoai_handler_invite.go 파일을 생성합니다:

convoai/convoai_handler_invite.go를 터치합니다.

다음 내용을 추가합니다:

package convoai

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"crypto/rand"

	"github.com/AgoraIO-Community/convo-ai-go-server/token_service"
)

// HandleInviteAgent processes the agent invitation request
func (s *ConvoAIService) HandleInviteAgent(req InviteAgentRequest) (*InviteAgentResponse, error) {
	// Generate token for the agent
	tokenReq := token_service.TokenRequest{
		TokenType: "rtc",
		Channel:   req.ChannelName,
		Uid:       "0",
		RtcRole:   "publisher",
	}

	token, err := s.tokenService.GenRtcToken(tokenReq)
	if err != nil {
		return nil, fmt.Errorf("failed to generate token: %v", err)
	}

	// Get TTS config based on vendor
	ttsConfig, err := s.getTTSConfig()
	if err != nil {
		return nil, fmt.Errorf("failed to get TTS config: %v", err)
	}

	// Set up system message for AI behavior
	systemMessage := SystemMessage{
		Role:    "system",
		Content: "You are a helpful assistant. Pretend that the text input is audio, and you are responding to it. Speak fast, clearly, and concisely.",
	}

	// Set default modalities if not provided
	inputModalities := req.InputModalities
	if len(inputModalities) == 0 {
		inputModalities = []string{"text"}
	}

	outputModalities := req.OutputModalities
	if len(outputModalities) == 0 {
		outputModalities = []string{"text", "audio"}
	}

	// Build the request body for Agora Conversation AI service
	agoraReq := AgoraStartRequest{
		Name: fmt.Sprintf("agent-%d-%s", time.Now().UnixNano(), randomString(6)),
		Properties: Properties{
			Channel:         req.ChannelName,
			Token:           token,
			AgentRtcUID:     s.config.AgentUID,
			RemoteRtcUIDs:   getRemoteRtcUIDs(req.RequesterID),
			EnableStringUID: isStringUID(req.RequesterID),
			IdleTimeout:     30,
			ASR: ASR{
				Language: "en-US",
				Task:     "conversation",
			},
			LLM: LLM{
				URL:             s.config.LLMURL,
				APIKey:          s.config.LLMToken,
				SystemMessages:  []SystemMessage{systemMessage},
				GreetingMessage: "Hello! How can I assist you today?",
				FailureMessage:  "Please wait a moment.",
				MaxHistory:      10,
				Params: LLMParams{
					Model:       s.config.LLMModel,
					MaxTokens:   1024,
					Temperature: 0.7,
					TopP:        0.95,
				},
				InputModalities:  inputModalities,
				OutputModalities: outputModalities,
			},
			TTS: *ttsConfig,
			VAD: VAD{
				SilenceDurationMS:   480,
				SpeechDurationMS:    15000,
				Threshold:           0.5,
				InterruptDurationMS: 160,
				PrefixPaddingMS:     300,
			},
			AdvancedFeatures: Features{
				EnableAIVAD: false,
				EnableBHVS:  false,
			},
		},
	}

	// Debug logging
	prettyJSON, _ := json.MarshalIndent(agoraReq, "", "  ")
	fmt.Printf("Sending request to start agent: %s\n", string(prettyJSON))

	// Convert request to JSON
	jsonData, err := json.Marshal(agoraReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %v", err)
	}

	// Create the HTTP request
	url := fmt.Sprintf("%s/%s/join", s.config.BaseURL, s.config.AppID)
	fmt.Printf("URL: %s\n", url)
	httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %v", err)
	}

	// Add headers
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Authorization", s.getBasicAuth())

	// Send the request using a client with a timeout
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %v (URL: %s)", err, url)
	}
	defer resp.Body.Close()

	// Handle response
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("failed to start conversation: status=%d, body=%s, url=%s, headers=%v",
			resp.StatusCode, string(body), url, httpReq.Header)
	}

	// Parse the response
	var agoraResp map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&agoraResp); err != nil {
		return nil, fmt.Errorf("failed to decode response: %v", err)
	}

	// Create the response
	response := &InviteAgentResponse{
		AgentID:  agoraResp["agent_id"].(string),
		CreateTS: time.Now().Unix(),
		Status:   "RUNNING",
	}

	return response, nil
}

// getRemoteRtcUIDs returns the appropriate RemoteRtcUIDs array based on the requesterID
func getRemoteRtcUIDs(requesterID string) []string {
	return []string{requesterID}
}

// Add this helper function
func randomString(n int) string {
	const letters = "abcdefghijklmnopqrstuvwxyz"
	b := make([]byte, n)
	rand.Read(b)
	for i := range b {
		b[i] = letters[int(b[i])%len(letters)]
	}
	return string(b)
}

에이전트 핸들러 제거

에이전트가 대화실에 참여한 후에는 해당 에이전트를 대화실에서 제거하는 방법이 필요합니다. 이때 제거 핸들러가 사용됩니다. 이 핸들러는 agentID를 받아 Agora의 Conversational AI 엔진에 요청을 전송하여 에이전트를 채널에서 제거합니다.

convoai_handler_remove.go 파일을 생성합니다:

touch convoai/convoai_handler_remove.go

다음 내용을 추가하세요:

package convoai

import (
 "fmt"
 "net/http"
 "time"
)

// HandleRemoveAgent processes the agent removal request
func (s *ConvoAIService) HandleRemoveAgent(req RemoveAgentRequest) (*RemoveAgentResponse, error) {
 // Create the HTTP request
 url := fmt.Sprintf("%s/%s/agents/%s/leave", s.config.BaseURL, s.config.AppID, req.AgentID)
 httpReq, err := http.NewRequest("POST", url, nil)
 if err != nil {
  return nil, fmt.Errorf("failed to create request: %v", err)
 }

 // Add headers
 auth := s.getBasicAuth()
 httpReq.Header.Set("Content-Type", "application/json")
 httpReq.Header.Set("Authorization", auth)
 // Send the request using a client with a timeout
 client := &http.Client{Timeout: 10 * time.Second}
 resp, err := client.Do(httpReq)
 if err != nil {
  return nil, fmt.Errorf("failed to send request: %v", err)
 }
 defer resp.Body.Close()
 if resp.StatusCode != http.StatusOK {
  return nil, fmt.Errorf("failed to remove agent: %d", resp.StatusCode)
 }

 // Return success response
 response := &RemoveAgentResponse{
  Success: true,
  AgentID: req.AgentID,
 }
 return response, nil
}

유틸리티 함수

초대 및 제거 경로 모두에서 요청 헤더에 BasicAuthorization을 사용해야 하므로, 이를 처리하기 위해 유틸리티 함수를 설정하겠습니다.

또 다른 유틸리티로 getTTSConfig를 구축해야 합니다.이 함수를 별도로 호출해야 하는 이유는 일반적으로 단일 TTS 구성만 사용하기 때문입니다. 데모 목적으로 Agora의 Convo AI 엔진이 지원하는 모든 TTS 공급업체의 구성을 구현하는 방법을 보여주기 위해 이 방식으로 구축했습니다.

convoai-utils.go 파일을 생성합니다:

touch convoai/convoai-utils.go

다음 내용을 추가하세요:

package convoai

import (
 "encoding/base64"
 "errors"
 "fmt"
 "strconv"
)

func (s *ConvoAIService) getBasicAuth() string {
 auth := fmt.Sprintf("%s:%s", s.config.CustomerID, s.config.CustomerSecret)
 return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}

// Helper function to check if the string is purely numeric (false) or contains any non-digit characters (true)
func isStringUID(s string) bool {
 for _, r := range s {
  if r < '0' || r > '9' {
   return true // Contains non-digit character
  }
 }
 return false // Contains only digits
}

// getTTSConfig returns the appropriate TTS configuration based on the configured vendor
func (s *ConvoAIService) getTTSConfig() (*TTSConfig, error) {
 switch s.config.TTSVendor {
 case string(TTSVendorMicrosoft):
  if s.config.MicrosoftTTS == nil ||
   s.config.MicrosoftTTS.Key == "" ||
   s.config.MicrosoftTTS.Region == "" ||
   s.config.MicrosoftTTS.VoiceName == "" ||
   s.config.MicrosoftTTS.Rate == "" ||
   s.config.MicrosoftTTS.Volume == "" {
   return nil, fmt.Errorf("missing Microsoft TTS configuration")
  }
  // Convert rate and volume from string to float64
  rate, err := strconv.ParseFloat(s.config.MicrosoftTTS.Rate, 64)
  if err != nil {
   return nil, fmt.Errorf("invalid rate value: %v", err)
  }
  volume, err := strconv.ParseFloat(s.config.MicrosoftTTS.Volume, 64)
  if err != nil {
   return nil, fmt.Errorf("invalid volume value: %v", err)
  }
  return &TTSConfig{
   Vendor: TTSVendorMicrosoft,
   Params: map[string]interface{}{
    "key":        s.config.MicrosoftTTS.Key,
    "region":     s.config.MicrosoftTTS.Region,
    "voice_name": s.config.MicrosoftTTS.VoiceName,
    "rate":       rate,
    "volume":     volume,
   },
  }, nil
 case string(TTSVendorElevenLabs):
  if s.config.ElevenLabsTTS == nil ||
   s.config.ElevenLabsTTS.Key == "" ||
   s.config.ElevenLabsTTS.ModelID == "" ||
   s.config.ElevenLabsTTS.VoiceID == "" {
   return nil, fmt.Errorf("missing ElevenLabs TTS configuration")
  }
  return &TTSConfig{
   Vendor: TTSVendorElevenLabs,
   Params: map[string]interface{}{
    "api_key":  s.config.ElevenLabsTTS.Key,
    "model_id": s.config.ElevenLabsTTS.ModelID,
    "voice_id": s.config.ElevenLabsTTS.VoiceID,
   },
  }, nil
 default:
  return nil, fmt.Errorf("unsupported TTS vendor: %s", s.config.TTSVendor)
 }
}

// validateInviteRequest validates the invite agent request
func (s *ConvoAIService) validateInviteRequest(req *InviteAgentRequest) error {
 if req.RequesterID == "" {
  return errors.New("requester_id is required")
 }
 if req.ChannelName == "" {
  return errors.New("channel_name is required")
 }

 // Validate channel_name length
 if len(req.ChannelName) < 3 || len(req.ChannelName) > 64 {
  return errors.New("channel_name length must be between 3 and 64 characters")
 }
 return nil
}

// validateRemoveRequest validates the remove agent request
func (s *ConvoAIService) validateRemoveRequest(req *RemoveAgentRequest) error {
 if req.AgentID == "" {
  return errors.New("agent_id is required")
 }
 return nil
}

HTTP 헤더

헤더 관련 논리를 모두 처리하려면 httpHeaders.go 파일을 생성하세요:

To handle all header-related logic, create the httpHeaders.go file: 

touch http_headers/httpHeaders.go

다음 내용을 추가하세요:

package http_headers

import (
 "net/http"
 "strings"
 "time"
 "github.com/gin-gonic/gin"
)

// HttpHeaders holds configurations for handling requests, such as CORS settings.
type HttpHeaders struct {
 AllowOrigin string // List of origins allowed to access the resources.
}

// NewHttpHeaders initializes and returns a new Middleware object with specified CORS settings.
func NewHttpHeaders(allowOrigin string) *HttpHeaders {
 return &HttpHeaders{AllowOrigin: allowOrigin}
}

// NoCache sets HTTP headers to prevent client-side caching of responses.
func (m *HttpHeaders) NoCache() gin.HandlerFunc {
 return func(c *gin.Context) {
  // Set multiple cache-related headers to ensure responses are not cached.
  c.Header("Cache-Control", "private, no-cache, no-store, must-revalidate")
  c.Header("Expires", "-1")
  c.Header("Pragma", "no-cache")
 }
}

// CORShttpHeaders adds CORS (Cross-Origin Resource Sharing) headers to responses and handles pre-flight requests.
// It allows web applications at different domains to interact more securely.
func (m *HttpHeaders) CORShttpHeaders() gin.HandlerFunc {
 return func(c *gin.Context) {
  origin := c.Request.Header.Get("Origin")
  // Check if the origin of the request is allowed to access the resource.
  if !m.isOriginAllowed(origin) {
   // If not allowed, return a JSON error and abort the request.
   c.Header("Content-Type", "application/json")
   c.JSON(http.StatusForbidden, gin.H{
    "error": "Origin not allowed",
   })
   c.Abort()
   return
  }

  // Set CORS headers to allow requests from the specified origin.
  c.Header("Access-Control-Allow-Origin", origin)
  c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE, PATCH, OPTIONS")
  c.Header("Access-Control-Allow-Headers", "Origin, Content-Type")
  // Handle pre-flight OPTIONS requests.
  if c.Request.Method == "OPTIONS" {
   c.AbortWithStatus(http.StatusNoContent)
   return
  }
  c.Next()
 }
}

// isOriginAllowed checks whether the provided origin is in the list of allowed origins.
func (m *HttpHeaders) isOriginAllowed(origin string) bool {
 if m.AllowOrigin == "*" {
  // Allow any origin if the configured setting is "*".
  return true
 }
 allowedOrigins := strings.Split(m.AllowOrigin, ",")
 for _, allowed := range allowedOrigins {
  if origin == allowed {
   return true
  }
 }
 return false
}

// Timestamp adds a timestamp header to responses.
// This can be useful for debugging and logging purposes to track when a response was generated.
func (m *HttpHeaders) Timestamp() gin.HandlerFunc {
 return func(c *gin.Context) {
  c.Next() // Proceed to the next middleware/handler.
  // Add the current timestamp to the response header after handling the request.
  timestamp := time.Now().Format(time.RFC3339)
  c.Writer.Header().Set("X-Timestamp", timestamp)
 }
}

메인 서버 업데이트

메인 main.go 파일을 업데이트하여 헤더를 추가하고 convoai-service를 등록합니다.

cmd/main.go 파일을 열고 다음을 추가합니다:

import(
// Previous imports remain the same
	"github.com/AgoraIO-Community/convo-ai-go-server/convoai"
	"github.com/AgoraIO-Community/convo-ai-go-server/http_headers"
);

// Previous code remains the same..
func setupServer() *http.Server {
    // Previous code remains the same..
    // Set up router with headers
    router := gin.Default()
    // Replace headers TODO:
    var httpHeaders = http_headers.NewHttpHeaders(corsAllowOrigin)
    router.Use(httpHeaders.NoCache())
    router.Use(httpHeaders.CORShttpHeaders())
    router.Use(httpHeaders.Timestamp())

    // Initialize services & register routes
    tokenService := token_service.NewTokenService(config.AppID, config.AppCertificate)
    tokenService.RegisterRoutes(router)
    convoAIService := convoai.NewConvoAIService(config, tokenService)
    convoAIService.RegisterRoutes(router)

// Rest of the code remains the same...

지금까지 토큰 서비스가 존재하지 않는다는 것을 알아차리셨을 것입니다. 현재는 오류를 무시해 주세요. 다음 단계에서 토큰 서비스를 구현할 예정이며, 이로 인해 프론트엔드 애플리케이션과의 테스트 및 통합이 더 쉬워질 것입니다.

토큰 생성

convoai-service에서는 토큰 서비스를 사용합니다. 이 토큰을 인증 서비스에 연결하여 토큰을 생성하도록 할 수 있지만, 이 가이드에서는 convoai-service 및 필요 시 클라이언트 애플리케이션 모두를 위해 토큰 서비스를 구현할 것입니다.

이 코드를 설명하는 것은 이 가이드의 범위를 약간 벗어나지만, 토큰에 익숙하지 않다면 제 가이드 Building a Token Server for Agora Applications using Golang를 참고하시기 바랍니다.

토큰 서비스

토큰 서비스 및 핸들러 파일을 생성합니다:

touch token_service/token-service.go
touch token_service/token_handlers.go

먼저, token-service.go에 토큰 서비스 정의를 추가합니다:

package token_service

import (
 "encoding/json"
 "net/http"
 "os"
 "github.com/gin-gonic/gin"
)

// TokenService represents the main application token service.
type TokenService struct {
 Server         *http.Server   // The HTTP server for the application
 Sigint         chan os.Signal // Channel to handle OS signals, such as Ctrl+C
 appID          string         // The Agora app ID
 appCertificate string         // The Agora app certificate
}

// TokenRequest is a struct representing the JSON payload structure for token generation requests.
type TokenRequest struct {
 TokenType         string `json:"tokenType"`         // The token type: "rtc", "rtm", or "chat"
 Channel           string `json:"channel,omitempty"` // The channel name (used for RTC and RTM tokens)
 RtcRole           string `json:"role,omitempty"`    // The role of the user for RTC tokens (publisher or subscriber)
 Uid               string `json:"uid,omitempty"`     // The user ID or account (used for RTC, RTM, and some chat tokens)
 ExpirationSeconds int    `json:"expire,omitempty"`  // The token expiration time in seconds (used for all token types)
}

// NewTokenService initializes and returns a TokenService pointer with all configurations set.
func NewTokenService(appIDEnv string, appCertEnv string) *TokenService {
 return &TokenService{
  appID:          appIDEnv,
  appCertificate: appCertEnv,
 }
}

// RegisterRoutes registers the routes for the TokenService.
func (s *TokenService) RegisterRoutes(r *gin.Engine) {
 api := r.Group("/token")
 api.POST("/getNew", s.GetToken)
}

// GetToken handles the HTTP request to generate a token based on the provided TokenRequest.
func (s *TokenService) GetToken(c *gin.Context) {
 var req = c.Request
 var respWriter = c.Writer
 var tokenReq TokenRequest
 // Parse the request body into a TokenRequest struct
 err := json.NewDecoder(req.Body).Decode(&tokenReq)
 if err != nil {
  http.Error(respWriter, err.Error(), http.StatusBadRequest)
  return
 }
 s.HandleGetToken(tokenReq, respWriter)
}

다음으로, token_handlers.go에 토큰 핸들러를 추가합니다:

package token_service

import (
 "encoding/json"
 "errors"
 "net/http"
 "strconv"
 "github.com/AgoraIO-Community/go-tokenbuilder/chatTokenBuilder"
 rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder"
 rtmtokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtmtokenbuilder"
)

// HandleGetToken handles the HTTP request to generate a token based on the provided tokenType.
func (s *TokenService) HandleGetToken(tokenReq TokenRequest, w http.ResponseWriter) {
 var token string
 var tokenErr error
 switch tokenReq.TokenType {
 case "rtc":
  token, tokenErr = s.GenRtcToken(tokenReq)
 case "rtm":
  token, tokenErr = s.GenRtmToken(tokenReq)
 case "chat":
  token, tokenErr = s.GenChatToken(tokenReq)
 default:
  http.Error(w, "Unsupported tokenType", http.StatusBadRequest)
  return
 }
 if tokenErr != nil {
  http.Error(w, tokenErr.Error(), http.StatusBadRequest)
  return
 }
 response := struct {
  Token string `json:"token"`
 }{Token: token}
 w.Header().Set("Content-Type", "application/json")
 w.WriteHeader(http.StatusOK)
 json.NewEncoder(w).Encode(response)
}

// GenRtcToken generates an RTC token based on the provided TokenRequest and returns it.
func (s *TokenService) GenRtcToken(tokenRequest TokenRequest) (string, error) {
 if tokenRequest.Channel == "" {
  return "", errors.New("invalid: missing channel name")
 }
 if tokenRequest.Uid == "" {
  return "", errors.New("invalid: missing user ID or account")
 }
 var userRole rtctokenbuilder2.Role
 if tokenRequest.RtcRole == "publisher" {
  userRole = rtctokenbuilder2.RolePublisher
 } else {
  userRole = rtctokenbuilder2.RoleSubscriber
 }
 if tokenRequest.ExpirationSeconds == 0 {
  tokenRequest.ExpirationSeconds = 3600
 }
 uid64, parseErr := strconv.ParseUint(tokenRequest.Uid, 10, 64)
 if parseErr != nil {
  return rtctokenbuilder2.BuildTokenWithAccount(
   s.appID, s.appCertificate, tokenRequest.Channel,
   tokenRequest.Uid, userRole, uint32(tokenRequest.ExpirationSeconds),
  )
 }
 return rtctokenbuilder2.BuildTokenWithUid(
  s.appID, s.appCertificate, tokenRequest.Channel,
  uint32(uid64), userRole, uint32(tokenRequest.ExpirationSeconds),
 )
}

// GenRtmToken generates an RTM (Real-Time Messaging) token based on the provided TokenRequest and returns it.
func (s *TokenService) GenRtmToken(tokenRequest TokenRequest) (string, error) {
 if tokenRequest.Uid == "" {
  return "", errors.New("invalid: missing user ID or account")
 }
 if tokenRequest.ExpirationSeconds == 0 {
  tokenRequest.ExpirationSeconds = 3600
 }
 return rtmtokenbuilder2.BuildToken(
  s.appID, s.appCertificate,
  tokenRequest.Uid,
  uint32(tokenRequest.ExpirationSeconds),
  tokenRequest.Channel,
 )
}

// GenChatToken generates a chat token based on the provided TokenRequest and returns it.
func (s *TokenService) GenChatToken(tokenRequest TokenRequest) (string, error) {
 if tokenRequest.ExpirationSeconds == 0 {
  tokenRequest.ExpirationSeconds = 3600
 }
 var chatToken string
 var tokenErr error
 if tokenRequest.Uid == "" {
  chatToken, tokenErr = chatTokenBuilder.BuildChatAppToken(
   s.appID, s.appCertificate, uint32(tokenRequest.ExpirationSeconds),
  )
 } else {
  chatToken, tokenErr = chatTokenBuilder.BuildChatUserToken(
   s.appID, s.appCertificate,
   tokenRequest.Uid,
   uint32(tokenRequest.ExpirationSeconds),
  )
 }
 return chatToken, tokenErr
}

토큰 생성이 완료되었으니, API의 안정성과 보안을 확보하기 위해 일부 검증 미들웨어를 추가해 보겠습니다.

환경 검증

필요한 모든 환경 변수가 설정되었는지 확인하는 검증 유틸리티를 생성합니다. validation/validation.go 파일을 생성합니다.

touch validation/validation.go

다음 내용을 추가하세요:

package validation

import (
 "errors"
 "strings"
 "github.com/AgoraIO-Community/convo-ai-go-server/convoai"
)

// ValidateEnvironment checks if all required environment variables are set
func ValidateEnvironment(config *convoai.ConvoAIConfig) error {

 // Validate Agora Configuration
 if config.AppID == "" || config.AppCertificate == "" {
  return errors.New("config error: Agora credentials (APP_ID, APP_CERTIFICATE) are not set")
 }

 if config.CustomerID == "" || config.CustomerSecret == "" || config.BaseURL == "" {
  return errors.New("config error: Agora Conversation AI credentials (CUSTOMER_ID, CUSTOMER_SECRET, BASE_URL) are not set")
 }

 // Validate LLM Configuration
 if config.LLMURL == "" || config.LLMToken == "" {
  return errors.New("config error: LLM configuration (LLM_URL, LLM_TOKEN) is not set")
 }

 // Validate TTS Configuration
 if config.TTSVendor == "" {
  return errors.New("config error: TTS_VENDOR is not set")
 }

 if err := validateTTSConfig(config); err != nil {
  return err
 }

 // Validate Modalities (optional, using defaults if not set)
 if config.InputModalities != "" && !validateModalities(config.InputModalities) {
  return errors.New("config error: Invalid INPUT_MODALITIES format")
 }

 if config.OutputModalities != "" && !validateModalities(config.OutputModalities) {
  return errors.New("config error: Invalid OUTPUT_MODALITIES format")
 }
 return nil
}

// Validates the TTS configuration based on the vendor
func validateTTSConfig(config *convoai.ConvoAIConfig) error {
 switch config.TTSVendor {
 case "microsoft":
  if config.MicrosoftTTS == nil {
   return errors.New("config error: Microsoft TTS configuration is missing")
  }
  if config.MicrosoftTTS.Key == "" ||
   config.MicrosoftTTS.Region == "" ||
   config.MicrosoftTTS.VoiceName == "" {
   return errors.New("config error: Microsoft TTS configuration is incomplete")
  }
 case "elevenlabs":
  if config.ElevenLabsTTS == nil {
   return errors.New("config error: ElevenLabs TTS configuration is missing")
  }
  if config.ElevenLabsTTS.Key == "" ||
   config.ElevenLabsTTS.VoiceID == "" ||
   config.ElevenLabsTTS.ModelID == "" {
   return errors.New("config error: ElevenLabs TTS configuration is incomplete")
  }
 default:
  return errors.New("config error: Unsupported TTS vendor: " + config.TTSVendor)
 }
 return nil
}

// Checks if the modalities string is properly formatted
func validateModalities(modalities string) bool {
 // map of valid modalities
 validModalities := map[string]bool{
  "text":  true,
  "audio": true,
 }

 // split the modalities string and check if each modality is valid
 for _, modality := range strings.Split(modalities, ",") {
  if !validModalities[strings.TrimSpace(modality)] {
   return false
  }
 }
 return true
}

이 유틸리티는 서버가 시작되기 전에 모든 필수 환경 변수가 올바르게 설정되었는지 확인합니다.

main.go 파일을 열고 setupServer 함수를 업데이트하여 유틸리티를 사용하도록 설정합니다:

// Just below: Load configuration
// Replace the TODO: comment with the following:
    // Validate environment configuration
    if err := validation.ValidateEnvironment(config); err != nil {
        log.Fatal("FATAL ERROR: ", err)
    }
// Rest of the code remains the same...

서버 실행

모든 구성 요소가 준비되었으니 이제 서버를 실행해 보겠습니다. 먼저 .env 파일에 필요한 모든 자격 증명을 설정했는지 확인하세요. 서버는 시작 시 이 환경 변수를 자동으로 로드합니다.

서버 빌드 및 실행:

go build -o server./server

모든 설정이 올바르게 이루어졌다면 서버가 시작되고 구성된 포트(기본값은 8080)에서 연결을 대기 중일 것입니다.

서버 테스트

엔드포인트를 테스트하기 전에 클라이언트 측 애플리케이션이 실행 중인지 확인하세요. Agora의 비디오 SDK를 구현한 애플리케이션(웹, 모바일, 데스크톱)을 사용할 수 있습니다. 애플리케이션이 없다면 Agora의 Voice Demo를 사용할 수 있지만, 채널에 참여하기 전에 토큰 요청을 반드시 수행해야 합니다.

curl을 사용하여 API 엔드포인트를 테스트해 보겠습니다:

1. 토큰 생성

curl -X POST http://localhost:8080/token/getNew \
  -H "Content-Type: application/json" \
  -d '{
    "tokenType": "rtc",
    "channel": "test-channel",
    "uid": "1234",
    "role": "publisher"
  }'

예상 응답:

{
  "token": "007eJxTYBAxNdgrlvnEfm3o..."
}

2. AI 에이전트를 초대하세요

curl -X POST http://localhost:8080/agent/invite \
  -H "Content-Type: application/json" \
  -d '{
    "requester_id": "1234",
    "channel_name": "test-channel",
    "input_modalities": ["text"],
    "output_modalities": ["text", "audio"]
  }'

{
  "agent_id": "agent-123abc",
  "create_ts": 1665481725000,
  "status": "RUNNING"
}

3. AI 에이전트 제거

curl -X POST http://localhost:8080/agent/remove \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent-123abc"
  }'

예상 응답:

{
  "success": true,
  "agent_id": "agent-123abc"
}

맞춤 설정

Agora Conversational AI Engine은 여러 가지 맞춤 설정을 지원합니다.

에이전트 맞춤 설정

convoai_handler_invite.go 파일에서 시스템 메시지를 수정하여 에이전트의 동작을 맞춤 설정할 수 있습니다:

systemMessage := SystemMessage{
    Role:    "system",
    Content: "You are a technical support specialist named Alex. Your responses should be friendly but concise, focused on helping users solve their technical problems. Use simple language but don't oversimplify technical concepts.",
}

채널에 참여할 때 에이전트가 처음 말하는 인사 메시지를 업데이트하여 제어할 수 있습니다:

LLM: LLM{
    // ... other configurations
    GreetingMessage: "Hello! I'm Alex, your technical support specialist. How can I assist you today?",
    FailureMessage:  "I'm processing your request. Please give me a moment.",
    // ... rest of the configuration
}

음성 합성 맞춤 설정

응용 프로그램에 적합한 음성을 선택하려면 음성 라이브러리를 탐색하세요:

.env파일을 적절한 음성 설정으로 업데이트하세요.

음성 활동 감지(VAD) 미세 조정

convoai_handler_invite.go파일에서 VAD 설정을 조정하여 대화 흐름을 최적화하세요:

VAD: VAD{
    SilenceDurationMS:   600,      // How long to wait after silence to end turn
    SpeechDurationMS:    10000,     // Maximum duration for a single speech segment
    Threshold:           0.6,       // Speech detection sensitivity
    InterruptDurationMS: 200,       // How quickly interruptions are detected
    PrefixPaddingMS:     400,       // Audio padding at the beginning of speech
},

환경 변수 참조 가이드

다음은 .env 파일용 환경 변수의 전체 목록입니다:

# Server Configuration
PORT=8080
CORS_ALLOW_ORIGIN=*

# Agora Configuration
AGORA_APP_ID=your_app_id
AGORA_APP_CERTIFICATE=your_app_certificate
AGORA_CONVO_AI_BASE_URL=https://api.agora.io/api/conversational-ai-agent/v2/projects
AGORA_CUSTOMER_ID=your_customer_id
AGORA_CUSTOMER_SECRET=your_customer_secret
AGENT_UID=Agent

# LLM Configuration
LLM_URL=https://api.openai.com/v1/chat/completions
LLM_TOKEN=your_openai_api_key
LLM_MODEL=gpt-4o-mini

# Input/Output Modalities
INPUT_MODALITIES=text
OUTPUT_MODALITIES=text,audio

# TTS Configuration
TTS_VENDOR=microsoft  # or elevenlabs
# Microsoft TTS Configuration
MICROSOFT_TTS_KEY=your_microsoft_tts_key
MICROSOFT_TTS_REGION=your_microsoft_tts_region
MICROSOFT_TTS_VOICE_NAME=en-US-GuyNeural
MICROSOFT_TTS_RATE=1.0
MICROSOFT_TTS_VOLUME=100.0

# ElevenLabs TTS Configuration
ELEVENLABS_API_KEY=your_elevenlabs_api_key
ELEVENLABS_VOICE_ID=your_elevenlabs_voice_id
ELEVENLABS_MODEL_ID=eleven_monolingual_v1

다음 단계

축하합니다! Agora의 Conversational AI Engine과 통합된 Go 서버를 구축하셨습니다. 이 마이크로서비스를 기존 Agora 백엔드와 통합하세요.

Agora의 Conversational AI Engine에 대한 자세한 내용은 공식 문서를 참고하세요.

전체 소스 코드는 GitHub 리포지토리를 확인하세요.

개발을 즐겁게 진행하세요!

RTE Telehealth 2023
Join us for RTE Telehealth - a virtual webinar where we’ll explore how AI and AR/VR technologies are shaping the future of healthcare delivery.

Learn more about Agora's video and voice solutions

Ready to chat through your real-time video and voice needs? We're here to help! Current Twilio customers get up to 2 months FREE.

Complete the form, and one of our experts will be in touch.

Try Agora for Free

Sign up and start building! You don’t pay until you scale.
Try for Free