From 215e76012e43235d272b0c96159e302830748d0c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 28 Apr 2025 20:04:51 -0300 Subject: [PATCH] translate to english --- .cursorrules | 8 + .env.example | 28 +- README.md | 1029 +------------------------- docs/README.md | 19 + docs/contributing/CODE_OF_CONDUCT.md | 65 ++ docs/contributing/CONTRIBUTING.md | 129 ++++ docs/technical/API_FLOW.md | 213 ++++++ docs/technical/ARCHITECTURE.md | 222 ++++++ docs/technical/DATA_MODEL.md | 317 ++++++++ docs/technical/DEPLOYMENT.md | 509 +++++++++++++ evo-ai-api.postman_collection.json | 750 ------------------- scripts/run_seeders.py | 56 +- scripts/seeders/admin_seeder.py | 30 +- scripts/seeders/agent_seeder.py | 116 ++- scripts/seeders/client_seeder.py | 48 +- scripts/seeders/contact_seeder.py | 42 +- scripts/seeders/mcp_server_seeder.py | 42 +- scripts/seeders/tool_seeder.py | 35 +- src/api/admin_routes.py | 78 +- src/api/agent_routes.py | 121 +++ src/api/auth_routes.py | 112 +-- src/api/chat_routes.py | 74 ++ src/api/client_routes.py | 140 ++++ src/api/contact_routes.py | 122 +++ src/api/mcp_server_routes.py | 97 +++ src/api/routes.py | 663 ----------------- src/api/session_routes.py | 138 ++++ src/api/tool_routes.py | 97 +++ src/config/settings.py | 82 +- src/core/exceptions.py | 14 +- src/core/jwt_middleware.py | 58 +- src/examples/agent_example.py | 54 -- src/main.py | 74 +- src/models/models.py | 10 +- src/schemas/agent_config.py | 48 +- src/schemas/audit.py | 8 +- src/schemas/chat.py | 26 +- src/schemas/schemas.py | 34 +- src/schemas/user.py | 4 +- src/services/agent_builder.py | 97 ++- src/services/agent_runner.py | 34 +- src/services/agent_service.py | 98 ++- src/services/audit_service.py | 56 +- src/services/auth_service.py | 77 +- src/services/client_service.py | 74 +- src/services/contact_service.py | 38 +- src/services/custom_tools.py | 56 +- src/services/mcp_server_service.py | 38 +- src/services/mcp_service.py | 53 +- src/services/session_service.py | 53 +- src/services/tool_service.py | 38 +- src/services/user_service.py | 278 +++---- src/utils/logger.py | 15 +- src/utils/security.py | 12 +- 54 files changed, 3269 insertions(+), 3460 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/contributing/CODE_OF_CONDUCT.md create mode 100644 docs/contributing/CONTRIBUTING.md create mode 100644 docs/technical/API_FLOW.md create mode 100644 docs/technical/ARCHITECTURE.md create mode 100644 docs/technical/DATA_MODEL.md create mode 100644 docs/technical/DEPLOYMENT.md delete mode 100644 evo-ai-api.postman_collection.json create mode 100644 src/api/agent_routes.py create mode 100644 src/api/chat_routes.py create mode 100644 src/api/client_routes.py create mode 100644 src/api/contact_routes.py create mode 100644 src/api/mcp_server_routes.py delete mode 100644 src/api/routes.py create mode 100644 src/api/session_routes.py create mode 100644 src/api/tool_routes.py delete mode 100644 src/examples/agent_example.py diff --git a/.cursorrules b/.cursorrules index ec0d0b92..35727f6d 100644 --- a/.cursorrules +++ b/.cursorrules @@ -55,6 +55,14 @@ src/ ## Code Standards +### Language Requirements +- **ALL code comments, docstrings, and logging messages MUST be written in English** +- Variable names, function names, and class names must be in English +- API error messages must be in English +- Documentation (including inline comments) must be in English +- Code examples in documentation must be in English +- Commit messages must be in English + ### Schemas (Pydantic) - Use `BaseModel` as base for all schemas - Define fields with explicit types diff --git a/.env.example b/.env.example index bf610fd1..6e92e9bc 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +1,38 @@ -# Configurações do banco de dados +# Database settings POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai" -# Configurações de logging +# Logging settings LOG_LEVEL="INFO" LOG_DIR="logs" -# Configurações do Redis +# Redis settings REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 -REDIS_PASSWORD="sua-senha-redis" +REDIS_PASSWORD="your-redis-password" -# TTL do cache de ferramentas em segundos (1 hora) +# Tools cache TTL in seconds (1 hour) TOOLS_CACHE_TTL=3600 -# Configurações JWT -JWT_SECRET_KEY="sua-chave-secreta-jwt" +# JWT settings +JWT_SECRET_KEY="your-jwt-secret-key" JWT_ALGORITHM="HS256" -# Em minutos +# In minutes JWT_EXPIRATION_TIME=30 # SendGrid -SENDGRID_API_KEY="sua-sendgrid-api-key" +SENDGRID_API_KEY="your-sendgrid-api-key" EMAIL_FROM="noreply@yourdomain.com" APP_URL="https://yourdomain.com" -# Configurações do Servidor +# Server settings HOST="0.0.0.0" PORT=8000 DEBUG=false -# Configurações de Seeders +# Seeder settings ADMIN_EMAIL="admin@evoai.com" -ADMIN_INITIAL_PASSWORD="senhaforte123" -DEMO_EMAIL="demo@exemplo.com" +ADMIN_INITIAL_PASSWORD="strongpassword123" +DEMO_EMAIL="demo@example.com" DEMO_PASSWORD="demo123" -DEMO_CLIENT_NAME="Cliente Demo" +DEMO_CLIENT_NAME="Demo Client" diff --git a/README.md b/README.md index 4bc32c18..b4a96b9b 100644 --- a/README.md +++ b/README.md @@ -156,981 +156,6 @@ The API will be available at `http://localhost:8000` ## 📚 API Documentation -### Authentication - -#### Register User -```http -POST /api/v1/auth/register -``` - -**Request Body:** -```json -{ - "email": "user@example.com", - "password": "securePassword123", - "name": "Company Name" -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "email": "user@example.com", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "is_active": false, - "email_verified": false, - "is_admin": false, - "created_at": "2023-07-10T15:00:00.000Z" -} -``` - -Registers a new user and sends a verification email. The user will remain inactive until the email is verified. - -#### Login -```http -POST /api/v1/auth/login -``` - -**Request Body:** -```json -{ - "email": "user@example.com", - "password": "securePassword123" -} -``` - -**Response (200 OK):** -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "token_type": "bearer" -} -``` - -Authenticates the user and returns a valid JWT token for use in subsequent requests. - -#### Recover logged user data -```http -POST /api/v1/auth/me -``` - -**Response (200 OK):** -```json -{ - "email": "user@example.com", - "id": "777d7036-ebe7-4b79-9cc4-981fa0640d2a", - "client_id": "161e054a-6654-4a2e-90dd-b2499685797a", - "is_active": true, - "email_verified": true, - "is_admin": false, - "created_at": "2025-04-28T18:56:42.832905Z" -} -``` - -#### Verify Email -```http -GET /api/v1/auth/verify-email/{token} -``` - -**Response (200 OK):** -```json -{ - "message": "Email successfully verified. Your account is now active." -} -``` - -Verifies the user's email using the token sent by email. Activates the user's account. - -#### Resend Verification -```http -POST /api/v1/auth/resend-verification -``` - -**Request Body:** -```json -{ - "email": "user@example.com" -} -``` - -**Response (200 OK):** -```json -{ - "message": "Verification email resent. Please check your inbox." -} -``` - -Resends the verification email for users with unverified email. - -#### Forgot Password -```http -POST /api/v1/auth/forgot-password -``` - -**Request Body:** -```json -{ - "email": "user@example.com" -} -``` - -**Response (200 OK):** -```json -{ - "message": "If the email exists in our database, a password reset link will be sent." -} -``` - -Sends an email with password recovery instructions if the email is registered. - -#### Reset Password -```http -POST /api/v1/auth/reset-password -``` - -**Request Body:** -```json -{ - "token": "password-reset-token-received-by-email", - "new_password": "newSecurePassword456" -} -``` - -**Response (200 OK):** -```json -{ - "message": "Password successfully reset." -} -``` - -Resets the user's password using the token received by email. - -### Clients - -#### Create Client -```http -POST /api/v1/clients/ -``` - -**Request Body:** -```json -{ - "name": "Company Name", - "email": "user@example.com" -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "Company Name", - "email": "user@example.com" - "created_at": "2023-07-10T15:00:00.000Z" -} -``` - -Creates a new client. Requires administrator permissions. - -#### List Clients -```http -GET /api/v1/clients/ -``` - -**Query Parameters:** -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "Company Name", - "email": "user@example.com", - "created_at": "2023-07-10T15:00:00.000Z" - } -] -``` - -Lists all clients with pagination. For administrator users, returns all clients. For regular users, returns only the client they are associated with. - -#### Get Client -```http -GET /api/v1/clients/{client_id} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "Company Name", - "email": "user@example.com", - "created_at": "2023-07-10T15:00:00.000Z" -} -``` - -Gets a specific client. The user must have permission to access this client. - -#### Update Client -```http -PUT /api/v1/clients/{client_id} -``` - -**Request Body:** -```json -{ - "name": "New Company Name", - "email": "user@example.com" -} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "New Company Name", - "email": "user@example.com", - "created_at": "2023-07-10T15:00:00.000Z" -} -``` - -Updates client data. The user must have permission to access this client. - -#### Delete Client -```http -DELETE /api/v1/clients/{client_id} -``` - -**Response (204 No Content)** - -Deletes a client. Requires administrator permissions. - -### Contacts - -#### Create Contact -```http -POST /api/v1/contacts/ -``` - -**Request Body:** -```json -{ - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "ext_id": "optional-external-id", - "name": "Contact Name", - "meta": { - "phone": "+15551234567", - "category": "customer", - "notes": "Additional information" - } -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "ext_id": "optional-external-id", - "name": "Contact Name", - "meta": { - "phone": "+15551234567", - "category": "customer", - "notes": "Additional information" - } -} -``` - -Creates a new contact. The user must have permission to access the specified client. - -#### List Contacts -```http -GET /api/v1/contacts/{client_id} -``` - -**Query Parameters:** -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "ext_id": "optional-external-id", - "name": "Contact Name", - "meta": { - "phone": "+15551234567", - "category": "customer" - } - } -] -``` - -Lists contacts of a client. The user must have permission to access this client. - -#### Search Contact -```http -GET /contact/{contact_id} -``` - -#### Update Contact -```http -PUT /contact/{contact_id} -``` -Updates contact data. - -#### Remove Contact -```http -DELETE /contact/{contact_id} -``` -Removes a contact. - -### Agents - -#### Create Agent -```http -POST /api/v1/agents/ -``` - -**Request Body:** -```json -{ - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "customer-service-agent", - "description": "Agent for customer service", - "type": "llm", - "model": "claude-3-opus-20240229", - "api_key": "your-api-key-here", - "instruction": "You are a customer service assistant for company X. Always be polite and try to solve customer problems efficiently.", - "config": { - "temperature": 0.7, - "max_tokens": 1024, - "tools": ["web_search", "knowledge_base"] - } -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "customer-service-agent", - "description": "Agent for customer service", - "type": "llm", - "model": "claude-3-opus-20240229", - "api_key": "your-api-key-here", - "instruction": "You are a customer service assistant for company X. Always be polite and try to solve customer problems efficiently.", - "config": { - "temperature": 0.7, - "max_tokens": 1024, - "tools": ["web_search", "knowledge_base"] - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" -} -``` - -Creates a new agent. The user must have permission to access the specified client. - -**Notes about agent types:** -- For `llm` type agents, the `model` and `api_key` fields are required -- For `sequential`, `parallel`, or `loop` type agents, the configuration must include a list of `sub_agents` - -#### List Agents -```http -GET /api/v1/agents/{client_id} -``` - -**Query Parameters:** -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "customer-service-agent", - "description": "Agent for customer service", - "type": "llm", - "model": "claude-3-opus-20240229", - "instruction": "You are a customer service assistant...", - "config": { - "temperature": 0.7, - "max_tokens": 1024, - "tools": ["web_search", "knowledge_base"] - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" - } -] -``` - -Lists all agents of a client. The user must have permission to access the specified client. - -#### Get Agent -```http -GET /api/v1/agents/{agent_id} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "customer-service-agent", - "description": "Agent for customer service", - "type": "llm", - "model": "claude-3-opus-20240229", - "instruction": "You are a customer service assistant...", - "config": { - "temperature": 0.7, - "max_tokens": 1024, - "tools": ["web_search", "knowledge_base"] - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" -} -``` - -Gets a specific agent. The user must have permission to access this agent. - -#### Update Agent -```http -PUT /api/v1/agents/{agent_id} -``` - -**Request Body:** -```json -{ - "name": "new-customer-service-agent", - "description": "Updated agent for customer service", - "type": "llm", - "model": "claude-3-sonnet-20240229", - "instruction": "You are a customer service assistant for company X...", - "config": { - "temperature": 0.5, - "max_tokens": 2048, - "tools": ["web_search", "knowledge_base", "calculator"] - } -} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "new-customer-service-agent", - "description": "Updated agent for customer service", - "type": "llm", - "model": "claude-3-sonnet-20240229", - "instruction": "You are a customer service assistant for company X...", - "config": { - "temperature": 0.5, - "max_tokens": 2048, - "tools": ["web_search", "knowledge_base", "calculator"] - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:05:00.000Z" -} -``` - -Updates agent data. The user must have permission to access this agent. - -#### Delete Agent -```http -DELETE /api/v1/agents/{agent_id} -``` - -**Response (204 No Content)** - -Deletes an agent. The user must have permission to access this agent. - -### MCP Servers - -#### Create MCP Server -```http -POST /api/v1/mcp-servers/ -``` - -**Request Body:** -```json -{ - "name": "openai-server", - "description": "MCP server for OpenAI API access", - "config_json": { - "base_url": "https://api.openai.com/v1", - "timeout": 30 - }, - "environments": { - "OPENAI_API_KEY": "${OPENAI_API_KEY}" - }, - "tools": ["web_search", "knowledge_base"], - "type": "official" -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "openai-server", - "description": "MCP server for OpenAI API access", - "config_json": { - "base_url": "https://api.openai.com/v1", - "timeout": 30 - }, - "environments": { - "OPENAI_API_KEY": "${OPENAI_API_KEY}" - }, - "tools": ["web_search", "knowledge_base"], - "type": "official", - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" -} -``` - -Creates a new MCP server. Requires administrator permissions. - -#### List MCP Servers -```http -GET /api/v1/mcp-servers/ -``` - -**Query Parameters:** -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "openai-server", - "description": "MCP server for OpenAI API access", - "config_json": { - "base_url": "https://api.openai.com/v1", - "timeout": 30 - }, - "environments": { - "OPENAI_API_KEY": "${OPENAI_API_KEY}" - }, - "tools": ["web_search", "knowledge_base"], - "type": "official", - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" - } -] -``` - -Lists all available MCP servers. - -#### Get MCP Server -```http -GET /api/v1/mcp-servers/{server_id} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "openai-server", - "description": "MCP server for OpenAI API access", - "config_json": { - "base_url": "https://api.openai.com/v1", - "timeout": 30 - }, - "environments": { - "OPENAI_API_KEY": "${OPENAI_API_KEY}" - }, - "tools": ["web_search", "knowledge_base"], - "type": "official", - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" -} -``` - -Gets a specific MCP server. - -#### Update MCP Server -```http -PUT /api/v1/mcp-servers/{server_id} -``` - -**Request Body:** -```json -{ - "name": "updated-openai-server", - "description": "Updated MCP server for OpenAI API access", - "config_json": { - "base_url": "https://api.openai.com/v1", - "timeout": 60 - }, - "environments": { - "OPENAI_API_KEY": "${OPENAI_API_KEY}", - "OPENAI_ORG_ID": "${OPENAI_ORG_ID}" - }, - "tools": ["web_search", "knowledge_base", "image_generation"], - "type": "official" -} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "updated-openai-server", - "description": "Updated MCP server for OpenAI API access", - "config_json": { - "base_url": "https://api.openai.com/v1", - "timeout": 60 - }, - "environments": { - "OPENAI_API_KEY": "${OPENAI_API_KEY}", - "OPENAI_ORG_ID": "${OPENAI_ORG_ID}" - }, - "tools": ["web_search", "knowledge_base", "image_generation"], - "type": "official", - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:05:00.000Z" -} -``` - -Updates an MCP server. Requires administrator permissions. - -#### Delete MCP Server -```http -DELETE /api/v1/mcp-servers/{server_id} -``` - -**Response (204 No Content)** - -Deletes an MCP server. Requires administrator permissions. - -### Tools - -#### Create Tool -```http -POST /api/v1/tools/ -``` - -**Request Body:** -```json -{ - "name": "web_search", - "description": "Real-time web search", - "config_json": { - "api_url": "https://api.search.com", - "max_results": 5 - }, - "environments": { - "SEARCH_API_KEY": "${SEARCH_API_KEY}" - } -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "web_search", - "description": "Real-time web search", - "config_json": { - "api_url": "https://api.search.com", - "max_results": 5 - }, - "environments": { - "SEARCH_API_KEY": "${SEARCH_API_KEY}" - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" -} -``` - -Creates a new tool. Requires administrator permissions. - -#### List Tools -```http -GET /api/v1/tools/ -``` - -**Query Parameters:** -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "web_search", - "description": "Real-time web search", - "config_json": { - "api_url": "https://api.search.com", - "max_results": 5 - }, - "environments": { - "SEARCH_API_KEY": "${SEARCH_API_KEY}" - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" - } -] -``` - -Lists all available tools. - -#### Get Tool -```http -GET /api/v1/tools/{tool_id} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "web_search", - "description": "Real-time web search", - "config_json": { - "api_url": "https://api.search.com", - "max_results": 5 - }, - "environments": { - "SEARCH_API_KEY": "${SEARCH_API_KEY}" - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:00:00.000Z" -} -``` - -Gets a specific tool. - -#### Update Tool -```http -PUT /api/v1/tools/{tool_id} -``` - -**Request Body:** -```json -{ - "name": "web_search_pro", - "description": "Real-time web search with advanced filters", - "config_json": { - "api_url": "https://api.search.com/v2", - "max_results": 10, - "filters": { - "safe_search": true, - "time_range": "week" - } - }, - "environments": { - "SEARCH_API_KEY": "${SEARCH_API_KEY}" - } -} -``` - -**Response (200 OK):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "web_search_pro", - "description": "Real-time web search with advanced filters", - "config_json": { - "api_url": "https://api.search.com/v2", - "max_results": 10, - "filters": { - "safe_search": true, - "time_range": "week" - } - }, - "environments": { - "SEARCH_API_KEY": "${SEARCH_API_KEY}" - }, - "created_at": "2023-07-10T15:00:00.000Z", - "updated_at": "2023-07-10T15:05:00.000Z" -} -``` - -Updates a tool. Requires administrator permissions. - -#### Delete Tool -```http -DELETE /api/v1/tools/{tool_id} -``` - -**Response (204 No Content)** - -Deletes a tool. Requires administrator permissions. - -### Chat - -#### Send Message -```http -POST /api/v1/chat -``` - -**Request Body:** -```json -{ - "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "message": "Hello, I need help with my order." -} -``` - -**Response (200 OK):** -```json -{ - "response": "Hello! Of course, I'm here to help with your order. Could you please provide your order number or more details about the issue you're experiencing?", - "status": "success", - "timestamp": "2023-07-10T15:00:00.000Z" -} -``` - -Sends a message to an agent and returns the generated response. The user must have permission to access the specified agent and contact. - -#### Conversation History -```http -GET /api/v1/chat/history/{contact_id} -``` - -**Query Parameters:** -- `agent_id` (optional): Filter by a specific agent -- `start_date` (optional): Start date in ISO 8601 format -- `end_date` (optional): End date in ISO 8601 format -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "message": "Hello, I need help with my order.", - "response": "Hello! Of course, I'm here to help with your order. Could you please provide your order number or more details about the issue you're experiencing?", - "timestamp": "2023-07-10T15:00:00.000Z" - } -] -``` - -Retrieves the conversation history of a specific contact. The user must have permission to access the specified contact. - -### Administration - -#### Audit Logs -```http -GET /api/v1/admin/audit-logs -``` - -**Query Parameters:** -- `user_id` (optional): Filter by user -- `action` (optional): Filter by action type -- `start_date` (optional): Start date in ISO 8601 format -- `end_date` (optional): End date in ISO 8601 format -- `resource_type` (optional): Type of affected resource -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "action": "CREATE", - "resource_type": "AGENT", - "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2023-07-10T15:00:00.000Z", - "ip_address": "192.168.1.1", - "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - "details": { - "before": null, - "after": { - "name": "customer-service-agent", - "type": "llm" - } - } - } -] -``` - -Retrieves audit logs. Requires administrator permissions. - -#### List Administrators -```http -GET /api/v1/admin/users -``` - -**Query Parameters:** -- `skip` (optional): Number of records to skip (default: 0) -- `limit` (optional): Maximum number of records to return (default: 100) - -**Response (200 OK):** -```json -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "email": "admin@example.com", - "name": "Administrator", - "is_active": true, - "email_verified": true, - "is_admin": true, - "created_at": "2023-07-10T15:00:00.000Z" - } -] -``` - -Lists all administrator users. Requires administrator permissions. - -#### Create Administrator -```http -POST /api/v1/admin/users -``` - -**Request Body:** -```json -{ - "email": "new_admin@example.com", - "password": "securePassword123", - "name": "New Administrator" -} -``` - -**Response (201 Created):** -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "email": "new_admin@example.com", - "name": "New Administrator", - "is_active": true, - "email_verified": true, - "is_admin": true, - "created_at": "2023-07-10T15:00:00.000Z" -} -``` - -Creates a new administrator user. Requires administrator permissions. - -#### Deactivate Administrator -```http -DELETE /api/v1/admin/users/{user_id} -``` - -**Response (204 No Content)** - -Deactivates an administrator user. Requires administrator permissions. The user is not removed from the database, just marked as inactive. - -## 📝 Interactive Documentation - The interactive API documentation is available at: - Swagger UI: `http://localhost:8000/docs` - ReDoc: `http://localhost:8000/redoc` @@ -1160,65 +185,65 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file - [SQLAlchemy](https://www.sqlalchemy.org/) - [Google ADK](https://github.com/google/adk) -## Executando com Docker +## Running with Docker -Para facilitar a implantação e execução da aplicação, fornecemos configurações para Docker e Docker Compose. +To facilitate deployment and execution of the application, we provide Docker and Docker Compose configurations. -### Pré-requisitos +### Prerequisites -- Docker instalado -- Docker Compose instalado +- Docker installed +- Docker Compose installed -### Configuração +### Configuration -1. Configure as variáveis de ambiente necessárias no arquivo `.env` na raiz do projeto (ou use variáveis de ambiente do sistema) +1. Configure the necessary environment variables in the `.env` file at the root of the project (or use system environment variables) -2. Construa a imagem Docker: +2. Build the Docker image: ```bash make docker-build ``` -3. Inicie os serviços (API, PostgreSQL e Redis): +3. Start the services (API, PostgreSQL, and Redis): ```bash make docker-up ``` -4. Popule o banco de dados com dados iniciais: +4. Populate the database with initial data: ```bash make docker-seed ``` -5. Para verificar os logs da aplicação: +5. To check application logs: ```bash make docker-logs ``` -6. Para parar os serviços: +6. To stop the services: ```bash make docker-down ``` -### Serviços Disponíveis +### Available Services - **API**: http://localhost:8000 -- **Documentação da API**: http://localhost:8000/docs +- **API Documentation**: http://localhost:8000/docs - **PostgreSQL**: localhost:5432 - **Redis**: localhost:6379 -### Volumes Persistentes +### Persistent Volumes -O Docker Compose configura volumes persistentes para: -- Dados do PostgreSQL -- Dados do Redis -- Diretório de logs da aplicação +Docker Compose sets up persistent volumes for: +- PostgreSQL data +- Redis data +- Application logs directory -### Variáveis de Ambiente +### Environment Variables -As principais variáveis de ambiente usadas pelo contêiner da API: +The main environment variables used by the API container: -- `POSTGRES_CONNECTION_STRING`: String de conexão com o PostgreSQL -- `REDIS_HOST`: Host do Redis -- `JWT_SECRET_KEY`: Chave secreta para geração de tokens JWT -- `SENDGRID_API_KEY`: Chave da API do SendGrid para envio de emails -- `EMAIL_FROM`: Email usado como remetente -- `APP_URL`: URL base da aplicação \ No newline at end of file +- `POSTGRES_CONNECTION_STRING`: PostgreSQL connection string +- `REDIS_HOST`: Redis host +- `JWT_SECRET_KEY`: Secret key for JWT token generation +- `SENDGRID_API_KEY`: SendGrid API key for sending emails +- `EMAIL_FROM`: Email used as sender +- `APP_URL`: Base URL of the application \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..24cce7f1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# Evo AI Documentation + +This directory contains comprehensive documentation for the Evo AI platform. + +## Structure + +- **swagger/** - OpenAPI/Swagger documentation for the REST API +- **technical/** - Technical documentation for developers, including architecture diagrams, data models, and workflows +- **contributing/** - Guidelines and information for contributors + +## Purpose + +These documents aim to provide clear and detailed information for: + +1. Developers who want to contribute to the Evo AI codebase +2. Developers who want to integrate with the Evo AI API +3. Technical teams who want to understand the architecture and implementation details + +All documentation is maintained in English to ensure accessibility for a global developer community. \ No newline at end of file diff --git a/docs/contributing/CODE_OF_CONDUCT.md b/docs/contributing/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..7b7d5a71 --- /dev/null +++ b/docs/contributing/CODE_OF_CONDUCT.md @@ -0,0 +1,65 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Project maintainers are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainers responsible for enforcement at +[INSERT CONTACT EMAIL]. All complaints will be reviewed and investigated +promptly and fairly. + +All project maintainers are obligated to respect the privacy and security of the +reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. \ No newline at end of file diff --git a/docs/contributing/CONTRIBUTING.md b/docs/contributing/CONTRIBUTING.md new file mode 100644 index 00000000..352a3b4b --- /dev/null +++ b/docs/contributing/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing to Evo AI + +Thank you for your interest in contributing to Evo AI! This document provides guidelines and instructions for contributing to the project. + +## Getting Started + +### Prerequisites + +- Python 3.8+ +- PostgreSQL +- Redis +- Git + +### Setup Development Environment + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com/YOUR-USERNAME/evo-ai.git + cd evo-ai + ``` +3. Create a virtual environment: + ```bash + python -m venv .venv + source .venv/bin/activate # Linux/Mac + # or + .venv\Scripts\activate # Windows + ``` +4. Install dependencies: + ```bash + pip install -r requirements.txt + pip install -r requirements-dev.txt + ``` +5. Set up environment variables: + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` +6. Run database migrations: + ```bash + make alembic-upgrade + ``` + +## Development Workflow + +### Branching Strategy + +- `main` - Main branch, contains stable code +- `feature/*` - For new features +- `bugfix/*` - For bug fixes +- `docs/*` - For documentation changes + +### Creating a New Feature + +1. Create a new branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` +2. Make your changes +3. Run tests: + ```bash + make test + ``` +4. Commit your changes: + ```bash + git commit -m "Add feature: description of your changes" + ``` +5. Push to your fork: + ```bash + git push origin feature/your-feature-name + ``` +6. Create a Pull Request to the main repository + +## Coding Standards + +### Python Code Style + +- Follow PEP 8 +- Use 4 spaces for indentation +- Maximum line length of 79 characters +- Use descriptive variable names +- Write docstrings for all functions, classes, and modules + +### Commit Messages + +- Use the present tense ("Add feature" not "Added feature") +- Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +- First line should be a summary under 50 characters +- Reference issues and pull requests where appropriate + +## Testing + +- All new features should include tests +- All bug fixes should include tests that reproduce the bug +- Run the full test suite before submitting a PR + +## Documentation + +- Update documentation for any new features or API changes +- Documentation should be written in English +- Use Markdown for formatting + +## Pull Request Process + +1. Ensure your code follows the coding standards +2. Update the documentation as needed +3. Include tests for new functionality +4. Ensure the test suite passes +5. Update the CHANGELOG.md if applicable +6. The PR will be reviewed by maintainers +7. Once approved, it will be merged into the main branch + +## Code Review Process + +All submissions require review. We use GitHub pull requests for this purpose. + +Reviewers will check for: +- Code quality and style +- Test coverage +- Documentation +- Appropriateness of the change + +## Community + +- Be respectful and considerate of others +- Help others who have questions +- Follow the code of conduct + +Thank you for contributing to Evo AI! \ No newline at end of file diff --git a/docs/technical/API_FLOW.md b/docs/technical/API_FLOW.md new file mode 100644 index 00000000..9e8fb5bc --- /dev/null +++ b/docs/technical/API_FLOW.md @@ -0,0 +1,213 @@ +# Evo AI - API Flows + +This document describes common API flows and usage patterns for the Evo AI platform. + +## Authentication Flow + +### User Registration and Verification + +```mermaid +sequenceDiagram + Client->>API: POST /api/v1/auth/register + API->>Database: Create user (inactive) + API->>Email Service: Send verification email + API-->>Client: Return user details + Client->>API: GET /api/v1/auth/verify-email/{token} + API->>Database: Activate user + API-->>Client: Return success message +``` + +### Login Flow + +```mermaid +sequenceDiagram + Client->>API: POST /api/v1/auth/login + API->>Database: Validate credentials + API->>Auth Service: Generate JWT token + API-->>Client: Return JWT token + Client->>API: Request with Authorization header + API->>Auth Middleware: Validate token + API-->>Client: Return protected resource +``` + +### Password Recovery + +```mermaid +sequenceDiagram + Client->>API: POST /api/v1/auth/forgot-password + API->>Database: Find user by email + API->>Email Service: Send password reset email + API-->>Client: Return success message + Client->>API: POST /api/v1/auth/reset-password + API->>Auth Service: Validate reset token + API->>Database: Update password + API-->>Client: Return success message +``` + +## Agent Management + +### Creating and Using an Agent + +```mermaid +sequenceDiagram + Client->>API: POST /api/v1/agents/ + API->>Database: Create agent + API-->>Client: Return agent details + Client->>API: POST /api/v1/chat + API->>Agent Service: Process message + Agent Service->>External LLM: Send prompt + External LLM-->>Agent Service: Return response + Agent Service->>Database: Store conversation + API-->>Client: Return agent response +``` + +### Sequential Agent Flow + +```mermaid +sequenceDiagram + Client->>API: POST /api/v1/chat (sequential agent) + API->>Agent Service: Process message + Agent Service->>Sub-Agent 1: Process first step + Sub-Agent 1-->>Agent Service: Return intermediate result + Agent Service->>Sub-Agent 2: Process with previous result + Sub-Agent 2-->>Agent Service: Return intermediate result + Agent Service->>Sub-Agent 3: Process final step + Sub-Agent 3-->>Agent Service: Return final result + Agent Service->>Database: Store conversation + API-->>Client: Return final response +``` + +## Client and Contact Management + +### Client Creation and Management + +```mermaid +sequenceDiagram + Admin->>API: POST /api/v1/clients/ + API->>Database: Create client + API-->>Admin: Return client details + Admin->>API: PUT /api/v1/clients/{client_id} + API->>Database: Update client + API-->>Admin: Return updated client + Client User->>API: GET /api/v1/clients/ + API->>Auth Service: Check permissions + API->>Database: Fetch client(s) + API-->>Client User: Return client details +``` + +### Contact Management + +```mermaid +sequenceDiagram + Client User->>API: POST /api/v1/contacts/ + API->>Auth Service: Check permissions + API->>Database: Create contact + API-->>Client User: Return contact details + Client User->>API: GET /api/v1/contacts/{client_id} + API->>Auth Service: Check permissions + API->>Database: Fetch contacts + API-->>Client User: Return contact list + Client User->>API: POST /api/v1/chat + API->>Database: Validate contact belongs to client + API->>Agent Service: Process message + API-->>Client User: Return agent response +``` + +## MCP Server and Tool Management + +### MCP Server Configuration + +```mermaid +sequenceDiagram + Admin->>API: POST /api/v1/mcp-servers/ + API->>Auth Service: Verify admin permissions + API->>Database: Create MCP server + API-->>Admin: Return server details + Admin->>API: PUT /api/v1/mcp-servers/{server_id} + API->>Auth Service: Verify admin permissions + API->>Database: Update server configuration + API-->>Admin: Return updated server +``` + +### Tool Configuration and Usage + +```mermaid +sequenceDiagram + Admin->>API: POST /api/v1/tools/ + API->>Auth Service: Verify admin permissions + API->>Database: Create tool + API-->>Admin: Return tool details + Client User->>API: POST /api/v1/chat (with tool) + API->>Agent Service: Process message + Agent Service->>Tool Service: Execute tool + Tool Service->>External API: Make external call + External API-->>Tool Service: Return result + Tool Service-->>Agent Service: Return tool result + Agent Service-->>API: Return final response + API-->>Client User: Return agent response +``` + +## Audit and Monitoring + +### Audit Log Flow + +```mermaid +sequenceDiagram + User->>API: Perform administrative action + API->>Auth Service: Verify permissions + API->>Audit Service: Log action + Audit Service->>Database: Store audit record + API->>Database: Perform action + API-->>User: Return action result + Admin->>API: GET /api/v1/admin/audit-logs + API->>Auth Service: Verify admin permissions + API->>Database: Fetch audit logs + API-->>Admin: Return audit history +``` + +## Error Handling + +### Common Error Flows + +```mermaid +sequenceDiagram + Client->>API: Invalid request + API->>Middleware: Process request + Middleware->>Exception Handler: Handle validation error + Exception Handler-->>Client: Return 422 Validation Error + Client->>API: Request protected resource + API->>Auth Middleware: Validate JWT + Auth Middleware->>Exception Handler: Handle authentication error + Exception Handler-->>Client: Return 401 Unauthorized + Client->>API: Request resource without permission + API->>Auth Service: Check resource permissions + Auth Service->>Exception Handler: Handle permission error + Exception Handler-->>Client: Return 403 Forbidden +``` + +## API Integration Best Practices + +1. **Authentication**: + - Store JWT tokens securely + - Implement token refresh mechanism + - Handle token expiration gracefully + +2. **Error Handling**: + - Implement proper error handling for all API calls + - Pay attention to HTTP status codes + - Log detailed error information for debugging + +3. **Resource Management**: + - Use pagination for listing resources + - Filter only the data you need + - Consider implementing client-side caching for frequently accessed data + +4. **Agent Configuration**: + - Start with preset agent templates + - Test agent configurations with sample data + - Monitor and adjust agent parameters based on performance + +5. **Security**: + - Never expose API keys or tokens in client-side code + - Validate all user input before sending to the API + - Implement proper permission checks in your application \ No newline at end of file diff --git a/docs/technical/ARCHITECTURE.md b/docs/technical/ARCHITECTURE.md new file mode 100644 index 00000000..c81c1b74 --- /dev/null +++ b/docs/technical/ARCHITECTURE.md @@ -0,0 +1,222 @@ +# Evo AI - System Architecture + +This document provides an overview of the Evo AI system architecture, explaining how different components interact and the design decisions behind the implementation. + +## High-Level Architecture + +Evo AI follows a layered architecture pattern with clear separation of concerns: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ FastAPI REST API Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ API Routes │ │ Middleware │ │ Exception │ │ +│ │ │ │ │ │ Handlers │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Service Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Agent │ │ User │ │ MCP │ │ +│ │ Services │ │ Services │ │ Services │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Client │ │ Contact │ │ Tool │ │ +│ │ Services │ │ Services │ │ Services │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Data Access Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ SQLAlchemy │ │ Alembic │ │ Redis │ │ +│ │ ORM │ │ Migrations │ │ Cache │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ External Storage Systems │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ +│ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Component Details + +### API Layer + +The API Layer is implemented using FastAPI and handles all HTTP requests and responses. Key components include: + +1. **API Routes** (`src/api/`): + - Defines all endpoints for the REST API + - Handles request validation using Pydantic models + - Manages authentication and authorization + - Delegates business logic to the Service Layer + +2. **Middleware** (`src/core/`): + - JWT Authentication middleware + - Error handling middleware + - Request logging middleware + +3. **Exception Handling**: + - Centralized error handling with appropriate HTTP status codes + - Standardized error responses + +### Service Layer + +The Service Layer contains the core business logic of the application. It includes: + +1. **Agent Service** (`src/services/agent_service.py`): + - Agent creation, configuration, and management + - Integration with LLM providers + +2. **Client Service** (`src/services/client_service.py`): + - Client management functionality + - Client resource access control + +3. **MCP Server Service** (`src/services/mcp_server_service.py`): + - Management of Multi-provider Cognitive Processing (MCP) servers + - Configuration of server environments and tools + +4. **User Service** (`src/services/user_service.py`): + - User management and authentication + - Email verification + +5. **Additional Services**: + - Contact Service + - Tool Service + - Email Service + - Audit Service + +### Data Access Layer + +The Data Access Layer manages all interactions with the database and caching systems: + +1. **SQLAlchemy ORM** (`src/models/`): + - Defines database models and relationships + - Provides methods for CRUD operations + - Implements transactions and error handling + +2. **Alembic Migrations**: + - Manages database schema changes + - Handles version control for database schema + +3. **Redis Cache**: + - Stores session data + - Caches frequently accessed data + - Manages JWT token blacklisting + +### External Systems + +1. **PostgreSQL**: + - Primary relational database + - Stores all persistent data + - Manages relationships between entities + +2. **Redis**: + - Secondary database for caching + - Session management + - Rate limiting support + +3. **Email System** (SendGrid): + - Handles email notifications + - Manages email templates + - Provides delivery tracking + +## Authentication Flow + +``` +┌─────────┐ ┌────────────┐ ┌──────────────┐ ┌─────────────┐ +│ User │ │ API Layer │ │ Auth Service │ │ User Service│ +└────┬────┘ └──────┬─────┘ └──────┬───────┘ └──────┬──────┘ + │ Login Request │ │ │ + │──────────────────>│ │ │ + │ │ Authenticate User │ │ + │ │──────────────────>│ │ + │ │ │ Validate Credentials + │ │ │────────────────────>│ + │ │ │ │ + │ │ │ Result │ + │ │ │<────────────────────│ + │ │ │ │ + │ │ Generate JWT Token│ │ + │ │<──────────────────│ │ + │ JWT Token │ │ │ + │<──────────────────│ │ │ + │ │ │ │ +``` + +## Data Model + +The core entities in the system are: + +1. **Users**: Application users with authentication information +2. **Clients**: Organizations or accounts using the system +3. **Agents**: AI agents with configurations and capabilities +4. **Contacts**: End-users interacting with agents +5. **MCP Servers**: Server configurations for different AI providers +6. **Tools**: Tools that can be used by agents + +The relationships between these entities are described in detail in the `DATA_MODEL.md` document. + +## Security Considerations + +1. **Authentication**: + - JWT-based authentication with short-lived tokens + - Secure password hashing with bcrypt + - Email verification for new accounts + - Account lockout after multiple failed attempts + +2. **Authorization**: + - Role-based access control (admin vs regular users) + - Resource-based access control (client-specific resources) + - JWT payload containing essential user data for quick authorization checks + +3. **Data Protection**: + - Environment variables for sensitive data + - Encrypted connections to databases + - No storage of plaintext passwords or API keys + +## Deployment Architecture + +Evo AI can be deployed using Docker containers for easier scaling and management: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Load Balancer │ +└─────────────────────────────────────────────────────────────┘ + │ │ + ┌───────────┘ └───────────┐ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│ API │ │ API │ +│ Container│ │ Container│ +└──────────┘ └──────────┘ + │ │ + └───────────┐ ┌───────────┘ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ PostgreSQL Cluster │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ Redis Cluster │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Further Reading + +- See `DATA_MODEL.md` for detailed database schema information +- See `API_FLOW.md` for common API interaction patterns +- See `DEPLOYMENT.md` for deployment instructions and configurations \ No newline at end of file diff --git a/docs/technical/DATA_MODEL.md b/docs/technical/DATA_MODEL.md new file mode 100644 index 00000000..51907587 --- /dev/null +++ b/docs/technical/DATA_MODEL.md @@ -0,0 +1,317 @@ +# Evo AI - Data Model + +This document describes the database schema and entity relationships in the Evo AI platform. + +## Database Schema + +The Evo AI platform uses PostgreSQL as its primary database. Below is a detailed description of each table and its relationships. + +## Entity Relationship Diagram + +``` +┌───────────┐ ┌───────────┐ ┌───────────┐ +│ │ │ │ │ │ +│ User │──┐ │ Client │◄─────│ Agent │ +│ │ │ │ │ │ │ +└───────────┘ │ └───────────┘ └───────────┘ + │ ▲ ▲ + │ │ │ + └────────►│ │ + │ │ + ┌────────┴──────┐ │ + │ │ │ + │ Contact │─────────┐│ + │ │ ││ + └───────────────┘ ││ + ││ + ┌───────────────┐ ││ + │ │◄────────┘│ + │ Tool │ │ + │ │ │ + └───────────────┘ │ + │ + ┌───────────────┐ │ + │ │◄─────────┘ + │ MCP Server │ + │ │ + └───────────────┘ +``` + +## Tables + +### User + +The User table stores information about system users. + +```sql +CREATE TABLE user ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + client_id UUID REFERENCES client(id) ON DELETE CASCADE, + is_active BOOLEAN DEFAULT false, + email_verified BOOLEAN DEFAULT false, + is_admin BOOLEAN DEFAULT false, + failed_login_attempts INTEGER DEFAULT 0, + locked_until TIMESTAMP, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **email**: User's email address (unique) +- **password_hash**: Bcrypt-hashed password +- **client_id**: Reference to the client organization (null for admin users) +- **is_active**: Whether the user is active +- **email_verified**: Whether the email has been verified +- **is_admin**: Whether the user has admin privileges +- **failed_login_attempts**: Counter for failed login attempts +- **locked_until**: Timestamp until when the account is locked +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### Client + +The Client table stores information about client organizations. + +```sql +CREATE TABLE client ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **name**: Client name +- **email**: Client email contact +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### Agent + +The Agent table stores information about AI agents. + +```sql +CREATE TABLE agent ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + client_id UUID NOT NULL REFERENCES client(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + type VARCHAR(50) NOT NULL, + model VARCHAR(255), + api_key TEXT, + instruction TEXT, + config_json JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **client_id**: Reference to the client that owns this agent +- **name**: Agent name +- **description**: Agent description +- **type**: Agent type (e.g., "llm", "sequential", "parallel", "loop") +- **model**: LLM model name (for "llm" type agents) +- **api_key**: API key for the model provider (encrypted) +- **instruction**: System instructions for the agent +- **config_json**: JSON configuration specific to the agent type +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### Contact + +The Contact table stores information about end-users that interact with agents. + +```sql +CREATE TABLE contact ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + client_id UUID NOT NULL REFERENCES client(id) ON DELETE CASCADE, + ext_id VARCHAR(255), + name VARCHAR(255) NOT NULL, + meta JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **client_id**: Reference to the client that owns this contact +- **ext_id**: Optional external ID for integration +- **name**: Contact name +- **meta**: Additional metadata in JSON format +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### MCP Server + +The MCP Server table stores information about Multi-provider Cognitive Processing servers. + +```sql +CREATE TABLE mcp_server ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + config_json JSONB NOT NULL DEFAULT '{}', + environments JSONB NOT NULL DEFAULT '{}', + tools JSONB NOT NULL DEFAULT '[]', + type VARCHAR(50) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **name**: Server name +- **description**: Server description +- **config_json**: JSON configuration for the server +- **environments**: Environment variables as JSON +- **tools**: List of tools supported by this server +- **type**: Server type (e.g., "official", "custom") +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### Tool + +The Tool table stores information about tools that can be used by agents. + +```sql +CREATE TABLE tool ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + config_json JSONB NOT NULL DEFAULT '{}', + environments JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **name**: Tool name +- **description**: Tool description +- **config_json**: JSON configuration for the tool +- **environments**: Environment variables as JSON +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### Conversation + +The Conversation table stores chat history between contacts and agents. + +```sql +CREATE TABLE conversation ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + agent_id UUID NOT NULL REFERENCES agent(id) ON DELETE CASCADE, + contact_id UUID NOT NULL REFERENCES contact(id) ON DELETE CASCADE, + message TEXT NOT NULL, + response TEXT NOT NULL, + meta JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **agent_id**: Reference to the agent +- **contact_id**: Reference to the contact +- **message**: Message sent by the contact +- **response**: Response generated by the agent +- **meta**: Additional metadata (e.g., tokens used, tools called) +- **created_at**: Creation timestamp + +### Audit Log + +The Audit Log table stores records of administrative actions. + +```sql +CREATE TABLE audit_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES "user"(id) ON DELETE SET NULL, + action VARCHAR(50) NOT NULL, + resource_type VARCHAR(50) NOT NULL, + resource_id UUID, + details JSONB NOT NULL DEFAULT '{}', + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +- **id**: Unique identifier (UUID) +- **user_id**: Reference to the user who performed the action +- **action**: Type of action (e.g., "CREATE", "UPDATE", "DELETE") +- **resource_type**: Type of resource affected (e.g., "AGENT", "CLIENT") +- **resource_id**: Identifier of the affected resource +- **details**: JSON with before/after state +- **ip_address**: IP address of the user +- **user_agent**: User-agent string +- **created_at**: Creation timestamp + +## Indexes + +To optimize performance, the following indexes are created: + +```sql +-- User indexes +CREATE INDEX idx_user_email ON "user" (email); +CREATE INDEX idx_user_client_id ON "user" (client_id); + +-- Agent indexes +CREATE INDEX idx_agent_client_id ON agent (client_id); +CREATE INDEX idx_agent_name ON agent (name); + +-- Contact indexes +CREATE INDEX idx_contact_client_id ON contact (client_id); +CREATE INDEX idx_contact_ext_id ON contact (ext_id); + +-- Conversation indexes +CREATE INDEX idx_conversation_agent_id ON conversation (agent_id); +CREATE INDEX idx_conversation_contact_id ON conversation (contact_id); +CREATE INDEX idx_conversation_created_at ON conversation (created_at); + +-- Audit log indexes +CREATE INDEX idx_audit_log_user_id ON audit_log (user_id); +CREATE INDEX idx_audit_log_resource_type ON audit_log (resource_type); +CREATE INDEX idx_audit_log_resource_id ON audit_log (resource_id); +CREATE INDEX idx_audit_log_created_at ON audit_log (created_at); +``` + +## Relationships + +1. **User to Client**: Many-to-one relationship. Each user belongs to at most one client (except for admin users). + +2. **Client to Agent**: One-to-many relationship. Each client can have multiple agents. + +3. **Client to Contact**: One-to-many relationship. Each client can have multiple contacts. + +4. **Agent to Conversation**: One-to-many relationship. Each agent can have multiple conversations. + +5. **Contact to Conversation**: One-to-many relationship. Each contact can have multiple conversations. + +6. **User to Audit Log**: One-to-many relationship. Each user can have multiple audit logs. + +## Data Security + +1. **Passwords**: All passwords are hashed using bcrypt before storage. + +2. **API Keys**: API keys are stored with encryption. + +3. **Sensitive Data**: Sensitive data in JSON fields is encrypted where appropriate. + +4. **Cascading Deletes**: When a parent record is deleted, related records are automatically deleted to maintain referential integrity. + +## Notes on JSONB Fields + +PostgreSQL's JSONB fields provide flexibility for storing semi-structured data: + +1. **config_json**: Used to store configuration parameters that may vary by agent type or tool. + +2. **meta**: Used to store additional attributes that don't warrant their own columns. + +3. **environments**: Used to store environment variables needed for tools and MCP servers. + +This approach allows for extensibility without requiring database schema changes. \ No newline at end of file diff --git a/docs/technical/DEPLOYMENT.md b/docs/technical/DEPLOYMENT.md new file mode 100644 index 00000000..d9b84c6b --- /dev/null +++ b/docs/technical/DEPLOYMENT.md @@ -0,0 +1,509 @@ +# Evo AI - Deployment Guide + +This document provides detailed instructions for deploying the Evo AI platform in different environments. + +## Prerequisites + +- Docker and Docker Compose +- PostgreSQL database +- Redis instance +- SendGrid account for email services +- Domain name (for production deployments) +- SSL certificate (for production deployments) + +## Environment Configuration + +The Evo AI platform uses environment variables for configuration. Create a `.env` file based on the example below: + +``` +# Database Configuration +POSTGRES_CONNECTION_STRING=postgresql://username:password@postgres:5432/evo_ai +POSTGRES_USER=username +POSTGRES_PASSWORD=password +POSTGRES_DB=evo_ai + +# Redis Configuration +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= + +# JWT Configuration +JWT_SECRET_KEY=your-secret-key-at-least-32-characters +JWT_ALGORITHM=HS256 +JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# Email Configuration +SENDGRID_API_KEY=your-sendgrid-api-key +EMAIL_FROM=noreply@your-domain.com +EMAIL_FROM_NAME=Evo AI Platform + +# Application Configuration +APP_URL=https://your-domain.com +ENVIRONMENT=production # development, testing, or production +DEBUG=false +``` + +## Development Deployment + +### Using Docker Compose + +1. Clone the repository: + ```bash + git clone https://github.com/your-username/evo-ai.git + cd evo-ai + ``` + +2. Create a `.env` file: + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` + +3. Start the development environment: + ```bash + make docker-up + ``` + +4. Apply database migrations: + ```bash + make docker-migrate + ``` + +5. Access the API at `http://localhost:8000` + +### Local Development (without Docker) + +1. Clone the repository: + ```bash + git clone https://github.com/your-username/evo-ai.git + cd evo-ai + ``` + +2. Create a virtual environment: + ```bash + python -m venv .venv + source .venv/bin/activate # Linux/Mac + # or + .venv\Scripts\activate # Windows + ``` + +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +4. Create a `.env` file: + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` + +5. Apply database migrations: + ```bash + make alembic-upgrade + ``` + +6. Start the development server: + ```bash + make run + ``` + +7. Access the API at `http://localhost:8000` + +## Production Deployment + +### Docker Swarm + +1. Initialize Docker Swarm (if not already done): + ```bash + docker swarm init + ``` + +2. Create a `.env` file for production: + ```bash + cp .env.example .env.prod + # Edit .env.prod with your production configuration + ``` + +3. Deploy the stack: + ```bash + docker stack deploy -c docker-compose.prod.yml evo-ai + ``` + +4. Verify the deployment: + ```bash + docker stack ps evo-ai + ``` + +### Kubernetes + +1. Create Kubernetes configuration files: + + **postgres-deployment.yaml**: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: postgres + spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:13 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: evo-ai-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: evo-ai-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: evo-ai-secrets + key: POSTGRES_DB + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: postgres-pvc + ``` + + **redis-deployment.yaml**: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: redis + spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:6 + ports: + - containerPort: 6379 + volumeMounts: + - name: redis-data + mountPath: /data + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: redis-pvc + ``` + + **api-deployment.yaml**: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: evo-ai-api + spec: + replicas: 3 + selector: + matchLabels: + app: evo-ai-api + template: + metadata: + labels: + app: evo-ai-api + spec: + containers: + - name: evo-ai-api + image: your-registry/evo-ai-api:latest + ports: + - containerPort: 8000 + envFrom: + - secretRef: + name: evo-ai-secrets + - configMapRef: + name: evo-ai-config + readinessProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 15 + periodSeconds: 20 + ``` + +2. Create Kubernetes secrets: + ```bash + kubectl create secret generic evo-ai-secrets \ + --from-literal=POSTGRES_USER=username \ + --from-literal=POSTGRES_PASSWORD=password \ + --from-literal=POSTGRES_DB=evo_ai \ + --from-literal=JWT_SECRET_KEY=your-secret-key \ + --from-literal=SENDGRID_API_KEY=your-sendgrid-api-key + ``` + +3. Create ConfigMap: + ```bash + kubectl create configmap evo-ai-config \ + --from-literal=POSTGRES_CONNECTION_STRING=postgresql://username:password@postgres:5432/evo_ai \ + --from-literal=REDIS_HOST=redis \ + --from-literal=REDIS_PORT=6379 \ + --from-literal=JWT_ALGORITHM=HS256 \ + --from-literal=JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 \ + --from-literal=EMAIL_FROM=noreply@your-domain.com \ + --from-literal=EMAIL_FROM_NAME="Evo AI Platform" \ + --from-literal=APP_URL=https://your-domain.com \ + --from-literal=ENVIRONMENT=production \ + --from-literal=DEBUG=false + ``` + +4. Apply the configurations: + ```bash + kubectl apply -f postgres-deployment.yaml + kubectl apply -f redis-deployment.yaml + kubectl apply -f api-deployment.yaml + ``` + +5. Create services: + ```bash + kubectl expose deployment postgres --port=5432 --type=ClusterIP + kubectl expose deployment redis --port=6379 --type=ClusterIP + kubectl expose deployment evo-ai-api --port=80 --target-port=8000 --type=LoadBalancer + ``` + +## Scaling Considerations + +### Database Scaling + +For production environments with high load, consider: + +1. **PostgreSQL Replication**: + - Set up a master-slave replication + - Use read replicas for read-heavy operations + - Consider using a managed PostgreSQL service (AWS RDS, Azure Database, etc.) + +2. **Redis Cluster**: + - Implement Redis Sentinel for high availability + - Use Redis Cluster for horizontal scaling + - Consider using a managed Redis service (AWS ElastiCache, Azure Cache, etc.) + +### API Scaling + +1. **Horizontal Scaling**: + - Increase the number of API containers/pods + - Use a load balancer to distribute traffic + +2. **Vertical Scaling**: + - Increase resources (CPU, memory) for API containers + +3. **Caching Strategy**: + - Implement response caching for frequent requests + - Use Redis for distributed caching + +## Monitoring and Logging + +### Monitoring + +1. **Prometheus and Grafana**: + - Set up Prometheus for metrics collection + - Configure Grafana dashboards for visualization + - Monitor API response times, error rates, and system resources + +2. **Health Checks**: + - Use the `/api/v1/health` endpoint to check system health + - Set up alerts for when services are down + +### Logging + +1. **Centralized Logging**: + - Configure ELK Stack (Elasticsearch, Logstash, Kibana) + - Or use a managed logging service (AWS CloudWatch, Datadog, etc.) + +2. **Log Levels**: + - In production, set log level to INFO or WARNING + - In development, set log level to DEBUG for more details + +## Backup and Recovery + +1. **Database Backups**: + - Schedule regular PostgreSQL backups + - Store backups in a secure location (e.g., AWS S3, Azure Blob Storage) + - Test restoration procedures regularly + +2. **Application State**: + - Store configuration in version control + - Document environment setup and dependencies + +## SSL Configuration + +For production deployments, SSL is required: + +1. **Using Nginx**: + ```nginx + server { + listen 80; + server_name your-domain.com; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name your-domain.com; + + ssl_certificate /path/to/certificate.crt; + ssl_certificate_key /path/to/private.key; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://evo-ai-api:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + ``` + +2. **Using Let's Encrypt and Certbot**: + ```bash + certbot --nginx -d your-domain.com + ``` + +## Troubleshooting + +### Common Issues + +1. **Database Connection Errors**: + - Check PostgreSQL connection string + - Verify network connectivity between API and database + - Check database credentials + +2. **Redis Connection Issues**: + - Verify Redis host and port + - Check network connectivity to Redis + - Ensure Redis service is running + +3. **Email Sending Failures**: + - Verify SendGrid API key + - Check email templates + - Test email sending with SendGrid debugging tools + +### Debugging + +1. **Container Logs**: + ```bash + # Docker + docker logs + + # Kubernetes + kubectl logs + ``` + +2. **API Logs**: + - Check `/logs` directory + - Set DEBUG=true in development to get more detailed logs + +3. **Database Connection Testing**: + ```bash + psql postgresql://username:password@postgres:5432/evo_ai + ``` + +4. **Health Check**: + ```bash + curl http://localhost:8000/api/v1/health + ``` + +## Security Considerations + +1. **API Security**: + - Keep JWT_SECRET_KEY secure and random + - Rotate JWT secrets periodically + - Set appropriate token expiration times + +2. **Network Security**: + - Use internal networks for database and Redis + - Expose only the API through a load balancer + - Implement a Web Application Firewall (WAF) + +3. **Data Protection**: + - Encrypt sensitive data in database + - Implement proper access controls + - Regularly audit system access + +## Continuous Integration/Deployment + +### GitHub Actions Example + +```yaml +name: Deploy Evo AI + +on: + push: + branches: [ main ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + + - name: Run tests + run: | + pytest + + - name: Build Docker image + run: | + docker build -t your-registry/evo-ai-api:latest . + + - name: Push to registry + run: | + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + docker push your-registry/evo-ai-api:latest + + - name: Deploy to production + run: | + # Deployment commands depending on your environment + # For example, if using Kubernetes: + kubectl set image deployment/evo-ai-api evo-ai-api=your-registry/evo-ai-api:latest +``` + +## Conclusion + +This deployment guide covers the basics of deploying the Evo AI platform in different environments. For specific needs or custom deployments, additional configuration may be required. Always follow security best practices and ensure proper monitoring and backup procedures are in place. \ No newline at end of file diff --git a/evo-ai-api.postman_collection.json b/evo-ai-api.postman_collection.json deleted file mode 100644 index 7da1d207..00000000 --- a/evo-ai-api.postman_collection.json +++ /dev/null @@ -1,750 +0,0 @@ -{ - "info": { - "_postman_id": "a2a-saas-api", - "name": "Evo AI API", - "description": "API para execução de agentes de IA", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Chat", - "item": [ - { - "name": "Enviar Mensagem", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"agent_id\": \"uuid-do-agente\",\n \"contact_id\": \"uuid-do-contato\",\n \"message\": \"Olá, como posso ajudar?\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/chat", - "host": ["{{base_url}}"], - "path": ["api", "v1", "chat"] - } - } - } - ] - }, - { - "name": "Clientes", - "item": [ - { - "name": "Criar Cliente", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Nome do Cliente\",\n \"email\": \"cliente@exemplo.com\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/clients/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "clients", ""] - } - } - }, - { - "name": "Listar Clientes", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/clients/?skip=0&limit=100", - "host": ["{{base_url}}"], - "path": ["api", "v1", "clients", ""], - "query": [ - { - "key": "skip", - "value": "0" - }, - { - "key": "limit", - "value": "100" - } - ] - } - } - }, - { - "name": "Buscar Cliente", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/clients/{{client_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "clients", "{{client_id}}"] - } - } - }, - { - "name": "Atualizar Cliente", - "request": { - "method": "PUT", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Novo Nome do Cliente\",\n \"email\": \"novo-email@exemplo.com\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/clients/{{client_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "clients", "{{client_id}}"] - } - } - }, - { - "name": "Remover Cliente", - "request": { - "method": "DELETE", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/clients/{{client_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "clients", "{{client_id}}"] - } - } - } - ] - }, - { - "name": "Contatos", - "item": [ - { - "name": "Criar Contato", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"Nome do Contato\",\n \"email\": \"contato@exemplo.com\",\n \"phone\": \"(11) 99999-9999\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/contacts/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "contacts", ""] - } - } - }, - { - "name": "Listar Contatos", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/contacts/{{client_id}}?skip=0&limit=100", - "host": ["{{base_url}}"], - "path": ["api", "v1", "contacts", "{{client_id}}"], - "query": [ - { - "key": "skip", - "value": "0" - }, - { - "key": "limit", - "value": "100" - } - ] - } - } - }, - { - "name": "Buscar Contato", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/contact/{{contact_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "contact", "{{contact_id}}"] - } - } - }, - { - "name": "Atualizar Contato", - "request": { - "method": "PUT", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"Novo Nome do Contato\",\n \"email\": \"novo-email@exemplo.com\",\n \"phone\": \"(11) 99999-9999\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/contact/{{contact_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "contact", "{{contact_id}}"] - } - } - }, - { - "name": "Remover Contato", - "request": { - "method": "DELETE", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/contact/{{contact_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "contact", "{{contact_id}}"] - } - } - } - ] - }, - { - "name": "Agentes", - "item": [ - { - "name": "Criar Agente LLM", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"meu_agente_llm\",\n \"type\": \"llm\",\n \"model\": \"gpt-4\",\n \"api_key\": \"chave-api-do-modelo\",\n \"instruction\": \"Instruções para o agente\",\n \"config\": {\n \"tools\": [\n {\n \"id\": \"uuid-da-ferramenta\",\n \"envs\": {\n \"API_KEY\": \"chave-api-da-ferramenta\",\n \"ENDPOINT\": \"http://localhost:8000\"\n }\n }\n ],\n \"mcp_servers\": [\n {\n \"id\": \"uuid-do-servidor\",\n \"envs\": {\n \"API_KEY\": \"chave-api-do-servidor\",\n \"ENDPOINT\": \"http://localhost:8001\"\n }\n }\n ],\n \"custom_tools\": {\n \"http_tools\": [\n {\n \"name\": \"list_all_knowledge_base\",\n \"method\": \"GET\",\n \"values\": {\n \"tenant_id\": \"45cffb85-51c8-41ed-aa8d-710970a7ce50\"\n },\n \"headers\": {\n \"x-api-key\": \"79405047-7a5e-4b18-b25a-4af149d747dc\"\n },\n \"endpoint\": \"http://localhost:5540/api/v1/knowledge\",\n \"parameters\": {\n \"query_params\": {\n \"include\": [\"tenant_id\"]\n }\n },\n \"description\": \"List all knowledge base.\",\n \"error_handling\": {\n \"timeout\": 5000,\n \"retry_count\": 3,\n \"fallback_response\": {\n \"error\": \"list_knowledge_error\",\n \"message\": \"Erro ao listar knowledges\"\n }\n }\n }\n ]\n },\n \"sub_agents\": [\"uuid-do-sub-agente\"]\n }\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/agents/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agents", ""] - } - } - }, - { - "name": "Criar Agente Sequential", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"meu_agente_sequential\",\n \"type\": \"sequential\",\n \"model\": \"gpt-4\",\n \"api_key\": \"chave-api-do-modelo\",\n \"instruction\": \"Instruções para o agente\",\n \"config\": {\n \"sub_agents\": [\"uuid-do-sub-agente-1\", \"uuid-do-sub-agente-2\"]\n }\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/agents/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agents", ""] - } - } - }, - { - "name": "Criar Agente Parallel", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"meu_agente_parallel\",\n \"type\": \"parallel\",\n \"model\": \"gpt-4\",\n \"api_key\": \"chave-api-do-modelo\",\n \"instruction\": \"Instruções para o agente\",\n \"config\": {\n \"sub_agents\": [\"uuid-do-sub-agente-1\", \"uuid-do-sub-agente-2\"]\n }\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/agents/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agents", ""] - } - } - }, - { - "name": "Criar Agente Loop", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"meu_agente_loop\",\n \"type\": \"loop\",\n \"model\": \"gpt-4\",\n \"api_key\": \"chave-api-do-modelo\",\n \"instruction\": \"Instruções para o agente\",\n \"config\": {\n \"sub_agents\": [\"uuid-do-sub-agente\"],\n \"max_iterations\": 5,\n \"condition\": \"condição_para_parar\"\n }\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/agents/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agents", ""] - } - } - }, - { - "name": "Listar Agentes", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/agents/{{client_id}}?skip=0&limit=100", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agents", "{{client_id}}"], - "query": [ - { - "key": "skip", - "value": "0" - }, - { - "key": "limit", - "value": "100" - } - ] - } - } - }, - { - "name": "Buscar Agente", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/agent/{{agent_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agent", "{{agent_id}}"] - } - } - }, - { - "name": "Atualizar Agente", - "request": { - "method": "PUT", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": \"uuid-do-cliente\",\n \"name\": \"Novo Nome do Agente\",\n \"description\": \"Nova descrição do agente\",\n \"model\": \"gpt-4\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/agent/{{agent_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agent", "{{agent_id}}"] - } - } - }, - { - "name": "Remover Agente", - "request": { - "method": "DELETE", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/agent/{{agent_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "agent", "{{agent_id}}"] - } - } - } - ] - }, - { - "name": "Servidores MCP", - "item": [ - { - "name": "Criar Servidor MCP", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Nome do Servidor\",\n \"url\": \"http://localhost:8000\",\n \"api_key\": \"chave-api-do-servidor\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/mcp-servers/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "mcp-servers", ""] - } - } - }, - { - "name": "Listar Servidores MCP", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/mcp-servers/?skip=0&limit=100", - "host": ["{{base_url}}"], - "path": ["api", "v1", "mcp-servers", ""], - "query": [ - { - "key": "skip", - "value": "0" - }, - { - "key": "limit", - "value": "100" - } - ] - } - } - }, - { - "name": "Buscar Servidor MCP", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/mcp-servers/{{server_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "mcp-servers", "{{server_id}}"] - } - } - }, - { - "name": "Atualizar Servidor MCP", - "request": { - "method": "PUT", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Novo Nome do Servidor\",\n \"url\": \"http://novo-servidor:8000\",\n \"api_key\": \"nova-chave-api\"\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/mcp-servers/{{server_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "mcp-servers", "{{server_id}}"] - } - } - }, - { - "name": "Remover Servidor MCP", - "request": { - "method": "DELETE", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/mcp-servers/{{server_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "mcp-servers", "{{server_id}}"] - } - } - } - ] - }, - { - "name": "Ferramentas", - "item": [ - { - "name": "Criar Ferramenta", - "request": { - "method": "POST", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Nome da Ferramenta\",\n \"description\": \"Descrição da ferramenta\",\n \"type\": \"tipo-da-ferramenta\",\n \"config\": {\n \"key\": \"value\"\n }\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/tools/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "tools", ""] - } - } - }, - { - "name": "Listar Ferramentas", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/tools/?skip=0&limit=100", - "host": ["{{base_url}}"], - "path": ["api", "v1", "tools", ""], - "query": [ - { - "key": "skip", - "value": "0" - }, - { - "key": "limit", - "value": "100" - } - ] - } - } - }, - { - "name": "Buscar Ferramenta", - "request": { - "method": "GET", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/tools/{{tool_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "tools", "{{tool_id}}"] - } - } - }, - { - "name": "Atualizar Ferramenta", - "request": { - "method": "PUT", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Novo Nome da Ferramenta\",\n \"description\": \"Nova descrição da ferramenta\",\n \"type\": \"novo-tipo\",\n \"config\": {\n \"new_key\": \"new_value\"\n }\n}" - }, - "url": { - "raw": "{{base_url}}/api/v1/tools/{{tool_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "tools", "{{tool_id}}"] - } - } - }, - { - "name": "Remover Ferramenta", - "request": { - "method": "DELETE", - "header": [ - { - "key": "X-API-Key", - "value": "{{api_key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/api/v1/tools/{{tool_id}}", - "host": ["{{base_url}}"], - "path": ["api", "v1", "tools", "{{tool_id}}"] - } - } - } - ] - } - ], - "variable": [ - { - "key": "base_url", - "value": "http://localhost:8000", - "type": "string" - }, - { - "key": "api_key", - "value": "sua-api-key-aqui", - "type": "string" - } - ] -} \ No newline at end of file diff --git a/scripts/run_seeders.py b/scripts/run_seeders.py index 7ce2b0b9..f322aa31 100644 --- a/scripts/run_seeders.py +++ b/scripts/run_seeders.py @@ -1,6 +1,6 @@ """ -Script principal para executar todos os seeders em sequência. -Verifica as dependências entre os seeders e executa na ordem correta. +Main script to run all seeders in sequence. +Checks dependencies between seeders and runs them in the correct order. """ import os @@ -9,11 +9,11 @@ import logging import argparse from dotenv import load_dotenv -# Configurar logging +# Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) -# Importar seeders +# Import seeders sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from scripts.seeders.admin_seeder import create_admin_user from scripts.seeders.client_seeder import create_demo_client_and_user @@ -23,28 +23,28 @@ from scripts.seeders.tool_seeder import create_tools from scripts.seeders.contact_seeder import create_demo_contacts def setup_environment(): - """Configura o ambiente para os seeders""" + """Configure the environment for seeders""" load_dotenv() - # Verificar se as variáveis de ambiente essenciais estão definidas + # Check if essential environment variables are defined required_vars = ["POSTGRES_CONNECTION_STRING"] missing_vars = [var for var in required_vars if not os.getenv(var)] if missing_vars: - logger.error(f"Variáveis de ambiente necessárias não definidas: {', '.join(missing_vars)}") + logger.error(f"Required environment variables not defined: {', '.join(missing_vars)}") return False return True def run_seeders(seeders): """ - Executa os seeders especificados + Run the specified seeders Args: - seeders (list): Lista de seeders para executar + seeders (list): List of seeders to run Returns: - bool: True se todos os seeders foram executados com sucesso, False caso contrário + bool: True if all seeders were executed successfully, False otherwise """ all_seeders = { "admin": create_admin_user, @@ -55,58 +55,58 @@ def run_seeders(seeders): "contacts": create_demo_contacts } - # Define a ordem correta de execução (dependências) + # Define the correct execution order (dependencies) seeder_order = ["admin", "client", "mcp_servers", "tools", "agents", "contacts"] - # Se nenhum seeder for especificado, executar todos + # If no seeder is specified, run all if not seeders: seeders = seeder_order else: - # Verificar se todos os seeders especificados existem + # Check if all specified seeders exist invalid_seeders = [s for s in seeders if s not in all_seeders] if invalid_seeders: - logger.error(f"Seeders inválidos: {', '.join(invalid_seeders)}") - logger.info(f"Seeders disponíveis: {', '.join(all_seeders.keys())}") + logger.error(f"Invalid seeders: {', '.join(invalid_seeders)}") + logger.info(f"Available seeders: {', '.join(all_seeders.keys())}") return False - # Garantir que seeders sejam executados na ordem correta + # Ensure seeders are executed in the correct order seeders = [s for s in seeder_order if s in seeders] - # Executar seeders + # Run seeders success = True for seeder_name in seeders: - logger.info(f"Executando seeder: {seeder_name}") + logger.info(f"Running seeder: {seeder_name}") try: seeder_func = all_seeders[seeder_name] if not seeder_func(): - logger.error(f"Falha ao executar seeder: {seeder_name}") + logger.error(f"Failed to run seeder: {seeder_name}") success = False except Exception as e: - logger.error(f"Erro ao executar seeder {seeder_name}: {str(e)}") + logger.error(f"Error running seeder {seeder_name}: {str(e)}") success = False return success def main(): - """Função principal""" - parser = argparse.ArgumentParser(description='Executa seeders para popular o banco de dados') - parser.add_argument('--seeders', nargs='+', help='Seeders para executar (admin, client, agents, mcp_servers, tools, contacts)') + """Main function""" + parser = argparse.ArgumentParser(description='Run seeders to populate the database') + parser.add_argument('--seeders', nargs='+', help='Seeders to run (admin, client, agents, mcp_servers, tools, contacts)') args = parser.parse_args() - # Configurar ambiente + # Configure environment if not setup_environment(): sys.exit(1) - # Executar seeders + # Run seeders success = run_seeders(args.seeders) - # Saída + # Output if success: - logger.info("Todos os seeders foram executados com sucesso") + logger.info("All seeders were executed successfully") sys.exit(0) else: - logger.error("Houve erros ao executar os seeders") + logger.error("There were errors running the seeders") sys.exit(1) if __name__ == "__main__": diff --git a/scripts/seeders/admin_seeder.py b/scripts/seeders/admin_seeder.py index 85ff023c..076458b2 100644 --- a/scripts/seeders/admin_seeder.py +++ b/scripts/seeders/admin_seeder.py @@ -1,7 +1,7 @@ """ -Script para criar um usuário administrador inicial: +Script to create an initial admin user: - Email: admin@evoai.com -- Senha: definida nas variáveis de ambiente ADMIN_INITIAL_PASSWORD +- Password: defined in the ADMIN_INITIAL_PASSWORD environment variable - is_admin: True - is_active: True - email_verified: True @@ -21,27 +21,27 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( logger = logging.getLogger(__name__) def create_admin_user(): - """Cria um usuário administrador inicial no sistema""" + """Create an initial admin user in the system""" try: - # Carregar variáveis de ambiente + # Load environment variables load_dotenv() - # Obter configurações do banco de dados + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: - logger.error("Variável de ambiente POSTGRES_CONNECTION_STRING não definida") + logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - # Obter senha do administrador + # Get admin password admin_password = os.getenv("ADMIN_INITIAL_PASSWORD") if not admin_password: - logger.error("Variável de ambiente ADMIN_INITIAL_PASSWORD não definida") + logger.error("Environment variable ADMIN_INITIAL_PASSWORD not defined") return False - # Configuração do email do admin + # Admin email configuration admin_email = os.getenv("ADMIN_EMAIL", "admin@evoai.com") - # Conectar ao banco de dados + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() @@ -49,10 +49,10 @@ def create_admin_user(): # Verificar se o administrador já existe existing_admin = session.query(User).filter(User.email == admin_email).first() if existing_admin: - logger.info(f"Administrador com email {admin_email} já existe") + logger.info(f"Admin with email {admin_email} already exists") return True - # Criar administrador + # Create admin admin_user = User( email=admin_email, password_hash=get_password_hash(admin_password), @@ -61,15 +61,15 @@ def create_admin_user(): email_verified=True ) - # Adicionar e comitar + # Add and commit session.add(admin_user) session.commit() - logger.info(f"Administrador criado com sucesso: {admin_email}") + logger.info(f"Admin created successfully: {admin_email}") return True except Exception as e: - logger.error(f"Erro ao criar administrador: {str(e)}") + logger.error(f"Error creating admin: {str(e)}") return False finally: session.close() diff --git a/scripts/seeders/agent_seeder.py b/scripts/seeders/agent_seeder.py index a4eb3591..8751726a 100644 --- a/scripts/seeders/agent_seeder.py +++ b/scripts/seeders/agent_seeder.py @@ -1,9 +1,9 @@ """ -Script para criar agentes de exemplo para o cliente demo: -- Agente Atendimento: configurado para responder perguntas gerais -- Agente Vendas: configurado para responder sobre produtos -- Agente FAQ: configurado para responder perguntas frequentes -Cada agente com instruções e configurações pré-definidas +Script to create example agents for the demo client: +- Agent Support: configured to answer general questions +- Agent Sales: configured to answer about products +- Agent FAQ: configured to answer frequently asked questions +Each agent with pre-defined instructions and configurations """ import os @@ -21,18 +21,18 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( logger = logging.getLogger(__name__) def create_demo_agents(): - """Cria agentes de exemplo para o cliente demo""" + """Create example agents for the demo client""" try: - # Carregar variáveis de ambiente + # Load environment variables load_dotenv() - # Obter configurações do banco de dados + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: - logger.error("Variável de ambiente POSTGRES_CONNECTION_STRING não definida") + logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - # Conectar ao banco de dados + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() @@ -43,7 +43,7 @@ def create_demo_agents(): demo_user = session.query(User).filter(User.email == demo_email).first() if not demo_user or not demo_user.client_id: - logger.error(f"Usuário demo não encontrado ou não associado a um cliente: {demo_email}") + logger.error(f"Demo user not found or not associated with a client: {demo_email}") return False client_id = demo_user.client_id @@ -51,79 +51,75 @@ def create_demo_agents(): # Verificar se já existem agentes para este cliente existing_agents = session.query(Agent).filter(Agent.client_id == client_id).all() if existing_agents: - logger.info(f"Já existem {len(existing_agents)} agentes para o cliente {client_id}") + logger.info(f"There are already {len(existing_agents)} agents for the client {client_id}") return True - # Definições dos agentes de exemplo + # Example agent definitions agents = [ { - "name": "Atendimento_Geral", - "description": "Agente para atendimento geral e dúvidas básicas", + "name": "Support_Agent", + "description": "Agent for general support and basic questions", "type": "llm", - "model": "gpt-3.5-turbo", - "api_key": "${OPENAI_API_KEY}", # Será substituído pela variável de ambiente + "model": "gpt-4.1-nano", + "api_key": "your-api-key-here", "instruction": """ - Você é um assistente de atendimento ao cliente da empresa. - Seja cordial, objetivo e eficiente. Responda às dúvidas dos clientes - de forma clara e sucinta. Se não souber a resposta, informe que irá - consultar um especialista e retornará em breve. + You are a customer support agent. + Be friendly, objective and efficient. Answer customer questions + in a clear and concise manner. If you don't know the answer, + inform that you will consult a specialist and return soon. """, "config": { - "temperature": 0.7, - "max_tokens": 500, - "tools": [] + "tools": [], + "mcp_servers": [], + "custom_tools": [], + "sub_agents": [] } }, { - "name": "Vendas_Produtos", - "description": "Agente especializado em vendas e informações sobre produtos", + "name": "Sales_Products", + "description": "Specialized agent in sales and information about products", "type": "llm", - "model": "claude-3-sonnet-20240229", - "api_key": "${ANTHROPIC_API_KEY}", # Será substituído pela variável de ambiente + "model": "gpt-4.1-nano", + "api_key": "your-api-key-here", "instruction": """ - Você é um especialista em vendas da empresa. - Seu objetivo é fornecer informações detalhadas sobre produtos, - comparar diferentes opções, destacar benefícios e vantagens competitivas. - Use uma linguagem persuasiva mas honesta, e sempre busque entender - as necessidades do cliente antes de recomendar um produto. + You are a sales specialist. + Your goal is to provide detailed information about products, + compare different options, highlight benefits and competitive advantages. + Use a persuasive but honest language, and always seek to understand + the customer's needs before recommending a product. """, "config": { - "temperature": 0.8, - "max_tokens": 800, - "tools": ["web_search"] + "tools": [], + "mcp_servers": [], + "custom_tools": [], + "sub_agents": [] } }, { "name": "FAQ_Bot", - "description": "Agente para responder perguntas frequentes", + "description": "Agent for answering frequently asked questions", "type": "llm", - "model": "gemini-pro", - "api_key": "${GOOGLE_API_KEY}", # Será substituído pela variável de ambiente + "model": "gpt-4.1-nano", + "api_key": "your-api-key-here", "instruction": """ - Você é um assistente especializado em responder perguntas frequentes. - Suas respostas devem ser diretas, objetivas e baseadas nas informações - da empresa. Utilize uma linguagem simples e acessível. Se a pergunta - não estiver relacionada às FAQs disponíveis, direcione o cliente para - o canal de atendimento adequado. + You are a specialized agent for answering frequently asked questions. + Your answers should be direct, objective and based on the information + of the company. Use a simple and accessible language. If the question + is not related to the available FAQs, redirect the client to the + appropriate support channel. """, "config": { - "temperature": 0.5, - "max_tokens": 400, - "tools": [] + "tools": [], + "mcp_servers": [], + "custom_tools": [], + "sub_agents": [] } } ] - # Criar os agentes + # Create the agents for agent_data in agents: - # Substituir placeholders de API Keys por variáveis de ambiente quando disponíveis - if "${OPENAI_API_KEY}" in agent_data["api_key"]: - agent_data["api_key"] = os.getenv("OPENAI_API_KEY", "") - elif "${ANTHROPIC_API_KEY}" in agent_data["api_key"]: - agent_data["api_key"] = os.getenv("ANTHROPIC_API_KEY", "") - elif "${GOOGLE_API_KEY}" in agent_data["api_key"]: - agent_data["api_key"] = os.getenv("GOOGLE_API_KEY", "") - + # Create the agent agent = Agent( client_id=client_id, name=agent_data["name"], @@ -136,19 +132,19 @@ def create_demo_agents(): ) session.add(agent) - logger.info(f"Agente '{agent_data['name']}' criado para o cliente {client_id}") + logger.info(f"Agent '{agent_data['name']}' created for the client {client_id}") session.commit() - logger.info(f"Todos os agentes de exemplo foram criados com sucesso para o cliente {client_id}") + logger.info(f"All example agents were created successfully for the client {client_id}") return True except SQLAlchemyError as e: session.rollback() - logger.error(f"Erro de banco de dados ao criar agentes de exemplo: {str(e)}") + logger.error(f"Database error when creating example agents: {str(e)}") return False except Exception as e: - logger.error(f"Erro ao criar agentes de exemplo: {str(e)}") + logger.error(f"Error when creating example agents: {str(e)}") return False finally: session.close() diff --git a/scripts/seeders/client_seeder.py b/scripts/seeders/client_seeder.py index 0419180a..0f2108a5 100644 --- a/scripts/seeders/client_seeder.py +++ b/scripts/seeders/client_seeder.py @@ -1,9 +1,9 @@ """ -Script para criar um cliente de exemplo: -- Nome: Cliente Demo -- Com usuário associado: - - Email: demo@exemplo.com - - Senha: demo123 (ou definida em variável de ambiente) +Script to create a demo client: +- Name: Demo Client +- With associated user: + - Email: demo@example.com + - Password: demo123 (or defined in environment variable) - is_admin: False - is_active: True - email_verified: True @@ -24,42 +24,42 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( logger = logging.getLogger(__name__) def create_demo_client_and_user(): - """Cria um cliente e usuário de demonstração no sistema""" + """Create a demo client and user in the system""" try: - # Carregar variáveis de ambiente + # Load environment variables load_dotenv() - # Obter configurações do banco de dados + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: - logger.error("Variável de ambiente POSTGRES_CONNECTION_STRING não definida") + logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - # Obter senha do usuário demo (ou usar padrão) + # Get demo user password (or use default) demo_password = os.getenv("DEMO_PASSWORD", "demo123") - # Configurações do cliente e usuário demo - demo_client_name = os.getenv("DEMO_CLIENT_NAME", "Cliente Demo") - demo_email = os.getenv("DEMO_EMAIL", "demo@exemplo.com") + # Demo client and user settings + demo_client_name = os.getenv("DEMO_CLIENT_NAME", "Demo Client") + demo_email = os.getenv("DEMO_EMAIL", "demo@example.com") - # Conectar ao banco de dados + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() try: - # Verificar se o usuário já existe + # Check if the user already exists existing_user = session.query(User).filter(User.email == demo_email).first() if existing_user: - logger.info(f"Usuário demo com email {demo_email} já existe") + logger.info(f"Demo user with email {demo_email} already exists") return True - # Criar cliente demo + # Create demo client demo_client = Client(name=demo_client_name) session.add(demo_client) - session.flush() # Obter o ID do cliente + session.flush() # Get the client ID - # Criar usuário demo associado ao cliente + # Create demo user associated with the client demo_user = User( email=demo_email, password_hash=get_password_hash(demo_password), @@ -69,21 +69,21 @@ def create_demo_client_and_user(): email_verified=True ) - # Adicionar e comitar + # Add and commit session.add(demo_user) session.commit() - logger.info(f"Cliente demo '{demo_client_name}' criado com sucesso") - logger.info(f"Usuário demo criado com sucesso: {demo_email}") + logger.info(f"Demo client '{demo_client_name}' created successfully") + logger.info(f"Demo user created successfully: {demo_email}") return True except SQLAlchemyError as e: session.rollback() - logger.error(f"Erro de banco de dados ao criar cliente/usuário demo: {str(e)}") + logger.error(f"Database error when creating demo client/user: {str(e)}") return False except Exception as e: - logger.error(f"Erro ao criar cliente/usuário demo: {str(e)}") + logger.error(f"Error when creating demo client/user: {str(e)}") return False finally: session.close() diff --git a/scripts/seeders/contact_seeder.py b/scripts/seeders/contact_seeder.py index 4f034615..6640aa99 100644 --- a/scripts/seeders/contact_seeder.py +++ b/scripts/seeders/contact_seeder.py @@ -1,8 +1,8 @@ """ -Script para criar contatos de exemplo para o cliente demo: -- Contatos com histórico de conversas -- Diferentes perfis de cliente -- Dados fictícios para demonstração +Script to create example contacts for the demo client: +- Contacts with conversation history +- Different client profiles +- Fake data for demonstration """ import os @@ -15,7 +15,7 @@ from sqlalchemy.exc import SQLAlchemyError from dotenv import load_dotenv from src.models.models import Contact, User, Client -# Configurar logging +# Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -23,46 +23,46 @@ logger = logging.getLogger(__name__) def create_demo_contacts(): - """Cria contatos de exemplo para o cliente demo""" + """Create example contacts for the demo client""" try: - # Carregar variáveis de ambiente + # Load environment variables load_dotenv() - # Obter configurações do banco de dados + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: - logger.error("Variável de ambiente POSTGRES_CONNECTION_STRING não definida") + logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - # Conectar ao banco de dados + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() try: - # Obter o cliente demo pelo email do usuário - demo_email = os.getenv("DEMO_EMAIL", "demo@exemplo.com") + # Get demo client by user email + demo_email = os.getenv("DEMO_EMAIL", "demo@example.com") demo_user = session.query(User).filter(User.email == demo_email).first() if not demo_user or not demo_user.client_id: logger.error( - f"Usuário demo não encontrado ou não associado a um cliente: {demo_email}" + f"Demo user not found or not associated with a client: {demo_email}" ) return False client_id = demo_user.client_id - # Verificar se já existem contatos para este cliente + # Check if there are already contacts for this client existing_contacts = ( session.query(Contact).filter(Contact.client_id == client_id).all() ) if existing_contacts: logger.info( - f"Já existem {len(existing_contacts)} contatos para o cliente {client_id}" + f"There are already {len(existing_contacts)} contacts for the client {client_id}" ) return True - # Definições dos contatos de exemplo + # Example contact definitions contacts = [ { "name": "Maria Silva", @@ -145,7 +145,7 @@ def create_demo_contacts(): }, ] - # Criar os contatos + # Create the contacts for contact_data in contacts: contact = Contact( client_id=client_id, @@ -156,24 +156,24 @@ def create_demo_contacts(): session.add(contact) logger.info( - f"Contato '{contact_data['name']}' criado para o cliente {client_id}" + f"Contact '{contact_data['name']}' created for the client {client_id}" ) session.commit() logger.info( - f"Todos os contatos de exemplo foram criados com sucesso para o cliente {client_id}" + f"All example contacts were created successfully for the client {client_id}" ) return True except SQLAlchemyError as e: session.rollback() logger.error( - f"Erro de banco de dados ao criar contatos de exemplo: {str(e)}" + f"Database error when creating example contacts: {str(e)}" ) return False except Exception as e: - logger.error(f"Erro ao criar contatos de exemplo: {str(e)}") + logger.error(f"Error when creating example contacts: {str(e)}") return False finally: session.close() diff --git a/scripts/seeders/mcp_server_seeder.py b/scripts/seeders/mcp_server_seeder.py index 1ff7142a..c06d1006 100644 --- a/scripts/seeders/mcp_server_seeder.py +++ b/scripts/seeders/mcp_server_seeder.py @@ -1,10 +1,10 @@ """ -Script para criar servidores MCP padrão: -- Servidor Anthropic Claude -- Servidor OpenAI GPT -- Servidor Google Gemini -- Servidor Ollama (local) -Cada um com configurações padrão para produção +Script to create default MCP servers: +- Anthropic Claude server +- OpenAI GPT server +- Google Gemini server +- Ollama (local) server +Each with default production configurations """ import os @@ -16,7 +16,7 @@ from sqlalchemy.exc import SQLAlchemyError from dotenv import load_dotenv from src.models.models import MCPServer -# Configurar logging +# Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -24,32 +24,32 @@ logger = logging.getLogger(__name__) def create_mcp_servers(): - """Cria servidores MCP padrão no sistema""" + """Create default MCP servers in the system""" try: - # Carregar variáveis de ambiente + # Load environment variables load_dotenv() - # Obter configurações do banco de dados + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: - logger.error("Variável de ambiente POSTGRES_CONNECTION_STRING não definida") + logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - # Conectar ao banco de dados + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() try: - # Verificar se já existem servidores MCP + # Check if there are already MCP servers existing_servers = session.query(MCPServer).all() if existing_servers: logger.info( - f"Já existem {len(existing_servers)} servidores MCP cadastrados" + f"There are already {len(existing_servers)} MCP servers registered" ) return True - # Definições dos servidores MCP + # MCP servers definitions mcp_servers = [ { "name": "Sequential Thinking", @@ -62,7 +62,7 @@ def create_mcp_servers(): ], }, "environments": {}, - "tools": ["sequential_thinking"], + "tools": ["sequentialthinking"], "type": "community", "id": "4519dd69-9343-4792-af51-dc4d322fb0c9", "created_at": "2025-04-28T15:14:16.901236Z", @@ -180,7 +180,7 @@ def create_mcp_servers(): }, ] - # Criar os servidores MCP + # Create the MCP servers for server_data in mcp_servers: server = MCPServer( name=server_data["name"], @@ -192,19 +192,19 @@ def create_mcp_servers(): ) session.add(server) - logger.info(f"Servidor MCP '{server_data['name']}' criado com sucesso") + logger.info(f"MCP server '{server_data['name']}' created successfully") session.commit() - logger.info("Todos os servidores MCP foram criados com sucesso") + logger.info("All MCP servers were created successfully") return True except SQLAlchemyError as e: session.rollback() - logger.error(f"Erro de banco de dados ao criar servidores MCP: {str(e)}") + logger.error(f"Database error when creating MCP servers: {str(e)}") return False except Exception as e: - logger.error(f"Erro ao criar servidores MCP: {str(e)}") + logger.error(f"Error when creating MCP servers: {str(e)}") return False finally: session.close() diff --git a/scripts/seeders/tool_seeder.py b/scripts/seeders/tool_seeder.py index 476253c8..db3014fa 100644 --- a/scripts/seeders/tool_seeder.py +++ b/scripts/seeders/tool_seeder.py @@ -1,10 +1,7 @@ """ -Script para criar ferramentas padrão: -- Pesquisa Web -- Consulta a Documentos -- Consulta a Base de Conhecimento -- Integração WhatsApp/Telegram -Cada uma com configurações básicas para demonstração +Script to create default tools: +- +Each with basic configurations for demonstration """ import os @@ -16,38 +13,38 @@ from sqlalchemy.exc import SQLAlchemyError from dotenv import load_dotenv from src.models.models import Tool -# Configurar logging +# Configure logging 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""" try: - # Carregar variáveis de ambiente + # Load environment variables load_dotenv() - # Obter configurações do banco de dados + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: - logger.error("Variável de ambiente POSTGRES_CONNECTION_STRING não definida") + logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - # Conectar ao banco de dados + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() try: - # Verificar se já existem ferramentas + # Check if there are already tools existing_tools = session.query(Tool).all() if existing_tools: - logger.info(f"Já existem {len(existing_tools)} ferramentas cadastradas") + logger.info(f"There are already {len(existing_tools)} tools registered") return True - # Definições das ferramentas + # Tools definitions tools = [] - # Criar as ferramentas + # Create the tools for tool_data in tools: tool = Tool( @@ -58,19 +55,19 @@ def create_tools(): ) session.add(tool) - logger.info(f"Ferramenta '{tool_data['name']}' criada com sucesso") + logger.info(f"Tool '{tool_data['name']}' created successfully") session.commit() - logger.info("Todas as ferramentas foram criadas com sucesso") + logger.info("All tools were created successfully") return True except SQLAlchemyError as e: session.rollback() - logger.error(f"Erro de banco de dados ao criar ferramentas: {str(e)}") + logger.error(f"Database error when creating tools: {str(e)}") return False except Exception as e: - logger.error(f"Erro ao criar ferramentas: {str(e)}") + logger.error(f"Error when creating tools: {str(e)}") return False finally: session.close() diff --git a/src/api/admin_routes.py b/src/api/admin_routes.py index 4a84a09d..d6818421 100644 --- a/src/api/admin_routes.py +++ b/src/api/admin_routes.py @@ -13,12 +13,12 @@ from src.schemas.user import UserResponse, AdminUserCreate router = APIRouter( prefix="/admin", - tags=["administração"], - dependencies=[Depends(verify_admin)], # Todas as rotas requerem permissão de admin - responses={403: {"description": "Permissão negada"}}, + tags=["admin"], + dependencies=[Depends(verify_admin)], + responses={403: {"description": "Permission denied"}}, ) -# Rotas para auditoria +# Audit routes @router.get("/audit-logs", response_model=List[AuditLogResponse]) async def read_audit_logs( filters: AuditLogFilter = Depends(), @@ -26,15 +26,15 @@ async def read_audit_logs( payload: dict = Depends(get_jwt_token), ): """ - Obter logs de auditoria com filtros opcionais + Get audit logs with optional filters Args: - filters: Filtros para busca de logs - db: Sessão do banco de dados - payload: Payload do token JWT + filters: Filters for log search + db: Database session + payload: JWT token payload Returns: - List[AuditLogResponse]: Lista de logs de auditoria + List[AuditLogResponse]: List of audit logs """ return get_audit_logs( db, @@ -48,7 +48,7 @@ async def read_audit_logs( end_date=filters.end_date ) -# Rotas para administradores +# Admin routes @router.get("/users", response_model=List[UserResponse]) async def read_admin_users( skip: int = 0, @@ -57,16 +57,16 @@ async def read_admin_users( payload: dict = Depends(get_jwt_token), ): """ - Listar usuários administradores + List admin users Args: - skip: Número de registros para pular - limit: Número máximo de registros para retornar - db: Sessão do banco de dados - payload: Payload do token JWT + skip: Number of records to skip + limit: Maximum number of records to return + db: Database session + payload: JWT token payload Returns: - List[UserResponse]: Lista de usuários administradores + List[UserResponse]: List of admin users """ return get_admin_users(db, skip, limit) @@ -78,29 +78,29 @@ async def create_new_admin_user( payload: dict = Depends(get_jwt_token), ): """ - Criar um novo usuário administrador + Create a new admin user Args: - user_data: Dados do usuário a ser criado - request: Objeto Request do FastAPI - db: Sessão do banco de dados - payload: Payload do token JWT + user_data: User data to be created + request: FastAPI Request object + db: Database session + payload: JWT token payload Returns: - UserResponse: Dados do usuário criado + UserResponse: Created user data Raises: - HTTPException: Se houver erro na criação + HTTPException: If there is an error in creation """ - # Obter o ID do usuário atual + # Get current user ID user_id = payload.get("user_id") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Não foi possível identificar o usuário logado" + detail="Unable to identify the logged in user" ) - # Criar o usuário administrador + # Create admin user user, message = create_admin_user(db, user_data) if not user: raise HTTPException( @@ -108,7 +108,7 @@ async def create_new_admin_user( detail=message ) - # Registrar ação no log de auditoria + # Register action in audit log create_audit_log( db, user_id=uuid.UUID(user_id), @@ -129,33 +129,33 @@ async def deactivate_admin_user( payload: dict = Depends(get_jwt_token), ): """ - Desativar um usuário administrador (não exclui, apenas desativa) + Deactivate an admin user (does not delete, only deactivates) Args: - user_id: ID do usuário a ser desativado - request: Objeto Request do FastAPI - db: Sessão do banco de dados - payload: Payload do token JWT + user_id: ID of the user to be deactivated + request: FastAPI Request object + db: Database session + payload: JWT token payload Raises: - HTTPException: Se houver erro na desativação + HTTPException: If there is an error in deactivation """ - # Obter o ID do usuário atual + # Get current user ID current_user_id = payload.get("user_id") if not current_user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Não foi possível identificar o usuário logado" + detail="Unable to identify the logged in user" ) - # Não permitir desativar a si mesmo + # Do not allow deactivating yourself if str(user_id) == current_user_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Não é possível desativar seu próprio usuário" + detail="Unable to deactivate your own user" ) - # Desativar o usuário + # Deactivate user success, message = deactivate_user(db, user_id) if not success: raise HTTPException( @@ -163,7 +163,7 @@ async def deactivate_admin_user( detail=message ) - # Registrar ação no log de auditoria + # Register action in audit log create_audit_log( db, user_id=uuid.UUID(current_user_id), diff --git a/src/api/agent_routes.py b/src/api/agent_routes.py new file mode 100644 index 00000000..91efcb74 --- /dev/null +++ b/src/api/agent_routes.py @@ -0,0 +1,121 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from src.config.database import get_db +from typing import List, Dict, Any +import uuid +from src.core.jwt_middleware import ( + get_jwt_token, + verify_user_client, +) +from src.core.jwt_middleware import ( + get_jwt_token, + verify_user_client, +) +from src.schemas.schemas import ( + Agent, + AgentCreate, +) +from src.services import ( + agent_service, +) +import logging + +logger = logging.getLogger(__name__) + + +router = APIRouter( + prefix="/agents", + tags=["agents"], + responses={404: {"description": "Not found"}}, +) + + +@router.post("/", response_model=Agent, status_code=status.HTTP_201_CREATED) +async def create_agent( + agent: AgentCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Verify if the user has access to the agent's client + await verify_user_client(payload, db, agent.client_id) + + return agent_service.create_agent(db, agent) + + +@router.get("/{client_id}", response_model=List[Agent]) +async def read_agents( + client_id: uuid.UUID, + skip: int = 0, + limit: int = 100, + 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, client_id) + + return agent_service.get_agents_by_client(db, client_id, skip, limit) + + +@router.get("/{agent_id}", response_model=Agent) +async def read_agent( + agent_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + db_agent = agent_service.get_agent(db, agent_id) + if db_agent is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" + ) + + # Verify if the user has access to the agent's client + await verify_user_client(payload, db, db_agent.client_id) + + return db_agent + + +@router.put("/{agent_id}", response_model=Agent) +async def update_agent( + agent_id: uuid.UUID, + agent_data: Dict[str, Any], + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the current agent + db_agent = agent_service.get_agent(db, agent_id) + if db_agent is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" + ) + + # Verify if the user has access to the agent's client + await verify_user_client(payload, db, db_agent.client_id) + + # If trying to change the client_id, verify permission for the new client as well + if "client_id" in agent_data and agent_data["client_id"] != str(db_agent.client_id): + new_client_id = uuid.UUID(agent_data["client_id"]) + await verify_user_client(payload, db, new_client_id) + + return await agent_service.update_agent(db, agent_id, agent_data) + + +@router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_agent( + agent_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the agent + db_agent = agent_service.get_agent(db, agent_id) + if db_agent is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" + ) + + # Verify if the user has access to the agent's client + await verify_user_client(payload, db, db_agent.client_id) + + if not agent_service.delete_agent(db, agent_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" + ) diff --git a/src/api/auth_routes.py b/src/api/auth_routes.py index fb37c303..655e4815 100644 --- a/src/api/auth_routes.py +++ b/src/api/auth_routes.py @@ -30,8 +30,8 @@ logger = logging.getLogger(__name__) router = APIRouter( prefix="/auth", - tags=["autenticação"], - responses={404: {"description": "Não encontrado"}}, + tags=["authentication"], + responses={404: {"description": "Not found"}}, ) @@ -40,17 +40,17 @@ router = APIRouter( ) async def register_user(user_data: UserCreate, db: Session = Depends(get_db)): """ - Registra um novo usuário (cliente) no sistema + Register a new user (client) in the system Args: - user_data: Dados do usuário a ser registrado - db: Sessão do banco de dados + user_data: User data to be registered + db: Database session Returns: - UserResponse: Dados do usuário criado + UserResponse: Created user data Raises: - HTTPException: Se houver erro no registro + HTTPException: If there is an error in registration """ user, message = create_user(db, user_data, is_admin=False) if not user: @@ -70,19 +70,19 @@ async def register_admin( current_admin: User = Depends(get_current_admin_user), ): """ - Registra um novo administrador no sistema. - Apenas administradores existentes podem criar novos administradores. + Register a new admin in the system. + Only existing admins can create new admins. Args: - user_data: Dados do administrador a ser registrado - db: Sessão do banco de dados - current_admin: Administrador atual (autenticado) + user_data: Admin data to be registered + db: Database session + current_admin: Current admin (authenticated) Returns: - UserResponse: Dados do administrador criado + UserResponse: Created admin data Raises: - HTTPException: Se houver erro no registro + HTTPException: If there is an error in registration """ user, message = create_user(db, user_data, is_admin=True) if not user: @@ -90,7 +90,7 @@ async def register_admin( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) logger.info( - f"Administrador registrado com sucesso: {user.email} (criado por {current_admin.email})" + f"Admin registered successfully: {user.email} (created by {current_admin.email})" ) return user @@ -98,24 +98,24 @@ async def register_admin( @router.get("/verify-email/{token}", response_model=MessageResponse) async def verify_user_email(token: str, db: Session = Depends(get_db)): """ - Verifica o email de um usuário usando o token fornecido + Verify user email using the provided token Args: - token: Token de verificação - db: Sessão do banco de dados + token: Verification token + db: Database session Returns: - MessageResponse: Mensagem de sucesso + MessageResponse: Success message Raises: - HTTPException: Se o token for inválido ou expirado + HTTPException: If the token is invalid or expired """ success, message = verify_email(db, token) if not success: - logger.warning(f"Falha na verificação de email: {message}") + logger.warning(f"Failed to verify email: {message}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) - logger.info(f"Email verificado com sucesso usando token: {token}") + logger.info(f"Email verified successfully using token: {token}") return {"message": message} @@ -124,55 +124,55 @@ async def resend_verification_email( email_data: ForgotPassword, db: Session = Depends(get_db) ): """ - Reenvia o email de verificação para o usuário + Resend verification email to the user Args: - email_data: Email do usuário - db: Sessão do banco de dados + email_data: User email + db: Database session Returns: - MessageResponse: Mensagem de sucesso + MessageResponse: Success message Raises: - HTTPException: Se houver erro no reenvio + HTTPException: If there is an error in resending """ success, message = resend_verification(db, email_data.email) if not success: - logger.warning(f"Falha no reenvio de verificação: {message}") + logger.warning(f"Failed to resend verification: {message}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) - logger.info(f"Email de verificação reenviado com sucesso para: {email_data.email}") + logger.info(f"Verification email resent successfully to: {email_data.email}") return {"message": message} @router.post("/login", response_model=TokenResponse) async def login_for_access_token(form_data: UserLogin, db: Session = Depends(get_db)): """ - Realiza login e retorna um token de acesso JWT + Perform login and return a JWT access token Args: - form_data: Dados de login (email e senha) - db: Sessão do banco de dados + form_data: Login data (email and password) + db: Database session Returns: - TokenResponse: Token de acesso e tipo + TokenResponse: Access token and type Raises: - HTTPException: Se as credenciais forem inválidas + HTTPException: If credentials are invalid """ user = authenticate_user(db, form_data.email, form_data.password) if not user: logger.warning( - f"Tentativa de login com credenciais inválidas: {form_data.email}" + f"Login attempt with invalid credentials: {form_data.email}" ) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Email ou senha incorretos", + detail="Invalid email or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token = create_access_token(user) - logger.info(f"Login realizado com sucesso para usuário: {user.email}") + logger.info(f"Login successful for user: {user.email}") return {"access_token": access_token, "token_type": "bearer"} @@ -181,46 +181,46 @@ async def forgot_user_password( email_data: ForgotPassword, db: Session = Depends(get_db) ): """ - Inicia o processo de recuperação de senha + Initiate the password recovery process Args: - email_data: Email do usuário - db: Sessão do banco de dados + email_data: User email + db: Database session Returns: - MessageResponse: Mensagem de sucesso + MessageResponse: Success message Raises: - HTTPException: Se houver erro no processo + HTTPException: If there is an error in the process """ success, message = forgot_password(db, email_data.email) - # Sempre retornamos a mesma mensagem por segurança + # Always return the same message for security return { - "message": "Se o email estiver cadastrado, você receberá instruções para redefinir sua senha." + "message": "If the email is registered, you will receive instructions to reset your password." } @router.post("/reset-password", response_model=MessageResponse) async def reset_user_password(reset_data: PasswordReset, db: Session = Depends(get_db)): """ - Redefine a senha do usuário usando o token fornecido + Reset user password using the provided token Args: - reset_data: Token e nova senha - db: Sessão do banco de dados + reset_data: Token and new password + db: Database session Returns: - MessageResponse: Mensagem de sucesso + MessageResponse: Success message Raises: - HTTPException: Se o token for inválido ou expirado + HTTPException: If the token is invalid or expired """ success, message = reset_password(db, reset_data.token, reset_data.new_password) if not success: - logger.warning(f"Falha na redefinição de senha: {message}") + logger.warning(f"Failed to reset password: {message}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) - logger.info("Senha redefinida com sucesso") + logger.info("Password reset successfully") return {"message": message} @@ -229,16 +229,16 @@ async def get_current_user( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """ - Obtém os dados do usuário autenticado + Get the authenticated user's data Args: - db: Sessão do banco de dados - current_user: Usuário autenticado + db: Database session + current_user: Authenticated user Returns: - UserResponse: Dados do usuário autenticado + UserResponse: Authenticated user data Raises: - HTTPException: Se o usuário não estiver autenticado + HTTPException: If the user is not authenticated """ return current_user diff --git a/src/api/chat_routes.py b/src/api/chat_routes.py new file mode 100644 index 00000000..e977c275 --- /dev/null +++ b/src/api/chat_routes.py @@ -0,0 +1,74 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from src.config.database import get_db +from src.core.jwt_middleware import ( + get_jwt_token, + verify_user_client, +) +from src.services import ( + agent_service, +) +from src.schemas.chat import ChatRequest, ChatResponse, ErrorResponse +from src.services.agent_runner import run_agent +from src.core.exceptions import AgentNotFoundError +from src.main import session_service, artifacts_service, memory_service + +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter( + prefix="/chat", + tags=["chat"], + responses={404: {"description": "Not found"}}, +) + + +@router.post( + "/", + response_model=ChatResponse, + responses={ + 400: {"model": ErrorResponse}, + 404: {"model": ErrorResponse}, + 500: {"model": ErrorResponse}, + }, +) +async def chat( + request: ChatRequest, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Verify if the agent belongs to the user's client + agent = agent_service.get_agent(db, request.agent_id) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" + ) + + # Verify if the user has access to the agent (via client) + await verify_user_client(payload, db, agent.client_id) + + try: + final_response_text = await run_agent( + request.agent_id, + request.contact_id, + request.message, + session_service, + artifacts_service, + memory_service, + db, + ) + + return { + "response": final_response_text, + "status": "success", + "timestamp": datetime.now().isoformat(), + } + + except AgentNotFoundError as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) + ) \ No newline at end of file diff --git a/src/api/client_routes.py b/src/api/client_routes.py new file mode 100644 index 00000000..685437e1 --- /dev/null +++ b/src/api/client_routes.py @@ -0,0 +1,140 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, EmailStr +from sqlalchemy.orm import Session +from src.config.database import get_db +from typing import List +import uuid +from src.core.jwt_middleware import ( + get_jwt_token, + verify_user_client, + verify_admin, + get_current_user_client_id, +) +from src.schemas.schemas import ( + Client, + ClientCreate, +) +from src.schemas.user import UserCreate +from src.services import ( + client_service, +) +import logging + +logger = logging.getLogger(__name__) + + +class ClientRegistration(BaseModel): + name: str + email: EmailStr + password: str + + +router = APIRouter( + prefix="/clients", + tags=["clients"], + responses={404: {"description": "Not found"}}, +) + + +@router.post("/", response_model=Client, status_code=status.HTTP_201_CREATED) +async def create_user( + registration: ClientRegistration, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + """ + Create a client and a user associated with it + + Args: + registration: Client and user data to be created + db: Database session + payload: JWT token payload + + Returns: + Client: Created client + """ + # Only administrators can create clients + await verify_admin(payload) + + # Create ClientCreate and UserCreate objects from ClientRegistration + client = ClientCreate(name=registration.name, email=registration.email) + user = UserCreate( + email=registration.email, password=registration.password, name=registration.name + ) + + # Create client with user + client_obj, message = client_service.create_client_with_user(db, client, user) + if not client_obj: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) + + return client_obj + + +@router.get("/", response_model=List[Client]) +async def read_clients( + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # If admin, can see all clients + # If regular user, can only see their own client + client_id = get_current_user_client_id(payload) + + if client_id: + # Regular user - returns only their own client + client = client_service.get_client(db, client_id) + return [client] if client else [] + else: + # Administrator - returns all clients + return client_service.get_clients(db, skip, limit) + + +@router.get("/{client_id}", response_model=Client) +async def read_client( + client_id: uuid.UUID, + 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, client_id) + + db_client = client_service.get_client(db, client_id) + if db_client is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Client not found" + ) + return db_client + + +@router.put("/{client_id}", response_model=Client) +async def update_client( + client_id: uuid.UUID, + client: ClientCreate, + 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, client_id) + + db_client = client_service.update_client(db, client_id, client) + if db_client is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Client not found" + ) + return db_client + + +@router.delete("/{client_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_client( + client_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can delete clients + await verify_admin(payload) + + if not client_service.delete_client(db, client_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Client not found" + ) diff --git a/src/api/contact_routes.py b/src/api/contact_routes.py new file mode 100644 index 00000000..11fa15f7 --- /dev/null +++ b/src/api/contact_routes.py @@ -0,0 +1,122 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from src.config.database import get_db +from typing import List +import uuid +from src.core.jwt_middleware import ( + get_jwt_token, + verify_user_client, +) +from src.schemas.schemas import ( + Contact, + ContactCreate, +) +from src.services import ( + contact_service, +) +import logging + +logger = logging.getLogger(__name__) + + +router = APIRouter( + prefix="/contacts", + tags=["contacts"], + responses={404: {"description": "Not found"}}, +) + + +@router.post("/", response_model=Contact, status_code=status.HTTP_201_CREATED) +async def create_contact( + contact: ContactCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Verify if the user has access to the contact's client + await verify_user_client(payload, db, contact.client_id) + + return contact_service.create_contact(db, contact) + + +@router.get("/{client_id}", response_model=List[Contact]) +async def read_contacts( + client_id: uuid.UUID, + skip: int = 0, + limit: int = 100, + 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, client_id) + + return contact_service.get_contacts_by_client(db, client_id, skip, limit) + + +@router.get("/{contact_id}", response_model=Contact) +async def read_contact( + contact_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + db_contact = contact_service.get_contact(db, contact_id) + if db_contact is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" + ) + + # Verify if the user has access to the contact's client + await verify_user_client(payload, db, db_contact.client_id) + + return db_contact + + +@router.put("/{contact_id}", response_model=Contact) +async def update_contact( + contact_id: uuid.UUID, + contact: ContactCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the current contact + db_current_contact = contact_service.get_contact(db, contact_id) + if db_current_contact is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" + ) + + # Verify if the user has access to the contact's client + await verify_user_client(payload, db, db_current_contact.client_id) + + # Verify if the user is trying to change the client + if contact.client_id != db_current_contact.client_id: + # Verify if the user has access to the new client as well + await verify_user_client(payload, db, contact.client_id) + + db_contact = contact_service.update_contact(db, contact_id, contact) + if db_contact is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" + ) + return db_contact + + +@router.delete("/{contact_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_contact( + contact_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the contact + db_contact = contact_service.get_contact(db, contact_id) + if db_contact is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" + ) + + # Verify if the user has access to the contact's client + await verify_user_client(payload, db, db_contact.client_id) + + if not contact_service.delete_contact(db, contact_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" + ) diff --git a/src/api/mcp_server_routes.py b/src/api/mcp_server_routes.py new file mode 100644 index 00000000..4d5ad42e --- /dev/null +++ b/src/api/mcp_server_routes.py @@ -0,0 +1,97 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from src.config.database import get_db +from typing import List +import uuid +from src.core.jwt_middleware import ( + get_jwt_token, + verify_admin, +) +from src.schemas.schemas import ( + MCPServer, + MCPServerCreate, +) +from src.services import ( + mcp_server_service, +) +import logging + +logger = logging.getLogger(__name__) + + +router = APIRouter( + prefix="/mcp-servers", + tags=["mcp-servers"], + responses={404: {"description": "Not found"}}, +) + + +@router.post("/", response_model=MCPServer, status_code=status.HTTP_201_CREATED) +async def create_mcp_server( + server: MCPServerCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can create MCP servers + await verify_admin(payload) + + return mcp_server_service.create_mcp_server(db, server) + + +@router.get("/", response_model=List[MCPServer]) +async def read_mcp_servers( + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # All authenticated users can list MCP servers + return mcp_server_service.get_mcp_servers(db, skip, limit) + + +@router.get("/{server_id}", response_model=MCPServer) +async def read_mcp_server( + server_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # All authenticated users can view MCP server details + db_server = mcp_server_service.get_mcp_server(db, server_id) + if db_server is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="MCP server not found" + ) + return db_server + + +@router.put("/{server_id}", response_model=MCPServer) +async def update_mcp_server( + server_id: uuid.UUID, + server: MCPServerCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can update MCP servers + await verify_admin(payload) + + db_server = mcp_server_service.update_mcp_server(db, server_id, server) + if db_server is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="MCP server not found" + ) + return db_server + + +@router.delete("/{server_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_mcp_server( + server_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can delete MCP servers + await verify_admin(payload) + + if not mcp_server_service.delete_mcp_server(db, server_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="MCP server not found" + ) diff --git a/src/api/routes.py b/src/api/routes.py deleted file mode 100644 index 5628e3d5..00000000 --- a/src/api/routes.py +++ /dev/null @@ -1,663 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Body -from sqlalchemy.orm import Session -from typing import List, Dict, Any -import uuid -from datetime import datetime -from pydantic import BaseModel, EmailStr - -from src.config.database import get_db -from src.core.jwt_middleware import ( - get_jwt_token, - verify_user_client, - verify_admin, - get_current_user_client_id, -) -from src.schemas.schemas import ( - Client, - ClientCreate, - Contact, - ContactCreate, - Agent, - AgentCreate, - MCPServer, - MCPServerCreate, - Tool, - ToolCreate, -) -from src.schemas.user import UserCreate -from src.services import ( - client_service, - contact_service, - agent_service, - mcp_server_service, - tool_service, -) -from src.schemas.chat import ChatRequest, ChatResponse, ErrorResponse -from src.services.agent_runner import run_agent -from src.core.exceptions import AgentNotFoundError -from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService -from google.adk.sessions import DatabaseSessionService -from google.adk.memory import InMemoryMemoryService -from google.adk.events import Event -from google.adk.sessions import Session as Adk_Session -from src.config.settings import settings -from src.services.session_service import ( - get_session_events, - get_session_by_id, - delete_session, - get_sessions_by_agent, - get_sessions_by_client, -) - -router = APIRouter() - -# Configuração do PostgreSQL -POSTGRES_CONNECTION_STRING = settings.POSTGRES_CONNECTION_STRING - -# Inicializar os serviços globalmente -session_service = DatabaseSessionService(db_url=POSTGRES_CONNECTION_STRING) -artifacts_service = InMemoryArtifactService() -memory_service = InMemoryMemoryService() - -# Definindo um novo schema para registro combinado de cliente e usuário -class ClientRegistration(BaseModel): - name: str - email: EmailStr - password: str - - -@router.post( - "/chat", - response_model=ChatResponse, - responses={ - 400: {"model": ErrorResponse}, - 404: {"model": ErrorResponse}, - 500: {"model": ErrorResponse}, - }, -) -async def chat( - request: ChatRequest, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o agente pertence ao cliente do usuário - agent = agent_service.get_agent(db, request.agent_id) - if not agent: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado" - ) - - # Verificar se o usuário tem acesso ao agente (via cliente) - await verify_user_client(payload, db, agent.client_id) - - try: - final_response_text = await run_agent( - request.agent_id, - request.contact_id, - request.message, - session_service, - artifacts_service, - memory_service, - db, - ) - - return { - "response": final_response_text, - "status": "success", - "timestamp": datetime.now().isoformat(), - } - - except AgentNotFoundError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) - ) - - -# Rotas para Sessões -@router.get("/sessions/client/{client_id}", response_model=List[Adk_Session]) -async def get_client_sessions( - client_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso aos dados deste cliente - await verify_user_client(payload, db, client_id) - return get_sessions_by_client(db, client_id) - - -@router.get("/sessions/agent/{agent_id}", response_model=List[Adk_Session]) -async def get_agent_sessions( - agent_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), - skip: int = 0, - limit: int = 100, -): - # Verificar se o agente pertence ao cliente do usuário - 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" - ) - - # Verificar se o usuário tem acesso ao agente (via cliente) - await verify_user_client(payload, db, agent.client_id) - - return get_sessions_by_agent(db, agent_id, skip, limit) - - -@router.get("/sessions/{session_id}", response_model=Adk_Session) -async def get_session( - session_id: str, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Obter a sessão - session = get_session_by_id(session_service, session_id) - if not session: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Sessão não encontrada" - ) - - # Verificar se o agente da sessão pertence ao cliente do usuário - agent_id = uuid.UUID(session.agent_id) if session.agent_id else None - if agent_id: - agent = agent_service.get_agent(db, agent_id) - if agent: - await verify_user_client(payload, db, agent.client_id) - - return session - - -@router.get( - "/sessions/{session_id}/messages", - response_model=List[Event], -) -async def get_agent_messages( - session_id: str, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Obter a sessão - session = get_session_by_id(session_service, session_id) - if not session: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Sessão não encontrada" - ) - - # Verificar se o agente da sessão pertence ao cliente do usuário - agent_id = uuid.UUID(session.agent_id) if session.agent_id else None - if agent_id: - agent = agent_service.get_agent(db, agent_id) - if agent: - await verify_user_client(payload, db, agent.client_id) - - return get_session_events(session_service, session_id) - - -@router.delete( - "/sessions/{session_id}", - status_code=status.HTTP_204_NO_CONTENT, -) -async def remove_session( - session_id: str, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Obter a sessão - session = get_session_by_id(session_service, session_id) - if not session: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Sessão não encontrada" - ) - - # Verificar se o agente da sessão pertence ao cliente do usuário - agent_id = uuid.UUID(session.agent_id) if session.agent_id else None - if agent_id: - agent = agent_service.get_agent(db, agent_id) - if agent: - await verify_user_client(payload, db, agent.client_id) - - return delete_session(session_service, session_id) - - -# Rotas para Clientes - - -@router.post("/clients/", response_model=Client, status_code=status.HTTP_201_CREATED) -async def create_user( - registration: ClientRegistration, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - """ - Cria um cliente e um usuário associado a ele - - Args: - registration: Dados do cliente e usuário a serem criados - db: Sessão do banco de dados - payload: Payload do token JWT - - Returns: - Client: Cliente criado - """ - # Apenas administradores podem criar clientes - await verify_admin(payload) - - # Criar objetos ClientCreate e UserCreate a partir do ClientRegistration - client = ClientCreate(name=registration.name, email=registration.email) - user = UserCreate(email=registration.email, password=registration.password, name=registration.name) - - # Criar cliente com usuário - client_obj, message = client_service.create_client_with_user(db, client, user) - if not client_obj: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) - - return client_obj - - -@router.get("/clients/", response_model=List[Client]) -async def read_clients( - skip: int = 0, - limit: int = 100, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Se for administrador, pode ver todos os clientes - # Se for usuário comum, só vê o próprio cliente - client_id = get_current_user_client_id(payload) - - if client_id: - # Usuário comum - retorna apenas seu próprio cliente - client = client_service.get_client(db, client_id) - return [client] if client else [] - else: - # Administrador - retorna todos os clientes - return client_service.get_clients(db, skip, limit) - - -@router.get("/clients/{client_id}", response_model=Client) -async def read_client( - client_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso aos dados deste cliente - await verify_user_client(payload, db, client_id) - - db_client = client_service.get_client(db, client_id) - if db_client is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Cliente não encontrado" - ) - return db_client - - -@router.put("/clients/{client_id}", response_model=Client) -async def update_client( - client_id: uuid.UUID, - client: ClientCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso aos dados deste cliente - await verify_user_client(payload, db, client_id) - - db_client = client_service.update_client(db, client_id, client) - if db_client is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Cliente não encontrado" - ) - return db_client - - -@router.delete("/clients/{client_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_client( - client_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem excluir clientes - await verify_admin(payload) - - if not client_service.delete_client(db, client_id): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Cliente não encontrado" - ) - - -# Rotas para Contatos -@router.post("/contacts/", response_model=Contact, status_code=status.HTTP_201_CREATED) -async def create_contact( - contact: ContactCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso ao cliente do contato - await verify_user_client(payload, db, contact.client_id) - - return contact_service.create_contact(db, contact) - - -@router.get("/contacts/{client_id}", response_model=List[Contact]) -async def read_contacts( - client_id: uuid.UUID, - skip: int = 0, - limit: int = 100, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso aos dados deste cliente - await verify_user_client(payload, db, client_id) - - return contact_service.get_contacts_by_client(db, client_id, skip, limit) - - -@router.get("/contact/{contact_id}", response_model=Contact) -async def read_contact( - contact_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - db_contact = contact_service.get_contact(db, contact_id) - if db_contact is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado" - ) - - # Verificar se o usuário tem acesso ao cliente do contato - await verify_user_client(payload, db, db_contact.client_id) - - return db_contact - - -@router.put("/contact/{contact_id}", response_model=Contact) -async def update_contact( - contact_id: uuid.UUID, - contact: ContactCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Buscar o contato atual - db_current_contact = contact_service.get_contact(db, contact_id) - if db_current_contact is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado" - ) - - # Verificar se o usuário tem acesso ao cliente do contato - await verify_user_client(payload, db, db_current_contact.client_id) - - # Verificar se está tentando mudar o cliente - if contact.client_id != db_current_contact.client_id: - # Verificar se o usuário tem acesso ao novo cliente também - await verify_user_client(payload, db, contact.client_id) - - db_contact = contact_service.update_contact(db, contact_id, contact) - if db_contact is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado" - ) - return db_contact - - -@router.delete("/contact/{contact_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_contact( - contact_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Buscar o contato - db_contact = contact_service.get_contact(db, contact_id) - if db_contact is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado" - ) - - # Verificar se o usuário tem acesso ao cliente do contato - await verify_user_client(payload, db, db_contact.client_id) - - if not contact_service.delete_contact(db, contact_id): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado" - ) - - -# Rotas para Agentes -@router.post("/agents/", response_model=Agent, status_code=status.HTTP_201_CREATED) -async def create_agent( - agent: AgentCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso ao cliente do agente - await verify_user_client(payload, db, agent.client_id) - - return agent_service.create_agent(db, agent) - - -@router.get("/agents/{client_id}", response_model=List[Agent]) -async def read_agents( - client_id: uuid.UUID, - skip: int = 0, - limit: int = 100, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Verificar se o usuário tem acesso aos dados deste cliente - await verify_user_client(payload, db, client_id) - - return agent_service.get_agents_by_client(db, client_id, skip, limit) - - -@router.get("/agent/{agent_id}", response_model=Agent) -async def read_agent( - agent_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - db_agent = agent_service.get_agent(db, agent_id) - if db_agent is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado" - ) - - # Verificar se o usuário tem acesso ao cliente do agente - await verify_user_client(payload, db, db_agent.client_id) - - return db_agent - - -@router.put("/agent/{agent_id}", response_model=Agent) -async def update_agent( - agent_id: uuid.UUID, - agent_data: Dict[str, Any], - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Buscar o agente atual - db_agent = agent_service.get_agent(db, agent_id) - if db_agent is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado" - ) - - # Verificar se o usuário tem acesso ao cliente do agente - await verify_user_client(payload, db, db_agent.client_id) - - # Se estiver tentando mudar o client_id, verificar permissão para o novo cliente também - if "client_id" in agent_data and agent_data["client_id"] != str(db_agent.client_id): - new_client_id = uuid.UUID(agent_data["client_id"]) - await verify_user_client(payload, db, new_client_id) - - return await agent_service.update_agent(db, agent_id, agent_data) - - -@router.delete("/agent/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_agent( - agent_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Buscar o agente - db_agent = agent_service.get_agent(db, agent_id) - if db_agent is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado" - ) - - # Verificar se o usuário tem acesso ao cliente do agente - await verify_user_client(payload, db, db_agent.client_id) - - if not agent_service.delete_agent(db, agent_id): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado" - ) - - -# Rotas para Servidores MCP -@router.post( - "/mcp-servers/", response_model=MCPServer, status_code=status.HTTP_201_CREATED -) -async def create_mcp_server( - server: MCPServerCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem criar servidores MCP - await verify_admin(payload) - - return mcp_server_service.create_mcp_server(db, server) - - -@router.get("/mcp-servers/", response_model=List[MCPServer]) -async def read_mcp_servers( - skip: int = 0, - limit: int = 100, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Todos os usuários autenticados podem listar servidores MCP - return mcp_server_service.get_mcp_servers(db, skip, limit) - - -@router.get("/mcp-servers/{server_id}", response_model=MCPServer) -async def read_mcp_server( - server_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Todos os usuários autenticados podem ver detalhes do servidor MCP - db_server = mcp_server_service.get_mcp_server(db, server_id) - if db_server is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Servidor MCP não encontrado" - ) - return db_server - - -@router.put("/mcp-servers/{server_id}", response_model=MCPServer) -async def update_mcp_server( - server_id: uuid.UUID, - server: MCPServerCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem atualizar servidores MCP - await verify_admin(payload) - - db_server = mcp_server_service.update_mcp_server(db, server_id, server) - if db_server is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Servidor MCP não encontrado" - ) - return db_server - - -@router.delete("/mcp-servers/{server_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_mcp_server( - server_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem excluir servidores MCP - await verify_admin(payload) - - if not mcp_server_service.delete_mcp_server(db, server_id): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Servidor MCP não encontrado" - ) - - -# Rotas para Ferramentas -@router.post("/tools/", response_model=Tool, status_code=status.HTTP_201_CREATED) -async def create_tool( - tool: ToolCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem criar ferramentas - await verify_admin(payload) - - return tool_service.create_tool(db, tool) - - -@router.get("/tools/", response_model=List[Tool]) -async def read_tools( - skip: int = 0, - limit: int = 100, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Todos os usuários autenticados podem listar ferramentas - return tool_service.get_tools(db, skip, limit) - - -@router.get("/tools/{tool_id}", response_model=Tool) -async def read_tool( - tool_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Todos os usuários autenticados podem ver detalhes da ferramenta - db_tool = tool_service.get_tool(db, tool_id) - if db_tool is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Ferramenta não encontrada" - ) - return db_tool - - -@router.put("/tools/{tool_id}", response_model=Tool) -async def update_tool( - tool_id: uuid.UUID, - tool: ToolCreate, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem atualizar ferramentas - await verify_admin(payload) - - db_tool = tool_service.update_tool(db, tool_id, tool) - if db_tool is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Ferramenta não encontrada" - ) - return db_tool - - -@router.delete("/tools/{tool_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_tool( - tool_id: uuid.UUID, - db: Session = Depends(get_db), - payload: dict = Depends(get_jwt_token), -): - # Apenas administradores podem excluir ferramentas - await verify_admin(payload) - - if not tool_service.delete_tool(db, tool_id): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Ferramenta não encontrada" - ) diff --git a/src/api/session_routes.py b/src/api/session_routes.py new file mode 100644 index 00000000..d68be678 --- /dev/null +++ b/src/api/session_routes.py @@ -0,0 +1,138 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from src.config.database import get_db +from typing import List +import uuid +from src.core.jwt_middleware import ( + get_jwt_token, + verify_user_client, +) +from src.services import ( + agent_service, +) +from google.adk.events import Event +from google.adk.sessions import Session as Adk_Session +from src.services.session_service import ( + get_session_events, + get_session_by_id, + delete_session, + get_sessions_by_agent, + get_sessions_by_client, +) +from src.main import session_service +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter( + prefix="/sessions", + tags=["sessions"], + responses={404: {"description": "Not found"}}, +) + +# Session Routes +@router.get("/client/{client_id}", response_model=List[Adk_Session]) +async def get_client_sessions( + client_id: uuid.UUID, + 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, client_id) + return get_sessions_by_client(db, client_id) + + +@router.get("/agent/{agent_id}", response_model=List[Adk_Session]) +async def get_agent_sessions( + agent_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), + skip: int = 0, + limit: int = 100, +): + # Verify if the agent belongs to the user's client + agent = agent_service.get_agent(db, agent_id) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" + ) + + # Verify if the user has access to the agent (via client) + await verify_user_client(payload, db, agent.client_id) + + return get_sessions_by_agent(db, agent_id, skip, limit) + + +@router.get("/{session_id}", response_model=Adk_Session) +async def get_session( + session_id: str, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the session + session = get_session_by_id(session_service, session_id) + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Session not found" + ) + + # Verify if the session's agent belongs to the user's client + agent_id = uuid.UUID(session.agent_id) if session.agent_id else None + if agent_id: + agent = agent_service.get_agent(db, agent_id) + if agent: + await verify_user_client(payload, db, agent.client_id) + + return session + + +@router.get( + "/{session_id}/messages", + response_model=List[Event], +) +async def get_agent_messages( + session_id: str, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the session + session = get_session_by_id(session_service, session_id) + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Session not found" + ) + + # Verify if the session's agent belongs to the user's client + agent_id = uuid.UUID(session.agent_id) if session.agent_id else None + if agent_id: + agent = agent_service.get_agent(db, agent_id) + if agent: + await verify_user_client(payload, db, agent.client_id) + + return get_session_events(session_service, session_id) + + +@router.delete( + "/{session_id}", + status_code=status.HTTP_204_NO_CONTENT, +) +async def remove_session( + session_id: str, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Get the session + session = get_session_by_id(session_service, session_id) + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Session not found" + ) + + # Verify if the session's agent belongs to the user's client + agent_id = uuid.UUID(session.agent_id) if session.agent_id else None + if agent_id: + agent = agent_service.get_agent(db, agent_id) + if agent: + await verify_user_client(payload, db, agent.client_id) + + return delete_session(session_service, session_id) diff --git a/src/api/tool_routes.py b/src/api/tool_routes.py new file mode 100644 index 00000000..344719d9 --- /dev/null +++ b/src/api/tool_routes.py @@ -0,0 +1,97 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from src.config.database import get_db +from typing import List +import uuid +from src.core.jwt_middleware import ( + get_jwt_token, + verify_admin, +) +from src.schemas.schemas import ( + Tool, + ToolCreate, +) +from src.services import ( + tool_service, +) +import logging + +logger = logging.getLogger(__name__) + + +router = APIRouter( + prefix="/tools", + tags=["tools"], + responses={404: {"description": "Not found"}}, +) + + +@router.post("/", response_model=Tool, status_code=status.HTTP_201_CREATED) +async def create_tool( + tool: ToolCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can create tools + await verify_admin(payload) + + return tool_service.create_tool(db, tool) + + +@router.get("/", response_model=List[Tool]) +async def read_tools( + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # All authenticated users can list tools + return tool_service.get_tools(db, skip, limit) + + +@router.get("/{tool_id}", response_model=Tool) +async def read_tool( + tool_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # All authenticated users can view tool details + db_tool = tool_service.get_tool(db, tool_id) + if db_tool is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Tool not found" + ) + return db_tool + + +@router.put("/{tool_id}", response_model=Tool) +async def update_tool( + tool_id: uuid.UUID, + tool: ToolCreate, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can update tools + await verify_admin(payload) + + db_tool = tool_service.update_tool(db, tool_id, tool) + if db_tool is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Tool not found" + ) + return db_tool + + +@router.delete("/{tool_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_tool( + tool_id: uuid.UUID, + db: Session = Depends(get_db), + payload: dict = Depends(get_jwt_token), +): + # Only administrators can delete tools + await verify_admin(payload) + + if not tool_service.delete_tool(db, tool_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Tool not found" + ) diff --git a/src/config/settings.py b/src/config/settings.py index c1c3b45a..81632a73 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -4,77 +4,83 @@ from pydantic_settings import BaseSettings from functools import lru_cache import secrets + class Settings(BaseSettings): - """Configurações do projeto""" - - # Configurações da API - API_TITLE: str = "Agent Runner API" - API_DESCRIPTION: str = "API para execução de agentes de IA" - API_VERSION: str = "1.0.0" - - # Configurações do banco de dados + """Project settings""" + + # API settings + API_TITLE: str = os.getenv("API_TITLE", "Evo AI API") + API_DESCRIPTION: str = os.getenv("API_DESCRIPTION", "API for executing AI agents") + API_VERSION: str = os.getenv("API_VERSION", "1.0.0") + + # Database settings POSTGRES_CONNECTION_STRING: str = os.getenv( - "POSTGRES_CONNECTION_STRING", - "postgresql://postgres:root@localhost:5432/evo_ai" + "POSTGRES_CONNECTION_STRING", "postgresql://postgres:root@localhost:5432/evo_ai" ) - - # Configurações de logging + + # Logging settings LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") LOG_DIR: str = "logs" - - # Configurações da API de Conhecimento + + # Knowledge API settings KNOWLEDGE_API_URL: str = os.getenv("KNOWLEDGE_API_URL", "http://localhost:5540") KNOWLEDGE_API_KEY: str = os.getenv("KNOWLEDGE_API_KEY", "") TENANT_ID: str = os.getenv("TENANT_ID", "") - - # Configurações do Redis + + # Redis settings REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost") REDIS_PORT: int = int(os.getenv("REDIS_PORT", 6379)) REDIS_DB: int = int(os.getenv("REDIS_DB", 0)) REDIS_PASSWORD: Optional[str] = os.getenv("REDIS_PASSWORD") - - # TTL do cache de ferramentas em segundos (1 hora) + + # Tool cache TTL in seconds (1 hour) TOOLS_CACHE_TTL: int = int(os.getenv("TOOLS_CACHE_TTL", 3600)) - - # Configurações JWT + + # JWT settings JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", secrets.token_urlsafe(32)) JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") JWT_EXPIRATION_TIME: int = int(os.getenv("JWT_EXPIRATION_TIME", 30)) - - # Configurações SendGrid + + # SendGrid settings SENDGRID_API_KEY: str = os.getenv("SENDGRID_API_KEY", "") EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@yourdomain.com") APP_URL: str = os.getenv("APP_URL", "http://localhost:8000") - - # Configurações do Servidor + + # Server settings HOST: str = os.getenv("HOST", "0.0.0.0") PORT: int = int(os.getenv("PORT", 8000)) DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true" - - # Configurações de CORS + + # CORS settings CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "*").split(",") - - # Configurações de Token - TOKEN_EXPIRY_HOURS: int = int(os.getenv("TOKEN_EXPIRY_HOURS", 24)) # Tokens de verificação/reset - - # Configurações de Segurança + + # Token settings + TOKEN_EXPIRY_HOURS: int = int( + os.getenv("TOKEN_EXPIRY_HOURS", 24) + ) # Verification/reset tokens + + # Security settings PASSWORD_MIN_LENGTH: int = int(os.getenv("PASSWORD_MIN_LENGTH", 8)) MAX_LOGIN_ATTEMPTS: int = int(os.getenv("MAX_LOGIN_ATTEMPTS", 5)) LOGIN_LOCKOUT_MINUTES: int = int(os.getenv("LOGIN_LOCKOUT_MINUTES", 30)) - - # Configurações de Seeders + + # Seeder settings ADMIN_EMAIL: str = os.getenv("ADMIN_EMAIL", "admin@evoai.com") - ADMIN_INITIAL_PASSWORD: str = os.getenv("ADMIN_INITIAL_PASSWORD", "senhaforte123") - DEMO_EMAIL: str = os.getenv("DEMO_EMAIL", "demo@exemplo.com") + ADMIN_INITIAL_PASSWORD: str = os.getenv( + "ADMIN_INITIAL_PASSWORD", "strongpassword123" + ) + DEMO_EMAIL: str = os.getenv("DEMO_EMAIL", "demo@example.com") DEMO_PASSWORD: str = os.getenv("DEMO_PASSWORD", "demo123") - DEMO_CLIENT_NAME: str = os.getenv("DEMO_CLIENT_NAME", "Cliente Demo") - + DEMO_CLIENT_NAME: str = os.getenv("DEMO_CLIENT_NAME", "Demo Client") + class Config: env_file = ".env" case_sensitive = True + @lru_cache() def get_settings() -> Settings: return Settings() -settings = get_settings() \ No newline at end of file + +settings = get_settings() diff --git a/src/core/exceptions.py b/src/core/exceptions.py index 8eba5e3a..9d553e57 100644 --- a/src/core/exceptions.py +++ b/src/core/exceptions.py @@ -2,7 +2,7 @@ from fastapi import HTTPException from typing import Optional, Dict, Any class BaseAPIException(HTTPException): - """Classe base para exceções da API""" + """Base class for API exceptions""" def __init__( self, status_code: int, @@ -17,16 +17,16 @@ class BaseAPIException(HTTPException): }) class AgentNotFoundError(BaseAPIException): - """Exceção para quando o agente não é encontrado""" + """Exception when the agent is not found""" def __init__(self, agent_id: str): super().__init__( status_code=404, - message=f"Agente com ID {agent_id} não encontrado", + message=f"Agent with ID {agent_id} not found", error_code="AGENT_NOT_FOUND" ) class InvalidParameterError(BaseAPIException): - """Exceção para parâmetros inválidos""" + """Exception for invalid parameters""" def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): super().__init__( status_code=400, @@ -36,7 +36,7 @@ class InvalidParameterError(BaseAPIException): ) class InvalidRequestError(BaseAPIException): - """Exceção para requisições inválidas""" + """Exception for invalid requests""" def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): super().__init__( status_code=400, @@ -46,8 +46,8 @@ class InvalidRequestError(BaseAPIException): ) class InternalServerError(BaseAPIException): - """Exceção para erros internos do servidor""" - def __init__(self, message: str = "Erro interno do servidor"): + """Exception for server errors""" + def __init__(self, message: str = "Server error"): super().__init__( status_code=500, message=message, diff --git a/src/core/jwt_middleware.py b/src/core/jwt_middleware.py index 203ec667..977e5a45 100644 --- a/src/core/jwt_middleware.py +++ b/src/core/jwt_middleware.py @@ -5,8 +5,6 @@ from src.config.settings import settings from datetime import datetime from sqlalchemy.orm import Session from src.config.database import get_db -from src.models.models import User -from src.services.user_service import get_user_by_email from uuid import UUID import logging from typing import Optional @@ -17,20 +15,20 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") async def get_jwt_token(token: str = Depends(oauth2_scheme)) -> dict: """ - Extrai e valida o token JWT + Extracts and validates the JWT token Args: token: Token JWT Returns: - dict: Dados do payload do token + dict: Token payload data Raises: - HTTPException: Se o token for inválido + HTTPException: If the token is invalid """ credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Credenciais inválidas", + detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) @@ -43,18 +41,18 @@ async def get_jwt_token(token: str = Depends(oauth2_scheme)) -> dict: email: str = payload.get("sub") if email is None: - logger.warning("Token sem email (sub)") + logger.warning("Token without email (sub)") raise credentials_exception exp = payload.get("exp") if exp is None or datetime.fromtimestamp(exp) < datetime.utcnow(): - logger.warning(f"Token expirado para {email}") + logger.warning(f"Token expired for {email}") raise credentials_exception return payload except JWTError as e: - logger.error(f"Erro ao decodificar token JWT: {str(e)}") + logger.error(f"Error decoding JWT token: {str(e)}") raise credentials_exception async def verify_user_client( @@ -63,77 +61,77 @@ async def verify_user_client( required_client_id: UUID = None ) -> bool: """ - Verifica se o usuário está associado ao cliente especificado + Verifies if the user is associated with the specified client Args: - payload: Payload do token JWT - db: Sessão do banco de dados - required_client_id: ID do cliente que deve ser verificado + payload: Token JWT payload + db: Database session + required_client_id: Client ID to be verified Returns: bool: True se verificado com sucesso Raises: - HTTPException: Se o usuário não tiver permissão + HTTPException: If the user does not have permission """ - # Administradores têm acesso a todos os clientes + # Administrators have access to all clients if payload.get("is_admin", False): return True # Para não-admins, verificar se o client_id corresponde user_client_id = payload.get("client_id") if not user_client_id: - logger.warning(f"Usuário não-admin sem client_id no token: {payload.get('sub')}") + logger.warning(f"Non-admin user without client_id in token: {payload.get('sub')}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Usuário não associado a um cliente" + detail="User not associated with a client" ) - # Se não foi especificado um client_id para verificar, qualquer cliente é válido + # If no client_id is specified to verify, any client is valid if not required_client_id: return True - # Verificar se o client_id do usuário corresponde ao required_client_id + # Verify if the user's client_id corresponds to the required_client_id if str(user_client_id) != str(required_client_id): - logger.warning(f"Acesso negado: Usuário {payload.get('sub')} tentou acessar recursos do cliente {required_client_id}") + logger.warning(f"Access denied: User {payload.get('sub')} tried to access resources of client {required_client_id}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Permissão negada para acessar recursos deste cliente" + detail="Access denied to access resources of this client" ) return True async def verify_admin(payload: dict = Depends(get_jwt_token)) -> bool: """ - Verifica se o usuário é um administrador + Verifies if the user is an administrator Args: - payload: Payload do token JWT + payload: Token JWT payload Returns: - bool: True se for administrador + bool: True if the user is an administrator Raises: - HTTPException: Se o usuário não for administrador + HTTPException: If the user is not an administrator """ if not payload.get("is_admin", False): - logger.warning(f"Acesso admin negado para usuário: {payload.get('sub')}") + logger.warning(f"Access denied to admin: User {payload.get('sub')}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Permissão negada. Acesso restrito a administradores." + detail="Access denied. Restricted to administrators." ) return True def get_current_user_client_id(payload: dict = Depends(get_jwt_token)) -> Optional[UUID]: """ - Obtém o ID do cliente associado ao usuário atual + Gets the ID of the client associated with the current user Args: - payload: Payload do token JWT + payload: Token JWT payload Returns: - Optional[UUID]: ID do cliente ou None se for administrador + Optional[UUID]: Client ID or None if the user is an administrator """ if payload.get("is_admin", False): return None diff --git a/src/examples/agent_example.py b/src/examples/agent_example.py deleted file mode 100644 index ac7d29fe..00000000 --- a/src/examples/agent_example.py +++ /dev/null @@ -1,54 +0,0 @@ -import asyncio -from src.services.agent_builder import AgentBuilder -from src.services.mcp_builder import MCPBuilder - -async def main(): - # Configuração dos servidores MCP - mcp_config = { - "brave-search": { - "url": "http://localhost:8000", - "headers": { - "Authorization": "Bearer seu_token_aqui" - } - }, - "google-calendar-mcp": { - "url": "http://localhost:8001", - "headers": { - "Authorization": "Bearer seu_token_aqui" - } - } - } - - # Configuração do agente - agent_config = { - "model": "gemini-pro", - "name": "Agente de Pesquisa", - "description": "Agente especializado em pesquisas e agendamentos", - "instruction": "Use as ferramentas disponíveis para realizar pesquisas e agendamentos", - "tools": { - "custom_tool": { - "type": "search", - "config": { - "api_key": "sua_chave_aqui" - } - } - } - } - - # Cria o builder - builder = AgentBuilder(None) # None pois não estamos usando banco de dados neste exemplo - builder.set_mcp_config(mcp_config) - - # Constrói o agente - agent, exit_stack = await builder.build_agent(agent_config) - - try: - # Usa o agente - response = await agent.run("Pesquise sobre inteligência artificial") - print(response) - finally: - # Limpa os recursos - await exit_stack.aclose() - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/src/main.py b/src/main.py index b39023e8..aa2fa5d0 100644 --- a/src/main.py +++ b/src/main.py @@ -2,53 +2,44 @@ import os import sys from pathlib import Path -# Adiciona o diretório raiz ao PYTHONPATH +# Add the root directory to PYTHONPATH root_dir = Path(__file__).parent.parent sys.path.append(str(root_dir)) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from typing import Dict, Any from src.config.database import engine, Base -from src.api.routes import router from src.api.auth_routes import router as auth_router from src.api.admin_routes import router as admin_router +from src.api.chat_routes import router as chat_router +from src.api.session_routes import router as session_router +from src.api.agent_routes import router as agent_router +from src.api.contact_routes import router as contact_router +from src.api.mcp_server_routes import router as mcp_server_router +from src.api.tool_routes import router as tool_router +from src.api.client_routes import router as client_router from src.config.settings import settings from src.utils.logger import setup_logger +from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService +from google.adk.sessions import DatabaseSessionService +from google.adk.memory import InMemoryMemoryService -# Configurar logger +# Configure logger logger = setup_logger(__name__) -# Inicialização do FastAPI + +session_service = DatabaseSessionService(db_url=settings.POSTGRES_CONNECTION_STRING) +artifacts_service = InMemoryArtifactService() +memory_service = InMemoryMemoryService() + +# FastAPI initialization app = FastAPI( title=settings.API_TITLE, - description=settings.API_DESCRIPTION + """ - \n\n - ## Autenticação - Esta API utiliza autenticação JWT (JSON Web Token). Para acessar os endpoints protegidos: - - 1. Registre-se em `/api/v1/auth/register` ou faça login em `/api/v1/auth/login` - 2. Use o token recebido no header de autorização: `Authorization: Bearer {token}` - 3. Tokens expiram após o tempo configurado (padrão: 30 minutos) - - Diferente da versão anterior que usava API Key, o sistema JWT: - - Identifica o usuário específico que está fazendo a requisição - - Limita o acesso apenas aos recursos do cliente ao qual o usuário está associado - - Distingue entre usuários comuns e administradores para controle de acesso - - ## Área Administrativa - Funcionalidades exclusivas para administradores estão disponíveis em `/api/v1/admin/*`: - - - Gerenciamento de usuários administradores - - Logs de auditoria para rastreamento de ações - - Controle de acesso privilegiado - - Essas rotas são acessíveis apenas para usuários com flag `is_admin=true`. - """, + description=settings.API_DESCRIPTION, version=settings.API_VERSION, ) -# Configuração de CORS +# CORS configuration app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, @@ -57,25 +48,34 @@ app.add_middleware( allow_headers=["*"], ) -# Configuração do PostgreSQL +# PostgreSQL configuration POSTGRES_CONNECTION_STRING = os.getenv( "POSTGRES_CONNECTION_STRING", "postgresql://postgres:root@localhost:5432/evo_ai" ) -# Criar as tabelas no banco de dados +# Create database tables Base.metadata.create_all(bind=engine) -# Incluir as rotas -app.include_router(auth_router, prefix="/api/v1") -app.include_router(admin_router, prefix="/api/v1") -app.include_router(router, prefix="/api/v1") +API_PREFIX = "/api/v1" + +# Include routes +app.include_router(auth_router, prefix=API_PREFIX) +app.include_router(admin_router, prefix=API_PREFIX) +app.include_router(mcp_server_router, prefix=API_PREFIX) +app.include_router(tool_router, prefix=API_PREFIX) +app.include_router(client_router, prefix=API_PREFIX) +app.include_router(chat_router, prefix=API_PREFIX) +app.include_router(session_router, prefix=API_PREFIX) +app.include_router(agent_router, prefix=API_PREFIX) +app.include_router(contact_router, prefix=API_PREFIX) + @app.get("/") def read_root(): return { - "message": "Bem-vindo à API Evo AI", + "message": "Welcome to Evo AI API", "documentation": "/docs", "version": settings.API_VERSION, - "auth": "Para acessar a API, use autenticação JWT via '/api/v1/auth/login'" + "auth": "To access the API, use JWT authentication via '/api/v1/auth/login'" } diff --git a/src/models/models.py b/src/models/models.py index 51a79b0b..45a786af 100644 --- a/src/models/models.py +++ b/src/models/models.py @@ -30,7 +30,7 @@ class User(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - # Relacionamento com Client (One-to-One, opcional para administradores) + # Relationship with Client (One-to-One, optional for administrators) client = relationship("Client", backref=backref("user", uselist=False, cascade="all, delete-orphan")) class Contact(Base): @@ -64,7 +64,7 @@ class Agent(Base): ) def to_dict(self): - """Converte o objeto para dicionário, convertendo UUIDs para strings""" + """Converts the object to a dictionary, converting UUIDs to strings""" result = {} for key, value in self.__dict__.items(): if key.startswith('_'): @@ -80,7 +80,7 @@ class Agent(Base): return result def _convert_dict(self, d): - """Converte UUIDs em um dicionário para strings""" + """Converts UUIDs to a dictionary for strings""" result = {} for key, value in d.items(): if isinstance(value, uuid.UUID): @@ -123,7 +123,7 @@ class Tool(Base): class Session(Base): __tablename__ = "sessions" - # A diretiva abaixo faz com que o Alembic ignore esta tabela nas migrações + # 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) @@ -146,5 +146,5 @@ class AuditLog(Base): user_agent = Column(String, nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) - # Relacionamento com User + # Relationship with User user = relationship("User", backref="audit_logs") \ No newline at end of file diff --git a/src/schemas/agent_config.py b/src/schemas/agent_config.py index efbf7a26..8a7c36d8 100644 --- a/src/schemas/agent_config.py +++ b/src/schemas/agent_config.py @@ -3,24 +3,24 @@ from pydantic import BaseModel, Field from uuid import UUID class ToolConfig(BaseModel): - """Configuração de uma ferramenta""" + """Configuration of a tool""" id: UUID - envs: Dict[str, str] = Field(default_factory=dict, description="Variáveis de ambiente da ferramenta") + envs: Dict[str, str] = Field(default_factory=dict, description="Environment variables of the tool") class Config: from_attributes = True class MCPServerConfig(BaseModel): - """Configuração de um servidor MCP""" + """Configuration of an MCP server""" id: UUID - envs: Dict[str, str] = Field(default_factory=dict, description="Variáveis de ambiente do servidor") - tools: List[str] = Field(default_factory=list, description="Lista de ferramentas do servidor") + envs: Dict[str, str] = Field(default_factory=dict, description="Environment variables of the server") + tools: List[str] = Field(default_factory=list, description="List of tools of the server") class Config: from_attributes = True class HTTPToolParameter(BaseModel): - """Parâmetro de uma ferramenta HTTP""" + """Parameter of an HTTP tool""" type: str required: bool description: str @@ -29,7 +29,7 @@ class HTTPToolParameter(BaseModel): from_attributes = True class HTTPToolParameters(BaseModel): - """Parâmetros de uma ferramenta HTTP""" + """Parameters of an HTTP tool""" path_params: Optional[Dict[str, str]] = None query_params: Optional[Dict[str, Union[str, List[str]]]] = None body_params: Optional[Dict[str, HTTPToolParameter]] = None @@ -38,7 +38,7 @@ class HTTPToolParameters(BaseModel): from_attributes = True class HTTPToolErrorHandling(BaseModel): - """Configuração de tratamento de erros""" + """Configuration of error handling""" timeout: int retry_count: int fallback_response: Dict[str, str] @@ -47,7 +47,7 @@ class HTTPToolErrorHandling(BaseModel): from_attributes = True class HTTPTool(BaseModel): - """Configuração de uma ferramenta HTTP""" + """Configuration of an HTTP tool""" name: str method: str values: Dict[str, str] @@ -61,41 +61,41 @@ class HTTPTool(BaseModel): from_attributes = True class CustomTools(BaseModel): - """Configuração de ferramentas personalizadas""" - http_tools: List[HTTPTool] = Field(default_factory=list, description="Lista de ferramentas HTTP") + """Configuration of custom tools""" + http_tools: List[HTTPTool] = Field(default_factory=list, description="List of HTTP tools") class Config: from_attributes = True class LLMConfig(BaseModel): - """Configuração para agentes do tipo LLM""" - tools: Optional[List[ToolConfig]] = Field(default=None, description="Lista de ferramentas disponíveis") - custom_tools: Optional[CustomTools] = Field(default=None, description="Ferramentas personalizadas") - mcp_servers: Optional[List[MCPServerConfig]] = Field(default=None, description="Lista de servidores MCP") - sub_agents: Optional[List[UUID]] = Field(default=None, description="Lista de IDs dos sub-agentes") + """Configuration for LLM agents""" + tools: Optional[List[ToolConfig]] = Field(default=None, description="List of available tools") + custom_tools: Optional[CustomTools] = Field(default=None, description="Custom tools") + mcp_servers: Optional[List[MCPServerConfig]] = Field(default=None, description="List of MCP servers") + sub_agents: Optional[List[UUID]] = Field(default=None, description="List of IDs of sub-agents") class Config: from_attributes = True class SequentialConfig(BaseModel): - """Configuração para agentes do tipo Sequential""" - sub_agents: List[UUID] = Field(..., description="Lista de IDs dos sub-agentes em ordem de execução") + """Configuration for sequential agents""" + sub_agents: List[UUID] = Field(..., description="List of IDs of sub-agents in execution order") class Config: from_attributes = True class ParallelConfig(BaseModel): - """Configuração para agentes do tipo Parallel""" - sub_agents: List[UUID] = Field(..., description="Lista de IDs dos sub-agentes para execução paralela") + """Configuration for parallel agents""" + sub_agents: List[UUID] = Field(..., description="List of IDs of sub-agents for parallel execution") class Config: from_attributes = True class LoopConfig(BaseModel): - """Configuração para agentes do tipo Loop""" - sub_agents: List[UUID] = Field(..., description="Lista de IDs dos sub-agentes para execução em loop") - max_iterations: Optional[int] = Field(default=None, description="Número máximo de iterações") - condition: Optional[str] = Field(default=None, description="Condição para parar o loop") + """Configuration for loop agents""" + sub_agents: List[UUID] = Field(..., description="List of IDs of sub-agents for loop execution") + max_iterations: Optional[int] = Field(default=None, description="Maximum number of iterations") + condition: Optional[str] = Field(default=None, description="Condition to stop the loop") class Config: from_attributes = True \ No newline at end of file diff --git a/src/schemas/audit.py b/src/schemas/audit.py index 52be0c07..60129145 100644 --- a/src/schemas/audit.py +++ b/src/schemas/audit.py @@ -4,18 +4,18 @@ from datetime import datetime from uuid import UUID class AuditLogBase(BaseModel): - """Schema base para log de auditoria""" + """Base schema for audit log""" action: str resource_type: str resource_id: Optional[str] = None details: Optional[Dict[str, Any]] = None class AuditLogCreate(AuditLogBase): - """Schema para criação de log de auditoria""" + """Schema for creating audit log""" pass class AuditLogResponse(AuditLogBase): - """Schema para resposta de log de auditoria""" + """Schema for audit log response""" id: UUID user_id: Optional[UUID] = None ip_address: Optional[str] = None @@ -26,7 +26,7 @@ class AuditLogResponse(AuditLogBase): from_attributes = True class AuditLogFilter(BaseModel): - """Schema para filtros de busca de logs de auditoria""" + """Schema for audit log search filters""" user_id: Optional[UUID] = None action: Optional[str] = None resource_type: Optional[str] = None diff --git a/src/schemas/chat.py b/src/schemas/chat.py index bc7a058b..8a84fa40 100644 --- a/src/schemas/chat.py +++ b/src/schemas/chat.py @@ -2,20 +2,20 @@ from pydantic import BaseModel, Field from typing import Dict, Any, Optional class ChatRequest(BaseModel): - """Schema para requisições de chat""" - agent_id: str = Field(..., description="ID do agente que irá processar a mensagem") - contact_id: str = Field(..., description="ID do contato que irá processar a mensagem") - message: str = Field(..., description="Mensagem do usuário") + """Schema for chat requests""" + agent_id: str = Field(..., description="ID of the agent that will process the message") + contact_id: str = Field(..., description="ID of the contact that will process the message") + message: str = Field(..., description="User message") class ChatResponse(BaseModel): - """Schema para respostas do chat""" - response: str = Field(..., description="Resposta do agente") - status: str = Field(..., description="Status da operação") - error: Optional[str] = Field(None, description="Mensagem de erro, se houver") - timestamp: str = Field(..., description="Timestamp da resposta") + """Schema for chat responses""" + response: str = Field(..., description="Agent response") + status: str = Field(..., description="Operation status") + error: Optional[str] = Field(None, description="Error message, if there is one") + timestamp: str = Field(..., description="Timestamp of the response") class ErrorResponse(BaseModel): - """Schema para respostas de erro""" - error: str = Field(..., description="Mensagem de erro") - status_code: int = Field(..., description="Código HTTP do erro") - details: Optional[Dict[str, Any]] = Field(None, description="Detalhes adicionais do erro") \ No newline at end of file + """Schema for error responses""" + error: str = Field(..., description="Error message") + status_code: int = Field(..., description="HTTP status code of the error") + details: Optional[Dict[str, Any]] = Field(None, description="Additional error details") \ No newline at end of file diff --git a/src/schemas/schemas.py b/src/schemas/schemas.py index 7c96052c..007cedc8 100644 --- a/src/schemas/schemas.py +++ b/src/schemas/schemas.py @@ -36,36 +36,36 @@ class Contact(ContactBase): from_attributes = True class AgentBase(BaseModel): - name: str = Field(..., description="Nome do agente (sem espaços ou caracteres especiais)") - description: Optional[str] = Field(None, description="Descrição do agente") - type: str = Field(..., description="Tipo do agente (llm, sequential, parallel, loop)") - model: Optional[str] = Field(None, description="Modelo do agente (obrigatório apenas para tipo llm)") - api_key: Optional[str] = Field(None, description="API Key do agente (obrigatória apenas para tipo llm)") + name: str = Field(..., description="Agent name (no spaces or special characters)") + description: Optional[str] = Field(None, description="Agent description") + type: str = Field(..., description="Agent type (llm, sequential, parallel, loop)") + 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)") instruction: Optional[str] = None - config: Union[LLMConfig, Dict[str, Any]] = Field(..., description="Configuração do agente baseada no tipo") + config: Union[LLMConfig, Dict[str, Any]] = Field(..., description="Agent configuration based on type") @validator('name') def validate_name(cls, v): if not re.match(r'^[a-zA-Z0-9_-]+$', v): - raise ValueError('O nome do agente não pode conter espaços ou caracteres especiais') + raise ValueError('Agent name cannot contain spaces or special characters') return v @validator('type') def validate_type(cls, v): if v not in ['llm', 'sequential', 'parallel', 'loop']: - raise ValueError('Tipo de agente inválido. Deve ser: llm, sequential, parallel ou loop') + raise ValueError('Invalid agent type. Must be: llm, sequential, parallel or loop') return v @validator('model') def validate_model(cls, v, values): if 'type' in values and values['type'] == 'llm' and not v: - raise ValueError('Modelo é obrigatório para agentes do tipo llm') + 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 é obrigatória para agentes do tipo llm') + raise ValueError('API Key is required for llm type agents') return v @validator('config') @@ -76,21 +76,21 @@ class AgentBase(BaseModel): if values['type'] == 'llm': if isinstance(v, dict): try: - # Converte o dicionário para LLMConfig + # Convert the dictionary to LLMConfig v = LLMConfig(**v) except Exception as e: - raise ValueError(f'Configuração inválida para agente LLM: {str(e)}') + raise ValueError(f'Invalid LLM configuration for agent: {str(e)}') elif not isinstance(v, LLMConfig): - raise ValueError('Configuração inválida para agente LLM') + raise ValueError('Invalid LLM configuration for agent') elif values['type'] in ['sequential', 'parallel', 'loop']: if not isinstance(v, dict): - raise ValueError(f'Configuração inválida para agente {values["type"]}') + raise ValueError(f'Invalid configuration for agent {values["type"]}') if 'sub_agents' not in v: - raise ValueError(f'Agente {values["type"]} deve ter sub_agents') + raise ValueError(f'Agent {values["type"]} must have sub_agents') if not isinstance(v['sub_agents'], list): - raise ValueError('sub_agents deve ser uma lista') + raise ValueError('sub_agents must be a list') if not v['sub_agents']: - raise ValueError(f'Agente {values["type"]} deve ter pelo menos um sub-agente') + raise ValueError(f'Agent {values["type"]} must have at least one sub-agent') return v class AgentCreate(AgentBase): diff --git a/src/schemas/user.py b/src/schemas/user.py index 85ace718..5a07c5b6 100644 --- a/src/schemas/user.py +++ b/src/schemas/user.py @@ -8,7 +8,7 @@ class UserBase(BaseModel): class UserCreate(UserBase): password: str - name: str # Para criação do cliente associado + name: str # For client creation class AdminUserCreate(UserBase): password: str @@ -34,7 +34,7 @@ class TokenResponse(BaseModel): token_type: str class TokenData(BaseModel): - sub: str # email do usuário + sub: str # user email exp: datetime is_admin: bool client_id: Optional[UUID] = None diff --git a/src/services/agent_builder.py b/src/services/agent_builder.py index a2952ffc..d2dae0d4 100644 --- a/src/services/agent_builder.py +++ b/src/services/agent_builder.py @@ -1,7 +1,6 @@ from typing import List, Optional, Tuple from google.adk.agents.llm_agent import LlmAgent from google.adk.agents import SequentialAgent, ParallelAgent, LoopAgent -from google.adk.memory import InMemoryMemoryService from google.adk.models.lite_llm import LiteLlm from src.utils.logger import setup_logger from src.core.exceptions import AgentNotFoundError @@ -26,25 +25,25 @@ def before_model_callback( callback_context: CallbackContext, llm_request: LlmRequest ) -> Optional[LlmResponse]: """ - Callback executado antes do modelo gerar uma resposta. - Sempre executa a busca na base de conhecimento antes de prosseguir. + Callback executed before the model generates a response. + Always executes a search in the knowledge base before proceeding. """ try: agent_name = callback_context.agent_name logger.debug(f"🔄 Before model call for agent: {agent_name}") - # Extrai a última mensagem do usuário + # Extract the last user message last_user_message = "" if llm_request.contents and llm_request.contents[-1].role == "user": if llm_request.contents[-1].parts: last_user_message = llm_request.contents[-1].parts[0].text logger.debug(f"📝 Última mensagem do usuário: {last_user_message}") - # Extrai e formata o histórico de mensagens + # Extract and format the history of messages history = [] for content in llm_request.contents: if content.parts and content.parts[0].text: - # Substitui 'model' por 'assistant' no role + # Replace 'model' with 'assistant' in the role role = "assistant" if content.role == "model" else content.role history.append( { @@ -56,12 +55,12 @@ def before_model_callback( } ) - # loga o histórico de mensagens - logger.debug(f"📝 Histórico de mensagens: {history}") + # log the history of messages + logger.debug(f"📝 History of messages: {history}") if last_user_message: - logger.info("🔍 Executando busca na base de conhecimento") - # Executa a busca na base de conhecimento de forma síncrona + logger.info("🔍 Executing knowledge base search") + # Execute the knowledge base search synchronously search_results = search_knowledge_base_function_sync( last_user_message, history ) @@ -69,10 +68,10 @@ def before_model_callback( if search_results: logger.info("✅ Resultados encontrados, adicionando ao contexto") - # Obtém a instrução original do sistema + # Get the original system instruction original_instruction = llm_request.config.system_instruction or "" - # Adiciona os resultados da busca e o histórico ao contexto do sistema + # Add the search results and history to the system context modified_text = ( original_instruction + "\n\n\n" @@ -84,23 +83,23 @@ def before_model_callback( llm_request.config.system_instruction = modified_text logger.debug( - f"📝 Instrução do sistema atualizada com resultados da busca e histórico" + f"📝 System instruction updated with search results and history" ) else: - logger.warning("⚠️ Nenhum resultado encontrado na busca") + logger.warning("⚠️ No results found in the search") else: - logger.warning("⚠️ Nenhuma mensagem do usuário encontrada") + logger.warning("⚠️ No user message found") - logger.info("✅ Before_model_callback finalizado") + logger.info("✅ before_model_callback finished") return None except Exception as e: - logger.error(f"❌ Erro no before_model_callback: {str(e)}", exc_info=True) + logger.error(f"❌ Error in before_model_callback: {str(e)}", exc_info=True) return None def search_knowledge_base_function_sync(query: str, history=[]): """ - Search knowledge base de forma síncrona. + Search knowledge base synchronously. Args: query (str): The search query, with user message and history messages, all in one string @@ -109,21 +108,21 @@ def search_knowledge_base_function_sync(query: str, history=[]): dict: The search results """ try: - logger.info("🔍 Iniciando busca na base de conhecimento") - logger.debug(f"Query recebida: {query}") + logger.info("🔍 Starting knowledge base search") + logger.debug(f"Received query: {query}") # url = os.getenv("KNOWLEDGE_API_URL") + "/api/v1/search" url = os.getenv("KNOWLEDGE_API_URL") + "/api/v1/knowledge" tenant_id = os.getenv("TENANT_ID") url = url + "?tenant_id=" + tenant_id - logger.debug(f"URL da API: {url}") + logger.debug(f"API URL: {url}") logger.debug(f"Tenant ID: {tenant_id}") headers = { "x-api-key": f"{os.getenv('KNOWLEDGE_API_KEY')}", "Content-Type": "application/json", } - logger.debug(f"Headers configurados: {headers}") + logger.debug(f"Headers configured: {headers}") payload = { "gemini_api_key": os.getenv("GOOGLE_API_KEY"), @@ -134,31 +133,31 @@ def search_knowledge_base_function_sync(query: str, history=[]): "history": history, } - logger.debug(f"Payload da requisição: {payload}") + logger.debug(f"Request payload: {payload}") - # Usando requests para fazer a requisição síncrona com timeout - logger.info("🔄 Fazendo requisição síncrona para a API de conhecimento") + # Using requests to make a synchronous request with timeout + logger.info("🔄 Making synchronous request to the knowledge API") # response = requests.post(url, headers=headers, json=payload) response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: - logger.info("✅ Busca realizada com sucesso") + logger.info("✅ Search executed successfully") result = response.json() - logger.debug(f"Resultado da busca: {result}") + logger.debug(f"Search result: {result}") return result else: logger.error( - f"❌ Erro ao realizar busca. Status code: {response.status_code}" + f"❌ Error performing search. Status code: {response.status_code}" ) return None except requests.exceptions.Timeout: - logger.error("❌ Timeout ao realizar busca na base de conhecimento") + logger.error("❌ Timeout performing search") return None except requests.exceptions.RequestException as e: - logger.error(f"❌ Erro na requisição: {str(e)}", exc_info=True) + logger.error(f"❌ Error in request: {str(e)}", exc_info=True) return None except Exception as e: - logger.error(f"❌ Erro ao realizar busca: {str(e)}", exc_info=True) + logger.error(f"❌ Error performing search: {str(e)}", exc_info=True) return None @@ -171,19 +170,19 @@ class AgentBuilder: async def _create_llm_agent( self, agent ) -> Tuple[LlmAgent, Optional[AsyncExitStack]]: - """Cria um agente LLM a partir dos dados do agente.""" - # Obtém ferramentas personalizadas da configuração + """Create an LLM agent from the agent data.""" + # Get custom tools from the configuration custom_tools = [] if agent.config.get("tools"): custom_tools = self.custom_tool_builder.build_tools(agent.config["tools"]) - # Obtém ferramentas MCP da configuração + # Get MCP tools from the configuration mcp_tools = [] mcp_exit_stack = None if agent.config.get("mcp_servers"): mcp_tools, mcp_exit_stack = await self.mcp_service.build_tools(agent.config, self.db) - # Combina todas as ferramentas + # Combine all tools all_tools = custom_tools + mcp_tools now = datetime.now() @@ -192,7 +191,7 @@ class AgentBuilder: current_date_iso = now.strftime("%Y-%m-%d") current_time = now.strftime("%H:%M") - # Substitui as variáveis no prompt + # Substitute variables in the prompt formatted_prompt = agent.instruction.format( current_datetime=current_datetime, current_day_of_week=current_day_of_week, @@ -200,7 +199,7 @@ class AgentBuilder: current_time=current_time, ) - # Verifica se load_memory está habilitado + # Check if load_memory is enabled # before_model_callback_func = None if agent.config.get("load_memory") == True: all_tools.append(load_memory) @@ -222,17 +221,17 @@ class AgentBuilder: async def _get_sub_agents( self, sub_agent_ids: List[str] ) -> List[Tuple[LlmAgent, Optional[AsyncExitStack]]]: - """Obtém e cria os sub-agentes LLM.""" + """Get and create LLM sub-agents.""" sub_agents = [] for sub_agent_id in sub_agent_ids: agent = get_agent(self.db, sub_agent_id) if agent is None: - raise AgentNotFoundError(f"Agente com ID {sub_agent_id} não encontrado") + raise AgentNotFoundError(f"Agent with ID {sub_agent_id} not found") if agent.type != "llm": raise ValueError( - f"Agente {agent.name} (ID: {agent.id}) não é um agente LLM" + f"Agent {agent.name} (ID: {agent.id}) is not an LLM agent" ) sub_agent, exit_stack = await self._create_llm_agent(agent) @@ -243,8 +242,8 @@ class AgentBuilder: async def build_llm_agent( self, root_agent ) -> Tuple[LlmAgent, Optional[AsyncExitStack]]: - """Constrói um agente LLM com seus sub-agentes.""" - logger.info("Criando agente LLM") + """Build an LLM agent with its sub-agents.""" + logger.info("Creating LLM agent") sub_agents = [] if root_agent.config.get("sub_agents"): @@ -262,8 +261,8 @@ class AgentBuilder: async def build_composite_agent( self, root_agent ) -> Tuple[SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack]]: - """Constrói um agente composto (Sequential, Parallel ou Loop) com seus sub-agentes.""" - logger.info(f"Processando sub-agentes para agente {root_agent.type}") + """Build a composite agent (Sequential, Parallel or Loop) with its sub-agents.""" + logger.info(f"Processing sub-agents for agent {root_agent.type}") sub_agents_with_stacks = await self._get_sub_agents( root_agent.config.get("sub_agents", []) @@ -271,7 +270,7 @@ class AgentBuilder: sub_agents = [agent for agent, _ in sub_agents_with_stacks] if root_agent.type == "sequential": - logger.info("Criando SequentialAgent") + logger.info("Creating SequentialAgent") return ( SequentialAgent( name=root_agent.name, @@ -281,7 +280,7 @@ class AgentBuilder: None, ) elif root_agent.type == "parallel": - logger.info("Criando ParallelAgent") + logger.info("Creating ParallelAgent") return ( ParallelAgent( name=root_agent.name, @@ -291,7 +290,7 @@ class AgentBuilder: None, ) elif root_agent.type == "loop": - logger.info("Criando LoopAgent") + logger.info("Creating LoopAgent") return ( LoopAgent( name=root_agent.name, @@ -302,14 +301,14 @@ class AgentBuilder: None, ) else: - raise ValueError(f"Tipo de agente inválido: {root_agent.type}") + raise ValueError(f"Invalid agent type: {root_agent.type}") async def build_agent( self, root_agent ) -> Tuple[ LlmAgent | SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack] ]: - """Constrói o agente apropriado baseado no tipo do agente root.""" + """Build the appropriate agent based on the type of the root agent.""" if root_agent.type == "llm": return await self.build_llm_agent(root_agent) else: diff --git a/src/services/agent_runner.py b/src/services/agent_runner.py index 3c5a7c86..f81867b8 100644 --- a/src/services/agent_runner.py +++ b/src/services/agent_runner.py @@ -1,8 +1,3 @@ -import os -from typing import Any, Dict -from google.adk.agents.llm_agent import LlmAgent -from google.adk.models.lite_llm import LiteLlm -from google.adk.agents import SequentialAgent, ParallelAgent, LoopAgent from google.adk.runners import Runner from google.genai.types import Content, Part from google.adk.sessions import DatabaseSessionService @@ -13,7 +8,6 @@ from src.core.exceptions import AgentNotFoundError, InternalServerError from src.services.agent_service import get_agent from src.services.agent_builder import AgentBuilder from sqlalchemy.orm import Session -from contextlib import AsyncExitStack logger = setup_logger(__name__) @@ -29,23 +23,23 @@ async def run_agent( ): try: logger.info( - f"Iniciando execução do agente {agent_id} para contato {contact_id}" + f"Starting execution of agent {agent_id} for contact {contact_id}" ) - logger.info(f"Mensagem recebida: {message}") + logger.info(f"Received message: {message}") get_root_agent = get_agent(db, agent_id) logger.info( - f"Agente root encontrado: {get_root_agent.name} (tipo: {get_root_agent.type})" + f"Root agent found: {get_root_agent.name} (type: {get_root_agent.type})" ) if get_root_agent is None: - raise AgentNotFoundError(f"Agente com ID {agent_id} não encontrado") + raise AgentNotFoundError(f"Agent with ID {agent_id} not found") - # Usando o AgentBuilder para criar o agente + # Using the AgentBuilder to create the agent agent_builder = AgentBuilder(db) root_agent, exit_stack = await agent_builder.build_agent(get_root_agent) - logger.info("Configurando Runner") + logger.info("Configuring Runner") agent_runner = Runner( agent=root_agent, app_name=agent_id, @@ -55,7 +49,7 @@ async def run_agent( ) session_id = contact_id + "_" + agent_id - logger.info(f"Buscando sessão para contato {contact_id}") + logger.info(f"Searching session for contact {contact_id}") session = session_service.get_session( app_name=agent_id, user_id=contact_id, @@ -63,7 +57,7 @@ async def run_agent( ) if session is None: - logger.info(f"Criando nova sessão para contato {contact_id}") + logger.info(f"Creating new session for contact {contact_id}") session = session_service.create_session( app_name=agent_id, user_id=contact_id, @@ -71,7 +65,7 @@ async def run_agent( ) content = Content(role="user", parts=[Part(text=message)]) - logger.info("Iniciando execução do agente") + logger.info("Starting agent execution") final_response_text = None try: @@ -82,7 +76,7 @@ async def run_agent( ): if event.is_final_response() and event.content and event.content.parts: final_response_text = event.content.parts[0].text - logger.info(f"Resposta final recebida: {final_response_text}") + logger.info(f"Final response received: {final_response_text}") completed_session = session_service.get_session( app_name=agent_id, @@ -93,15 +87,15 @@ async def run_agent( memory_service.add_session_to_memory(completed_session) finally: - # Garante que o exit_stack seja fechado corretamente + # Ensure the exit_stack is closed correctly if exit_stack: await exit_stack.aclose() - logger.info("Execução do agente concluída com sucesso") + logger.info("Agent execution completed successfully") return final_response_text except AgentNotFoundError as e: - logger.error(f"Erro ao processar requisição: {str(e)}") + logger.error(f"Error processing request: {str(e)}") raise e except Exception as e: - logger.error(f"Erro interno ao processar requisição: {str(e)}", exc_info=True) + logger.error(f"Internal error processing request: {str(e)}", exc_info=True) raise InternalServerError(str(e)) diff --git a/src/services/agent_service.py b/src/services/agent_service.py index e3b35e22..9e48642c 100644 --- a/src/services/agent_service.py +++ b/src/services/agent_service.py @@ -3,12 +3,6 @@ from sqlalchemy.exc import SQLAlchemyError from fastapi import HTTPException, status from src.models.models import Agent from src.schemas.schemas import AgentCreate -from src.schemas.agent_config import ( - LLMConfig, - SequentialConfig, - ParallelConfig, - LoopConfig, -) from typing import List, Optional, Dict, Any from src.services.mcp_server_service import get_mcp_server import uuid @@ -18,7 +12,7 @@ logger = logging.getLogger(__name__) def validate_sub_agents(db: Session, sub_agents: List[uuid.UUID]) -> bool: - """Valida se todos os sub-agentes existem""" + """Validate if all sub-agents exist""" for agent_id in sub_agents: agent = get_agent(db, agent_id) if not agent: @@ -27,18 +21,18 @@ def validate_sub_agents(db: Session, sub_agents: List[uuid.UUID]) -> bool: def get_agent(db: Session, agent_id: uuid.UUID) -> Optional[Agent]: - """Busca um agente pelo ID""" + """Search for an agent by ID""" try: agent = db.query(Agent).filter(Agent.id == agent_id).first() if not agent: - logger.warning(f"Agente não encontrado: {agent_id}") + logger.warning(f"Agent not found: {agent_id}") return None return agent except SQLAlchemyError as e: - logger.error(f"Erro ao buscar agente {agent_id}: {str(e)}") + logger.error(f"Error searching for agent {agent_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar agente", + detail="Error searching for agent", ) @@ -49,72 +43,72 @@ def get_agents_by_client( limit: int = 100, active_only: bool = True, ) -> List[Agent]: - """Busca agentes de um cliente com paginação""" + """Search for agents by client with pagination""" try: query = db.query(Agent).filter(Agent.client_id == client_id) return query.offset(skip).limit(limit).all() except SQLAlchemyError as e: - logger.error(f"Erro ao buscar agentes do cliente {client_id}: {str(e)}") + logger.error(f"Error searching for client agents {client_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar agentes", + detail="Error searching for agents", ) def create_agent(db: Session, agent: AgentCreate) -> Agent: - """Cria um novo agente""" + """Create a new agent""" try: - # Validação adicional de sub-agentes + # Additional sub-agent validation if agent.type != "llm": if not isinstance(agent.config, dict): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Configuração inválida: deve ser um objeto com sub_agents", + detail="Invalid configuration: must be an object with sub_agents", ) if "sub_agents" not in agent.config: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Configuração inválida: sub_agents é obrigatório para agentes do tipo sequential, parallel ou loop", + detail="Invalid configuration: sub_agents is required for sequential, parallel or loop agents", ) if not agent.config["sub_agents"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Configuração inválida: sub_agents não pode estar vazio", + detail="Invalid configuration: sub_agents cannot be empty", ) if not validate_sub_agents(db, agent.config["sub_agents"]): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Um ou mais sub-agentes não existem", + detail="One or more sub-agents do not exist", ) - # Processa a configuração antes de criar o agente + # Process the configuration before creating the agent config = agent.config if isinstance(config, dict): - # Processa servidores MCP + # Process MCP servers if "mcp_servers" in config: processed_servers = [] for server in config["mcp_servers"]: - # Busca o servidor MCP no banco + # Search for MCP server in the database mcp_server = get_mcp_server(db, server["id"]) if not mcp_server: raise HTTPException( status_code=400, - detail=f"Servidor MCP não encontrado: {server['id']}", + detail=f"MCP server not found: {server['id']}", ) - # Verifica se todas as variáveis de ambiente necessárias estão preenchidas + # Check if all required environment variables are provided for env_key, env_value in mcp_server.environments.items(): if env_key not in server.get("envs", {}): raise HTTPException( status_code=400, - detail=f"Variável de ambiente '{env_key}' não fornecida para o servidor MCP {mcp_server.name}", + detail=f"Environment variable '{env_key}' not provided for MCP server {mcp_server.name}", ) - # Adiciona o servidor processado com suas ferramentas + # Add the processed server with its tools processed_servers.append( { "id": str(server["id"]), @@ -125,13 +119,13 @@ def create_agent(db: Session, agent: AgentCreate) -> Agent: config["mcp_servers"] = processed_servers - # Processa sub-agentes + # Process sub-agents if "sub_agents" in config: config["sub_agents"] = [ str(agent_id) for agent_id in config["sub_agents"] ] - # Processa ferramentas + # Process tools if "tools" in config: config["tools"] = [ {"id": str(tool["id"]), "envs": tool["envs"]} @@ -144,51 +138,51 @@ def create_agent(db: Session, agent: AgentCreate) -> Agent: db.add(db_agent) db.commit() db.refresh(db_agent) - logger.info(f"Agente criado com sucesso: {db_agent.id}") + logger.info(f"Agent created successfully: {db_agent.id}") return db_agent except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar agente: {str(e)}") + logger.error(f"Error creating agent: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao criar agente", + detail="Error creating agent", ) async def update_agent( db: Session, agent_id: uuid.UUID, agent_data: Dict[str, Any] ) -> Agent: - """Atualiza um agente existente""" + """Update an existing agent""" try: agent = db.query(Agent).filter(Agent.id == agent_id).first() if not agent: - raise HTTPException(status_code=404, detail="Agente não encontrado") + raise HTTPException(status_code=404, detail="Agent not found") - # Converte os UUIDs em strings antes de salvar + # Convert UUIDs to strings before saving if "config" in agent_data: config = agent_data["config"] - # Processa servidores MCP + # Process MCP servers if "mcp_servers" in config: processed_servers = [] for server in config["mcp_servers"]: - # Busca o servidor MCP no banco + # Search for MCP server in the database mcp_server = get_mcp_server(db, server["id"]) if not mcp_server: raise HTTPException( status_code=400, - detail=f"Servidor MCP não encontrado: {server['id']}", + detail=f"MCP server not found: {server['id']}", ) - # Verifica se todas as variáveis de ambiente necessárias estão preenchidas + # Check if all required environment variables are provided for env_key, env_value in mcp_server.environments.items(): if env_key not in server.get("envs", {}): raise HTTPException( status_code=400, - detail=f"Variável de ambiente '{env_key}' não fornecida para o servidor MCP {mcp_server.name}", + detail=f"Environment variable '{env_key}' not provided for MCP server {mcp_server.name}", ) - # Adiciona o servidor processado + # Add the processed server processed_servers.append( { "id": str(server["id"]), @@ -199,13 +193,13 @@ async def update_agent( config["mcp_servers"] = processed_servers - # Processa sub-agentes + # Process sub-agents if "sub_agents" in config: config["sub_agents"] = [ str(agent_id) for agent_id in config["sub_agents"] ] - # Processa ferramentas + # Process tools if "tools" in config: config["tools"] = [ {"id": str(tool["id"]), "envs": tool["envs"]} @@ -223,43 +217,43 @@ async def update_agent( except Exception as e: db.rollback() raise HTTPException( - status_code=500, detail=f"Erro ao atualizar agente: {str(e)}" + status_code=500, detail=f"Error updating agent: {str(e)}" ) def delete_agent(db: Session, agent_id: uuid.UUID) -> bool: - """Remove um agente (soft delete)""" + """Remove an agent (soft delete)""" try: db_agent = get_agent(db, agent_id) if not db_agent: return False db.commit() - logger.info(f"Agente desativado com sucesso: {agent_id}") + logger.info(f"Agent deactivated successfully: {agent_id}") return True except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao desativar agente {agent_id}: {str(e)}") + logger.error(f"Error deactivating agent {agent_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao desativar agente", + detail="Error deactivating agent", ) def activate_agent(db: Session, agent_id: uuid.UUID) -> bool: - """Reativa um agente""" + """Reactivate an agent""" try: db_agent = get_agent(db, agent_id) if not db_agent: return False db.commit() - logger.info(f"Agente reativado com sucesso: {agent_id}") + logger.info(f"Agent reactivated successfully: {agent_id}") return True except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao reativar agente {agent_id}: {str(e)}") + logger.error(f"Error reactivating agent {agent_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao reativar agente", + detail="Error reactivating agent", ) diff --git a/src/services/audit_service.py b/src/services/audit_service.py index f1799f50..ecec8910 100644 --- a/src/services/audit_service.py +++ b/src/services/audit_service.py @@ -20,19 +20,19 @@ def create_audit_log( request: Optional[Request] = None ) -> Optional[AuditLog]: """ - Cria um novo registro de auditoria + Create a new audit log Args: - db: Sessão do banco de dados - user_id: ID do usuário que realizou a ação (ou None se anônimo) - action: Ação realizada (ex: "create", "update", "delete") - resource_type: Tipo de recurso (ex: "client", "agent", "user") - resource_id: ID do recurso (opcional) - details: Detalhes adicionais da ação (opcional) - request: Objeto Request do FastAPI (opcional, para obter IP e User-Agent) + db: Database session + user_id: User ID that performed the action (or None if anonymous) + action: Action performed (ex: "create", "update", "delete") + resource_type: Resource type (ex: "client", "agent", "user") + resource_id: Resource ID (optional) + details: Additional details of the action (optional) + request: FastAPI Request object (optional, to get IP and User-Agent) Returns: - Optional[AuditLog]: Registro de auditoria criado ou None em caso de erro + Optional[AuditLog]: Created audit log or None in case of error """ try: ip_address = None @@ -42,9 +42,9 @@ def create_audit_log( ip_address = request.client.host if hasattr(request, 'client') else None user_agent = request.headers.get("user-agent") - # Converter details para formato serializável + # Convert details to serializable format if details: - # Converter UUIDs para strings + # Convert UUIDs to strings for key, value in details.items(): if isinstance(value, uuid.UUID): details[key] = str(value) @@ -64,7 +64,7 @@ def create_audit_log( db.refresh(audit_log) logger.info( - f"Audit log criado: {action} em {resource_type}" + + f"Audit log created: {action} in {resource_type}" + (f" (ID: {resource_id})" if resource_id else "") ) @@ -72,10 +72,10 @@ def create_audit_log( except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar registro de auditoria: {str(e)}") + logger.error(f"Error creating audit log: {str(e)}") return None except Exception as e: - logger.error(f"Erro inesperado ao criar registro de auditoria: {str(e)}") + logger.error(f"Unexpected error creating audit log: {str(e)}") return None def get_audit_logs( @@ -90,25 +90,25 @@ def get_audit_logs( end_date: Optional[datetime] = None ) -> List[AuditLog]: """ - Obtém registros de auditoria com filtros opcionais + Get audit logs with optional filters Args: - db: Sessão do banco de dados - skip: Número de registros para pular - limit: Número máximo de registros para retornar - user_id: Filtrar por ID do usuário - action: Filtrar por ação - resource_type: Filtrar por tipo de recurso - resource_id: Filtrar por ID do recurso - start_date: Data inicial - end_date: Data final + db: Database session + skip: Number of records to skip + limit: Maximum number of records to return + user_id: Filter by user ID + action: Filter by action + resource_type: Filter by resource type + resource_id: Filter by resource ID + start_date: Start date + end_date: End date Returns: - List[AuditLog]: Lista de registros de auditoria + List[AuditLog]: List of audit logs """ query = db.query(AuditLog) - # Aplicar filtros, se fornecidos + # Apply filters, if provided if user_id: query = query.filter(AuditLog.user_id == user_id) @@ -127,10 +127,10 @@ def get_audit_logs( if end_date: query = query.filter(AuditLog.created_at <= end_date) - # Ordenar por data de criação (mais recentes primeiro) + # Order by creation date (most recent first) query = query.order_by(AuditLog.created_at.desc()) - # Aplicar paginação + # Apply pagination query = query.offset(skip).limit(limit) return query.all() \ No newline at end of file diff --git a/src/services/auth_service.py b/src/services/auth_service.py index 8f6d3449..b1800e3a 100644 --- a/src/services/auth_service.py +++ b/src/services/auth_service.py @@ -1,63 +1,62 @@ from sqlalchemy.orm import Session from src.models.models import User from src.schemas.user import TokenData -from src.services.user_service import authenticate_user, get_user_by_email +from src.services.user_service import get_user_by_email from src.utils.security import create_jwt_token from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from src.config.settings import settings from src.config.database import get_db -from datetime import datetime, timedelta +from datetime import datetime import logging -from typing import Optional logger = logging.getLogger(__name__) -# Definir scheme de autenticação OAuth2 com password flow +# Define OAuth2 authentication scheme with password flow oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: """ - Obtém o usuário atual a partir do token JWT + Get the current user from the JWT token Args: - token: Token JWT - db: Sessão do banco de dados + token: JWT token + db: Database session Returns: - User: Usuário atual + User: Current user Raises: - HTTPException: Se o token for inválido ou o usuário não for encontrado + HTTPException: If the token is invalid or the user is not found """ credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Credenciais inválidas", + detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: - # Decodificar o token + # Decode the token payload = jwt.decode( token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM] ) - # Extrair dados do token + # Extract token data email: str = payload.get("sub") if email is None: - logger.warning("Token sem email (sub)") + logger.warning("Token without email (sub)") raise credentials_exception - # Verificar se o token expirou + # Check if the token has expired exp = payload.get("exp") if exp is None or datetime.fromtimestamp(exp) < datetime.utcnow(): - logger.warning(f"Token expirado para {email}") + logger.warning(f"Token expired for {email}") raise credentials_exception - # Criar objeto TokenData + # Create TokenData object token_data = TokenData( sub=email, exp=datetime.fromtimestamp(exp), @@ -66,85 +65,85 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De ) except JWTError as e: - logger.error(f"Erro ao decodificar token JWT: {str(e)}") + logger.error(f"Error decoding JWT token: {str(e)}") raise credentials_exception - # Buscar usuário no banco de dados + # Search for user in the database user = get_user_by_email(db, email=token_data.sub) if user is None: - logger.warning(f"Usuário não encontrado para o email: {token_data.sub}") + logger.warning(f"User not found for email: {token_data.sub}") raise credentials_exception if not user.is_active: - logger.warning(f"Tentativa de acesso com usuário inativo: {user.email}") + logger.warning(f"Attempt to access inactive user: {user.email}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Usuário inativo" + detail="Inactive user" ) return user async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: """ - Verifica se o usuário atual está ativo + Check if the current user is active Args: - current_user: Usuário atual + current_user: Current user Returns: - User: Usuário atual se estiver ativo + User: Current user if active Raises: - HTTPException: Se o usuário não estiver ativo + HTTPException: If the user is not active """ if not current_user.is_active: - logger.warning(f"Tentativa de acesso com usuário inativo: {current_user.email}") + logger.warning(f"Attempt to access inactive user: {current_user.email}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Usuário inativo" + detail="Inactive user" ) return current_user async def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User: """ - Verifica se o usuário atual é um administrador + Check if the current user is an administrator Args: - current_user: Usuário atual + current_user: Current user Returns: - User: Usuário atual se for administrador + User: Current user if administrator Raises: - HTTPException: Se o usuário não for administrador + HTTPException: If the user is not an administrator """ if not current_user.is_admin: - logger.warning(f"Tentativa de acesso admin por usuário não-admin: {current_user.email}") + logger.warning(f"Attempt to access admin by non-admin user: {current_user.email}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Permissão negada. Acesso restrito a administradores." + detail="Access denied. Restricted to administrators." ) return current_user def create_access_token(user: User) -> str: """ - Cria um token de acesso JWT para o usuário + Create a JWT access token for the user Args: - user: Usuário para o qual criar o token + user: User for which to create the token Returns: - str: Token JWT + str: JWT token """ - # Dados a serem incluídos no token + # Data to be included in the token token_data = { "sub": user.email, "is_admin": user.is_admin, } - # Incluir client_id apenas se não for administrador + # Include client_id only if not administrator and client_id is set if not user.is_admin and user.client_id: token_data["client_id"] = str(user.client_id) - # Criar token + # Create token return create_jwt_token(token_data) \ No newline at end of file diff --git a/src/services/client_service.py b/src/services/client_service.py index 8419aa34..609b00fe 100644 --- a/src/services/client_service.py +++ b/src/services/client_service.py @@ -12,50 +12,50 @@ import logging logger = logging.getLogger(__name__) def get_client(db: Session, client_id: uuid.UUID) -> Optional[Client]: - """Busca um cliente pelo ID""" + """Search for a client by ID""" try: client = db.query(Client).filter(Client.id == client_id).first() if not client: - logger.warning(f"Cliente não encontrado: {client_id}") + logger.warning(f"Client not found: {client_id}") return None return client except SQLAlchemyError as e: - logger.error(f"Erro ao buscar cliente {client_id}: {str(e)}") + logger.error(f"Error searching for client {client_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar cliente" + detail="Error searching for client" ) def get_clients(db: Session, skip: int = 0, limit: int = 100) -> List[Client]: - """Busca todos os clientes com paginação""" + """Search for all clients with pagination""" try: return db.query(Client).offset(skip).limit(limit).all() except SQLAlchemyError as e: - logger.error(f"Erro ao buscar clientes: {str(e)}") + logger.error(f"Error searching for clients: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar clientes" + detail="Error searching for clients" ) def create_client(db: Session, client: ClientCreate) -> Client: - """Cria um novo cliente""" + """Create a new client""" try: db_client = Client(**client.model_dump()) db.add(db_client) db.commit() db.refresh(db_client) - logger.info(f"Cliente criado com sucesso: {db_client.id}") + logger.info(f"Client created successfully: {db_client.id}") return db_client except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar cliente: {str(e)}") + logger.error(f"Error creating client: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao criar cliente" + detail="Error creating client" ) def update_client(db: Session, client_id: uuid.UUID, client: ClientCreate) -> Optional[Client]: - """Atualiza um cliente existente""" + """Updates an existing client""" try: db_client = get_client(db, client_id) if not db_client: @@ -66,18 +66,18 @@ def update_client(db: Session, client_id: uuid.UUID, client: ClientCreate) -> Op db.commit() db.refresh(db_client) - logger.info(f"Cliente atualizado com sucesso: {client_id}") + logger.info(f"Client updated successfully: {client_id}") return db_client except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao atualizar cliente {client_id}: {str(e)}") + logger.error(f"Error updating client {client_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao atualizar cliente" + detail="Error updating client" ) def delete_client(db: Session, client_id: uuid.UUID) -> bool: - """Remove um cliente""" + """Removes a client""" try: db_client = get_client(db, client_id) if not db_client: @@ -85,54 +85,54 @@ def delete_client(db: Session, client_id: uuid.UUID) -> bool: db.delete(db_client) db.commit() - logger.info(f"Cliente removido com sucesso: {client_id}") + logger.info(f"Client removed successfully: {client_id}") return True except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao remover cliente {client_id}: {str(e)}") + logger.error(f"Error removing client {client_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao remover cliente" + detail="Error removing client" ) def create_client_with_user(db: Session, client_data: ClientCreate, user_data: UserCreate) -> Tuple[Optional[Client], str]: """ - Cria um novo cliente com um usuário associado + Creates a new client with an associated user Args: - db: Sessão do banco de dados - client_data: Dados do cliente a ser criado - user_data: Dados do usuário a ser criado + db: Database session + client_data: Client data to be created + user_data: User data to be created Returns: - Tuple[Optional[Client], str]: Tupla com o cliente criado (ou None em caso de erro) e mensagem de status + Tuple[Optional[Client], str]: Tuple with the created client (or None in case of error) and status message """ try: - # Iniciar transação - primeiro cria o cliente + # Start transaction - first create the client client = Client(**client_data.model_dump()) db.add(client) - db.flush() # Obter o ID do cliente sem confirmar a transação + db.flush() # Get client ID without committing the transaction - # Usar o ID do cliente para criar o usuário associado + # Use client ID to create the associated user user, message = create_user(db, user_data, is_admin=False, client_id=client.id) if not user: - # Se houve erro na criação do usuário, fazer rollback + # If there was an error creating the user, rollback db.rollback() - logger.error(f"Erro ao criar usuário para o cliente: {message}") - return None, f"Erro ao criar usuário: {message}" + logger.error(f"Error creating user for client: {message}") + return None, f"Error creating user: {message}" - # Se tudo correu bem, confirmar a transação + # If everything went well, commit the transaction db.commit() - logger.info(f"Cliente e usuário criados com sucesso: {client.id}") - return client, "Cliente e usuário criados com sucesso" + logger.info(f"Client and user created successfully: {client.id}") + return client, "Client and user created successfully" except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar cliente com usuário: {str(e)}") - return None, f"Erro ao criar cliente com usuário: {str(e)}" + logger.error(f"Error creating client with user: {str(e)}") + return None, f"Error creating client with user: {str(e)}" except Exception as e: db.rollback() - logger.error(f"Erro inesperado ao criar cliente com usuário: {str(e)}") - return None, f"Erro inesperado: {str(e)}" \ No newline at end of file + logger.error(f"Unexpected error creating client with user: {str(e)}") + return None, f"Unexpected error: {str(e)}" \ No newline at end of file diff --git a/src/services/contact_service.py b/src/services/contact_service.py index 90aecaf5..7378a81c 100644 --- a/src/services/contact_service.py +++ b/src/services/contact_service.py @@ -10,50 +10,50 @@ import logging logger = logging.getLogger(__name__) def get_contact(db: Session, contact_id: uuid.UUID) -> Optional[Contact]: - """Busca um contato pelo ID""" + """Search for a contact by ID""" try: contact = db.query(Contact).filter(Contact.id == contact_id).first() if not contact: - logger.warning(f"Contato não encontrado: {contact_id}") + logger.warning(f"Contact not found: {contact_id}") return None return contact except SQLAlchemyError as e: - logger.error(f"Erro ao buscar contato {contact_id}: {str(e)}") + logger.error(f"Error searching for contact {contact_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar contato" + detail="Error searching for contact" ) def get_contacts_by_client(db: Session, client_id: uuid.UUID, skip: int = 0, limit: int = 100) -> List[Contact]: - """Busca contatos de um cliente com paginação""" + """Search for contacts of a client with pagination""" try: return db.query(Contact).filter(Contact.client_id == client_id).offset(skip).limit(limit).all() except SQLAlchemyError as e: - logger.error(f"Erro ao buscar contatos do cliente {client_id}: {str(e)}") + logger.error(f"Error searching for contacts of client {client_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar contatos" + detail="Error searching for contacts" ) def create_contact(db: Session, contact: ContactCreate) -> Contact: - """Cria um novo contato""" + """Create a new contact""" try: db_contact = Contact(**contact.model_dump()) db.add(db_contact) db.commit() db.refresh(db_contact) - logger.info(f"Contato criado com sucesso: {db_contact.id}") + logger.info(f"Contact created successfully: {db_contact.id}") return db_contact except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar contato: {str(e)}") + logger.error(f"Error creating contact: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao criar contato" + detail="Error creating contact" ) def update_contact(db: Session, contact_id: uuid.UUID, contact: ContactCreate) -> Optional[Contact]: - """Atualiza um contato existente""" + """Update an existing contact""" try: db_contact = get_contact(db, contact_id) if not db_contact: @@ -64,18 +64,18 @@ def update_contact(db: Session, contact_id: uuid.UUID, contact: ContactCreate) - db.commit() db.refresh(db_contact) - logger.info(f"Contato atualizado com sucesso: {contact_id}") + logger.info(f"Contact updated successfully: {contact_id}") return db_contact except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao atualizar contato {contact_id}: {str(e)}") + logger.error(f"Error updating contact {contact_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao atualizar contato" + detail="Error updating contact" ) def delete_contact(db: Session, contact_id: uuid.UUID) -> bool: - """Remove um contato""" + """Remove a contact""" try: db_contact = get_contact(db, contact_id) if not db_contact: @@ -83,12 +83,12 @@ def delete_contact(db: Session, contact_id: uuid.UUID) -> bool: db.delete(db_contact) db.commit() - logger.info(f"Contato removido com sucesso: {contact_id}") + logger.info(f"Contact removed successfully: {contact_id}") return True except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao remover contato {contact_id}: {str(e)}") + logger.error(f"Error removing contact {contact_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao remover contato" + detail="Error removing contact" ) \ No newline at end of file diff --git a/src/services/custom_tools.py b/src/services/custom_tools.py index ed01e43e..5b11cdb2 100644 --- a/src/services/custom_tools.py +++ b/src/services/custom_tools.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from google.adk.tools import FunctionTool import requests import json @@ -11,7 +11,7 @@ class CustomToolBuilder: self.tools = [] def _create_http_tool(self, tool_config: Dict[str, Any]) -> FunctionTool: - """Cria uma ferramenta HTTP baseada na configuração fornecida.""" + """Create an HTTP tool based on the provided configuration.""" name = tool_config["name"] description = tool_config["description"] endpoint = tool_config["endpoint"] @@ -23,35 +23,35 @@ class CustomToolBuilder: def http_tool(**kwargs): try: - # Combina valores padrão com valores fornecidos + # Combines default values with provided values all_values = {**values, **kwargs} - # Substitui placeholders nos headers + # Substitutes placeholders in headers processed_headers = { k: v.format(**all_values) if isinstance(v, str) else v for k, v in headers.items() } - # Processa path parameters + # Processes path parameters url = endpoint for param, value in parameters.get("path_params", {}).items(): if param in all_values: url = url.replace(f"{{{param}}}", str(all_values[param])) - # Processa query parameters + # Process query parameters query_params = {} for param, value in parameters.get("query_params", {}).items(): if isinstance(value, list): - # Se o valor for uma lista, junta com vírgula + # If the value is a list, join with comma query_params[param] = ",".join(value) elif param in all_values: - # Se o parâmetro estiver nos valores, usa o valor fornecido + # If the parameter is in the values, use the provided value query_params[param] = all_values[param] else: - # Caso contrário, usa o valor padrão da configuração + # Otherwise, use the default value from the configuration query_params[param] = value - # Adiciona valores padrão aos query params se não estiverem presentes + # Adds default values to query params if they are not present for param, value in values.items(): if param not in query_params and param not in parameters.get("path_params", {}): query_params[param] = value @@ -62,12 +62,12 @@ class CustomToolBuilder: if param in all_values: body_data[param] = all_values[param] - # Adiciona valores padrão ao body se não estiverem presentes + # Adds default values to body if they are not present for param, value in values.items(): if param not in body_data and param not in query_params and param not in parameters.get("path_params", {}): body_data[param] = value - # Faz a requisição HTTP + # Makes the HTTP request response = requests.request( method=method, url=url, @@ -79,64 +79,64 @@ class CustomToolBuilder: if response.status_code >= 400: raise requests.exceptions.HTTPError( - f"Erro na requisição: {response.status_code} - {response.text}" + f"Error in the request: {response.status_code} - {response.text}" ) - # Sempre retorna a resposta como string + # Always returns the response as a string return json.dumps(response.json()) except Exception as e: - logger.error(f"Erro ao executar ferramenta {name}: {str(e)}") + logger.error(f"Error executing tool {name}: {str(e)}") return json.dumps(error_handling.get("fallback_response", { "error": "tool_execution_error", "message": str(e) })) - # Adiciona docstring dinâmica baseada na configuração + # Adds dynamic docstring based on the configuration param_docs = [] - # Adiciona path parameters + # Adds path parameters for param, value in parameters.get("path_params", {}).items(): param_docs.append(f"{param}: {value}") - # Adiciona query parameters + # Adds query parameters for param, value in parameters.get("query_params", {}).items(): if isinstance(value, list): param_docs.append(f"{param}: List[{', '.join(value)}]") else: param_docs.append(f"{param}: {value}") - # Adiciona body parameters + # Adds body parameters for param, param_config in parameters.get("body_params", {}).items(): - required = "Obrigatório" if param_config.get("required", False) else "Opcional" + required = "Required" if param_config.get("required", False) else "Optional" param_docs.append(f"{param} ({param_config['type']}, {required}): {param_config['description']}") - # Adiciona valores padrão + # Adds default values if values: - param_docs.append("\nValores padrão:") + param_docs.append("\nDefault values:") for param, value in values.items(): param_docs.append(f"{param}: {value}") http_tool.__doc__ = f""" {description} - Parâmetros: + Parameters: {chr(10).join(param_docs)} - Retorna: - String contendo a resposta em formato JSON + Returns: + String containing the response in JSON format """ - # Define o nome da função para ser usado pelo ADK + # Defines the function name to be used by the ADK http_tool.__name__ = name return FunctionTool(func=http_tool) def build_tools(self, tools_config: Dict[str, Any]) -> List[FunctionTool]: - """Constrói uma lista de ferramentas baseada na configuração fornecida.""" + """Builds a list of tools based on the provided configuration.""" self.tools = [] - # Processa ferramentas HTTP + # Processes HTTP tools for http_tool_config in tools_config.get("http_tools", []): self.tools.append(self._create_http_tool(http_tool_config)) diff --git a/src/services/mcp_server_service.py b/src/services/mcp_server_service.py index 6d223ed1..2f89abaa 100644 --- a/src/services/mcp_server_service.py +++ b/src/services/mcp_server_service.py @@ -10,50 +10,50 @@ import logging logger = logging.getLogger(__name__) def get_mcp_server(db: Session, server_id: uuid.UUID) -> Optional[MCPServer]: - """Busca um servidor MCP pelo ID""" + """Search for an MCP server by ID""" try: server = db.query(MCPServer).filter(MCPServer.id == server_id).first() if not server: - logger.warning(f"Servidor MCP não encontrado: {server_id}") + logger.warning(f"MCP server not found: {server_id}") return None return server except SQLAlchemyError as e: - logger.error(f"Erro ao buscar servidor MCP {server_id}: {str(e)}") + logger.error(f"Error searching for MCP server {server_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar servidor MCP" + detail="Error searching for MCP server" ) def get_mcp_servers(db: Session, skip: int = 0, limit: int = 100) -> List[MCPServer]: - """Busca todos os servidores MCP com paginação""" + """Search for all MCP servers with pagination""" try: return db.query(MCPServer).offset(skip).limit(limit).all() except SQLAlchemyError as e: - logger.error(f"Erro ao buscar servidores MCP: {str(e)}") + logger.error(f"Error searching for MCP servers: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar servidores MCP" + detail="Error searching for MCP servers" ) def create_mcp_server(db: Session, server: MCPServerCreate) -> MCPServer: - """Cria um novo servidor MCP""" + """Create a new MCP server""" try: db_server = MCPServer(**server.model_dump()) db.add(db_server) db.commit() db.refresh(db_server) - logger.info(f"Servidor MCP criado com sucesso: {db_server.id}") + logger.info(f"MCP server created successfully: {db_server.id}") return db_server except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar servidor MCP: {str(e)}") + logger.error(f"Error creating MCP server: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao criar servidor MCP" + detail="Error creating MCP server" ) def update_mcp_server(db: Session, server_id: uuid.UUID, server: MCPServerCreate) -> Optional[MCPServer]: - """Atualiza um servidor MCP existente""" + """Update an existing MCP server""" try: db_server = get_mcp_server(db, server_id) if not db_server: @@ -64,18 +64,18 @@ def update_mcp_server(db: Session, server_id: uuid.UUID, server: MCPServerCreate db.commit() db.refresh(db_server) - logger.info(f"Servidor MCP atualizado com sucesso: {server_id}") + logger.info(f"MCP server updated successfully: {server_id}") return db_server except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao atualizar servidor MCP {server_id}: {str(e)}") + logger.error(f"Error updating MCP server {server_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao atualizar servidor MCP" + detail="Error updating MCP server" ) def delete_mcp_server(db: Session, server_id: uuid.UUID) -> bool: - """Remove um servidor MCP""" + """Remove an MCP server""" try: db_server = get_mcp_server(db, server_id) if not db_server: @@ -83,12 +83,12 @@ def delete_mcp_server(db: Session, server_id: uuid.UUID) -> bool: db.delete(db_server) db.commit() - logger.info(f"Servidor MCP removido com sucesso: {server_id}") + logger.info(f"MCP server removed successfully: {server_id}") return True except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao remover servidor MCP {server_id}: {str(e)}") + logger.error(f"Error removing MCP server {server_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao remover servidor MCP" + detail="Error removing MCP server" ) \ No newline at end of file diff --git a/src/services/mcp_service.py b/src/services/mcp_service.py index 407834e8..c65a68e7 100644 --- a/src/services/mcp_service.py +++ b/src/services/mcp_service.py @@ -6,7 +6,6 @@ from google.adk.tools.mcp_tool.mcp_toolset import ( ) from contextlib import AsyncExitStack import os -import logging from src.utils.logger import setup_logger from src.services.mcp_server_service import get_mcp_server from sqlalchemy.orm import Session @@ -19,21 +18,21 @@ class MCPService: self.exit_stack = AsyncExitStack() async def _connect_to_mcp_server(self, server_config: Dict[str, Any]) -> Tuple[List[Any], Optional[AsyncExitStack]]: - """Conecta a um servidor MCP específico e retorna suas ferramentas.""" + """Connect to a specific MCP server and return its tools.""" try: - # Determina o tipo de servidor (local ou remoto) + # Determines the type of server (local or remote) if "url" in server_config: - # Servidor remoto (SSE) + # Remote server (SSE) connection_params = SseServerParams( url=server_config["url"], headers=server_config.get("headers", {}) ) else: - # Servidor local (Stdio) + # Local server (Stdio) command = server_config.get("command", "npx") args = server_config.get("args", []) - # Adiciona variáveis de ambiente se especificadas + # Adds environment variables if specified env = server_config.get("env", {}) if env: for key, value in env.items(): @@ -51,13 +50,13 @@ class MCPService: return tools, exit_stack except Exception as e: - logger.error(f"Erro ao conectar ao servidor MCP: {e}") + logger.error(f"Error connecting to MCP server: {e}") return [], None def _filter_incompatible_tools(self, tools: List[Any]) -> List[Any]: - """Filtra ferramentas incompatíveis com o modelo.""" + """Filters incompatible tools with the model.""" problematic_tools = [ - "create_pull_request_review", # Esta ferramenta causa o erro 400 INVALID_ARGUMENT + "create_pull_request_review", # This tool causes the 400 INVALID_ARGUMENT error ] filtered_tools = [] @@ -65,43 +64,43 @@ class MCPService: for tool in tools: if tool.name in problematic_tools: - logger.warning(f"Removendo ferramenta incompatível: {tool.name}") + logger.warning(f"Removing incompatible tool: {tool.name}") removed_count += 1 else: filtered_tools.append(tool) if removed_count > 0: - logger.warning(f"Removidas {removed_count} ferramentas incompatíveis.") + logger.warning(f"Removed {removed_count} incompatible tools.") return filtered_tools def _filter_tools_by_agent(self, tools: List[Any], agent_tools: List[str]) -> List[Any]: - """Filtra ferramentas compatíveis com o agente.""" + """Filters tools compatible with the agent.""" filtered_tools = [] for tool in tools: - logger.info(f"Ferramenta: {tool.name}") + logger.info(f"Tool: {tool.name}") if tool.name in agent_tools: filtered_tools.append(tool) return filtered_tools async def build_tools(self, mcp_config: Dict[str, Any], db: Session) -> Tuple[List[Any], AsyncExitStack]: - """Constrói uma lista de ferramentas a partir de múltiplos servidores MCP.""" + """Builds a list of tools from multiple MCP servers.""" self.tools = [] self.exit_stack = AsyncExitStack() - # Processa cada servidor MCP da configuração + # Process each MCP server in the configuration for server in mcp_config.get("mcp_servers", []): try: - # Busca o servidor MCP no banco + # Search for the MCP server in the database mcp_server = get_mcp_server(db, server['id']) if not mcp_server: logger.warning(f"Servidor MCP não encontrado: {server['id']}") continue - # Prepara a configuração do servidor + # Prepares the server configuration server_config = mcp_server.config_json.copy() - # Substitui as variáveis de ambiente no config_json + # Replaces the environment variables in the config_json if 'env' in server_config: for key, value in server_config['env'].items(): if value.startswith('env@@'): @@ -109,31 +108,31 @@ class MCPService: if env_key in server.get('envs', {}): server_config['env'][key] = server['envs'][env_key] else: - logger.warning(f"Variável de ambiente '{env_key}' não fornecida para o servidor MCP {mcp_server.name}") + logger.warning(f"Environment variable '{env_key}' not provided for the MCP server {mcp_server.name}") continue - logger.info(f"Conectando ao servidor MCP: {mcp_server.name}") + logger.info(f"Connecting to MCP server: {mcp_server.name}") tools, exit_stack = await self._connect_to_mcp_server(server_config) if tools and exit_stack: - # Filtra ferramentas incompatíveis + # Filters incompatible tools filtered_tools = self._filter_incompatible_tools(tools) - # Filtra ferramentas compatíveis com o agente + # Filters tools compatible with the agent agent_tools = server.get('tools', []) filtered_tools = self._filter_tools_by_agent(filtered_tools, agent_tools) self.tools.extend(filtered_tools) - # Registra o exit_stack com o AsyncExitStack + # Registers the exit_stack with the AsyncExitStack await self.exit_stack.enter_async_context(exit_stack) - logger.info(f"Conectado com sucesso. Adicionadas {len(filtered_tools)} ferramentas.") + logger.info(f"Connected successfully. Added {len(filtered_tools)} tools.") else: - logger.warning(f"Falha na conexão ou nenhuma ferramenta disponível para {mcp_server.name}") + logger.warning(f"Failed to connect or no tools available for {mcp_server.name}") except Exception as e: - logger.error(f"Erro ao conectar ao servidor MCP {server['id']}: {e}") + logger.error(f"Error connecting to MCP server {server['id']}: {e}") continue - logger.info(f"MCP Toolset criado com sucesso. Total de {len(self.tools)} ferramentas.") + logger.info(f"MCP Toolset created successfully. Total of {len(self.tools)} tools.") return self.tools, self.exit_stack \ No newline at end of file diff --git a/src/services/session_service.py b/src/services/session_service.py index 25fa6b2c..f4095677 100644 --- a/src/services/session_service.py +++ b/src/services/session_service.py @@ -7,8 +7,7 @@ from typing import Optional, List from fastapi import HTTPException, status from sqlalchemy.exc import SQLAlchemyError -from src.services.agent_service import get_agent, get_agents_by_client -from src.services.contact_service import get_contact +from src.services.agent_service import get_agents_by_client import uuid import logging @@ -20,7 +19,7 @@ def get_sessions_by_client( db: Session, client_id: uuid.UUID, ) -> List[SessionModel]: - """Busca sessões de um cliente com paginação""" + """Search for sessions of a client with pagination""" try: agents_by_client = get_agents_by_client(db, client_id) sessions = [] @@ -29,10 +28,10 @@ def get_sessions_by_client( return sessions except SQLAlchemyError as e: - logger.error(f"Erro ao buscar sessões do cliente {client_id}: {str(e)}") + logger.error(f"Error searching for sessions of client {client_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar sessões", + detail="Error searching for sessions", ) @@ -42,38 +41,38 @@ def get_sessions_by_agent( skip: int = 0, limit: int = 100, ) -> List[SessionModel]: - """Busca sessões de um agente com paginação""" + """Search for sessions of an agent with pagination""" try: agent_id_str = str(agent_id) query = db.query(SessionModel).filter(SessionModel.app_name == agent_id_str) return query.offset(skip).limit(limit).all() except SQLAlchemyError as e: - logger.error(f"Erro ao buscar sessões do agente {agent_id_str}: {str(e)}") + logger.error(f"Error searching for sessions of agent {agent_id_str}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar sessões", + detail="Error searching for sessions", ) def get_session_by_id( session_service: DatabaseSessionService, session_id: str ) -> Optional[SessionADK]: - """Busca uma sessão pelo ID""" + """Search for a session by ID""" try: if not session_id or "_" not in session_id: - logger.error(f"ID de sessão inválido: {session_id}") + logger.error(f"Invalid session ID: {session_id}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="ID de sessão inválido. Formato esperado: app_name_user_id", + detail="Invalid session ID. Expected format: app_name_user_id", ) parts = session_id.split("_", 1) if len(parts) != 2: - logger.error(f"Formato de ID de sessão inválido: {session_id}") + logger.error(f"Invalid session ID format: {session_id}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Formato de ID de sessão inválido. Formato esperado: app_name_user_id", + detail="Invalid session ID format. Expected format: app_name_user_id", ) user_id, app_name = parts @@ -85,28 +84,28 @@ def get_session_by_id( ) if session is None: - logger.error(f"Sessão não encontrada: {session_id}") + logger.error(f"Session not found: {session_id}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Sessão não encontrada: {session_id}", + detail=f"Session not found: {session_id}", ) return session except Exception as e: - logger.error(f"Erro ao buscar sessão {session_id}: {str(e)}") + logger.error(f"Error searching for session {session_id}: {str(e)}") if isinstance(e, HTTPException): raise e raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Erro ao buscar sessão: {str(e)}", + detail=f"Error searching for session: {str(e)}", ) def delete_session(session_service: DatabaseSessionService, session_id: str) -> None: - """Deleta uma sessão pelo ID""" + """Deletes a session by ID""" try: session = get_session_by_id(session_service, session_id) - # Se chegou aqui, a sessão existe (get_session_by_id já valida) + # If we get here, the session exists (get_session_by_id already validates) session_service.delete_session( app_name=session.app_name, @@ -115,34 +114,34 @@ def delete_session(session_service: DatabaseSessionService, session_id: str) -> ) return None except HTTPException: - # Repassa exceções HTTP do get_session_by_id + # Passes HTTP exceptions from get_session_by_id raise except Exception as e: - logger.error(f"Erro ao deletar sessão {session_id}: {str(e)}") + logger.error(f"Error deleting session {session_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Erro ao deletar sessão: {str(e)}", + detail=f"Error deleting session: {str(e)}", ) def get_session_events( session_service: DatabaseSessionService, session_id: str ) -> List[Event]: - """Busca os eventos de uma sessão pelo ID""" + """Search for the events of a session by ID""" try: session = get_session_by_id(session_service, session_id) - # Se chegou aqui, a sessão existe (get_session_by_id já valida) + # If we get here, the session exists (get_session_by_id already validates) if not hasattr(session, 'events') or session.events is None: return [] return session.events except HTTPException: - # Repassa exceções HTTP do get_session_by_id + # Passes HTTP exceptions from get_session_by_id raise except Exception as e: - logger.error(f"Erro ao buscar eventos da sessão {session_id}: {str(e)}") + logger.error(f"Error searching for events of session {session_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Erro ao buscar eventos da sessão: {str(e)}", + detail=f"Error searching for events of session: {str(e)}", ) diff --git a/src/services/tool_service.py b/src/services/tool_service.py index 6b89f593..00af61c6 100644 --- a/src/services/tool_service.py +++ b/src/services/tool_service.py @@ -10,50 +10,50 @@ import logging logger = logging.getLogger(__name__) def get_tool(db: Session, tool_id: uuid.UUID) -> Optional[Tool]: - """Busca uma ferramenta pelo ID""" + """Search for a tool by ID""" try: tool = db.query(Tool).filter(Tool.id == tool_id).first() if not tool: - logger.warning(f"Ferramenta não encontrada: {tool_id}") + logger.warning(f"Tool not found: {tool_id}") return None return tool except SQLAlchemyError as e: - logger.error(f"Erro ao buscar ferramenta {tool_id}: {str(e)}") + logger.error(f"Error searching for tool {tool_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar ferramenta" + detail="Error searching for tool" ) def get_tools(db: Session, skip: int = 0, limit: int = 100) -> List[Tool]: - """Busca todas as ferramentas com paginação""" + """Search for all tools with pagination""" try: return db.query(Tool).offset(skip).limit(limit).all() except SQLAlchemyError as e: - logger.error(f"Erro ao buscar ferramentas: {str(e)}") + logger.error(f"Error searching for tools: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao buscar ferramentas" + detail="Error searching for tools" ) def create_tool(db: Session, tool: ToolCreate) -> Tool: - """Cria uma nova ferramenta""" + """Creates a new tool""" try: db_tool = Tool(**tool.model_dump()) db.add(db_tool) db.commit() db.refresh(db_tool) - logger.info(f"Ferramenta criada com sucesso: {db_tool.id}") + logger.info(f"Tool created successfully: {db_tool.id}") return db_tool except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar ferramenta: {str(e)}") + logger.error(f"Error creating tool: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao criar ferramenta" + detail="Error creating tool" ) def update_tool(db: Session, tool_id: uuid.UUID, tool: ToolCreate) -> Optional[Tool]: - """Atualiza uma ferramenta existente""" + """Updates an existing tool""" try: db_tool = get_tool(db, tool_id) if not db_tool: @@ -64,18 +64,18 @@ def update_tool(db: Session, tool_id: uuid.UUID, tool: ToolCreate) -> Optional[T db.commit() db.refresh(db_tool) - logger.info(f"Ferramenta atualizada com sucesso: {tool_id}") + logger.info(f"Tool updated successfully: {tool_id}") return db_tool except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao atualizar ferramenta {tool_id}: {str(e)}") + logger.error(f"Error updating tool {tool_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao atualizar ferramenta" + detail="Error updating tool" ) def delete_tool(db: Session, tool_id: uuid.UUID) -> bool: - """Remove uma ferramenta""" + """Remove a tool""" try: db_tool = get_tool(db, tool_id) if not db_tool: @@ -83,12 +83,12 @@ def delete_tool(db: Session, tool_id: uuid.UUID) -> bool: db.delete(db_tool) db.commit() - logger.info(f"Ferramenta removida com sucesso: {tool_id}") + logger.info(f"Tool removed successfully: {tool_id}") return True except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao remover ferramenta {tool_id}: {str(e)}") + logger.error(f"Error removing tool {tool_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Erro ao remover ferramenta" + detail="Error removing tool" ) \ No newline at end of file diff --git a/src/services/user_service.py b/src/services/user_service.py index d78d9b65..c87ead6f 100644 --- a/src/services/user_service.py +++ b/src/services/user_service.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session from sqlalchemy.exc import SQLAlchemyError from src.models.models import User, Client -from src.schemas.user import UserCreate, UserResponse +from src.schemas.user import UserCreate from src.utils.security import get_password_hash, verify_password, generate_token from src.services.email_service import send_verification_email, send_password_reset_email from datetime import datetime, timedelta @@ -13,47 +13,47 @@ logger = logging.getLogger(__name__) def create_user(db: Session, user_data: UserCreate, is_admin: bool = False, client_id: Optional[uuid.UUID] = None) -> Tuple[Optional[User], str]: """ - Cria um novo usuário no sistema + Creates a new user in the system Args: - db: Sessão do banco de dados - user_data: Dados do usuário a ser criado - is_admin: Se o usuário é um administrador - client_id: ID do cliente associado (opcional, será criado um novo se não fornecido) + db: Database session + user_data: User data to be created + is_admin: If the user is an administrator + client_id: Associated client ID (optional, a new one will be created if not provided) Returns: - Tuple[Optional[User], str]: Tupla com o usuário criado (ou None em caso de erro) e mensagem de status + Tuple[Optional[User], str]: Tuple with the created user (or None in case of error) and status message """ try: - # Verificar se email já existe + # Check if email already exists db_user = db.query(User).filter(User.email == user_data.email).first() if db_user: - logger.warning(f"Tentativa de cadastro com email já existente: {user_data.email}") - return None, "Email já cadastrado" + logger.warning(f"Attempt to register with existing email: {user_data.email}") + return None, "Email already registered" - # Criar token de verificação + # Create verification token verification_token = generate_token() token_expiry = datetime.utcnow() + timedelta(hours=24) - # Iniciar transação + # Start transaction user = None local_client_id = client_id try: - # Se não for admin e não tiver client_id, criar um cliente associado + # If not admin and no client_id, create an associated client if not is_admin and local_client_id is None: client = Client(name=user_data.name) db.add(client) - db.flush() # Obter o ID do cliente + db.flush() # Get the client ID local_client_id = client.id - # Criar usuário + # Create user user = User( email=user_data.email, password_hash=get_password_hash(user_data.password), client_id=local_client_id, is_admin=is_admin, - is_active=False, # Inativo até verificar email + is_active=False, # Inactive until email is verified email_verified=False, verification_token=verification_token, verification_token_expiry=token_expiry @@ -61,248 +61,248 @@ def create_user(db: Session, user_data: UserCreate, is_admin: bool = False, clie db.add(user) db.commit() - # Enviar email de verificação + # Send verification email email_sent = send_verification_email(user.email, verification_token) if not email_sent: - logger.error(f"Falha ao enviar email de verificação para {user.email}") - # Não fazemos rollback aqui, apenas logamos o erro + logger.error(f"Failed to send verification email to {user.email}") + # We don't do rollback here, we just log the error - logger.info(f"Usuário criado com sucesso: {user.email}") - return user, "Usuário criado com sucesso. Verifique seu email para ativar sua conta." + logger.info(f"User created successfully: {user.email}") + return user, "User created successfully. Check your email to activate your account." except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao criar usuário: {str(e)}") - return None, f"Erro ao criar usuário: {str(e)}" + logger.error(f"Error creating user: {str(e)}") + return None, f"Error creating user: {str(e)}" except Exception as e: - logger.error(f"Erro inesperado ao criar usuário: {str(e)}") - return None, f"Erro inesperado: {str(e)}" + logger.error(f"Unexpected error creating user: {str(e)}") + return None, f"Unexpected error: {str(e)}" def verify_email(db: Session, token: str) -> Tuple[bool, str]: """ - Verifica o email do usuário usando o token fornecido + Verify the user's email using the provided token Args: - db: Sessão do banco de dados - token: Token de verificação + db: Database session + token: Verification token Returns: - Tuple[bool, str]: Tupla com status da verificação e mensagem + Tuple[bool, str]: Tuple with verification status and message """ try: - # Buscar usuário pelo token + # Search for user by token user = db.query(User).filter(User.verification_token == token).first() if not user: - logger.warning(f"Tentativa de verificação com token inválido: {token}") - return False, "Token de verificação inválido" + logger.warning(f"Attempt to verify with invalid token: {token}") + return False, "Invalid verification token" - # Verificar se o token expirou + # Check if the token has expired now = datetime.utcnow() expiry = user.verification_token_expiry - # Garantir que ambas as datas sejam do mesmo tipo (aware ou naive) + # Ensure both dates are of the same type (aware or naive) if expiry.tzinfo is not None and now.tzinfo is None: - # Se expiry tem fuso e now não, converter now para ter fuso + # If expiry has timezone and now doesn't, convert now to have timezone now = now.replace(tzinfo=expiry.tzinfo) elif now.tzinfo is not None and expiry.tzinfo is None: - # Se now tem fuso e expiry não, converter expiry para ter fuso + # If now has timezone and expiry doesn't, convert expiry to have timezone expiry = expiry.replace(tzinfo=now.tzinfo) if expiry < now: - logger.warning(f"Tentativa de verificação com token expirado para usuário: {user.email}") - return False, "Token de verificação expirado" + logger.warning(f"Attempt to verify with expired token for user: {user.email}") + return False, "Verification token expired" - # Atualizar usuário + # Update user user.email_verified = True user.is_active = True user.verification_token = None user.verification_token_expiry = None db.commit() - logger.info(f"Email verificado com sucesso para usuário: {user.email}") - return True, "Email verificado com sucesso. Sua conta está ativa." + logger.info(f"Email verified successfully for user: {user.email}") + return True, "Email verified successfully. Your account is active." except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao verificar email: {str(e)}") - return False, f"Erro ao verificar email: {str(e)}" + logger.error(f"Error verifying email: {str(e)}") + return False, f"Error verifying email: {str(e)}" except Exception as e: - logger.error(f"Erro inesperado ao verificar email: {str(e)}") - return False, f"Erro inesperado: {str(e)}" + logger.error(f"Unexpected error verifying email: {str(e)}") + return False, f"Unexpected error: {str(e)}" def resend_verification(db: Session, email: str) -> Tuple[bool, str]: """ - Reenvia o email de verificação + Resend the verification email Args: - db: Sessão do banco de dados - email: Email do usuário + db: Database session + email: User email Returns: - Tuple[bool, str]: Tupla com status da operação e mensagem + Tuple[bool, str]: Tuple with operation status and message """ try: - # Buscar usuário pelo email + # Search for user by email user = db.query(User).filter(User.email == email).first() if not user: - logger.warning(f"Tentativa de reenvio de verificação para email inexistente: {email}") - return False, "Email não encontrado" + logger.warning(f"Attempt to resend verification email for non-existent email: {email}") + return False, "Email not found" if user.email_verified: - logger.info(f"Tentativa de reenvio de verificação para email já verificado: {email}") - return False, "Email já foi verificado" + logger.info(f"Attempt to resend verification email for already verified email: {email}") + return False, "Email already verified" - # Gerar novo token + # Generate new token verification_token = generate_token() token_expiry = datetime.utcnow() + timedelta(hours=24) - # Atualizar usuário + # Update user user.verification_token = verification_token user.verification_token_expiry = token_expiry db.commit() - # Enviar email + # Send email email_sent = send_verification_email(user.email, verification_token) if not email_sent: - logger.error(f"Falha ao reenviar email de verificação para {user.email}") - return False, "Falha ao enviar email de verificação" + logger.error(f"Failed to resend verification email to {user.email}") + return False, "Failed to send verification email" - logger.info(f"Email de verificação reenviado com sucesso para: {user.email}") - return True, "Email de verificação reenviado. Verifique sua caixa de entrada." + logger.info(f"Verification email resent successfully to: {user.email}") + return True, "Verification email resent. Check your inbox." except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao reenviar verificação: {str(e)}") - return False, f"Erro ao reenviar verificação: {str(e)}" + logger.error(f"Error resending verification: {str(e)}") + return False, f"Error resending verification: {str(e)}" except Exception as e: - logger.error(f"Erro inesperado ao reenviar verificação: {str(e)}") - return False, f"Erro inesperado: {str(e)}" + logger.error(f"Unexpected error resending verification: {str(e)}") + return False, f"Unexpected error: {str(e)}" def forgot_password(db: Session, email: str) -> Tuple[bool, str]: """ - Inicia o processo de recuperação de senha + Initiates the password recovery process Args: - db: Sessão do banco de dados - email: Email do usuário + db: Database session + email: User email Returns: - Tuple[bool, str]: Tupla com status da operação e mensagem + Tuple[bool, str]: Tuple with operation status and message """ try: - # Buscar usuário pelo email + # Search for user by email user = db.query(User).filter(User.email == email).first() if not user: - # Por segurança, não informamos se o email existe ou não - logger.info(f"Tentativa de recuperação de senha para email inexistente: {email}") - return True, "Se o email estiver cadastrado, você receberá instruções para redefinir sua senha." + # For security, we don't inform if the email exists or not + logger.info(f"Attempt to recover password for non-existent email: {email}") + return True, "If the email is registered, you will receive instructions to reset your password." - # Gerar token de reset + # Generate reset token reset_token = generate_token() - token_expiry = datetime.utcnow() + timedelta(hours=1) # Token válido por 1 hora + token_expiry = datetime.utcnow() + timedelta(hours=1) # Token valid for 1 hour - # Atualizar usuário + # Update user user.password_reset_token = reset_token user.password_reset_expiry = token_expiry db.commit() - # Enviar email + # Send email email_sent = send_password_reset_email(user.email, reset_token) if not email_sent: - logger.error(f"Falha ao enviar email de recuperação de senha para {user.email}") - return False, "Falha ao enviar email de recuperação de senha" + logger.error(f"Failed to send password reset email to {user.email}") + return False, "Failed to send password reset email" - logger.info(f"Email de recuperação de senha enviado com sucesso para: {user.email}") - return True, "Se o email estiver cadastrado, você receberá instruções para redefinir sua senha." + logger.info(f"Password reset email sent successfully to: {user.email}") + return True, "If the email is registered, you will receive instructions to reset your password." except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao processar recuperação de senha: {str(e)}") - return False, f"Erro ao processar recuperação de senha: {str(e)}" + logger.error(f"Error processing password recovery: {str(e)}") + return False, f"Error processing password recovery: {str(e)}" except Exception as e: - logger.error(f"Erro inesperado ao processar recuperação de senha: {str(e)}") - return False, f"Erro inesperado: {str(e)}" + logger.error(f"Unexpected error processing password recovery: {str(e)}") + return False, f"Unexpected error: {str(e)}" def reset_password(db: Session, token: str, new_password: str) -> Tuple[bool, str]: """ - Redefine a senha do usuário usando o token fornecido + Resets the user's password using the provided token Args: - db: Sessão do banco de dados - token: Token de redefinição de senha - new_password: Nova senha + db: Database session + token: Password reset token + new_password: New password Returns: - Tuple[bool, str]: Tupla com status da operação e mensagem + Tuple[bool, str]: Tuple with operation status and message """ try: - # Buscar usuário pelo token + # Search for user by token user = db.query(User).filter(User.password_reset_token == token).first() if not user: - logger.warning(f"Tentativa de redefinição de senha com token inválido: {token}") - return False, "Token de redefinição de senha inválido" + logger.warning(f"Attempt to reset password with invalid token: {token}") + return False, "Invalid password reset token" - # Verificar se o token expirou + # Check if the token has expired if user.password_reset_expiry < datetime.utcnow(): - logger.warning(f"Tentativa de redefinição de senha com token expirado para usuário: {user.email}") - return False, "Token de redefinição de senha expirado" + logger.warning(f"Attempt to reset password with expired token for user: {user.email}") + return False, "Password reset token expired" - # Atualizar senha + # Update password user.password_hash = get_password_hash(new_password) user.password_reset_token = None user.password_reset_expiry = None db.commit() - logger.info(f"Senha redefinida com sucesso para usuário: {user.email}") - return True, "Senha redefinida com sucesso. Você já pode fazer login com sua nova senha." + logger.info(f"Password reset successfully for user: {user.email}") + return True, "Password reset successfully. You can now login with your new password." except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao redefinir senha: {str(e)}") - return False, f"Erro ao redefinir senha: {str(e)}" + logger.error(f"Error resetting password: {str(e)}") + return False, f"Error resetting password: {str(e)}" except Exception as e: - logger.error(f"Erro inesperado ao redefinir senha: {str(e)}") - return False, f"Erro inesperado: {str(e)}" + logger.error(f"Unexpected error resetting password: {str(e)}") + return False, f"Unexpected error: {str(e)}" def get_user_by_email(db: Session, email: str) -> Optional[User]: """ - Busca um usuário pelo email + Searches for a user by email Args: - db: Sessão do banco de dados - email: Email do usuário + db: Database session + email: User email Returns: - Optional[User]: Usuário encontrado ou None + Optional[User]: User found or None """ try: return db.query(User).filter(User.email == email).first() except Exception as e: - logger.error(f"Erro ao buscar usuário por email: {str(e)}") + logger.error(f"Error searching for user by email: {str(e)}") return None def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: """ - Autentica um usuário com email e senha + Authenticates a user with email and password Args: - db: Sessão do banco de dados - email: Email do usuário - password: Senha do usuário + db: Database session + email: User email + password: User password Returns: - Optional[User]: Usuário autenticado ou None + Optional[User]: Authenticated user or None """ user = get_user_by_email(db, email) if not user: @@ -315,73 +315,73 @@ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: def get_admin_users(db: Session, skip: int = 0, limit: int = 100): """ - Lista os usuários administradores + Lists the admin users Args: - db: Sessão do banco de dados - skip: Número de registros para pular - limit: Número máximo de registros para retornar + db: Database session + skip: Number of records to skip + limit: Maximum number of records to return Returns: - List[User]: Lista de usuários administradores + List[User]: List of admin users """ try: users = db.query(User).filter(User.is_admin == True).offset(skip).limit(limit).all() - logger.info(f"Listagem de administradores: {len(users)} encontrados") + logger.info(f"List of admins: {len(users)} found") return users except SQLAlchemyError as e: - logger.error(f"Erro ao listar administradores: {str(e)}") + logger.error(f"Error listing admins: {str(e)}") return [] except Exception as e: - logger.error(f"Erro inesperado ao listar administradores: {str(e)}") + logger.error(f"Unexpected error listing admins: {str(e)}") return [] def create_admin_user(db: Session, user_data: UserCreate) -> Tuple[Optional[User], str]: """ - Cria um novo usuário administrador + Creates a new admin user Args: - db: Sessão do banco de dados - user_data: Dados do usuário a ser criado + db: Database session + user_data: User data to be created Returns: - Tuple[Optional[User], str]: Tupla com o usuário criado (ou None em caso de erro) e mensagem de status + Tuple[Optional[User], str]: Tuple with the created user (or None in case of error) and status message """ return create_user(db, user_data, is_admin=True) def deactivate_user(db: Session, user_id: uuid.UUID) -> Tuple[bool, str]: """ - Desativa um usuário (não exclui, apenas marca como inativo) + Deactivates a user (does not delete, only marks as inactive) Args: - db: Sessão do banco de dados - user_id: ID do usuário a ser desativado + db: Database session + user_id: ID of the user to be deactivated Returns: - Tuple[bool, str]: Tupla com status da operação e mensagem + Tuple[bool, str]: Tuple with operation status and message """ try: - # Buscar usuário pelo ID + # Search for user by ID user = db.query(User).filter(User.id == user_id).first() if not user: - logger.warning(f"Tentativa de desativação de usuário inexistente: {user_id}") - return False, "Usuário não encontrado" + logger.warning(f"Attempt to deactivate non-existent user: {user_id}") + return False, "User not found" - # Desativar usuário + # Deactivate user user.is_active = False db.commit() - logger.info(f"Usuário desativado com sucesso: {user.email}") - return True, "Usuário desativado com sucesso" + logger.info(f"User deactivated successfully: {user.email}") + return True, "User deactivated successfully" except SQLAlchemyError as e: db.rollback() - logger.error(f"Erro ao desativar usuário: {str(e)}") - return False, f"Erro ao desativar usuário: {str(e)}" + logger.error(f"Error deactivating user: {str(e)}") + return False, f"Error deactivating user: {str(e)}" except Exception as e: - logger.error(f"Erro inesperado ao desativar usuário: {str(e)}") - return False, f"Erro inesperado: {str(e)}" \ No newline at end of file + logger.error(f"Unexpected error deactivating user: {str(e)}") + return False, f"Unexpected error: {str(e)}" \ No newline at end of file diff --git a/src/utils/logger.py b/src/utils/logger.py index 48e08107..879e47a4 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -1,11 +1,10 @@ import logging import os import sys -from typing import Optional from src.config.settings import settings class CustomFormatter(logging.Formatter): - """Formatação personalizada para logs""" + """Custom formatter for logs""" grey = "\x1b[38;20m" yellow = "\x1b[33;20m" @@ -30,31 +29,31 @@ class CustomFormatter(logging.Formatter): def setup_logger(name: str) -> logging.Logger: """ - Configura um logger personalizado + Configures a custom logger Args: - name: Nome do logger + name: Logger name Returns: logging.Logger: Logger configurado """ logger = logging.getLogger(name) - # Remove handlers existentes para evitar duplicação + # Remove existing handlers to avoid duplication if logger.handlers: logger.handlers.clear() - # Configura o nível do logger baseado na variável de ambiente ou configuração + # Configure the logger level based on the environment variable or configuration log_level = getattr(logging, os.getenv("LOG_LEVEL", settings.LOG_LEVEL).upper()) logger.setLevel(log_level) - # Handler para console + # Console handler console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(CustomFormatter()) console_handler.setLevel(log_level) logger.addHandler(console_handler) - # Impede que os logs sejam propagados para o logger root + # Prevent logs from being propagated to the root logger logger.propagate = False return logger \ No newline at end of file diff --git a/src/utils/security.py b/src/utils/security.py index ecedb0c6..1f77016f 100644 --- a/src/utils/security.py +++ b/src/utils/security.py @@ -10,7 +10,7 @@ from dataclasses import dataclass logger = logging.getLogger(__name__) -# Corrigir erro do bcrypt com passlib +# Fix bcrypt error with passlib if not hasattr(bcrypt, '__about__'): @dataclass class BcryptAbout: @@ -18,19 +18,19 @@ if not hasattr(bcrypt, '__about__'): setattr(bcrypt, "__about__", BcryptAbout()) -# Contexto para hash de senhas usando bcrypt +# Context for password hashing using bcrypt pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def get_password_hash(password: str) -> str: - """Cria um hash da senha fornecida""" + """Creates a password hash""" return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str) -> bool: - """Verifica se a senha fornecida corresponde ao hash armazenado""" + """Verifies if the provided password matches the stored hash""" return pwd_context.verify(plain_password, hashed_password) def create_jwt_token(data: dict, expires_delta: timedelta = None) -> str: - """Cria um token JWT""" + """Creates a JWT token""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta @@ -45,7 +45,7 @@ def create_jwt_token(data: dict, expires_delta: timedelta = None) -> str: return encoded_jwt def generate_token(length: int = 32) -> str: - """Gera um token seguro para verificação de email ou reset de senha""" + """Generates a secure token for email verification or password reset""" alphabet = string.ascii_letters + string.digits token = ''.join(secrets.choice(alphabet) for _ in range(length)) return token \ No newline at end of file