translate to english

This commit is contained in:
Davidson Gomes 2025-04-28 20:04:51 -03:00
parent c3baa54215
commit 215e76012e
54 changed files with 3269 additions and 3460 deletions

View File

@ -55,6 +55,14 @@ src/
## Code Standards ## 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) ### Schemas (Pydantic)
- Use `BaseModel` as base for all schemas - Use `BaseModel` as base for all schemas
- Define fields with explicit types - Define fields with explicit types

View File

@ -1,38 +1,38 @@
# Configurações do banco de dados # Database settings
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="INFO" LOG_LEVEL="INFO"
LOG_DIR="logs" LOG_DIR="logs"
# Configurações do Redis # Redis settings
REDIS_HOST="localhost" REDIS_HOST="localhost"
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_DB=0 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 TOOLS_CACHE_TTL=3600
# Configurações JWT # JWT settings
JWT_SECRET_KEY="sua-chave-secreta-jwt" JWT_SECRET_KEY="your-jwt-secret-key"
JWT_ALGORITHM="HS256" JWT_ALGORITHM="HS256"
# Em minutos # In minutes
JWT_EXPIRATION_TIME=30 JWT_EXPIRATION_TIME=30
# SendGrid # SendGrid
SENDGRID_API_KEY="sua-sendgrid-api-key" SENDGRID_API_KEY="your-sendgrid-api-key"
EMAIL_FROM="noreply@yourdomain.com" EMAIL_FROM="noreply@yourdomain.com"
APP_URL="https://yourdomain.com" APP_URL="https://yourdomain.com"
# Configurações do Servidor # Server settings
HOST="0.0.0.0" HOST="0.0.0.0"
PORT=8000 PORT=8000
DEBUG=false DEBUG=false
# Configurações de Seeders # Seeder settings
ADMIN_EMAIL="admin@evoai.com" ADMIN_EMAIL="admin@evoai.com"
ADMIN_INITIAL_PASSWORD="senhaforte123" ADMIN_INITIAL_PASSWORD="strongpassword123"
DEMO_EMAIL="demo@exemplo.com" DEMO_EMAIL="demo@example.com"
DEMO_PASSWORD="demo123" DEMO_PASSWORD="demo123"
DEMO_CLIENT_NAME="Cliente Demo" DEMO_CLIENT_NAME="Demo Client"

1029
README.md

File diff suppressed because it is too large Load Diff

19
docs/README.md Normal file
View File

@ -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.

View File

@ -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.

View File

@ -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!

213
docs/technical/API_FLOW.md Normal file
View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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 <container_id>
# Kubernetes
kubectl logs <pod_name>
```
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.

View File

@ -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"
}
]
}

View File

@ -1,6 +1,6 @@
""" """
Script principal para executar todos os seeders em sequência. Main script to run all seeders in sequence.
Verifica as dependências entre os seeders e executa na ordem correta. Checks dependencies between seeders and runs them in the correct order.
""" """
import os import os
@ -9,11 +9,11 @@ import logging
import argparse import argparse
from dotenv import load_dotenv from dotenv import load_dotenv
# Configurar logging # Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Importar seeders # Import seeders
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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.admin_seeder import create_admin_user
from scripts.seeders.client_seeder import create_demo_client_and_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 from scripts.seeders.contact_seeder import create_demo_contacts
def setup_environment(): def setup_environment():
"""Configura o ambiente para os seeders""" """Configure the environment for seeders"""
load_dotenv() 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"] required_vars = ["POSTGRES_CONNECTION_STRING"]
missing_vars = [var for var in required_vars if not os.getenv(var)] missing_vars = [var for var in required_vars if not os.getenv(var)]
if missing_vars: 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 False
return True return True
def run_seeders(seeders): def run_seeders(seeders):
""" """
Executa os seeders especificados Run the specified seeders
Args: Args:
seeders (list): Lista de seeders para executar seeders (list): List of seeders to run
Returns: 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 = { all_seeders = {
"admin": create_admin_user, "admin": create_admin_user,
@ -55,58 +55,58 @@ def run_seeders(seeders):
"contacts": create_demo_contacts "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"] 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: if not seeders:
seeders = seeder_order seeders = seeder_order
else: 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] invalid_seeders = [s for s in seeders if s not in all_seeders]
if invalid_seeders: if invalid_seeders:
logger.error(f"Seeders inválidos: {', '.join(invalid_seeders)}") logger.error(f"Invalid seeders: {', '.join(invalid_seeders)}")
logger.info(f"Seeders disponíveis: {', '.join(all_seeders.keys())}") logger.info(f"Available seeders: {', '.join(all_seeders.keys())}")
return False 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] seeders = [s for s in seeder_order if s in seeders]
# Executar seeders # Run seeders
success = True success = True
for seeder_name in seeders: for seeder_name in seeders:
logger.info(f"Executando seeder: {seeder_name}") logger.info(f"Running seeder: {seeder_name}")
try: try:
seeder_func = all_seeders[seeder_name] seeder_func = all_seeders[seeder_name]
if not seeder_func(): if not seeder_func():
logger.error(f"Falha ao executar seeder: {seeder_name}") logger.error(f"Failed to run seeder: {seeder_name}")
success = False success = False
except Exception as e: 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 success = False
return success return success
def main(): def main():
"""Função principal""" """Main function"""
parser = argparse.ArgumentParser(description='Executa seeders para popular o banco de dados') parser = argparse.ArgumentParser(description='Run seeders to populate the database')
parser.add_argument('--seeders', nargs='+', help='Seeders para executar (admin, client, agents, mcp_servers, tools, contacts)') parser.add_argument('--seeders', nargs='+', help='Seeders to run (admin, client, agents, mcp_servers, tools, contacts)')
args = parser.parse_args() args = parser.parse_args()
# Configurar ambiente # Configure environment
if not setup_environment(): if not setup_environment():
sys.exit(1) sys.exit(1)
# Executar seeders # Run seeders
success = run_seeders(args.seeders) success = run_seeders(args.seeders)
# Saída # Output
if success: if success:
logger.info("Todos os seeders foram executados com sucesso") logger.info("All seeders were executed successfully")
sys.exit(0) sys.exit(0)
else: else:
logger.error("Houve erros ao executar os seeders") logger.error("There were errors running the seeders")
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,7 +1,7 @@
""" """
Script para criar um usuário administrador inicial: Script to create an initial admin user:
- Email: admin@evoai.com - 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_admin: True
- is_active: True - is_active: True
- email_verified: True - email_verified: True
@ -21,27 +21,27 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_admin_user(): def create_admin_user():
"""Cria um usuário administrador inicial no sistema""" """Create an initial admin user in the system"""
try: try:
# Carregar variáveis de ambiente # Load environment variables
load_dotenv() load_dotenv()
# Obter configurações do banco de dados # Get database settings
db_url = os.getenv("POSTGRES_CONNECTION_STRING") db_url = os.getenv("POSTGRES_CONNECTION_STRING")
if not db_url: 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 return False
# Obter senha do administrador # Get admin password
admin_password = os.getenv("ADMIN_INITIAL_PASSWORD") admin_password = os.getenv("ADMIN_INITIAL_PASSWORD")
if not admin_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 return False
# Configuração do email do admin # Admin email configuration
admin_email = os.getenv("ADMIN_EMAIL", "admin@evoai.com") admin_email = os.getenv("ADMIN_EMAIL", "admin@evoai.com")
# Conectar ao banco de dados # Connect to the database
engine = create_engine(db_url) engine = create_engine(db_url)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
@ -49,10 +49,10 @@ def create_admin_user():
# Verificar se o administrador já existe # Verificar se o administrador já existe
existing_admin = session.query(User).filter(User.email == admin_email).first() existing_admin = session.query(User).filter(User.email == admin_email).first()
if existing_admin: if existing_admin:
logger.info(f"Administrador com email {admin_email} já existe") logger.info(f"Admin with email {admin_email} already exists")
return True return True
# Criar administrador # Create admin
admin_user = User( admin_user = User(
email=admin_email, email=admin_email,
password_hash=get_password_hash(admin_password), password_hash=get_password_hash(admin_password),
@ -61,15 +61,15 @@ def create_admin_user():
email_verified=True email_verified=True
) )
# Adicionar e comitar # Add and commit
session.add(admin_user) session.add(admin_user)
session.commit() session.commit()
logger.info(f"Administrador criado com sucesso: {admin_email}") logger.info(f"Admin created successfully: {admin_email}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Erro ao criar administrador: {str(e)}") logger.error(f"Error creating admin: {str(e)}")
return False return False
finally: finally:
session.close() session.close()

View File

@ -1,9 +1,9 @@
""" """
Script para criar agentes de exemplo para o cliente demo: Script to create example agents for the demo client:
- Agente Atendimento: configurado para responder perguntas gerais - Agent Support: configured to answer general questions
- Agente Vendas: configurado para responder sobre produtos - Agent Sales: configured to answer about products
- Agente FAQ: configurado para responder perguntas frequentes - Agent FAQ: configured to answer frequently asked questions
Cada agente com instruções e configurações pré-definidas Each agent with pre-defined instructions and configurations
""" """
import os import os
@ -21,18 +21,18 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_demo_agents(): def create_demo_agents():
"""Cria agentes de exemplo para o cliente demo""" """Create example agents for the demo client"""
try: try:
# Carregar variáveis de ambiente # Load environment variables
load_dotenv() load_dotenv()
# Obter configurações do banco de dados # Get database settings
db_url = os.getenv("POSTGRES_CONNECTION_STRING") db_url = os.getenv("POSTGRES_CONNECTION_STRING")
if not db_url: 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 return False
# Conectar ao banco de dados # Connect to the database
engine = create_engine(db_url) engine = create_engine(db_url)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
@ -43,7 +43,7 @@ def create_demo_agents():
demo_user = session.query(User).filter(User.email == demo_email).first() demo_user = session.query(User).filter(User.email == demo_email).first()
if not demo_user or not demo_user.client_id: 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 return False
client_id = demo_user.client_id client_id = demo_user.client_id
@ -51,79 +51,75 @@ def create_demo_agents():
# Verificar se já existem agentes para este cliente # Verificar se já existem agentes para este cliente
existing_agents = session.query(Agent).filter(Agent.client_id == client_id).all() existing_agents = session.query(Agent).filter(Agent.client_id == client_id).all()
if existing_agents: 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 return True
# Definições dos agentes de exemplo # Example agent definitions
agents = [ agents = [
{ {
"name": "Atendimento_Geral", "name": "Support_Agent",
"description": "Agente para atendimento geral e dúvidas básicas", "description": "Agent for general support and basic questions",
"type": "llm", "type": "llm",
"model": "gpt-3.5-turbo", "model": "gpt-4.1-nano",
"api_key": "${OPENAI_API_KEY}", # Será substituído pela variável de ambiente "api_key": "your-api-key-here",
"instruction": """ "instruction": """
Você é um assistente de atendimento ao cliente da empresa. You are a customer support agent.
Seja cordial, objetivo e eficiente. Responda às dúvidas dos clientes Be friendly, objective and efficient. Answer customer questions
de forma clara e sucinta. Se não souber a resposta, informe que irá in a clear and concise manner. If you don't know the answer,
consultar um especialista e retornará em breve. inform that you will consult a specialist and return soon.
""", """,
"config": { "config": {
"temperature": 0.7, "tools": [],
"max_tokens": 500, "mcp_servers": [],
"tools": [] "custom_tools": [],
"sub_agents": []
} }
}, },
{ {
"name": "Vendas_Produtos", "name": "Sales_Products",
"description": "Agente especializado em vendas e informações sobre produtos", "description": "Specialized agent in sales and information about products",
"type": "llm", "type": "llm",
"model": "claude-3-sonnet-20240229", "model": "gpt-4.1-nano",
"api_key": "${ANTHROPIC_API_KEY}", # Será substituído pela variável de ambiente "api_key": "your-api-key-here",
"instruction": """ "instruction": """
Você é um especialista em vendas da empresa. You are a sales specialist.
Seu objetivo é fornecer informações detalhadas sobre produtos, Your goal is to provide detailed information about products,
comparar diferentes opções, destacar benefícios e vantagens competitivas. compare different options, highlight benefits and competitive advantages.
Use uma linguagem persuasiva mas honesta, e sempre busque entender Use a persuasive but honest language, and always seek to understand
as necessidades do cliente antes de recomendar um produto. the customer's needs before recommending a product.
""", """,
"config": { "config": {
"temperature": 0.8, "tools": [],
"max_tokens": 800, "mcp_servers": [],
"tools": ["web_search"] "custom_tools": [],
"sub_agents": []
} }
}, },
{ {
"name": "FAQ_Bot", "name": "FAQ_Bot",
"description": "Agente para responder perguntas frequentes", "description": "Agent for answering frequently asked questions",
"type": "llm", "type": "llm",
"model": "gemini-pro", "model": "gpt-4.1-nano",
"api_key": "${GOOGLE_API_KEY}", # Será substituído pela variável de ambiente "api_key": "your-api-key-here",
"instruction": """ "instruction": """
Você é um assistente especializado em responder perguntas frequentes. You are a specialized agent for answering frequently asked questions.
Suas respostas devem ser diretas, objetivas e baseadas nas informações Your answers should be direct, objective and based on the information
da empresa. Utilize uma linguagem simples e acessível. Se a pergunta of the company. Use a simple and accessible language. If the question
não estiver relacionada às FAQs disponíveis, direcione o cliente para is not related to the available FAQs, redirect the client to the
o canal de atendimento adequado. appropriate support channel.
""", """,
"config": { "config": {
"temperature": 0.5, "tools": [],
"max_tokens": 400, "mcp_servers": [],
"tools": [] "custom_tools": [],
"sub_agents": []
} }
} }
] ]
# Criar os agentes # Create the agents
for agent_data in agents: for agent_data in agents:
# Substituir placeholders de API Keys por variáveis de ambiente quando disponíveis # Create the agent
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", "")
agent = Agent( agent = Agent(
client_id=client_id, client_id=client_id,
name=agent_data["name"], name=agent_data["name"],
@ -136,19 +132,19 @@ def create_demo_agents():
) )
session.add(agent) 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() 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 return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
session.rollback() 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 return False
except Exception as e: 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 return False
finally: finally:
session.close() session.close()

View File

@ -1,9 +1,9 @@
""" """
Script para criar um cliente de exemplo: Script to create a demo client:
- Nome: Cliente Demo - Name: Demo Client
- Com usuário associado: - With associated user:
- Email: demo@exemplo.com - Email: demo@example.com
- Senha: demo123 (ou definida em variável de ambiente) - Password: demo123 (or defined in environment variable)
- is_admin: False - is_admin: False
- is_active: True - is_active: True
- email_verified: True - email_verified: True
@ -24,42 +24,42 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_demo_client_and_user(): 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: try:
# Carregar variáveis de ambiente # Load environment variables
load_dotenv() load_dotenv()
# Obter configurações do banco de dados # Get database settings
db_url = os.getenv("POSTGRES_CONNECTION_STRING") db_url = os.getenv("POSTGRES_CONNECTION_STRING")
if not db_url: 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 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") demo_password = os.getenv("DEMO_PASSWORD", "demo123")
# Configurações do cliente e usuário demo # Demo client and user settings
demo_client_name = os.getenv("DEMO_CLIENT_NAME", "Cliente Demo") demo_client_name = os.getenv("DEMO_CLIENT_NAME", "Demo Client")
demo_email = os.getenv("DEMO_EMAIL", "demo@exemplo.com") demo_email = os.getenv("DEMO_EMAIL", "demo@example.com")
# Conectar ao banco de dados # Connect to the database
engine = create_engine(db_url) engine = create_engine(db_url)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
try: 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() existing_user = session.query(User).filter(User.email == demo_email).first()
if existing_user: 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 return True
# Criar cliente demo # Create demo client
demo_client = Client(name=demo_client_name) demo_client = Client(name=demo_client_name)
session.add(demo_client) 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( demo_user = User(
email=demo_email, email=demo_email,
password_hash=get_password_hash(demo_password), password_hash=get_password_hash(demo_password),
@ -69,21 +69,21 @@ def create_demo_client_and_user():
email_verified=True email_verified=True
) )
# Adicionar e comitar # Add and commit
session.add(demo_user) session.add(demo_user)
session.commit() session.commit()
logger.info(f"Cliente demo '{demo_client_name}' criado com sucesso") logger.info(f"Demo client '{demo_client_name}' created successfully")
logger.info(f"Usuário demo criado com sucesso: {demo_email}") logger.info(f"Demo user created successfully: {demo_email}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
session.rollback() 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 return False
except Exception as e: 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 return False
finally: finally:
session.close() session.close()

View File

@ -1,8 +1,8 @@
""" """
Script para criar contatos de exemplo para o cliente demo: Script to create example contacts for the demo client:
- Contatos com histórico de conversas - Contacts with conversation history
- Diferentes perfis de cliente - Different client profiles
- Dados fictícios para demonstração - Fake data for demonstration
""" """
import os import os
@ -15,7 +15,7 @@ from sqlalchemy.exc import SQLAlchemyError
from dotenv import load_dotenv from dotenv import load_dotenv
from src.models.models import Contact, User, Client from src.models.models import Contact, User, Client
# Configurar logging # Configure logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
) )
@ -23,46 +23,46 @@ logger = logging.getLogger(__name__)
def create_demo_contacts(): def create_demo_contacts():
"""Cria contatos de exemplo para o cliente demo""" """Create example contacts for the demo client"""
try: try:
# Carregar variáveis de ambiente # Load environment variables
load_dotenv() load_dotenv()
# Obter configurações do banco de dados # Get database settings
db_url = os.getenv("POSTGRES_CONNECTION_STRING") db_url = os.getenv("POSTGRES_CONNECTION_STRING")
if not db_url: 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 return False
# Conectar ao banco de dados # Connect to the database
engine = create_engine(db_url) engine = create_engine(db_url)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
try: try:
# Obter o cliente demo pelo email do usuário # Get demo client by user email
demo_email = os.getenv("DEMO_EMAIL", "demo@exemplo.com") demo_email = os.getenv("DEMO_EMAIL", "demo@example.com")
demo_user = session.query(User).filter(User.email == demo_email).first() demo_user = session.query(User).filter(User.email == demo_email).first()
if not demo_user or not demo_user.client_id: if not demo_user or not demo_user.client_id:
logger.error( 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 return False
client_id = demo_user.client_id 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 = ( existing_contacts = (
session.query(Contact).filter(Contact.client_id == client_id).all() session.query(Contact).filter(Contact.client_id == client_id).all()
) )
if existing_contacts: if existing_contacts:
logger.info( 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 return True
# Definições dos contatos de exemplo # Example contact definitions
contacts = [ contacts = [
{ {
"name": "Maria Silva", "name": "Maria Silva",
@ -145,7 +145,7 @@ def create_demo_contacts():
}, },
] ]
# Criar os contatos # Create the contacts
for contact_data in contacts: for contact_data in contacts:
contact = Contact( contact = Contact(
client_id=client_id, client_id=client_id,
@ -156,24 +156,24 @@ def create_demo_contacts():
session.add(contact) session.add(contact)
logger.info( 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() session.commit()
logger.info( 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 return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
session.rollback() session.rollback()
logger.error( 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 return False
except Exception as e: 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 return False
finally: finally:
session.close() session.close()

View File

@ -1,10 +1,10 @@
""" """
Script para criar servidores MCP padrão: Script to create default MCP servers:
- Servidor Anthropic Claude - Anthropic Claude server
- Servidor OpenAI GPT - OpenAI GPT server
- Servidor Google Gemini - Google Gemini server
- Servidor Ollama (local) - Ollama (local) server
Cada um com configurações padrão para produção Each with default production configurations
""" """
import os import os
@ -16,7 +16,7 @@ from sqlalchemy.exc import SQLAlchemyError
from dotenv import load_dotenv from dotenv import load_dotenv
from src.models.models import MCPServer from src.models.models import MCPServer
# Configurar logging # Configure logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
) )
@ -24,32 +24,32 @@ logger = logging.getLogger(__name__)
def create_mcp_servers(): def create_mcp_servers():
"""Cria servidores MCP padrão no sistema""" """Create default MCP servers in the system"""
try: try:
# Carregar variáveis de ambiente # Load environment variables
load_dotenv() load_dotenv()
# Obter configurações do banco de dados # Get database settings
db_url = os.getenv("POSTGRES_CONNECTION_STRING") db_url = os.getenv("POSTGRES_CONNECTION_STRING")
if not db_url: 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 return False
# Conectar ao banco de dados # Connect to the database
engine = create_engine(db_url) engine = create_engine(db_url)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
try: try:
# Verificar se já existem servidores MCP # Check if there are already MCP servers
existing_servers = session.query(MCPServer).all() existing_servers = session.query(MCPServer).all()
if existing_servers: if existing_servers:
logger.info( logger.info(
f"Já existem {len(existing_servers)} servidores MCP cadastrados" f"There are already {len(existing_servers)} MCP servers registered"
) )
return True return True
# Definições dos servidores MCP # MCP servers definitions
mcp_servers = [ mcp_servers = [
{ {
"name": "Sequential Thinking", "name": "Sequential Thinking",
@ -62,7 +62,7 @@ def create_mcp_servers():
], ],
}, },
"environments": {}, "environments": {},
"tools": ["sequential_thinking"], "tools": ["sequentialthinking"],
"type": "community", "type": "community",
"id": "4519dd69-9343-4792-af51-dc4d322fb0c9", "id": "4519dd69-9343-4792-af51-dc4d322fb0c9",
"created_at": "2025-04-28T15:14:16.901236Z", "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: for server_data in mcp_servers:
server = MCPServer( server = MCPServer(
name=server_data["name"], name=server_data["name"],
@ -192,19 +192,19 @@ def create_mcp_servers():
) )
session.add(server) 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() session.commit()
logger.info("Todos os servidores MCP foram criados com sucesso") logger.info("All MCP servers were created successfully")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
session.rollback() 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 return False
except Exception as e: 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 return False
finally: finally:
session.close() session.close()

View File

@ -1,10 +1,7 @@
""" """
Script para criar ferramentas padrão: Script to create default tools:
- Pesquisa Web -
- Consulta a Documentos Each with basic configurations for demonstration
- Consulta a Base de Conhecimento
- Integração WhatsApp/Telegram
Cada uma com configurações básicas para demonstração
""" """
import os import os
@ -16,38 +13,38 @@ from sqlalchemy.exc import SQLAlchemyError
from dotenv import load_dotenv from dotenv import load_dotenv
from src.models.models import Tool from src.models.models import Tool
# Configurar logging # Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_tools(): def create_tools():
"""Cria ferramentas padrão no sistema""" """Cria ferramentas padrão no sistema"""
try: try:
# Carregar variáveis de ambiente # Load environment variables
load_dotenv() load_dotenv()
# Obter configurações do banco de dados # Get database settings
db_url = os.getenv("POSTGRES_CONNECTION_STRING") db_url = os.getenv("POSTGRES_CONNECTION_STRING")
if not db_url: 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 return False
# Conectar ao banco de dados # Connect to the database
engine = create_engine(db_url) engine = create_engine(db_url)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
try: try:
# Verificar se já existem ferramentas # Check if there are already tools
existing_tools = session.query(Tool).all() existing_tools = session.query(Tool).all()
if existing_tools: 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 return True
# Definições das ferramentas # Tools definitions
tools = [] tools = []
# Criar as ferramentas # Create the tools
for tool_data in tools: for tool_data in tools:
tool = Tool( tool = Tool(
@ -58,19 +55,19 @@ def create_tools():
) )
session.add(tool) session.add(tool)
logger.info(f"Ferramenta '{tool_data['name']}' criada com sucesso") logger.info(f"Tool '{tool_data['name']}' created successfully")
session.commit() session.commit()
logger.info("Todas as ferramentas foram criadas com sucesso") logger.info("All tools were created successfully")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
session.rollback() 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 return False
except Exception as e: except Exception as e:
logger.error(f"Erro ao criar ferramentas: {str(e)}") logger.error(f"Error when creating tools: {str(e)}")
return False return False
finally: finally:
session.close() session.close()

View File

@ -13,12 +13,12 @@ from src.schemas.user import UserResponse, AdminUserCreate
router = APIRouter( router = APIRouter(
prefix="/admin", prefix="/admin",
tags=["administração"], tags=["admin"],
dependencies=[Depends(verify_admin)], # Todas as rotas requerem permissão de admin dependencies=[Depends(verify_admin)],
responses={403: {"description": "Permissão negada"}}, responses={403: {"description": "Permission denied"}},
) )
# Rotas para auditoria # Audit routes
@router.get("/audit-logs", response_model=List[AuditLogResponse]) @router.get("/audit-logs", response_model=List[AuditLogResponse])
async def read_audit_logs( async def read_audit_logs(
filters: AuditLogFilter = Depends(), filters: AuditLogFilter = Depends(),
@ -26,15 +26,15 @@ async def read_audit_logs(
payload: dict = Depends(get_jwt_token), payload: dict = Depends(get_jwt_token),
): ):
""" """
Obter logs de auditoria com filtros opcionais Get audit logs with optional filters
Args: Args:
filters: Filtros para busca de logs filters: Filters for log search
db: Sessão do banco de dados db: Database session
payload: Payload do token JWT payload: JWT token payload
Returns: Returns:
List[AuditLogResponse]: Lista de logs de auditoria List[AuditLogResponse]: List of audit logs
""" """
return get_audit_logs( return get_audit_logs(
db, db,
@ -48,7 +48,7 @@ async def read_audit_logs(
end_date=filters.end_date end_date=filters.end_date
) )
# Rotas para administradores # Admin routes
@router.get("/users", response_model=List[UserResponse]) @router.get("/users", response_model=List[UserResponse])
async def read_admin_users( async def read_admin_users(
skip: int = 0, skip: int = 0,
@ -57,16 +57,16 @@ async def read_admin_users(
payload: dict = Depends(get_jwt_token), payload: dict = Depends(get_jwt_token),
): ):
""" """
Listar usuários administradores List admin users
Args: Args:
skip: Número de registros para pular skip: Number of records to skip
limit: Número máximo de registros para retornar limit: Maximum number of records to return
db: Sessão do banco de dados db: Database session
payload: Payload do token JWT payload: JWT token payload
Returns: Returns:
List[UserResponse]: Lista de usuários administradores List[UserResponse]: List of admin users
""" """
return get_admin_users(db, skip, limit) return get_admin_users(db, skip, limit)
@ -78,29 +78,29 @@ async def create_new_admin_user(
payload: dict = Depends(get_jwt_token), payload: dict = Depends(get_jwt_token),
): ):
""" """
Criar um novo usuário administrador Create a new admin user
Args: Args:
user_data: Dados do usuário a ser criado user_data: User data to be created
request: Objeto Request do FastAPI request: FastAPI Request object
db: Sessão do banco de dados db: Database session
payload: Payload do token JWT payload: JWT token payload
Returns: Returns:
UserResponse: Dados do usuário criado UserResponse: Created user data
Raises: 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") user_id = payload.get("user_id")
if not user_id: if not user_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, 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) user, message = create_admin_user(db, user_data)
if not user: if not user:
raise HTTPException( raise HTTPException(
@ -108,7 +108,7 @@ async def create_new_admin_user(
detail=message detail=message
) )
# Registrar ação no log de auditoria # Register action in audit log
create_audit_log( create_audit_log(
db, db,
user_id=uuid.UUID(user_id), user_id=uuid.UUID(user_id),
@ -129,33 +129,33 @@ async def deactivate_admin_user(
payload: dict = Depends(get_jwt_token), 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: Args:
user_id: ID do usuário a ser desativado user_id: ID of the user to be deactivated
request: Objeto Request do FastAPI request: FastAPI Request object
db: Sessão do banco de dados db: Database session
payload: Payload do token JWT payload: JWT token payload
Raises: 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") current_user_id = payload.get("user_id")
if not current_user_id: if not current_user_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, 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: if str(user_id) == current_user_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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) success, message = deactivate_user(db, user_id)
if not success: if not success:
raise HTTPException( raise HTTPException(
@ -163,7 +163,7 @@ async def deactivate_admin_user(
detail=message detail=message
) )
# Registrar ação no log de auditoria # Register action in audit log
create_audit_log( create_audit_log(
db, db,
user_id=uuid.UUID(current_user_id), user_id=uuid.UUID(current_user_id),

121
src/api/agent_routes.py Normal file
View File

@ -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"
)

View File

@ -30,8 +30,8 @@ logger = logging.getLogger(__name__)
router = APIRouter( router = APIRouter(
prefix="/auth", prefix="/auth",
tags=["autenticação"], tags=["authentication"],
responses={404: {"description": "Não encontrado"}}, responses={404: {"description": "Not found"}},
) )
@ -40,17 +40,17 @@ router = APIRouter(
) )
async def register_user(user_data: UserCreate, db: Session = Depends(get_db)): 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: Args:
user_data: Dados do usuário a ser registrado user_data: User data to be registered
db: Sessão do banco de dados db: Database session
Returns: Returns:
UserResponse: Dados do usuário criado UserResponse: Created user data
Raises: 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) user, message = create_user(db, user_data, is_admin=False)
if not user: if not user:
@ -70,19 +70,19 @@ async def register_admin(
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
""" """
Registra um novo administrador no sistema. Register a new admin in the system.
Apenas administradores existentes podem criar novos administradores. Only existing admins can create new admins.
Args: Args:
user_data: Dados do administrador a ser registrado user_data: Admin data to be registered
db: Sessão do banco de dados db: Database session
current_admin: Administrador atual (autenticado) current_admin: Current admin (authenticated)
Returns: Returns:
UserResponse: Dados do administrador criado UserResponse: Created admin data
Raises: 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) user, message = create_user(db, user_data, is_admin=True)
if not user: if not user:
@ -90,7 +90,7 @@ async def register_admin(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
logger.info( 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 return user
@ -98,24 +98,24 @@ async def register_admin(
@router.get("/verify-email/{token}", response_model=MessageResponse) @router.get("/verify-email/{token}", response_model=MessageResponse)
async def verify_user_email(token: str, db: Session = Depends(get_db)): 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: Args:
token: Token de verificação token: Verification token
db: Sessão do banco de dados db: Database session
Returns: Returns:
MessageResponse: Mensagem de sucesso MessageResponse: Success message
Raises: Raises:
HTTPException: Se o token for inválido ou expirado HTTPException: If the token is invalid or expired
""" """
success, message = verify_email(db, token) success, message = verify_email(db, token)
if not success: 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) 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} return {"message": message}
@ -124,55 +124,55 @@ async def resend_verification_email(
email_data: ForgotPassword, db: Session = Depends(get_db) 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: Args:
email_data: Email do usuário email_data: User email
db: Sessão do banco de dados db: Database session
Returns: Returns:
MessageResponse: Mensagem de sucesso MessageResponse: Success message
Raises: Raises:
HTTPException: Se houver erro no reenvio HTTPException: If there is an error in resending
""" """
success, message = resend_verification(db, email_data.email) success, message = resend_verification(db, email_data.email)
if not success: 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) 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} return {"message": message}
@router.post("/login", response_model=TokenResponse) @router.post("/login", response_model=TokenResponse)
async def login_for_access_token(form_data: UserLogin, db: Session = Depends(get_db)): 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: Args:
form_data: Dados de login (email e senha) form_data: Login data (email and password)
db: Sessão do banco de dados db: Database session
Returns: Returns:
TokenResponse: Token de acesso e tipo TokenResponse: Access token and type
Raises: Raises:
HTTPException: Se as credenciais forem inválidas HTTPException: If credentials are invalid
""" """
user = authenticate_user(db, form_data.email, form_data.password) user = authenticate_user(db, form_data.email, form_data.password)
if not user: if not user:
logger.warning( logger.warning(
f"Tentativa de login com credenciais inválidas: {form_data.email}" f"Login attempt with invalid credentials: {form_data.email}"
) )
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou senha incorretos", detail="Invalid email or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token = create_access_token(user) 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"} 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) email_data: ForgotPassword, db: Session = Depends(get_db)
): ):
""" """
Inicia o processo de recuperação de senha Initiate the password recovery process
Args: Args:
email_data: Email do usuário email_data: User email
db: Sessão do banco de dados db: Database session
Returns: Returns:
MessageResponse: Mensagem de sucesso MessageResponse: Success message
Raises: Raises:
HTTPException: Se houver erro no processo HTTPException: If there is an error in the process
""" """
success, message = forgot_password(db, email_data.email) success, message = forgot_password(db, email_data.email)
# Sempre retornamos a mesma mensagem por segurança # Always return the same message for security
return { 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) @router.post("/reset-password", response_model=MessageResponse)
async def reset_user_password(reset_data: PasswordReset, db: Session = Depends(get_db)): 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: Args:
reset_data: Token e nova senha reset_data: Token and new password
db: Sessão do banco de dados db: Database session
Returns: Returns:
MessageResponse: Mensagem de sucesso MessageResponse: Success message
Raises: 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) success, message = reset_password(db, reset_data.token, reset_data.new_password)
if not success: 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) 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} return {"message": message}
@ -229,16 +229,16 @@ async def get_current_user(
db: Session = Depends(get_db), current_user: User = Depends(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: Args:
db: Sessão do banco de dados db: Database session
current_user: Usuário autenticado current_user: Authenticated user
Returns: Returns:
UserResponse: Dados do usuário autenticado UserResponse: Authenticated user data
Raises: Raises:
HTTPException: Se o usuário não estiver autenticado HTTPException: If the user is not authenticated
""" """
return current_user return current_user

74
src/api/chat_routes.py Normal file
View File

@ -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)
)

140
src/api/client_routes.py Normal file
View File

@ -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"
)

122
src/api/contact_routes.py Normal file
View File

@ -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"
)

View File

@ -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"
)

View File

@ -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"
)

138
src/api/session_routes.py Normal file
View File

@ -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)

97
src/api/tool_routes.py Normal file
View File

@ -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"
)

View File

@ -4,77 +4,83 @@ from pydantic_settings import BaseSettings
from functools import lru_cache from functools import lru_cache
import secrets import secrets
class Settings(BaseSettings): class Settings(BaseSettings):
"""Configurações do projeto""" """Project settings"""
# Configurações da API # API settings
API_TITLE: str = "Agent Runner API" API_TITLE: str = os.getenv("API_TITLE", "Evo AI API")
API_DESCRIPTION: str = "API para execução de agentes de IA" API_DESCRIPTION: str = os.getenv("API_DESCRIPTION", "API for executing AI agents")
API_VERSION: str = "1.0.0" API_VERSION: str = os.getenv("API_VERSION", "1.0.0")
# Configurações do banco de dados # Database settings
POSTGRES_CONNECTION_STRING: str = os.getenv( POSTGRES_CONNECTION_STRING: str = os.getenv(
"POSTGRES_CONNECTION_STRING", "POSTGRES_CONNECTION_STRING", "postgresql://postgres:root@localhost:5432/evo_ai"
"postgresql://postgres:root@localhost:5432/evo_ai"
) )
# Configurações de logging # Logging settings
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
LOG_DIR: str = "logs" 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_URL: str = os.getenv("KNOWLEDGE_API_URL", "http://localhost:5540")
KNOWLEDGE_API_KEY: str = os.getenv("KNOWLEDGE_API_KEY", "") KNOWLEDGE_API_KEY: str = os.getenv("KNOWLEDGE_API_KEY", "")
TENANT_ID: str = os.getenv("TENANT_ID", "") TENANT_ID: str = os.getenv("TENANT_ID", "")
# Configurações do Redis # Redis settings
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost") REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", 6379)) REDIS_PORT: int = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB: int = int(os.getenv("REDIS_DB", 0)) REDIS_DB: int = int(os.getenv("REDIS_DB", 0))
REDIS_PASSWORD: Optional[str] = os.getenv("REDIS_PASSWORD") 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)) 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_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", secrets.token_urlsafe(32))
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
JWT_EXPIRATION_TIME: int = int(os.getenv("JWT_EXPIRATION_TIME", 30)) 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", "") SENDGRID_API_KEY: str = os.getenv("SENDGRID_API_KEY", "")
EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@yourdomain.com") EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@yourdomain.com")
APP_URL: str = os.getenv("APP_URL", "http://localhost:8000") 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") HOST: str = os.getenv("HOST", "0.0.0.0")
PORT: int = int(os.getenv("PORT", 8000)) PORT: int = int(os.getenv("PORT", 8000))
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true" DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
# Configurações de CORS # CORS settings
CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "*").split(",") CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "*").split(",")
# Configurações de Token # Token settings
TOKEN_EXPIRY_HOURS: int = int(os.getenv("TOKEN_EXPIRY_HOURS", 24)) # Tokens de verificação/reset TOKEN_EXPIRY_HOURS: int = int(
os.getenv("TOKEN_EXPIRY_HOURS", 24)
# Configurações de Segurança ) # Verification/reset tokens
# Security settings
PASSWORD_MIN_LENGTH: int = int(os.getenv("PASSWORD_MIN_LENGTH", 8)) PASSWORD_MIN_LENGTH: int = int(os.getenv("PASSWORD_MIN_LENGTH", 8))
MAX_LOGIN_ATTEMPTS: int = int(os.getenv("MAX_LOGIN_ATTEMPTS", 5)) MAX_LOGIN_ATTEMPTS: int = int(os.getenv("MAX_LOGIN_ATTEMPTS", 5))
LOGIN_LOCKOUT_MINUTES: int = int(os.getenv("LOGIN_LOCKOUT_MINUTES", 30)) 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_EMAIL: str = os.getenv("ADMIN_EMAIL", "admin@evoai.com")
ADMIN_INITIAL_PASSWORD: str = os.getenv("ADMIN_INITIAL_PASSWORD", "senhaforte123") ADMIN_INITIAL_PASSWORD: str = os.getenv(
DEMO_EMAIL: str = os.getenv("DEMO_EMAIL", "demo@exemplo.com") "ADMIN_INITIAL_PASSWORD", "strongpassword123"
)
DEMO_EMAIL: str = os.getenv("DEMO_EMAIL", "demo@example.com")
DEMO_PASSWORD: str = os.getenv("DEMO_PASSWORD", "demo123") 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: class Config:
env_file = ".env" env_file = ".env"
case_sensitive = True case_sensitive = True
@lru_cache() @lru_cache()
def get_settings() -> Settings: def get_settings() -> Settings:
return Settings() return Settings()
settings = get_settings()
settings = get_settings()

View File

@ -2,7 +2,7 @@ from fastapi import HTTPException
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
class BaseAPIException(HTTPException): class BaseAPIException(HTTPException):
"""Classe base para exceções da API""" """Base class for API exceptions"""
def __init__( def __init__(
self, self,
status_code: int, status_code: int,
@ -17,16 +17,16 @@ class BaseAPIException(HTTPException):
}) })
class AgentNotFoundError(BaseAPIException): 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): def __init__(self, agent_id: str):
super().__init__( super().__init__(
status_code=404, 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" error_code="AGENT_NOT_FOUND"
) )
class InvalidParameterError(BaseAPIException): 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): def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__( super().__init__(
status_code=400, status_code=400,
@ -36,7 +36,7 @@ class InvalidParameterError(BaseAPIException):
) )
class InvalidRequestError(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): def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__( super().__init__(
status_code=400, status_code=400,
@ -46,8 +46,8 @@ class InvalidRequestError(BaseAPIException):
) )
class InternalServerError(BaseAPIException): class InternalServerError(BaseAPIException):
"""Exceção para erros internos do servidor""" """Exception for server errors"""
def __init__(self, message: str = "Erro interno do servidor"): def __init__(self, message: str = "Server error"):
super().__init__( super().__init__(
status_code=500, status_code=500,
message=message, message=message,

View File

@ -5,8 +5,6 @@ from src.config.settings import settings
from datetime import datetime from datetime import datetime
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import get_db 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 from uuid import UUID
import logging import logging
from typing import Optional 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: async def get_jwt_token(token: str = Depends(oauth2_scheme)) -> dict:
""" """
Extrai e valida o token JWT Extracts and validates the JWT token
Args: Args:
token: Token JWT token: Token JWT
Returns: Returns:
dict: Dados do payload do token dict: Token payload data
Raises: Raises:
HTTPException: Se o token for inválido HTTPException: If the token is invalid
""" """
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais inválidas", detail="Invalid credentials",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
@ -43,18 +41,18 @@ async def get_jwt_token(token: str = Depends(oauth2_scheme)) -> dict:
email: str = payload.get("sub") email: str = payload.get("sub")
if email is None: if email is None:
logger.warning("Token sem email (sub)") logger.warning("Token without email (sub)")
raise credentials_exception raise credentials_exception
exp = payload.get("exp") exp = payload.get("exp")
if exp is None or datetime.fromtimestamp(exp) < datetime.utcnow(): 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 raise credentials_exception
return payload return payload
except JWTError as e: 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 raise credentials_exception
async def verify_user_client( async def verify_user_client(
@ -63,77 +61,77 @@ async def verify_user_client(
required_client_id: UUID = None required_client_id: UUID = None
) -> bool: ) -> bool:
""" """
Verifica se o usuário está associado ao cliente especificado Verifies if the user is associated with the specified client
Args: Args:
payload: Payload do token JWT payload: Token JWT payload
db: Sessão do banco de dados db: Database session
required_client_id: ID do cliente que deve ser verificado required_client_id: Client ID to be verified
Returns: Returns:
bool: True se verificado com sucesso bool: True se verificado com sucesso
Raises: 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): if payload.get("is_admin", False):
return True return True
# Para não-admins, verificar se o client_id corresponde # Para não-admins, verificar se o client_id corresponde
user_client_id = payload.get("client_id") user_client_id = payload.get("client_id")
if not user_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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, 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: if not required_client_id:
return True 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): 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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, 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 return True
async def verify_admin(payload: dict = Depends(get_jwt_token)) -> bool: 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: Args:
payload: Payload do token JWT payload: Token JWT payload
Returns: Returns:
bool: True se for administrador bool: True if the user is an administrator
Raises: 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): 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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Permissão negada. Acesso restrito a administradores." detail="Access denied. Restricted to administrators."
) )
return True return True
def get_current_user_client_id(payload: dict = Depends(get_jwt_token)) -> Optional[UUID]: 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: Args:
payload: Payload do token JWT payload: Token JWT payload
Returns: 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): if payload.get("is_admin", False):
return None return None

View File

@ -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())

View File

@ -2,53 +2,44 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
# Adiciona o diretório raiz ao PYTHONPATH # Add the root directory to PYTHONPATH
root_dir = Path(__file__).parent.parent root_dir = Path(__file__).parent.parent
sys.path.append(str(root_dir)) sys.path.append(str(root_dir))
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from typing import Dict, Any
from src.config.database import engine, Base 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.auth_routes import router as auth_router
from src.api.admin_routes import router as admin_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.config.settings import settings
from src.utils.logger import setup_logger 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__) 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( app = FastAPI(
title=settings.API_TITLE, title=settings.API_TITLE,
description=settings.API_DESCRIPTION + """ 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`.
""",
version=settings.API_VERSION, version=settings.API_VERSION,
) )
# Configuração de CORS # CORS configuration
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, allow_origins=settings.CORS_ORIGINS,
@ -57,25 +48,34 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Configuração do PostgreSQL # PostgreSQL configuration
POSTGRES_CONNECTION_STRING = os.getenv( POSTGRES_CONNECTION_STRING = os.getenv(
"POSTGRES_CONNECTION_STRING", "POSTGRES_CONNECTION_STRING",
"postgresql://postgres:root@localhost:5432/evo_ai" "postgresql://postgres:root@localhost:5432/evo_ai"
) )
# Criar as tabelas no banco de dados # Create database tables
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
# Incluir as rotas API_PREFIX = "/api/v1"
app.include_router(auth_router, prefix="/api/v1")
app.include_router(admin_router, prefix="/api/v1") # Include routes
app.include_router(router, prefix="/api/v1") 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("/") @app.get("/")
def read_root(): def read_root():
return { return {
"message": "Bem-vindo à API Evo AI", "message": "Welcome to Evo AI API",
"documentation": "/docs", "documentation": "/docs",
"version": settings.API_VERSION, "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'"
} }

View File

@ -30,7 +30,7 @@ class User(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 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")) client = relationship("Client", backref=backref("user", uselist=False, cascade="all, delete-orphan"))
class Contact(Base): class Contact(Base):
@ -64,7 +64,7 @@ class Agent(Base):
) )
def to_dict(self): 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 = {} result = {}
for key, value in self.__dict__.items(): for key, value in self.__dict__.items():
if key.startswith('_'): if key.startswith('_'):
@ -80,7 +80,7 @@ class Agent(Base):
return result return result
def _convert_dict(self, d): def _convert_dict(self, d):
"""Converte UUIDs em um dicionário para strings""" """Converts UUIDs to a dictionary for strings"""
result = {} result = {}
for key, value in d.items(): for key, value in d.items():
if isinstance(value, uuid.UUID): if isinstance(value, uuid.UUID):
@ -123,7 +123,7 @@ class Tool(Base):
class Session(Base): class Session(Base):
__tablename__ = "sessions" __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}} __table_args__ = {'extend_existing': True, 'info': {'skip_autogenerate': True}}
id = Column(String, primary_key=True) id = Column(String, primary_key=True)
@ -146,5 +146,5 @@ class AuditLog(Base):
user_agent = Column(String, nullable=True) user_agent = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relacionamento com User # Relationship with User
user = relationship("User", backref="audit_logs") user = relationship("User", backref="audit_logs")

View File

@ -3,24 +3,24 @@ from pydantic import BaseModel, Field
from uuid import UUID from uuid import UUID
class ToolConfig(BaseModel): class ToolConfig(BaseModel):
"""Configuração de uma ferramenta""" """Configuration of a tool"""
id: UUID 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: class Config:
from_attributes = True from_attributes = True
class MCPServerConfig(BaseModel): class MCPServerConfig(BaseModel):
"""Configuração de um servidor MCP""" """Configuration of an MCP server"""
id: UUID id: UUID
envs: Dict[str, str] = Field(default_factory=dict, description="Variáveis de ambiente do servidor") envs: Dict[str, str] = Field(default_factory=dict, description="Environment variables of the server")
tools: List[str] = Field(default_factory=list, description="Lista de ferramentas do servidor") tools: List[str] = Field(default_factory=list, description="List of tools of the server")
class Config: class Config:
from_attributes = True from_attributes = True
class HTTPToolParameter(BaseModel): class HTTPToolParameter(BaseModel):
"""Parâmetro de uma ferramenta HTTP""" """Parameter of an HTTP tool"""
type: str type: str
required: bool required: bool
description: str description: str
@ -29,7 +29,7 @@ class HTTPToolParameter(BaseModel):
from_attributes = True from_attributes = True
class HTTPToolParameters(BaseModel): class HTTPToolParameters(BaseModel):
"""Parâmetros de uma ferramenta HTTP""" """Parameters of an HTTP tool"""
path_params: Optional[Dict[str, str]] = None path_params: Optional[Dict[str, str]] = None
query_params: Optional[Dict[str, Union[str, List[str]]]] = None query_params: Optional[Dict[str, Union[str, List[str]]]] = None
body_params: Optional[Dict[str, HTTPToolParameter]] = None body_params: Optional[Dict[str, HTTPToolParameter]] = None
@ -38,7 +38,7 @@ class HTTPToolParameters(BaseModel):
from_attributes = True from_attributes = True
class HTTPToolErrorHandling(BaseModel): class HTTPToolErrorHandling(BaseModel):
"""Configuração de tratamento de erros""" """Configuration of error handling"""
timeout: int timeout: int
retry_count: int retry_count: int
fallback_response: Dict[str, str] fallback_response: Dict[str, str]
@ -47,7 +47,7 @@ class HTTPToolErrorHandling(BaseModel):
from_attributes = True from_attributes = True
class HTTPTool(BaseModel): class HTTPTool(BaseModel):
"""Configuração de uma ferramenta HTTP""" """Configuration of an HTTP tool"""
name: str name: str
method: str method: str
values: Dict[str, str] values: Dict[str, str]
@ -61,41 +61,41 @@ class HTTPTool(BaseModel):
from_attributes = True from_attributes = True
class CustomTools(BaseModel): class CustomTools(BaseModel):
"""Configuração de ferramentas personalizadas""" """Configuration of custom tools"""
http_tools: List[HTTPTool] = Field(default_factory=list, description="Lista de ferramentas HTTP") http_tools: List[HTTPTool] = Field(default_factory=list, description="List of HTTP tools")
class Config: class Config:
from_attributes = True from_attributes = True
class LLMConfig(BaseModel): class LLMConfig(BaseModel):
"""Configuração para agentes do tipo LLM""" """Configuration for LLM agents"""
tools: Optional[List[ToolConfig]] = Field(default=None, description="Lista de ferramentas disponíveis") tools: Optional[List[ToolConfig]] = Field(default=None, description="List of available tools")
custom_tools: Optional[CustomTools] = Field(default=None, description="Ferramentas personalizadas") custom_tools: Optional[CustomTools] = Field(default=None, description="Custom tools")
mcp_servers: Optional[List[MCPServerConfig]] = Field(default=None, description="Lista de servidores MCP") mcp_servers: Optional[List[MCPServerConfig]] = Field(default=None, description="List of MCP servers")
sub_agents: Optional[List[UUID]] = Field(default=None, description="Lista de IDs dos sub-agentes") sub_agents: Optional[List[UUID]] = Field(default=None, description="List of IDs of sub-agents")
class Config: class Config:
from_attributes = True from_attributes = True
class SequentialConfig(BaseModel): class SequentialConfig(BaseModel):
"""Configuração para agentes do tipo Sequential""" """Configuration for sequential agents"""
sub_agents: List[UUID] = Field(..., description="Lista de IDs dos sub-agentes em ordem de execução") sub_agents: List[UUID] = Field(..., description="List of IDs of sub-agents in execution order")
class Config: class Config:
from_attributes = True from_attributes = True
class ParallelConfig(BaseModel): class ParallelConfig(BaseModel):
"""Configuração para agentes do tipo Parallel""" """Configuration for parallel agents"""
sub_agents: List[UUID] = Field(..., description="Lista de IDs dos sub-agentes para execução paralela") sub_agents: List[UUID] = Field(..., description="List of IDs of sub-agents for parallel execution")
class Config: class Config:
from_attributes = True from_attributes = True
class LoopConfig(BaseModel): class LoopConfig(BaseModel):
"""Configuração para agentes do tipo Loop""" """Configuration for loop agents"""
sub_agents: List[UUID] = Field(..., description="Lista de IDs dos sub-agentes para execução em loop") sub_agents: List[UUID] = Field(..., description="List of IDs of sub-agents for loop execution")
max_iterations: Optional[int] = Field(default=None, description="Número máximo de iterações") max_iterations: Optional[int] = Field(default=None, description="Maximum number of iterations")
condition: Optional[str] = Field(default=None, description="Condição para parar o loop") condition: Optional[str] = Field(default=None, description="Condition to stop the loop")
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -4,18 +4,18 @@ from datetime import datetime
from uuid import UUID from uuid import UUID
class AuditLogBase(BaseModel): class AuditLogBase(BaseModel):
"""Schema base para log de auditoria""" """Base schema for audit log"""
action: str action: str
resource_type: str resource_type: str
resource_id: Optional[str] = None resource_id: Optional[str] = None
details: Optional[Dict[str, Any]] = None details: Optional[Dict[str, Any]] = None
class AuditLogCreate(AuditLogBase): class AuditLogCreate(AuditLogBase):
"""Schema para criação de log de auditoria""" """Schema for creating audit log"""
pass pass
class AuditLogResponse(AuditLogBase): class AuditLogResponse(AuditLogBase):
"""Schema para resposta de log de auditoria""" """Schema for audit log response"""
id: UUID id: UUID
user_id: Optional[UUID] = None user_id: Optional[UUID] = None
ip_address: Optional[str] = None ip_address: Optional[str] = None
@ -26,7 +26,7 @@ class AuditLogResponse(AuditLogBase):
from_attributes = True from_attributes = True
class AuditLogFilter(BaseModel): class AuditLogFilter(BaseModel):
"""Schema para filtros de busca de logs de auditoria""" """Schema for audit log search filters"""
user_id: Optional[UUID] = None user_id: Optional[UUID] = None
action: Optional[str] = None action: Optional[str] = None
resource_type: Optional[str] = None resource_type: Optional[str] = None

View File

@ -2,20 +2,20 @@ from pydantic import BaseModel, Field
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
class ChatRequest(BaseModel): class ChatRequest(BaseModel):
"""Schema para requisições de chat""" """Schema for chat requests"""
agent_id: str = Field(..., description="ID do agente que irá processar a mensagem") agent_id: str = Field(..., description="ID of the agent that will process the message")
contact_id: str = Field(..., description="ID do contato que irá processar a mensagem") contact_id: str = Field(..., description="ID of the contact that will process the message")
message: str = Field(..., description="Mensagem do usuário") message: str = Field(..., description="User message")
class ChatResponse(BaseModel): class ChatResponse(BaseModel):
"""Schema para respostas do chat""" """Schema for chat responses"""
response: str = Field(..., description="Resposta do agente") response: str = Field(..., description="Agent response")
status: str = Field(..., description="Status da operação") status: str = Field(..., description="Operation status")
error: Optional[str] = Field(None, description="Mensagem de erro, se houver") error: Optional[str] = Field(None, description="Error message, if there is one")
timestamp: str = Field(..., description="Timestamp da resposta") timestamp: str = Field(..., description="Timestamp of the response")
class ErrorResponse(BaseModel): class ErrorResponse(BaseModel):
"""Schema para respostas de erro""" """Schema for error responses"""
error: str = Field(..., description="Mensagem de erro") error: str = Field(..., description="Error message")
status_code: int = Field(..., description="Código HTTP do erro") status_code: int = Field(..., description="HTTP status code of the error")
details: Optional[Dict[str, Any]] = Field(None, description="Detalhes adicionais do erro") details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")

View File

@ -36,36 +36,36 @@ class Contact(ContactBase):
from_attributes = True from_attributes = True
class AgentBase(BaseModel): class AgentBase(BaseModel):
name: str = Field(..., description="Nome do agente (sem espaços ou caracteres especiais)") name: str = Field(..., description="Agent name (no spaces or special characters)")
description: Optional[str] = Field(None, description="Descrição do agente") description: Optional[str] = Field(None, description="Agent description")
type: str = Field(..., description="Tipo do agente (llm, sequential, parallel, loop)") type: str = Field(..., description="Agent type (llm, sequential, parallel, loop)")
model: Optional[str] = Field(None, description="Modelo do agente (obrigatório apenas para tipo llm)") model: Optional[str] = Field(None, description="Agent model (required only for llm type)")
api_key: Optional[str] = Field(None, description="API Key do agente (obrigatória apenas para tipo llm)") api_key: Optional[str] = Field(None, description="Agent API Key (required only for llm type)")
instruction: Optional[str] = None 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') @validator('name')
def validate_name(cls, v): def validate_name(cls, v):
if not re.match(r'^[a-zA-Z0-9_-]+$', 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 return v
@validator('type') @validator('type')
def validate_type(cls, v): def validate_type(cls, v):
if v not in ['llm', 'sequential', 'parallel', 'loop']: 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 return v
@validator('model') @validator('model')
def validate_model(cls, v, values): def validate_model(cls, v, values):
if 'type' in values and values['type'] == 'llm' and not v: 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 return v
@validator('api_key') @validator('api_key')
def validate_api_key(cls, v, values): def validate_api_key(cls, v, values):
if 'type' in values and values['type'] == 'llm' and not v: 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 return v
@validator('config') @validator('config')
@ -76,21 +76,21 @@ class AgentBase(BaseModel):
if values['type'] == 'llm': if values['type'] == 'llm':
if isinstance(v, dict): if isinstance(v, dict):
try: try:
# Converte o dicionário para LLMConfig # Convert the dictionary to LLMConfig
v = LLMConfig(**v) v = LLMConfig(**v)
except Exception as e: 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): 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']: elif values['type'] in ['sequential', 'parallel', 'loop']:
if not isinstance(v, dict): 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: 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): 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']: 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 return v
class AgentCreate(AgentBase): class AgentCreate(AgentBase):

View File

@ -8,7 +8,7 @@ class UserBase(BaseModel):
class UserCreate(UserBase): class UserCreate(UserBase):
password: str password: str
name: str # Para criação do cliente associado name: str # For client creation
class AdminUserCreate(UserBase): class AdminUserCreate(UserBase):
password: str password: str
@ -34,7 +34,7 @@ class TokenResponse(BaseModel):
token_type: str token_type: str
class TokenData(BaseModel): class TokenData(BaseModel):
sub: str # email do usuário sub: str # user email
exp: datetime exp: datetime
is_admin: bool is_admin: bool
client_id: Optional[UUID] = None client_id: Optional[UUID] = None

View File

@ -1,7 +1,6 @@
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from google.adk.agents.llm_agent import LlmAgent from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents import SequentialAgent, ParallelAgent, LoopAgent from google.adk.agents import SequentialAgent, ParallelAgent, LoopAgent
from google.adk.memory import InMemoryMemoryService
from google.adk.models.lite_llm import LiteLlm from google.adk.models.lite_llm import LiteLlm
from src.utils.logger import setup_logger from src.utils.logger import setup_logger
from src.core.exceptions import AgentNotFoundError from src.core.exceptions import AgentNotFoundError
@ -26,25 +25,25 @@ def before_model_callback(
callback_context: CallbackContext, llm_request: LlmRequest callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]: ) -> Optional[LlmResponse]:
""" """
Callback executado antes do modelo gerar uma resposta. Callback executed before the model generates a response.
Sempre executa a busca na base de conhecimento antes de prosseguir. Always executes a search in the knowledge base before proceeding.
""" """
try: try:
agent_name = callback_context.agent_name agent_name = callback_context.agent_name
logger.debug(f"🔄 Before model call for agent: {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 = "" last_user_message = ""
if llm_request.contents and llm_request.contents[-1].role == "user": if llm_request.contents and llm_request.contents[-1].role == "user":
if llm_request.contents[-1].parts: if llm_request.contents[-1].parts:
last_user_message = llm_request.contents[-1].parts[0].text last_user_message = llm_request.contents[-1].parts[0].text
logger.debug(f"📝 Última mensagem do usuário: {last_user_message}") 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 = [] history = []
for content in llm_request.contents: for content in llm_request.contents:
if content.parts and content.parts[0].text: 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 role = "assistant" if content.role == "model" else content.role
history.append( history.append(
{ {
@ -56,12 +55,12 @@ def before_model_callback(
} }
) )
# loga o histórico de mensagens # log the history of messages
logger.debug(f"📝 Histórico de mensagens: {history}") logger.debug(f"📝 History of messages: {history}")
if last_user_message: if last_user_message:
logger.info("🔍 Executando busca na base de conhecimento") logger.info("🔍 Executing knowledge base search")
# Executa a busca na base de conhecimento de forma síncrona # Execute the knowledge base search synchronously
search_results = search_knowledge_base_function_sync( search_results = search_knowledge_base_function_sync(
last_user_message, history last_user_message, history
) )
@ -69,10 +68,10 @@ def before_model_callback(
if search_results: if search_results:
logger.info("✅ Resultados encontrados, adicionando ao contexto") 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 "" 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 = ( modified_text = (
original_instruction original_instruction
+ "\n\n<knowledge_context>\n" + "\n\n<knowledge_context>\n"
@ -84,23 +83,23 @@ def before_model_callback(
llm_request.config.system_instruction = modified_text llm_request.config.system_instruction = modified_text
logger.debug( 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: else:
logger.warning("⚠️ Nenhum resultado encontrado na busca") logger.warning("⚠️ No results found in the search")
else: 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 return None
except Exception as e: 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 return None
def search_knowledge_base_function_sync(query: str, history=[]): def search_knowledge_base_function_sync(query: str, history=[]):
""" """
Search knowledge base de forma síncrona. Search knowledge base synchronously.
Args: Args:
query (str): The search query, with user message and history messages, all in one string 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 dict: The search results
""" """
try: try:
logger.info("🔍 Iniciando busca na base de conhecimento") logger.info("🔍 Starting knowledge base search")
logger.debug(f"Query recebida: {query}") logger.debug(f"Received query: {query}")
# url = os.getenv("KNOWLEDGE_API_URL") + "/api/v1/search" # url = os.getenv("KNOWLEDGE_API_URL") + "/api/v1/search"
url = os.getenv("KNOWLEDGE_API_URL") + "/api/v1/knowledge" url = os.getenv("KNOWLEDGE_API_URL") + "/api/v1/knowledge"
tenant_id = os.getenv("TENANT_ID") tenant_id = os.getenv("TENANT_ID")
url = url + "?tenant_id=" + 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}") logger.debug(f"Tenant ID: {tenant_id}")
headers = { headers = {
"x-api-key": f"{os.getenv('KNOWLEDGE_API_KEY')}", "x-api-key": f"{os.getenv('KNOWLEDGE_API_KEY')}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
logger.debug(f"Headers configurados: {headers}") logger.debug(f"Headers configured: {headers}")
payload = { payload = {
"gemini_api_key": os.getenv("GOOGLE_API_KEY"), "gemini_api_key": os.getenv("GOOGLE_API_KEY"),
@ -134,31 +133,31 @@ def search_knowledge_base_function_sync(query: str, history=[]):
"history": 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 # Using requests to make a synchronous request with timeout
logger.info("🔄 Fazendo requisição síncrona para a API de conhecimento") logger.info("🔄 Making synchronous request to the knowledge API")
# response = requests.post(url, headers=headers, json=payload) # response = requests.post(url, headers=headers, json=payload)
response = requests.get(url, headers=headers, timeout=10) response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200: if response.status_code == 200:
logger.info("Busca realizada com sucesso") logger.info("Search executed successfully")
result = response.json() result = response.json()
logger.debug(f"Resultado da busca: {result}") logger.debug(f"Search result: {result}")
return result return result
else: else:
logger.error( logger.error(
f"❌ Erro ao realizar busca. Status code: {response.status_code}" f"❌ Error performing search. Status code: {response.status_code}"
) )
return None return None
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
logger.error("❌ Timeout ao realizar busca na base de conhecimento") logger.error("❌ Timeout performing search")
return None return None
except requests.exceptions.RequestException as e: 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 return None
except Exception as e: 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 return None
@ -171,19 +170,19 @@ class AgentBuilder:
async def _create_llm_agent( async def _create_llm_agent(
self, agent self, agent
) -> Tuple[LlmAgent, Optional[AsyncExitStack]]: ) -> Tuple[LlmAgent, Optional[AsyncExitStack]]:
"""Cria um agente LLM a partir dos dados do agente.""" """Create an LLM agent from the agent data."""
# Obtém ferramentas personalizadas da configuração # Get custom tools from the configuration
custom_tools = [] custom_tools = []
if agent.config.get("tools"): if agent.config.get("tools"):
custom_tools = self.custom_tool_builder.build_tools(agent.config["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_tools = []
mcp_exit_stack = None mcp_exit_stack = None
if agent.config.get("mcp_servers"): if agent.config.get("mcp_servers"):
mcp_tools, mcp_exit_stack = await self.mcp_service.build_tools(agent.config, self.db) 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 all_tools = custom_tools + mcp_tools
now = datetime.now() now = datetime.now()
@ -192,7 +191,7 @@ class AgentBuilder:
current_date_iso = now.strftime("%Y-%m-%d") current_date_iso = now.strftime("%Y-%m-%d")
current_time = now.strftime("%H:%M") current_time = now.strftime("%H:%M")
# Substitui as variáveis no prompt # Substitute variables in the prompt
formatted_prompt = agent.instruction.format( formatted_prompt = agent.instruction.format(
current_datetime=current_datetime, current_datetime=current_datetime,
current_day_of_week=current_day_of_week, current_day_of_week=current_day_of_week,
@ -200,7 +199,7 @@ class AgentBuilder:
current_time=current_time, current_time=current_time,
) )
# Verifica se load_memory está habilitado # Check if load_memory is enabled
# before_model_callback_func = None # before_model_callback_func = None
if agent.config.get("load_memory") == True: if agent.config.get("load_memory") == True:
all_tools.append(load_memory) all_tools.append(load_memory)
@ -222,17 +221,17 @@ class AgentBuilder:
async def _get_sub_agents( async def _get_sub_agents(
self, sub_agent_ids: List[str] self, sub_agent_ids: List[str]
) -> List[Tuple[LlmAgent, Optional[AsyncExitStack]]]: ) -> List[Tuple[LlmAgent, Optional[AsyncExitStack]]]:
"""Obtém e cria os sub-agentes LLM.""" """Get and create LLM sub-agents."""
sub_agents = [] sub_agents = []
for sub_agent_id in sub_agent_ids: for sub_agent_id in sub_agent_ids:
agent = get_agent(self.db, sub_agent_id) agent = get_agent(self.db, sub_agent_id)
if agent is None: 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": if agent.type != "llm":
raise ValueError( 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) sub_agent, exit_stack = await self._create_llm_agent(agent)
@ -243,8 +242,8 @@ class AgentBuilder:
async def build_llm_agent( async def build_llm_agent(
self, root_agent self, root_agent
) -> Tuple[LlmAgent, Optional[AsyncExitStack]]: ) -> Tuple[LlmAgent, Optional[AsyncExitStack]]:
"""Constrói um agente LLM com seus sub-agentes.""" """Build an LLM agent with its sub-agents."""
logger.info("Criando agente LLM") logger.info("Creating LLM agent")
sub_agents = [] sub_agents = []
if root_agent.config.get("sub_agents"): if root_agent.config.get("sub_agents"):
@ -262,8 +261,8 @@ class AgentBuilder:
async def build_composite_agent( async def build_composite_agent(
self, root_agent self, root_agent
) -> Tuple[SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack]]: ) -> Tuple[SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack]]:
"""Constrói um agente composto (Sequential, Parallel ou Loop) com seus sub-agentes.""" """Build a composite agent (Sequential, Parallel or Loop) with its sub-agents."""
logger.info(f"Processando sub-agentes para agente {root_agent.type}") logger.info(f"Processing sub-agents for agent {root_agent.type}")
sub_agents_with_stacks = await self._get_sub_agents( sub_agents_with_stacks = await self._get_sub_agents(
root_agent.config.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] sub_agents = [agent for agent, _ in sub_agents_with_stacks]
if root_agent.type == "sequential": if root_agent.type == "sequential":
logger.info("Criando SequentialAgent") logger.info("Creating SequentialAgent")
return ( return (
SequentialAgent( SequentialAgent(
name=root_agent.name, name=root_agent.name,
@ -281,7 +280,7 @@ class AgentBuilder:
None, None,
) )
elif root_agent.type == "parallel": elif root_agent.type == "parallel":
logger.info("Criando ParallelAgent") logger.info("Creating ParallelAgent")
return ( return (
ParallelAgent( ParallelAgent(
name=root_agent.name, name=root_agent.name,
@ -291,7 +290,7 @@ class AgentBuilder:
None, None,
) )
elif root_agent.type == "loop": elif root_agent.type == "loop":
logger.info("Criando LoopAgent") logger.info("Creating LoopAgent")
return ( return (
LoopAgent( LoopAgent(
name=root_agent.name, name=root_agent.name,
@ -302,14 +301,14 @@ class AgentBuilder:
None, None,
) )
else: 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( async def build_agent(
self, root_agent self, root_agent
) -> Tuple[ ) -> Tuple[
LlmAgent | SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack] 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": if root_agent.type == "llm":
return await self.build_llm_agent(root_agent) return await self.build_llm_agent(root_agent)
else: else:

View File

@ -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.adk.runners import Runner
from google.genai.types import Content, Part from google.genai.types import Content, Part
from google.adk.sessions import DatabaseSessionService 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_service import get_agent
from src.services.agent_builder import AgentBuilder from src.services.agent_builder import AgentBuilder
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from contextlib import AsyncExitStack
logger = setup_logger(__name__) logger = setup_logger(__name__)
@ -29,23 +23,23 @@ async def run_agent(
): ):
try: try:
logger.info( 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) get_root_agent = get_agent(db, agent_id)
logger.info( 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: 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) agent_builder = AgentBuilder(db)
root_agent, exit_stack = await agent_builder.build_agent(get_root_agent) root_agent, exit_stack = await agent_builder.build_agent(get_root_agent)
logger.info("Configurando Runner") logger.info("Configuring Runner")
agent_runner = Runner( agent_runner = Runner(
agent=root_agent, agent=root_agent,
app_name=agent_id, app_name=agent_id,
@ -55,7 +49,7 @@ async def run_agent(
) )
session_id = contact_id + "_" + agent_id 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( session = session_service.get_session(
app_name=agent_id, app_name=agent_id,
user_id=contact_id, user_id=contact_id,
@ -63,7 +57,7 @@ async def run_agent(
) )
if session is None: 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( session = session_service.create_session(
app_name=agent_id, app_name=agent_id,
user_id=contact_id, user_id=contact_id,
@ -71,7 +65,7 @@ async def run_agent(
) )
content = Content(role="user", parts=[Part(text=message)]) content = Content(role="user", parts=[Part(text=message)])
logger.info("Iniciando execução do agente") logger.info("Starting agent execution")
final_response_text = None final_response_text = None
try: try:
@ -82,7 +76,7 @@ async def run_agent(
): ):
if event.is_final_response() and event.content and event.content.parts: if event.is_final_response() and event.content and event.content.parts:
final_response_text = event.content.parts[0].text 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( completed_session = session_service.get_session(
app_name=agent_id, app_name=agent_id,
@ -93,15 +87,15 @@ async def run_agent(
memory_service.add_session_to_memory(completed_session) memory_service.add_session_to_memory(completed_session)
finally: finally:
# Garante que o exit_stack seja fechado corretamente # Ensure the exit_stack is closed correctly
if exit_stack: if exit_stack:
await exit_stack.aclose() await exit_stack.aclose()
logger.info("Execução do agente concluída com sucesso") logger.info("Agent execution completed successfully")
return final_response_text return final_response_text
except AgentNotFoundError as e: except AgentNotFoundError as e:
logger.error(f"Erro ao processar requisição: {str(e)}") logger.error(f"Error processing request: {str(e)}")
raise e raise e
except Exception as 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)) raise InternalServerError(str(e))

View File

@ -3,12 +3,6 @@ from sqlalchemy.exc import SQLAlchemyError
from fastapi import HTTPException, status from fastapi import HTTPException, status
from src.models.models import Agent from src.models.models import Agent
from src.schemas.schemas import AgentCreate from src.schemas.schemas import AgentCreate
from src.schemas.agent_config import (
LLMConfig,
SequentialConfig,
ParallelConfig,
LoopConfig,
)
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from src.services.mcp_server_service import get_mcp_server from src.services.mcp_server_service import get_mcp_server
import uuid import uuid
@ -18,7 +12,7 @@ logger = logging.getLogger(__name__)
def validate_sub_agents(db: Session, sub_agents: List[uuid.UUID]) -> bool: 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: for agent_id in sub_agents:
agent = get_agent(db, agent_id) agent = get_agent(db, agent_id)
if not agent: 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]: def get_agent(db: Session, agent_id: uuid.UUID) -> Optional[Agent]:
"""Busca um agente pelo ID""" """Search for an agent by ID"""
try: try:
agent = db.query(Agent).filter(Agent.id == agent_id).first() agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent: if not agent:
logger.warning(f"Agente não encontrado: {agent_id}") logger.warning(f"Agent not found: {agent_id}")
return None return None
return agent return agent
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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, limit: int = 100,
active_only: bool = True, active_only: bool = True,
) -> List[Agent]: ) -> List[Agent]:
"""Busca agentes de um cliente com paginação""" """Search for agents by client with pagination"""
try: try:
query = db.query(Agent).filter(Agent.client_id == client_id) query = db.query(Agent).filter(Agent.client_id == client_id)
return query.offset(skip).limit(limit).all() return query.offset(skip).limit(limit).all()
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def create_agent(db: Session, agent: AgentCreate) -> Agent:
"""Cria um novo agente""" """Create a new agent"""
try: try:
# Validação adicional de sub-agentes # Additional sub-agent validation
if agent.type != "llm": if agent.type != "llm":
if not isinstance(agent.config, dict): if not isinstance(agent.config, dict):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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: if "sub_agents" not in agent.config:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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"]: if not agent.config["sub_agents"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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"]): if not validate_sub_agents(db, agent.config["sub_agents"]):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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 config = agent.config
if isinstance(config, dict): if isinstance(config, dict):
# Processa servidores MCP # Process MCP servers
if "mcp_servers" in config: if "mcp_servers" in config:
processed_servers = [] processed_servers = []
for server in config["mcp_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"]) mcp_server = get_mcp_server(db, server["id"])
if not mcp_server: if not mcp_server:
raise HTTPException( raise HTTPException(
status_code=400, 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(): for env_key, env_value in mcp_server.environments.items():
if env_key not in server.get("envs", {}): if env_key not in server.get("envs", {}):
raise HTTPException( raise HTTPException(
status_code=400, 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( processed_servers.append(
{ {
"id": str(server["id"]), "id": str(server["id"]),
@ -125,13 +119,13 @@ def create_agent(db: Session, agent: AgentCreate) -> Agent:
config["mcp_servers"] = processed_servers config["mcp_servers"] = processed_servers
# Processa sub-agentes # Process sub-agents
if "sub_agents" in config: if "sub_agents" in config:
config["sub_agents"] = [ config["sub_agents"] = [
str(agent_id) for agent_id in config["sub_agents"] str(agent_id) for agent_id in config["sub_agents"]
] ]
# Processa ferramentas # Process tools
if "tools" in config: if "tools" in config:
config["tools"] = [ config["tools"] = [
{"id": str(tool["id"]), "envs": tool["envs"]} {"id": str(tool["id"]), "envs": tool["envs"]}
@ -144,51 +138,51 @@ def create_agent(db: Session, agent: AgentCreate) -> Agent:
db.add(db_agent) db.add(db_agent)
db.commit() db.commit()
db.refresh(db_agent) 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 return db_agent
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar agente: {str(e)}") logger.error(f"Error creating agent: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao criar agente", detail="Error creating agent",
) )
async def update_agent( async def update_agent(
db: Session, agent_id: uuid.UUID, agent_data: Dict[str, Any] db: Session, agent_id: uuid.UUID, agent_data: Dict[str, Any]
) -> Agent: ) -> Agent:
"""Atualiza um agente existente""" """Update an existing agent"""
try: try:
agent = db.query(Agent).filter(Agent.id == agent_id).first() agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent: 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: if "config" in agent_data:
config = agent_data["config"] config = agent_data["config"]
# Processa servidores MCP # Process MCP servers
if "mcp_servers" in config: if "mcp_servers" in config:
processed_servers = [] processed_servers = []
for server in config["mcp_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"]) mcp_server = get_mcp_server(db, server["id"])
if not mcp_server: if not mcp_server:
raise HTTPException( raise HTTPException(
status_code=400, 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(): for env_key, env_value in mcp_server.environments.items():
if env_key not in server.get("envs", {}): if env_key not in server.get("envs", {}):
raise HTTPException( raise HTTPException(
status_code=400, 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( processed_servers.append(
{ {
"id": str(server["id"]), "id": str(server["id"]),
@ -199,13 +193,13 @@ async def update_agent(
config["mcp_servers"] = processed_servers config["mcp_servers"] = processed_servers
# Processa sub-agentes # Process sub-agents
if "sub_agents" in config: if "sub_agents" in config:
config["sub_agents"] = [ config["sub_agents"] = [
str(agent_id) for agent_id in config["sub_agents"] str(agent_id) for agent_id in config["sub_agents"]
] ]
# Processa ferramentas # Process tools
if "tools" in config: if "tools" in config:
config["tools"] = [ config["tools"] = [
{"id": str(tool["id"]), "envs": tool["envs"]} {"id": str(tool["id"]), "envs": tool["envs"]}
@ -223,43 +217,43 @@ async def update_agent(
except Exception as e: except Exception as e:
db.rollback() db.rollback()
raise HTTPException( 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: def delete_agent(db: Session, agent_id: uuid.UUID) -> bool:
"""Remove um agente (soft delete)""" """Remove an agent (soft delete)"""
try: try:
db_agent = get_agent(db, agent_id) db_agent = get_agent(db, agent_id)
if not db_agent: if not db_agent:
return False return False
db.commit() db.commit()
logger.info(f"Agente desativado com sucesso: {agent_id}") logger.info(f"Agent deactivated successfully: {agent_id}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def activate_agent(db: Session, agent_id: uuid.UUID) -> bool:
"""Reativa um agente""" """Reactivate an agent"""
try: try:
db_agent = get_agent(db, agent_id) db_agent = get_agent(db, agent_id)
if not db_agent: if not db_agent:
return False return False
db.commit() db.commit()
logger.info(f"Agente reativado com sucesso: {agent_id}") logger.info(f"Agent reactivated successfully: {agent_id}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao reativar agente", detail="Error reactivating agent",
) )

View File

@ -20,19 +20,19 @@ def create_audit_log(
request: Optional[Request] = None request: Optional[Request] = None
) -> Optional[AuditLog]: ) -> Optional[AuditLog]:
""" """
Cria um novo registro de auditoria Create a new audit log
Args: Args:
db: Sessão do banco de dados db: Database session
user_id: ID do usuário que realizou a ação (ou None se anônimo) user_id: User ID that performed the action (or None if anonymous)
action: Ação realizada (ex: "create", "update", "delete") action: Action performed (ex: "create", "update", "delete")
resource_type: Tipo de recurso (ex: "client", "agent", "user") resource_type: Resource type (ex: "client", "agent", "user")
resource_id: ID do recurso (opcional) resource_id: Resource ID (optional)
details: Detalhes adicionais da ação (opcional) details: Additional details of the action (optional)
request: Objeto Request do FastAPI (opcional, para obter IP e User-Agent) request: FastAPI Request object (optional, to get IP and User-Agent)
Returns: 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: try:
ip_address = None ip_address = None
@ -42,9 +42,9 @@ def create_audit_log(
ip_address = request.client.host if hasattr(request, 'client') else None ip_address = request.client.host if hasattr(request, 'client') else None
user_agent = request.headers.get("user-agent") user_agent = request.headers.get("user-agent")
# Converter details para formato serializável # Convert details to serializable format
if details: if details:
# Converter UUIDs para strings # Convert UUIDs to strings
for key, value in details.items(): for key, value in details.items():
if isinstance(value, uuid.UUID): if isinstance(value, uuid.UUID):
details[key] = str(value) details[key] = str(value)
@ -64,7 +64,7 @@ def create_audit_log(
db.refresh(audit_log) db.refresh(audit_log)
logger.info( 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 "") (f" (ID: {resource_id})" if resource_id else "")
) )
@ -72,10 +72,10 @@ def create_audit_log(
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar registro de auditoria: {str(e)}") logger.error(f"Error creating audit log: {str(e)}")
return None return None
except Exception as e: 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 return None
def get_audit_logs( def get_audit_logs(
@ -90,25 +90,25 @@ def get_audit_logs(
end_date: Optional[datetime] = None end_date: Optional[datetime] = None
) -> List[AuditLog]: ) -> List[AuditLog]:
""" """
Obtém registros de auditoria com filtros opcionais Get audit logs with optional filters
Args: Args:
db: Sessão do banco de dados db: Database session
skip: Número de registros para pular skip: Number of records to skip
limit: Número máximo de registros para retornar limit: Maximum number of records to return
user_id: Filtrar por ID do usuário user_id: Filter by user ID
action: Filtrar por ação action: Filter by action
resource_type: Filtrar por tipo de recurso resource_type: Filter by resource type
resource_id: Filtrar por ID do recurso resource_id: Filter by resource ID
start_date: Data inicial start_date: Start date
end_date: Data final end_date: End date
Returns: Returns:
List[AuditLog]: Lista de registros de auditoria List[AuditLog]: List of audit logs
""" """
query = db.query(AuditLog) query = db.query(AuditLog)
# Aplicar filtros, se fornecidos # Apply filters, if provided
if user_id: if user_id:
query = query.filter(AuditLog.user_id == user_id) query = query.filter(AuditLog.user_id == user_id)
@ -127,10 +127,10 @@ def get_audit_logs(
if end_date: if end_date:
query = query.filter(AuditLog.created_at <= 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()) query = query.order_by(AuditLog.created_at.desc())
# Aplicar paginação # Apply pagination
query = query.offset(skip).limit(limit) query = query.offset(skip).limit(limit)
return query.all() return query.all()

View File

@ -1,63 +1,62 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.models.models import User from src.models.models import User
from src.schemas.user import TokenData 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 src.utils.security import create_jwt_token
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt from jose import JWTError, jwt
from src.config.settings import settings from src.config.settings import settings
from src.config.database import get_db from src.config.database import get_db
from datetime import datetime, timedelta from datetime import datetime
import logging import logging
from typing import Optional
logger = logging.getLogger(__name__) 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") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: 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: Args:
token: Token JWT token: JWT token
db: Sessão do banco de dados db: Database session
Returns: Returns:
User: Usuário atual User: Current user
Raises: 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( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais inválidas", detail="Invalid credentials",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
# Decodificar o token # Decode the token
payload = jwt.decode( payload = jwt.decode(
token, token,
settings.JWT_SECRET_KEY, settings.JWT_SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM] algorithms=[settings.JWT_ALGORITHM]
) )
# Extrair dados do token # Extract token data
email: str = payload.get("sub") email: str = payload.get("sub")
if email is None: if email is None:
logger.warning("Token sem email (sub)") logger.warning("Token without email (sub)")
raise credentials_exception raise credentials_exception
# Verificar se o token expirou # Check if the token has expired
exp = payload.get("exp") exp = payload.get("exp")
if exp is None or datetime.fromtimestamp(exp) < datetime.utcnow(): 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 raise credentials_exception
# Criar objeto TokenData # Create TokenData object
token_data = TokenData( token_data = TokenData(
sub=email, sub=email,
exp=datetime.fromtimestamp(exp), 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: 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 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) user = get_user_by_email(db, email=token_data.sub)
if user is None: 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 raise credentials_exception
if not user.is_active: 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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Usuário inativo" detail="Inactive user"
) )
return user return user
async def get_current_active_user(current_user: User = Depends(get_current_user)) -> 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: Args:
current_user: Usuário atual current_user: Current user
Returns: Returns:
User: Usuário atual se estiver ativo User: Current user if active
Raises: Raises:
HTTPException: Se o usuário não estiver ativo HTTPException: If the user is not active
""" """
if not current_user.is_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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Usuário inativo" detail="Inactive user"
) )
return current_user return current_user
async def get_current_admin_user(current_user: User = Depends(get_current_user)) -> 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: Args:
current_user: Usuário atual current_user: Current user
Returns: Returns:
User: Usuário atual se for administrador User: Current user if administrator
Raises: Raises:
HTTPException: Se o usuário não for administrador HTTPException: If the user is not an administrator
""" """
if not current_user.is_admin: 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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Permissão negada. Acesso restrito a administradores." detail="Access denied. Restricted to administrators."
) )
return current_user return current_user
def create_access_token(user: User) -> str: 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: Args:
user: Usuário para o qual criar o token user: User for which to create the token
Returns: Returns:
str: Token JWT str: JWT token
""" """
# Dados a serem incluídos no token # Data to be included in the token
token_data = { token_data = {
"sub": user.email, "sub": user.email,
"is_admin": user.is_admin, "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: if not user.is_admin and user.client_id:
token_data["client_id"] = str(user.client_id) token_data["client_id"] = str(user.client_id)
# Criar token # Create token
return create_jwt_token(token_data) return create_jwt_token(token_data)

View File

@ -12,50 +12,50 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_client(db: Session, client_id: uuid.UUID) -> Optional[Client]: def get_client(db: Session, client_id: uuid.UUID) -> Optional[Client]:
"""Busca um cliente pelo ID""" """Search for a client by ID"""
try: try:
client = db.query(Client).filter(Client.id == client_id).first() client = db.query(Client).filter(Client.id == client_id).first()
if not client: if not client:
logger.warning(f"Cliente não encontrado: {client_id}") logger.warning(f"Client not found: {client_id}")
return None return None
return client return client
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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]: 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: try:
return db.query(Client).offset(skip).limit(limit).all() return db.query(Client).offset(skip).limit(limit).all()
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Erro ao buscar clientes: {str(e)}") logger.error(f"Error searching for clients: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao buscar clientes" detail="Error searching for clients"
) )
def create_client(db: Session, client: ClientCreate) -> Client: def create_client(db: Session, client: ClientCreate) -> Client:
"""Cria um novo cliente""" """Create a new client"""
try: try:
db_client = Client(**client.model_dump()) db_client = Client(**client.model_dump())
db.add(db_client) db.add(db_client)
db.commit() db.commit()
db.refresh(db_client) 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 return db_client
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar cliente: {str(e)}") logger.error(f"Error creating client: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao criar cliente" detail="Error creating client"
) )
def update_client(db: Session, client_id: uuid.UUID, client: ClientCreate) -> Optional[Client]: def update_client(db: Session, client_id: uuid.UUID, client: ClientCreate) -> Optional[Client]:
"""Atualiza um cliente existente""" """Updates an existing client"""
try: try:
db_client = get_client(db, client_id) db_client = get_client(db, client_id)
if not db_client: if not db_client:
@ -66,18 +66,18 @@ def update_client(db: Session, client_id: uuid.UUID, client: ClientCreate) -> Op
db.commit() db.commit()
db.refresh(db_client) db.refresh(db_client)
logger.info(f"Cliente atualizado com sucesso: {client_id}") logger.info(f"Client updated successfully: {client_id}")
return db_client return db_client
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def delete_client(db: Session, client_id: uuid.UUID) -> bool:
"""Remove um cliente""" """Removes a client"""
try: try:
db_client = get_client(db, client_id) db_client = get_client(db, client_id)
if not db_client: if not db_client:
@ -85,54 +85,54 @@ def delete_client(db: Session, client_id: uuid.UUID) -> bool:
db.delete(db_client) db.delete(db_client)
db.commit() db.commit()
logger.info(f"Cliente removido com sucesso: {client_id}") logger.info(f"Client removed successfully: {client_id}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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]: 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: Args:
db: Sessão do banco de dados db: Database session
client_data: Dados do cliente a ser criado client_data: Client data to be created
user_data: Dados do usuário a ser criado user_data: User data to be created
Returns: 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: try:
# Iniciar transação - primeiro cria o cliente # Start transaction - first create the client
client = Client(**client_data.model_dump()) client = Client(**client_data.model_dump())
db.add(client) 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) user, message = create_user(db, user_data, is_admin=False, client_id=client.id)
if not user: 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() db.rollback()
logger.error(f"Erro ao criar usuário para o cliente: {message}") logger.error(f"Error creating user for client: {message}")
return None, f"Erro ao criar usuário: {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() db.commit()
logger.info(f"Cliente e usuário criados com sucesso: {client.id}") logger.info(f"Client and user created successfully: {client.id}")
return client, "Cliente e usuário criados com sucesso" return client, "Client and user created successfully"
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar cliente com usuário: {str(e)}") logger.error(f"Error creating client with user: {str(e)}")
return None, f"Erro ao criar cliente com usuário: {str(e)}" return None, f"Error creating client with user: {str(e)}"
except Exception as e: except Exception as e:
db.rollback() db.rollback()
logger.error(f"Erro inesperado ao criar cliente com usuário: {str(e)}") logger.error(f"Unexpected error creating client with user: {str(e)}")
return None, f"Erro inesperado: {str(e)}" return None, f"Unexpected error: {str(e)}"

View File

@ -10,50 +10,50 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_contact(db: Session, contact_id: uuid.UUID) -> Optional[Contact]: def get_contact(db: Session, contact_id: uuid.UUID) -> Optional[Contact]:
"""Busca um contato pelo ID""" """Search for a contact by ID"""
try: try:
contact = db.query(Contact).filter(Contact.id == contact_id).first() contact = db.query(Contact).filter(Contact.id == contact_id).first()
if not contact: if not contact:
logger.warning(f"Contato não encontrado: {contact_id}") logger.warning(f"Contact not found: {contact_id}")
return None return None
return contact return contact
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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]: 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: try:
return db.query(Contact).filter(Contact.client_id == client_id).offset(skip).limit(limit).all() return db.query(Contact).filter(Contact.client_id == client_id).offset(skip).limit(limit).all()
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def create_contact(db: Session, contact: ContactCreate) -> Contact:
"""Cria um novo contato""" """Create a new contact"""
try: try:
db_contact = Contact(**contact.model_dump()) db_contact = Contact(**contact.model_dump())
db.add(db_contact) db.add(db_contact)
db.commit() db.commit()
db.refresh(db_contact) 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 return db_contact
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar contato: {str(e)}") logger.error(f"Error creating contact: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao criar contato" detail="Error creating contact"
) )
def update_contact(db: Session, contact_id: uuid.UUID, contact: ContactCreate) -> Optional[Contact]: def update_contact(db: Session, contact_id: uuid.UUID, contact: ContactCreate) -> Optional[Contact]:
"""Atualiza um contato existente""" """Update an existing contact"""
try: try:
db_contact = get_contact(db, contact_id) db_contact = get_contact(db, contact_id)
if not db_contact: if not db_contact:
@ -64,18 +64,18 @@ def update_contact(db: Session, contact_id: uuid.UUID, contact: ContactCreate) -
db.commit() db.commit()
db.refresh(db_contact) db.refresh(db_contact)
logger.info(f"Contato atualizado com sucesso: {contact_id}") logger.info(f"Contact updated successfully: {contact_id}")
return db_contact return db_contact
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def delete_contact(db: Session, contact_id: uuid.UUID) -> bool:
"""Remove um contato""" """Remove a contact"""
try: try:
db_contact = get_contact(db, contact_id) db_contact = get_contact(db, contact_id)
if not db_contact: if not db_contact:
@ -83,12 +83,12 @@ def delete_contact(db: Session, contact_id: uuid.UUID) -> bool:
db.delete(db_contact) db.delete(db_contact)
db.commit() db.commit()
logger.info(f"Contato removido com sucesso: {contact_id}") logger.info(f"Contact removed successfully: {contact_id}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao remover contato" detail="Error removing contact"
) )

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional from typing import Any, Dict, List
from google.adk.tools import FunctionTool from google.adk.tools import FunctionTool
import requests import requests
import json import json
@ -11,7 +11,7 @@ class CustomToolBuilder:
self.tools = [] self.tools = []
def _create_http_tool(self, tool_config: Dict[str, Any]) -> FunctionTool: 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"] name = tool_config["name"]
description = tool_config["description"] description = tool_config["description"]
endpoint = tool_config["endpoint"] endpoint = tool_config["endpoint"]
@ -23,35 +23,35 @@ class CustomToolBuilder:
def http_tool(**kwargs): def http_tool(**kwargs):
try: try:
# Combina valores padrão com valores fornecidos # Combines default values with provided values
all_values = {**values, **kwargs} all_values = {**values, **kwargs}
# Substitui placeholders nos headers # Substitutes placeholders in headers
processed_headers = { processed_headers = {
k: v.format(**all_values) if isinstance(v, str) else v k: v.format(**all_values) if isinstance(v, str) else v
for k, v in headers.items() for k, v in headers.items()
} }
# Processa path parameters # Processes path parameters
url = endpoint url = endpoint
for param, value in parameters.get("path_params", {}).items(): for param, value in parameters.get("path_params", {}).items():
if param in all_values: if param in all_values:
url = url.replace(f"{{{param}}}", str(all_values[param])) url = url.replace(f"{{{param}}}", str(all_values[param]))
# Processa query parameters # Process query parameters
query_params = {} query_params = {}
for param, value in parameters.get("query_params", {}).items(): for param, value in parameters.get("query_params", {}).items():
if isinstance(value, list): 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) query_params[param] = ",".join(value)
elif param in all_values: 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] query_params[param] = all_values[param]
else: else:
# Caso contrário, usa o valor padrão da configuração # Otherwise, use the default value from the configuration
query_params[param] = value 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(): for param, value in values.items():
if param not in query_params and param not in parameters.get("path_params", {}): if param not in query_params and param not in parameters.get("path_params", {}):
query_params[param] = value query_params[param] = value
@ -62,12 +62,12 @@ class CustomToolBuilder:
if param in all_values: if param in all_values:
body_data[param] = all_values[param] 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(): 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", {}): if param not in body_data and param not in query_params and param not in parameters.get("path_params", {}):
body_data[param] = value body_data[param] = value
# Faz a requisição HTTP # Makes the HTTP request
response = requests.request( response = requests.request(
method=method, method=method,
url=url, url=url,
@ -79,64 +79,64 @@ class CustomToolBuilder:
if response.status_code >= 400: if response.status_code >= 400:
raise requests.exceptions.HTTPError( 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()) return json.dumps(response.json())
except Exception as e: 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", { return json.dumps(error_handling.get("fallback_response", {
"error": "tool_execution_error", "error": "tool_execution_error",
"message": str(e) "message": str(e)
})) }))
# Adiciona docstring dinâmica baseada na configuração # Adds dynamic docstring based on the configuration
param_docs = [] param_docs = []
# Adiciona path parameters # Adds path parameters
for param, value in parameters.get("path_params", {}).items(): for param, value in parameters.get("path_params", {}).items():
param_docs.append(f"{param}: {value}") param_docs.append(f"{param}: {value}")
# Adiciona query parameters # Adds query parameters
for param, value in parameters.get("query_params", {}).items(): for param, value in parameters.get("query_params", {}).items():
if isinstance(value, list): if isinstance(value, list):
param_docs.append(f"{param}: List[{', '.join(value)}]") param_docs.append(f"{param}: List[{', '.join(value)}]")
else: else:
param_docs.append(f"{param}: {value}") param_docs.append(f"{param}: {value}")
# Adiciona body parameters # Adds body parameters
for param, param_config in parameters.get("body_params", {}).items(): 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']}") param_docs.append(f"{param} ({param_config['type']}, {required}): {param_config['description']}")
# Adiciona valores padrão # Adds default values
if values: if values:
param_docs.append("\nValores padrão:") param_docs.append("\nDefault values:")
for param, value in values.items(): for param, value in values.items():
param_docs.append(f"{param}: {value}") param_docs.append(f"{param}: {value}")
http_tool.__doc__ = f""" http_tool.__doc__ = f"""
{description} {description}
Parâmetros: Parameters:
{chr(10).join(param_docs)} {chr(10).join(param_docs)}
Retorna: Returns:
String contendo a resposta em formato JSON 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 http_tool.__name__ = name
return FunctionTool(func=http_tool) return FunctionTool(func=http_tool)
def build_tools(self, tools_config: Dict[str, Any]) -> List[FunctionTool]: 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 = [] self.tools = []
# Processa ferramentas HTTP # Processes HTTP tools
for http_tool_config in tools_config.get("http_tools", []): for http_tool_config in tools_config.get("http_tools", []):
self.tools.append(self._create_http_tool(http_tool_config)) self.tools.append(self._create_http_tool(http_tool_config))

View File

@ -10,50 +10,50 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_mcp_server(db: Session, server_id: uuid.UUID) -> Optional[MCPServer]: 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: try:
server = db.query(MCPServer).filter(MCPServer.id == server_id).first() server = db.query(MCPServer).filter(MCPServer.id == server_id).first()
if not server: 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 None
return server return server
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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]: 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: try:
return db.query(MCPServer).offset(skip).limit(limit).all() return db.query(MCPServer).offset(skip).limit(limit).all()
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def create_mcp_server(db: Session, server: MCPServerCreate) -> MCPServer:
"""Cria um novo servidor MCP""" """Create a new MCP server"""
try: try:
db_server = MCPServer(**server.model_dump()) db_server = MCPServer(**server.model_dump())
db.add(db_server) db.add(db_server)
db.commit() db.commit()
db.refresh(db_server) 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 return db_server
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar servidor MCP: {str(e)}") logger.error(f"Error creating MCP server: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao criar servidor MCP" detail="Error creating MCP server"
) )
def update_mcp_server(db: Session, server_id: uuid.UUID, server: MCPServerCreate) -> Optional[MCPServer]: 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: try:
db_server = get_mcp_server(db, server_id) db_server = get_mcp_server(db, server_id)
if not db_server: if not db_server:
@ -64,18 +64,18 @@ def update_mcp_server(db: Session, server_id: uuid.UUID, server: MCPServerCreate
db.commit() db.commit()
db.refresh(db_server) 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 return db_server
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def delete_mcp_server(db: Session, server_id: uuid.UUID) -> bool:
"""Remove um servidor MCP""" """Remove an MCP server"""
try: try:
db_server = get_mcp_server(db, server_id) db_server = get_mcp_server(db, server_id)
if not db_server: if not db_server:
@ -83,12 +83,12 @@ def delete_mcp_server(db: Session, server_id: uuid.UUID) -> bool:
db.delete(db_server) db.delete(db_server)
db.commit() db.commit()
logger.info(f"Servidor MCP removido com sucesso: {server_id}") logger.info(f"MCP server removed successfully: {server_id}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao remover servidor MCP" detail="Error removing MCP server"
) )

View File

@ -6,7 +6,6 @@ from google.adk.tools.mcp_tool.mcp_toolset import (
) )
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
import os import os
import logging
from src.utils.logger import setup_logger from src.utils.logger import setup_logger
from src.services.mcp_server_service import get_mcp_server from src.services.mcp_server_service import get_mcp_server
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -19,21 +18,21 @@ class MCPService:
self.exit_stack = AsyncExitStack() self.exit_stack = AsyncExitStack()
async def _connect_to_mcp_server(self, server_config: Dict[str, Any]) -> Tuple[List[Any], Optional[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: try:
# Determina o tipo de servidor (local ou remoto) # Determines the type of server (local or remote)
if "url" in server_config: if "url" in server_config:
# Servidor remoto (SSE) # Remote server (SSE)
connection_params = SseServerParams( connection_params = SseServerParams(
url=server_config["url"], url=server_config["url"],
headers=server_config.get("headers", {}) headers=server_config.get("headers", {})
) )
else: else:
# Servidor local (Stdio) # Local server (Stdio)
command = server_config.get("command", "npx") command = server_config.get("command", "npx")
args = server_config.get("args", []) args = server_config.get("args", [])
# Adiciona variáveis de ambiente se especificadas # Adds environment variables if specified
env = server_config.get("env", {}) env = server_config.get("env", {})
if env: if env:
for key, value in env.items(): for key, value in env.items():
@ -51,13 +50,13 @@ class MCPService:
return tools, exit_stack return tools, exit_stack
except Exception as e: 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 return [], None
def _filter_incompatible_tools(self, tools: List[Any]) -> List[Any]: 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 = [ 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 = [] filtered_tools = []
@ -65,43 +64,43 @@ class MCPService:
for tool in tools: for tool in tools:
if tool.name in problematic_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 removed_count += 1
else: else:
filtered_tools.append(tool) filtered_tools.append(tool)
if removed_count > 0: if removed_count > 0:
logger.warning(f"Removidas {removed_count} ferramentas incompatíveis.") logger.warning(f"Removed {removed_count} incompatible tools.")
return filtered_tools return filtered_tools
def _filter_tools_by_agent(self, tools: List[Any], agent_tools: List[str]) -> List[Any]: 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 = [] filtered_tools = []
for tool in tools: for tool in tools:
logger.info(f"Ferramenta: {tool.name}") logger.info(f"Tool: {tool.name}")
if tool.name in agent_tools: if tool.name in agent_tools:
filtered_tools.append(tool) filtered_tools.append(tool)
return filtered_tools return filtered_tools
async def build_tools(self, mcp_config: Dict[str, Any], db: Session) -> Tuple[List[Any], AsyncExitStack]: 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.tools = []
self.exit_stack = AsyncExitStack() 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", []): for server in mcp_config.get("mcp_servers", []):
try: try:
# Busca o servidor MCP no banco # Search for the MCP server in the database
mcp_server = get_mcp_server(db, server['id']) mcp_server = get_mcp_server(db, server['id'])
if not mcp_server: if not mcp_server:
logger.warning(f"Servidor MCP não encontrado: {server['id']}") logger.warning(f"Servidor MCP não encontrado: {server['id']}")
continue continue
# Prepara a configuração do servidor # Prepares the server configuration
server_config = mcp_server.config_json.copy() 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: if 'env' in server_config:
for key, value in server_config['env'].items(): for key, value in server_config['env'].items():
if value.startswith('env@@'): if value.startswith('env@@'):
@ -109,31 +108,31 @@ class MCPService:
if env_key in server.get('envs', {}): if env_key in server.get('envs', {}):
server_config['env'][key] = server['envs'][env_key] server_config['env'][key] = server['envs'][env_key]
else: 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 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) tools, exit_stack = await self._connect_to_mcp_server(server_config)
if tools and exit_stack: if tools and exit_stack:
# Filtra ferramentas incompatíveis # Filters incompatible tools
filtered_tools = self._filter_incompatible_tools(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', []) agent_tools = server.get('tools', [])
filtered_tools = self._filter_tools_by_agent(filtered_tools, agent_tools) filtered_tools = self._filter_tools_by_agent(filtered_tools, agent_tools)
self.tools.extend(filtered_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) 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: 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: 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 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 return self.tools, self.exit_stack

View File

@ -7,8 +7,7 @@ from typing import Optional, List
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from src.services.agent_service import get_agent, get_agents_by_client from src.services.agent_service import get_agents_by_client
from src.services.contact_service import get_contact
import uuid import uuid
import logging import logging
@ -20,7 +19,7 @@ def get_sessions_by_client(
db: Session, db: Session,
client_id: uuid.UUID, client_id: uuid.UUID,
) -> List[SessionModel]: ) -> List[SessionModel]:
"""Busca sessões de um cliente com paginação""" """Search for sessions of a client with pagination"""
try: try:
agents_by_client = get_agents_by_client(db, client_id) agents_by_client = get_agents_by_client(db, client_id)
sessions = [] sessions = []
@ -29,10 +28,10 @@ def get_sessions_by_client(
return sessions return sessions
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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, skip: int = 0,
limit: int = 100, limit: int = 100,
) -> List[SessionModel]: ) -> List[SessionModel]:
"""Busca sessões de um agente com paginação""" """Search for sessions of an agent with pagination"""
try: try:
agent_id_str = str(agent_id) agent_id_str = str(agent_id)
query = db.query(SessionModel).filter(SessionModel.app_name == agent_id_str) query = db.query(SessionModel).filter(SessionModel.app_name == agent_id_str)
return query.offset(skip).limit(limit).all() return query.offset(skip).limit(limit).all()
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao buscar sessões", detail="Error searching for sessions",
) )
def get_session_by_id( def get_session_by_id(
session_service: DatabaseSessionService, session_id: str session_service: DatabaseSessionService, session_id: str
) -> Optional[SessionADK]: ) -> Optional[SessionADK]:
"""Busca uma sessão pelo ID""" """Search for a session by ID"""
try: try:
if not session_id or "_" not in session_id: 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( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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) parts = session_id.split("_", 1)
if len(parts) != 2: 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( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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 user_id, app_name = parts
@ -85,28 +84,28 @@ def get_session_by_id(
) )
if session is None: if session is None:
logger.error(f"Sessão não encontrada: {session_id}") logger.error(f"Session not found: {session_id}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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 return session
except Exception as e: 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): if isinstance(e, HTTPException):
raise e raise e
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erro ao buscar sessão: {str(e)}", detail=f"Error searching for session: {str(e)}",
) )
def delete_session(session_service: DatabaseSessionService, session_id: str) -> None: def delete_session(session_service: DatabaseSessionService, session_id: str) -> None:
"""Deleta uma sessão pelo ID""" """Deletes a session by ID"""
try: try:
session = get_session_by_id(session_service, session_id) 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( session_service.delete_session(
app_name=session.app_name, app_name=session.app_name,
@ -115,34 +114,34 @@ def delete_session(session_service: DatabaseSessionService, session_id: str) ->
) )
return None return None
except HTTPException: except HTTPException:
# Repassa exceções HTTP do get_session_by_id # Passes HTTP exceptions from get_session_by_id
raise raise
except Exception as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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( def get_session_events(
session_service: DatabaseSessionService, session_id: str session_service: DatabaseSessionService, session_id: str
) -> List[Event]: ) -> List[Event]:
"""Busca os eventos de uma sessão pelo ID""" """Search for the events of a session by ID"""
try: try:
session = get_session_by_id(session_service, session_id) 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: if not hasattr(session, 'events') or session.events is None:
return [] return []
return session.events return session.events
except HTTPException: except HTTPException:
# Repassa exceções HTTP do get_session_by_id # Passes HTTP exceptions from get_session_by_id
raise raise
except Exception as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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)}",
) )

View File

@ -10,50 +10,50 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_tool(db: Session, tool_id: uuid.UUID) -> Optional[Tool]: def get_tool(db: Session, tool_id: uuid.UUID) -> Optional[Tool]:
"""Busca uma ferramenta pelo ID""" """Search for a tool by ID"""
try: try:
tool = db.query(Tool).filter(Tool.id == tool_id).first() tool = db.query(Tool).filter(Tool.id == tool_id).first()
if not tool: if not tool:
logger.warning(f"Ferramenta não encontrada: {tool_id}") logger.warning(f"Tool not found: {tool_id}")
return None return None
return tool return tool
except SQLAlchemyError as e: 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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]: 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: try:
return db.query(Tool).offset(skip).limit(limit).all() return db.query(Tool).offset(skip).limit(limit).all()
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Erro ao buscar ferramentas: {str(e)}") logger.error(f"Error searching for tools: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao buscar ferramentas" detail="Error searching for tools"
) )
def create_tool(db: Session, tool: ToolCreate) -> Tool: def create_tool(db: Session, tool: ToolCreate) -> Tool:
"""Cria uma nova ferramenta""" """Creates a new tool"""
try: try:
db_tool = Tool(**tool.model_dump()) db_tool = Tool(**tool.model_dump())
db.add(db_tool) db.add(db_tool)
db.commit() db.commit()
db.refresh(db_tool) 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 return db_tool
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar ferramenta: {str(e)}") logger.error(f"Error creating tool: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao criar ferramenta" detail="Error creating tool"
) )
def update_tool(db: Session, tool_id: uuid.UUID, tool: ToolCreate) -> Optional[Tool]: def update_tool(db: Session, tool_id: uuid.UUID, tool: ToolCreate) -> Optional[Tool]:
"""Atualiza uma ferramenta existente""" """Updates an existing tool"""
try: try:
db_tool = get_tool(db, tool_id) db_tool = get_tool(db, tool_id)
if not db_tool: if not db_tool:
@ -64,18 +64,18 @@ def update_tool(db: Session, tool_id: uuid.UUID, tool: ToolCreate) -> Optional[T
db.commit() db.commit()
db.refresh(db_tool) db.refresh(db_tool)
logger.info(f"Ferramenta atualizada com sucesso: {tool_id}") logger.info(f"Tool updated successfully: {tool_id}")
return db_tool return db_tool
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: def delete_tool(db: Session, tool_id: uuid.UUID) -> bool:
"""Remove uma ferramenta""" """Remove a tool"""
try: try:
db_tool = get_tool(db, tool_id) db_tool = get_tool(db, tool_id)
if not db_tool: if not db_tool:
@ -83,12 +83,12 @@ def delete_tool(db: Session, tool_id: uuid.UUID) -> bool:
db.delete(db_tool) db.delete(db_tool)
db.commit() db.commit()
logger.info(f"Ferramenta removida com sucesso: {tool_id}") logger.info(f"Tool removed successfully: {tool_id}")
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao remover ferramenta" detail="Error removing tool"
) )

View File

@ -1,7 +1,7 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from src.models.models import User, Client 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.utils.security import get_password_hash, verify_password, generate_token
from src.services.email_service import send_verification_email, send_password_reset_email from src.services.email_service import send_verification_email, send_password_reset_email
from datetime import datetime, timedelta 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]: 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: Args:
db: Sessão do banco de dados db: Database session
user_data: Dados do usuário a ser criado user_data: User data to be created
is_admin: Se o usuário é um administrador is_admin: If the user is an administrator
client_id: ID do cliente associado (opcional, será criado um novo se não fornecido) client_id: Associated client ID (optional, a new one will be created if not provided)
Returns: 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: try:
# Verificar se email já existe # Check if email already exists
db_user = db.query(User).filter(User.email == user_data.email).first() db_user = db.query(User).filter(User.email == user_data.email).first()
if db_user: if db_user:
logger.warning(f"Tentativa de cadastro com email já existente: {user_data.email}") logger.warning(f"Attempt to register with existing email: {user_data.email}")
return None, "Email já cadastrado" return None, "Email already registered"
# Criar token de verificação # Create verification token
verification_token = generate_token() verification_token = generate_token()
token_expiry = datetime.utcnow() + timedelta(hours=24) token_expiry = datetime.utcnow() + timedelta(hours=24)
# Iniciar transação # Start transaction
user = None user = None
local_client_id = client_id local_client_id = client_id
try: 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: if not is_admin and local_client_id is None:
client = Client(name=user_data.name) client = Client(name=user_data.name)
db.add(client) db.add(client)
db.flush() # Obter o ID do cliente db.flush() # Get the client ID
local_client_id = client.id local_client_id = client.id
# Criar usuário # Create user
user = User( user = User(
email=user_data.email, email=user_data.email,
password_hash=get_password_hash(user_data.password), password_hash=get_password_hash(user_data.password),
client_id=local_client_id, client_id=local_client_id,
is_admin=is_admin, is_admin=is_admin,
is_active=False, # Inativo até verificar email is_active=False, # Inactive until email is verified
email_verified=False, email_verified=False,
verification_token=verification_token, verification_token=verification_token,
verification_token_expiry=token_expiry 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.add(user)
db.commit() db.commit()
# Enviar email de verificação # Send verification email
email_sent = send_verification_email(user.email, verification_token) email_sent = send_verification_email(user.email, verification_token)
if not email_sent: if not email_sent:
logger.error(f"Falha ao enviar email de verificação para {user.email}") logger.error(f"Failed to send verification email to {user.email}")
# Não fazemos rollback aqui, apenas logamos o erro # We don't do rollback here, we just log the error
logger.info(f"Usuário criado com sucesso: {user.email}") logger.info(f"User created successfully: {user.email}")
return user, "Usuário criado com sucesso. Verifique seu email para ativar sua conta." return user, "User created successfully. Check your email to activate your account."
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao criar usuário: {str(e)}") logger.error(f"Error creating user: {str(e)}")
return None, f"Erro ao criar usuário: {str(e)}" return None, f"Error creating user: {str(e)}"
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao criar usuário: {str(e)}") logger.error(f"Unexpected error creating user: {str(e)}")
return None, f"Erro inesperado: {str(e)}" return None, f"Unexpected error: {str(e)}"
def verify_email(db: Session, token: str) -> Tuple[bool, str]: 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: Args:
db: Sessão do banco de dados db: Database session
token: Token de verificação token: Verification token
Returns: Returns:
Tuple[bool, str]: Tupla com status da verificação e mensagem Tuple[bool, str]: Tuple with verification status and message
""" """
try: try:
# Buscar usuário pelo token # Search for user by token
user = db.query(User).filter(User.verification_token == token).first() user = db.query(User).filter(User.verification_token == token).first()
if not user: if not user:
logger.warning(f"Tentativa de verificação com token inválido: {token}") logger.warning(f"Attempt to verify with invalid token: {token}")
return False, "Token de verificação inválido" return False, "Invalid verification token"
# Verificar se o token expirou # Check if the token has expired
now = datetime.utcnow() now = datetime.utcnow()
expiry = user.verification_token_expiry 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: 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) now = now.replace(tzinfo=expiry.tzinfo)
elif now.tzinfo is not None and expiry.tzinfo is None: 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) expiry = expiry.replace(tzinfo=now.tzinfo)
if expiry < now: if expiry < now:
logger.warning(f"Tentativa de verificação com token expirado para usuário: {user.email}") logger.warning(f"Attempt to verify with expired token for user: {user.email}")
return False, "Token de verificação expirado" return False, "Verification token expired"
# Atualizar usuário # Update user
user.email_verified = True user.email_verified = True
user.is_active = True user.is_active = True
user.verification_token = None user.verification_token = None
user.verification_token_expiry = None user.verification_token_expiry = None
db.commit() db.commit()
logger.info(f"Email verificado com sucesso para usuário: {user.email}") logger.info(f"Email verified successfully for user: {user.email}")
return True, "Email verificado com sucesso. Sua conta está ativa." return True, "Email verified successfully. Your account is active."
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao verificar email: {str(e)}") logger.error(f"Error verifying email: {str(e)}")
return False, f"Erro ao verificar email: {str(e)}" return False, f"Error verifying email: {str(e)}"
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao verificar email: {str(e)}") logger.error(f"Unexpected error verifying email: {str(e)}")
return False, f"Erro inesperado: {str(e)}" return False, f"Unexpected error: {str(e)}"
def resend_verification(db: Session, email: str) -> Tuple[bool, str]: def resend_verification(db: Session, email: str) -> Tuple[bool, str]:
""" """
Reenvia o email de verificação Resend the verification email
Args: Args:
db: Sessão do banco de dados db: Database session
email: Email do usuário email: User email
Returns: Returns:
Tuple[bool, str]: Tupla com status da operação e mensagem Tuple[bool, str]: Tuple with operation status and message
""" """
try: try:
# Buscar usuário pelo email # Search for user by email
user = db.query(User).filter(User.email == email).first() user = db.query(User).filter(User.email == email).first()
if not user: if not user:
logger.warning(f"Tentativa de reenvio de verificação para email inexistente: {email}") logger.warning(f"Attempt to resend verification email for non-existent email: {email}")
return False, "Email não encontrado" return False, "Email not found"
if user.email_verified: if user.email_verified:
logger.info(f"Tentativa de reenvio de verificação para email já verificado: {email}") logger.info(f"Attempt to resend verification email for already verified email: {email}")
return False, "Email já foi verificado" return False, "Email already verified"
# Gerar novo token # Generate new token
verification_token = generate_token() verification_token = generate_token()
token_expiry = datetime.utcnow() + timedelta(hours=24) token_expiry = datetime.utcnow() + timedelta(hours=24)
# Atualizar usuário # Update user
user.verification_token = verification_token user.verification_token = verification_token
user.verification_token_expiry = token_expiry user.verification_token_expiry = token_expiry
db.commit() db.commit()
# Enviar email # Send email
email_sent = send_verification_email(user.email, verification_token) email_sent = send_verification_email(user.email, verification_token)
if not email_sent: if not email_sent:
logger.error(f"Falha ao reenviar email de verificação para {user.email}") logger.error(f"Failed to resend verification email to {user.email}")
return False, "Falha ao enviar email de verificação" return False, "Failed to send verification email"
logger.info(f"Email de verificação reenviado com sucesso para: {user.email}") logger.info(f"Verification email resent successfully to: {user.email}")
return True, "Email de verificação reenviado. Verifique sua caixa de entrada." return True, "Verification email resent. Check your inbox."
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao reenviar verificação: {str(e)}") logger.error(f"Error resending verification: {str(e)}")
return False, f"Erro ao reenviar verificação: {str(e)}" return False, f"Error resending verification: {str(e)}"
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao reenviar verificação: {str(e)}") logger.error(f"Unexpected error resending verification: {str(e)}")
return False, f"Erro inesperado: {str(e)}" return False, f"Unexpected error: {str(e)}"
def forgot_password(db: Session, email: str) -> Tuple[bool, str]: def forgot_password(db: Session, email: str) -> Tuple[bool, str]:
""" """
Inicia o processo de recuperação de senha Initiates the password recovery process
Args: Args:
db: Sessão do banco de dados db: Database session
email: Email do usuário email: User email
Returns: Returns:
Tuple[bool, str]: Tupla com status da operação e mensagem Tuple[bool, str]: Tuple with operation status and message
""" """
try: try:
# Buscar usuário pelo email # Search for user by email
user = db.query(User).filter(User.email == email).first() user = db.query(User).filter(User.email == email).first()
if not user: if not user:
# Por segurança, não informamos se o email existe ou não # For security, we don't inform if the email exists or not
logger.info(f"Tentativa de recuperação de senha para email inexistente: {email}") logger.info(f"Attempt to recover password for non-existent email: {email}")
return True, "Se o email estiver cadastrado, você receberá instruções para redefinir sua senha." 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() 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_token = reset_token
user.password_reset_expiry = token_expiry user.password_reset_expiry = token_expiry
db.commit() db.commit()
# Enviar email # Send email
email_sent = send_password_reset_email(user.email, reset_token) email_sent = send_password_reset_email(user.email, reset_token)
if not email_sent: if not email_sent:
logger.error(f"Falha ao enviar email de recuperação de senha para {user.email}") logger.error(f"Failed to send password reset email to {user.email}")
return False, "Falha ao enviar email de recuperação de senha" return False, "Failed to send password reset email"
logger.info(f"Email de recuperação de senha enviado com sucesso para: {user.email}") logger.info(f"Password reset email sent successfully to: {user.email}")
return True, "Se o email estiver cadastrado, você receberá instruções para redefinir sua senha." return True, "If the email is registered, you will receive instructions to reset your password."
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao processar recuperação de senha: {str(e)}") logger.error(f"Error processing password recovery: {str(e)}")
return False, f"Erro ao processar recuperação de senha: {str(e)}" return False, f"Error processing password recovery: {str(e)}"
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao processar recuperação de senha: {str(e)}") logger.error(f"Unexpected error processing password recovery: {str(e)}")
return False, f"Erro inesperado: {str(e)}" return False, f"Unexpected error: {str(e)}"
def reset_password(db: Session, token: str, new_password: str) -> Tuple[bool, str]: 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: Args:
db: Sessão do banco de dados db: Database session
token: Token de redefinição de senha token: Password reset token
new_password: Nova senha new_password: New password
Returns: Returns:
Tuple[bool, str]: Tupla com status da operação e mensagem Tuple[bool, str]: Tuple with operation status and message
""" """
try: try:
# Buscar usuário pelo token # Search for user by token
user = db.query(User).filter(User.password_reset_token == token).first() user = db.query(User).filter(User.password_reset_token == token).first()
if not user: if not user:
logger.warning(f"Tentativa de redefinição de senha com token inválido: {token}") logger.warning(f"Attempt to reset password with invalid token: {token}")
return False, "Token de redefinição de senha inválido" return False, "Invalid password reset token"
# Verificar se o token expirou # Check if the token has expired
if user.password_reset_expiry < datetime.utcnow(): if user.password_reset_expiry < datetime.utcnow():
logger.warning(f"Tentativa de redefinição de senha com token expirado para usuário: {user.email}") logger.warning(f"Attempt to reset password with expired token for user: {user.email}")
return False, "Token de redefinição de senha expirado" return False, "Password reset token expired"
# Atualizar senha # Update password
user.password_hash = get_password_hash(new_password) user.password_hash = get_password_hash(new_password)
user.password_reset_token = None user.password_reset_token = None
user.password_reset_expiry = None user.password_reset_expiry = None
db.commit() db.commit()
logger.info(f"Senha redefinida com sucesso para usuário: {user.email}") logger.info(f"Password reset successfully for user: {user.email}")
return True, "Senha redefinida com sucesso. Você já pode fazer login com sua nova senha." return True, "Password reset successfully. You can now login with your new password."
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao redefinir senha: {str(e)}") logger.error(f"Error resetting password: {str(e)}")
return False, f"Erro ao redefinir senha: {str(e)}" return False, f"Error resetting password: {str(e)}"
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao redefinir senha: {str(e)}") logger.error(f"Unexpected error resetting password: {str(e)}")
return False, f"Erro inesperado: {str(e)}" return False, f"Unexpected error: {str(e)}"
def get_user_by_email(db: Session, email: str) -> Optional[User]: def get_user_by_email(db: Session, email: str) -> Optional[User]:
""" """
Busca um usuário pelo email Searches for a user by email
Args: Args:
db: Sessão do banco de dados db: Database session
email: Email do usuário email: User email
Returns: Returns:
Optional[User]: Usuário encontrado ou None Optional[User]: User found or None
""" """
try: try:
return db.query(User).filter(User.email == email).first() return db.query(User).filter(User.email == email).first()
except Exception as e: 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 return None
def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: 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: Args:
db: Sessão do banco de dados db: Database session
email: Email do usuário email: User email
password: Senha do usuário password: User password
Returns: Returns:
Optional[User]: Usuário autenticado ou None Optional[User]: Authenticated user or None
""" """
user = get_user_by_email(db, email) user = get_user_by_email(db, email)
if not user: 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): def get_admin_users(db: Session, skip: int = 0, limit: int = 100):
""" """
Lista os usuários administradores Lists the admin users
Args: Args:
db: Sessão do banco de dados db: Database session
skip: Número de registros para pular skip: Number of records to skip
limit: Número máximo de registros para retornar limit: Maximum number of records to return
Returns: Returns:
List[User]: Lista de usuários administradores List[User]: List of admin users
""" """
try: try:
users = db.query(User).filter(User.is_admin == True).offset(skip).limit(limit).all() 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 return users
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Erro ao listar administradores: {str(e)}") logger.error(f"Error listing admins: {str(e)}")
return [] return []
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao listar administradores: {str(e)}") logger.error(f"Unexpected error listing admins: {str(e)}")
return [] return []
def create_admin_user(db: Session, user_data: UserCreate) -> Tuple[Optional[User], str]: 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: Args:
db: Sessão do banco de dados db: Database session
user_data: Dados do usuário a ser criado user_data: User data to be created
Returns: 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) return create_user(db, user_data, is_admin=True)
def deactivate_user(db: Session, user_id: uuid.UUID) -> Tuple[bool, str]: 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: Args:
db: Sessão do banco de dados db: Database session
user_id: ID do usuário a ser desativado user_id: ID of the user to be deactivated
Returns: Returns:
Tuple[bool, str]: Tupla com status da operação e mensagem Tuple[bool, str]: Tuple with operation status and message
""" """
try: try:
# Buscar usuário pelo ID # Search for user by ID
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
if not user: if not user:
logger.warning(f"Tentativa de desativação de usuário inexistente: {user_id}") logger.warning(f"Attempt to deactivate non-existent user: {user_id}")
return False, "Usuário não encontrado" return False, "User not found"
# Desativar usuário # Deactivate user
user.is_active = False user.is_active = False
db.commit() db.commit()
logger.info(f"Usuário desativado com sucesso: {user.email}") logger.info(f"User deactivated successfully: {user.email}")
return True, "Usuário desativado com sucesso" return True, "User deactivated successfully"
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.rollback() db.rollback()
logger.error(f"Erro ao desativar usuário: {str(e)}") logger.error(f"Error deactivating user: {str(e)}")
return False, f"Erro ao desativar usuário: {str(e)}" return False, f"Error deactivating user: {str(e)}"
except Exception as e: except Exception as e:
logger.error(f"Erro inesperado ao desativar usuário: {str(e)}") logger.error(f"Unexpected error deactivating user: {str(e)}")
return False, f"Erro inesperado: {str(e)}" return False, f"Unexpected error: {str(e)}"

View File

@ -1,11 +1,10 @@
import logging import logging
import os import os
import sys import sys
from typing import Optional
from src.config.settings import settings from src.config.settings import settings
class CustomFormatter(logging.Formatter): class CustomFormatter(logging.Formatter):
"""Formatação personalizada para logs""" """Custom formatter for logs"""
grey = "\x1b[38;20m" grey = "\x1b[38;20m"
yellow = "\x1b[33;20m" yellow = "\x1b[33;20m"
@ -30,31 +29,31 @@ class CustomFormatter(logging.Formatter):
def setup_logger(name: str) -> logging.Logger: def setup_logger(name: str) -> logging.Logger:
""" """
Configura um logger personalizado Configures a custom logger
Args: Args:
name: Nome do logger name: Logger name
Returns: Returns:
logging.Logger: Logger configurado logging.Logger: Logger configurado
""" """
logger = logging.getLogger(name) logger = logging.getLogger(name)
# Remove handlers existentes para evitar duplicação # Remove existing handlers to avoid duplication
if logger.handlers: if logger.handlers:
logger.handlers.clear() 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()) log_level = getattr(logging, os.getenv("LOG_LEVEL", settings.LOG_LEVEL).upper())
logger.setLevel(log_level) logger.setLevel(log_level)
# Handler para console # Console handler
console_handler = logging.StreamHandler(sys.stdout) console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(CustomFormatter()) console_handler.setFormatter(CustomFormatter())
console_handler.setLevel(log_level) console_handler.setLevel(log_level)
logger.addHandler(console_handler) 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 logger.propagate = False
return logger return logger

View File

@ -10,7 +10,7 @@ from dataclasses import dataclass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Corrigir erro do bcrypt com passlib # Fix bcrypt error with passlib
if not hasattr(bcrypt, '__about__'): if not hasattr(bcrypt, '__about__'):
@dataclass @dataclass
class BcryptAbout: class BcryptAbout:
@ -18,19 +18,19 @@ if not hasattr(bcrypt, '__about__'):
setattr(bcrypt, "__about__", BcryptAbout()) setattr(bcrypt, "__about__", BcryptAbout())
# Contexto para hash de senhas usando bcrypt # Context for password hashing using bcrypt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
"""Cria um hash da senha fornecida""" """Creates a password hash"""
return pwd_context.hash(password) return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool: 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) return pwd_context.verify(plain_password, hashed_password)
def create_jwt_token(data: dict, expires_delta: timedelta = None) -> str: def create_jwt_token(data: dict, expires_delta: timedelta = None) -> str:
"""Cria um token JWT""" """Creates a JWT token"""
to_encode = data.copy() to_encode = data.copy()
if expires_delta: if expires_delta:
expire = datetime.utcnow() + 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 return encoded_jwt
def generate_token(length: int = 32) -> str: 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 alphabet = string.ascii_letters + string.digits
token = ''.join(secrets.choice(alphabet) for _ in range(length)) token = ''.join(secrets.choice(alphabet) for _ in range(length))
return token return token