diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..c6803bc214c80649e3ed7300b79ca836eb0b719f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,75 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git/ +.gitignore + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log + +# Environment files +.env +.env.local +.env.production + +# Test files +.pytest_cache/ +.coverage +htmlcov/ + +# Documentation +README.md +*.md + +# Docker +Dockerfile +.dockerignore \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..ad4dbef32900de8911a344aa9156aeed2efab97c --- /dev/null +++ b/.env @@ -0,0 +1,16 @@ +# Database +DATABASE_URL=sqlite:///./todo_app.db +# For production use PostgreSQL: +# DATABASE_URL=postgresql://user:password@localhost/todo_app + +# Groq Configuration +GROQ_API_KEY=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 +GROQ_MODEL_NAME=llama3-8b-8192 + +# API Configuration +API_V1_STR=/api/v1 +PROJECT_NAME=AI Todo App +DEBUG=True + +# CORS +BACKEND_CORS_ORIGINS=["http://localhost:3000", "https://yourdomain.vercel.app"] \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..f886de0bf8091564779caa258a0c12bcda0ee8b6 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,117 @@ +# Deployment Guide + +## Hugging Face Spaces Deployment + +### 1. Create a Hugging Face Space + +1. Go to [Hugging Face Spaces](https://huggingface.co/spaces) +2. Click "Create new Space" +3. Choose "Docker" as the SDK +4. Set the Space name and visibility +5. Click "Create Space" + +### 2. Configure Environment Variables + +In your Hugging Face Space settings, add these environment variables: + +``` +DATABASE_URL=sqlite:///./todo_app.db +GROQ_API_KEY=your_actual_groq_api_key +GROQ_MODEL_NAME=llama3-8b-8192 +API_V1_STR=/api/v1 +PROJECT_NAME=AI Todo App +DEBUG=False +BACKEND_CORS_ORIGINS=["https://your-frontend-domain.com"] +``` + +### 3. Update Dockerfile for HF Spaces + +The Dockerfile is already optimized for Hugging Face Spaces. Make sure to: + +1. Set `DEBUG=False` in production +2. Configure proper CORS origins +3. Set your actual Groq API key + +### 4. Build and Deploy + +The Space will automatically build and deploy when you push to the repository. + +## Local Development + +### Using Docker Compose + +```bash +# Build and run +docker-compose up --build + +# Run in background +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down +``` + +### Using Docker directly + +```bash +# Build image +docker build -t todo-ai-app . + +# Run container +docker run -p 8000:8000 -e GROQ_API_KEY=your_key todo-ai-app +``` + +## Environment Variables + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `DATABASE_URL` | Database connection string | No | `sqlite:///./todo_app.db` | +| `GROQ_API_KEY` | Groq API key for AI features | Yes | - | +| `GROQ_MODEL_NAME` | Groq model to use | No | `llama3-8b-8192` | +| `API_V1_STR` | API version prefix | No | `/api/v1` | +| `PROJECT_NAME` | Application name | No | `AI Todo App` | +| `DEBUG` | Debug mode | No | `True` | +| `BACKEND_CORS_ORIGINS` | CORS allowed origins | No | `["http://localhost:3000"]` | + +## Health Check + +The application includes a health check endpoint at `/health` that returns: + +```json +{ + "status": "healthy" +} +``` + +## API Documentation + +Once deployed, you can access: +- API Documentation: `https://your-space.hf.space/docs` +- Health Check: `https://your-space.hf.space/health` +- Root endpoint: `https://your-space.hf.space/` + +**Note**: The application runs on port 7860, which is the standard port for Hugging Face Spaces. + +## Troubleshooting + +### Common Issues + +1. **Port binding**: Make sure port 8000 is exposed +2. **Environment variables**: Ensure all required env vars are set +3. **Database**: SQLite database will be created automatically +4. **CORS**: Update CORS origins for your frontend domain + +### Logs + +Check application logs for debugging: + +```bash +# Docker Compose +docker-compose logs app + +# Docker +docker logs +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1c4e9430e6f4c13a44dc4eb0c1524b8f8e5fd172 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Use Python 3.11 slim image for smaller size +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create a non-root user for security +RUN useradd --create-home --shell /bin/bash app && \ + chown -R app:app /app +USER app + +# Expose port +EXPOSE 7860 + +# Health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:7860/health')" || exit 1 + +# Run the application +CMD ["python", "run.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..041ca3a07a5835096eb5883ddfa116a61393f13b --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +--- +title: AI Todo App Backend +emoji: ๐Ÿค– +colorFrom: blue +colorTo: purple +sdk: docker +pinned: false +license: mit +--- + +# AI Todo App Backend + +A FastAPI-based backend application for an AI-powered todo management system with Groq AI integration. + +## Features + +- **FastAPI REST API** with automatic documentation +- **SQLite Database** with SQLAlchemy ORM +- **Groq AI Integration** for intelligent todo management +- **CORS Support** for frontend integration +- **Health Check Endpoints** for monitoring +- **Docker Containerization** for easy deployment + +## API Endpoints + +- **GET /** - Root endpoint with app info +- **GET /health** - Health check endpoint +- **GET /docs** - Interactive API documentation (Swagger UI) +- **GET /api/v1/** - API v1 endpoints + +## Environment Variables + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `DATABASE_URL` | Database connection string | No | `sqlite:///./todo_app.db` | +| `GROQ_API_KEY` | Groq API key for AI features | Yes | - | +| `GROQ_MODEL_NAME` | Groq model to use | No | `llama3-8b-8192` | +| `API_V1_STR` | API version prefix | No | `/api/v1` | +| `PROJECT_NAME` | Application name | No | `AI Todo App` | +| `DEBUG` | Debug mode | No | `True` | +| `BACKEND_CORS_ORIGINS` | CORS allowed origins | No | `["http://localhost:3000"]` | + +## Local Development + +### Using Docker Compose + +```bash +# Build and run +docker-compose up --build + +# Run in background +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down +``` + +### Using Docker directly + +```bash +# Build image +docker build -t todo-ai-app . + +# Run container +docker run -p 7860:7860 -e GROQ_API_KEY=your_key todo-ai-app +``` + +## Deployment + +This application is deployed on Hugging Face Spaces and runs on port 7860. + +### Access Points + +- **Application**: https://huggingface.co/spaces/Muhammadbilal10101/todo-ai +- **API Documentation**: https://huggingface.co/spaces/Muhammadbilal10101/todo-ai/docs +- **Health Check**: https://huggingface.co/spaces/Muhammadbilal10101/todo-ai/health + +## Technology Stack + +- **FastAPI** - Modern, fast web framework for building APIs +- **SQLAlchemy** - SQL toolkit and ORM +- **Alembic** - Database migration tool +- **Pydantic** - Data validation using Python type annotations +- **Groq** - Fast AI inference API +- **Docker** - Containerization platform +- **SQLite** - Lightweight database + +## Project Structure + +``` +backend/ +โ”œโ”€โ”€ app/ +โ”‚ โ”œโ”€โ”€ api/ # API routes +โ”‚ โ”œโ”€โ”€ core/ # Core configuration +โ”‚ โ”œโ”€โ”€ models/ # Database models +โ”‚ โ”œโ”€โ”€ schemas/ # Pydantic schemas +โ”‚ โ””โ”€โ”€ services/ # Business logic +โ”œโ”€โ”€ alembic/ # Database migrations +โ”œโ”€โ”€ Dockerfile # Docker configuration +โ”œโ”€โ”€ docker-compose.yml # Local development +โ”œโ”€โ”€ requirements.txt # Python dependencies +โ””โ”€โ”€ run.py # Application entry point +``` + +## License + +MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..22a3af0c025a2ca9c3bf0dbdbe272cd0a0e810ce --- /dev/null +++ b/alembic.ini @@ -0,0 +1,41 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os +sqlalchemy.url = sqlite:///./todo_app.db + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000000000000000000000000000000000000..70af7c49e65ebee284bfa62f19d0ea6c1da841ff --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,59 @@ +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context +import os +import sys + +# Add your project directory to Python path +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + +from app.core.config import settings +from app.core.database import Base +from app.models import todo, subtask # Import all models + +# Alembic Config object +config = context.config + +# Set SQLAlchemy URL from settings +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +# Interpret the config file for Python logging +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# Target metadata for 'autogenerate' support +target_metadata = Base.metadata + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode.""" + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4a3cb690db745a3263d27fd29e10a863dc7690c0 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# AI Todo App Backend \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b6da024252cf91b0fe4c4583ed5c826413a4c1e Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f74e8e0a0c5bb3fa69e958259afcd5969940d292 Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f885ebd47aad5aa01324471d53224cceb6c20fe5 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1 @@ +# API endpoints \ No newline at end of file diff --git a/app/api/__pycache__/__init__.cpython-312.pyc b/app/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba44bce54836d2ac6dce6fa35635e3ac751aff7c Binary files /dev/null and b/app/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a84f3b8387356f585dd103fe5a43b5d730339319 --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1 @@ +# API v1 endpoints \ No newline at end of file diff --git a/app/api/v1/__pycache__/__init__.cpython-312.pyc b/app/api/v1/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..502051903a2870395b0f37ee11c5ff15e84332f0 Binary files /dev/null and b/app/api/v1/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/v1/__pycache__/api.cpython-312.pyc b/app/api/v1/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25d0aded725d19e1d34bd8b89d7b76886331fa9d Binary files /dev/null and b/app/api/v1/__pycache__/api.cpython-312.pyc differ diff --git a/app/api/v1/api.py b/app/api/v1/api.py new file mode 100644 index 0000000000000000000000000000000000000000..39dd0b8141256f633d4e9ed68958ba5a49f4364d --- /dev/null +++ b/app/api/v1/api.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter +from .endpoints import todos, subtasks, translation + +api_router = APIRouter() + +api_router.include_router(todos.router, prefix="/todos", tags=["todos"]) +api_router.include_router(subtasks.router, prefix="", tags=["subtasks"]) +api_router.include_router(translation.router, prefix="/todos", tags=["translation"]) \ No newline at end of file diff --git a/app/api/v1/endpoints/__init__.py b/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..948dd644e0cc6ffee3655bc28d6cf9a87011ffd2 --- /dev/null +++ b/app/api/v1/endpoints/__init__.py @@ -0,0 +1 @@ +# API endpoint modules \ No newline at end of file diff --git a/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f929003e4dc102d9d3ceca29e8738165e8a982b Binary files /dev/null and b/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/__pycache__/subtasks.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/subtasks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8140602dcba84d0836fca326157b05cd8a9345c2 Binary files /dev/null and b/app/api/v1/endpoints/__pycache__/subtasks.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/__pycache__/todos.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/todos.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c724dfa1bc720dff6ecaa44fd8edb1d96c4f5147 Binary files /dev/null and b/app/api/v1/endpoints/__pycache__/todos.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/__pycache__/translation.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/translation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53dd1f88f8bfb1c115f3e4c8205468db1fd2dab8 Binary files /dev/null and b/app/api/v1/endpoints/__pycache__/translation.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/subtasks.py b/app/api/v1/endpoints/subtasks.py new file mode 100644 index 0000000000000000000000000000000000000000..b152bb39ea91e6b3f9ebd3a0cd5b79d09628837f --- /dev/null +++ b/app/api/v1/endpoints/subtasks.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ....core.database import get_db +from ....schemas.subtask import Subtask, SubtaskGenerateRequest, SubtaskUpdate +from ....services.todo_service import TodoService + +router = APIRouter() + +@router.post("/todos/{todo_id}/generate", response_model=List[Subtask]) +async def generate_subtasks( + todo_id: int, + request: SubtaskGenerateRequest, + db: Session = Depends(get_db) +): + """Generate AI subtasks for a todo""" + todo_service = TodoService(db) + + try: + subtasks = await todo_service.generate_subtasks( + todo_id=todo_id, + max_subtasks=request.max_subtasks + ) + return subtasks + except ValueError 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=f"Failed to generate subtasks: {str(e)}" + ) + +@router.get("/todos/{todo_id}/subtasks", response_model=List[Subtask]) +def get_todo_subtasks(todo_id: int, db: Session = Depends(get_db)): + """Get all subtasks for a todo""" + todo_service = TodoService(db) + todo = todo_service.get_todo(todo_id) + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + return todo.subtasks + +@router.put("/subtasks/{subtask_id}", response_model=Subtask) +def update_subtask( + subtask_id: int, + subtask_update: SubtaskUpdate, + db: Session = Depends(get_db) +): + """Update a subtask""" + todo_service = TodoService(db) + updated_subtask = todo_service.update_subtask(subtask_id, subtask_update) + if not updated_subtask: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Subtask not found" + ) + return updated_subtask \ No newline at end of file diff --git a/app/api/v1/endpoints/todos.py b/app/api/v1/endpoints/todos.py new file mode 100644 index 0000000000000000000000000000000000000000..e84376676140383f2f183362b94d7c4d5623da58 --- /dev/null +++ b/app/api/v1/endpoints/todos.py @@ -0,0 +1,75 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ....core.database import get_db +from ....schemas.todo import Todo, TodoCreate, TodoUpdate, TodoWithRelations +from ....services.todo_service import TodoService + +router = APIRouter() + +@router.get("/", response_model=List[TodoWithRelations]) +def get_todos( + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db) +): + """Get all todos with subtasks and translations""" + todo_service = TodoService(db) + return todo_service.get_todos(skip=skip, limit=limit) + +@router.get("/{todo_id}", response_model=TodoWithRelations) +def get_todo(todo_id: int, db: Session = Depends(get_db)): + """Get a specific todo with subtasks and translations""" + todo_service = TodoService(db) + todo = todo_service.get_todo(todo_id) + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + return todo + +@router.post("/", response_model=TodoWithRelations, status_code=status.HTTP_201_CREATED) +def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): + """Create a new todo""" + todo_service = TodoService(db) + return todo_service.create_todo(todo) + +@router.put("/{todo_id}", response_model=TodoWithRelations) +def update_todo( + todo_id: int, + todo_update: TodoUpdate, + db: Session = Depends(get_db) +): + """Update an existing todo""" + todo_service = TodoService(db) + updated_todo = todo_service.update_todo(todo_id, todo_update) + if not updated_todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + return updated_todo + +@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_todo(todo_id: int, db: Session = Depends(get_db)): + """Delete a todo""" + todo_service = TodoService(db) + success = todo_service.delete_todo(todo_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + +@router.patch("/{todo_id}/toggle", response_model=TodoWithRelations) +def toggle_todo_completion(todo_id: int, db: Session = Depends(get_db)): + """Toggle todo completion status""" + todo_service = TodoService(db) + updated_todo = todo_service.toggle_todo_completion(todo_id) + if not updated_todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + return updated_todo \ No newline at end of file diff --git a/app/api/v1/endpoints/translation.py b/app/api/v1/endpoints/translation.py new file mode 100644 index 0000000000000000000000000000000000000000..114b500b0a9b3ef9f2bbfcdc4a882cfee1736783 --- /dev/null +++ b/app/api/v1/endpoints/translation.py @@ -0,0 +1,67 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ....core.database import get_db +from ....schemas.translation import Translation, TranslationRequest, TodoTranslationRequest +from ....services.todo_service import TodoService + +router = APIRouter() + +@router.post("/{todo_id}/translate", response_model=Translation) +async def translate_todo( + todo_id: int, + request: TodoTranslationRequest, + db: Session = Depends(get_db) +): + """Translate a todo to target language""" + todo_service = TodoService(db) + + try: + translation = await todo_service.translate_todo( + todo_id=todo_id, + target_language=request.target_language + ) + if not translation: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + return translation + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + +@router.get("/{todo_id}/translations", response_model=List[Translation]) +def get_todo_translations(todo_id: int, db: Session = Depends(get_db)): + """Get all translations for a todo""" + todo_service = TodoService(db) + todo = todo_service.get_todo(todo_id) + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Todo not found" + ) + return todo_service.get_todo_translations(todo_id) + +@router.post("/translate", response_model=dict) +async def translate_text(request: TranslationRequest): + """Translate any text to target language""" + from ....services.translation_service import translation_service + + try: + translated_text = await translation_service.translate_text( + text=request.text, + target_language=request.target_language + ) + return { + "original_text": request.text, + "translated_text": translated_text, + "target_language": request.target_language + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b8ecf2f7eb6a063df313acde46713bc2206d1576 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +# Core configuration and database setup \ No newline at end of file diff --git a/app/core/__pycache__/__init__.cpython-312.pyc b/app/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..072e9d58ea72d1b11e3b7136b231511d17ac3094 Binary files /dev/null and b/app/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/core/__pycache__/config.cpython-312.pyc b/app/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f946b5b2812c6b202fe417062e8bfb2018cb5a98 Binary files /dev/null and b/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/app/core/__pycache__/database.cpython-312.pyc b/app/core/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1e20c1b17600b91422fd85994b666fcf01c07af Binary files /dev/null and b/app/core/__pycache__/database.cpython-312.pyc differ diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000000000000000000000000000000000000..a8e4b823d8afcb19d3362f5adec5d346f833561c --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,28 @@ +from pydantic_settings import BaseSettings +from typing import List +import os + +class Settings(BaseSettings): + # API Settings + API_V1_STR: str = "/api/v1" + PROJECT_NAME: str = "AI Todo App" + DEBUG: bool = True + + # Database + DATABASE_URL: str = "sqlite:///./todo_app.db" + + # Groq Configuration + GROQ_API_KEY: str + GROQ_MODEL_NAME: str = "llama3-8b-8192" + + # CORS + BACKEND_CORS_ORIGINS: List[str] = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + ] + + class Config: + env_file = ".env" + case_sensitive = True + +settings = Settings() \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000000000000000000000000000000000000..bb1879f67e75c790e070701ca9f04583307533c0 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,27 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from .config import settings + +# Create engine +if settings.DATABASE_URL.startswith("sqlite"): + engine = create_engine( + settings.DATABASE_URL, + connect_args={"check_same_thread": False} + ) +else: + engine = create_engine(settings.DATABASE_URL) + +# Create session +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base class for models +Base = declarative_base() + +# Dependency to get database session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..95a9e007d82c6c01d9b564c4e589f96c0a884aa4 --- /dev/null +++ b/app/main.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from .core.config import settings +from .core.database import engine, Base +from .api.v1.api import api_router + +# Create database tables +Base.metadata.create_all(bind=engine) + +# Create FastAPI app +app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json", + debug=settings.DEBUG +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.BACKEND_CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include API router +app.include_router(api_router, prefix=settings.API_V1_STR) + +@app.get("/") +async def root(): + return {"message": "AI Todo App API", "version": "1.0.0"} + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ac59f3cf80340f1bac9807b046ed6843d075b2f6 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# Database models \ No newline at end of file diff --git a/app/models/__pycache__/__init__.cpython-312.pyc b/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72277fc30786036dc3a5bbd416860caa2b94a618 Binary files /dev/null and b/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/models/__pycache__/subtask.cpython-312.pyc b/app/models/__pycache__/subtask.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56917336bf20309dad421b0edd6befaa57f6a8e9 Binary files /dev/null and b/app/models/__pycache__/subtask.cpython-312.pyc differ diff --git a/app/models/__pycache__/todo.cpython-312.pyc b/app/models/__pycache__/todo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e67d429f932dcd7ea72f92f1341a811521b1fa1c Binary files /dev/null and b/app/models/__pycache__/todo.cpython-312.pyc differ diff --git a/app/models/subtask.py b/app/models/subtask.py new file mode 100644 index 0000000000000000000000000000000000000000..efa17df8cafee71cc2a9273347a4ffcc116b24dd --- /dev/null +++ b/app/models/subtask.py @@ -0,0 +1,31 @@ +from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from ..core.database import Base + +class Subtask(Base): + __tablename__ = "subtasks" + + id = Column(Integer, primary_key=True, index=True) + todo_id = Column(Integer, ForeignKey("todos.id"), nullable=False) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + completed = Column(Boolean, default=False, nullable=False) + order_index = Column(Integer, default=0) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + # Relationships + todo = relationship("Todo", back_populates="subtasks") + +class Translation(Base): + __tablename__ = "translations" + + id = Column(Integer, primary_key=True, index=True) + todo_id = Column(Integer, ForeignKey("todos.id"), nullable=False) + language = Column(String(50), nullable=False) + translated_title = Column(String(500), nullable=False) + translated_description = Column(Text, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + # Relationships + todo = relationship("Todo", back_populates="translations") \ No newline at end of file diff --git a/app/models/todo.py b/app/models/todo.py new file mode 100644 index 0000000000000000000000000000000000000000..68d5e0f3a7f67f92478ff206755c15f38f967ba2 --- /dev/null +++ b/app/models/todo.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from ..core.database import Base + +class Todo(Base): + __tablename__ = "todos" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(255), nullable=False, index=True) + description = Column(Text, nullable=True) + completed = Column(Boolean, default=False, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + # Relationships + subtasks = relationship("Subtask", back_populates="todo", cascade="all, delete-orphan") + translations = relationship("Translation", back_populates="todo", cascade="all, delete-orphan") \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c15d85d3701c7169b7cb94d6ce3e5872ff26acff --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1 @@ +# Pydantic schemas for request/response models \ No newline at end of file diff --git a/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aad22ebd6d9ef1fe79e6f54fe9504a2b5c0f4d91 Binary files /dev/null and b/app/schemas/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/subtask.cpython-312.pyc b/app/schemas/__pycache__/subtask.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1df3963104cd12e2d83d836770e000f4b42b5b87 Binary files /dev/null and b/app/schemas/__pycache__/subtask.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/todo.cpython-312.pyc b/app/schemas/__pycache__/todo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf491a352153e9eb1e836939fbdea2b1bb5867c8 Binary files /dev/null and b/app/schemas/__pycache__/todo.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/translation.cpython-312.pyc b/app/schemas/__pycache__/translation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4092ae1c3741d481d9fa656e20704c318c846aef Binary files /dev/null and b/app/schemas/__pycache__/translation.cpython-312.pyc differ diff --git a/app/schemas/subtask.py b/app/schemas/subtask.py new file mode 100644 index 0000000000000000000000000000000000000000..9ac8bd48fa1108b8a2764a77a7d2c77de43b3fbb --- /dev/null +++ b/app/schemas/subtask.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + +class SubtaskBase(BaseModel): + title: str = Field(..., min_length=1, max_length=255) + description: Optional[str] = None + +class SubtaskCreate(SubtaskBase): + todo_id: int + order_index: Optional[int] = 0 + +class SubtaskUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=255) + description: Optional[str] = None + completed: Optional[bool] = None + order_index: Optional[int] = None + +class Subtask(SubtaskBase): + id: int + todo_id: int + completed: bool + order_index: int + created_at: datetime + + class Config: + from_attributes = True + +class SubtaskGenerateRequest(BaseModel): + todo_id: int + max_subtasks: Optional[int] = Field(default=5, ge=1, le=10) \ No newline at end of file diff --git a/app/schemas/todo.py b/app/schemas/todo.py new file mode 100644 index 0000000000000000000000000000000000000000..d0f98253e641f01ea039c90f9afd6f86ae74b3f8 --- /dev/null +++ b/app/schemas/todo.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel, Field +from typing import Optional, List, TYPE_CHECKING +from datetime import datetime + +if TYPE_CHECKING: + from .subtask import Subtask + from .translation import Translation + +# Base Todo schema +class TodoBase(BaseModel): + title: str = Field(..., min_length=1, max_length=255) + description: Optional[str] = None + +# Schema for creating Todo +class TodoCreate(TodoBase): + pass + +# Schema for updating Todo +class TodoUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=255) + description: Optional[str] = None + completed: Optional[bool] = None + +# Schema for Todo response +class Todo(TodoBase): + id: int + completed: bool + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + +# Schema for Todo with relations +class TodoWithRelations(Todo): + subtasks: List["Subtask"] = [] + translations: List["Translation"] = [] + +# Import the actual classes to resolve forward references +from .subtask import Subtask +from .translation import Translation + +# Rebuild the model to resolve forward references +TodoWithRelations.model_rebuild() \ No newline at end of file diff --git a/app/schemas/translation.py b/app/schemas/translation.py new file mode 100644 index 0000000000000000000000000000000000000000..1fc4b79ece02d01e32dd1093bafea09c1e5fd2dd --- /dev/null +++ b/app/schemas/translation.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + +class TranslationBase(BaseModel): + language: str = Field(..., min_length=2, max_length=50) + translated_title: str = Field(..., min_length=1, max_length=500) + translated_description: Optional[str] = None + +class Translation(TranslationBase): + id: int + todo_id: int + created_at: datetime + + class Config: + from_attributes = True + +class TranslationRequest(BaseModel): + text: str = Field(..., min_length=1) + target_language: str = Field(..., min_length=2, max_length=50) + +class TodoTranslationRequest(BaseModel): + target_language: str = Field(..., min_length=2, max_length=50) \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e834ff249e8dd7072868810b2216397ad3d830cf --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1 @@ +# Business logic services \ No newline at end of file diff --git a/app/services/__pycache__/__init__.cpython-312.pyc b/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bef8e7e47132ac27551d2582bf1e002dcb717a9 Binary files /dev/null and b/app/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/services/__pycache__/ai_service.cpython-312.pyc b/app/services/__pycache__/ai_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bb42d1c45f1f81c7325a58a71cc80d994f2c2c1 Binary files /dev/null and b/app/services/__pycache__/ai_service.cpython-312.pyc differ diff --git a/app/services/__pycache__/todo_service.cpython-312.pyc b/app/services/__pycache__/todo_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0da546220f3a14c46b3f64061423eef7ab38bf98 Binary files /dev/null and b/app/services/__pycache__/todo_service.cpython-312.pyc differ diff --git a/app/services/__pycache__/translation_service.cpython-312.pyc b/app/services/__pycache__/translation_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51b96b59c5a7de9d6a6f9f28eb8613b66f3ff56d Binary files /dev/null and b/app/services/__pycache__/translation_service.cpython-312.pyc differ diff --git a/app/services/ai_service.py b/app/services/ai_service.py new file mode 100644 index 0000000000000000000000000000000000000000..ef7086c533a9af941283d2a6d352020b0bc2f89d --- /dev/null +++ b/app/services/ai_service.py @@ -0,0 +1,200 @@ +from langchain_groq import ChatGroq +from langchain.schema import HumanMessage, SystemMessage +from typing import List, Dict +import json +import logging +from ..core.config import settings + +logger = logging.getLogger(__name__) + +class AIService: + def __init__(self): + self.llm = ChatGroq( + groq_api_key=settings.GROQ_API_KEY, + model_name=settings.GROQ_MODEL_NAME, + temperature=0.7, + max_tokens=1000 + ) + + async def generate_subtasks(self, todo_title: str, todo_description: str = "", max_subtasks: int = 5) -> List[Dict[str, str]]: + """Generate subtasks for a given todo using AI""" + try: + system_prompt = f"""You are an expert project management assistant specializing in software development tasks. + Given a main task, break it down into {max_subtasks} or fewer specific, actionable subtasks. + + CRITICAL: You must return ONLY a valid JSON array. Do not include any other text, explanations, or formatting. + + Requirements: + - Each subtask should be concrete and immediately actionable + - Focus on specific technical implementation steps + - Consider the context of software development, database, AI services, and API development + - Subtasks should follow a logical progression (setup โ†’ implementation โ†’ testing โ†’ deployment) + - Each subtask should be specific enough that someone can start working on it immediately + - Return ONLY a valid JSON array format - no other text + - Each subtask should have 'title' and 'description' fields + - Keep titles concise but descriptive + - Descriptions should provide clear guidance on what needs to be done + + For software development tasks, consider these categories: + - Setup/Configuration (environment, dependencies, project structure) + - Core Implementation (main features, database models, API endpoints) + - Integration (connecting services, APIs, databases) + - Testing (unit tests, integration tests, validation) + - Documentation (README, API docs, deployment guides) + + Example format for a task like "Implement backend with database, AI service, FastAPI": + [ + {{"title": "Set up FastAPI project structure", "description": "Create FastAPI app with proper directory structure and dependencies"}}, + {{"title": "Design and implement database models", "description": "Create SQLAlchemy models for todos, subtasks, and translations"}}, + {{"title": "Set up database migrations", "description": "Configure Alembic and create initial migration scripts"}}, + {{"title": "Implement core API endpoints", "description": "Build CRUD operations for todos with proper validation"}}, + {{"title": "Integrate AI service for subtask generation", "description": "Connect Groq API and implement subtask generation logic"}}, + {{"title": "Add translation service", "description": "Implement AI-powered translation for todos and subtasks"}}, + {{"title": "Create API documentation", "description": "Set up OpenAPI/Swagger documentation with examples"}}, + {{"title": "Add error handling and validation", "description": "Implement proper error responses and input validation"}} + ] + + IMPORTANT: Return ONLY the JSON array, no other text or formatting.""" + + user_prompt = f"""Main Task: {todo_title} + {f"Additional Context: {todo_description}" if todo_description else ""} + + Please break this down into specific, actionable subtasks. + Focus on concrete implementation steps that can be completed independently. + Consider the technical requirements and break down each major component into specific tasks. + + IMPORTANT: If the task mentions specific features (like database, AI service, FastAPI), + create separate subtasks for each feature. For example: + - If it mentions "database", create subtasks for database setup, models, migrations + - If it mentions "AI service", create subtasks for AI integration, API calls, error handling + - If it mentions "FastAPI", create subtasks for API endpoints, routing, documentation + + Make each subtask specific to the actual features mentioned in the task.""" + + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_prompt) + ] + logger.info(f"Messages: {user_prompt}") + + response = await self.llm.ainvoke(messages) + + logger.info(f"AI response: {response.content}") + + # Parse the JSON response + try: + # Clean the response content + content = response.content.strip() + + # Try to find JSON array in the response + import re + + # Look for JSON array pattern + json_match = re.search(r'\[.*\]', content, re.DOTALL) + if json_match: + content = json_match.group() + + # Try to parse as JSON + subtasks = json.loads(content) + + # Validate the structure + if not isinstance(subtasks, list): + raise ValueError("Response is not a list") + + validated_subtasks = [] + for subtask in subtasks[:max_subtasks]: + if isinstance(subtask, dict) and 'title' in subtask: + validated_subtasks.append({ + 'title': subtask.get('title', '').strip(), + 'description': subtask.get('description', '').strip() + }) + + if validated_subtasks: + return validated_subtasks + else: + raise ValueError("No valid subtasks found") + + except (json.JSONDecodeError, ValueError) as e: + logger.error(f"Failed to parse AI response as JSON: {response.content}") + logger.error(f"Error details: {str(e)}") + return self._fallback_subtasks(todo_title) + + except Exception as e: + logger.error(f"Error generating subtasks: {str(e)}") + return self._fallback_subtasks(todo_title) + + def _fallback_subtasks(self, todo_title: str) -> List[Dict[str, str]]: + """Fallback subtasks when AI fails""" + # Check if the task mentions specific features + todo_lower = todo_title.lower() + if "database" in todo_lower or "db" in todo_lower: + return [ + { + "title": "Set up database models", + "description": "Create SQLAlchemy models for the application data" + }, + { + "title": "Configure database connection", + "description": "Set up database URL and connection pooling" + }, + { + "title": "Create database migrations", + "description": "Set up Alembic and create initial migration scripts" + } + ] + elif "ai" in todo_lower or "artificial intelligence" in todo_lower: + return [ + { + "title": "Set up AI service integration", + "description": "Configure API keys and service connections" + }, + { + "title": "Implement AI feature logic", + "description": "Build the core AI functionality and API calls" + }, + { + "title": "Add error handling for AI service", + "description": "Implement proper error handling and fallbacks" + } + ] + elif "fastapi" in todo_lower or "api" in todo_lower: + return [ + { + "title": "Set up FastAPI project structure", + "description": "Create FastAPI app with proper directory structure" + }, + { + "title": "Implement API endpoints", + "description": "Build RESTful endpoints with proper routing" + }, + { + "title": "Add API documentation", + "description": "Set up OpenAPI/Swagger documentation" + } + ] + else: + return [ + { + "title": "Analyze requirements", + "description": f"Break down {todo_title} into specific technical requirements" + }, + { + "title": "Set up development environment", + "description": "Configure project structure and dependencies" + }, + { + "title": "Implement core features", + "description": "Build the main functionality based on requirements" + }, + { + "title": "Test and validate", + "description": "Create tests and validate all features work correctly" + }, + { + "title": "Document and deploy", + "description": "Create documentation and prepare for deployment" + } + ] + +# Global instance +ai_service = AIService() \ No newline at end of file diff --git a/app/services/todo_service.py b/app/services/todo_service.py new file mode 100644 index 0000000000000000000000000000000000000000..91739aa38544a6920811da5a16adf5ea58a63bde --- /dev/null +++ b/app/services/todo_service.py @@ -0,0 +1,177 @@ +from sqlalchemy.orm import Session +from typing import List, Optional +from ..models.todo import Todo +from ..models.subtask import Subtask, Translation +from ..schemas.todo import TodoCreate, TodoUpdate +from ..schemas.subtask import SubtaskCreate, SubtaskUpdate +from .ai_service import ai_service +from .translation_service import translation_service +import logging + +logger = logging.getLogger(__name__) + +class TodoService: + def __init__(self, db: Session): + self.db = db + + def get_todos(self, skip: int = 0, limit: int = 100) -> List[Todo]: + """Get all todos with pagination and relations""" + from sqlalchemy.orm import joinedload + return self.db.query(Todo).options( + joinedload(Todo.subtasks), + joinedload(Todo.translations) + ).offset(skip).limit(limit).all() + + def get_todo(self, todo_id: int) -> Optional[Todo]: + """Get a specific todo by ID with relations""" + from sqlalchemy.orm import joinedload + return self.db.query(Todo).options( + joinedload(Todo.subtasks), + joinedload(Todo.translations) + ).filter(Todo.id == todo_id).first() + + def create_todo(self, todo: TodoCreate) -> Todo: + """Create a new todo""" + db_todo = Todo(**todo.model_dump()) + self.db.add(db_todo) + self.db.commit() + self.db.refresh(db_todo) + + # Return the todo with relations + return self.get_todo(db_todo.id) + + def update_todo(self, todo_id: int, todo_update: TodoUpdate) -> Optional[Todo]: + """Update an existing todo""" + db_todo = self.get_todo(todo_id) + if not db_todo: + return None + + update_data = todo_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_todo, field, value) + + self.db.commit() + + # Return the updated todo with relations + return self.get_todo(todo_id) + + def delete_todo(self, todo_id: int) -> bool: + """Delete a todo""" + db_todo = self.get_todo(todo_id) + if not db_todo: + return False + + self.db.delete(db_todo) + self.db.commit() + return True + + def toggle_todo_completion(self, todo_id: int) -> Optional[Todo]: + """Toggle todo completion status""" + db_todo = self.get_todo(todo_id) + if not db_todo: + return None + + db_todo.completed = not db_todo.completed + self.db.commit() + + # Get the updated todo with relations + updated_todo = self.get_todo(todo_id) + return updated_todo + + async def generate_subtasks(self, todo_id: int, max_subtasks: int = 5) -> List[Subtask]: + """Generate AI subtasks for a todo""" + db_todo = self.get_todo(todo_id) + if not db_todo: + raise ValueError("Todo not found") + + # Generate subtasks using AI + ai_subtasks = await ai_service.generate_subtasks( + todo_title=db_todo.title, + todo_description=db_todo.description or "", + max_subtasks=max_subtasks + ) + + # Save subtasks to database + created_subtasks = [] + for i, subtask_data in enumerate(ai_subtasks): + subtask = Subtask( + todo_id=todo_id, + title=subtask_data['title'], + description=subtask_data['description'], + order_index=i + ) + self.db.add(subtask) + created_subtasks.append(subtask) + + self.db.commit() + + # Refresh all subtasks + for subtask in created_subtasks: + self.db.refresh(subtask) + + return created_subtasks + + async def translate_todo(self, todo_id: int, target_language: str) -> Optional[Translation]: + """Translate a todo to target language""" + db_todo = self.get_todo(todo_id) + if not db_todo: + return None + + # Check if translation already exists + existing_translation = self.db.query(Translation).filter( + Translation.todo_id == todo_id, + Translation.language == target_language + ).first() + + if existing_translation: + return existing_translation + + # Translate using AI + try: + translated_title = await translation_service.translate_text( + db_todo.title, target_language + ) + + translated_description = None + if db_todo.description: + translated_description = await translation_service.translate_text( + db_todo.description, target_language + ) + + # Save translation + translation = Translation( + todo_id=todo_id, + language=target_language, + translated_title=translated_title, + translated_description=translated_description + ) + + self.db.add(translation) + self.db.commit() + self.db.refresh(translation) + + return translation + + except Exception as e: + logger.error(f"Translation failed: {str(e)}") + raise Exception(f"Translation failed: {str(e)}") + + def get_todo_translations(self, todo_id: int) -> List[Translation]: + """Get all translations for a todo""" + return self.db.query(Translation).filter(Translation.todo_id == todo_id).all() + + def update_subtask(self, subtask_id: int, subtask_update: SubtaskUpdate) -> Optional[Subtask]: + """Update a subtask""" + from ..models.subtask import Subtask + + db_subtask = self.db.query(Subtask).filter(Subtask.id == subtask_id).first() + if not db_subtask: + return None + + update_data = subtask_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_subtask, field, value) + + self.db.commit() + self.db.refresh(db_subtask) + return db_subtask \ No newline at end of file diff --git a/app/services/translation_service.py b/app/services/translation_service.py new file mode 100644 index 0000000000000000000000000000000000000000..9e0023eaa53797650307d2ea6677f000a5dceb15 --- /dev/null +++ b/app/services/translation_service.py @@ -0,0 +1,77 @@ +from langchain_groq import ChatGroq +from langchain.schema import HumanMessage, SystemMessage +import logging +from ..core.config import settings + +logger = logging.getLogger(__name__) + +class TranslationService: + def __init__(self): + self.llm = ChatGroq( + groq_api_key=settings.GROQ_API_KEY, + model_name=settings.GROQ_MODEL_NAME, + temperature=0.1, # Lower temperature for translation accuracy + max_tokens=500 + ) + + async def translate_text(self, text: str, target_language: str) -> str: + """Translate text to target language using AI""" + try: + system_prompt = f"""You are a professional translator. + Translate the given text to {target_language} accurately while maintaining the original meaning and tone. + + Rules: + - Provide ONLY the translation, no explanations + - Maintain the original formatting if any + - Keep the same tone and style + - If the target language is unclear, ask for clarification + """ + + user_prompt = f"Translate this text to {target_language}: {text}" + + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_prompt) + ] + + response = await self.llm.ainvoke(messages) + translated_text = response.content.strip() + + # Basic validation + if not translated_text or translated_text.lower() == text.lower(): + raise ValueError("Translation failed or returned same text") + + return translated_text + + except Exception as e: + logger.error(f"Translation error: {str(e)}") + raise Exception(f"Translation failed: {str(e)}") + + async def detect_language(self, text: str) -> str: + """Detect the language of given text""" + try: + system_prompt = """You are a language detection expert. + Identify the language of the given text and respond with ONLY the language name in English. + Examples: English, Spanish, French, German, Chinese, Arabic, etc.""" + + user_prompt = f"What language is this text: {text}" + + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_prompt) + ] + + response = await self.llm.ainvoke(messages) + return response.content.strip() + + except Exception as e: + logger.error(f"Language detection error: {str(e)}") + return "Unknown" + + def is_language_supported(self, language: str) -> bool: + """Check if language is supported (basic validation)""" + # For now, assume all languages are supported by the AI model + return len(language.strip()) >= 2 + +# Global instance +translation_service = TranslationService() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b62733dc51bb35ae914d59b462dd5cdb9817a091 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + app: + build: . + ports: + - "7860:7860" + environment: + - DATABASE_URL=sqlite:///./todo_app.db + - GROQ_API_KEY=${GROQ_API_KEY} + - GROQ_MODEL_NAME=llama3-8b-8192 + - API_V1_STR=/api/v1 + - PROJECT_NAME=AI Todo App + - DEBUG=True + - BACKEND_CORS_ORIGINS=["http://localhost:3000", "https://yourdomain.vercel.app"] + volumes: + - ./todo_app.db:/app/todo_app.db + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000000000000000000000000000000000000..54a22871562b119e241222f704cc1989ecf9a97d --- /dev/null +++ b/env.example @@ -0,0 +1,16 @@ +# Database +DATABASE_URL=sqlite:///./todo_app.db +# For production use PostgreSQL: +# DATABASE_URL=postgresql://user:password@localhost/todo_app + +# Groq Configuration +GROQ_API_KEY=your_groq_api_key_here +GROQ_MODEL_NAME=llama3-8b-8192 + +# API Configuration +API_V1_STR=/api/v1 +PROJECT_NAME=AI Todo App +DEBUG=True + +# CORS +BACKEND_CORS_ORIGINS=["http://localhost:3000", "https://yourdomain.vercel.app"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..214de1b1b30cf90a1a450381a581573aaf059a97 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +fastapi +uvicorn[standard] +sqlalchemy +alembic +pydantic +pydantic-settings +python-dotenv +langchain +langchain-groq +groq +httpx +pytest +pytest-asyncio +python-multipart \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000000000000000000000000000000000000..5b15891e79974d2a465e2ae3d6bdf9fe18650105 --- /dev/null +++ b/run.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +""" +Simple script to run the FastAPI application +""" + +import uvicorn +import os +import sys + +# Add the current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +if __name__ == "__main__": + print("๐Ÿš€ Starting AI Todo App Backend...") + print("๐Ÿ“š API Documentation: http://localhost:7860/docs") + print("๐Ÿ” Health Check: http://localhost:7860/health") + print("=" * 50) + + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=7860, + reload=True, + log_level="info" + ) \ No newline at end of file diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000000000000000000000000000000000000..b82b4ed3368040edb0a7b462743f481f226c5262 --- /dev/null +++ b/test_api.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Simple API test script to verify endpoints work correctly +""" + +import requests +import json +import time +import sys +import os + +# Add the current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +BASE_URL = "http://localhost:8000/api/v1" + +def test_health_check(): + """Test the health check endpoint""" + try: + response = requests.get("http://localhost:8000/health") + if response.status_code == 200: + print("โœ… Health check passed") + return True + else: + print(f"โŒ Health check failed: {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("โŒ Could not connect to server. Make sure it's running on http://localhost:8000") + return False + +def test_create_todo(): + """Test creating a todo""" + try: + todo_data = { + "title": "Test Todo", + "description": "This is a test todo for API verification" + } + + response = requests.post( + f"{BASE_URL}/todos/", + json=todo_data, + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 201: + todo = response.json() + print(f"โœ… Created todo: {todo['title']} (ID: {todo['id']})") + return todo['id'] + else: + print(f"โŒ Failed to create todo: {response.status_code} - {response.text}") + return None + except Exception as e: + print(f"โŒ Error creating todo: {e}") + return None + +def test_get_todos(): + """Test getting all todos""" + try: + response = requests.get(f"{BASE_URL}/todos/") + + if response.status_code == 200: + todos = response.json() + print(f"โœ… Retrieved {len(todos)} todos") + return len(todos) > 0 + else: + print(f"โŒ Failed to get todos: {response.status_code}") + return False + except Exception as e: + print(f"โŒ Error getting todos: {e}") + return False + +def test_get_todo(todo_id): + """Test getting a specific todo""" + try: + response = requests.get(f"{BASE_URL}/todos/{todo_id}") + + if response.status_code == 200: + todo = response.json() + print(f"โœ… Retrieved todo: {todo['title']}") + return True + else: + print(f"โŒ Failed to get todo: {response.status_code}") + return False + except Exception as e: + print(f"โŒ Error getting todo: {e}") + return False + +def test_toggle_todo(todo_id): + """Test toggling todo completion""" + try: + response = requests.patch(f"{BASE_URL}/todos/{todo_id}/toggle") + + if response.status_code == 200: + todo = response.json() + print(f"โœ… Toggled todo completion: {todo['completed']}") + return True + else: + print(f"โŒ Failed to toggle todo: {response.status_code}") + return False + except Exception as e: + print(f"โŒ Error toggling todo: {e}") + return False + +def test_update_todo(todo_id): + """Test updating a todo""" + try: + update_data = { + "title": "Updated Test Todo", + "description": "This todo has been updated" + } + + response = requests.put( + f"{BASE_URL}/todos/{todo_id}", + json=update_data, + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 200: + todo = response.json() + print(f"โœ… Updated todo: {todo['title']}") + return True + else: + print(f"โŒ Failed to update todo: {response.status_code}") + return False + except Exception as e: + print(f"โŒ Error updating todo: {e}") + return False + +def test_delete_todo(todo_id): + """Test deleting a todo""" + try: + response = requests.delete(f"{BASE_URL}/todos/{todo_id}") + + if response.status_code == 204: + print("โœ… Deleted todo successfully") + return True + else: + print(f"โŒ Failed to delete todo: {response.status_code}") + return False + except Exception as e: + print(f"โŒ Error deleting todo: {e}") + return False + +def main(): + """Run all API tests""" + print("๐Ÿงช Testing AI Todo App API Endpoints") + print("=" * 50) + + # Test health check first + if not test_health_check(): + print("\nโŒ Server is not running. Please start the server first:") + print(" python run.py") + return False + + print("\n๐Ÿ” Running API tests...") + + # Test CRUD operations + todo_id = test_create_todo() + if not todo_id: + return False + + if not test_get_todos(): + return False + + if not test_get_todo(todo_id): + return False + + if not test_toggle_todo(todo_id): + return False + + if not test_update_todo(todo_id): + return False + + if not test_delete_todo(todo_id): + return False + + print("\n" + "=" * 50) + print("โœ… All API tests passed!") + print("\n๐ŸŽ‰ The backend is working correctly!") + print("\n๐Ÿ“š You can now:") + print(" - View API docs at: http://localhost:8000/docs") + print(" - Test AI features (requires GROQ_API_KEY)") + print(" - Integrate with your frontend application") + + return True + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test_setup.py b/test_setup.py new file mode 100644 index 0000000000000000000000000000000000000000..e280d4c97d62c3d7459fea7096c76ef84be10aa8 --- /dev/null +++ b/test_setup.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Simple test script to verify the backend setup +""" + +import asyncio +import sys +import os + +# Add the current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +def test_imports(): + """Test that all modules can be imported""" + try: + from app.core.config import settings + from app.core.database import get_db, Base, engine + from app.models.todo import Todo + from app.models.subtask import Subtask, Translation + from app.schemas.todo import TodoCreate, TodoUpdate + from app.services.todo_service import TodoService + from app.services.ai_service import ai_service + from app.services.translation_service import translation_service + print("โœ… All imports successful") + return True + except Exception as e: + print(f"โŒ Import error: {e}") + return False + +def test_database_connection(): + """Test database connection and table creation""" + try: + from app.core.database import engine, Base + from app.models import todo, subtask + + # Create tables + Base.metadata.create_all(bind=engine) + print("โœ… Database connection and table creation successful") + return True + except Exception as e: + print(f"โŒ Database error: {e}") + return False + +def test_config(): + """Test configuration loading""" + try: + from app.core.config import settings + print(f"โœ… Configuration loaded successfully") + print(f" - Project Name: {settings.PROJECT_NAME}") + print(f" - Database URL: {settings.DATABASE_URL}") + print(f" - API Version: {settings.API_V1_STR}") + return True + except Exception as e: + print(f"โŒ Configuration error: {e}") + return False + +async def test_ai_service(): + """Test AI service initialization""" + try: + from app.services.ai_service import ai_service + print("โœ… AI service initialized successfully") + return True + except Exception as e: + print(f"โŒ AI service error: {e}") + print(" Note: This is expected if GROQ_API_KEY is not set") + return True # Don't fail the test for missing API key + +async def test_translation_service(): + """Test translation service initialization""" + try: + from app.services.translation_service import translation_service + print("โœ… Translation service initialized successfully") + return True + except Exception as e: + print(f"โŒ Translation service error: {e}") + print(" Note: This is expected if GROQ_API_KEY is not set") + return True # Don't fail the test for missing API key + +def main(): + """Run all tests""" + print("๐Ÿงช Testing AI Todo App Backend Setup") + print("=" * 50) + + tests = [ + ("Import Test", test_imports), + ("Configuration Test", test_config), + ("Database Test", test_database_connection), + ] + + async_tests = [ + ("AI Service Test", test_ai_service), + ("Translation Service Test", test_translation_service), + ] + + # Run synchronous tests + for test_name, test_func in tests: + print(f"\n๐Ÿ” Running {test_name}...") + if not test_func(): + print(f"โŒ {test_name} failed") + return False + + # Run asynchronous tests + for test_name, test_func in async_tests: + print(f"\n๐Ÿ” Running {test_name}...") + if not asyncio.run(test_func()): + print(f"โŒ {test_name} failed") + return False + + print("\n" + "=" * 50) + print("โœ… All tests passed! Backend is ready to run.") + print("\n๐Ÿš€ To start the server, run:") + print(" uvicorn app.main:app --reload --host 0.0.0.0 --port 8000") + print("\n๐Ÿ“š API Documentation will be available at:") + print(" http://localhost:8000/docs") + + return True + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file