Multi-Tenant Support
Multi-Tenant Support enables Agno-Go to serve multiple users with a single Agent instance while ensuring complete isolation of conversation history and session state between users.
Overview
Multi-tenant architecture allows a single application instance to serve multiple users (tenants) with complete data isolation:
┌─────────────────┐
│ Agent Instance │
└────────┬────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User A │ │ User B │ │ User C │
│ Messages │ │ Messages │ │ Messages │
└──────────┘ └──────────┘ └──────────┘
What is Multi-Tenancy?
Multi-tenancy is an architecture pattern where a single application instance serves multiple isolated users or organizations. Each tenant's data is completely separated from others.
Without Multi-Tenant
// ❌ Each user needs a separate Agent instance
userAgents := make(map[string]*agent.Agent)
agent1, _ := agent.New(config) // User 1
agent2, _ := agent.New(config) // User 2
agent3, _ := agent.New(config) // User 3
// ... 1000+ users = 1000+ Agent instances
Problems:
- High memory usage: 1000 users = 1000 Agent instances
- Difficult to manage: Manual agent lifecycle management
- Resource waste: Each agent has duplicate configuration
With Multi-Tenant
// ✅ Single Agent instance serves all users
sharedAgent, _ := agent.New(config)
// Different users use different userID
output1, _ := sharedAgent.Run(ctx, "user-1 input", "user-1")
output2, _ := sharedAgent.Run(ctx, "user-2 input", "user-2")
output3, _ := sharedAgent.Run(ctx, "user-3 input", "user-3")
Advantages:
- ✅ Low memory usage: Single Agent instance
- ✅ Easy management: Unified configuration and updates
- ✅ Efficient resource utilization: Shared model and tools
Quick Start
1. Create Multi-Tenant Agent
package main
import (
"context"
"fmt"
"github.com/rexleimo/agno-go/pkg/agno/agent"
"github.com/rexleimo/agno-go/pkg/agno/memory"
"github.com/rexleimo/agno-go/pkg/agno/models/openai"
)
func main() {
// Create model
model, _ := openai.New("gpt-4", openai.Config{
APIKey: "your-api-key",
})
// Create multi-tenant Memory
mem := memory.NewInMemory(100) // Automatically supports multi-tenancy
// Create Agent
myAgent, _ := agent.New(&agent.Config{
Name: "customer-service",
Model: model,
Memory: mem,
Instructions: "You are a helpful customer service agent.",
})
// Conversations for different users
ctx := context.Background()
// User A's conversation
myAgent.UserID = "user-a"
output1, _ := myAgent.Run(ctx, "My name is Alice")
fmt.Printf("User A: %s\n", output1.Content)
output2, _ := myAgent.Run(ctx, "What's my name?") // "Your name is Alice"
fmt.Printf("User A: %s\n", output2.Content)
// User B's conversation
myAgent.UserID = "user-b"
output3, _ := myAgent.Run(ctx, "My name is Bob")
fmt.Printf("User B: %s\n", output3.Content)
output4, _ := myAgent.Run(ctx, "What's my name?") // "Your name is Bob"
fmt.Printf("User B: %s\n", output4.Content)
// User A talks again
myAgent.UserID = "user-a"
output5, _ := myAgent.Run(ctx, "What's my name?") // "Your name is Alice"
fmt.Printf("User A: %s\n", output5.Content)
}
2. Web API Example
package main
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/rexleimo/agno-go/pkg/agno/agent"
)
var sharedAgent *agent.Agent
func main() {
// Initialize Agent
sharedAgent, _ = agent.New(&agent.Config{
Name: "api-agent",
Model: model,
Memory: memory.NewInMemory(100),
})
// Setup routes
router := gin.Default()
router.POST("/chat", handleChat)
router.Run(":8080")
}
type ChatRequest struct {
UserID string `json:"user_id"`
Message string `json:"message"`
}
type ChatResponse struct {
UserID string `json:"user_id"`
Reply string `json:"reply"`
}
func handleChat(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Set current user ID
sharedAgent.UserID = req.UserID
// Run conversation
output, err := sharedAgent.Run(context.Background(), req.Message)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, ChatResponse{
UserID: req.UserID,
Reply: output.Content,
})
}
Test:
# User A's conversation
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "user-a", "message": "My name is Alice"}'
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "user-a", "message": "What is my name?"}'
# Response: {"user_id":"user-a","reply":"Your name is Alice"}
# User B's conversation
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "user-b", "message": "My name is Bob"}'
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "user-b", "message": "What is my name?"}'
# Response: {"user_id":"user-b","reply":"Your name is Bob"}
Memory Management
Memory Interface
The Memory interface supports optional userID
parameter:
// pkg/agno/memory/memory.go
type Memory interface {
// Add message (supports optional userID)
Add(message *types.Message, userID ...string)
// Get message history (supports optional userID)
GetMessages(userID ...string) []*types.Message
// Clear messages for specific user
Clear(userID ...string)
// Clear messages for all users
ClearAll()
// Get message count for specific user
Size(userID ...string) int
}
InMemory Implementation
type InMemory struct {
userMessages map[string][]*types.Message // User ID → Message list
maxSize int
mu sync.RWMutex
}
// Default user ID
const defaultUserID = "default"
// Get user ID (backward compatible)
func getUserID(userID ...string) string {
if len(userID) > 0 && userID[0] != "" {
return userID[0]
}
return defaultUserID
}
Usage Examples
Basic Usage
mem := memory.NewInMemory(100)
// User A's messages
mem.Add(types.NewUserMessage("Hello from Alice"), "user-a")
mem.Add(types.NewAssistantMessage("Hi Alice!"), "user-a")
// User B's messages
mem.Add(types.NewUserMessage("Hello from Bob"), "user-b")
mem.Add(types.NewAssistantMessage("Hi Bob!"), "user-b")
// Get messages for each user
messagesA := mem.GetMessages("user-a") // 2 messages
messagesB := mem.GetMessages("user-b") // 2 messages
fmt.Printf("User A has %d messages\n", len(messagesA)) // 2
fmt.Printf("User B has %d messages\n", len(messagesB)) // 2
Backward Compatibility
mem := memory.NewInMemory(100)
// No userID specified (uses default "default")
mem.Add(types.NewUserMessage("Hello"))
messages := mem.GetMessages()
// Equivalent to:
mem.Add(types.NewUserMessage("Hello"), "default")
messages := mem.GetMessages("default")
Clear Operations
mem := memory.NewInMemory(100)
// Add messages for different users
mem.Add(types.NewUserMessage("User A msg"), "user-a")
mem.Add(types.NewUserMessage("User B msg"), "user-b")
// Clear specific user
mem.Clear("user-a")
fmt.Printf("User A: %d messages\n", mem.Size("user-a")) // 0
fmt.Printf("User B: %d messages\n", mem.Size("user-b")) // 1
// Clear all users
mem.ClearAll()
fmt.Printf("User A: %d messages\n", mem.Size("user-a")) // 0
fmt.Printf("User B: %d messages\n", mem.Size("user-b")) // 0
Agent Integration
Agent Configuration
type Agent struct {
ID string
Name string
Model models.Model
Toolkits []toolkit.Toolkit
Memory memory.Memory
Instructions string
MaxLoops int
UserID string // ⭐ NEW: Multi-tenant user ID
}
type Config struct {
Name string
Model models.Model
Toolkits []toolkit.Toolkit
Memory memory.Memory
Instructions string
MaxLoops int
UserID string // ⭐ NEW: Multi-tenant user ID
}
Run Method Implementation
// pkg/agno/agent/agent.go
func (a *Agent) Run(ctx context.Context, input string) (*RunOutput, error) {
// ...
// All Memory calls pass UserID
userMsg := types.NewUserMessage(input)
a.Memory.Add(userMsg, a.UserID) // ⭐ Pass UserID
// ...
messages := a.Memory.GetMessages(a.UserID) // ⭐ Pass UserID
// ...
a.Memory.Add(types.NewAssistantMessage(content), a.UserID) // ⭐ Pass UserID
}
Usage Patterns
Pattern 1: Shared Agent + Switch UserID
agent, _ := agent.New(&agent.Config{
Name: "shared-agent",
Model: model,
Memory: memory.NewInMemory(100),
})
// Handle User A's request
agent.UserID = "user-a"
output, _ := agent.Run(ctx, "User A message")
// Handle User B's request
agent.UserID = "user-b"
output, _ := agent.Run(ctx, "User B message")
⚠️ Note: This approach requires careful UserID switching in concurrent environments
Pattern 2: Separate Agent per User (Recommended for High Concurrency)
// Create Agent factory
func createUserAgent(userID string) (*agent.Agent, error) {
return agent.New(&agent.Config{
Name: "user-agent",
Model: sharedModel, // Can share Model
Memory: memory.NewInMemory(100),
UserID: userID, // Set fixed UserID
})
}
// Use Agent pool
userAgents := make(map[string]*agent.Agent)
// User A
if _, exists := userAgents["user-a"]; !exists {
userAgents["user-a"], _ = createUserAgent("user-a")
}
output, _ := userAgents["user-a"].Run(ctx, "User A message")
// User B
if _, exists := userAgents["user-b"]; !exists {
userAgents["user-b"], _ = createUserAgent("user-b")
}
output, _ := userAgents["user-b"].Run(ctx, "User B message")
Data Isolation Guarantees
1. Memory Isolation
// Test: Multi-tenant isolation
mem := memory.NewInMemory(100)
// User A adds 10 messages
for i := 0; i < 10; i++ {
mem.Add(types.NewUserMessage(fmt.Sprintf("User A message %d", i)), "user-a")
}
// User B adds 5 messages
for i := 0; i < 5; i++ {
mem.Add(types.NewUserMessage(fmt.Sprintf("User B message %d", i)), "user-b")
}
// Verify isolation
assert.Equal(t, 10, mem.Size("user-a")) // ✅
assert.Equal(t, 5, mem.Size("user-b")) // ✅
assert.Equal(t, 0, mem.Size("user-c")) // ✅ Non-existent user
messagesA := mem.GetMessages("user-a")
messagesB := mem.GetMessages("user-b")
// User A cannot see User B's messages
for _, msg := range messagesA {
assert.NotContains(t, msg.Content, "User B") // ✅
}
2. Concurrency Safety
// Test: 1000 concurrent requests
mem := memory.NewInMemory(100)
var wg sync.WaitGroup
// 10 users, 100 concurrent requests each
for userID := 0; userID < 10; userID++ {
for i := 0; i < 100; i++ {
wg.Add(1)
go func(uid, msgID int) {
defer wg.Done()
userIDStr := fmt.Sprintf("user-%d", uid)
msg := types.NewUserMessage(fmt.Sprintf("Message %d", msgID))
mem.Add(msg, userIDStr)
}(userID, i)
}
}
wg.Wait()
// Verify each user has correct number of messages
for userID := 0; userID < 10; userID++ {
userIDStr := fmt.Sprintf("user-%d", userID)
assert.Equal(t, 100, mem.Size(userIDStr)) // ✅
}
Best Practices
1. UserID Naming Convention
// ✅ Recommended: Use consistent naming convention
"user-{uuid}" // user-123e4567-e89b-12d3-a456-426614174000
"org-{org_id}-user-{id}" // org-acme-user-001
"tenant-{id}" // tenant-12345
// ❌ Avoid: Use unstable identifiers
"{ip_address}" // IP may change
"{session_id}" // Session expires
2. Error Handling
// Validate UserID
func validateUserID(userID string) error {
if userID == "" {
return fmt.Errorf("userID cannot be empty")
}
if len(userID) > 255 {
return fmt.Errorf("userID too long (max 255 chars)")
}
// Can add more validation rules
return nil
}
// Validate at API layer
func handleChat(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if err := validateUserID(req.UserID); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// ...
}
3. Logging and Monitoring
// Log UserID for each request
logger.Info("Processing request",
"user_id", userID,
"input_length", len(input),
"timestamp", time.Now(),
)
// Monitoring metrics
metrics.RecordUserRequest(userID)
metrics.RecordMessageCount(userID, mem.Size(userID))
4. Security Considerations
// Use encrypted UserID
func encryptUserID(plainUserID string) string {
// Use encryption algorithm
return encryptedID
}
// Access control
func checkUserPermission(userID string, action string) bool {
// Implement permission check logic
return hasPermission
}
Troubleshooting
Common Issues
1. User Data Confusion
Symptom: User A sees User B's messages
Cause: UserID not properly passed
Solution:
// ❌ Wrong
agent.Run(ctx, input) // UserID not set
// ✅ Correct
agent.UserID = "user-a"
agent.Run(ctx, input)
2. High Memory Usage
Symptom: Memory continuously growing
Cause: Inactive user data not cleaned up
Solution:
// Periodic cleanup
go func() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
cleanupInactiveUsers(mem, 24*time.Hour)
}
}()
3. Concurrent Race Conditions
Symptom: Data occasionally lost or duplicated
Cause: Shared Agent's UserID field modified by multiple goroutines
Solution:
// ❌ Wrong: Concurrent modification of shared Agent
var sharedAgent *agent.Agent
go func() { sharedAgent.UserID = "user-a"; sharedAgent.Run(ctx, input) }()
go func() { sharedAgent.UserID = "user-b"; sharedAgent.Run(ctx, input) }()
// ✅ Correct: Separate Agent per user
agentA := createUserAgent("user-a")
agentB := createUserAgent("user-b")
go func() { agentA.Run(ctx, input) }()
go func() { agentB.Run(ctx, input) }()
Integration with Other Features
A2A Interface + Multi-Tenant
// A2A request contains contextID, can be used as userID
type Message struct {
MessageID string `json:"messageId"`
Role string `json:"role"`
AgentID string `json:"agentId"`
ContextID string `json:"contextId"` // ⭐ Can be used as userID
Parts []Part `json:"parts"`
}
// Set UserID during mapping
func MapA2ARequestToRunInput(req *JSONRPC2Request) (*RunInput, error) {
// ...
agent.UserID = req.Params.Message.ContextID // ⭐ Use contextID as userID
// ...
}
Session State + Multi-Tenant
// ExecutionContext supports both SessionID and UserID
execCtx := workflow.NewExecutionContextWithSession(
"input",
"session-123", // SessionID: Identifier for single session
"user-a", // UserID: User identifier
)
// SessionID: For session state management
// UserID: For multi-tenant data isolation
Related Documentation
- A2A Interface - Agent-to-agent communication
- Session State Management - Workflow session management
- Memory Guide - Memory usage guide
Testing
Complete test coverage includes:
- ✅ Multi-user data isolation
- ✅ Concurrency safety (1000 goroutines)
- ✅ Agent integration tests
- ✅ Memory capacity management
- ✅ Clear operation correctness
Test Coverage: 93.1% (Memory module)
Run tests:
cd pkg/agno/memory
go test -v -run TestInMemory
cd pkg/agno/agent
go test -v -run TestAgent_MultiTenant
Last Updated: 2025-01-08