Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ VALKEY_PASSWORD=
# Valkey database index (0-15 for standalone mode)
VALKEY_DB=0
# Key prefix for namespacing (useful for multi-tenant setups)
VALKEY_KEY_PREFIX=azwap:
VALKEY_KEY_PREFIX=azwap:

# Portal Settings
# Master key for internal services (Bots/Admin) to generate Magic Links
PORTAL_INTERNAL_KEY=your_super_secret_internal_key_here
# Shared secret for Portal JWT signing (should be different from APP_SECRET_KEY for isolation)
PORTAL_JWT_SECRET=your_portal_specific_jwt_secret_here
6 changes: 6 additions & 0 deletions src/botengine/application/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@ func (s *botService) Create(ctx context.Context, req domainBot.CreateBotRequest)
ChatwootCredentialID: strings.TrimSpace(req.ChatwootCredentialID),
ChatwootBotToken: strings.TrimSpace(req.ChatwootBotToken),
Whitelist: req.Whitelist,
Variants: req.Variants,
}

bot.SanitizeVariants()

if err := s.repo.Create(ctx, bot); err != nil {
return domainBot.Bot{}, err
}
Expand Down Expand Up @@ -209,6 +212,9 @@ func (s *botService) Update(ctx context.Context, id string, req domainBot.Update
updated.ChatwootCredentialID = strings.TrimSpace(req.ChatwootCredentialID)
updated.ChatwootBotToken = strings.TrimSpace(req.ChatwootBotToken)
updated.Whitelist = req.Whitelist
updated.Variants = req.Variants

updated.SanitizeVariants()

if err := s.repo.Update(ctx, updated); err != nil {
return domainBot.Bot{}, err
Expand Down
78 changes: 70 additions & 8 deletions src/botengine/domain/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bot

import (
"context"
"strings"

domainHealth "github.com/AzielCF/az-wap/domains/health"
)
Expand Down Expand Up @@ -39,8 +40,23 @@ type Bot struct {
ChatwootCredentialID string `json:"chatwoot_credential_id,omitempty"`
ChatwootBotToken string `json:"chatwoot_bot_token,omitempty"`
// New fields added
ChatwootCredential ChatwootCredential `json:"chatwoot_credential,omitempty"`
Whitelist []string `json:"whitelist,omitempty"`
ChatwootCredential ChatwootCredential `json:"chatwoot_credential,omitempty"`
Whitelist []string `json:"whitelist,omitempty"`
Variants map[string]BotVariant `json:"variants,omitempty"`
}

type BotVariant struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
SystemPrompt string `json:"system_prompt"`
AllowedTools []string `json:"allowed_tools,omitempty"`
AllowedMCPs []string `json:"allowed_mcps,omitempty"`

AudioEnabled *bool `json:"audio_enabled,omitempty"`
ImageEnabled *bool `json:"image_enabled,omitempty"`
VideoEnabled *bool `json:"video_enabled,omitempty"`
DocumentEnabled *bool `json:"document_enabled,omitempty"`
MemoryEnabled *bool `json:"memory_enabled,omitempty"`
}

type ChatwootCredential struct {
Expand All @@ -67,9 +83,10 @@ type CreateBotRequest struct {
MultimodalModel string `json:"multimodal_model"`
CredentialID string `json:"credential_id"`
// Optional Chatwoot config
ChatwootCredentialID string `json:"chatwoot_credential_id"`
ChatwootBotToken string `json:"chatwoot_bot_token"`
Whitelist []string `json:"whitelist"`
ChatwootCredentialID string `json:"chatwoot_credential_id"`
ChatwootBotToken string `json:"chatwoot_bot_token"`
Whitelist []string `json:"whitelist"`
Variants map[string]BotVariant `json:"variants"`
}

type UpdateBotRequest struct {
Expand All @@ -91,9 +108,10 @@ type UpdateBotRequest struct {
MultimodalModel string `json:"multimodal_model"`
CredentialID string `json:"credential_id"`
// Optional Chatwoot config
ChatwootCredentialID string `json:"chatwoot_credential_id"`
ChatwootBotToken string `json:"chatwoot_bot_token"`
Whitelist []string `json:"whitelist"`
ChatwootCredentialID string `json:"chatwoot_credential_id"`
ChatwootBotToken string `json:"chatwoot_bot_token"`
Whitelist []string `json:"whitelist"`
Variants map[string]BotVariant `json:"variants"`
}

type IBotUsecase interface {
Expand All @@ -106,3 +124,47 @@ type IBotUsecase interface {
SetHealthUsecase(h domainHealth.IHealthUsecase)
Shutdown()
}

func (b *Bot) SanitizeVariants() {
if b.Variants == nil {
return
}

cleaned := make(map[string]BotVariant)
for key, variant := range b.Variants {
name := strings.TrimSpace(variant.Name)
if name == "" {
continue // Ignorar variantes sin nombre
}

variant.Name = name
variant.Description = strings.TrimSpace(variant.Description)
variant.SystemPrompt = strings.TrimSpace(variant.SystemPrompt)

// Clean tools string slice
var validTools []string
for _, t := range variant.AllowedTools {
if trimmed := strings.TrimSpace(t); trimmed != "" {
validTools = append(validTools, trimmed)
}
}
variant.AllowedTools = validTools

// Clean mcps string slice
var validMCPs []string
for _, m := range variant.AllowedMCPs {
if trimmed := strings.TrimSpace(m); trimmed != "" {
validMCPs = append(validMCPs, trimmed)
}
}
variant.AllowedMCPs = validMCPs

cleaned[key] = variant
}

if len(cleaned) == 0 {
b.Variants = nil
} else {
b.Variants = cleaned
}
}
29 changes: 16 additions & 13 deletions src/botengine/repository/bot_gorm.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,20 @@ type botModel struct {
Model string
SystemPrompt string
KnowledgeBase string
AudioEnabled bool `gorm:"column:audio_enabled;not null;default:false"`
ImageEnabled bool `gorm:"column:image_enabled;not null;default:false"`
VideoEnabled bool `gorm:"column:video_enabled;not null;default:false"`
DocumentEnabled bool `gorm:"column:document_enabled;not null;default:false"`
MemoryEnabled bool `gorm:"column:memory_enabled;not null;default:false"`
MindsetModel sql.NullString `gorm:"column:mindset_model"`
MultimodalModel sql.NullString `gorm:"column:multimodal_model"`
CredentialID sql.NullString `gorm:"column:credential_id"`
ChatwootCredentialID sql.NullString `gorm:"column:chatwoot_credential_id"`
ChatwootBotToken sql.NullString `gorm:"column:chatwoot_bot_token"`
Whitelist sql.NullString `gorm:"column:whitelist"` // CSV string
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
AudioEnabled bool `gorm:"column:audio_enabled;not null;default:false"`
ImageEnabled bool `gorm:"column:image_enabled;not null;default:false"`
VideoEnabled bool `gorm:"column:video_enabled;not null;default:false"`
DocumentEnabled bool `gorm:"column:document_enabled;not null;default:false"`
MemoryEnabled bool `gorm:"column:memory_enabled;not null;default:false"`
MindsetModel sql.NullString `gorm:"column:mindset_model"`
MultimodalModel sql.NullString `gorm:"column:multimodal_model"`
CredentialID sql.NullString `gorm:"column:credential_id"`
ChatwootCredentialID sql.NullString `gorm:"column:chatwoot_credential_id"`
ChatwootBotToken sql.NullString `gorm:"column:chatwoot_bot_token"`
Whitelist sql.NullString `gorm:"column:whitelist"` // CSV string
Variants map[string]domainBot.BotVariant `gorm:"serializer:json"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}

// TableName especifica el nombre de la tabla para GORM.
Expand Down Expand Up @@ -126,6 +127,7 @@ func toBotModel(b domainBot.Bot) botModel {
ChatwootCredentialID: sql.NullString{String: b.ChatwootCredentialID, Valid: b.ChatwootCredentialID != ""},
ChatwootBotToken: sql.NullString{String: b.ChatwootBotToken, Valid: b.ChatwootBotToken != ""},
Whitelist: sql.NullString{String: strings.Join(b.Whitelist, ","), Valid: len(b.Whitelist) > 0},
Variants: b.Variants,
}
}

Expand Down Expand Up @@ -157,6 +159,7 @@ func fromBotModel(m botModel) domainBot.Bot {
ChatwootCredentialID: nullStringValue(m.ChatwootCredentialID),
ChatwootBotToken: nullStringValue(m.ChatwootBotToken),
Whitelist: whitelist,
Variants: m.Variants,
}
}

Expand Down
11 changes: 8 additions & 3 deletions src/botengine/tools/only-clients/client_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/AzielCF/az-wap/botengine/domain"
domainMCP "github.com/AzielCF/az-wap/botengine/domain/mcp"
clientsDomain "github.com/AzielCF/az-wap/clients/domain"
portalAuth "github.com/AzielCF/az-wap/clients_portal/auth/application"
)

const (
Expand All @@ -33,12 +34,16 @@ var AllowedClientFields = []string{

// ClientTools provides tools for the client to manage their information
type ClientTools struct {
clientRepo clientsDomain.ClientRepository
clientRepo clientsDomain.ClientRepository
authService *portalAuth.AuthService
}

// NewClientTools creates a new instance of ClientTools
func NewClientTools(clientRepo clientsDomain.ClientRepository) *ClientTools {
return &ClientTools{clientRepo: clientRepo}
func NewClientTools(clientRepo clientsDomain.ClientRepository, authService *portalAuth.AuthService) *ClientTools {
return &ClientTools{
clientRepo: clientRepo,
authService: authService,
}
}

// UpdateMyInfoTool allows the client to update their personal information
Expand Down
76 changes: 76 additions & 0 deletions src/botengine/tools/only-clients/portal_tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package onlyclients

import (
"context"
"fmt"

"github.com/AzielCF/az-wap/botengine/domain"
domainMCP "github.com/AzielCF/az-wap/botengine/domain/mcp"
coreconfig "github.com/AzielCF/az-wap/core/config"
)

// GetPortalLinkTool allows a registered client to get their portal access link
func (t *ClientTools) GetPortalLinkTool() *domain.NativeTool {
return &domain.NativeTool{
IsVisible: IsClientRegistered,
Tool: domainMCP.Tool{
Name: "get_portal_access_link",
Description: "Generates a secure, temporary magic link for the user to access their client portal. Use this ONLY when the user asks for 'access', 'portal', 'link to my account', or similar. If the user is not a registered client, it will fail.",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
"required": []string{},
},
},
Handler: func(ctx context.Context, ctxData map[string]interface{}, args map[string]interface{}) (map[string]interface{}, error) {
// 1. Get client ID from context (Already verified by IsVisible)
clientID := ""
phone := ""

if cc, ok := ctxData["client_context"].(*domain.ClientContext); ok && cc != nil {
clientID = cc.ClientID
phone = cc.Phone
}

// Fallback if context is somehow missing (safety)
if clientID == "" {
if metadata, ok := ctxData["metadata"].(map[string]any); ok {
if cID, ok := metadata["client_id"].(string); ok {
clientID = cID
}
if ph, ok := metadata["phone"].(string); ok {
phone = ph
}
}
}

if clientID == "" {
return map[string]interface{}{
"success": false,
"message": "I couldn't identify your client profile. Access denied.",
}, nil
}

// 2. Generate Magic Link via AuthService
token, err := t.authService.GenerateMagicLink(ctx, clientID, phone)
if err != nil {
return nil, fmt.Errorf("failed to generate portal link: %w", err)
}

// 3. Build the final URL
portalURL := coreconfig.Global.App.PortalURL
if portalURL == "" {
// Fallback to local server if no dedicated portal URL is set
portalURL = fmt.Sprintf("%s/portal", coreconfig.Global.App.BaseUrl)
}

accessLink := fmt.Sprintf("%s/auth/redeem?token=%s", portalURL, token)

return map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Access link generated: %s (Expires in 15m). Please send this link to the user.", accessLink),
"link": accessLink,
}, nil
},
}
}
56 changes: 29 additions & 27 deletions src/clients/adapter/rest/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,39 @@ import (

// CreateClientRequest representa la petición para crear un cliente
type CreateClientRequest struct {
PlatformID string `json:"platform_id"`
PlatformType string `json:"platform_type"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Phone string `json:"phone"`
Tier string `json:"tier"`
Tags []string `json:"tags"`
Metadata map[string]any `json:"metadata"`
Notes string `json:"notes"`
Language string `json:"language"`
Timezone string `json:"timezone"`
Country string `json:"country"`
AllowedBots []string `json:"allowed_bots"`
IsTester bool `json:"is_tester"`
PlatformID string `json:"platform_id"`
PlatformType string `json:"platform_type"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Phone string `json:"phone"`
Tier string `json:"tier"`
Tags []string `json:"tags"`
Metadata map[string]any `json:"metadata"`
Notes string `json:"notes"`
Language string `json:"language"`
Timezone string `json:"timezone"`
Country string `json:"country"`
AllowedBots []string `json:"allowed_bots"`
OwnedChannels []string `json:"owned_channels"`
IsTester bool `json:"is_tester"`
}

// UpdateClientRequest representa la petición para actualizar un cliente
type UpdateClientRequest struct {
PlatformID *string `json:"platform_id"`
DisplayName *string `json:"display_name"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Tier *string `json:"tier"`
Tags []string `json:"tags"`
Metadata map[string]any `json:"metadata"`
Notes *string `json:"notes"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
Country *string `json:"country"`
AllowedBots []string `json:"allowed_bots"`
IsTester *bool `json:"is_tester"`
PlatformID *string `json:"platform_id"`
DisplayName *string `json:"display_name"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Tier *string `json:"tier"`
Tags []string `json:"tags"`
Metadata map[string]any `json:"metadata"`
Notes *string `json:"notes"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
Country *string `json:"country"`
AllowedBots []string `json:"allowed_bots"`
OwnedChannels []string `json:"owned_channels"`
IsTester *bool `json:"is_tester"`
}

// CreateSubscriptionRequest representa la petición para crear una suscripción
Expand Down
Loading