Skip to content

Commit b781913

Browse files
committed
feat: refactor configuration and update AI adapter to use event-specific URLs
1 parent 5639ebd commit b781913

10 files changed

Lines changed: 192 additions & 65 deletions

File tree

.env.example

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
LISTEN_ADDR=:8080
1+
LISTEN_ADDR=:8090
22
REDIS_URL=redis://localhost:6379
3-
BOT_RUNTIME_SECRET=change_me_in_production
4-
AI_PROCESSOR_BASE_URL=http://ai-processor:8000
3+
BOT_RUNTIME_SECRET=
54
AI_CALL_TIMEOUT_SECONDS=30

Makefile

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,146 @@
1-
.PHONY: run test build lint docker
1+
.PHONY: help dev run build test test-coverage test-race lint vet fmt check \
2+
deps deps-update deps-clean clean clean-all \
3+
docker-build docker-run setup
24

3-
run:
4-
go run main.go
5+
include .env
6+
export
57

6-
test:
7-
go test ./...
8+
# Configurações
9+
APP_NAME=evo-bot-runtime
10+
MAIN_PATH=./cmd/server
11+
BUILD_DIR=bin
12+
GO=go
13+
GOFLAGS=-v
814

9-
build:
10-
go build -o bin/evo-bot-runtime .
15+
# Cores para output
16+
GREEN=\033[0;32m
17+
YELLOW=\033[0;33m
18+
RED=\033[0;31m
19+
NC=\033[0m # No Color
1120

12-
lint:
13-
gear validate
14-
go vet ./...
21+
##@ Ajuda
1522

16-
docker:
17-
docker build -t evo-bot-runtime .
23+
help: ## Exibe esta mensagem de ajuda
24+
@echo "$(GREEN)Evo Bot Runtime - Makefile$(NC)"
25+
@echo ""
26+
@awk 'BEGIN {FS = ":.*##"; printf "\nUso:\n make $(YELLOW)<target>$(NC)\n"} /^[a-zA-Z_-]+:.*?##/ { printf " $(GREEN)%-15s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(YELLOW)%s$(NC)\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
27+
28+
##@ Desenvolvimento
29+
30+
dev: ## Roda a aplicação em modo desenvolvimento
31+
@echo "$(GREEN)🔧 Iniciando Evo Bot Runtime em modo desenvolvimento...$(NC)"
32+
$(GO) run -race $(MAIN_PATH)
33+
34+
run: ## Roda a aplicação em modo produção
35+
@echo "$(GREEN)🚀 Iniciando Evo Bot Runtime...$(NC)"
36+
$(GO) run $(MAIN_PATH)
37+
38+
##@ Build
39+
40+
build: ## Compila a aplicação
41+
@echo "$(GREEN)🔨 Compilando $(APP_NAME)...$(NC)"
42+
@mkdir -p $(BUILD_DIR)
43+
$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH)
44+
@echo "$(GREEN)✅ Build completo: $(BUILD_DIR)/$(APP_NAME)$(NC)"
45+
46+
build-linux: ## Compila para Linux
47+
@echo "$(GREEN)🔨 Compilando para Linux...$(NC)"
48+
@mkdir -p $(BUILD_DIR)
49+
GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH)
50+
@echo "$(GREEN)✅ Build Linux completo$(NC)"
51+
52+
##@ Testes
53+
54+
test: ## Roda todos os testes
55+
@echo "$(GREEN)🧪 Rodando testes...$(NC)"
56+
$(GO) test -v ./...
57+
58+
test-coverage: ## Roda testes com cobertura
59+
@echo "$(GREEN)🧪 Rodando testes com cobertura...$(NC)"
60+
$(GO) test -v -coverprofile=coverage.out ./...
61+
$(GO) tool cover -html=coverage.out -o coverage.html
62+
@echo "$(GREEN)✅ Cobertura gerada: coverage.html$(NC)"
63+
64+
test-race: ## Roda testes verificando race conditions
65+
@echo "$(GREEN)🧪 Rodando testes com race detector...$(NC)"
66+
$(GO) test -race -v ./...
67+
68+
##@ Dependências
69+
70+
deps: ## Instala dependências
71+
@echo "$(GREEN)📦 Instalando dependências...$(NC)"
72+
$(GO) mod download
73+
$(GO) mod verify
74+
@echo "$(GREEN)✅ Dependências instaladas$(NC)"
75+
76+
deps-update: ## Atualiza dependências
77+
@echo "$(GREEN)📦 Atualizando dependências...$(NC)"
78+
$(GO) get -u ./...
79+
$(GO) mod tidy
80+
@echo "$(GREEN)✅ Dependências atualizadas$(NC)"
81+
82+
deps-clean: ## Limpa dependências não utilizadas
83+
@echo "$(GREEN)🧹 Limpando dependências...$(NC)"
84+
$(GO) mod tidy
85+
@echo "$(GREEN)✅ Dependências limpas$(NC)"
86+
87+
##@ Docker
88+
89+
docker-build: ## Build da imagem Docker
90+
@echo "$(GREEN)🐳 Construindo imagem Docker...$(NC)"
91+
docker build -t $(APP_NAME):latest .
92+
@echo "$(GREEN)✅ Imagem Docker construída$(NC)"
93+
94+
docker-run: ## Roda container Docker
95+
@echo "$(GREEN)🐳 Iniciando container...$(NC)"
96+
docker run -p 8080:8080 --env-file .env $(APP_NAME):latest
97+
98+
##@ Linting e Formatação
99+
100+
fmt: ## Formata o código
101+
@echo "$(GREEN)✨ Formatando código...$(NC)"
102+
$(GO) fmt ./...
103+
@echo "$(GREEN)✅ Código formatado$(NC)"
104+
105+
lint: ## Executa linter (requer golangci-lint)
106+
@echo "$(GREEN)🔍 Executando linter...$(NC)"
107+
@if command -v golangci-lint > /dev/null; then \
108+
golangci-lint run ./...; \
109+
echo "$(GREEN)✅ Lint completo$(NC)"; \
110+
else \
111+
echo "$(RED)❌ golangci-lint não instalado. Instale com: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest$(NC)"; \
112+
exit 1; \
113+
fi
114+
115+
vet: ## Executa go vet
116+
@echo "$(GREEN)🔍 Executando go vet...$(NC)"
117+
$(GO) vet ./...
118+
@echo "$(GREEN)✅ Vet completo$(NC)"
119+
120+
check: fmt vet lint test ## Executa todas as verificações
121+
122+
##@ Limpeza
123+
124+
clean: ## Remove arquivos de build
125+
@echo "$(YELLOW)🧹 Limpando arquivos de build...$(NC)"
126+
@rm -rf $(BUILD_DIR)
127+
@rm -f coverage.out coverage.html
128+
@echo "$(GREEN)✅ Limpeza completa$(NC)"
129+
130+
clean-all: clean ## Remove arquivos de build e cache
131+
@echo "$(YELLOW)🧹 Limpeza completa (incluindo cache)...$(NC)"
132+
$(GO) clean -cache -testcache -modcache
133+
@echo "$(GREEN)✅ Limpeza completa$(NC)"
134+
135+
##@ Utilitários
136+
137+
setup: deps ## Setup completo do ambiente de desenvolvimento
138+
@echo "$(GREEN)🎉 Setup completo!$(NC)"
139+
@echo ""
140+
@echo "Para começar a desenvolver, rode:"
141+
@echo " $(YELLOW)make dev$(NC)"
142+
@echo ""
143+
@echo "Outros comandos úteis:"
144+
@echo " $(YELLOW)make help$(NC) - Ver todos os comandos"
145+
@echo " $(YELLOW)make test$(NC) - Rodar testes"
146+
@echo " $(YELLOW)make build$(NC) - Compilar a aplicação"

cmd/server/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ func main() {
5050
// Step 5: debounce engine
5151
debounce := debounceService.NewDebounceEngine(pipelineRepo)
5252

53-
// Step 6: AI adapter
54-
aiAdapter := aiService.NewAIAdapter(cfg.AIProcessorBaseURL, cfg.AICallTimeoutSeconds)
53+
// Step 6: AI adapter (URL comes from each event's outgoing_url)
54+
aiAdapter := aiService.NewAIAdapter(cfg.AICallTimeoutSeconds)
5555

56-
// Step 7: dispatch engine
57-
dispatch := dispatchService.NewDispatchEngine()
56+
// Step 7: dispatch engine (sends secret header on postback to CRM)
57+
dispatch := dispatchService.NewDispatchEngine(cfg.BotRuntimeSecret)
5858

5959
// Step 8: pipeline service
6060
pipeline := pipelineService.NewPipelineService(pipelineRepo, debounce, aiAdapter, dispatch)

internal/config/config.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ type Config struct {
1010
ListenAddr string
1111
RedisURL string
1212
BotRuntimeSecret string
13-
AIProcessorBaseURL string
1413
AICallTimeoutSeconds int
1514
}
1615

@@ -23,14 +22,7 @@ func Load() (*Config, error) {
2322
if err != nil {
2423
return nil, err
2524
}
26-
botRuntimeSecret, err := mustGetEnv("BOT_RUNTIME_SECRET")
27-
if err != nil {
28-
return nil, err
29-
}
30-
aiProcessorBaseURL, err := mustGetEnv("AI_PROCESSOR_BASE_URL")
31-
if err != nil {
32-
return nil, err
33-
}
25+
botRuntimeSecret := os.Getenv("BOT_RUNTIME_SECRET")
3426
aiCallTimeout, err := getEnvIntOrDefault("AI_CALL_TIMEOUT_SECONDS", 30)
3527
if err != nil {
3628
return nil, err
@@ -40,7 +32,6 @@ func Load() (*Config, error) {
4032
ListenAddr: listenAddr,
4133
RedisURL: redisURL,
4234
BotRuntimeSecret: botRuntimeSecret,
43-
AIProcessorBaseURL: aiProcessorBaseURL,
4435
AICallTimeoutSeconds: aiCallTimeout,
4536
}, nil
4637
}

pkg/ai/model/a2a.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package model
33
// A2ARequest carries the data needed to call AI Processor via JSON-RPC 2.0.
44
// Fields are NOT serialised directly — the adapter builds the wire format.
55
type A2ARequest struct {
6-
AgentBotID string // used to construct URL path: /api/v1/a2a/{AgentBotID}
6+
OutgoingURL string // full A2A endpoint URL from agent_bot.outgoing_url
77
ContactID int64 // used for userId in JSON-RPC params
88
ConversationID int64 // used for contextId in JSON-RPC params
99
ApiKey string // used for X-API-Key header (per-event auth)

pkg/ai/service/ai_adapter.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"io"
1010
"log/slog"
1111
"net/http"
12-
"strings"
1312
"time"
1413

1514
brtErrors "github.com/EvolutionAPI/evo-bot-runtime/internal/errors"
@@ -26,16 +25,14 @@ type AIAdapter interface {
2625
}
2726

2827
type aiAdapter struct {
29-
baseURL string
3028
timeoutSecs int
3129
client *http.Client
3230
}
3331

3432
// NewAIAdapter constructs the adapter. Returns interface (GEAR R03).
35-
// baseURL is the AI Processor base URL without path (e.g. http://ai-processor:8000).
36-
func NewAIAdapter(baseURL string, timeoutSecs int) AIAdapter {
33+
// The AI Processor URL comes from each event's outgoing_url field.
34+
func NewAIAdapter(timeoutSecs int) AIAdapter {
3735
return &aiAdapter{
38-
baseURL: strings.TrimRight(baseURL, "/"),
3936
timeoutSecs: timeoutSecs,
4037
client: &http.Client{},
4138
}
@@ -48,8 +45,8 @@ func (a *aiAdapter) Call(ctx context.Context, req *model.A2ARequest) (*model.Nor
4845
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(a.timeoutSecs)*time.Second)
4946
defer cancel()
5047

51-
// Build per-event URL: {baseURL}/api/v1/a2a/{agent_bot_id}
52-
url := fmt.Sprintf("%s/api/v1/a2a/%s", a.baseURL, req.AgentBotID)
48+
// Use the full outgoing_url provided by the CRM (already contains the agent ID)
49+
url := req.OutgoingURL
5350

5451
// Build JSON-RPC 2.0 envelope
5552
rpcReq := model.JSONRPCRequest{

pkg/dispatch/service/dispatch_engine.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,19 @@ type postbackRequest struct {
3939

4040
type dispatchEngineImpl struct {
4141
client *http.Client
42+
secret string
4243
}
4344

4445
// postbackClientTimeout is the maximum time allowed for a single HTTP postback call.
4546
// The per-call context (propagated via Dispatch) can still cancel earlier.
4647
const postbackClientTimeout = 30 * time.Second
4748

4849
// NewDispatchEngine constructs the engine. Returns interface (GEAR R03).
49-
// postbackURL is passed per-call (in MessageEvent, not global config).
50-
func NewDispatchEngine() DispatchEngine {
50+
// secret is the BOT_RUNTIME_SECRET sent as X-Bot-Runtime-Secret header on postback.
51+
func NewDispatchEngine(secret string) DispatchEngine {
5152
return &dispatchEngineImpl{
5253
client: &http.Client{Timeout: postbackClientTimeout},
54+
secret: secret,
5355
}
5456
}
5557

@@ -128,6 +130,9 @@ func (d *dispatchEngineImpl) sendPart(ctx context.Context, postbackURL, content
128130
return fmt.Errorf("new_request: %w", err)
129131
}
130132
req.Header.Set("Content-Type", "application/json")
133+
if d.secret != "" {
134+
req.Header.Set("X-Bot-Runtime-Secret", d.secret)
135+
}
131136

132137
resp, err := d.client.Do(req)
133138
if err != nil {

pkg/pipeline/model/pipeline.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,29 @@ type PairID struct {
2727
// BotConfig and PostbackURL are persisted for StageDebounce so that the service can
2828
// reconstruct the pipelineEntry correctly after a restart (NFR-01 recovery).
2929
type PipelineState struct {
30-
Stage Stage `json:"stage"`
31-
CreatedAt time.Time `json:"created_at"`
32-
BotConfig BotConfig `json:"bot_config,omitempty"`
33-
PostbackURL string `json:"postback_url,omitempty"`
34-
AgentBotID string `json:"agent_bot_id,omitempty"`
35-
ApiKey string `json:"api_key,omitempty"`
30+
Stage Stage `json:"stage"`
31+
CreatedAt time.Time `json:"created_at"`
32+
BotConfig BotConfig `json:"bot_config,omitempty"`
33+
PostbackURL string `json:"postback_url,omitempty"`
34+
AgentBotID string `json:"agent_bot_id,omitempty"`
35+
ApiKey string `json:"api_key,omitempty"`
36+
OutgoingURL string `json:"outgoing_url,omitempty"`
37+
Metadata map[string]any `json:"metadata,omitempty"`
3638
}
3739

3840
// MessageEvent is the inbound payload from evo-ai-crm AgentBotListener.
3941
// All JSON tags are snake_case — matches the wire format exactly.
4042
type MessageEvent struct {
41-
AgentBotID string `json:"agent_bot_id"`
42-
ConversationID int64 `json:"conversation_id"`
43-
ContactID int64 `json:"contact_id"`
44-
MessageID string `json:"message_id"`
45-
MessageContent string `json:"message_content"`
46-
ApiKey string `json:"api_key"`
47-
BotConfig BotConfig `json:"bot_config"`
48-
PostbackURL string `json:"postback_url"`
43+
AgentBotID string `json:"agent_bot_id"`
44+
ConversationID int64 `json:"conversation_id"`
45+
ContactID int64 `json:"contact_id"`
46+
MessageID string `json:"message_id"`
47+
MessageContent string `json:"message_content"`
48+
ApiKey string `json:"api_key"`
49+
OutgoingURL string `json:"outgoing_url"`
50+
BotConfig BotConfig `json:"bot_config"`
51+
PostbackURL string `json:"postback_url"`
52+
Metadata map[string]any `json:"metadata,omitempty"`
4953
}
5054

5155
// BotConfig carries per-bot runtime configuration provided by the caller.

0 commit comments

Comments
 (0)