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
|
# In seconds
|
||||||
JWT_EXPIRATION_TIME=3600
|
JWT_EXPIRATION_TIME=3600
|
||||||
|
|
||||||
|
# Encryption key for API keys
|
||||||
|
ENCRYPTION_KEY="your-encryption-key"
|
||||||
|
|
||||||
# SendGrid
|
# SendGrid
|
||||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||||
EMAIL_FROM="noreply@yourdomain.com"
|
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
|
- JWT authentication with email verification
|
||||||
- **Agent 2 Agent (A2A) Protocol Support**: Interoperability between AI agents following Google's A2A specification
|
- **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
|
- **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
|
## 🤖 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",
|
"description": "Specialized personal assistant",
|
||||||
"type": "llm",
|
"type": "llm",
|
||||||
"model": "gpt-4",
|
"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",
|
"instruction": "Detailed instructions for agent behavior",
|
||||||
"config": {
|
"config": {
|
||||||
"tools": [
|
"tools": [
|
||||||
@ -69,6 +72,7 @@ Agent that implements Google's A2A protocol for agent interoperability.
|
|||||||
"client_id": "{{client_id}}",
|
"client_id": "{{client_id}}",
|
||||||
"type": "a2a",
|
"type": "a2a",
|
||||||
"agent_card_url": "http://localhost:8001/api/v1/a2a/your-agent/.well-known/agent.json",
|
"agent_card_url": "http://localhost:8001/api/v1/a2a/your-agent/.well-known/agent.json",
|
||||||
|
"folder_id": "folder_id (optional)",
|
||||||
"config": {
|
"config": {
|
||||||
"sub_agents": ["sub-agent-uuid"]
|
"sub_agents": ["sub-agent-uuid"]
|
||||||
}
|
}
|
||||||
@ -84,6 +88,7 @@ Executes a sequence of sub-agents in a specific order.
|
|||||||
"client_id": "{{client_id}}",
|
"client_id": "{{client_id}}",
|
||||||
"name": "processing_flow",
|
"name": "processing_flow",
|
||||||
"type": "sequential",
|
"type": "sequential",
|
||||||
|
"folder_id": "folder_id (optional)",
|
||||||
"config": {
|
"config": {
|
||||||
"sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"]
|
"sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"]
|
||||||
}
|
}
|
||||||
@ -99,6 +104,7 @@ Executes multiple sub-agents simultaneously.
|
|||||||
"client_id": "{{client_id}}",
|
"client_id": "{{client_id}}",
|
||||||
"name": "parallel_processing",
|
"name": "parallel_processing",
|
||||||
"type": "parallel",
|
"type": "parallel",
|
||||||
|
"folder_id": "folder_id (optional)",
|
||||||
"config": {
|
"config": {
|
||||||
"sub_agents": ["agent-uuid-1", "agent-uuid-2"]
|
"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}}",
|
"client_id": "{{client_id}}",
|
||||||
"name": "loop_processing",
|
"name": "loop_processing",
|
||||||
"type": "loop",
|
"type": "loop",
|
||||||
|
"folder_id": "folder_id (optional)",
|
||||||
"config": {
|
"config": {
|
||||||
"sub_agents": ["sub-agent-uuid"],
|
"sub_agents": ["sub-agent-uuid"],
|
||||||
"max_iterations": 5
|
"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}}",
|
"client_id": "{{client_id}}",
|
||||||
"name": "workflow_agent",
|
"name": "workflow_agent",
|
||||||
"type": "workflow",
|
"type": "workflow",
|
||||||
|
"folder_id": "folder_id (optional)",
|
||||||
"config": {
|
"config": {
|
||||||
"sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"],
|
"sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"],
|
||||||
"workflow": {
|
"workflow": {
|
||||||
@ -469,6 +477,9 @@ JWT_EXPIRATION_TIME=30 # In seconds
|
|||||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||||
EMAIL_FROM="noreply@yourdomain.com"
|
EMAIL_FROM="noreply@yourdomain.com"
|
||||||
APP_URL="https://yourdomain.com"
|
APP_URL="https://yourdomain.com"
|
||||||
|
|
||||||
|
# Encryption for API keys
|
||||||
|
ENCRYPTION_KEY="your-encryption-key"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Dependencies
|
### Project Dependencies
|
||||||
@ -734,3 +745,149 @@ The main environment variables used by the API container:
|
|||||||
- `SENDGRID_API_KEY`: SendGrid API key for sending emails
|
- `SENDGRID_API_KEY`: SendGrid API key for sending emails
|
||||||
- `EMAIL_FROM`: Email used as sender
|
- `EMAIL_FROM`: Email used as sender
|
||||||
- `APP_URL`: Base URL of the application
|
- `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,10 +16,12 @@ from dotenv import load_dotenv
|
|||||||
from src.models.models import User
|
from src.models.models import User
|
||||||
from src.utils.security import get_password_hash
|
from src.utils.security import get_password_hash
|
||||||
|
|
||||||
# Configurar logging
|
logging.basicConfig(
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_admin_user():
|
def create_admin_user():
|
||||||
"""Create an initial admin user in the system"""
|
"""Create an initial admin user in the system"""
|
||||||
try:
|
try:
|
||||||
@ -46,7 +48,7 @@ def create_admin_user():
|
|||||||
Session = sessionmaker(bind=engine)
|
Session = sessionmaker(bind=engine)
|
||||||
session = Session()
|
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()
|
existing_admin = session.query(User).filter(User.email == admin_email).first()
|
||||||
if existing_admin:
|
if existing_admin:
|
||||||
logger.info(f"Admin with email {admin_email} already exists")
|
logger.info(f"Admin with email {admin_email} already exists")
|
||||||
@ -58,7 +60,7 @@ def create_admin_user():
|
|||||||
password_hash=get_password_hash(admin_password),
|
password_hash=get_password_hash(admin_password),
|
||||||
is_admin=True,
|
is_admin=True,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
email_verified=True
|
email_verified=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add and commit
|
# Add and commit
|
||||||
@ -74,6 +76,7 @@ def create_admin_user():
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = create_admin_user()
|
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.models.models import User, Client
|
||||||
from src.utils.security import get_password_hash
|
from src.utils.security import get_password_hash
|
||||||
|
|
||||||
# Configurar logging
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
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 dotenv import load_dotenv
|
||||||
from src.models.models import MCPServer
|
from src.models.models import MCPServer
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
)
|
)
|
||||||
|
@ -14,11 +14,14 @@ from dotenv import load_dotenv
|
|||||||
from src.models.models import Tool
|
from src.models.models import Tool
|
||||||
|
|
||||||
# Configure logging
|
# 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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_tools():
|
def create_tools():
|
||||||
"""Cria ferramentas padrão no sistema"""
|
"""Create default tools in the system"""
|
||||||
try:
|
try:
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@ -51,7 +54,7 @@ def create_tools():
|
|||||||
name=tool_data["name"],
|
name=tool_data["name"],
|
||||||
description=tool_data["description"],
|
description=tool_data["description"],
|
||||||
config_json=tool_data["config_json"],
|
config_json=tool_data["config_json"],
|
||||||
environments=tool_data["environments"]
|
environments=tool_data["environments"],
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(tool)
|
session.add(tool)
|
||||||
@ -72,6 +75,7 @@ def create_tools():
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = create_tools()
|
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,
|
AgentFolder,
|
||||||
AgentFolderCreate,
|
AgentFolderCreate,
|
||||||
AgentFolderUpdate,
|
AgentFolderUpdate,
|
||||||
|
ApiKey,
|
||||||
|
ApiKeyCreate,
|
||||||
|
ApiKeyUpdate,
|
||||||
)
|
)
|
||||||
from src.services import (
|
from src.services import agent_service, mcp_server_service, apikey_service
|
||||||
agent_service,
|
|
||||||
mcp_server_service,
|
|
||||||
)
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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(
|
@router.post(
|
||||||
"/folders", response_model=AgentFolder, status_code=status.HTTP_201_CREATED
|
"/folders", response_model=AgentFolder, status_code=status.HTTP_201_CREATED
|
||||||
)
|
)
|
||||||
@ -77,8 +212,8 @@ async def create_folder(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Cria uma nova pasta para organizar agentes"""
|
"""Create a new folder to organize agents"""
|
||||||
# Verifica se o usuário tem acesso ao cliente da pasta
|
# Verify if the user has access to the folder's client
|
||||||
await verify_user_client(payload, db, folder.client_id)
|
await verify_user_client(payload, db, folder.client_id)
|
||||||
|
|
||||||
return agent_service.create_agent_folder(
|
return agent_service.create_agent_folder(
|
||||||
@ -94,8 +229,8 @@ async def read_folders(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Lista as pastas de agentes de um cliente"""
|
"""List agent folders for a client"""
|
||||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
await verify_user_client(payload, db, x_client_id)
|
||||||
|
|
||||||
return agent_service.get_agent_folders_by_client(db, x_client_id, skip, limit)
|
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),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Obtém os detalhes de uma pasta específica"""
|
"""Get details of a specific folder"""
|
||||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
await verify_user_client(payload, db, x_client_id)
|
||||||
|
|
||||||
folder = agent_service.get_agent_folder(db, folder_id)
|
folder = agent_service.get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(
|
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:
|
if folder.client_id != x_client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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
|
return folder
|
||||||
@ -136,25 +271,25 @@ async def update_folder(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Atualiza uma pasta de agentes"""
|
"""Update an agent folder"""
|
||||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
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)
|
folder = agent_service.get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(
|
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:
|
if folder.client_id != x_client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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(
|
updated_folder = agent_service.update_agent_folder(
|
||||||
db, folder_id, folder_data.name, folder_data.description
|
db, folder_id, folder_data.name, folder_data.description
|
||||||
)
|
)
|
||||||
@ -168,28 +303,28 @@ async def delete_folder(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Remove uma pasta de agentes"""
|
"""Remove an agent folder"""
|
||||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
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)
|
folder = agent_service.get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(
|
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:
|
if folder.client_id != x_client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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):
|
if not agent_service.delete_agent_folder(db, folder_id):
|
||||||
raise HTTPException(
|
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),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Lista os agentes em uma pasta específica"""
|
"""List agents in a specific folder"""
|
||||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
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)
|
folder = agent_service.get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(
|
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:
|
if folder.client_id != x_client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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)
|
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:
|
for agent in agents:
|
||||||
if not agent.agent_card_url:
|
if not agent.agent_card_url:
|
||||||
agent.agent_card_url = agent.agent_card_url_property
|
agent.agent_card_url = agent.agent_card_url_property
|
||||||
@ -239,39 +374,39 @@ async def assign_agent_to_folder(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
"""Atribui um agente a uma pasta ou remove da pasta atual (se folder_id=None)"""
|
"""Assign an agent to a folder or remove from the current folder (if folder_id=None)"""
|
||||||
# Verifica se o usuário tem acesso aos dados deste cliente
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
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)
|
agent = agent_service.get_agent(db, agent_id)
|
||||||
if not agent:
|
if not agent:
|
||||||
raise HTTPException(
|
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:
|
if agent.client_id != x_client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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:
|
if folder_id:
|
||||||
folder = agent_service.get_agent_folder(db, folder_id)
|
folder = agent_service.get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(
|
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:
|
if folder.client_id != x_client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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)
|
updated_agent = agent_service.assign_agent_to_folder(db, agent_id, folder_id)
|
||||||
|
|
||||||
if not updated_agent.agent_card_url:
|
if not updated_agent.agent_card_url:
|
||||||
@ -280,22 +415,24 @@ async def assign_agent_to_folder(
|
|||||||
return updated_agent
|
return updated_agent
|
||||||
|
|
||||||
|
|
||||||
# Modificação nas rotas existentes para suportar filtro por pasta
|
# Agent routes (after specific routes)
|
||||||
@router.get("/", response_model=List[Agent])
|
@router.get("/", response_model=List[Agent])
|
||||||
async def read_agents(
|
async def read_agents(
|
||||||
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
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),
|
db: Session = Depends(get_db),
|
||||||
payload: dict = Depends(get_jwt_token),
|
payload: dict = Depends(get_jwt_token),
|
||||||
):
|
):
|
||||||
# Verify if the user has access to this client's data
|
# Verify if the user has access to this client's data
|
||||||
await verify_user_client(payload, db, x_client_id)
|
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(
|
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:
|
for agent in agents:
|
||||||
|
@ -49,6 +49,9 @@ class Settings(BaseSettings):
|
|||||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||||
JWT_EXPIRATION_TIME: int = int(os.getenv("JWT_EXPIRATION_TIME", 3600))
|
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 settings
|
||||||
SENDGRID_API_KEY: str = os.getenv("SENDGRID_API_KEY", "")
|
SENDGRID_API_KEY: str = os.getenv("SENDGRID_API_KEY", "")
|
||||||
EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@yourdomain.com")
|
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())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Relationship with Client (One-to-One, optional for administrators)
|
|
||||||
client = relationship(
|
client = relationship(
|
||||||
"Client", backref=backref("user", uselist=False, cascade="all, delete-orphan")
|
"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())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Relação com o cliente
|
|
||||||
client = relationship("Client", backref="agent_folders")
|
client = relationship("Client", backref="agent_folders")
|
||||||
|
|
||||||
# Relação com os agentes
|
|
||||||
agents = relationship("Agent", back_populates="folder")
|
agents = relationship("Agent", back_populates="folder")
|
||||||
|
|
||||||
|
|
||||||
@ -77,10 +74,13 @@ class Agent(Base):
|
|||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
type = Column(String, nullable=False)
|
type = Column(String, nullable=False)
|
||||||
model = Column(String, nullable=True, default="")
|
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)
|
instruction = Column(Text)
|
||||||
agent_card_url = Column(String, nullable=True)
|
agent_card_url = Column(String, nullable=True)
|
||||||
# Nova coluna para a pasta - opcional (nullable=True)
|
|
||||||
folder_id = Column(
|
folder_id = Column(
|
||||||
UUID(as_uuid=True),
|
UUID(as_uuid=True),
|
||||||
ForeignKey("agent_folders.id", ondelete="SET NULL"),
|
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")
|
folder = relationship("AgentFolder", back_populates="agents")
|
||||||
|
|
||||||
|
api_key_ref = relationship("ApiKey", foreign_keys=[api_key_id])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agent_card_url_property(self) -> str:
|
def agent_card_url_property(self) -> str:
|
||||||
"""Virtual URL for the agent card"""
|
"""Virtual URL for the agent card"""
|
||||||
@ -192,7 +193,6 @@ class Tool(Base):
|
|||||||
|
|
||||||
class Session(Base):
|
class Session(Base):
|
||||||
__tablename__ = "sessions"
|
__tablename__ = "sessions"
|
||||||
# The directive below makes Alembic ignore this table in migrations
|
|
||||||
__table_args__ = {"extend_existing": True, "info": {"skip_autogenerate": True}}
|
__table_args__ = {"extend_existing": True, "info": {"skip_autogenerate": True}}
|
||||||
|
|
||||||
id = Column(String, primary_key=True)
|
id = Column(String, primary_key=True)
|
||||||
@ -218,5 +218,19 @@ class AuditLog(Base):
|
|||||||
user_agent = Column(String, nullable=True)
|
user_agent = Column(String, nullable=True)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
# Relationship with User
|
|
||||||
user = relationship("User", backref="audit_logs")
|
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
|
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):
|
class AgentBase(BaseModel):
|
||||||
name: Optional[str] = Field(
|
name: Optional[str] = Field(
|
||||||
None, description="Agent name (no spaces or special characters)"
|
None, description="Agent name (no spaces or special characters)"
|
||||||
@ -44,8 +71,8 @@ class AgentBase(BaseModel):
|
|||||||
model: Optional[str] = Field(
|
model: Optional[str] = Field(
|
||||||
None, description="Agent model (required only for llm type)"
|
None, description="Agent model (required only for llm type)"
|
||||||
)
|
)
|
||||||
api_key: Optional[str] = Field(
|
api_key_id: Optional[UUID4] = Field(
|
||||||
None, description="Agent API Key (required only for llm type)"
|
None, description="Reference to a stored API Key ID"
|
||||||
)
|
)
|
||||||
instruction: Optional[str] = None
|
instruction: Optional[str] = None
|
||||||
agent_card_url: Optional[str] = Field(
|
agent_card_url: Optional[str] = Field(
|
||||||
@ -88,12 +115,22 @@ class AgentBase(BaseModel):
|
|||||||
raise ValueError("Model is required for llm type agents")
|
raise ValueError("Model is required for llm type agents")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@validator("api_key")
|
@validator("api_key_id")
|
||||||
def validate_api_key(cls, v, values):
|
def validate_api_key_id(cls, v, values):
|
||||||
if "type" in values and values["type"] == "llm" and not v:
|
|
||||||
raise ValueError("API Key is required for llm type agents")
|
|
||||||
return v
|
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")
|
@validator("config")
|
||||||
def validate_config(cls, v, values):
|
def validate_config(cls, v, values):
|
||||||
if "type" in values and values["type"] == "a2a":
|
if "type" in values and values["type"] == "a2a":
|
||||||
@ -215,7 +252,6 @@ class Tool(ToolBase):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
# Schema para pasta de agentes
|
|
||||||
class AgentFolderBase(BaseModel):
|
class AgentFolderBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: Optional[str] = None
|
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.mcp_service import MCPService
|
||||||
from src.services.a2a_agent import A2ACustomAgent
|
from src.services.a2a_agent import A2ACustomAgent
|
||||||
from src.services.workflow_agent import WorkflowAgent
|
from src.services.workflow_agent import WorkflowAgent
|
||||||
|
from src.services.apikey_service import get_decrypted_api_key
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from google.adk.tools import load_memory
|
from google.adk.tools import load_memory
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
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"
|
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
|
# Check if load_memory is enabled
|
||||||
# before_model_callback_func = None
|
|
||||||
if agent.config.get("load_memory"):
|
if agent.config.get("load_memory"):
|
||||||
all_tools.append(load_memory)
|
all_tools.append(load_memory)
|
||||||
formatted_prompt = (
|
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"
|
+ "\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 (
|
return (
|
||||||
LlmAgent(
|
LlmAgent(
|
||||||
name=agent.name,
|
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,
|
instruction=formatted_prompt,
|
||||||
description=agent.description,
|
description=agent.description,
|
||||||
tools=all_tools,
|
tools=all_tools,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from fastapi import HTTPException, status
|
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 src.schemas.schemas import AgentCreate
|
||||||
from typing import List, Optional, Dict, Any, Union
|
from typing import List, Optional, Dict, Any, Union
|
||||||
from src.services.mcp_server_service import get_mcp_server
|
from src.services.mcp_server_service import get_mcp_server
|
||||||
@ -90,15 +90,29 @@ def get_agents_by_client(
|
|||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
active_only: bool = True,
|
active_only: bool = True,
|
||||||
folder_id: Optional[uuid.UUID] = None,
|
folder_id: Optional[uuid.UUID] = None,
|
||||||
|
sort_by: str = "name",
|
||||||
|
sort_direction: str = "asc",
|
||||||
) -> List[Agent]:
|
) -> List[Agent]:
|
||||||
"""Search for agents by client with pagination and optional folder filter"""
|
"""Search for agents by client with pagination and optional folder filter"""
|
||||||
try:
|
try:
|
||||||
query = db.query(Agent).filter(Agent.client_id == client_id)
|
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:
|
if folder_id is not None:
|
||||||
query = query.filter(Agent.folder_id == folder_id)
|
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()
|
agents = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
return agents
|
return agents
|
||||||
@ -356,6 +370,28 @@ async def update_agent(
|
|||||||
if not agent:
|
if not agent:
|
||||||
raise HTTPException(status_code=404, detail="Agent not found")
|
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 "type" in agent_data and agent_data["type"] == "a2a":
|
||||||
if "agent_card_url" not in agent_data or not agent_data["agent_card_url"]:
|
if "agent_card_url" not in agent_data or not agent_data["agent_card_url"]:
|
||||||
raise HTTPException(
|
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(
|
def create_agent_folder(
|
||||||
db: Session, client_id: uuid.UUID, name: str, description: Optional[str] = None
|
db: Session, client_id: uuid.UUID, name: str, description: Optional[str] = None
|
||||||
) -> AgentFolder:
|
) -> AgentFolder:
|
||||||
"""Cria uma nova pasta para organizar agentes"""
|
"""Create a new folder to organize agents"""
|
||||||
try:
|
try:
|
||||||
folder = AgentFolder(client_id=client_id, name=name, description=description)
|
folder = AgentFolder(client_id=client_id, name=name, description=description)
|
||||||
db.add(folder)
|
db.add(folder)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(folder)
|
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
|
return folder
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.error(f"Erro ao criar pasta de agentes: {str(e)}")
|
logger.error(f"Error creating agent folder: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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]:
|
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:
|
try:
|
||||||
return db.query(AgentFolder).filter(AgentFolder.id == folder_id).first()
|
return db.query(AgentFolder).filter(AgentFolder.id == folder_id).first()
|
||||||
except SQLAlchemyError as e:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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(
|
def get_agent_folders_by_client(
|
||||||
db: Session, client_id: uuid.UUID, skip: int = 0, limit: int = 100
|
db: Session, client_id: uuid.UUID, skip: int = 0, limit: int = 100
|
||||||
) -> List[AgentFolder]:
|
) -> List[AgentFolder]:
|
||||||
"""Lista as pastas de agentes de um cliente"""
|
"""List the agent folders of a client"""
|
||||||
try:
|
try:
|
||||||
return (
|
return (
|
||||||
db.query(AgentFolder)
|
db.query(AgentFolder)
|
||||||
@ -657,10 +693,10 @@ def get_agent_folders_by_client(
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
except SQLAlchemyError as e:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
) -> Optional[AgentFolder]:
|
) -> Optional[AgentFolder]:
|
||||||
"""Atualiza uma pasta de agentes"""
|
"""Update an agent folder"""
|
||||||
try:
|
try:
|
||||||
folder = get_agent_folder(db, folder_id)
|
folder = get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
@ -683,94 +719,94 @@ def update_agent_folder(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(folder)
|
db.refresh(folder)
|
||||||
logger.info(f"Pasta de agentes atualizada: {folder_id}")
|
logger.info(f"Agent folder updated: {folder_id}")
|
||||||
return folder
|
return folder
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
db.rollback()
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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:
|
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:
|
try:
|
||||||
folder = get_agent_folder(db, folder_id)
|
folder = get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
return False
|
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()
|
agents = db.query(Agent).filter(Agent.folder_id == folder_id).all()
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
agent.folder_id = None
|
agent.folder_id = None
|
||||||
|
|
||||||
# Deleta a pasta
|
# Delete the folder
|
||||||
db.delete(folder)
|
db.delete(folder)
|
||||||
db.commit()
|
db.commit()
|
||||||
logger.info(f"Pasta de agentes removida: {folder_id}")
|
logger.info(f"Agent folder removed: {folder_id}")
|
||||||
return True
|
return True
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
db.rollback()
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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(
|
def assign_agent_to_folder(
|
||||||
db: Session, agent_id: uuid.UUID, folder_id: Optional[uuid.UUID]
|
db: Session, agent_id: uuid.UUID, folder_id: Optional[uuid.UUID]
|
||||||
) -> Optional[Agent]:
|
) -> 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:
|
try:
|
||||||
agent = get_agent(db, agent_id)
|
agent = get_agent(db, agent_id)
|
||||||
if not agent:
|
if not agent:
|
||||||
return None
|
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:
|
if folder_id is None:
|
||||||
agent.folder_id = None
|
agent.folder_id = None
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(agent)
|
db.refresh(agent)
|
||||||
logger.info(f"Agente removido da pasta: {agent_id}")
|
logger.info(f"Agent removed from folder: {agent_id}")
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
# Verifica se a pasta existe
|
# Verify if the folder exists
|
||||||
folder = get_agent_folder(db, folder_id)
|
folder = get_agent_folder(db, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
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:
|
if folder.client_id != agent.client_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
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
|
agent.folder_id = folder_id
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(agent)
|
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
|
return agent
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.error(f"Erro ao atribuir agente à pasta: {str(e)}")
|
logger.error(f"Error assigning agent to folder: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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(
|
def get_agents_by_folder(
|
||||||
db: Session, folder_id: uuid.UUID, skip: int = 0, limit: int = 100
|
db: Session, folder_id: uuid.UUID, skip: int = 0, limit: int = 100
|
||||||
) -> List[Agent]:
|
) -> List[Agent]:
|
||||||
"""Lista os agentes de uma pasta específica"""
|
"""List the agents of a specific folder"""
|
||||||
try:
|
try:
|
||||||
return (
|
return (
|
||||||
db.query(Agent)
|
db.query(Agent)
|
||||||
@ -780,8 +816,8 @@ def get_agents_by_folder(
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
except SQLAlchemyError as e:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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", [])
|
content = state.get("content", [])
|
||||||
conversation_history = state.get("conversation_history", [])
|
conversation_history = state.get("conversation_history", [])
|
||||||
|
|
||||||
# Obter apenas o evento mais recente para avaliação
|
|
||||||
latest_event = None
|
latest_event = None
|
||||||
if content and len(content) > 0:
|
if content and len(content) > 0:
|
||||||
# Ignorar eventos gerados por nós de condição para avaliação
|
|
||||||
for event in reversed(content):
|
for event in reversed(content):
|
||||||
# Verificar se é um evento gerado pelo condition_node
|
|
||||||
if (
|
if (
|
||||||
event.author != "agent"
|
event.author != "agent"
|
||||||
or not hasattr(event.content, "parts")
|
or not hasattr(event.content, "parts")
|
||||||
@ -214,10 +211,10 @@ class WorkflowAgent(BaseAgent):
|
|||||||
break
|
break
|
||||||
if latest_event:
|
if latest_event:
|
||||||
print(
|
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()
|
evaluation_state = state.copy()
|
||||||
if latest_event:
|
if latest_event:
|
||||||
evaluation_state["content"] = [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