feat(api): implement secure API key management with encryption and CRUD operations
This commit is contained in:
parent
72f666aca1
commit
17b4c20a4c
@ -31,6 +31,9 @@ JWT_ALGORITHM="HS256"
|
||||
# In seconds
|
||||
JWT_EXPIRATION_TIME=3600
|
||||
|
||||
# Encryption key for API keys
|
||||
ENCRYPTION_KEY="your-encryption-key"
|
||||
|
||||
# SendGrid
|
||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||
EMAIL_FROM="noreply@yourdomain.com"
|
||||
|
159
README.md
159
README.md
@ -14,6 +14,8 @@ The Evo AI platform allows:
|
||||
- JWT authentication with email verification
|
||||
- **Agent 2 Agent (A2A) Protocol Support**: Interoperability between AI agents following Google's A2A specification
|
||||
- **Workflow Agent with LangGraph**: Building complex agent workflows with LangGraph and ReactFlow
|
||||
- **Secure API Key Management**: Encrypted storage of API keys with Fernet encryption
|
||||
- **Agent Organization**: Folder structure for organizing agents by categories
|
||||
|
||||
## 🤖 Agent Types and Creation
|
||||
|
||||
@ -30,7 +32,8 @@ Agent based on language models like GPT-4, Claude, etc. Can be configured with t
|
||||
"description": "Specialized personal assistant",
|
||||
"type": "llm",
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key",
|
||||
"api_key_id": "stored-api-key-uuid",
|
||||
"folder_id": "folder_id (optional)",
|
||||
"instruction": "Detailed instructions for agent behavior",
|
||||
"config": {
|
||||
"tools": [
|
||||
@ -69,6 +72,7 @@ Agent that implements Google's A2A protocol for agent interoperability.
|
||||
"client_id": "{{client_id}}",
|
||||
"type": "a2a",
|
||||
"agent_card_url": "http://localhost:8001/api/v1/a2a/your-agent/.well-known/agent.json",
|
||||
"folder_id": "folder_id (optional)",
|
||||
"config": {
|
||||
"sub_agents": ["sub-agent-uuid"]
|
||||
}
|
||||
@ -84,6 +88,7 @@ Executes a sequence of sub-agents in a specific order.
|
||||
"client_id": "{{client_id}}",
|
||||
"name": "processing_flow",
|
||||
"type": "sequential",
|
||||
"folder_id": "folder_id (optional)",
|
||||
"config": {
|
||||
"sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"]
|
||||
}
|
||||
@ -99,6 +104,7 @@ Executes multiple sub-agents simultaneously.
|
||||
"client_id": "{{client_id}}",
|
||||
"name": "parallel_processing",
|
||||
"type": "parallel",
|
||||
"folder_id": "folder_id (optional)",
|
||||
"config": {
|
||||
"sub_agents": ["agent-uuid-1", "agent-uuid-2"]
|
||||
}
|
||||
@ -114,6 +120,7 @@ Executes sub-agents in a loop with a defined maximum number of iterations.
|
||||
"client_id": "{{client_id}}",
|
||||
"name": "loop_processing",
|
||||
"type": "loop",
|
||||
"folder_id": "folder_id (optional)",
|
||||
"config": {
|
||||
"sub_agents": ["sub-agent-uuid"],
|
||||
"max_iterations": 5
|
||||
@ -130,6 +137,7 @@ Executes sub-agents in a custom workflow defined by a graph structure. This agen
|
||||
"client_id": "{{client_id}}",
|
||||
"name": "workflow_agent",
|
||||
"type": "workflow",
|
||||
"folder_id": "folder_id (optional)",
|
||||
"config": {
|
||||
"sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"],
|
||||
"workflow": {
|
||||
@ -469,6 +477,9 @@ JWT_EXPIRATION_TIME=30 # In seconds
|
||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||
EMAIL_FROM="noreply@yourdomain.com"
|
||||
APP_URL="https://yourdomain.com"
|
||||
|
||||
# Encryption for API keys
|
||||
ENCRYPTION_KEY="your-encryption-key"
|
||||
```
|
||||
|
||||
### Project Dependencies
|
||||
@ -734,3 +745,149 @@ The main environment variables used by the API container:
|
||||
- `SENDGRID_API_KEY`: SendGrid API key for sending emails
|
||||
- `EMAIL_FROM`: Email used as sender
|
||||
- `APP_URL`: Base URL of the application
|
||||
|
||||
## 🔒 Secure API Key Management
|
||||
|
||||
Evo AI implements a secure API key management system that protects sensitive credentials:
|
||||
|
||||
- **Encrypted Storage**: API keys are encrypted using Fernet symmetric encryption before storage
|
||||
- **Secure References**: Agents reference API keys by UUID (api_key_id) instead of storing raw keys
|
||||
- **Centralized Management**: API keys can be created, updated, and rotated without changing agent configurations
|
||||
- **Client Isolation**: API keys are scoped to specific clients for better security isolation
|
||||
|
||||
### Encryption Configuration
|
||||
|
||||
The encryption system uses a secure key defined in the `.env` file:
|
||||
|
||||
```env
|
||||
ENCRYPTION_KEY="your-secure-encryption-key"
|
||||
```
|
||||
|
||||
If not provided, a secure key will be generated automatically at startup.
|
||||
|
||||
### API Key Management
|
||||
|
||||
API keys can be managed through dedicated endpoints:
|
||||
|
||||
```http
|
||||
# Create a new API key
|
||||
POST /api/v1/agents/apikeys
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
{
|
||||
"client_id": "client-uuid",
|
||||
"name": "My OpenAI Key",
|
||||
"provider": "openai",
|
||||
"key_value": "sk-actual-api-key-value"
|
||||
}
|
||||
|
||||
# List all API keys for a client
|
||||
GET /api/v1/agents/apikeys
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
# Get a specific API key
|
||||
GET /api/v1/agents/apikeys/{key_id}
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
# Update an API key
|
||||
PUT /api/v1/agents/apikeys/{key_id}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
{
|
||||
"name": "Updated Key Name",
|
||||
"provider": "anthropic",
|
||||
"key_value": "new-key-value",
|
||||
"is_active": true
|
||||
}
|
||||
|
||||
# Delete an API key (soft delete)
|
||||
DELETE /api/v1/agents/apikeys/{key_id}
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
```
|
||||
|
||||
## 📁 Agent Organization
|
||||
|
||||
Agents can be organized into folders for better management:
|
||||
|
||||
### Creating and Managing Folders
|
||||
|
||||
```http
|
||||
# Create a new folder
|
||||
POST /api/v1/agents/folders
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer your-token-jwt
|
||||
|
||||
{
|
||||
"client_id": "client-uuid",
|
||||
"name": "Marketing Agents",
|
||||
"description": "Agents for content marketing tasks"
|
||||
}
|
||||
|
||||
# List all folders
|
||||
GET /api/v1/agents/folders
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
# Get a specific folder
|
||||
GET /api/v1/agents/folders/{folder_id}
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
# Update a folder
|
||||
PUT /api/v1/agents/folders/{folder_id}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
{
|
||||
"name": "Updated Folder Name",
|
||||
"description": "Updated folder description"
|
||||
}
|
||||
|
||||
# Delete a folder
|
||||
DELETE /api/v1/agents/folders/{folder_id}
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
# List agents in a folder
|
||||
GET /api/v1/agents/folders/{folder_id}/agents
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
# Assign an agent to a folder
|
||||
PUT /api/v1/agents/{agent_id}/folder
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
{
|
||||
"folder_id": "folder-uuid"
|
||||
}
|
||||
|
||||
# Remove an agent from any folder
|
||||
PUT /api/v1/agents/{agent_id}/folder
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
|
||||
{
|
||||
"folder_id": null
|
||||
}
|
||||
```
|
||||
|
||||
### Filtering Agents by Folder
|
||||
|
||||
When listing agents, you can filter by folder:
|
||||
|
||||
```http
|
||||
GET /api/v1/agents?folder_id=folder-uuid
|
||||
Authorization: Bearer your-token-jwt
|
||||
x-client-id: client-uuid
|
||||
```
|
||||
|
@ -16,64 +16,67 @@ from dotenv import load_dotenv
|
||||
from src.models.models import User
|
||||
from src.utils.security import get_password_hash
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_admin_user():
|
||||
"""Create an initial admin user in the system"""
|
||||
try:
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Get database settings
|
||||
db_url = os.getenv("POSTGRES_CONNECTION_STRING")
|
||||
if not db_url:
|
||||
logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined")
|
||||
return False
|
||||
|
||||
|
||||
# Get admin password
|
||||
admin_password = os.getenv("ADMIN_INITIAL_PASSWORD")
|
||||
if not admin_password:
|
||||
logger.error("Environment variable ADMIN_INITIAL_PASSWORD not defined")
|
||||
return False
|
||||
|
||||
|
||||
# Admin email configuration
|
||||
admin_email = os.getenv("ADMIN_EMAIL", "admin@evoai.com")
|
||||
|
||||
|
||||
# Connect to the database
|
||||
engine = create_engine(db_url)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Verificar se o administrador já existe
|
||||
|
||||
# Verify if the admin already exists
|
||||
existing_admin = session.query(User).filter(User.email == admin_email).first()
|
||||
if existing_admin:
|
||||
logger.info(f"Admin with email {admin_email} already exists")
|
||||
return True
|
||||
|
||||
|
||||
# Create admin
|
||||
admin_user = User(
|
||||
email=admin_email,
|
||||
password_hash=get_password_hash(admin_password),
|
||||
is_admin=True,
|
||||
is_active=True,
|
||||
email_verified=True
|
||||
email_verified=True,
|
||||
)
|
||||
|
||||
|
||||
# Add and commit
|
||||
session.add(admin_user)
|
||||
session.commit()
|
||||
|
||||
|
||||
logger.info(f"Admin created successfully: {admin_email}")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating admin: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = create_admin_user()
|
||||
sys.exit(0 if success else 1)
|
||||
sys.exit(0 if success else 1)
|
||||
|
@ -19,7 +19,6 @@ from dotenv import load_dotenv
|
||||
from src.models.models import User, Client
|
||||
from src.utils.security import get_password_hash
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
@ -16,7 +16,6 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from dotenv import load_dotenv
|
||||
from src.models.models import MCPServer
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
@ -14,64 +14,68 @@ from dotenv import load_dotenv
|
||||
from src.models.models import Tool
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_tools():
|
||||
"""Cria ferramentas padrão no sistema"""
|
||||
"""Create default tools in the system"""
|
||||
try:
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Get database settings
|
||||
db_url = os.getenv("POSTGRES_CONNECTION_STRING")
|
||||
if not db_url:
|
||||
logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined")
|
||||
return False
|
||||
|
||||
|
||||
# Connect to the database
|
||||
engine = create_engine(db_url)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
|
||||
try:
|
||||
# Check if there are already tools
|
||||
existing_tools = session.query(Tool).all()
|
||||
if existing_tools:
|
||||
logger.info(f"There are already {len(existing_tools)} tools registered")
|
||||
return True
|
||||
|
||||
|
||||
# Tools definitions
|
||||
tools = []
|
||||
|
||||
|
||||
# Create the tools
|
||||
for tool_data in tools:
|
||||
|
||||
|
||||
tool = Tool(
|
||||
name=tool_data["name"],
|
||||
description=tool_data["description"],
|
||||
config_json=tool_data["config_json"],
|
||||
environments=tool_data["environments"]
|
||||
environments=tool_data["environments"],
|
||||
)
|
||||
|
||||
|
||||
session.add(tool)
|
||||
logger.info(f"Tool '{tool_data['name']}' created successfully")
|
||||
|
||||
|
||||
session.commit()
|
||||
logger.info("All tools were created successfully")
|
||||
return True
|
||||
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
session.rollback()
|
||||
logger.error(f"Database error when creating tools: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error when creating tools: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = create_tools()
|
||||
sys.exit(0 if success else 1)
|
||||
sys.exit(0 if success else 1)
|
||||
|
@ -13,11 +13,11 @@ from src.schemas.schemas import (
|
||||
AgentFolder,
|
||||
AgentFolderCreate,
|
||||
AgentFolderUpdate,
|
||||
ApiKey,
|
||||
ApiKeyCreate,
|
||||
ApiKeyUpdate,
|
||||
)
|
||||
from src.services import (
|
||||
agent_service,
|
||||
mcp_server_service,
|
||||
)
|
||||
from src.services import agent_service, mcp_server_service, apikey_service
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -68,7 +68,142 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
# Rotas para pastas de agentes
|
||||
@router.post("/apikeys", response_model=ApiKey, status_code=status.HTTP_201_CREATED)
|
||||
async def create_api_key(
|
||||
key: ApiKeyCreate,
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Create a new API key"""
|
||||
await verify_user_client(payload, db, key.client_id)
|
||||
|
||||
db_key = apikey_service.create_api_key(
|
||||
db, key.client_id, key.name, key.provider, key.key_value
|
||||
)
|
||||
|
||||
return db_key
|
||||
|
||||
|
||||
@router.get("/apikeys", response_model=List[ApiKey])
|
||||
async def read_api_keys(
|
||||
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
sort_by: str = Query(
|
||||
"name", description="Field to sort: name, provider, created_at"
|
||||
),
|
||||
sort_direction: str = Query("asc", description="Sort direction: asc, desc"),
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""List API keys for a client"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
keys = apikey_service.get_api_keys_by_client(
|
||||
db, x_client_id, skip, limit, sort_by, sort_direction
|
||||
)
|
||||
return keys
|
||||
|
||||
|
||||
@router.get("/apikeys/{key_id}", response_model=ApiKey)
|
||||
async def read_api_key(
|
||||
key_id: uuid.UUID,
|
||||
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Get details of a specific API key"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
key = apikey_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found"
|
||||
)
|
||||
|
||||
# Verify if the key belongs to the specified client
|
||||
if key.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="API Key does not belong to the specified client",
|
||||
)
|
||||
|
||||
return key
|
||||
|
||||
|
||||
@router.put("/apikeys/{key_id}", response_model=ApiKey)
|
||||
async def update_api_key(
|
||||
key_id: uuid.UUID,
|
||||
key_data: ApiKeyUpdate,
|
||||
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Update an API key"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Verify if the key exists
|
||||
key = apikey_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found"
|
||||
)
|
||||
|
||||
# Verify if the key belongs to the specified client
|
||||
if key.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="API Key does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Update the key
|
||||
updated_key = apikey_service.update_api_key(
|
||||
db,
|
||||
key_id,
|
||||
key_data.name,
|
||||
key_data.provider,
|
||||
key_data.key_value,
|
||||
key_data.is_active,
|
||||
)
|
||||
return updated_key
|
||||
|
||||
|
||||
@router.delete("/apikeys/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_api_key(
|
||||
key_id: uuid.UUID,
|
||||
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Deactivate an API key (soft delete)"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Verify if the key exists
|
||||
key = apikey_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found"
|
||||
)
|
||||
|
||||
# Verify if the key belongs to the specified client
|
||||
if key.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="API Key does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Deactivate the key
|
||||
if not apikey_service.delete_api_key(db, key_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found"
|
||||
)
|
||||
|
||||
|
||||
# Agent folder routes
|
||||
@router.post(
|
||||
"/folders", response_model=AgentFolder, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
@ -77,8 +212,8 @@ async def create_folder(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Cria uma nova pasta para organizar agentes"""
|
||||
# Verifica se o usuário tem acesso ao cliente da pasta
|
||||
"""Create a new folder to organize agents"""
|
||||
# Verify if the user has access to the folder's client
|
||||
await verify_user_client(payload, db, folder.client_id)
|
||||
|
||||
return agent_service.create_agent_folder(
|
||||
@ -94,8 +229,8 @@ async def read_folders(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Lista as pastas de agentes de um cliente"""
|
||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
||||
"""List agent folders for a client"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
return agent_service.get_agent_folders_by_client(db, x_client_id, skip, limit)
|
||||
@ -108,21 +243,21 @@ async def read_folder(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Obtém os detalhes de uma pasta específica"""
|
||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
||||
"""Get details of a specific folder"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
folder = agent_service.get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
# Verifica se a pasta pertence ao cliente informado
|
||||
# Verify if the folder belongs to the specified client
|
||||
if folder.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Pasta não pertence ao cliente informado",
|
||||
detail="Folder does not belong to the specified client",
|
||||
)
|
||||
|
||||
return folder
|
||||
@ -136,25 +271,25 @@ async def update_folder(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Atualiza uma pasta de agentes"""
|
||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
||||
"""Update an agent folder"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Verifica se a pasta existe
|
||||
# Verify if the folder exists
|
||||
folder = agent_service.get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
# Verifica se a pasta pertence ao cliente informado
|
||||
# Verify if the folder belongs to the specified client
|
||||
if folder.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Pasta não pertence ao cliente informado",
|
||||
detail="Folder does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Atualiza a pasta
|
||||
# Update the folder
|
||||
updated_folder = agent_service.update_agent_folder(
|
||||
db, folder_id, folder_data.name, folder_data.description
|
||||
)
|
||||
@ -168,28 +303,28 @@ async def delete_folder(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Remove uma pasta de agentes"""
|
||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
||||
"""Remove an agent folder"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Verifica se a pasta existe
|
||||
# Verify if the folder exists
|
||||
folder = agent_service.get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
# Verifica se a pasta pertence ao cliente informado
|
||||
# Verify if the folder belongs to the specified client
|
||||
if folder.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Pasta não pertence ao cliente informado",
|
||||
detail="Folder does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Deleta a pasta
|
||||
# Delete the folder
|
||||
if not agent_service.delete_agent_folder(db, folder_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
|
||||
@ -202,28 +337,28 @@ async def read_folder_agents(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Lista os agentes em uma pasta específica"""
|
||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
||||
"""List agents in a specific folder"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Verifica se a pasta existe
|
||||
# Verify if the folder exists
|
||||
folder = agent_service.get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
# Verifica se a pasta pertence ao cliente informado
|
||||
# Verify if the folder belongs to the specified client
|
||||
if folder.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Pasta não pertence ao cliente informado",
|
||||
detail="Folder does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Lista os agentes da pasta
|
||||
# List the agents in the folder
|
||||
agents = agent_service.get_agents_by_folder(db, folder_id, skip, limit)
|
||||
|
||||
# Adiciona URL do agent card quando necessário
|
||||
# Add agent card URL when needed
|
||||
for agent in agents:
|
||||
if not agent.agent_card_url:
|
||||
agent.agent_card_url = agent.agent_card_url_property
|
||||
@ -239,39 +374,39 @@ async def assign_agent_to_folder(
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
"""Atribui um agente a uma pasta ou remove da pasta atual (se folder_id=None)"""
|
||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
||||
"""Assign an agent to a folder or remove from the current folder (if folder_id=None)"""
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Verifica se o agente existe
|
||||
# Verify if the agent exists
|
||||
agent = agent_service.get_agent(db, agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
|
||||
)
|
||||
|
||||
# Verifica se o agente pertence ao cliente informado
|
||||
# Verify if the agent belongs to the specified client
|
||||
if agent.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Agente não pertence ao cliente informado",
|
||||
detail="Agent does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Se folder_id for fornecido, verifica se a pasta existe e pertence ao mesmo cliente
|
||||
# If folder_id is provided, verify if the folder exists and belongs to the same client
|
||||
if folder_id:
|
||||
folder = agent_service.get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
if folder.client_id != x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Pasta não pertence ao cliente informado",
|
||||
detail="Folder does not belong to the specified client",
|
||||
)
|
||||
|
||||
# Atribui o agente à pasta ou remove da pasta atual
|
||||
# Assign the agent to the folder or remove from the current folder
|
||||
updated_agent = agent_service.assign_agent_to_folder(db, agent_id, folder_id)
|
||||
|
||||
if not updated_agent.agent_card_url:
|
||||
@ -280,22 +415,24 @@ async def assign_agent_to_folder(
|
||||
return updated_agent
|
||||
|
||||
|
||||
# Modificação nas rotas existentes para suportar filtro por pasta
|
||||
# Agent routes (after specific routes)
|
||||
@router.get("/", response_model=List[Agent])
|
||||
async def read_agents(
|
||||
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
folder_id: Optional[uuid.UUID] = Query(None, description="Filtrar por pasta"),
|
||||
folder_id: Optional[uuid.UUID] = Query(None, description="Filter by folder"),
|
||||
sort_by: str = Query("name", description="Field to sort: name, created_at"),
|
||||
sort_direction: str = Query("asc", description="Sort direction: asc, desc"),
|
||||
db: Session = Depends(get_db),
|
||||
payload: dict = Depends(get_jwt_token),
|
||||
):
|
||||
# Verify if the user has access to this client's data
|
||||
await verify_user_client(payload, db, x_client_id)
|
||||
|
||||
# Get agents with optional folder filter
|
||||
# Get agents with optional folder filter and sorting
|
||||
agents = agent_service.get_agents_by_client(
|
||||
db, x_client_id, skip, limit, True, folder_id
|
||||
db, x_client_id, skip, limit, True, folder_id, sort_by, sort_direction
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
|
@ -49,6 +49,9 @@ class Settings(BaseSettings):
|
||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
JWT_EXPIRATION_TIME: int = int(os.getenv("JWT_EXPIRATION_TIME", 3600))
|
||||
|
||||
# Encryption settings
|
||||
ENCRYPTION_KEY: str = os.getenv("ENCRYPTION_KEY", secrets.token_urlsafe(32))
|
||||
|
||||
# SendGrid settings
|
||||
SENDGRID_API_KEY: str = os.getenv("SENDGRID_API_KEY", "")
|
||||
EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@yourdomain.com")
|
||||
|
@ -45,7 +45,6 @@ class User(Base):
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationship with Client (One-to-One, optional for administrators)
|
||||
client = relationship(
|
||||
"Client", backref=backref("user", uselist=False, cascade="all, delete-orphan")
|
||||
)
|
||||
@ -61,10 +60,8 @@ class AgentFolder(Base):
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relação com o cliente
|
||||
client = relationship("Client", backref="agent_folders")
|
||||
|
||||
# Relação com os agentes
|
||||
agents = relationship("Agent", back_populates="folder")
|
||||
|
||||
|
||||
@ -77,10 +74,13 @@ class Agent(Base):
|
||||
description = Column(Text, nullable=True)
|
||||
type = Column(String, nullable=False)
|
||||
model = Column(String, nullable=True, default="")
|
||||
api_key = Column(String, nullable=True, default="")
|
||||
api_key_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("api_keys.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
instruction = Column(Text)
|
||||
agent_card_url = Column(String, nullable=True)
|
||||
# Nova coluna para a pasta - opcional (nullable=True)
|
||||
folder_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("agent_folders.id", ondelete="SET NULL"),
|
||||
@ -97,9 +97,10 @@ class Agent(Base):
|
||||
),
|
||||
)
|
||||
|
||||
# Relação com a pasta
|
||||
folder = relationship("AgentFolder", back_populates="agents")
|
||||
|
||||
api_key_ref = relationship("ApiKey", foreign_keys=[api_key_id])
|
||||
|
||||
@property
|
||||
def agent_card_url_property(self) -> str:
|
||||
"""Virtual URL for the agent card"""
|
||||
@ -192,7 +193,6 @@ class Tool(Base):
|
||||
|
||||
class Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
# The directive below makes Alembic ignore this table in migrations
|
||||
__table_args__ = {"extend_existing": True, "info": {"skip_autogenerate": True}}
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
@ -218,5 +218,19 @@ class AuditLog(Base):
|
||||
user_agent = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationship with User
|
||||
user = relationship("User", backref="audit_logs")
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
client_id = Column(UUID(as_uuid=True), ForeignKey("clients.id", ondelete="CASCADE"))
|
||||
name = Column(String, nullable=False)
|
||||
provider = Column(String, nullable=False)
|
||||
encrypted_key = Column(String, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
client = relationship("Client", backref="api_keys")
|
||||
|
@ -33,6 +33,33 @@ class Client(ClientBase):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ApiKeyBase(BaseModel):
|
||||
name: str
|
||||
provider: str
|
||||
|
||||
|
||||
class ApiKeyCreate(ApiKeyBase):
|
||||
client_id: UUID4
|
||||
key_value: str
|
||||
|
||||
|
||||
class ApiKeyUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
provider: Optional[str] = None
|
||||
key_value: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class ApiKey(ApiKeyBase):
|
||||
id: UUID4
|
||||
client_id: UUID4
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
is_active: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class AgentBase(BaseModel):
|
||||
name: Optional[str] = Field(
|
||||
None, description="Agent name (no spaces or special characters)"
|
||||
@ -44,8 +71,8 @@ class AgentBase(BaseModel):
|
||||
model: Optional[str] = Field(
|
||||
None, description="Agent model (required only for llm type)"
|
||||
)
|
||||
api_key: Optional[str] = Field(
|
||||
None, description="Agent API Key (required only for llm type)"
|
||||
api_key_id: Optional[UUID4] = Field(
|
||||
None, description="Reference to a stored API Key ID"
|
||||
)
|
||||
instruction: Optional[str] = None
|
||||
agent_card_url: Optional[str] = Field(
|
||||
@ -88,12 +115,22 @@ class AgentBase(BaseModel):
|
||||
raise ValueError("Model is required for llm type agents")
|
||||
return v
|
||||
|
||||
@validator("api_key")
|
||||
def validate_api_key(cls, v, values):
|
||||
if "type" in values and values["type"] == "llm" and not v:
|
||||
raise ValueError("API Key is required for llm type agents")
|
||||
@validator("api_key_id")
|
||||
def validate_api_key_id(cls, v, values):
|
||||
return v
|
||||
|
||||
# Código anterior (comentado temporariamente)
|
||||
# # Se o tipo for llm, api_key_id é obrigatório
|
||||
# if "type" in values and values["type"] == "llm" and not v:
|
||||
# # Verifica se tem api_key no config (retrocompatibilidade)
|
||||
# if "config" in values and values["config"] and "api_key" in values["config"]:
|
||||
# # Tem api_key no config, então aceita
|
||||
# return v
|
||||
# raise ValueError(
|
||||
# "api_key_id é obrigatório para agentes do tipo llm"
|
||||
# )
|
||||
# return v
|
||||
|
||||
@validator("config")
|
||||
def validate_config(cls, v, values):
|
||||
if "type" in values and values["type"] == "a2a":
|
||||
@ -215,7 +252,6 @@ class Tool(ToolBase):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Schema para pasta de agentes
|
||||
class AgentFolderBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
@ -10,11 +10,13 @@ from src.services.custom_tools import CustomToolBuilder
|
||||
from src.services.mcp_service import MCPService
|
||||
from src.services.a2a_agent import A2ACustomAgent
|
||||
from src.services.workflow_agent import WorkflowAgent
|
||||
from src.services.apikey_service import get_decrypted_api_key
|
||||
from sqlalchemy.orm import Session
|
||||
from contextlib import AsyncExitStack
|
||||
from google.adk.tools import load_memory
|
||||
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
@ -120,7 +122,6 @@ class AgentBuilder:
|
||||
formatted_prompt += "<get_current_time_instructions>Use the get_current_time tool to get the current time in a city. The tool is available in the tools section of the configuration. Use 'new york' by default if no city is provided.ALWAYS use the 'get_current_time' tool when you need to use the current date and time in any type of situation, whether in interaction with the user or for calling other tools.</get_current_time_instructions>\n\n"
|
||||
|
||||
# Check if load_memory is enabled
|
||||
# before_model_callback_func = None
|
||||
if agent.config.get("load_memory"):
|
||||
all_tools.append(load_memory)
|
||||
formatted_prompt = (
|
||||
@ -128,10 +129,48 @@ class AgentBuilder:
|
||||
+ "\n\n<memory_instructions>ALWAYS use the load_memory tool to retrieve knowledge for your context</memory_instructions>\n\n"
|
||||
)
|
||||
|
||||
# Get API key from api_key_id
|
||||
api_key = None
|
||||
|
||||
# Get API key from api_key_id
|
||||
if hasattr(agent, "api_key_id") and agent.api_key_id:
|
||||
decrypted_key = get_decrypted_api_key(self.db, agent.api_key_id)
|
||||
if decrypted_key:
|
||||
logger.info(f"Using stored API key for agent {agent.name}")
|
||||
api_key = decrypted_key
|
||||
else:
|
||||
logger.error(
|
||||
f"Stored API key not found for agent {agent.name}"
|
||||
)
|
||||
raise ValueError(
|
||||
f"API key with ID {agent.api_key_id} not found or inactive"
|
||||
)
|
||||
else:
|
||||
# Check if there is an API key in the config (temporary field)
|
||||
config_api_key = agent.config.get("api_key") if agent.config else None
|
||||
if config_api_key:
|
||||
logger.info(f"Using config API key for agent {agent.name}")
|
||||
# Check if it is a UUID of a stored key
|
||||
try:
|
||||
key_id = uuid.UUID(config_api_key)
|
||||
decrypted_key = get_decrypted_api_key(self.db, key_id)
|
||||
if decrypted_key:
|
||||
logger.info(f"Config API key is a valid reference")
|
||||
api_key = decrypted_key
|
||||
else:
|
||||
# Use the key directly
|
||||
api_key = config_api_key
|
||||
except (ValueError, TypeError):
|
||||
# It is not a UUID, use directly
|
||||
api_key = config_api_key
|
||||
else:
|
||||
logger.error(f"No API key configured for agent {agent.name}")
|
||||
raise ValueError(f"Agent {agent.name} does not have a configured API key")
|
||||
|
||||
return (
|
||||
LlmAgent(
|
||||
name=agent.name,
|
||||
model=LiteLlm(model=agent.model, api_key=agent.api_key),
|
||||
model=LiteLlm(model=agent.model, api_key=api_key),
|
||||
instruction=formatted_prompt,
|
||||
description=agent.description,
|
||||
tools=all_tools,
|
||||
|
@ -1,7 +1,7 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from fastapi import HTTPException, status
|
||||
from src.models.models import Agent, AgentFolder
|
||||
from src.models.models import Agent, AgentFolder, ApiKey
|
||||
from src.schemas.schemas import AgentCreate
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from src.services.mcp_server_service import get_mcp_server
|
||||
@ -90,15 +90,29 @@ def get_agents_by_client(
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
folder_id: Optional[uuid.UUID] = None,
|
||||
sort_by: str = "name",
|
||||
sort_direction: str = "asc",
|
||||
) -> List[Agent]:
|
||||
"""Search for agents by client with pagination and optional folder filter"""
|
||||
try:
|
||||
query = db.query(Agent).filter(Agent.client_id == client_id)
|
||||
|
||||
# Filtra por pasta se especificado
|
||||
# Filter by folder if specified
|
||||
if folder_id is not None:
|
||||
query = query.filter(Agent.folder_id == folder_id)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by == "name":
|
||||
if sort_direction.lower() == "desc":
|
||||
query = query.order_by(Agent.name.desc())
|
||||
else:
|
||||
query = query.order_by(Agent.name)
|
||||
elif sort_by == "created_at":
|
||||
if sort_direction.lower() == "desc":
|
||||
query = query.order_by(Agent.created_at.desc())
|
||||
else:
|
||||
query = query.order_by(Agent.created_at)
|
||||
|
||||
agents = query.offset(skip).limit(limit).all()
|
||||
|
||||
return agents
|
||||
@ -356,6 +370,28 @@ async def update_agent(
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
# Check if api_key_id is defined
|
||||
if "api_key_id" in agent_data and agent_data["api_key_id"]:
|
||||
# Check if the referenced API key exists
|
||||
api_key_id = agent_data["api_key_id"]
|
||||
if isinstance(api_key_id, str):
|
||||
api_key_id = uuid.UUID(api_key_id)
|
||||
|
||||
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"API Key with ID {api_key_id} not found",
|
||||
)
|
||||
|
||||
# Check if the key belongs to the agent's client
|
||||
if api_key.client_id != agent.client_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="API Key does not belong to the same client as the agent",
|
||||
)
|
||||
|
||||
# Continue with the original code
|
||||
if "type" in agent_data and agent_data["type"] == "a2a":
|
||||
if "agent_card_url" not in agent_data or not agent_data["agent_card_url"]:
|
||||
raise HTTPException(
|
||||
@ -611,43 +647,43 @@ def activate_agent(db: Session, agent_id: uuid.UUID) -> bool:
|
||||
)
|
||||
|
||||
|
||||
# Funções para pastas de agentes
|
||||
# Functions for agent folders
|
||||
def create_agent_folder(
|
||||
db: Session, client_id: uuid.UUID, name: str, description: Optional[str] = None
|
||||
) -> AgentFolder:
|
||||
"""Cria uma nova pasta para organizar agentes"""
|
||||
"""Create a new folder to organize agents"""
|
||||
try:
|
||||
folder = AgentFolder(client_id=client_id, name=name, description=description)
|
||||
db.add(folder)
|
||||
db.commit()
|
||||
db.refresh(folder)
|
||||
logger.info(f"Pasta de agentes criada com sucesso: {folder.id}")
|
||||
logger.info(f"Agent folder created successfully: {folder.id}")
|
||||
return folder
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao criar pasta de agentes: {str(e)}")
|
||||
logger.error(f"Error creating agent folder: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro ao criar pasta de agentes: {str(e)}",
|
||||
detail=f"Error creating agent folder: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
def get_agent_folder(db: Session, folder_id: uuid.UUID) -> Optional[AgentFolder]:
|
||||
"""Busca uma pasta de agentes pelo ID"""
|
||||
"""Search for an agent folder by ID"""
|
||||
try:
|
||||
return db.query(AgentFolder).filter(AgentFolder.id == folder_id).first()
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Erro ao buscar pasta de agentes {folder_id}: {str(e)}")
|
||||
logger.error(f"Error searching for agent folder {folder_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Erro ao buscar pasta de agentes",
|
||||
detail="Error searching for agent folder",
|
||||
)
|
||||
|
||||
|
||||
def get_agent_folders_by_client(
|
||||
db: Session, client_id: uuid.UUID, skip: int = 0, limit: int = 100
|
||||
) -> List[AgentFolder]:
|
||||
"""Lista as pastas de agentes de um cliente"""
|
||||
"""List the agent folders of a client"""
|
||||
try:
|
||||
return (
|
||||
db.query(AgentFolder)
|
||||
@ -657,10 +693,10 @@ def get_agent_folders_by_client(
|
||||
.all()
|
||||
)
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Erro ao listar pastas de agentes: {str(e)}")
|
||||
logger.error(f"Error listing agent folders: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Erro ao listar pastas de agentes",
|
||||
detail="Error listing agent folders",
|
||||
)
|
||||
|
||||
|
||||
@ -670,7 +706,7 @@ def update_agent_folder(
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> Optional[AgentFolder]:
|
||||
"""Atualiza uma pasta de agentes"""
|
||||
"""Update an agent folder"""
|
||||
try:
|
||||
folder = get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
@ -683,94 +719,94 @@ def update_agent_folder(
|
||||
|
||||
db.commit()
|
||||
db.refresh(folder)
|
||||
logger.info(f"Pasta de agentes atualizada: {folder_id}")
|
||||
logger.info(f"Agent folder updated: {folder_id}")
|
||||
return folder
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao atualizar pasta de agentes {folder_id}: {str(e)}")
|
||||
logger.error(f"Error updating agent folder {folder_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Erro ao atualizar pasta de agentes",
|
||||
detail="Error updating agent folder",
|
||||
)
|
||||
|
||||
|
||||
def delete_agent_folder(db: Session, folder_id: uuid.UUID) -> bool:
|
||||
"""Remove uma pasta de agentes e desvincula os agentes"""
|
||||
"""Remove an agent folder and unassign the agents"""
|
||||
try:
|
||||
folder = get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
return False
|
||||
|
||||
# Desvincula os agentes da pasta (não deleta os agentes)
|
||||
# Unassign the agents from the folder (do not delete the agents)
|
||||
agents = db.query(Agent).filter(Agent.folder_id == folder_id).all()
|
||||
for agent in agents:
|
||||
agent.folder_id = None
|
||||
|
||||
# Deleta a pasta
|
||||
# Delete the folder
|
||||
db.delete(folder)
|
||||
db.commit()
|
||||
logger.info(f"Pasta de agentes removida: {folder_id}")
|
||||
logger.info(f"Agent folder removed: {folder_id}")
|
||||
return True
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao remover pasta de agentes {folder_id}: {str(e)}")
|
||||
logger.error(f"Error removing agent folder {folder_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Erro ao remover pasta de agentes",
|
||||
detail="Error removing agent folder",
|
||||
)
|
||||
|
||||
|
||||
def assign_agent_to_folder(
|
||||
db: Session, agent_id: uuid.UUID, folder_id: Optional[uuid.UUID]
|
||||
) -> Optional[Agent]:
|
||||
"""Atribui um agente a uma pasta (ou remove da pasta se folder_id for None)"""
|
||||
"""Assign an agent to a folder (or remove from folder if folder_id is None)"""
|
||||
try:
|
||||
agent = get_agent(db, agent_id)
|
||||
if not agent:
|
||||
return None
|
||||
|
||||
# Se folder_id for None, remove o agente da pasta atual
|
||||
# If folder_id is None, remove the agent from the current folder
|
||||
if folder_id is None:
|
||||
agent.folder_id = None
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
logger.info(f"Agente removido da pasta: {agent_id}")
|
||||
logger.info(f"Agent removed from folder: {agent_id}")
|
||||
return agent
|
||||
|
||||
# Verifica se a pasta existe
|
||||
# Verify if the folder exists
|
||||
folder = get_agent_folder(db, folder_id)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Pasta não encontrada",
|
||||
detail="Folder not found",
|
||||
)
|
||||
|
||||
# Verifica se a pasta pertence ao mesmo cliente do agente
|
||||
# Verify if the folder belongs to the same client as the agent
|
||||
if folder.client_id != agent.client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="A pasta deve pertencer ao mesmo cliente do agente",
|
||||
detail="The folder must belong to the same client as the agent",
|
||||
)
|
||||
|
||||
# Atribui o agente à pasta
|
||||
# Assign the agent to the folder
|
||||
agent.folder_id = folder_id
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
logger.info(f"Agente atribuído à pasta {folder_id}: {agent_id}")
|
||||
logger.info(f"Agent assigned to folder: {folder_id}")
|
||||
return agent
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao atribuir agente à pasta: {str(e)}")
|
||||
logger.error(f"Error assigning agent to folder: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Erro ao atribuir agente à pasta",
|
||||
detail="Error assigning agent to folder",
|
||||
)
|
||||
|
||||
|
||||
def get_agents_by_folder(
|
||||
db: Session, folder_id: uuid.UUID, skip: int = 0, limit: int = 100
|
||||
) -> List[Agent]:
|
||||
"""Lista os agentes de uma pasta específica"""
|
||||
"""List the agents of a specific folder"""
|
||||
try:
|
||||
return (
|
||||
db.query(Agent)
|
||||
@ -780,8 +816,8 @@ def get_agents_by_folder(
|
||||
.all()
|
||||
)
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Erro ao listar agentes da pasta {folder_id}: {str(e)}")
|
||||
logger.error(f"Error listing agents of folder {folder_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Erro ao listar agentes da pasta",
|
||||
detail="Error listing agents of folder",
|
||||
)
|
||||
|
166
src/services/apikey_service.py
Normal file
166
src/services/apikey_service.py
Normal file
@ -0,0 +1,166 @@
|
||||
from src.models.models import ApiKey
|
||||
from src.utils.crypto import encrypt_api_key, decrypt_api_key
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from fastapi import HTTPException, status
|
||||
import uuid
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_api_key(
|
||||
db: Session, client_id: uuid.UUID, name: str, provider: str, key_value: str
|
||||
) -> ApiKey:
|
||||
"""Create a new encrypted API key"""
|
||||
try:
|
||||
# Encrypt the key before saving
|
||||
encrypted = encrypt_api_key(key_value)
|
||||
|
||||
# Create the ApiKey object
|
||||
api_key = ApiKey(
|
||||
client_id=client_id,
|
||||
name=name,
|
||||
provider=provider,
|
||||
encrypted_key=encrypted,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Save in the database
|
||||
db.add(api_key)
|
||||
db.commit()
|
||||
db.refresh(api_key)
|
||||
logger.info(f"API key '{name}' created for client {client_id}")
|
||||
return api_key
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating API key: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating API key: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
def get_api_key(db: Session, key_id: uuid.UUID) -> Optional[ApiKey]:
|
||||
"""Get an API key by ID"""
|
||||
try:
|
||||
return db.query(ApiKey).filter(ApiKey.id == key_id).first()
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error getting API key {key_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error getting API key",
|
||||
)
|
||||
|
||||
|
||||
def get_api_keys_by_client(
|
||||
db: Session,
|
||||
client_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
sort_by: str = "name",
|
||||
sort_direction: str = "asc",
|
||||
) -> List[ApiKey]:
|
||||
"""List the API keys of a client"""
|
||||
try:
|
||||
query = (
|
||||
db.query(ApiKey)
|
||||
.filter(ApiKey.client_id == client_id)
|
||||
.filter(ApiKey.is_active == True)
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by == "name":
|
||||
if sort_direction.lower() == "desc":
|
||||
query = query.order_by(ApiKey.name.desc())
|
||||
else:
|
||||
query = query.order_by(ApiKey.name)
|
||||
elif sort_by == "provider":
|
||||
if sort_direction.lower() == "desc":
|
||||
query = query.order_by(ApiKey.provider.desc())
|
||||
else:
|
||||
query = query.order_by(ApiKey.provider)
|
||||
elif sort_by == "created_at":
|
||||
if sort_direction.lower() == "desc":
|
||||
query = query.order_by(ApiKey.created_at.desc())
|
||||
else:
|
||||
query = query.order_by(ApiKey.created_at)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error listing API keys for client {client_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error listing API keys",
|
||||
)
|
||||
|
||||
|
||||
def get_decrypted_api_key(db: Session, key_id: uuid.UUID) -> Optional[str]:
|
||||
"""Get the decrypted value of an API key"""
|
||||
try:
|
||||
key = get_api_key(db, key_id)
|
||||
if not key or not key.is_active:
|
||||
logger.warning(f"API key {key_id} not found or inactive")
|
||||
return None
|
||||
return decrypt_api_key(key.encrypted_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Error decrypting API key {key_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def update_api_key(
|
||||
db: Session,
|
||||
key_id: uuid.UUID,
|
||||
name: Optional[str] = None,
|
||||
provider: Optional[str] = None,
|
||||
key_value: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
) -> Optional[ApiKey]:
|
||||
"""Update an API key"""
|
||||
try:
|
||||
key = get_api_key(db, key_id)
|
||||
if not key:
|
||||
return None
|
||||
|
||||
if name is not None:
|
||||
key.name = name
|
||||
if provider is not None:
|
||||
key.provider = provider
|
||||
if key_value is not None:
|
||||
key.encrypted_key = encrypt_api_key(key_value)
|
||||
if is_active is not None:
|
||||
key.is_active = is_active
|
||||
|
||||
db.commit()
|
||||
db.refresh(key)
|
||||
logger.info(f"API key {key_id} updated")
|
||||
return key
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating API key {key_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error updating API key",
|
||||
)
|
||||
|
||||
|
||||
def delete_api_key(db: Session, key_id: uuid.UUID) -> bool:
|
||||
"""Remove an API key (soft delete)"""
|
||||
try:
|
||||
key = get_api_key(db, key_id)
|
||||
if not key:
|
||||
return False
|
||||
|
||||
# Soft delete - only marks as inactive
|
||||
key.is_active = False
|
||||
db.commit()
|
||||
logger.info(f"API key {key_id} deactivated")
|
||||
return True
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting API key {key_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error deleting API key",
|
||||
)
|
@ -199,12 +199,9 @@ class WorkflowAgent(BaseAgent):
|
||||
content = state.get("content", [])
|
||||
conversation_history = state.get("conversation_history", [])
|
||||
|
||||
# Obter apenas o evento mais recente para avaliação
|
||||
latest_event = None
|
||||
if content and len(content) > 0:
|
||||
# Ignorar eventos gerados por nós de condição para avaliação
|
||||
for event in reversed(content):
|
||||
# Verificar se é um evento gerado pelo condition_node
|
||||
if (
|
||||
event.author != "agent"
|
||||
or not hasattr(event.content, "parts")
|
||||
@ -214,10 +211,10 @@ class WorkflowAgent(BaseAgent):
|
||||
break
|
||||
if latest_event:
|
||||
print(
|
||||
f"Avaliando condição apenas para o evento mais recente: '{latest_event}'"
|
||||
f"Evaluating condition only for the most recent event: '{latest_event}'"
|
||||
)
|
||||
|
||||
# Usar apenas o evento mais recente para avaliação de condição
|
||||
# Use only the most recent event for condition evaluation
|
||||
evaluation_state = state.copy()
|
||||
if latest_event:
|
||||
evaluation_state["content"] = [latest_event]
|
||||
|
39
src/utils/crypto.py
Normal file
39
src/utils/crypto.py
Normal file
@ -0,0 +1,39 @@
|
||||
from cryptography.fernet import Fernet
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Get the secret key from the .env or generate one
|
||||
SECRET_KEY = os.getenv("ENCRYPTION_KEY")
|
||||
if not SECRET_KEY:
|
||||
SECRET_KEY = Fernet.generate_key().decode()
|
||||
logger.warning(f"ENCRYPTION_KEY missing from .env. Generated: {SECRET_KEY}")
|
||||
|
||||
# Create the Fernet object with the key
|
||||
fernet = Fernet(SECRET_KEY.encode() if isinstance(SECRET_KEY, str) else SECRET_KEY)
|
||||
|
||||
|
||||
def encrypt_api_key(api_key: str) -> str:
|
||||
"""Encrypt an API key before saving in the database"""
|
||||
if not api_key:
|
||||
return ""
|
||||
try:
|
||||
return fernet.encrypt(api_key.encode()).decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Error encrypting API key: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def decrypt_api_key(encrypted_key: str) -> str:
|
||||
"""Decrypt an API key for use"""
|
||||
if not encrypted_key:
|
||||
return ""
|
||||
try:
|
||||
return fernet.decrypt(encrypted_key.encode()).decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Error decrypting API key: {str(e)}")
|
||||
raise
|
Loading…
Reference in New Issue
Block a user