From 422639a629dd0ba8b208027f0f94ee7988ac733b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 7 May 2025 06:43:33 -0300 Subject: [PATCH] chore(cleanup): remove unused files and update documentation for clarity --- .env | 50 --- .gitignore | 2 + README.md | 174 ++++++++- .../versions/c107446e38aa_init_migrations.py | 132 ------- .../versions/ebac70616dab_worflow_agent.py | 49 --- src/config/settings.py | 2 +- src/schemas/schemas.py | 2 +- src/schemas/streaming.py | 2 +- src/services/agent_service.py | 13 - src/services/custom_tools.py | 1 - src/services/workflow_agent.py | 293 ++++++++------- src/utils/streaming.py | 28 +- static/test_a2a_stream.html | 295 --------------- static/test_chat_stream.html | 343 ------------------ tests/__init__.py | 1 - tests/api/__init__.py | 1 - tests/api/test_root.py | 11 - tests/services/__init__.py | 1 - tests/services/test_auth_service.py | 27 -- 19 files changed, 333 insertions(+), 1094 deletions(-) delete mode 100644 .env delete mode 100644 migrations/versions/c107446e38aa_init_migrations.py delete mode 100644 migrations/versions/ebac70616dab_worflow_agent.py delete mode 100644 static/test_a2a_stream.html delete mode 100644 static/test_chat_stream.html delete mode 100644 tests/__init__.py delete mode 100644 tests/api/__init__.py delete mode 100644 tests/api/test_root.py delete mode 100644 tests/services/__init__.py delete mode 100644 tests/services/test_auth_service.py diff --git a/.env b/.env deleted file mode 100644 index 3c62fc04..00000000 --- a/.env +++ /dev/null @@ -1,50 +0,0 @@ -API_TITLE="Evo API" -API_DESCRIPTION="API para execução de agentes de IA" -API_VERSION="1.0.0" -API_URL="http://localhost:8000" - -ORGANIZATION_NAME="Evo AI" -ORGANIZATION_URL="https://evoai.evoapicloud.com" - -# Database settings -POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai" - -# Logging settings -LOG_LEVEL="INFO" -LOG_DIR="logs" - -# Redis settings -REDIS_HOST="localhost" -REDIS_PORT=6379 -REDIS_DB=8 -REDIS_PASSWORD="" -REDIS_SSL=false -REDIS_KEY_PREFIX="a2a:" -REDIS_TTL=3600 - -# Tools cache TTL in seconds (1 hour) -TOOLS_CACHE_TTL=3600 - -# JWT settings -JWT_SECRET_KEY="f6884ef5be4c279686ff90f0ed9d4656685eef9807245019ac94a3fbe32b0938" -JWT_ALGORITHM="HS256" -JWT_EXPIRATION_TIME=3600 - -# SendGrid -SENDGRID_API_KEY="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" -EMAIL_FROM="noreply@evolution-api.com" -APP_URL="https://evoai.evoapicloud.com" - -# Server settings -HOST="0.0.0.0" -PORT=8000 -DEBUG=false - -# Seeders settings -ADMIN_EMAIL="admin@evoai.com" -ADMIN_INITIAL_PASSWORD="senhaforte123" -DEMO_EMAIL="demo@exemplo.com" -DEMO_PASSWORD="demo123" -DEMO_CLIENT_NAME="Cliente Demo" - -# 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 diff --git a/.gitignore b/.gitignore index fe0630c1..faabdc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ celerybeat-schedule Thumbs.db uv.lock + +migrations/versions/* diff --git a/README.md b/README.md index e1e7981d..49b5d157 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Evo AI - AI Agents Platform -Evo AI is an free platform for creating and managing AI agents, enabling integration with different AI models and services. +Evo AI is an open-source platform for creating and managing AI agents, enabling integration with different AI models and services. ## 🚀 Overview @@ -13,6 +13,7 @@ The Evo AI platform allows: - Custom tools management - JWT authentication with email verification - **Agent 2 Agent (A2A) Protocol Support**: Interoperability between AI agents following Google's A2A specification +- **Workflow Agent with LangGraph**: Building complex agent workflows with LangGraph and ReactFlow ## 🤖 Agent Types and Creation @@ -120,6 +121,27 @@ Executes sub-agents in a loop with a defined maximum number of iterations. } ``` +### 6. Workflow Agent + +Executes sub-agents in a custom workflow defined by a graph structure. This agent type uses LangGraph for implementing complex agent workflows with conditional execution paths. + +```json +{ + "client_id": "{{client_id}}", + "name": "workflow_agent", + "type": "workflow", + "config": { + "sub_agents": ["agent-uuid-1", "agent-uuid-2", "agent-uuid-3"], + "workflow": { + "nodes": [], + "edges": [] + } + } +} +``` + +The workflow structure is built using ReactFlow in the frontend, allowing visual creation and editing of complex agent workflows with nodes (representing agents or decision points) and edges (representing flow connections). + ### Common Characteristics - All agent types can have sub-agents @@ -268,6 +290,8 @@ Authorization: Bearer your-token-jwt - **SendGrid**: Email service for notifications - **Jinja2**: Template engine for email rendering - **Bcrypt**: Password hashing and security +- **LangGraph**: Framework for building stateful, multi-agent workflows +- **ReactFlow**: Library for building node-based visual workflows ## 🤖 Agent 2 Agent (A2A) Protocol Support @@ -337,9 +361,25 @@ src/ └── config/ # Configurations ``` +## 📋 Prerequisites + +Before starting, make sure you have the following installed: + +- **Python**: 3.10 or higher +- **PostgreSQL**: 13.0 or higher +- **Redis**: 6.0 or higher +- **Git**: For version control +- **Make**: For running Makefile commands (usually pre-installed on Linux/Mac, for Windows use WSL or install via chocolatey) + +You'll also need the following accounts/API keys: + +- **OpenAI API Key**: Or API key from another AI provider +- **SendGrid Account**: For email functionality +- **Google API Key**: If using Google's A2A protocol implementation + ## 📋 Requirements -- Python 3.8+ +- Python 3.10+ - PostgreSQL - Redis - OpenAI API Key (or other AI provider) @@ -365,6 +405,14 @@ venv\Scripts\activate # Windows 3. Install dependencies: +```bash +pip install -e . # For basic installation +# or +pip install -e ".[dev]" # For development dependencies +``` + +Or using the Makefile: + ```bash make install # For basic installation # or @@ -378,12 +426,81 @@ cp .env.example .env # Edit the .env file with your settings ``` -5. Run migrations: +5. Initialize the database and run migrations: ```bash -make alembic-upgrade +make alembic-migrate message="init migrations" ``` +6. Seed the database with initial data: + +```bash +make seed-all +``` + +## 🚀 Getting Started + +After installation, follow these steps to set up your first agent: + +1. **Configure MCP Server**: Set up your Model Control Protocol server configuration first +2. **Create Client or Register**: Create a new client or register a user account +3. **Create Agents**: Set up the agents according to your needs (LLM, A2A, Sequential, Parallel, Loop, or Workflow) + +### Configuration (.env file) + +Configure your environment using the following key settings: + +```bash +# Database settings +POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai" + +# Redis settings +REDIS_HOST="localhost" +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD="your-redis-password" + +# JWT settings +JWT_SECRET_KEY="your-jwt-secret-key" +JWT_ALGORITHM="HS256" +JWT_EXPIRATION_TIME=30 # In minutes + +# SendGrid for emails +SENDGRID_API_KEY="your-sendgrid-api-key" +EMAIL_FROM="noreply@yourdomain.com" +APP_URL="https://yourdomain.com" + +# A2A settings +A2A_TASK_TTL=3600 +A2A_HISTORY_TTL=86400 +``` + +### Project Dependencies + +The project uses modern Python packaging standards with `pyproject.toml`. Key dependencies include: + +```toml +dependencies = [ + "fastapi==0.115.12", + "uvicorn==0.34.2", + "pydantic==2.11.3", + "sqlalchemy==2.0.40", + "psycopg2==2.9.10", + "alembic==1.15.2", + "redis==5.3.0", + "langgraph==0.4.1", + # ... other dependencies +] +``` + +For development, additional packages can be installed with: + +```bash +pip install -e ".[dev]" +``` + +This includes development tools like black, flake8, pytest, and more. + ## 🔐 Authentication The API uses JWT (JSON Web Token) authentication. To access the endpoints, you need to: @@ -482,21 +599,37 @@ The interactive API documentation is available at: ## 🤝 Contributing +We welcome contributions from the community! Here's how you can help: + 1. Fork the project 2. Create a feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +3. Make your changes and add tests if possible +4. Run tests and make sure they pass +5. Commit your changes following conventional commits format (`feat: add amazing feature`) +6. Push to the branch (`git push origin feature/AmazingFeature`) +7. Open a Pull Request + +Please read our [Contributing Guidelines](CONTRIBUTING.md) for more details. ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## 📊 Stargazers + +[![Stargazers repo roster for @your-username/evo-ai](https://reporoster.com/stars/your-username/evo-ai)](https://github.com/your-username/evo-ai/stargazers) + +## 🔄 Forks + +[![Forkers repo roster for @your-username/evo-ai](https://reporoster.com/forks/your-username/evo-ai)](https://github.com/your-username/evo-ai/network/members) + ## 🙏 Acknowledgments - [FastAPI](https://fastapi.tiangolo.com/) - [SQLAlchemy](https://www.sqlalchemy.org/) - [Google ADK](https://github.com/google/adk) +- [LangGraph](https://github.com/langchain-ai/langgraph) +- [ReactFlow](https://reactflow.dev/) ## 👨‍💻 Development Commands @@ -526,7 +659,7 @@ make clear-cache # Clear project cache ## 🐳 Running with Docker -To facilitate deployment and execution of the application, we provide Docker and Docker Compose configurations. +For quick setup and deployment, we provide Docker and Docker Compose configurations. ### Prerequisites @@ -535,7 +668,16 @@ To facilitate deployment and execution of the application, we provide Docker and ### Configuration -1. Configure the necessary environment variables in the `.env` file at the root of the project (or use system environment variables) +1. Create and configure the `.env` file: + +```bash +cp .env.example .env +# Edit the .env file with your settings, especially: +# - POSTGRES_CONNECTION_STRING +# - REDIS_HOST (should be "redis" when using Docker) +# - JWT_SECRET_KEY +# - SENDGRID_API_KEY +``` 2. Build the Docker image: @@ -549,19 +691,25 @@ make docker-build make docker-up ``` -4. Populate the database with initial data: +4. Apply migrations (first time only): + +```bash +docker-compose exec api python -m alembic upgrade head +``` + +5. Populate the database with initial data: ```bash make docker-seed ``` -5. To check application logs: +6. To check application logs: ```bash make docker-logs ``` -6. To stop the services: +7. To stop the services: ```bash make docker-down @@ -587,7 +735,7 @@ Docker Compose sets up persistent volumes for: The main environment variables used by the API container: - `POSTGRES_CONNECTION_STRING`: PostgreSQL connection string -- `REDIS_HOST`: Redis host +- `REDIS_HOST`: Redis host (use "redis" when running with Docker) - `JWT_SECRET_KEY`: Secret key for JWT token generation - `SENDGRID_API_KEY`: SendGrid API key for sending emails - `EMAIL_FROM`: Email used as sender diff --git a/migrations/versions/c107446e38aa_init_migrations.py b/migrations/versions/c107446e38aa_init_migrations.py deleted file mode 100644 index 47d02080..00000000 --- a/migrations/versions/c107446e38aa_init_migrations.py +++ /dev/null @@ -1,132 +0,0 @@ -"""init migrations - -Revision ID: c107446e38aa -Revises: -Create Date: 2025-05-02 08:01:10.713496 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'c107446e38aa' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('clients', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_clients_email'), 'clients', ['email'], unique=True) - op.create_table('mcp_servers', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('config_type', sa.String(), nullable=False), - sa.Column('config_json', sa.JSON(), nullable=False), - sa.Column('environments', sa.JSON(), nullable=False), - sa.Column('tools', sa.JSON(), nullable=False), - sa.Column('type', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.CheckConstraint("config_type IN ('studio', 'sse')", name='check_mcp_server_config_type'), - sa.CheckConstraint("type IN ('official', 'community')", name='check_mcp_server_type'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('tools', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('config_json', sa.JSON(), nullable=False), - sa.Column('environments', sa.JSON(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('agents', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('client_id', sa.UUID(), nullable=True), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('type', sa.String(), nullable=False), - sa.Column('model', sa.String(), nullable=True), - sa.Column('api_key', sa.String(), nullable=True), - sa.Column('instruction', sa.Text(), nullable=True), - sa.Column('agent_card_url', sa.String(), nullable=True), - sa.Column('config', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.CheckConstraint("type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a')", name='check_agent_type'), - sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('contacts', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('client_id', sa.UUID(), nullable=True), - sa.Column('ext_id', sa.String(), nullable=True), - sa.Column('name', sa.String(), nullable=True), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('users', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('password_hash', sa.String(), nullable=False), - sa.Column('client_id', sa.UUID(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.Column('email_verified', sa.Boolean(), nullable=True), - sa.Column('verification_token', sa.String(), nullable=True), - sa.Column('verification_token_expiry', sa.DateTime(timezone=True), nullable=True), - sa.Column('password_reset_token', sa.String(), nullable=True), - sa.Column('password_reset_expiry', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) - op.create_table('audit_logs', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('user_id', sa.UUID(), nullable=True), - sa.Column('action', sa.String(), nullable=False), - sa.Column('resource_type', sa.String(), nullable=False), - sa.Column('resource_id', sa.String(), nullable=True), - sa.Column('details', sa.JSON(), nullable=True), - sa.Column('ip_address', sa.String(), nullable=True), - sa.Column('user_agent', sa.String(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('audit_logs') - op.drop_index(op.f('ix_users_email'), table_name='users') - op.drop_table('users') - op.drop_table('contacts') - op.drop_table('agents') - op.drop_table('tools') - op.drop_table('mcp_servers') - op.drop_index(op.f('ix_clients_email'), table_name='clients') - op.drop_table('clients') - # ### end Alembic commands ### diff --git a/migrations/versions/ebac70616dab_worflow_agent.py b/migrations/versions/ebac70616dab_worflow_agent.py deleted file mode 100644 index aec30c06..00000000 --- a/migrations/versions/ebac70616dab_worflow_agent.py +++ /dev/null @@ -1,49 +0,0 @@ -"""worflow_agent - -Revision ID: ebac70616dab -Revises: c107446e38aa -Create Date: 2025-05-06 17:05:26.884902 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "ebac70616dab" -down_revision: Union[str, None] = "c107446e38aa" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - # Remover a constraint antiga - op.drop_constraint("check_agent_type", "agents", type_="check") - - # Adicionar a nova constraint que inclui o tipo 'workflow' - op.create_check_constraint( - "check_agent_type", - "agents", - "type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a', 'workflow')", - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - # Remover a constraint nova - op.drop_constraint("check_agent_type", "agents", type_="check") - - # Restaurar a constraint anterior sem o tipo 'workflow' - op.create_check_constraint( - "check_agent_type", - "agents", - "type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a')", - ) - # ### end Alembic commands ### diff --git a/src/config/settings.py b/src/config/settings.py index c5bb76ec..d950618d 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -4,7 +4,7 @@ from pydantic_settings import BaseSettings import secrets from dotenv import load_dotenv -# Carrega as variáveis do .env +# Load environment variables load_dotenv() diff --git a/src/schemas/schemas.py b/src/schemas/schemas.py index 1458630c..37a90814 100644 --- a/src/schemas/schemas.py +++ b/src/schemas/schemas.py @@ -120,7 +120,7 @@ class AgentBase(BaseModel): if "type" not in values: return v - # Para agentes workflow, não fazemos nenhuma validação + # For workflow agents, we do not perform any validation if "type" in values and values["type"] == "workflow": return v diff --git a/src/schemas/streaming.py b/src/schemas/streaming.py index 94c45af1..ca89101c 100644 --- a/src/schemas/streaming.py +++ b/src/schemas/streaming.py @@ -14,7 +14,7 @@ class Message(BaseModel): class TaskStatusUpdateEvent(BaseModel): - state: str = Field(..., description="Estado da tarefa (working, completed, failed)") + state: str = Field(..., description="Task state (working, completed, failed)") timestamp: datetime = Field(default_factory=datetime.utcnow) message: Optional[Message] = None error: Optional[Dict[str, Any]] = None diff --git a/src/services/agent_service.py b/src/services/agent_service.py index f3b59caf..47a737ee 100644 --- a/src/services/agent_service.py +++ b/src/services/agent_service.py @@ -147,13 +147,10 @@ async def create_agent(db: Session, agent: AgentCreate) -> Agent: detail=f"Failed to process agent card: {str(e)}", ) - # Para agentes workflow, não fazemos nenhuma validação específica - # apenas garantimos que config é um dicionário elif agent.type == "workflow": if not isinstance(agent.config, dict): agent.config = {} - # Garantir a API key if "api_key" not in agent.config or not agent.config["api_key"]: agent.config["api_key"] = generate_api_key() @@ -199,11 +196,9 @@ async def create_agent(db: Session, agent: AgentCreate) -> Agent: logger.info("Generating automatic API key for new agent") config["api_key"] = generate_api_key() - # Preservar todos os campos originais processed_config = {} processed_config["api_key"] = config.get("api_key", "") - # Copiar campos originais if "tools" in config: processed_config["tools"] = config["tools"] @@ -216,7 +211,6 @@ async def create_agent(db: Session, agent: AgentCreate) -> Agent: if "custom_mcp_servers" in config: processed_config["custom_mcp_servers"] = config["custom_mcp_servers"] - # Preservar outros campos não processados especificamente for key, value in config.items(): if key not in [ "api_key", @@ -228,7 +222,6 @@ async def create_agent(db: Session, agent: AgentCreate) -> Agent: ]: processed_config[key] = value - # Processar apenas campos que precisam de processamento # Process MCP servers if "mcp_servers" in config and config["mcp_servers"] is not None: processed_servers = [] @@ -298,7 +291,6 @@ async def create_agent(db: Session, agent: AgentCreate) -> Agent: # Convert tool id to string tool_id = tool["id"] - # Validar envs para garantir que não é None envs = tool.get("envs", {}) if envs is None: envs = {} @@ -439,11 +431,9 @@ async def update_agent( if "config" in agent_data: config = agent_data["config"] - # Preservar todos os campos originais processed_config = {} processed_config["api_key"] = config.get("api_key", "") - # Copiar campos originais if "tools" in config: processed_config["tools"] = config["tools"] @@ -456,7 +446,6 @@ async def update_agent( if "custom_mcp_servers" in config: processed_config["custom_mcp_servers"] = config["custom_mcp_servers"] - # Preservar outros campos não processados especificamente for key, value in config.items(): if key not in [ "api_key", @@ -468,7 +457,6 @@ async def update_agent( ]: processed_config[key] = value - # Processar apenas campos que precisam de processamento # Process MCP servers if "mcp_servers" in config and config["mcp_servers"] is not None: processed_servers = [] @@ -541,7 +529,6 @@ async def update_agent( # Convert tool id to string tool_id = tool["id"] - # Validar envs para garantir que não é None envs = tool.get("envs", {}) if envs is None: envs = {} diff --git a/src/services/custom_tools.py b/src/services/custom_tools.py index 2e2dc605..9d603395 100644 --- a/src/services/custom_tools.py +++ b/src/services/custom_tools.py @@ -59,7 +59,6 @@ class CustomToolBuilder: ): query_params[param] = value - # Processa body parameters body_data = {} for param, param_config in parameters.get("body_params", {}).items(): if param in all_values: diff --git a/src/services/workflow_agent.py b/src/services/workflow_agent.py index dc972a2a..5f1c7b21 100644 --- a/src/services/workflow_agent.py +++ b/src/services/workflow_agent.py @@ -4,19 +4,12 @@ from google.adk.agents.invocation_context import InvocationContext from google.adk.events import Event from google.genai.types import Content, Part -from typing import AsyncGenerator, Dict, Any, List, TypedDict, Annotated -import json +from typing import AsyncGenerator, Dict, Any, List, TypedDict import uuid -import asyncio -import httpx -import threading from google.adk.runners import Runner from src.services.agent_service import get_agent -# Remover importação circular -# from src.services.agent_builder import AgentBuilder - from sqlalchemy.orm import Session from langgraph.graph import StateGraph, END @@ -35,13 +28,13 @@ class State(TypedDict): class WorkflowAgent(BaseAgent): """ - Agente que implementa fluxos de trabalho usando LangGraph. + Agent that implements workflow flows using LangGraph. - Este agente permite definir e executar fluxos complexos entre vários agentes - utilizando o LangGraph para orquestração. + This agent allows defining and executing complex workflows between multiple agents + using LangGraph for orchestration. """ - # Declarações de campo para Pydantic + # Field declarations for Pydantic flow_json: Dict[str, Any] timeout: int db: Session @@ -56,16 +49,16 @@ class WorkflowAgent(BaseAgent): **kwargs, ): """ - Inicializa o agente de workflow. + Initializes the workflow agent. Args: - name: Nome do agente - flow_json: Definição do fluxo em formato JSON - timeout: Tempo máximo de execução (segundos) - sub_agents: Lista de sub-agentes a serem executados após o agente de workflow + name: Agent name + flow_json: Workflow definition in JSON format + timeout: Maximum execution time (seconds) + sub_agents: List of sub-agents to be executed after the workflow agent db: Session """ - # Inicializar classe base + # Initialize base class super().__init__( name=name, flow_json=flow_json, @@ -76,19 +69,19 @@ class WorkflowAgent(BaseAgent): ) print( - f"Agente de workflow inicializado com {len(flow_json.get('nodes', []))} nós" + f"Workflow agent initialized with {len(flow_json.get('nodes', []))} nodes" ) async def _create_node_functions(self, ctx: InvocationContext): - """Cria as funções para cada tipo de nó no fluxo.""" + """Creates functions for each type of node in the flow.""" - # Função para o nó inicial + # Function for the initial node async def start_node_function( state: State, node_id: str, node_data: Dict[str, Any], ) -> AsyncGenerator[State, None]: - print("\n🏁 NÓ INICIAL") + print("\n🏁 INITIAL NODE") content = state.get("content", []) @@ -109,7 +102,7 @@ class WorkflowAgent(BaseAgent): return session_id = state.get("session_id", "") - # Armazenar resultados específicos para este nó + # Store specific results for this node node_outputs = state.get("node_outputs", {}) node_outputs[node_id] = {"started_at": datetime.now().isoformat()} @@ -122,7 +115,7 @@ class WorkflowAgent(BaseAgent): "conversation_history": ctx.session.events, } - # Função genérica para nós de agente + # Generic function for agent nodes async def agent_node_function( state: State, node_id: str, node_data: Dict[str, Any] ) -> AsyncGenerator[State, None]: @@ -131,14 +124,14 @@ class WorkflowAgent(BaseAgent): agent_name = agent_config.get("name", "") agent_id = agent_config.get("id", "") - # Incrementar contador de ciclos + # Increment cycle counter cycle_count = state.get("cycle_count", 0) + 1 - print(f"\n👤 AGENTE: {agent_name} (Ciclo {cycle_count})") + print(f"\n👤 AGENT: {agent_name} (Cycle {cycle_count})") content = state.get("content", []) session_id = state.get("session_id", "") - # Obter o histórico de conversa + # Get conversation history conversation_history = state.get("conversation_history", []) agent = get_agent(self.db, agent_id) @@ -159,7 +152,7 @@ class WorkflowAgent(BaseAgent): } return - # Importação movida para dentro da função para evitar circular import + # Import moved to inside the function to avoid circular import from src.services.agent_builder import AgentBuilder agent_builder = AgentBuilder(self.db) @@ -179,8 +172,10 @@ class WorkflowAgent(BaseAgent): "cycle": cycle_count, } + content = content + new_content + yield { - "content": new_content, + "content": content, "status": "processed_by_agent", "node_outputs": node_outputs, "cycle_count": cycle_count, @@ -191,23 +186,23 @@ class WorkflowAgent(BaseAgent): if exit_stack: await exit_stack.aclose() - # Função para nós de condição + # Function for condition nodes async def condition_node_function( state: State, node_id: str, node_data: Dict[str, Any] ) -> AsyncGenerator[State, None]: - label = node_data.get("label", "Condição Sem Nome") + label = node_data.get("label", "No name condition") conditions = node_data.get("conditions", []) cycle_count = state.get("cycle_count", 0) - print(f"\n🔄 CONDIÇÃO: {label} (Ciclo {cycle_count})") + print(f"\n🔄 CONDITION: {label} (Cycle {cycle_count})") content = state.get("content", []) - print(f"Avaliando condição para conteúdo: '{content}'") + print(f"Evaluating condition for content: '{content}'") session_id = state.get("session_id", "") conversation_history = state.get("conversation_history", []) - # Verificar todas as condições + # Check all conditions conditions_met = [] for condition in conditions: condition_id = condition.get("id") @@ -217,18 +212,27 @@ class WorkflowAgent(BaseAgent): expected_value = condition_data.get("value") print( - f" Verificando se {field} {operator} '{expected_value}' (valor atual: '{state.get(field, '')}')" + f" Checking if {field} {operator} '{expected_value}' (current value: '{state.get(field, '')}')" ) if self._evaluate_condition(condition, state): conditions_met.append(condition_id) - print(f" ✅ Condição {condition_id} atendida!") + print(f" ✅ Condition {condition_id} met!") - # Verificar se o ciclo atingiu o limite (segurança extra) + # Check if the cycle reached the limit (extra security) if cycle_count >= 10: print( - f"⚠️ ATENÇÃO: Limite de ciclos atingido ({cycle_count}). Forçando término." + f"⚠️ ATTENTION: Cycle limit reached ({cycle_count}). Forcing termination." ) + + condition_content = [ + Event( + author="agent", + content=Content(parts=[Part(text="Cycle limit reached")]), + ) + ] + content = content + condition_content yield { + "content": content, "status": "cycle_limit_reached", "node_outputs": state.get("node_outputs", {}), "cycle_count": cycle_count, @@ -237,7 +241,7 @@ class WorkflowAgent(BaseAgent): } return - # Armazenar resultados específicos para este nó + # Store specific results for this node node_outputs = state.get("node_outputs", {}) node_outputs[node_id] = { "condition_evaluated": label, @@ -246,7 +250,22 @@ class WorkflowAgent(BaseAgent): "cycle": cycle_count, } + condition_content = [ + Event( + author="agent", + content=Content( + parts=[ + Part( + text=f"Condition evaluated: {label} with {str(conditions_met)}" + ) + ] + ), + ) + ] + content = content + condition_content + yield { + "content": content, "status": "condition_evaluated", "node_outputs": node_outputs, "cycle_count": cycle_count, @@ -261,7 +280,7 @@ class WorkflowAgent(BaseAgent): } def _evaluate_condition(self, condition: Dict[str, Any], state: State) -> bool: - """Avalia uma condição contra o estado atual.""" + """Evaluates a condition against the current state.""" condition_type = condition.get("type") condition_data = condition.get("data", {}) @@ -272,9 +291,9 @@ class WorkflowAgent(BaseAgent): actual_value = state.get(field, "") - # Tratamento especial para quando content é uma lista de Event + # Special treatment for when content is a list of Events if field == "content" and isinstance(actual_value, list) and actual_value: - # Extrai o texto de cada evento para comparação + # Extract text from each event for comparison extracted_texts = [] for event in actual_value: if hasattr(event, "content") and hasattr(event.content, "parts"): @@ -284,9 +303,9 @@ class WorkflowAgent(BaseAgent): if extracted_texts: actual_value = " ".join(extracted_texts) - print(f" Texto extraído dos eventos: '{actual_value[:100]}...'") + print(f" Extracted text from events: '{actual_value[:100]}...'") - # Converter valores para string para facilitar comparações + # Convert values to string for easier comparisons if actual_value is not None: actual_str = str(actual_value) else: @@ -297,58 +316,58 @@ class WorkflowAgent(BaseAgent): else: expected_str = "" - # Verificações de definição + # Checks for definition if operator == "is_defined": result = actual_value is not None and actual_value != "" - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result elif operator == "is_not_defined": result = actual_value is None or actual_value == "" - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result - # Verificações de igualdade + # Checks for equality elif operator == "equals": result = actual_str == expected_str - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result elif operator == "not_equals": result = actual_str != expected_str - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result - # Verificações de conteúdo + # Checks for content elif operator == "contains": - # Converter ambos para minúsculas para comparação sem diferenciação + # Convert both to lowercase for case-insensitive comparison expected_lower = expected_str.lower() actual_lower = actual_str.lower() print( - f" Comparação 'contains' sem distinção de maiúsculas/minúsculas: '{expected_lower}' em '{actual_lower[:100]}...'" + f" Comparison 'contains' without case distinction: '{expected_lower}' in '{actual_lower[:100]}...'" ) result = expected_lower in actual_lower - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result elif operator == "not_contains": expected_lower = expected_str.lower() actual_lower = actual_str.lower() print( - f" Comparação 'not_contains' sem distinção de maiúsculas/minúsculas: '{expected_lower}' em '{actual_lower[:100]}...'" + f" Comparison 'not_contains' without case distinction: '{expected_lower}' in '{actual_lower[:100]}...'" ) result = expected_lower not in actual_lower - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result - # Verificações de início e fim + # Checks for start and end elif operator == "starts_with": result = actual_str.lower().startswith(expected_str.lower()) - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result elif operator == "ends_with": result = actual_str.lower().endswith(expected_str.lower()) - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result - # Verificações numéricas (tentando converter para número) + # Numeric checks (attempting to convert to number) elif operator in [ "greater_than", "greater_than_or_equal", @@ -367,26 +386,26 @@ class WorkflowAgent(BaseAgent): result = actual_num < expected_num elif operator == "less_than_or_equal": result = actual_num <= expected_num - print(f" Verificação numérica '{operator}': {result}") + print(f" Numeric check '{operator}': {result}") return result except (ValueError, TypeError): - # Se não for possível converter para número, retorna falso + # If it's not possible to convert to number, return false print( - f" Erro ao converter valores para comparação numérica: '{actual_str[:100]}...' e '{expected_str}'" + f" Error converting values for numeric comparison: '{actual_str[:100]}...' and '{expected_str}'" ) return False - # Verificações com expressões regulares + # Checks with regular expressions elif operator == "matches": import re try: pattern = re.compile(expected_str, re.IGNORECASE) result = bool(pattern.search(actual_str)) - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result except re.error: - print(f" Erro na expressão regular: '{expected_str}'") + print(f" Error in regular expression: '{expected_str}'") return False elif operator == "not_matches": import re @@ -394,17 +413,17 @@ class WorkflowAgent(BaseAgent): try: pattern = re.compile(expected_str, re.IGNORECASE) result = not bool(pattern.search(actual_str)) - print(f" Verificação '{operator}': {result}") + print(f" Check '{operator}': {result}") return result except re.error: - print(f" Erro na expressão regular: '{expected_str}'") - return True # Se a regex for inválida, consideramos que não houve match + print(f" Error in regular expression: '{expected_str}'") + return True # If the regex is invalid, we consider that there was no match return False def _create_flow_router(self, flow_data: Dict[str, Any]): - """Cria um roteador baseado nas conexões no flow.json.""" - # Mapear conexões para entender como os nós se conectam + """Creates a router based on the connections in flow.json.""" + # Map connections to understand how nodes are connected edges_map = {} for edge in flow_data.get("edges", []): @@ -415,10 +434,10 @@ class WorkflowAgent(BaseAgent): if source not in edges_map: edges_map[source] = {} - # Armazenar o destino para cada handle específico + # Store the destination for each specific handle edges_map[source][source_handle] = target - # Mapear nós de condição e suas condições + # Map condition nodes and their conditions condition_nodes = {} for node in flow_data.get("nodes", []): if node.get("type") == "condition-node": @@ -426,35 +445,35 @@ class WorkflowAgent(BaseAgent): conditions = node.get("data", {}).get("conditions", []) condition_nodes[node_id] = conditions - # Função de roteamento para cada nó específico + # Routing function for each specific node def create_router_for_node(node_id: str): def router(state: State) -> str: - print(f"Roteando a partir do nó: {node_id}") + print(f"Routing from node: {node_id}") - # Verificar se o limite de ciclos foi atingido + # Check if the cycle limit has been reached cycle_count = state.get("cycle_count", 0) if cycle_count >= 10: print( - f"⚠️ Limite de ciclos ({cycle_count}) atingido. Finalizando o fluxo." + f"⚠️ Cycle limit ({cycle_count}) reached. Finalizing the flow." ) return END - # Se for um nó de condição, avaliar as condições + # If it's a condition node, evaluate the conditions if node_id in condition_nodes: conditions = condition_nodes[node_id] for condition in conditions: condition_id = condition.get("id") - # Verificar se a condição é atendida + # Check if the condition is met is_condition_met = self._evaluate_condition(condition, state) if is_condition_met: print( - f"Condição {condition_id} atendida. Movendo para o próximo nó." + f"Condition {condition_id} met. Moving to the next node." ) - # Encontrar a conexão que usa este condition_id como handle + # Find the connection that uses this condition_id as a handle if ( node_id in edges_map and condition_id in edges_map[node_id] @@ -462,37 +481,31 @@ class WorkflowAgent(BaseAgent): return edges_map[node_id][condition_id] else: print( - f"Condição {condition_id} NÃO atendida. Continuando avaliação ou usando caminho padrão." + f"Condition {condition_id} not met. Continuing evaluation or using default path." ) - # Se nenhuma condição for atendida, usar o bottom-handle se disponível + # If no condition is met, use the bottom-handle if available if node_id in edges_map and "bottom-handle" in edges_map[node_id]: - print( - "Nenhuma condição atendida. Usando caminho padrão (bottom-handle)." - ) + print("No condition met. Using default path (bottom-handle).") return edges_map[node_id]["bottom-handle"] else: - print( - "Nenhuma condição atendida e não há caminho padrão. Encerrando fluxo." - ) + print("No condition met and no default path. Closing the flow.") return END - # Para nós regulares, simplesmente seguir a primeira conexão disponível + # For regular nodes, simply follow the first available connection if node_id in edges_map: - # Tentar usar o handle padrão ou bottom-handle primeiro + # Try to use the default handle or bottom-handle first for handle in ["default", "bottom-handle"]: if handle in edges_map[node_id]: return edges_map[node_id][handle] - # Se nenhum handle específico for encontrado, usar o primeiro disponível + # If no specific handle is found, use the first available if edges_map[node_id]: first_handle = list(edges_map[node_id].keys())[0] return edges_map[node_id][first_handle] - # Se não houver conexão de saída, encerrar o fluxo - print( - f"Nenhum caminho a seguir a partir do nó {node_id}. Encerrando fluxo." - ) + # If there is no output connection, close the flow + print(f"No output connection from node {node_id}. Closing the flow.") return END return router @@ -502,30 +515,30 @@ class WorkflowAgent(BaseAgent): async def _create_graph( self, ctx: InvocationContext, flow_data: Dict[str, Any] ) -> StateGraph: - """Cria um StateGraph a partir dos dados do fluxo.""" - # Extrair nós do fluxo + """Creates a StateGraph from the flow data.""" + # Extract nodes from the flow nodes = flow_data.get("nodes", []) - # Inicializar StateGraph + # Initialize StateGraph graph_builder = StateGraph(State) - # Criar funções para cada tipo de nó + # Create functions for each node type node_functions = await self._create_node_functions(ctx) - # Dicionário para armazenar funções específicas para cada nó + # Dictionary to store specific functions for each node node_specific_functions = {} - # Adicionar nós ao grafo + # Add nodes to the graph for node in nodes: node_id = node.get("id") node_type = node.get("type") node_data = node.get("data", {}) if node_type in node_functions: - # Criar uma função específica para este nó + # Create a specific function for this node def create_node_function(node_type, node_id, node_data): async def node_function(state): - # Consumir o gerador assíncrono e retornar o último resultado + # Consume the asynchronous generator and return the last result result = None async for item in node_functions[node_type]( state, node_id, node_data @@ -535,106 +548,106 @@ class WorkflowAgent(BaseAgent): return node_function - # Adicionar função específica ao dicionário + # Add specific function to the dictionary node_specific_functions[node_id] = create_node_function( node_type, node_id, node_data ) - # Adicionar o nó ao grafo - print(f"Adicionando nó {node_id} do tipo {node_type}") + # Add node to the graph + print(f"Adding node {node_id} of type {node_type}") graph_builder.add_node(node_id, node_specific_functions[node_id]) - # Criar função para gerar roteadores específicos + # Create function to generate specific routers create_router = self._create_flow_router(flow_data) - # Adicionar conexões condicionais para cada nó + # Add conditional connections for each node for node in nodes: node_id = node.get("id") if node_id in node_specific_functions: - # Criar dicionário de possíveis destinos + # Create dictionary of possible destinations edge_destinations = {} - # Mapear todos os possíveis destinos + # Map all possible destinations for edge in flow_data.get("edges", []): if edge.get("source") == node_id: target = edge.get("target") if target in node_specific_functions: edge_destinations[target] = target - # Adicionar END como possível destino + # Add END as a possible destination edge_destinations[END] = END - # Criar roteador específico para este nó + # Create specific router for this node node_router = create_router(node_id) - # Adicionar conexões condicionais - print(f"Adicionando conexões condicionais para o nó {node_id}") - print(f"Destinos possíveis: {edge_destinations}") + # Add conditional connections + print(f"Adding conditional connections for node {node_id}") + print(f"Possible destinations: {edge_destinations}") graph_builder.add_conditional_edges( node_id, node_router, edge_destinations ) - # Encontrar o nó inicial (geralmente o start-node) + # Find the initial node (usually the start-node) entry_point = None for node in nodes: if node.get("type") == "start-node": entry_point = node.get("id") break - # Se não houver start-node, usar o primeiro nó encontrado + # If there is no start-node, use the first node found if not entry_point and nodes: entry_point = nodes[0].get("id") - # Definir ponto de entrada + # Define the entry point if entry_point: - print(f"Definindo ponto de entrada: {entry_point}") + print(f"Defining entry point: {entry_point}") graph_builder.set_entry_point(entry_point) - # Compilar o grafo + # Compile the graph return graph_builder.compile() async def _run_async_impl( self, ctx: InvocationContext ) -> AsyncGenerator[Event, None]: """ - Implementação do agente de workflow. + Implementation of the workflow agent. - Este método segue o padrão de implementação de agentes personalizados, - executando o fluxo de trabalho definido e retornando os resultados. + This method follows the pattern of custom agent implementation, + executing the defined workflow and returning the results. """ try: - # 1. Extrair a mensagem do usuário do contexto + # 1. Extract the user message from the context user_message = None - # Procurar a mensagem do usuário nos eventos da sessão + # Search for the user message in the session events if ctx.session and hasattr(ctx.session, "events") and ctx.session.events: for event in reversed(ctx.session.events): if event.author == "user" and event.content and event.content.parts: user_message = event.content.parts[0].text - print("Mensagem encontrada nos eventos da sessão") + print("Message found in session events") break - # Verificar no estado da sessão se a mensagem não foi encontrada nos eventos + # Check in the session state if the message was not found in the events if not user_message and ctx.session and ctx.session.state: if "user_message" in ctx.session.state: user_message = ctx.session.state["user_message"] elif "message" in ctx.session.state: user_message = ctx.session.state["message"] - # 2. Usar o ID da sessão como identificador estável + # 2. Use the session ID as a stable identifier session_id = ( str(ctx.session.id) if ctx.session and hasattr(ctx.session, "id") else str(uuid.uuid4()) ) - # 3. Criar o grafo de fluxo de trabalho a partir do JSON fornecido + # 3. Create the workflow graph from the provided JSON graph = await self._create_graph(ctx, self.flow_json) - # 4. Preparar o estado inicial + # 4. Prepare the initial state initial_state = State( content=[ Event( @@ -649,28 +662,28 @@ class WorkflowAgent(BaseAgent): conversation_history=ctx.session.events, ) - # 5. Executar o grafo - print("\n🚀 Iniciando execução do fluxo de trabalho:") - print(f"Conteúdo inicial: {user_message[:100]}...") + # 5. Execute the graph + print("\n🚀 Starting workflow execution:") + print(f"Initial content: {user_message[:100]}...") - # Executar o grafo com limite de recursão para evitar loops infinitos + # Execute the graph with a recursion limit to avoid infinite loops result = await graph.ainvoke(initial_state, {"recursion_limit": 20}) - # 6. Processar e retornar o resultado + # 6. Process and return the result final_content = result.get("content", []) - print(f"\n✅ RESULTADO FINAL: {final_content[:100]}...") + print(f"\n✅ FINAL RESULT: {final_content[:100]}...") for content in final_content: yield content - # Executar sub-agentes + # Execute sub-agents for sub_agent in self.sub_agents: async for event in sub_agent.run_async(ctx): yield event except Exception as e: - # Tratar qualquer erro não capturado - error_msg = f"Erro ao executar o agente de workflow: {str(e)}" + # Handle any uncaught errors + error_msg = f"Error executing the workflow agent: {str(e)}" print(error_msg) yield Event( author=self.name, diff --git a/src/utils/streaming.py b/src/utils/streaming.py index b2013e0f..89ae2316 100644 --- a/src/utils/streaming.py +++ b/src/utils/streaming.py @@ -9,15 +9,15 @@ class SSEUtils: generator: AsyncGenerator, timeout: int = 30, retry_attempts: int = 3 ) -> AsyncGenerator: """ - Adiciona timeout e retry a um gerador de eventos SSE. + Adds timeout and retry to an SSE event generator. Args: - generator: Gerador de eventos - timeout: Tempo máximo de espera em segundos - retry_attempts: Número de tentativas de reconexão + generator: Event generator + timeout: Maximum wait time in seconds + retry_attempts: Number of reconnection attempts Yields: - Eventos do gerador + Events from the generator """ attempts = 0 while attempts < retry_attempts: @@ -29,33 +29,33 @@ class SSEUtils: attempts += 1 if attempts >= retry_attempts: raise HTTPException( - status_code=408, detail="Timeout após múltiplas tentativas" + status_code=408, detail="Timeout after multiple attempts" ) - await asyncio.sleep(1) # Espera antes de tentar novamente + await asyncio.sleep(1) # Wait before trying again @staticmethod def format_error_event(error: Exception) -> str: """ - Formata um evento de erro SSE. + Formats an SSE error event. Args: - error: Exceção ocorrida + error: Occurred exception Returns: - String formatada do evento SSE + Formatted SSE error event """ return f"event: error\ndata: {str(error)}\n\n" @staticmethod def validate_sse_headers(headers: dict) -> None: """ - Valida headers necessários para SSE. + Validates required headers for SSE. Args: - headers: Dicionário de headers + headers: Dictionary of headers Raises: - HTTPException se headers inválidos + HTTPException if invalid headers """ required_headers = { "Accept": "text/event-stream", @@ -66,5 +66,5 @@ class SSEUtils: for header, value in required_headers.items(): if headers.get(header) != value: raise HTTPException( - status_code=400, detail=f"Header {header} inválido ou ausente" + status_code=400, detail=f"Invalid or missing header: {header}" ) diff --git a/static/test_a2a_stream.html b/static/test_a2a_stream.html deleted file mode 100644 index 257b198b..00000000 --- a/static/test_a2a_stream.html +++ /dev/null @@ -1,295 +0,0 @@ - - - - - - - Teste de Streaming A2A - - - - - -
-

Teste de Streaming A2A

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- Status: Não conectado -
- -
- -
-
- - - - - \ No newline at end of file diff --git a/static/test_chat_stream.html b/static/test_chat_stream.html deleted file mode 100644 index bf8c9c6e..00000000 --- a/static/test_chat_stream.html +++ /dev/null @@ -1,343 +0,0 @@ - - - - - ADK Streaming Test - - - - - -

ADK Streaming Test

-
-
Desconectado
-
- - - - -
- - -
-
- -
- - - - - \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 2c854ab4..00000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Package initialization for tests diff --git a/tests/api/__init__.py b/tests/api/__init__.py deleted file mode 100644 index 42b18b86..00000000 --- a/tests/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# API tests package diff --git a/tests/api/test_root.py b/tests/api/test_root.py deleted file mode 100644 index 95d42b0f..00000000 --- a/tests/api/test_root.py +++ /dev/null @@ -1,11 +0,0 @@ -def test_read_root(client): - """ - Test that the root endpoint returns the correct response. - """ - response = client.get("/") - assert response.status_code == 200 - data = response.json() - assert "message" in data - assert "documentation" in data - assert "version" in data - assert "auth" in data diff --git a/tests/services/__init__.py b/tests/services/__init__.py deleted file mode 100644 index 20068376..00000000 --- a/tests/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Services tests package diff --git a/tests/services/test_auth_service.py b/tests/services/test_auth_service.py deleted file mode 100644 index 5015c31b..00000000 --- a/tests/services/test_auth_service.py +++ /dev/null @@ -1,27 +0,0 @@ -from src.services.auth_service import create_access_token -from src.models.models import User -import uuid - - -def test_create_access_token(): - """ - Test that an access token is created with the correct data. - """ - # Create a mock user - user = User( - id=uuid.uuid4(), - email="test@example.com", - hashed_password="hashed_password", - is_active=True, - is_admin=False, - name="Test User", - client_id=uuid.uuid4(), - ) - - # Create token - token = create_access_token(user) - - # Simple validation - assert token is not None - assert isinstance(token, str) - assert len(token) > 0