translate to english
This commit is contained in:
parent
c3baa54215
commit
215e76012e
@ -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
|
||||||
|
28
.env.example
28
.env.example
@ -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"
|
||||||
|
19
docs/README.md
Normal file
19
docs/README.md
Normal 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.
|
65
docs/contributing/CODE_OF_CONDUCT.md
Normal file
65
docs/contributing/CODE_OF_CONDUCT.md
Normal 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.
|
129
docs/contributing/CONTRIBUTING.md
Normal file
129
docs/contributing/CONTRIBUTING.md
Normal 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
213
docs/technical/API_FLOW.md
Normal 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
|
222
docs/technical/ARCHITECTURE.md
Normal file
222
docs/technical/ARCHITECTURE.md
Normal 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
|
317
docs/technical/DATA_MODEL.md
Normal file
317
docs/technical/DATA_MODEL.md
Normal 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.
|
509
docs/technical/DEPLOYMENT.md
Normal file
509
docs/technical/DEPLOYMENT.md
Normal 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.
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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__":
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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
121
src/api/agent_routes.py
Normal 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"
|
||||||
|
)
|
@ -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
74
src/api/chat_routes.py
Normal 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
140
src/api/client_routes.py
Normal 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
122
src/api/contact_routes.py
Normal 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"
|
||||||
|
)
|
97
src/api/mcp_server_routes.py
Normal file
97
src/api/mcp_server_routes.py
Normal 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"
|
||||||
|
)
|
@ -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
138
src/api/session_routes.py
Normal 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
97
src/api/tool_routes.py
Normal 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"
|
||||||
|
)
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
|
74
src/main.py
74
src/main.py
@ -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'"
|
||||||
}
|
}
|
||||||
|
@ -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")
|
@ -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
|
@ -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
|
||||||
|
@ -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")
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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))
|
||||||
|
@ -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",
|
||||||
)
|
)
|
||||||
|
@ -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()
|
@ -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)
|
@ -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)}"
|
@ -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"
|
||||||
)
|
)
|
@ -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))
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
@ -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
|
@ -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)}",
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
)
|
)
|
@ -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)}"
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue
Block a user