Spaces:
Sleeping
feat(ui): AgentFactory-inspired dark theme redesign
Browse filesComplete UI/UX overhaul with professional dark theme inspired by AgentFactory:
## Theme Changes (custom.css)
- Primary accent: Cyan/Teal (#00d4aa) for robotics feel
- Dark backgrounds: #0f0f14, #18181f (deep charcoal)
- Secondary accent: Electric Blue (#3b82f6)
- Professional typography with Inter font
- Smooth transitions and hover effects
- Custom scrollbar styling
- Print-friendly styles
## Homepage Redesign (index.js + index.module.css)
- Full-height hero section with gradient background
- Grid pattern overlay effect
- Animated humanoid robot with glowing cyan accents
- Professional badge/tag component
- Gradient text for title highlights
- New Stats section (8 Chapters, 16 Weeks, etc.)
- Redesigned highlight cards with hover animations
- Features grid with icons
- CTA section with radial gradient background
## Component Updates
- Reading Progress Bar: Cyan-to-blue gradient with glow
- Chapter Navigation: Glass-morphism dark cards
- Both components with light/dark mode support
## Design Elements
- Deep dark backgrounds (AgentFactory-style)
- Cyan/teal primary accents
- Subtle grid patterns
- Glassmorphism effects
- Smooth hover animations
- Professional typography
- Mobile-responsive design
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- alembic.ini +116 -116
- api/feedback.py +107 -107
- cleanup_code.py +149 -149
- database/migrations/env.py +92 -92
- database/migrations/script.py.mako +26 -26
- database/migrations/versions/57c6b0ea13f8_add_translations_table.py +54 -54
- database/migrations/versions/809b34b1c5dc_add_translation_feedback_table.py +43 -43
- models/translation.py +34 -34
- models/translation_feedback.py +34 -34
- requirements.txt +17 -17
- services/rate_limiter.py +115 -115
- services/translation_service.py +145 -145
- start.bat +26 -26
- start.ps1 +23 -23
- test_formatted_message.py +61 -61
- test_openrouter_direct.py +79 -79
- test_rag_service_debug.py +73 -73
- tests/e2e/test_translation_flow.py +295 -295
- tests/integration/test_feedback_endpoint.py +126 -126
- tests/unit/test_feedback_model.py +81 -81
|
@@ -1,116 +1,116 @@
|
|
| 1 |
-
# A generic, single database configuration.
|
| 2 |
-
|
| 3 |
-
[alembic]
|
| 4 |
-
# path to migration scripts
|
| 5 |
-
script_location = database/migrations
|
| 6 |
-
|
| 7 |
-
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
| 8 |
-
# Uncomment the line below if you want the files to be prepended with date and time
|
| 9 |
-
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
| 10 |
-
# for all available tokens
|
| 11 |
-
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
| 12 |
-
|
| 13 |
-
# sys.path path, will be prepended to sys.path if present.
|
| 14 |
-
# defaults to the current working directory.
|
| 15 |
-
prepend_sys_path = .
|
| 16 |
-
|
| 17 |
-
# timezone to use when rendering the date within the migration file
|
| 18 |
-
# as well as the filename.
|
| 19 |
-
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
| 20 |
-
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
| 21 |
-
# string value is passed to ZoneInfo()
|
| 22 |
-
# leave blank for localtime
|
| 23 |
-
# timezone =
|
| 24 |
-
|
| 25 |
-
# max length of characters to apply to the
|
| 26 |
-
# "slug" field
|
| 27 |
-
# truncate_slug_length = 40
|
| 28 |
-
|
| 29 |
-
# set to 'true' to run the environment during
|
| 30 |
-
# the 'revision' command, regardless of autogenerate
|
| 31 |
-
# revision_environment = false
|
| 32 |
-
|
| 33 |
-
# set to 'true' to allow .pyc and .pyo files without
|
| 34 |
-
# a source .py file to be detected as revisions in the
|
| 35 |
-
# versions/ directory
|
| 36 |
-
# sourceless = false
|
| 37 |
-
|
| 38 |
-
# version location specification; This defaults
|
| 39 |
-
# to database/migrations/versions. When using multiple version
|
| 40 |
-
# directories, initial revisions must be specified with --version-path.
|
| 41 |
-
# The path separator used here should be the separator specified by "version_path_separator" below.
|
| 42 |
-
# version_locations = %(here)s/bar:%(here)s/bat:database/migrations/versions
|
| 43 |
-
|
| 44 |
-
# version path separator; As mentioned above, this is the character used to split
|
| 45 |
-
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
| 46 |
-
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
| 47 |
-
# Valid values for version_path_separator are:
|
| 48 |
-
#
|
| 49 |
-
# version_path_separator = :
|
| 50 |
-
# version_path_separator = ;
|
| 51 |
-
# version_path_separator = space
|
| 52 |
-
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
| 53 |
-
|
| 54 |
-
# set to 'true' to search source files recursively
|
| 55 |
-
# in each "version_locations" directory
|
| 56 |
-
# new in Alembic version 1.10
|
| 57 |
-
# recursive_version_locations = false
|
| 58 |
-
|
| 59 |
-
# the output encoding used when revision files
|
| 60 |
-
# are written from script.py.mako
|
| 61 |
-
# output_encoding = utf-8
|
| 62 |
-
|
| 63 |
-
# sqlalchemy.url will be set in env.py from environment variable
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
[post_write_hooks]
|
| 67 |
-
# post_write_hooks defines scripts or Python functions that are run
|
| 68 |
-
# on newly generated revision scripts. See the documentation for further
|
| 69 |
-
# detail and examples
|
| 70 |
-
|
| 71 |
-
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
| 72 |
-
# hooks = black
|
| 73 |
-
# black.type = console_scripts
|
| 74 |
-
# black.entrypoint = black
|
| 75 |
-
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
| 76 |
-
|
| 77 |
-
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
| 78 |
-
# hooks = ruff
|
| 79 |
-
# ruff.type = exec
|
| 80 |
-
# ruff.executable = %(here)s/.venv/bin/ruff
|
| 81 |
-
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
| 82 |
-
|
| 83 |
-
# Logging configuration
|
| 84 |
-
[loggers]
|
| 85 |
-
keys = root,sqlalchemy,alembic
|
| 86 |
-
|
| 87 |
-
[handlers]
|
| 88 |
-
keys = console
|
| 89 |
-
|
| 90 |
-
[formatters]
|
| 91 |
-
keys = generic
|
| 92 |
-
|
| 93 |
-
[logger_root]
|
| 94 |
-
level = WARN
|
| 95 |
-
handlers = console
|
| 96 |
-
qualname =
|
| 97 |
-
|
| 98 |
-
[logger_sqlalchemy]
|
| 99 |
-
level = WARN
|
| 100 |
-
handlers =
|
| 101 |
-
qualname = sqlalchemy.engine
|
| 102 |
-
|
| 103 |
-
[logger_alembic]
|
| 104 |
-
level = INFO
|
| 105 |
-
handlers =
|
| 106 |
-
qualname = alembic
|
| 107 |
-
|
| 108 |
-
[handler_console]
|
| 109 |
-
class = StreamHandler
|
| 110 |
-
args = (sys.stderr,)
|
| 111 |
-
level = NOTSET
|
| 112 |
-
formatter = generic
|
| 113 |
-
|
| 114 |
-
[formatter_generic]
|
| 115 |
-
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 116 |
-
datefmt = %H:%M:%S
|
|
|
|
| 1 |
+
# A generic, single database configuration.
|
| 2 |
+
|
| 3 |
+
[alembic]
|
| 4 |
+
# path to migration scripts
|
| 5 |
+
script_location = database/migrations
|
| 6 |
+
|
| 7 |
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
| 8 |
+
# Uncomment the line below if you want the files to be prepended with date and time
|
| 9 |
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
| 10 |
+
# for all available tokens
|
| 11 |
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
| 12 |
+
|
| 13 |
+
# sys.path path, will be prepended to sys.path if present.
|
| 14 |
+
# defaults to the current working directory.
|
| 15 |
+
prepend_sys_path = .
|
| 16 |
+
|
| 17 |
+
# timezone to use when rendering the date within the migration file
|
| 18 |
+
# as well as the filename.
|
| 19 |
+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
| 20 |
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
| 21 |
+
# string value is passed to ZoneInfo()
|
| 22 |
+
# leave blank for localtime
|
| 23 |
+
# timezone =
|
| 24 |
+
|
| 25 |
+
# max length of characters to apply to the
|
| 26 |
+
# "slug" field
|
| 27 |
+
# truncate_slug_length = 40
|
| 28 |
+
|
| 29 |
+
# set to 'true' to run the environment during
|
| 30 |
+
# the 'revision' command, regardless of autogenerate
|
| 31 |
+
# revision_environment = false
|
| 32 |
+
|
| 33 |
+
# set to 'true' to allow .pyc and .pyo files without
|
| 34 |
+
# a source .py file to be detected as revisions in the
|
| 35 |
+
# versions/ directory
|
| 36 |
+
# sourceless = false
|
| 37 |
+
|
| 38 |
+
# version location specification; This defaults
|
| 39 |
+
# to database/migrations/versions. When using multiple version
|
| 40 |
+
# directories, initial revisions must be specified with --version-path.
|
| 41 |
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
| 42 |
+
# version_locations = %(here)s/bar:%(here)s/bat:database/migrations/versions
|
| 43 |
+
|
| 44 |
+
# version path separator; As mentioned above, this is the character used to split
|
| 45 |
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
| 46 |
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
| 47 |
+
# Valid values for version_path_separator are:
|
| 48 |
+
#
|
| 49 |
+
# version_path_separator = :
|
| 50 |
+
# version_path_separator = ;
|
| 51 |
+
# version_path_separator = space
|
| 52 |
+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
| 53 |
+
|
| 54 |
+
# set to 'true' to search source files recursively
|
| 55 |
+
# in each "version_locations" directory
|
| 56 |
+
# new in Alembic version 1.10
|
| 57 |
+
# recursive_version_locations = false
|
| 58 |
+
|
| 59 |
+
# the output encoding used when revision files
|
| 60 |
+
# are written from script.py.mako
|
| 61 |
+
# output_encoding = utf-8
|
| 62 |
+
|
| 63 |
+
# sqlalchemy.url will be set in env.py from environment variable
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
[post_write_hooks]
|
| 67 |
+
# post_write_hooks defines scripts or Python functions that are run
|
| 68 |
+
# on newly generated revision scripts. See the documentation for further
|
| 69 |
+
# detail and examples
|
| 70 |
+
|
| 71 |
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
| 72 |
+
# hooks = black
|
| 73 |
+
# black.type = console_scripts
|
| 74 |
+
# black.entrypoint = black
|
| 75 |
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
| 76 |
+
|
| 77 |
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
| 78 |
+
# hooks = ruff
|
| 79 |
+
# ruff.type = exec
|
| 80 |
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
| 81 |
+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
| 82 |
+
|
| 83 |
+
# Logging configuration
|
| 84 |
+
[loggers]
|
| 85 |
+
keys = root,sqlalchemy,alembic
|
| 86 |
+
|
| 87 |
+
[handlers]
|
| 88 |
+
keys = console
|
| 89 |
+
|
| 90 |
+
[formatters]
|
| 91 |
+
keys = generic
|
| 92 |
+
|
| 93 |
+
[logger_root]
|
| 94 |
+
level = WARN
|
| 95 |
+
handlers = console
|
| 96 |
+
qualname =
|
| 97 |
+
|
| 98 |
+
[logger_sqlalchemy]
|
| 99 |
+
level = WARN
|
| 100 |
+
handlers =
|
| 101 |
+
qualname = sqlalchemy.engine
|
| 102 |
+
|
| 103 |
+
[logger_alembic]
|
| 104 |
+
level = INFO
|
| 105 |
+
handlers =
|
| 106 |
+
qualname = alembic
|
| 107 |
+
|
| 108 |
+
[handler_console]
|
| 109 |
+
class = StreamHandler
|
| 110 |
+
args = (sys.stderr,)
|
| 111 |
+
level = NOTSET
|
| 112 |
+
formatter = generic
|
| 113 |
+
|
| 114 |
+
[formatter_generic]
|
| 115 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 116 |
+
datefmt = %H:%M:%S
|
|
@@ -1,107 +1,107 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Translation Feedback API
|
| 3 |
-
|
| 4 |
-
Endpoint: POST /api/translate/feedback
|
| 5 |
-
Purpose: Allow users to report translation quality issues
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from fastapi import APIRouter, Depends, HTTPException, Header
|
| 9 |
-
from pydantic import BaseModel, Field
|
| 10 |
-
from auth.jwt_utils import get_current_user_id_from_token
|
| 11 |
-
from database.db import get_db
|
| 12 |
-
from models.translation_feedback import TranslationFeedback
|
| 13 |
-
from sqlalchemy import select
|
| 14 |
-
from database.models import Translation
|
| 15 |
-
import uuid
|
| 16 |
-
import logging
|
| 17 |
-
|
| 18 |
-
logger = logging.getLogger(__name__)
|
| 19 |
-
|
| 20 |
-
router = APIRouter()
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
# JWT verification dependency
|
| 24 |
-
def verify_jwt_token(authorization: str = Header(None)) -> dict:
|
| 25 |
-
"""Verify JWT token from Authorization header"""
|
| 26 |
-
if not authorization:
|
| 27 |
-
raise HTTPException(status_code=401, detail="Authorization header missing")
|
| 28 |
-
|
| 29 |
-
if not authorization.startswith("Bearer "):
|
| 30 |
-
raise HTTPException(status_code=401, detail="Invalid authorization format")
|
| 31 |
-
|
| 32 |
-
token = authorization[7:] # Remove "Bearer " prefix
|
| 33 |
-
user_id = get_current_user_id_from_token(token)
|
| 34 |
-
|
| 35 |
-
if not user_id:
|
| 36 |
-
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
| 37 |
-
|
| 38 |
-
return {"user_id": user_id}
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
class FeedbackRequest(BaseModel):
|
| 42 |
-
"""Request model for submitting translation feedback"""
|
| 43 |
-
translation_id: str = Field(..., description="UUID of the translation being reported")
|
| 44 |
-
issue_description: str = Field(..., min_length=10, max_length=2000, description="Description of the translation issue")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
class FeedbackResponse(BaseModel):
|
| 48 |
-
"""Response model for feedback submission"""
|
| 49 |
-
feedback_id: str
|
| 50 |
-
message: str
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
@router.post("/api/translate/feedback", response_model=FeedbackResponse, status_code=201)
|
| 54 |
-
def submit_translation_feedback(
|
| 55 |
-
request: FeedbackRequest,
|
| 56 |
-
user_data: dict = Depends(verify_jwt_token),
|
| 57 |
-
db = Depends(get_db)
|
| 58 |
-
):
|
| 59 |
-
"""
|
| 60 |
-
Submit feedback on translation quality
|
| 61 |
-
|
| 62 |
-
**Authentication**: JWT token required
|
| 63 |
-
|
| 64 |
-
**Request Body**:
|
| 65 |
-
- translation_id: UUID of the translation
|
| 66 |
-
- issue_description: Description of the issue (10-2000 characters)
|
| 67 |
-
|
| 68 |
-
**Response**: 201 Created with feedback_id
|
| 69 |
-
"""
|
| 70 |
-
user_id = user_data["user_id"]
|
| 71 |
-
|
| 72 |
-
try:
|
| 73 |
-
# Validate translation_id is a valid UUID
|
| 74 |
-
translation_uuid = uuid.UUID(request.translation_id)
|
| 75 |
-
except ValueError:
|
| 76 |
-
raise HTTPException(status_code=400, detail="Invalid translation_id format. Must be a valid UUID.")
|
| 77 |
-
|
| 78 |
-
try:
|
| 79 |
-
# Optional: Verify translation exists (can skip for better performance)
|
| 80 |
-
# result = await db.execute(select(Translation).where(Translation.id == translation_uuid))
|
| 81 |
-
# translation = result.scalar_one_or_none()
|
| 82 |
-
# if not translation:
|
| 83 |
-
# raise HTTPException(status_code=404, detail="Translation not found")
|
| 84 |
-
|
| 85 |
-
# Create feedback record
|
| 86 |
-
feedback = TranslationFeedback(
|
| 87 |
-
id=uuid.uuid4(),
|
| 88 |
-
translation_id=translation_uuid,
|
| 89 |
-
user_id=uuid.UUID(user_id),
|
| 90 |
-
issue_description=request.issue_description.strip()
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
-
db.add(feedback)
|
| 94 |
-
db.commit()
|
| 95 |
-
db.refresh(feedback)
|
| 96 |
-
|
| 97 |
-
logger.info(f"Feedback submitted: feedback_id={feedback.id}, translation_id={translation_uuid}, user_id={user_id}")
|
| 98 |
-
|
| 99 |
-
return FeedbackResponse(
|
| 100 |
-
feedback_id=str(feedback.id),
|
| 101 |
-
message="Thank you for your feedback! We'll review the translation quality issue."
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
except Exception as e:
|
| 105 |
-
logger.error(f"Failed to submit feedback: {str(e)}")
|
| 106 |
-
db.rollback()
|
| 107 |
-
raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Translation Feedback API
|
| 3 |
+
|
| 4 |
+
Endpoint: POST /api/translate/feedback
|
| 5 |
+
Purpose: Allow users to report translation quality issues
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from fastapi import APIRouter, Depends, HTTPException, Header
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from auth.jwt_utils import get_current_user_id_from_token
|
| 11 |
+
from database.db import get_db
|
| 12 |
+
from models.translation_feedback import TranslationFeedback
|
| 13 |
+
from sqlalchemy import select
|
| 14 |
+
from database.models import Translation
|
| 15 |
+
import uuid
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
router = APIRouter()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# JWT verification dependency
|
| 24 |
+
def verify_jwt_token(authorization: str = Header(None)) -> dict:
|
| 25 |
+
"""Verify JWT token from Authorization header"""
|
| 26 |
+
if not authorization:
|
| 27 |
+
raise HTTPException(status_code=401, detail="Authorization header missing")
|
| 28 |
+
|
| 29 |
+
if not authorization.startswith("Bearer "):
|
| 30 |
+
raise HTTPException(status_code=401, detail="Invalid authorization format")
|
| 31 |
+
|
| 32 |
+
token = authorization[7:] # Remove "Bearer " prefix
|
| 33 |
+
user_id = get_current_user_id_from_token(token)
|
| 34 |
+
|
| 35 |
+
if not user_id:
|
| 36 |
+
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
| 37 |
+
|
| 38 |
+
return {"user_id": user_id}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class FeedbackRequest(BaseModel):
|
| 42 |
+
"""Request model for submitting translation feedback"""
|
| 43 |
+
translation_id: str = Field(..., description="UUID of the translation being reported")
|
| 44 |
+
issue_description: str = Field(..., min_length=10, max_length=2000, description="Description of the translation issue")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class FeedbackResponse(BaseModel):
|
| 48 |
+
"""Response model for feedback submission"""
|
| 49 |
+
feedback_id: str
|
| 50 |
+
message: str
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@router.post("/api/translate/feedback", response_model=FeedbackResponse, status_code=201)
|
| 54 |
+
def submit_translation_feedback(
|
| 55 |
+
request: FeedbackRequest,
|
| 56 |
+
user_data: dict = Depends(verify_jwt_token),
|
| 57 |
+
db = Depends(get_db)
|
| 58 |
+
):
|
| 59 |
+
"""
|
| 60 |
+
Submit feedback on translation quality
|
| 61 |
+
|
| 62 |
+
**Authentication**: JWT token required
|
| 63 |
+
|
| 64 |
+
**Request Body**:
|
| 65 |
+
- translation_id: UUID of the translation
|
| 66 |
+
- issue_description: Description of the issue (10-2000 characters)
|
| 67 |
+
|
| 68 |
+
**Response**: 201 Created with feedback_id
|
| 69 |
+
"""
|
| 70 |
+
user_id = user_data["user_id"]
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
# Validate translation_id is a valid UUID
|
| 74 |
+
translation_uuid = uuid.UUID(request.translation_id)
|
| 75 |
+
except ValueError:
|
| 76 |
+
raise HTTPException(status_code=400, detail="Invalid translation_id format. Must be a valid UUID.")
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
# Optional: Verify translation exists (can skip for better performance)
|
| 80 |
+
# result = await db.execute(select(Translation).where(Translation.id == translation_uuid))
|
| 81 |
+
# translation = result.scalar_one_or_none()
|
| 82 |
+
# if not translation:
|
| 83 |
+
# raise HTTPException(status_code=404, detail="Translation not found")
|
| 84 |
+
|
| 85 |
+
# Create feedback record
|
| 86 |
+
feedback = TranslationFeedback(
|
| 87 |
+
id=uuid.uuid4(),
|
| 88 |
+
translation_id=translation_uuid,
|
| 89 |
+
user_id=uuid.UUID(user_id),
|
| 90 |
+
issue_description=request.issue_description.strip()
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
db.add(feedback)
|
| 94 |
+
db.commit()
|
| 95 |
+
db.refresh(feedback)
|
| 96 |
+
|
| 97 |
+
logger.info(f"Feedback submitted: feedback_id={feedback.id}, translation_id={translation_uuid}, user_id={user_id}")
|
| 98 |
+
|
| 99 |
+
return FeedbackResponse(
|
| 100 |
+
feedback_id=str(feedback.id),
|
| 101 |
+
message="Thank you for your feedback! We'll review the translation quality issue."
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"Failed to submit feedback: {str(e)}")
|
| 106 |
+
db.rollback()
|
| 107 |
+
raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
|
|
@@ -1,149 +1,149 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Code Cleanup Script for Feature 006: Urdu Translation
|
| 4 |
-
|
| 5 |
-
Removes unused imports and organizes code
|
| 6 |
-
Run: python3 cleanup_code.py
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import os
|
| 10 |
-
import re
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def remove_unused_imports(file_path):
|
| 15 |
-
"""Remove commented imports and organize imports"""
|
| 16 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 17 |
-
content = f.read()
|
| 18 |
-
|
| 19 |
-
original_content = content
|
| 20 |
-
lines = content.split('\n')
|
| 21 |
-
cleaned_lines = []
|
| 22 |
-
import_section = []
|
| 23 |
-
in_import_section = True
|
| 24 |
-
|
| 25 |
-
for line in lines:
|
| 26 |
-
# Skip empty lines in import section
|
| 27 |
-
if in_import_section and line.strip() == '':
|
| 28 |
-
continue
|
| 29 |
-
|
| 30 |
-
# Collect imports
|
| 31 |
-
if in_import_section and (line.startswith('import ') or line.startswith('from ')):
|
| 32 |
-
import_section.append(line)
|
| 33 |
-
continue
|
| 34 |
-
|
| 35 |
-
# End of import section
|
| 36 |
-
if in_import_section and not line.startswith('import ') and not line.startswith('from ') and line.strip():
|
| 37 |
-
in_import_section = False
|
| 38 |
-
# Add organized imports
|
| 39 |
-
import_section.sort()
|
| 40 |
-
cleaned_lines.extend(import_section)
|
| 41 |
-
cleaned_lines.append('')
|
| 42 |
-
|
| 43 |
-
cleaned_lines.append(line)
|
| 44 |
-
|
| 45 |
-
# Write back if changed
|
| 46 |
-
new_content = '\n'.join(cleaned_lines)
|
| 47 |
-
if new_content != original_content:
|
| 48 |
-
with open(file_path, 'w', encoding='utf-8') as f:
|
| 49 |
-
f.write(new_content)
|
| 50 |
-
return True
|
| 51 |
-
return False
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def cleanup_trailing_whitespace(file_path):
|
| 55 |
-
"""Remove trailing whitespace from lines"""
|
| 56 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 57 |
-
content = f.read()
|
| 58 |
-
|
| 59 |
-
lines = content.split('\n')
|
| 60 |
-
cleaned_lines = [line.rstrip() for line in lines]
|
| 61 |
-
new_content = '\n'.join(cleaned_lines)
|
| 62 |
-
|
| 63 |
-
if new_content != content:
|
| 64 |
-
with open(file_path, 'w', encoding='utf-8') as f:
|
| 65 |
-
f.write(new_content)
|
| 66 |
-
return True
|
| 67 |
-
return False
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def check_pep8_line_length(file_path):
|
| 71 |
-
"""Check for lines exceeding PEP 8 line length (120 chars is acceptable for modern code)"""
|
| 72 |
-
issues = []
|
| 73 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 74 |
-
for i, line in enumerate(f, 1):
|
| 75 |
-
if len(line.rstrip()) > 120:
|
| 76 |
-
issues.append(f" Line {i}: {len(line.rstrip())} characters")
|
| 77 |
-
|
| 78 |
-
return issues
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def main():
|
| 82 |
-
"""Main cleanup function"""
|
| 83 |
-
backend_dir = Path(__file__).parent
|
| 84 |
-
|
| 85 |
-
# Files to cleanup
|
| 86 |
-
files_to_check = [
|
| 87 |
-
'api/translation.py',
|
| 88 |
-
'api/feedback.py',
|
| 89 |
-
'services/translation_service.py',
|
| 90 |
-
'services/rate_limiter.py',
|
| 91 |
-
'models/translation_feedback.py',
|
| 92 |
-
'main.py'
|
| 93 |
-
]
|
| 94 |
-
|
| 95 |
-
print("🧹 Starting code cleanup...")
|
| 96 |
-
print(f"📁 Backend directory: {backend_dir}\n")
|
| 97 |
-
|
| 98 |
-
total_files = 0
|
| 99 |
-
files_modified = 0
|
| 100 |
-
|
| 101 |
-
for file_rel_path in files_to_check:
|
| 102 |
-
file_path = backend_dir / file_rel_path
|
| 103 |
-
|
| 104 |
-
if not file_path.exists():
|
| 105 |
-
print(f"⚠️ {file_rel_path} - NOT FOUND")
|
| 106 |
-
continue
|
| 107 |
-
|
| 108 |
-
total_files += 1
|
| 109 |
-
print(f"🔍 Checking {file_rel_path}...")
|
| 110 |
-
|
| 111 |
-
modified = False
|
| 112 |
-
|
| 113 |
-
# Cleanup trailing whitespace
|
| 114 |
-
if cleanup_trailing_whitespace(file_path):
|
| 115 |
-
print(f" ✅ Removed trailing whitespace")
|
| 116 |
-
modified = True
|
| 117 |
-
|
| 118 |
-
# Check line length
|
| 119 |
-
long_lines = check_pep8_line_length(file_path)
|
| 120 |
-
if long_lines:
|
| 121 |
-
print(f" ⚠️ Found {len(long_lines)} lines exceeding 120 characters:")
|
| 122 |
-
for issue in long_lines[:3]: # Show first 3
|
| 123 |
-
print(issue)
|
| 124 |
-
if len(long_lines) > 3:
|
| 125 |
-
print(f" ... and {len(long_lines) - 3} more")
|
| 126 |
-
|
| 127 |
-
if modified:
|
| 128 |
-
files_modified += 1
|
| 129 |
-
else:
|
| 130 |
-
print(f" ✓ No changes needed")
|
| 131 |
-
|
| 132 |
-
print()
|
| 133 |
-
|
| 134 |
-
print("═" * 50)
|
| 135 |
-
print(f"✅ Cleanup complete!")
|
| 136 |
-
print(f"📊 Files checked: {total_files}")
|
| 137 |
-
print(f"✏️ Files modified: {files_modified}")
|
| 138 |
-
print("═" * 50)
|
| 139 |
-
|
| 140 |
-
# Additional recommendations
|
| 141 |
-
print("\n📝 Additional Recommendations:")
|
| 142 |
-
print(" 1. Run: pip install black flake8 # Install linters")
|
| 143 |
-
print(" 2. Run: black backend/ # Auto-format code")
|
| 144 |
-
print(" 3. Run: flake8 backend/ # Check PEP 8 compliance")
|
| 145 |
-
print(" 4. Add .flake8 config to exclude tests/ from strict checks")
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
if __name__ == '__main__':
|
| 149 |
-
main()
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Code Cleanup Script for Feature 006: Urdu Translation
|
| 4 |
+
|
| 5 |
+
Removes unused imports and organizes code
|
| 6 |
+
Run: python3 cleanup_code.py
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def remove_unused_imports(file_path):
|
| 15 |
+
"""Remove commented imports and organize imports"""
|
| 16 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 17 |
+
content = f.read()
|
| 18 |
+
|
| 19 |
+
original_content = content
|
| 20 |
+
lines = content.split('\n')
|
| 21 |
+
cleaned_lines = []
|
| 22 |
+
import_section = []
|
| 23 |
+
in_import_section = True
|
| 24 |
+
|
| 25 |
+
for line in lines:
|
| 26 |
+
# Skip empty lines in import section
|
| 27 |
+
if in_import_section and line.strip() == '':
|
| 28 |
+
continue
|
| 29 |
+
|
| 30 |
+
# Collect imports
|
| 31 |
+
if in_import_section and (line.startswith('import ') or line.startswith('from ')):
|
| 32 |
+
import_section.append(line)
|
| 33 |
+
continue
|
| 34 |
+
|
| 35 |
+
# End of import section
|
| 36 |
+
if in_import_section and not line.startswith('import ') and not line.startswith('from ') and line.strip():
|
| 37 |
+
in_import_section = False
|
| 38 |
+
# Add organized imports
|
| 39 |
+
import_section.sort()
|
| 40 |
+
cleaned_lines.extend(import_section)
|
| 41 |
+
cleaned_lines.append('')
|
| 42 |
+
|
| 43 |
+
cleaned_lines.append(line)
|
| 44 |
+
|
| 45 |
+
# Write back if changed
|
| 46 |
+
new_content = '\n'.join(cleaned_lines)
|
| 47 |
+
if new_content != original_content:
|
| 48 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 49 |
+
f.write(new_content)
|
| 50 |
+
return True
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def cleanup_trailing_whitespace(file_path):
|
| 55 |
+
"""Remove trailing whitespace from lines"""
|
| 56 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 57 |
+
content = f.read()
|
| 58 |
+
|
| 59 |
+
lines = content.split('\n')
|
| 60 |
+
cleaned_lines = [line.rstrip() for line in lines]
|
| 61 |
+
new_content = '\n'.join(cleaned_lines)
|
| 62 |
+
|
| 63 |
+
if new_content != content:
|
| 64 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 65 |
+
f.write(new_content)
|
| 66 |
+
return True
|
| 67 |
+
return False
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def check_pep8_line_length(file_path):
|
| 71 |
+
"""Check for lines exceeding PEP 8 line length (120 chars is acceptable for modern code)"""
|
| 72 |
+
issues = []
|
| 73 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 74 |
+
for i, line in enumerate(f, 1):
|
| 75 |
+
if len(line.rstrip()) > 120:
|
| 76 |
+
issues.append(f" Line {i}: {len(line.rstrip())} characters")
|
| 77 |
+
|
| 78 |
+
return issues
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def main():
|
| 82 |
+
"""Main cleanup function"""
|
| 83 |
+
backend_dir = Path(__file__).parent
|
| 84 |
+
|
| 85 |
+
# Files to cleanup
|
| 86 |
+
files_to_check = [
|
| 87 |
+
'api/translation.py',
|
| 88 |
+
'api/feedback.py',
|
| 89 |
+
'services/translation_service.py',
|
| 90 |
+
'services/rate_limiter.py',
|
| 91 |
+
'models/translation_feedback.py',
|
| 92 |
+
'main.py'
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
print("🧹 Starting code cleanup...")
|
| 96 |
+
print(f"📁 Backend directory: {backend_dir}\n")
|
| 97 |
+
|
| 98 |
+
total_files = 0
|
| 99 |
+
files_modified = 0
|
| 100 |
+
|
| 101 |
+
for file_rel_path in files_to_check:
|
| 102 |
+
file_path = backend_dir / file_rel_path
|
| 103 |
+
|
| 104 |
+
if not file_path.exists():
|
| 105 |
+
print(f"⚠️ {file_rel_path} - NOT FOUND")
|
| 106 |
+
continue
|
| 107 |
+
|
| 108 |
+
total_files += 1
|
| 109 |
+
print(f"🔍 Checking {file_rel_path}...")
|
| 110 |
+
|
| 111 |
+
modified = False
|
| 112 |
+
|
| 113 |
+
# Cleanup trailing whitespace
|
| 114 |
+
if cleanup_trailing_whitespace(file_path):
|
| 115 |
+
print(f" ✅ Removed trailing whitespace")
|
| 116 |
+
modified = True
|
| 117 |
+
|
| 118 |
+
# Check line length
|
| 119 |
+
long_lines = check_pep8_line_length(file_path)
|
| 120 |
+
if long_lines:
|
| 121 |
+
print(f" ⚠️ Found {len(long_lines)} lines exceeding 120 characters:")
|
| 122 |
+
for issue in long_lines[:3]: # Show first 3
|
| 123 |
+
print(issue)
|
| 124 |
+
if len(long_lines) > 3:
|
| 125 |
+
print(f" ... and {len(long_lines) - 3} more")
|
| 126 |
+
|
| 127 |
+
if modified:
|
| 128 |
+
files_modified += 1
|
| 129 |
+
else:
|
| 130 |
+
print(f" ✓ No changes needed")
|
| 131 |
+
|
| 132 |
+
print()
|
| 133 |
+
|
| 134 |
+
print("═" * 50)
|
| 135 |
+
print(f"✅ Cleanup complete!")
|
| 136 |
+
print(f"📊 Files checked: {total_files}")
|
| 137 |
+
print(f"✏️ Files modified: {files_modified}")
|
| 138 |
+
print("═" * 50)
|
| 139 |
+
|
| 140 |
+
# Additional recommendations
|
| 141 |
+
print("\n📝 Additional Recommendations:")
|
| 142 |
+
print(" 1. Run: pip install black flake8 # Install linters")
|
| 143 |
+
print(" 2. Run: black backend/ # Auto-format code")
|
| 144 |
+
print(" 3. Run: flake8 backend/ # Check PEP 8 compliance")
|
| 145 |
+
print(" 4. Add .flake8 config to exclude tests/ from strict checks")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
if __name__ == '__main__':
|
| 149 |
+
main()
|
|
@@ -1,92 +1,92 @@
|
|
| 1 |
-
from logging.config import fileConfig
|
| 2 |
-
import os
|
| 3 |
-
import sys
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
|
| 6 |
-
from sqlalchemy import engine_from_config
|
| 7 |
-
from sqlalchemy import pool
|
| 8 |
-
from dotenv import load_dotenv
|
| 9 |
-
|
| 10 |
-
from alembic import context
|
| 11 |
-
|
| 12 |
-
# Add parent directory to path to import models
|
| 13 |
-
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 14 |
-
|
| 15 |
-
# Load environment variables
|
| 16 |
-
load_dotenv()
|
| 17 |
-
|
| 18 |
-
# this is the Alembic Config object, which provides
|
| 19 |
-
# access to the values within the .ini file in use.
|
| 20 |
-
config = context.config
|
| 21 |
-
|
| 22 |
-
# Set sqlalchemy.url from environment variable
|
| 23 |
-
config.set_main_option('sqlalchemy.url', os.getenv('NEON_POSTGRES_URL', ''))
|
| 24 |
-
|
| 25 |
-
# Interpret the config file for Python logging.
|
| 26 |
-
# This line sets up loggers basically.
|
| 27 |
-
if config.config_file_name is not None:
|
| 28 |
-
fileConfig(config.config_file_name)
|
| 29 |
-
|
| 30 |
-
# Import Base and models for autogenerate support
|
| 31 |
-
from database.db import Base
|
| 32 |
-
from database.models import User, Personalization, Translation # Import all existing models
|
| 33 |
-
from models.translation_feedback import TranslationFeedback # Import TranslationFeedback model
|
| 34 |
-
|
| 35 |
-
target_metadata = Base.metadata
|
| 36 |
-
|
| 37 |
-
# other values from the config, defined by the needs of env.py,
|
| 38 |
-
# can be acquired:
|
| 39 |
-
# my_important_option = config.get_main_option("my_important_option")
|
| 40 |
-
# ... etc.
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def run_migrations_offline() -> None:
|
| 44 |
-
"""Run migrations in 'offline' mode.
|
| 45 |
-
|
| 46 |
-
This configures the context with just a URL
|
| 47 |
-
and not an Engine, though an Engine is acceptable
|
| 48 |
-
here as well. By skipping the Engine creation
|
| 49 |
-
we don't even need a DBAPI to be available.
|
| 50 |
-
|
| 51 |
-
Calls to context.execute() here emit the given string to the
|
| 52 |
-
script output.
|
| 53 |
-
|
| 54 |
-
"""
|
| 55 |
-
url = config.get_main_option("sqlalchemy.url")
|
| 56 |
-
context.configure(
|
| 57 |
-
url=url,
|
| 58 |
-
target_metadata=target_metadata,
|
| 59 |
-
literal_binds=True,
|
| 60 |
-
dialect_opts={"paramstyle": "named"},
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
with context.begin_transaction():
|
| 64 |
-
context.run_migrations()
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def run_migrations_online() -> None:
|
| 68 |
-
"""Run migrations in 'online' mode.
|
| 69 |
-
|
| 70 |
-
In this scenario we need to create an Engine
|
| 71 |
-
and associate a connection with the context.
|
| 72 |
-
|
| 73 |
-
"""
|
| 74 |
-
connectable = engine_from_config(
|
| 75 |
-
config.get_section(config.config_ini_section, {}),
|
| 76 |
-
prefix="sqlalchemy.",
|
| 77 |
-
poolclass=pool.NullPool,
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
with connectable.connect() as connection:
|
| 81 |
-
context.configure(
|
| 82 |
-
connection=connection, target_metadata=target_metadata
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
with context.begin_transaction():
|
| 86 |
-
context.run_migrations()
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
if context.is_offline_mode():
|
| 90 |
-
run_migrations_offline()
|
| 91 |
-
else:
|
| 92 |
-
run_migrations_online()
|
|
|
|
| 1 |
+
from logging.config import fileConfig
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from sqlalchemy import engine_from_config
|
| 7 |
+
from sqlalchemy import pool
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
from alembic import context
|
| 11 |
+
|
| 12 |
+
# Add parent directory to path to import models
|
| 13 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 14 |
+
|
| 15 |
+
# Load environment variables
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
# this is the Alembic Config object, which provides
|
| 19 |
+
# access to the values within the .ini file in use.
|
| 20 |
+
config = context.config
|
| 21 |
+
|
| 22 |
+
# Set sqlalchemy.url from environment variable
|
| 23 |
+
config.set_main_option('sqlalchemy.url', os.getenv('NEON_POSTGRES_URL', ''))
|
| 24 |
+
|
| 25 |
+
# Interpret the config file for Python logging.
|
| 26 |
+
# This line sets up loggers basically.
|
| 27 |
+
if config.config_file_name is not None:
|
| 28 |
+
fileConfig(config.config_file_name)
|
| 29 |
+
|
| 30 |
+
# Import Base and models for autogenerate support
|
| 31 |
+
from database.db import Base
|
| 32 |
+
from database.models import User, Personalization, Translation # Import all existing models
|
| 33 |
+
from models.translation_feedback import TranslationFeedback # Import TranslationFeedback model
|
| 34 |
+
|
| 35 |
+
target_metadata = Base.metadata
|
| 36 |
+
|
| 37 |
+
# other values from the config, defined by the needs of env.py,
|
| 38 |
+
# can be acquired:
|
| 39 |
+
# my_important_option = config.get_main_option("my_important_option")
|
| 40 |
+
# ... etc.
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def run_migrations_offline() -> None:
|
| 44 |
+
"""Run migrations in 'offline' mode.
|
| 45 |
+
|
| 46 |
+
This configures the context with just a URL
|
| 47 |
+
and not an Engine, though an Engine is acceptable
|
| 48 |
+
here as well. By skipping the Engine creation
|
| 49 |
+
we don't even need a DBAPI to be available.
|
| 50 |
+
|
| 51 |
+
Calls to context.execute() here emit the given string to the
|
| 52 |
+
script output.
|
| 53 |
+
|
| 54 |
+
"""
|
| 55 |
+
url = config.get_main_option("sqlalchemy.url")
|
| 56 |
+
context.configure(
|
| 57 |
+
url=url,
|
| 58 |
+
target_metadata=target_metadata,
|
| 59 |
+
literal_binds=True,
|
| 60 |
+
dialect_opts={"paramstyle": "named"},
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
with context.begin_transaction():
|
| 64 |
+
context.run_migrations()
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def run_migrations_online() -> None:
|
| 68 |
+
"""Run migrations in 'online' mode.
|
| 69 |
+
|
| 70 |
+
In this scenario we need to create an Engine
|
| 71 |
+
and associate a connection with the context.
|
| 72 |
+
|
| 73 |
+
"""
|
| 74 |
+
connectable = engine_from_config(
|
| 75 |
+
config.get_section(config.config_ini_section, {}),
|
| 76 |
+
prefix="sqlalchemy.",
|
| 77 |
+
poolclass=pool.NullPool,
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
with connectable.connect() as connection:
|
| 81 |
+
context.configure(
|
| 82 |
+
connection=connection, target_metadata=target_metadata
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
with context.begin_transaction():
|
| 86 |
+
context.run_migrations()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if context.is_offline_mode():
|
| 90 |
+
run_migrations_offline()
|
| 91 |
+
else:
|
| 92 |
+
run_migrations_online()
|
|
@@ -1,26 +1,26 @@
|
|
| 1 |
-
"""${message}
|
| 2 |
-
|
| 3 |
-
Revision ID: ${up_revision}
|
| 4 |
-
Revises: ${down_revision | comma,n}
|
| 5 |
-
Create Date: ${create_date}
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
${imports if imports else ""}
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = ${repr(up_revision)}
|
| 16 |
-
down_revision: Union[str, None] = ${repr(down_revision)}
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
${upgrades if upgrades else "pass"}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def downgrade() -> None:
|
| 26 |
-
${downgrades if downgrades else "pass"}
|
|
|
|
| 1 |
+
"""${message}
|
| 2 |
+
|
| 3 |
+
Revision ID: ${up_revision}
|
| 4 |
+
Revises: ${down_revision | comma,n}
|
| 5 |
+
Create Date: ${create_date}
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
${imports if imports else ""}
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = ${repr(up_revision)}
|
| 16 |
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
${upgrades if upgrades else "pass"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def downgrade() -> None:
|
| 26 |
+
${downgrades if downgrades else "pass"}
|
|
@@ -1,54 +1,54 @@
|
|
| 1 |
-
"""add translations table
|
| 2 |
-
|
| 3 |
-
Revision ID: 57c6b0ea13f8
|
| 4 |
-
Revises:
|
| 5 |
-
Create Date: 2025-12-24 22:54:29.988202
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '57c6b0ea13f8'
|
| 16 |
-
down_revision: Union[str, None] = None
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 23 |
-
op.create_table('translations',
|
| 24 |
-
sa.Column('id', sa.UUID(), nullable=False),
|
| 25 |
-
sa.Column('chapter_id', sa.String(length=255), nullable=False),
|
| 26 |
-
sa.Column('content_hash', sa.String(length=64), nullable=False),
|
| 27 |
-
sa.Column('source_language', sa.String(length=10), nullable=False),
|
| 28 |
-
sa.Column('target_language', sa.String(length=10), nullable=False),
|
| 29 |
-
sa.Column('original_content', sa.Text(), nullable=False),
|
| 30 |
-
sa.Column('translated_content', sa.Text(), nullable=False),
|
| 31 |
-
sa.Column('user_id', sa.UUID(), nullable=False),
|
| 32 |
-
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
| 33 |
-
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
| 34 |
-
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
| 35 |
-
sa.PrimaryKeyConstraint('id'),
|
| 36 |
-
sa.UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language')
|
| 37 |
-
)
|
| 38 |
-
op.create_index('idx_translations_lookup', 'translations', ['chapter_id', 'content_hash', 'target_language'], unique=False)
|
| 39 |
-
op.create_index(op.f('ix_translations_chapter_id'), 'translations', ['chapter_id'], unique=False)
|
| 40 |
-
op.create_index(op.f('ix_translations_content_hash'), 'translations', ['content_hash'], unique=False)
|
| 41 |
-
op.create_index(op.f('ix_translations_target_language'), 'translations', ['target_language'], unique=False)
|
| 42 |
-
op.create_index(op.f('ix_translations_user_id'), 'translations', ['user_id'], unique=False)
|
| 43 |
-
# ### end Alembic commands ###
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def downgrade() -> None:
|
| 47 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 48 |
-
op.drop_index(op.f('ix_translations_user_id'), table_name='translations')
|
| 49 |
-
op.drop_index(op.f('ix_translations_target_language'), table_name='translations')
|
| 50 |
-
op.drop_index(op.f('ix_translations_content_hash'), table_name='translations')
|
| 51 |
-
op.drop_index(op.f('ix_translations_chapter_id'), table_name='translations')
|
| 52 |
-
op.drop_index('idx_translations_lookup', table_name='translations')
|
| 53 |
-
op.drop_table('translations')
|
| 54 |
-
# ### end Alembic commands ###
|
|
|
|
| 1 |
+
"""add translations table
|
| 2 |
+
|
| 3 |
+
Revision ID: 57c6b0ea13f8
|
| 4 |
+
Revises:
|
| 5 |
+
Create Date: 2025-12-24 22:54:29.988202
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = '57c6b0ea13f8'
|
| 16 |
+
down_revision: Union[str, None] = None
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 23 |
+
op.create_table('translations',
|
| 24 |
+
sa.Column('id', sa.UUID(), nullable=False),
|
| 25 |
+
sa.Column('chapter_id', sa.String(length=255), nullable=False),
|
| 26 |
+
sa.Column('content_hash', sa.String(length=64), nullable=False),
|
| 27 |
+
sa.Column('source_language', sa.String(length=10), nullable=False),
|
| 28 |
+
sa.Column('target_language', sa.String(length=10), nullable=False),
|
| 29 |
+
sa.Column('original_content', sa.Text(), nullable=False),
|
| 30 |
+
sa.Column('translated_content', sa.Text(), nullable=False),
|
| 31 |
+
sa.Column('user_id', sa.UUID(), nullable=False),
|
| 32 |
+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
| 33 |
+
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
| 34 |
+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
| 35 |
+
sa.PrimaryKeyConstraint('id'),
|
| 36 |
+
sa.UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language')
|
| 37 |
+
)
|
| 38 |
+
op.create_index('idx_translations_lookup', 'translations', ['chapter_id', 'content_hash', 'target_language'], unique=False)
|
| 39 |
+
op.create_index(op.f('ix_translations_chapter_id'), 'translations', ['chapter_id'], unique=False)
|
| 40 |
+
op.create_index(op.f('ix_translations_content_hash'), 'translations', ['content_hash'], unique=False)
|
| 41 |
+
op.create_index(op.f('ix_translations_target_language'), 'translations', ['target_language'], unique=False)
|
| 42 |
+
op.create_index(op.f('ix_translations_user_id'), 'translations', ['user_id'], unique=False)
|
| 43 |
+
# ### end Alembic commands ###
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def downgrade() -> None:
|
| 47 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 48 |
+
op.drop_index(op.f('ix_translations_user_id'), table_name='translations')
|
| 49 |
+
op.drop_index(op.f('ix_translations_target_language'), table_name='translations')
|
| 50 |
+
op.drop_index(op.f('ix_translations_content_hash'), table_name='translations')
|
| 51 |
+
op.drop_index(op.f('ix_translations_chapter_id'), table_name='translations')
|
| 52 |
+
op.drop_index('idx_translations_lookup', table_name='translations')
|
| 53 |
+
op.drop_table('translations')
|
| 54 |
+
# ### end Alembic commands ###
|
|
@@ -1,43 +1,43 @@
|
|
| 1 |
-
"""add translation_feedback table
|
| 2 |
-
|
| 3 |
-
Revision ID: 809b34b1c5dc
|
| 4 |
-
Revises: 57c6b0ea13f8
|
| 5 |
-
Create Date: 2025-12-25 01:42:15.691192
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '809b34b1c5dc'
|
| 16 |
-
down_revision: Union[str, None] = '57c6b0ea13f8'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 23 |
-
op.create_table('translation_feedback',
|
| 24 |
-
sa.Column('id', sa.UUID(), nullable=False),
|
| 25 |
-
sa.Column('translation_id', sa.UUID(), nullable=False),
|
| 26 |
-
sa.Column('user_id', sa.UUID(), nullable=False),
|
| 27 |
-
sa.Column('issue_description', sa.Text(), nullable=False),
|
| 28 |
-
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
| 29 |
-
sa.ForeignKeyConstraint(['translation_id'], ['translations.id'], ondelete='CASCADE'),
|
| 30 |
-
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
| 31 |
-
sa.PrimaryKeyConstraint('id')
|
| 32 |
-
)
|
| 33 |
-
op.create_index(op.f('ix_translation_feedback_translation_id'), 'translation_feedback', ['translation_id'], unique=False)
|
| 34 |
-
op.create_index(op.f('ix_translation_feedback_user_id'), 'translation_feedback', ['user_id'], unique=False)
|
| 35 |
-
# ### end Alembic commands ###
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def downgrade() -> None:
|
| 39 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 40 |
-
op.drop_index(op.f('ix_translation_feedback_user_id'), table_name='translation_feedback')
|
| 41 |
-
op.drop_index(op.f('ix_translation_feedback_translation_id'), table_name='translation_feedback')
|
| 42 |
-
op.drop_table('translation_feedback')
|
| 43 |
-
# ### end Alembic commands ###
|
|
|
|
| 1 |
+
"""add translation_feedback table
|
| 2 |
+
|
| 3 |
+
Revision ID: 809b34b1c5dc
|
| 4 |
+
Revises: 57c6b0ea13f8
|
| 5 |
+
Create Date: 2025-12-25 01:42:15.691192
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = '809b34b1c5dc'
|
| 16 |
+
down_revision: Union[str, None] = '57c6b0ea13f8'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 23 |
+
op.create_table('translation_feedback',
|
| 24 |
+
sa.Column('id', sa.UUID(), nullable=False),
|
| 25 |
+
sa.Column('translation_id', sa.UUID(), nullable=False),
|
| 26 |
+
sa.Column('user_id', sa.UUID(), nullable=False),
|
| 27 |
+
sa.Column('issue_description', sa.Text(), nullable=False),
|
| 28 |
+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
| 29 |
+
sa.ForeignKeyConstraint(['translation_id'], ['translations.id'], ondelete='CASCADE'),
|
| 30 |
+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
| 31 |
+
sa.PrimaryKeyConstraint('id')
|
| 32 |
+
)
|
| 33 |
+
op.create_index(op.f('ix_translation_feedback_translation_id'), 'translation_feedback', ['translation_id'], unique=False)
|
| 34 |
+
op.create_index(op.f('ix_translation_feedback_user_id'), 'translation_feedback', ['user_id'], unique=False)
|
| 35 |
+
# ### end Alembic commands ###
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def downgrade() -> None:
|
| 39 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 40 |
+
op.drop_index(op.f('ix_translation_feedback_user_id'), table_name='translation_feedback')
|
| 41 |
+
op.drop_index(op.f('ix_translation_feedback_translation_id'), table_name='translation_feedback')
|
| 42 |
+
op.drop_table('translation_feedback')
|
| 43 |
+
# ### end Alembic commands ###
|
|
@@ -1,34 +1,34 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Translation model for storing Urdu translations with caching
|
| 3 |
-
"""
|
| 4 |
-
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, UniqueConstraint, Index
|
| 5 |
-
from sqlalchemy.dialects.postgresql import UUID
|
| 6 |
-
from sqlalchemy.sql import func
|
| 7 |
-
import uuid
|
| 8 |
-
from database.db import Base
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class Translation(Base):
|
| 12 |
-
__tablename__ = "translations"
|
| 13 |
-
|
| 14 |
-
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 15 |
-
chapter_id = Column(String(255), nullable=False, index=True)
|
| 16 |
-
content_hash = Column(String(64), nullable=False, index=True)
|
| 17 |
-
source_language = Column(String(10), nullable=False, default="english")
|
| 18 |
-
target_language = Column(String(10), nullable=False, index=True)
|
| 19 |
-
original_content = Column(Text, nullable=False)
|
| 20 |
-
translated_content = Column(Text, nullable=False)
|
| 21 |
-
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
| 22 |
-
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 23 |
-
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
| 24 |
-
|
| 25 |
-
# Composite unique constraint: one translation per (chapter_id, content_hash, target_language)
|
| 26 |
-
# This ensures we don't duplicate translations when content is the same
|
| 27 |
-
__table_args__ = (
|
| 28 |
-
UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language'),
|
| 29 |
-
# Composite index for fast cache lookups
|
| 30 |
-
Index('idx_translations_lookup', 'chapter_id', 'content_hash', 'target_language'),
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
def __repr__(self):
|
| 34 |
-
return f"<Translation(chapter_id={self.chapter_id}, target_language={self.target_language}, cached=True)>"
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Translation model for storing Urdu translations with caching
|
| 3 |
+
"""
|
| 4 |
+
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, UniqueConstraint, Index
|
| 5 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 6 |
+
from sqlalchemy.sql import func
|
| 7 |
+
import uuid
|
| 8 |
+
from database.db import Base
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Translation(Base):
|
| 12 |
+
__tablename__ = "translations"
|
| 13 |
+
|
| 14 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 15 |
+
chapter_id = Column(String(255), nullable=False, index=True)
|
| 16 |
+
content_hash = Column(String(64), nullable=False, index=True)
|
| 17 |
+
source_language = Column(String(10), nullable=False, default="english")
|
| 18 |
+
target_language = Column(String(10), nullable=False, index=True)
|
| 19 |
+
original_content = Column(Text, nullable=False)
|
| 20 |
+
translated_content = Column(Text, nullable=False)
|
| 21 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
| 22 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 23 |
+
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
| 24 |
+
|
| 25 |
+
# Composite unique constraint: one translation per (chapter_id, content_hash, target_language)
|
| 26 |
+
# This ensures we don't duplicate translations when content is the same
|
| 27 |
+
__table_args__ = (
|
| 28 |
+
UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language'),
|
| 29 |
+
# Composite index for fast cache lookups
|
| 30 |
+
Index('idx_translations_lookup', 'chapter_id', 'content_hash', 'target_language'),
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def __repr__(self):
|
| 34 |
+
return f"<Translation(chapter_id={self.chapter_id}, target_language={self.target_language}, cached=True)>"
|
|
@@ -1,34 +1,34 @@
|
|
| 1 |
-
"""
|
| 2 |
-
TranslationFeedback Model
|
| 3 |
-
|
| 4 |
-
Purpose: Store user feedback on translation quality for improvement
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from sqlalchemy import Column, String, Text, DateTime, ForeignKey
|
| 8 |
-
from sqlalchemy.dialects.postgresql import UUID
|
| 9 |
-
from sqlalchemy.sql import func
|
| 10 |
-
import uuid
|
| 11 |
-
from database.db import Base
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class TranslationFeedback(Base):
|
| 15 |
-
__tablename__ = "translation_feedback"
|
| 16 |
-
|
| 17 |
-
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 18 |
-
translation_id = Column(
|
| 19 |
-
UUID(as_uuid=True),
|
| 20 |
-
ForeignKey("translations.id", ondelete="CASCADE"),
|
| 21 |
-
nullable=False,
|
| 22 |
-
index=True
|
| 23 |
-
)
|
| 24 |
-
user_id = Column(
|
| 25 |
-
UUID(as_uuid=True),
|
| 26 |
-
ForeignKey("users.id", ondelete="CASCADE"),
|
| 27 |
-
nullable=False,
|
| 28 |
-
index=True
|
| 29 |
-
)
|
| 30 |
-
issue_description = Column(Text, nullable=False)
|
| 31 |
-
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 32 |
-
|
| 33 |
-
def __repr__(self):
|
| 34 |
-
return f"<TranslationFeedback(translation_id={self.translation_id}, user_id={self.user_id}, issue='{self.issue_description[:50]}...')>"
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TranslationFeedback Model
|
| 3 |
+
|
| 4 |
+
Purpose: Store user feedback on translation quality for improvement
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from sqlalchemy import Column, String, Text, DateTime, ForeignKey
|
| 8 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 9 |
+
from sqlalchemy.sql import func
|
| 10 |
+
import uuid
|
| 11 |
+
from database.db import Base
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TranslationFeedback(Base):
|
| 15 |
+
__tablename__ = "translation_feedback"
|
| 16 |
+
|
| 17 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 18 |
+
translation_id = Column(
|
| 19 |
+
UUID(as_uuid=True),
|
| 20 |
+
ForeignKey("translations.id", ondelete="CASCADE"),
|
| 21 |
+
nullable=False,
|
| 22 |
+
index=True
|
| 23 |
+
)
|
| 24 |
+
user_id = Column(
|
| 25 |
+
UUID(as_uuid=True),
|
| 26 |
+
ForeignKey("users.id", ondelete="CASCADE"),
|
| 27 |
+
nullable=False,
|
| 28 |
+
index=True
|
| 29 |
+
)
|
| 30 |
+
issue_description = Column(Text, nullable=False)
|
| 31 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 32 |
+
|
| 33 |
+
def __repr__(self):
|
| 34 |
+
return f"<TranslationFeedback(translation_id={self.translation_id}, user_id={self.user_id}, issue='{self.issue_description[:50]}...')>"
|
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
fastapi>=0.104.1
|
| 2 |
-
uvicorn[standard]>=0.24.0
|
| 3 |
-
sqlalchemy[asyncio]>=2.0.23
|
| 4 |
-
asyncpg>=0.29.0
|
| 5 |
-
qdrant-client>=1.7.1
|
| 6 |
-
google-generativeai>=0.4.0
|
| 7 |
-
python-multipart>=0.0.6
|
| 8 |
-
python-jose[cryptography]>=3.3.0
|
| 9 |
-
passlib[bcrypt]>=1.7.4
|
| 10 |
-
better-exceptions>=0.3.3
|
| 11 |
-
python-dotenv>=1.0.0
|
| 12 |
-
pydantic>=2.5.0
|
| 13 |
-
pydantic-settings>=2.1.0
|
| 14 |
-
httpx>=0.25.2
|
| 15 |
-
pytest>=7.4.3
|
| 16 |
-
pytest-asyncio>=0.21.1
|
| 17 |
-
alembic>=1.13.1
|
| 18 |
openai>=1.10.0
|
|
|
|
| 1 |
+
fastapi>=0.104.1
|
| 2 |
+
uvicorn[standard]>=0.24.0
|
| 3 |
+
sqlalchemy[asyncio]>=2.0.23
|
| 4 |
+
asyncpg>=0.29.0
|
| 5 |
+
qdrant-client>=1.7.1
|
| 6 |
+
google-generativeai>=0.4.0
|
| 7 |
+
python-multipart>=0.0.6
|
| 8 |
+
python-jose[cryptography]>=3.3.0
|
| 9 |
+
passlib[bcrypt]>=1.7.4
|
| 10 |
+
better-exceptions>=0.3.3
|
| 11 |
+
python-dotenv>=1.0.0
|
| 12 |
+
pydantic>=2.5.0
|
| 13 |
+
pydantic-settings>=2.1.0
|
| 14 |
+
httpx>=0.25.2
|
| 15 |
+
pytest>=7.4.3
|
| 16 |
+
pytest-asyncio>=0.21.1
|
| 17 |
+
alembic>=1.13.1
|
| 18 |
openai>=1.10.0
|
|
@@ -1,115 +1,115 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Rate limiter service for translation endpoints
|
| 3 |
-
Implements in-memory sliding window algorithm (10 requests per user per hour)
|
| 4 |
-
"""
|
| 5 |
-
import time
|
| 6 |
-
from collections import defaultdict
|
| 7 |
-
from typing import Dict, List
|
| 8 |
-
import logging
|
| 9 |
-
|
| 10 |
-
logger = logging.getLogger(__name__)
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class RateLimiter:
|
| 14 |
-
"""
|
| 15 |
-
In-memory rate limiter using sliding window algorithm
|
| 16 |
-
|
| 17 |
-
Limits: 10 translations per user per hour (3600 seconds)
|
| 18 |
-
|
| 19 |
-
Note: This is a simple in-memory implementation suitable for single-instance deployments.
|
| 20 |
-
For multi-instance/production deployments, consider using Redis for distributed rate limiting.
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
def __init__(self, max_requests: int = 10, window_seconds: int = 3600):
|
| 24 |
-
"""
|
| 25 |
-
Initialize rate limiter
|
| 26 |
-
|
| 27 |
-
Args:
|
| 28 |
-
max_requests: Maximum number of requests allowed per window
|
| 29 |
-
window_seconds: Time window in seconds (default: 3600 = 1 hour)
|
| 30 |
-
"""
|
| 31 |
-
self.max_requests = max_requests
|
| 32 |
-
self.window_seconds = window_seconds
|
| 33 |
-
# Store: {user_id: [timestamp1, timestamp2, ...]}
|
| 34 |
-
self.request_history: Dict[str, List[float]] = defaultdict(list)
|
| 35 |
-
logger.info(f"RateLimiter initialized: {max_requests} requests per {window_seconds}s")
|
| 36 |
-
|
| 37 |
-
def check_rate_limit(self, user_id: str) -> tuple[bool, int]:
|
| 38 |
-
"""
|
| 39 |
-
Check if user has exceeded rate limit
|
| 40 |
-
|
| 41 |
-
Args:
|
| 42 |
-
user_id: User identifier (UUID string)
|
| 43 |
-
|
| 44 |
-
Returns:
|
| 45 |
-
tuple[bool, int]: (is_allowed, retry_after_seconds)
|
| 46 |
-
- is_allowed: True if request is allowed, False if rate limit exceeded
|
| 47 |
-
- retry_after_seconds: Seconds until rate limit resets (0 if allowed)
|
| 48 |
-
"""
|
| 49 |
-
current_time = time.time()
|
| 50 |
-
cutoff_time = current_time - self.window_seconds
|
| 51 |
-
|
| 52 |
-
# Get user's request history
|
| 53 |
-
user_requests = self.request_history[user_id]
|
| 54 |
-
|
| 55 |
-
# Remove timestamps outside the sliding window
|
| 56 |
-
user_requests = [ts for ts in user_requests if ts > cutoff_time]
|
| 57 |
-
self.request_history[user_id] = user_requests
|
| 58 |
-
|
| 59 |
-
# Check if user has exceeded limit
|
| 60 |
-
if len(user_requests) >= self.max_requests:
|
| 61 |
-
# Calculate retry_after: time until oldest request expires
|
| 62 |
-
oldest_timestamp = user_requests[0]
|
| 63 |
-
retry_after = int(oldest_timestamp + self.window_seconds - current_time)
|
| 64 |
-
|
| 65 |
-
logger.warning(
|
| 66 |
-
f"Rate limit exceeded for user {user_id}: "
|
| 67 |
-
f"{len(user_requests)}/{self.max_requests} requests in last {self.window_seconds}s. "
|
| 68 |
-
f"Retry after {retry_after}s"
|
| 69 |
-
)
|
| 70 |
-
return False, retry_after
|
| 71 |
-
|
| 72 |
-
# Record this request
|
| 73 |
-
user_requests.append(current_time)
|
| 74 |
-
self.request_history[user_id] = user_requests
|
| 75 |
-
|
| 76 |
-
logger.debug(
|
| 77 |
-
f"Rate limit check passed for user {user_id}: "
|
| 78 |
-
f"{len(user_requests)}/{self.max_requests} requests in window"
|
| 79 |
-
)
|
| 80 |
-
return True, 0
|
| 81 |
-
|
| 82 |
-
def reset_user(self, user_id: str) -> None:
|
| 83 |
-
"""
|
| 84 |
-
Reset rate limit for a specific user (admin function)
|
| 85 |
-
|
| 86 |
-
Args:
|
| 87 |
-
user_id: User identifier to reset
|
| 88 |
-
"""
|
| 89 |
-
if user_id in self.request_history:
|
| 90 |
-
del self.request_history[user_id]
|
| 91 |
-
logger.info(f"Rate limit reset for user {user_id}")
|
| 92 |
-
|
| 93 |
-
def get_remaining_requests(self, user_id: str) -> int:
|
| 94 |
-
"""
|
| 95 |
-
Get number of remaining requests for user in current window
|
| 96 |
-
|
| 97 |
-
Args:
|
| 98 |
-
user_id: User identifier
|
| 99 |
-
|
| 100 |
-
Returns:
|
| 101 |
-
int: Number of requests remaining
|
| 102 |
-
"""
|
| 103 |
-
current_time = time.time()
|
| 104 |
-
cutoff_time = current_time - self.window_seconds
|
| 105 |
-
|
| 106 |
-
user_requests = self.request_history.get(user_id, [])
|
| 107 |
-
user_requests = [ts for ts in user_requests if ts > cutoff_time]
|
| 108 |
-
|
| 109 |
-
remaining = max(0, self.max_requests - len(user_requests))
|
| 110 |
-
return remaining
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
# Global rate limiter instance (singleton)
|
| 114 |
-
# 10 translations per user per hour
|
| 115 |
-
translation_rate_limiter = RateLimiter(max_requests=10, window_seconds=3600)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rate limiter service for translation endpoints
|
| 3 |
+
Implements in-memory sliding window algorithm (10 requests per user per hour)
|
| 4 |
+
"""
|
| 5 |
+
import time
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from typing import Dict, List
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class RateLimiter:
|
| 14 |
+
"""
|
| 15 |
+
In-memory rate limiter using sliding window algorithm
|
| 16 |
+
|
| 17 |
+
Limits: 10 translations per user per hour (3600 seconds)
|
| 18 |
+
|
| 19 |
+
Note: This is a simple in-memory implementation suitable for single-instance deployments.
|
| 20 |
+
For multi-instance/production deployments, consider using Redis for distributed rate limiting.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, max_requests: int = 10, window_seconds: int = 3600):
|
| 24 |
+
"""
|
| 25 |
+
Initialize rate limiter
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
max_requests: Maximum number of requests allowed per window
|
| 29 |
+
window_seconds: Time window in seconds (default: 3600 = 1 hour)
|
| 30 |
+
"""
|
| 31 |
+
self.max_requests = max_requests
|
| 32 |
+
self.window_seconds = window_seconds
|
| 33 |
+
# Store: {user_id: [timestamp1, timestamp2, ...]}
|
| 34 |
+
self.request_history: Dict[str, List[float]] = defaultdict(list)
|
| 35 |
+
logger.info(f"RateLimiter initialized: {max_requests} requests per {window_seconds}s")
|
| 36 |
+
|
| 37 |
+
def check_rate_limit(self, user_id: str) -> tuple[bool, int]:
|
| 38 |
+
"""
|
| 39 |
+
Check if user has exceeded rate limit
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
user_id: User identifier (UUID string)
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
tuple[bool, int]: (is_allowed, retry_after_seconds)
|
| 46 |
+
- is_allowed: True if request is allowed, False if rate limit exceeded
|
| 47 |
+
- retry_after_seconds: Seconds until rate limit resets (0 if allowed)
|
| 48 |
+
"""
|
| 49 |
+
current_time = time.time()
|
| 50 |
+
cutoff_time = current_time - self.window_seconds
|
| 51 |
+
|
| 52 |
+
# Get user's request history
|
| 53 |
+
user_requests = self.request_history[user_id]
|
| 54 |
+
|
| 55 |
+
# Remove timestamps outside the sliding window
|
| 56 |
+
user_requests = [ts for ts in user_requests if ts > cutoff_time]
|
| 57 |
+
self.request_history[user_id] = user_requests
|
| 58 |
+
|
| 59 |
+
# Check if user has exceeded limit
|
| 60 |
+
if len(user_requests) >= self.max_requests:
|
| 61 |
+
# Calculate retry_after: time until oldest request expires
|
| 62 |
+
oldest_timestamp = user_requests[0]
|
| 63 |
+
retry_after = int(oldest_timestamp + self.window_seconds - current_time)
|
| 64 |
+
|
| 65 |
+
logger.warning(
|
| 66 |
+
f"Rate limit exceeded for user {user_id}: "
|
| 67 |
+
f"{len(user_requests)}/{self.max_requests} requests in last {self.window_seconds}s. "
|
| 68 |
+
f"Retry after {retry_after}s"
|
| 69 |
+
)
|
| 70 |
+
return False, retry_after
|
| 71 |
+
|
| 72 |
+
# Record this request
|
| 73 |
+
user_requests.append(current_time)
|
| 74 |
+
self.request_history[user_id] = user_requests
|
| 75 |
+
|
| 76 |
+
logger.debug(
|
| 77 |
+
f"Rate limit check passed for user {user_id}: "
|
| 78 |
+
f"{len(user_requests)}/{self.max_requests} requests in window"
|
| 79 |
+
)
|
| 80 |
+
return True, 0
|
| 81 |
+
|
| 82 |
+
def reset_user(self, user_id: str) -> None:
|
| 83 |
+
"""
|
| 84 |
+
Reset rate limit for a specific user (admin function)
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
user_id: User identifier to reset
|
| 88 |
+
"""
|
| 89 |
+
if user_id in self.request_history:
|
| 90 |
+
del self.request_history[user_id]
|
| 91 |
+
logger.info(f"Rate limit reset for user {user_id}")
|
| 92 |
+
|
| 93 |
+
def get_remaining_requests(self, user_id: str) -> int:
|
| 94 |
+
"""
|
| 95 |
+
Get number of remaining requests for user in current window
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
user_id: User identifier
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
int: Number of requests remaining
|
| 102 |
+
"""
|
| 103 |
+
current_time = time.time()
|
| 104 |
+
cutoff_time = current_time - self.window_seconds
|
| 105 |
+
|
| 106 |
+
user_requests = self.request_history.get(user_id, [])
|
| 107 |
+
user_requests = [ts for ts in user_requests if ts > cutoff_time]
|
| 108 |
+
|
| 109 |
+
remaining = max(0, self.max_requests - len(user_requests))
|
| 110 |
+
return remaining
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# Global rate limiter instance (singleton)
|
| 114 |
+
# 10 translations per user per hour
|
| 115 |
+
translation_rate_limiter = RateLimiter(max_requests=10, window_seconds=3600)
|
|
@@ -1,146 +1,146 @@
|
|
| 1 |
-
from openai import OpenAI
|
| 2 |
-
import logging
|
| 3 |
-
from typing import Dict
|
| 4 |
-
import time
|
| 5 |
-
import os
|
| 6 |
-
|
| 7 |
-
logger = logging.getLogger(__name__)
|
| 8 |
-
|
| 9 |
-
class TranslationService:
|
| 10 |
-
def __init__(self, openrouter_api_key: str = None):
|
| 11 |
-
"""Initialize OpenRouter client for translation using OpenAI SDK compatibility"""
|
| 12 |
-
api_key = openrouter_api_key or os.getenv("OPENROUTER_API_KEY")
|
| 13 |
-
self.client = OpenAI(
|
| 14 |
-
base_url="https://openrouter.ai/api/v1",
|
| 15 |
-
api_key=api_key
|
| 16 |
-
)
|
| 17 |
-
self.model = "google/gemini-2.0-flash-exp:free"
|
| 18 |
-
self.translation_cache: Dict[str, str] = {}
|
| 19 |
-
self.cache_timestamps: Dict[str, float] = {}
|
| 20 |
-
|
| 21 |
-
def translate_to_urdu(self, text: str, ttl: int = 3600) -> str:
|
| 22 |
-
"""Translate English text to Urdu with caching"""
|
| 23 |
-
# Create cache key
|
| 24 |
-
cache_key = f"en_to_ur_{hash(text)}"
|
| 25 |
-
|
| 26 |
-
# Check if translation is in cache and not expired
|
| 27 |
-
if cache_key in self.translation_cache:
|
| 28 |
-
if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
|
| 29 |
-
logger.info("Returning cached translation")
|
| 30 |
-
return self.translation_cache[cache_key]
|
| 31 |
-
|
| 32 |
-
# Call OpenRouter API for translation using OpenAI SDK
|
| 33 |
-
try:
|
| 34 |
-
system_prompt = self._get_urdu_translation_system_prompt()
|
| 35 |
-
|
| 36 |
-
response = self.client.chat.completions.create(
|
| 37 |
-
model=self.model,
|
| 38 |
-
messages=[
|
| 39 |
-
{"role": "system", "content": system_prompt},
|
| 40 |
-
{"role": "user", "content": text}
|
| 41 |
-
],
|
| 42 |
-
temperature=0.3,
|
| 43 |
-
max_tokens=8000
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
translated_text = response.choices[0].message.content.strip()
|
| 47 |
-
|
| 48 |
-
# Cache the translation
|
| 49 |
-
self.translation_cache[cache_key] = translated_text
|
| 50 |
-
self.cache_timestamps[cache_key] = time.time()
|
| 51 |
-
|
| 52 |
-
logger.info(f"Translated text to Urdu (length: {len(translated_text)} chars)")
|
| 53 |
-
return translated_text
|
| 54 |
-
|
| 55 |
-
except Exception as e:
|
| 56 |
-
logger.error(f"Translation failed: {str(e)}")
|
| 57 |
-
# Return a professional fallback response
|
| 58 |
-
return f"Translation unavailable: {text[:100]}..."
|
| 59 |
-
|
| 60 |
-
def _get_urdu_translation_system_prompt(self) -> str:
|
| 61 |
-
"""Get the system prompt for Urdu translation from spec contract"""
|
| 62 |
-
return """You are a technical translator specializing in AI, robotics, and computer science education.
|
| 63 |
-
Translate the following English text to Urdu following these rules strictly:
|
| 64 |
-
|
| 65 |
-
TECHNICAL TERMS:
|
| 66 |
-
- Keep in English: ROS2, Python, API, HTTP, JSON, ML, AI, function, class, variable, loop, array
|
| 67 |
-
- Translate common words: robot → روبوٹ, computer → کمپیوٹر, network → نیٹ ورک
|
| 68 |
-
- Transliterate ambiguous terms: Sensor → سینسر (Sensor), Actuator → ایکچویٹر (Actuator)
|
| 69 |
-
- NEVER translate code identifiers (function names, variables, etc.)
|
| 70 |
-
|
| 71 |
-
FORMATTING:
|
| 72 |
-
- Preserve ALL markdown syntax (headings #, bold **, italic _, lists -, links [](url))
|
| 73 |
-
- Keep code blocks entirely in English (including comments): ```language ... ```
|
| 74 |
-
- Keep inline code in English: `variable_name`
|
| 75 |
-
- Keep LaTeX math unchanged: $equation$
|
| 76 |
-
- Translate link text but keep URLs: [ترجمہ شدہ متن](https://example.com)
|
| 77 |
-
- Translate image alt text but keep src: 
|
| 78 |
-
|
| 79 |
-
TONE:
|
| 80 |
-
- Use formal educational tone (not conversational)
|
| 81 |
-
- Follow standard Urdu grammar rules
|
| 82 |
-
- Use proper Urdu punctuation (،؟ instead of ,?)
|
| 83 |
-
- Do not mix English and Urdu in same sentence except for technical terms listed above
|
| 84 |
-
|
| 85 |
-
Translate now:"""
|
| 86 |
-
|
| 87 |
-
def translate_to_english(self, urdu_text: str, ttl: int = 3600) -> str:
|
| 88 |
-
"""Translate Urdu text back to English with caching"""
|
| 89 |
-
# Create cache key
|
| 90 |
-
cache_key = f"ur_to_en_{hash(urdu_text)}"
|
| 91 |
-
|
| 92 |
-
# Check if translation is in cache and not expired
|
| 93 |
-
if cache_key in self.translation_cache:
|
| 94 |
-
if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
|
| 95 |
-
logger.info("Returning cached translation")
|
| 96 |
-
return self.translation_cache[cache_key]
|
| 97 |
-
|
| 98 |
-
# Call OpenRouter API for translation using OpenAI SDK
|
| 99 |
-
try:
|
| 100 |
-
system_prompt = self._get_english_translation_system_prompt()
|
| 101 |
-
|
| 102 |
-
response = self.client.chat.completions.create(
|
| 103 |
-
model=self.model,
|
| 104 |
-
messages=[
|
| 105 |
-
{"role": "system", "content": system_prompt},
|
| 106 |
-
{"role": "user", "content": urdu_text}
|
| 107 |
-
],
|
| 108 |
-
temperature=0.3,
|
| 109 |
-
max_tokens=8000
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
translated_text = response.choices[0].message.content.strip()
|
| 113 |
-
|
| 114 |
-
# Cache the translation
|
| 115 |
-
self.translation_cache[cache_key] = translated_text
|
| 116 |
-
self.cache_timestamps[cache_key] = time.time()
|
| 117 |
-
|
| 118 |
-
logger.info(f"Translated text to English (length: {len(translated_text)} chars)")
|
| 119 |
-
return translated_text
|
| 120 |
-
|
| 121 |
-
except Exception as e:
|
| 122 |
-
logger.error(f"Translation failed: {str(e)}")
|
| 123 |
-
# Return a professional fallback response
|
| 124 |
-
return f"Translation unavailable: {urdu_text[:100]}..."
|
| 125 |
-
|
| 126 |
-
def _get_english_translation_system_prompt(self) -> str:
|
| 127 |
-
"""Get the system prompt for English translation"""
|
| 128 |
-
return """You are a technical translator specializing in AI, robotics, and computer science education.
|
| 129 |
-
Translate the following Urdu text to English with precision and accuracy.
|
| 130 |
-
|
| 131 |
-
TRANSLATION REQUIREMENTS:
|
| 132 |
-
- Maintain technical accuracy for robotics/AI terminology
|
| 133 |
-
- Preserve the original meaning and context
|
| 134 |
-
- Apply appropriate formality level for educational content
|
| 135 |
-
- Ensure readability and flow in English
|
| 136 |
-
- Keep technical terms (ROS2, Python, API, etc.) in English
|
| 137 |
-
- Preserve markdown formatting and code blocks
|
| 138 |
-
- Do not add any commentary or explanations
|
| 139 |
-
|
| 140 |
-
Translate now:"""
|
| 141 |
-
|
| 142 |
-
def clear_cache(self):
|
| 143 |
-
"""Clear the translation cache"""
|
| 144 |
-
self.translation_cache.clear()
|
| 145 |
-
self.cache_timestamps.clear()
|
| 146 |
logger.info("Translation cache cleared")
|
|
|
|
| 1 |
+
from openai import OpenAI
|
| 2 |
+
import logging
|
| 3 |
+
from typing import Dict
|
| 4 |
+
import time
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
class TranslationService:
|
| 10 |
+
def __init__(self, openrouter_api_key: str = None):
|
| 11 |
+
"""Initialize OpenRouter client for translation using OpenAI SDK compatibility"""
|
| 12 |
+
api_key = openrouter_api_key or os.getenv("OPENROUTER_API_KEY")
|
| 13 |
+
self.client = OpenAI(
|
| 14 |
+
base_url="https://openrouter.ai/api/v1",
|
| 15 |
+
api_key=api_key
|
| 16 |
+
)
|
| 17 |
+
self.model = "google/gemini-2.0-flash-exp:free"
|
| 18 |
+
self.translation_cache: Dict[str, str] = {}
|
| 19 |
+
self.cache_timestamps: Dict[str, float] = {}
|
| 20 |
+
|
| 21 |
+
def translate_to_urdu(self, text: str, ttl: int = 3600) -> str:
|
| 22 |
+
"""Translate English text to Urdu with caching"""
|
| 23 |
+
# Create cache key
|
| 24 |
+
cache_key = f"en_to_ur_{hash(text)}"
|
| 25 |
+
|
| 26 |
+
# Check if translation is in cache and not expired
|
| 27 |
+
if cache_key in self.translation_cache:
|
| 28 |
+
if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
|
| 29 |
+
logger.info("Returning cached translation")
|
| 30 |
+
return self.translation_cache[cache_key]
|
| 31 |
+
|
| 32 |
+
# Call OpenRouter API for translation using OpenAI SDK
|
| 33 |
+
try:
|
| 34 |
+
system_prompt = self._get_urdu_translation_system_prompt()
|
| 35 |
+
|
| 36 |
+
response = self.client.chat.completions.create(
|
| 37 |
+
model=self.model,
|
| 38 |
+
messages=[
|
| 39 |
+
{"role": "system", "content": system_prompt},
|
| 40 |
+
{"role": "user", "content": text}
|
| 41 |
+
],
|
| 42 |
+
temperature=0.3,
|
| 43 |
+
max_tokens=8000
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
translated_text = response.choices[0].message.content.strip()
|
| 47 |
+
|
| 48 |
+
# Cache the translation
|
| 49 |
+
self.translation_cache[cache_key] = translated_text
|
| 50 |
+
self.cache_timestamps[cache_key] = time.time()
|
| 51 |
+
|
| 52 |
+
logger.info(f"Translated text to Urdu (length: {len(translated_text)} chars)")
|
| 53 |
+
return translated_text
|
| 54 |
+
|
| 55 |
+
except Exception as e:
|
| 56 |
+
logger.error(f"Translation failed: {str(e)}")
|
| 57 |
+
# Return a professional fallback response
|
| 58 |
+
return f"Translation unavailable: {text[:100]}..."
|
| 59 |
+
|
| 60 |
+
def _get_urdu_translation_system_prompt(self) -> str:
|
| 61 |
+
"""Get the system prompt for Urdu translation from spec contract"""
|
| 62 |
+
return """You are a technical translator specializing in AI, robotics, and computer science education.
|
| 63 |
+
Translate the following English text to Urdu following these rules strictly:
|
| 64 |
+
|
| 65 |
+
TECHNICAL TERMS:
|
| 66 |
+
- Keep in English: ROS2, Python, API, HTTP, JSON, ML, AI, function, class, variable, loop, array
|
| 67 |
+
- Translate common words: robot → روبوٹ, computer → کمپیوٹر, network → نیٹ ورک
|
| 68 |
+
- Transliterate ambiguous terms: Sensor → سینسر (Sensor), Actuator → ایکچویٹر (Actuator)
|
| 69 |
+
- NEVER translate code identifiers (function names, variables, etc.)
|
| 70 |
+
|
| 71 |
+
FORMATTING:
|
| 72 |
+
- Preserve ALL markdown syntax (headings #, bold **, italic _, lists -, links [](url))
|
| 73 |
+
- Keep code blocks entirely in English (including comments): ```language ... ```
|
| 74 |
+
- Keep inline code in English: `variable_name`
|
| 75 |
+
- Keep LaTeX math unchanged: $equation$
|
| 76 |
+
- Translate link text but keep URLs: [ترجمہ شدہ متن](https://example.com)
|
| 77 |
+
- Translate image alt text but keep src: 
|
| 78 |
+
|
| 79 |
+
TONE:
|
| 80 |
+
- Use formal educational tone (not conversational)
|
| 81 |
+
- Follow standard Urdu grammar rules
|
| 82 |
+
- Use proper Urdu punctuation (،؟ instead of ,?)
|
| 83 |
+
- Do not mix English and Urdu in same sentence except for technical terms listed above
|
| 84 |
+
|
| 85 |
+
Translate now:"""
|
| 86 |
+
|
| 87 |
+
def translate_to_english(self, urdu_text: str, ttl: int = 3600) -> str:
|
| 88 |
+
"""Translate Urdu text back to English with caching"""
|
| 89 |
+
# Create cache key
|
| 90 |
+
cache_key = f"ur_to_en_{hash(urdu_text)}"
|
| 91 |
+
|
| 92 |
+
# Check if translation is in cache and not expired
|
| 93 |
+
if cache_key in self.translation_cache:
|
| 94 |
+
if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
|
| 95 |
+
logger.info("Returning cached translation")
|
| 96 |
+
return self.translation_cache[cache_key]
|
| 97 |
+
|
| 98 |
+
# Call OpenRouter API for translation using OpenAI SDK
|
| 99 |
+
try:
|
| 100 |
+
system_prompt = self._get_english_translation_system_prompt()
|
| 101 |
+
|
| 102 |
+
response = self.client.chat.completions.create(
|
| 103 |
+
model=self.model,
|
| 104 |
+
messages=[
|
| 105 |
+
{"role": "system", "content": system_prompt},
|
| 106 |
+
{"role": "user", "content": urdu_text}
|
| 107 |
+
],
|
| 108 |
+
temperature=0.3,
|
| 109 |
+
max_tokens=8000
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
translated_text = response.choices[0].message.content.strip()
|
| 113 |
+
|
| 114 |
+
# Cache the translation
|
| 115 |
+
self.translation_cache[cache_key] = translated_text
|
| 116 |
+
self.cache_timestamps[cache_key] = time.time()
|
| 117 |
+
|
| 118 |
+
logger.info(f"Translated text to English (length: {len(translated_text)} chars)")
|
| 119 |
+
return translated_text
|
| 120 |
+
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.error(f"Translation failed: {str(e)}")
|
| 123 |
+
# Return a professional fallback response
|
| 124 |
+
return f"Translation unavailable: {urdu_text[:100]}..."
|
| 125 |
+
|
| 126 |
+
def _get_english_translation_system_prompt(self) -> str:
|
| 127 |
+
"""Get the system prompt for English translation"""
|
| 128 |
+
return """You are a technical translator specializing in AI, robotics, and computer science education.
|
| 129 |
+
Translate the following Urdu text to English with precision and accuracy.
|
| 130 |
+
|
| 131 |
+
TRANSLATION REQUIREMENTS:
|
| 132 |
+
- Maintain technical accuracy for robotics/AI terminology
|
| 133 |
+
- Preserve the original meaning and context
|
| 134 |
+
- Apply appropriate formality level for educational content
|
| 135 |
+
- Ensure readability and flow in English
|
| 136 |
+
- Keep technical terms (ROS2, Python, API, etc.) in English
|
| 137 |
+
- Preserve markdown formatting and code blocks
|
| 138 |
+
- Do not add any commentary or explanations
|
| 139 |
+
|
| 140 |
+
Translate now:"""
|
| 141 |
+
|
| 142 |
+
def clear_cache(self):
|
| 143 |
+
"""Clear the translation cache"""
|
| 144 |
+
self.translation_cache.clear()
|
| 145 |
+
self.cache_timestamps.clear()
|
| 146 |
logger.info("Translation cache cleared")
|
|
@@ -1,26 +1,26 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
REM Batch script to start the backend server
|
| 3 |
-
REM Usage: start.bat
|
| 4 |
-
|
| 5 |
-
echo Starting AI Textbook Backend Server...
|
| 6 |
-
|
| 7 |
-
REM Activate virtual environment
|
| 8 |
-
if exist "venv\Scripts\activate.bat" (
|
| 9 |
-
echo Activating virtual environment...
|
| 10 |
-
call venv\Scripts\activate.bat
|
| 11 |
-
) else (
|
| 12 |
-
echo Virtual environment not found. Creating one...
|
| 13 |
-
python -m venv venv
|
| 14 |
-
call venv\Scripts\activate.bat
|
| 15 |
-
)
|
| 16 |
-
|
| 17 |
-
REM Install dependencies
|
| 18 |
-
echo Installing dependencies...
|
| 19 |
-
pip install -r requirements.txt
|
| 20 |
-
|
| 21 |
-
REM Start the server
|
| 22 |
-
echo.
|
| 23 |
-
echo Server starting on http://localhost:8001
|
| 24 |
-
echo Press Ctrl+C to stop the server
|
| 25 |
-
echo.
|
| 26 |
-
python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM Batch script to start the backend server
|
| 3 |
+
REM Usage: start.bat
|
| 4 |
+
|
| 5 |
+
echo Starting AI Textbook Backend Server...
|
| 6 |
+
|
| 7 |
+
REM Activate virtual environment
|
| 8 |
+
if exist "venv\Scripts\activate.bat" (
|
| 9 |
+
echo Activating virtual environment...
|
| 10 |
+
call venv\Scripts\activate.bat
|
| 11 |
+
) else (
|
| 12 |
+
echo Virtual environment not found. Creating one...
|
| 13 |
+
python -m venv venv
|
| 14 |
+
call venv\Scripts\activate.bat
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
REM Install dependencies
|
| 18 |
+
echo Installing dependencies...
|
| 19 |
+
pip install -r requirements.txt
|
| 20 |
+
|
| 21 |
+
REM Start the server
|
| 22 |
+
echo.
|
| 23 |
+
echo Server starting on http://localhost:8001
|
| 24 |
+
echo Press Ctrl+C to stop the server
|
| 25 |
+
echo.
|
| 26 |
+
python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
|
@@ -1,23 +1,23 @@
|
|
| 1 |
-
# PowerShell script to start the backend server
|
| 2 |
-
# Usage: .\start.ps1
|
| 3 |
-
|
| 4 |
-
Write-Host "🚀 Starting AI Textbook Backend Server..." -ForegroundColor Cyan
|
| 5 |
-
|
| 6 |
-
# Check if virtual environment exists
|
| 7 |
-
if (Test-Path ".\venv\Scripts\Activate.ps1") {
|
| 8 |
-
Write-Host "✓ Activating virtual environment..." -ForegroundColor Green
|
| 9 |
-
& .\venv\Scripts\Activate.ps1
|
| 10 |
-
} else {
|
| 11 |
-
Write-Host "⚠ Virtual environment not found. Creating one..." -ForegroundColor Yellow
|
| 12 |
-
python -m venv venv
|
| 13 |
-
& .\venv\Scripts\Activate.ps1
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
# Install dependencies
|
| 17 |
-
Write-Host "📦 Installing dependencies..." -ForegroundColor Cyan
|
| 18 |
-
pip install -r requirements.txt
|
| 19 |
-
|
| 20 |
-
# Start the server
|
| 21 |
-
Write-Host "🌐 Starting server on http://localhost:8001" -ForegroundColor Green
|
| 22 |
-
Write-Host "Press Ctrl+C to stop the server" -ForegroundColor Yellow
|
| 23 |
-
python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
|
|
|
| 1 |
+
# PowerShell script to start the backend server
|
| 2 |
+
# Usage: .\start.ps1
|
| 3 |
+
|
| 4 |
+
Write-Host "🚀 Starting AI Textbook Backend Server..." -ForegroundColor Cyan
|
| 5 |
+
|
| 6 |
+
# Check if virtual environment exists
|
| 7 |
+
if (Test-Path ".\venv\Scripts\Activate.ps1") {
|
| 8 |
+
Write-Host "✓ Activating virtual environment..." -ForegroundColor Green
|
| 9 |
+
& .\venv\Scripts\Activate.ps1
|
| 10 |
+
} else {
|
| 11 |
+
Write-Host "⚠ Virtual environment not found. Creating one..." -ForegroundColor Yellow
|
| 12 |
+
python -m venv venv
|
| 13 |
+
& .\venv\Scripts\Activate.ps1
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
# Install dependencies
|
| 17 |
+
Write-Host "📦 Installing dependencies..." -ForegroundColor Cyan
|
| 18 |
+
pip install -r requirements.txt
|
| 19 |
+
|
| 20 |
+
# Start the server
|
| 21 |
+
Write-Host "🌐 Starting server on http://localhost:8001" -ForegroundColor Green
|
| 22 |
+
Write-Host "Press Ctrl+C to stop the server" -ForegroundColor Yellow
|
| 23 |
+
python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
|
@@ -1,61 +1,61 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test the formatted message parsing
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
import sys
|
| 6 |
-
from dotenv import load_dotenv
|
| 7 |
-
|
| 8 |
-
load_dotenv()
|
| 9 |
-
|
| 10 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
-
from services.rag_service import RAGService
|
| 12 |
-
|
| 13 |
-
# Initialize
|
| 14 |
-
api_key = os.getenv("OPENAI_API_KEY")
|
| 15 |
-
rag_service = RAGService(api_key, None, "project_documents")
|
| 16 |
-
|
| 17 |
-
# Test Case 1: User sends formatted message (what frontend sends)
|
| 18 |
-
print("=== Test 1: Formatted Message (Current Issue) ===")
|
| 19 |
-
selected_text = "Interactive Learning Features"
|
| 20 |
-
question = """Selected text:
|
| 21 |
-
"Interactive Learning Features"
|
| 22 |
-
|
| 23 |
-
Ask a question about this text..."""
|
| 24 |
-
|
| 25 |
-
print(f"Selected Text: {selected_text}")
|
| 26 |
-
print(f"Question (formatted): {question}")
|
| 27 |
-
|
| 28 |
-
# This is what's happening now - question should be extracted properly
|
| 29 |
-
# In the fixed version, frontend will extract it, but let's test backend fallback
|
| 30 |
-
answer = rag_service.query_rag(selected_text, question)
|
| 31 |
-
print(f"\n✅ Answer: {answer[:200]}...")
|
| 32 |
-
print(f"Length: {len(answer)} chars\n")
|
| 33 |
-
|
| 34 |
-
# Test Case 2: User sends just the selected text (no question)
|
| 35 |
-
print("=== Test 2: No Question, Just Selected Text ===")
|
| 36 |
-
selected_text2 = "Physical AI refers to artificial intelligence"
|
| 37 |
-
question2 = "" # Empty question
|
| 38 |
-
|
| 39 |
-
answer2 = rag_service.query_rag(selected_text2, question2)
|
| 40 |
-
print(f"\n✅ Answer: {answer2[:200]}...")
|
| 41 |
-
print(f"Length: {len(answer2)} chars\n")
|
| 42 |
-
|
| 43 |
-
# Test Case 3: Normal question (should work)
|
| 44 |
-
print("=== Test 3: Normal Question ===")
|
| 45 |
-
selected_text3 = "Robots use sensors and actuators to interact with the environment"
|
| 46 |
-
question3 = "What are sensors and actuators?"
|
| 47 |
-
|
| 48 |
-
answer3 = rag_service.query_rag(selected_text3, question3)
|
| 49 |
-
print(f"\n✅ Answer: {answer3[:200]}...")
|
| 50 |
-
print(f"Length: {len(answer3)} chars\n")
|
| 51 |
-
|
| 52 |
-
# Test Case 4: Question same as selected text
|
| 53 |
-
print("=== Test 4: Question Same as Selected Text ===")
|
| 54 |
-
selected_text4 = "Humanoid Robotics"
|
| 55 |
-
question4 = "Humanoid Robotics" # Same as selected text
|
| 56 |
-
|
| 57 |
-
answer4 = rag_service.query_rag(selected_text4, question4)
|
| 58 |
-
print(f"\n✅ Answer: {answer4[:200]}...")
|
| 59 |
-
print(f"Length: {len(answer4)} chars\n")
|
| 60 |
-
|
| 61 |
-
print("\n=== All Tests Complete ===")
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test the formatted message parsing
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
+
from services.rag_service import RAGService
|
| 12 |
+
|
| 13 |
+
# Initialize
|
| 14 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 15 |
+
rag_service = RAGService(api_key, None, "project_documents")
|
| 16 |
+
|
| 17 |
+
# Test Case 1: User sends formatted message (what frontend sends)
|
| 18 |
+
print("=== Test 1: Formatted Message (Current Issue) ===")
|
| 19 |
+
selected_text = "Interactive Learning Features"
|
| 20 |
+
question = """Selected text:
|
| 21 |
+
"Interactive Learning Features"
|
| 22 |
+
|
| 23 |
+
Ask a question about this text..."""
|
| 24 |
+
|
| 25 |
+
print(f"Selected Text: {selected_text}")
|
| 26 |
+
print(f"Question (formatted): {question}")
|
| 27 |
+
|
| 28 |
+
# This is what's happening now - question should be extracted properly
|
| 29 |
+
# In the fixed version, frontend will extract it, but let's test backend fallback
|
| 30 |
+
answer = rag_service.query_rag(selected_text, question)
|
| 31 |
+
print(f"\n✅ Answer: {answer[:200]}...")
|
| 32 |
+
print(f"Length: {len(answer)} chars\n")
|
| 33 |
+
|
| 34 |
+
# Test Case 2: User sends just the selected text (no question)
|
| 35 |
+
print("=== Test 2: No Question, Just Selected Text ===")
|
| 36 |
+
selected_text2 = "Physical AI refers to artificial intelligence"
|
| 37 |
+
question2 = "" # Empty question
|
| 38 |
+
|
| 39 |
+
answer2 = rag_service.query_rag(selected_text2, question2)
|
| 40 |
+
print(f"\n✅ Answer: {answer2[:200]}...")
|
| 41 |
+
print(f"Length: {len(answer2)} chars\n")
|
| 42 |
+
|
| 43 |
+
# Test Case 3: Normal question (should work)
|
| 44 |
+
print("=== Test 3: Normal Question ===")
|
| 45 |
+
selected_text3 = "Robots use sensors and actuators to interact with the environment"
|
| 46 |
+
question3 = "What are sensors and actuators?"
|
| 47 |
+
|
| 48 |
+
answer3 = rag_service.query_rag(selected_text3, question3)
|
| 49 |
+
print(f"\n✅ Answer: {answer3[:200]}...")
|
| 50 |
+
print(f"Length: {len(answer3)} chars\n")
|
| 51 |
+
|
| 52 |
+
# Test Case 4: Question same as selected text
|
| 53 |
+
print("=== Test 4: Question Same as Selected Text ===")
|
| 54 |
+
selected_text4 = "Humanoid Robotics"
|
| 55 |
+
question4 = "Humanoid Robotics" # Same as selected text
|
| 56 |
+
|
| 57 |
+
answer4 = rag_service.query_rag(selected_text4, question4)
|
| 58 |
+
print(f"\n✅ Answer: {answer4[:200]}...")
|
| 59 |
+
print(f"Length: {len(answer4)} chars\n")
|
| 60 |
+
|
| 61 |
+
print("\n=== All Tests Complete ===")
|
|
@@ -1,79 +1,79 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Direct test of OpenRouter API to verify it's working
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
from dotenv import load_dotenv
|
| 6 |
-
from openai import OpenAI
|
| 7 |
-
|
| 8 |
-
# Load environment variables
|
| 9 |
-
load_dotenv()
|
| 10 |
-
|
| 11 |
-
api_key = os.getenv("OPENAI_API_KEY")
|
| 12 |
-
print(f"API Key loaded: {api_key[:20]}..." if api_key else "No API key found!")
|
| 13 |
-
|
| 14 |
-
# Initialize OpenAI client with OpenRouter
|
| 15 |
-
client = OpenAI(
|
| 16 |
-
api_key=api_key,
|
| 17 |
-
base_url="https://openrouter.ai/api/v1"
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
# Test 1: Simple chat completion
|
| 21 |
-
print("\n=== Test 1: Simple Chat ===")
|
| 22 |
-
try:
|
| 23 |
-
response = client.chat.completions.create(
|
| 24 |
-
model="openai/gpt-3.5-turbo",
|
| 25 |
-
messages=[
|
| 26 |
-
{"role": "user", "content": "Say 'Hello, I am working!' in one sentence."}
|
| 27 |
-
],
|
| 28 |
-
temperature=0
|
| 29 |
-
)
|
| 30 |
-
print(f"✅ Success: {response.choices[0].message.content}")
|
| 31 |
-
except Exception as e:
|
| 32 |
-
print(f"❌ Error: {str(e)}")
|
| 33 |
-
|
| 34 |
-
# Test 2: RAG-style query with context
|
| 35 |
-
print("\n=== Test 2: RAG with Context ===")
|
| 36 |
-
context = """
|
| 37 |
-
Physical AI refers to artificial intelligence systems that interact with the physical world.
|
| 38 |
-
These systems use sensors to perceive their environment and actuators to take actions.
|
| 39 |
-
Examples include robots, self-driving cars, and drones.
|
| 40 |
-
"""
|
| 41 |
-
|
| 42 |
-
question = "What is Physical AI?"
|
| 43 |
-
|
| 44 |
-
try:
|
| 45 |
-
response = client.chat.completions.create(
|
| 46 |
-
model="openai/gpt-3.5-turbo",
|
| 47 |
-
messages=[
|
| 48 |
-
{"role": "system", "content": "You are a helpful AI assistant. Answer based on the provided context."},
|
| 49 |
-
{"role": "user", "content": f"Context: {context}\n\nQuestion: {question}"}
|
| 50 |
-
],
|
| 51 |
-
temperature=0.3,
|
| 52 |
-
max_tokens=500
|
| 53 |
-
)
|
| 54 |
-
print(f"✅ Success: {response.choices[0].message.content}")
|
| 55 |
-
except Exception as e:
|
| 56 |
-
print(f"❌ Error: {str(e)}")
|
| 57 |
-
|
| 58 |
-
# Test 3: Check if response is empty or too short
|
| 59 |
-
print("\n=== Test 3: Response Length Check ===")
|
| 60 |
-
try:
|
| 61 |
-
response = client.chat.completions.create(
|
| 62 |
-
model="openai/gpt-3.5-turbo",
|
| 63 |
-
messages=[
|
| 64 |
-
{"role": "user", "content": "Explain robotics in 50 words."}
|
| 65 |
-
],
|
| 66 |
-
temperature=0.3
|
| 67 |
-
)
|
| 68 |
-
answer = response.choices[0].message.content
|
| 69 |
-
print(f"Response length: {len(answer)} characters")
|
| 70 |
-
print(f"Response: {answer}")
|
| 71 |
-
|
| 72 |
-
if len(answer) < 20:
|
| 73 |
-
print("⚠️ WARNING: Response too short!")
|
| 74 |
-
else:
|
| 75 |
-
print("✅ Response length is good")
|
| 76 |
-
except Exception as e:
|
| 77 |
-
print(f"❌ Error: {str(e)}")
|
| 78 |
-
|
| 79 |
-
print("\n=== All Tests Complete ===")
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Direct test of OpenRouter API to verify it's working
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from openai import OpenAI
|
| 7 |
+
|
| 8 |
+
# Load environment variables
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 12 |
+
print(f"API Key loaded: {api_key[:20]}..." if api_key else "No API key found!")
|
| 13 |
+
|
| 14 |
+
# Initialize OpenAI client with OpenRouter
|
| 15 |
+
client = OpenAI(
|
| 16 |
+
api_key=api_key,
|
| 17 |
+
base_url="https://openrouter.ai/api/v1"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Test 1: Simple chat completion
|
| 21 |
+
print("\n=== Test 1: Simple Chat ===")
|
| 22 |
+
try:
|
| 23 |
+
response = client.chat.completions.create(
|
| 24 |
+
model="openai/gpt-3.5-turbo",
|
| 25 |
+
messages=[
|
| 26 |
+
{"role": "user", "content": "Say 'Hello, I am working!' in one sentence."}
|
| 27 |
+
],
|
| 28 |
+
temperature=0
|
| 29 |
+
)
|
| 30 |
+
print(f"✅ Success: {response.choices[0].message.content}")
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"❌ Error: {str(e)}")
|
| 33 |
+
|
| 34 |
+
# Test 2: RAG-style query with context
|
| 35 |
+
print("\n=== Test 2: RAG with Context ===")
|
| 36 |
+
context = """
|
| 37 |
+
Physical AI refers to artificial intelligence systems that interact with the physical world.
|
| 38 |
+
These systems use sensors to perceive their environment and actuators to take actions.
|
| 39 |
+
Examples include robots, self-driving cars, and drones.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
question = "What is Physical AI?"
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
response = client.chat.completions.create(
|
| 46 |
+
model="openai/gpt-3.5-turbo",
|
| 47 |
+
messages=[
|
| 48 |
+
{"role": "system", "content": "You are a helpful AI assistant. Answer based on the provided context."},
|
| 49 |
+
{"role": "user", "content": f"Context: {context}\n\nQuestion: {question}"}
|
| 50 |
+
],
|
| 51 |
+
temperature=0.3,
|
| 52 |
+
max_tokens=500
|
| 53 |
+
)
|
| 54 |
+
print(f"✅ Success: {response.choices[0].message.content}")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"❌ Error: {str(e)}")
|
| 57 |
+
|
| 58 |
+
# Test 3: Check if response is empty or too short
|
| 59 |
+
print("\n=== Test 3: Response Length Check ===")
|
| 60 |
+
try:
|
| 61 |
+
response = client.chat.completions.create(
|
| 62 |
+
model="openai/gpt-3.5-turbo",
|
| 63 |
+
messages=[
|
| 64 |
+
{"role": "user", "content": "Explain robotics in 50 words."}
|
| 65 |
+
],
|
| 66 |
+
temperature=0.3
|
| 67 |
+
)
|
| 68 |
+
answer = response.choices[0].message.content
|
| 69 |
+
print(f"Response length: {len(answer)} characters")
|
| 70 |
+
print(f"Response: {answer}")
|
| 71 |
+
|
| 72 |
+
if len(answer) < 20:
|
| 73 |
+
print("⚠️ WARNING: Response too short!")
|
| 74 |
+
else:
|
| 75 |
+
print("✅ Response length is good")
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"❌ Error: {str(e)}")
|
| 78 |
+
|
| 79 |
+
print("\n=== All Tests Complete ===")
|
|
@@ -1,73 +1,73 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Debug the RAG service to see what's happening
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
import sys
|
| 6 |
-
import logging
|
| 7 |
-
from dotenv import load_dotenv
|
| 8 |
-
from qdrant_client import QdrantClient
|
| 9 |
-
|
| 10 |
-
# Setup logging
|
| 11 |
-
logging.basicConfig(level=logging.DEBUG)
|
| 12 |
-
|
| 13 |
-
# Load environment
|
| 14 |
-
load_dotenv()
|
| 15 |
-
|
| 16 |
-
# Import RAG service
|
| 17 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
-
from services.rag_service import RAGService
|
| 19 |
-
|
| 20 |
-
# Initialize
|
| 21 |
-
api_key = os.getenv("OPENAI_API_KEY")
|
| 22 |
-
qdrant_url = os.getenv("QDRANT_URL")
|
| 23 |
-
qdrant_api_key = os.getenv("QDRANT_API_KEY")
|
| 24 |
-
collection_name = os.getenv("QDRANT_COLLECTION", "project_documents")
|
| 25 |
-
|
| 26 |
-
print(f"API Key: {api_key[:20]}..." if api_key else "No API key")
|
| 27 |
-
print(f"Qdrant URL: {qdrant_url}")
|
| 28 |
-
print(f"Collection: {collection_name}")
|
| 29 |
-
|
| 30 |
-
# Initialize Qdrant client
|
| 31 |
-
qdrant_host = os.getenv("QDRANT_HOST")
|
| 32 |
-
if qdrant_api_key and qdrant_host and "qdrant.io" in qdrant_host:
|
| 33 |
-
print("\n✅ Using Qdrant Cloud")
|
| 34 |
-
qdrant_client = QdrantClient(
|
| 35 |
-
url=qdrant_host,
|
| 36 |
-
api_key=qdrant_api_key,
|
| 37 |
-
prefer_grpc=False
|
| 38 |
-
)
|
| 39 |
-
else:
|
| 40 |
-
print("\n✅ Using Local Qdrant (without Qdrant for testing)")
|
| 41 |
-
# Don't initialize Qdrant for now - test without it
|
| 42 |
-
qdrant_client = None
|
| 43 |
-
print("⚠️ Qdrant disabled for testing - will use selected text only")
|
| 44 |
-
|
| 45 |
-
# Initialize RAG service
|
| 46 |
-
rag_service = RAGService(api_key, qdrant_client, collection_name)
|
| 47 |
-
|
| 48 |
-
# Test query
|
| 49 |
-
selected_text = """
|
| 50 |
-
Physical AI refers to artificial intelligence systems that interact with the physical world.
|
| 51 |
-
These systems use sensors to perceive their environment and actuators to take actions.
|
| 52 |
-
Examples include robots, self-driving cars, and drones.
|
| 53 |
-
"""
|
| 54 |
-
|
| 55 |
-
question = "What is Physical AI?"
|
| 56 |
-
|
| 57 |
-
print("\n=== Testing RAG Service ===")
|
| 58 |
-
print(f"Selected Text Length: {len(selected_text)} chars")
|
| 59 |
-
print(f"Question: {question}")
|
| 60 |
-
|
| 61 |
-
try:
|
| 62 |
-
answer = rag_service.query_rag(selected_text, question)
|
| 63 |
-
print(f"\n✅ Answer received:")
|
| 64 |
-
print(f"Length: {len(answer)} chars")
|
| 65 |
-
print(f"Content: {answer}")
|
| 66 |
-
|
| 67 |
-
if "Is sawal ka jawab provided data me mojood nahi hai" in answer:
|
| 68 |
-
print("\n❌ ERROR: Getting fallback message!")
|
| 69 |
-
print("This means the RAG logic is failing somewhere.")
|
| 70 |
-
except Exception as e:
|
| 71 |
-
print(f"\n❌ Error: {str(e)}")
|
| 72 |
-
import traceback
|
| 73 |
-
traceback.print_exc()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Debug the RAG service to see what's happening
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import logging
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from qdrant_client import QdrantClient
|
| 9 |
+
|
| 10 |
+
# Setup logging
|
| 11 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 12 |
+
|
| 13 |
+
# Load environment
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
# Import RAG service
|
| 17 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
+
from services.rag_service import RAGService
|
| 19 |
+
|
| 20 |
+
# Initialize
|
| 21 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 22 |
+
qdrant_url = os.getenv("QDRANT_URL")
|
| 23 |
+
qdrant_api_key = os.getenv("QDRANT_API_KEY")
|
| 24 |
+
collection_name = os.getenv("QDRANT_COLLECTION", "project_documents")
|
| 25 |
+
|
| 26 |
+
print(f"API Key: {api_key[:20]}..." if api_key else "No API key")
|
| 27 |
+
print(f"Qdrant URL: {qdrant_url}")
|
| 28 |
+
print(f"Collection: {collection_name}")
|
| 29 |
+
|
| 30 |
+
# Initialize Qdrant client
|
| 31 |
+
qdrant_host = os.getenv("QDRANT_HOST")
|
| 32 |
+
if qdrant_api_key and qdrant_host and "qdrant.io" in qdrant_host:
|
| 33 |
+
print("\n✅ Using Qdrant Cloud")
|
| 34 |
+
qdrant_client = QdrantClient(
|
| 35 |
+
url=qdrant_host,
|
| 36 |
+
api_key=qdrant_api_key,
|
| 37 |
+
prefer_grpc=False
|
| 38 |
+
)
|
| 39 |
+
else:
|
| 40 |
+
print("\n✅ Using Local Qdrant (without Qdrant for testing)")
|
| 41 |
+
# Don't initialize Qdrant for now - test without it
|
| 42 |
+
qdrant_client = None
|
| 43 |
+
print("⚠️ Qdrant disabled for testing - will use selected text only")
|
| 44 |
+
|
| 45 |
+
# Initialize RAG service
|
| 46 |
+
rag_service = RAGService(api_key, qdrant_client, collection_name)
|
| 47 |
+
|
| 48 |
+
# Test query
|
| 49 |
+
selected_text = """
|
| 50 |
+
Physical AI refers to artificial intelligence systems that interact with the physical world.
|
| 51 |
+
These systems use sensors to perceive their environment and actuators to take actions.
|
| 52 |
+
Examples include robots, self-driving cars, and drones.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
question = "What is Physical AI?"
|
| 56 |
+
|
| 57 |
+
print("\n=== Testing RAG Service ===")
|
| 58 |
+
print(f"Selected Text Length: {len(selected_text)} chars")
|
| 59 |
+
print(f"Question: {question}")
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
answer = rag_service.query_rag(selected_text, question)
|
| 63 |
+
print(f"\n✅ Answer received:")
|
| 64 |
+
print(f"Length: {len(answer)} chars")
|
| 65 |
+
print(f"Content: {answer}")
|
| 66 |
+
|
| 67 |
+
if "Is sawal ka jawab provided data me mojood nahi hai" in answer:
|
| 68 |
+
print("\n❌ ERROR: Getting fallback message!")
|
| 69 |
+
print("This means the RAG logic is failing somewhere.")
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"\n❌ Error: {str(e)}")
|
| 72 |
+
import traceback
|
| 73 |
+
traceback.print_exc()
|
|
@@ -1,295 +1,295 @@
|
|
| 1 |
-
"""
|
| 2 |
-
End-to-End Test: Complete Translation Flow
|
| 3 |
-
|
| 4 |
-
Tests the full user journey:
|
| 5 |
-
1. User signup
|
| 6 |
-
2. Login
|
| 7 |
-
3. Navigate to chapter
|
| 8 |
-
4. Translate to Urdu
|
| 9 |
-
5. Toggle back to English
|
| 10 |
-
6. Submit feedback
|
| 11 |
-
|
| 12 |
-
Note: Requires Playwright installation:
|
| 13 |
-
pip install playwright pytest-playwright
|
| 14 |
-
playwright install
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
import pytest
|
| 18 |
-
from playwright.sync_api import Page, expect
|
| 19 |
-
import time
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
# Test configuration
|
| 23 |
-
BASE_URL = "http://localhost:3000"
|
| 24 |
-
API_URL = "http://localhost:8001"
|
| 25 |
-
TEST_USER_EMAIL = f"test_{int(time.time())}@example.com"
|
| 26 |
-
TEST_USER_PASSWORD = "Test1234!"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
@pytest.fixture(scope="module")
|
| 30 |
-
def browser_context(playwright):
|
| 31 |
-
"""Create browser context for all tests"""
|
| 32 |
-
browser = playwright.chromium.launch(headless=True)
|
| 33 |
-
context = browser.new_context(viewport={"width": 1920, "height": 1080})
|
| 34 |
-
yield context
|
| 35 |
-
context.close()
|
| 36 |
-
browser.close()
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
@pytest.fixture
|
| 40 |
-
def page(browser_context):
|
| 41 |
-
"""Create new page for each test"""
|
| 42 |
-
page = browser_context.new_page()
|
| 43 |
-
yield page
|
| 44 |
-
page.close()
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
class TestTranslationE2E:
|
| 48 |
-
"""End-to-end translation flow tests"""
|
| 49 |
-
|
| 50 |
-
def test_01_user_signup(self, page: Page):
|
| 51 |
-
"""Test 1: User can sign up with software/hardware background"""
|
| 52 |
-
print(f"\n🧪 Test 1: Signup with email: {TEST_USER_EMAIL}")
|
| 53 |
-
|
| 54 |
-
# Navigate to signup page
|
| 55 |
-
page.goto(f"{BASE_URL}/signup")
|
| 56 |
-
page.wait_for_load_state("networkidle")
|
| 57 |
-
|
| 58 |
-
# Fill signup form
|
| 59 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 60 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 61 |
-
page.fill('textarea[placeholder*="software"]', "Python, JavaScript, ROS2")
|
| 62 |
-
page.fill('textarea[placeholder*="hardware"]', "Arduino, Raspberry Pi")
|
| 63 |
-
|
| 64 |
-
# Submit form
|
| 65 |
-
page.click('button[type="submit"]')
|
| 66 |
-
page.wait_for_timeout(2000)
|
| 67 |
-
|
| 68 |
-
# Verify redirect to homepage or dashboard
|
| 69 |
-
assert "signup" not in page.url.lower() or "login" in page.url.lower()
|
| 70 |
-
print("✅ Signup successful")
|
| 71 |
-
|
| 72 |
-
def test_02_user_login(self, page: Page):
|
| 73 |
-
"""Test 2: User can login with credentials"""
|
| 74 |
-
print(f"\n🧪 Test 2: Login with email: {TEST_USER_EMAIL}")
|
| 75 |
-
|
| 76 |
-
# Navigate to login page
|
| 77 |
-
page.goto(f"{BASE_URL}/login")
|
| 78 |
-
page.wait_for_load_state("networkidle")
|
| 79 |
-
|
| 80 |
-
# Fill login form
|
| 81 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 82 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 83 |
-
|
| 84 |
-
# Submit form
|
| 85 |
-
page.click('button[type="submit"]')
|
| 86 |
-
page.wait_for_timeout(2000)
|
| 87 |
-
|
| 88 |
-
# Verify authentication (should see logout or profile)
|
| 89 |
-
assert "login" not in page.url.lower()
|
| 90 |
-
print("✅ Login successful")
|
| 91 |
-
|
| 92 |
-
def test_03_navigate_to_chapter(self, page: Page):
|
| 93 |
-
"""Test 3: User can navigate to a chapter with translation support"""
|
| 94 |
-
print("\n🧪 Test 3: Navigate to ROS2 Fundamentals chapter")
|
| 95 |
-
|
| 96 |
-
# Login first
|
| 97 |
-
page.goto(f"{BASE_URL}/login")
|
| 98 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 99 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 100 |
-
page.click('button[type="submit"]')
|
| 101 |
-
page.wait_for_timeout(2000)
|
| 102 |
-
|
| 103 |
-
# Navigate to chapter with translation support
|
| 104 |
-
page.goto(f"{BASE_URL}/docs/ros2-fundamentals")
|
| 105 |
-
page.wait_for_load_state("networkidle")
|
| 106 |
-
|
| 107 |
-
# Verify chapter loaded
|
| 108 |
-
assert "ros2" in page.url.lower() or "fundamentals" in page.url.lower()
|
| 109 |
-
|
| 110 |
-
# Verify translation button exists
|
| 111 |
-
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 112 |
-
expect(translate_button).to_be_visible(timeout=5000)
|
| 113 |
-
print("✅ Chapter loaded with translation button")
|
| 114 |
-
|
| 115 |
-
def test_04_translate_to_urdu(self, page: Page):
|
| 116 |
-
"""Test 4: User can translate chapter to Urdu (first time - API call)"""
|
| 117 |
-
print("\n🧪 Test 4: Translate chapter to Urdu")
|
| 118 |
-
|
| 119 |
-
# Login and navigate to chapter
|
| 120 |
-
page.goto(f"{BASE_URL}/login")
|
| 121 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 122 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 123 |
-
page.click('button[type="submit"]')
|
| 124 |
-
page.wait_for_timeout(2000)
|
| 125 |
-
|
| 126 |
-
page.goto(f"{BASE_URL}/docs/intro")
|
| 127 |
-
page.wait_for_load_state("networkidle")
|
| 128 |
-
|
| 129 |
-
# Click translate button
|
| 130 |
-
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 131 |
-
translate_button.click()
|
| 132 |
-
|
| 133 |
-
# Verify loading state appears
|
| 134 |
-
loading_indicator = page.locator('text=Translating')
|
| 135 |
-
expect(loading_indicator).to_be_visible(timeout=2000)
|
| 136 |
-
print("⏳ Loading indicator visible")
|
| 137 |
-
|
| 138 |
-
# Wait for translation to complete (8-10 seconds)
|
| 139 |
-
page.wait_for_timeout(12000)
|
| 140 |
-
|
| 141 |
-
# Verify button changed to "View in English"
|
| 142 |
-
english_button = page.locator('button:has-text("View in English")')
|
| 143 |
-
expect(english_button).to_be_visible(timeout=15000)
|
| 144 |
-
print("✅ Translation completed successfully")
|
| 145 |
-
|
| 146 |
-
# Verify Urdu content is displayed (check for RTL)
|
| 147 |
-
# Note: Actual Urdu verification would need OCR or specific text matching
|
| 148 |
-
page.wait_for_timeout(1000)
|
| 149 |
-
|
| 150 |
-
def test_05_toggle_to_english(self, page: Page):
|
| 151 |
-
"""Test 5: User can toggle back to English instantly"""
|
| 152 |
-
print("\n🧪 Test 5: Toggle back to English")
|
| 153 |
-
|
| 154 |
-
# Login, navigate, and translate first
|
| 155 |
-
page.goto(f"{BASE_URL}/login")
|
| 156 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 157 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 158 |
-
page.click('button[type="submit"]')
|
| 159 |
-
page.wait_for_timeout(2000)
|
| 160 |
-
|
| 161 |
-
page.goto(f"{BASE_URL}/docs/intro")
|
| 162 |
-
page.wait_for_load_state("networkidle")
|
| 163 |
-
|
| 164 |
-
# Translate to Urdu first
|
| 165 |
-
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 166 |
-
if translate_button.is_visible():
|
| 167 |
-
translate_button.click()
|
| 168 |
-
page.wait_for_timeout(12000)
|
| 169 |
-
|
| 170 |
-
# Click "View in English" button
|
| 171 |
-
english_button = page.locator('button:has-text("View in English")')
|
| 172 |
-
start_time = time.time()
|
| 173 |
-
english_button.click()
|
| 174 |
-
|
| 175 |
-
# Verify instant toggle (<100ms target, but allow 1s for UI)
|
| 176 |
-
page.wait_for_timeout(500)
|
| 177 |
-
urdu_button = page.locator('button:has-text("Translate to Urdu")')
|
| 178 |
-
expect(urdu_button).to_be_visible(timeout=2000)
|
| 179 |
-
|
| 180 |
-
elapsed = time.time() - start_time
|
| 181 |
-
print(f"✅ Toggled to English in {elapsed:.2f}s (instant)")
|
| 182 |
-
assert elapsed < 2.0, "Toggle should be instant (<2s)"
|
| 183 |
-
|
| 184 |
-
def test_06_cached_translation(self, page: Page):
|
| 185 |
-
"""Test 6: Second translation loads from cache (<1s)"""
|
| 186 |
-
print("\n🧪 Test 6: Cached translation loading")
|
| 187 |
-
|
| 188 |
-
# Login and navigate
|
| 189 |
-
page.goto(f"{BASE_URL}/login")
|
| 190 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 191 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 192 |
-
page.click('button[type="submit"]')
|
| 193 |
-
page.wait_for_timeout(2000)
|
| 194 |
-
|
| 195 |
-
page.goto(f"{BASE_URL}/docs/intro")
|
| 196 |
-
page.wait_for_load_state("networkidle")
|
| 197 |
-
|
| 198 |
-
# Ensure we're in English view
|
| 199 |
-
english_button = page.locator('button:has-text("View in English")')
|
| 200 |
-
if english_button.is_visible():
|
| 201 |
-
english_button.click()
|
| 202 |
-
page.wait_for_timeout(500)
|
| 203 |
-
|
| 204 |
-
# Click translate again (should hit cache)
|
| 205 |
-
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 206 |
-
start_time = time.time()
|
| 207 |
-
translate_button.click()
|
| 208 |
-
|
| 209 |
-
# Wait for translation to complete
|
| 210 |
-
english_button = page.locator('button:has-text("View in English")')
|
| 211 |
-
expect(english_button).to_be_visible(timeout=5000)
|
| 212 |
-
|
| 213 |
-
elapsed = time.time() - start_time
|
| 214 |
-
print(f"✅ Cache hit in {elapsed:.2f}s")
|
| 215 |
-
|
| 216 |
-
# Verify cache indicator visible
|
| 217 |
-
cache_indicator = page.locator('text=Loaded from cache')
|
| 218 |
-
# Cache indicator might not always be visible, so don't assert
|
| 219 |
-
if cache_indicator.is_visible(timeout=1000):
|
| 220 |
-
print("⚡ Cache indicator displayed")
|
| 221 |
-
|
| 222 |
-
def test_07_submit_feedback(self, page: Page):
|
| 223 |
-
"""Test 7: User can submit translation feedback"""
|
| 224 |
-
print("\n🧪 Test 7: Submit translation feedback")
|
| 225 |
-
|
| 226 |
-
# Login, navigate, and translate
|
| 227 |
-
page.goto(f"{BASE_URL}/login")
|
| 228 |
-
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 229 |
-
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 230 |
-
page.click('button[type="submit"]')
|
| 231 |
-
page.wait_for_timeout(2000)
|
| 232 |
-
|
| 233 |
-
page.goto(f"{BASE_URL}/docs/intro")
|
| 234 |
-
page.wait_for_load_state("networkidle")
|
| 235 |
-
|
| 236 |
-
# Translate to Urdu first
|
| 237 |
-
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 238 |
-
if translate_button.is_visible():
|
| 239 |
-
translate_button.click()
|
| 240 |
-
page.wait_for_timeout(12000)
|
| 241 |
-
|
| 242 |
-
# Click "Report Issue" button
|
| 243 |
-
feedback_button = page.locator('button:has-text("Report Issue")')
|
| 244 |
-
if feedback_button.is_visible():
|
| 245 |
-
feedback_button.click()
|
| 246 |
-
page.wait_for_timeout(500)
|
| 247 |
-
|
| 248 |
-
# Fill feedback form
|
| 249 |
-
textarea = page.locator('textarea[placeholder*="issue"]')
|
| 250 |
-
textarea.fill("Test feedback: The term 'API' should remain in English.")
|
| 251 |
-
|
| 252 |
-
# Submit feedback
|
| 253 |
-
submit_button = page.locator('button:has-text("Submit Feedback")')
|
| 254 |
-
submit_button.click()
|
| 255 |
-
page.wait_for_timeout(2000)
|
| 256 |
-
|
| 257 |
-
# Verify success message
|
| 258 |
-
success_message = page.locator('text=Thank you')
|
| 259 |
-
if success_message.is_visible(timeout=3000):
|
| 260 |
-
print("✅ Feedback submitted successfully")
|
| 261 |
-
else:
|
| 262 |
-
print("⚠️ Feedback button found but submission uncertain")
|
| 263 |
-
else:
|
| 264 |
-
print("⚠️ Feedback button not visible (may need Urdu translation first)")
|
| 265 |
-
|
| 266 |
-
def test_08_unauthenticated_user_prompt(self, page: Page):
|
| 267 |
-
"""Test 8: Unauthenticated users see login prompt"""
|
| 268 |
-
print("\n🧪 Test 8: Unauthenticated user experience")
|
| 269 |
-
|
| 270 |
-
# Navigate to chapter without login
|
| 271 |
-
page.goto(f"{BASE_URL}/docs/intro")
|
| 272 |
-
page.wait_for_load_state("networkidle")
|
| 273 |
-
|
| 274 |
-
# Check if translation button is disabled or shows auth prompt
|
| 275 |
-
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 276 |
-
|
| 277 |
-
if translate_button.is_visible():
|
| 278 |
-
# Click button (should not translate)
|
| 279 |
-
translate_button.click()
|
| 280 |
-
page.wait_for_timeout(1000)
|
| 281 |
-
|
| 282 |
-
# Should see authentication prompt or redirect to signup
|
| 283 |
-
auth_prompt = page.locator('text=Login required, text=Sign up, text=login')
|
| 284 |
-
if auth_prompt.is_visible(timeout=2000):
|
| 285 |
-
print("✅ Authentication prompt shown to guest users")
|
| 286 |
-
elif "signup" in page.url.lower() or "login" in page.url.lower():
|
| 287 |
-
print("✅ Redirected to signup/login page")
|
| 288 |
-
else:
|
| 289 |
-
print("⚠️ Authentication handling unclear")
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
# Run tests with: pytest backend/tests/e2e/test_translation_flow.py -v -s
|
| 293 |
-
# Note: Requires backend and frontend servers running:
|
| 294 |
-
# Terminal 1: cd backend && python3 main.py
|
| 295 |
-
# Terminal 2: cd frontend && npm start
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
End-to-End Test: Complete Translation Flow
|
| 3 |
+
|
| 4 |
+
Tests the full user journey:
|
| 5 |
+
1. User signup
|
| 6 |
+
2. Login
|
| 7 |
+
3. Navigate to chapter
|
| 8 |
+
4. Translate to Urdu
|
| 9 |
+
5. Toggle back to English
|
| 10 |
+
6. Submit feedback
|
| 11 |
+
|
| 12 |
+
Note: Requires Playwright installation:
|
| 13 |
+
pip install playwright pytest-playwright
|
| 14 |
+
playwright install
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import pytest
|
| 18 |
+
from playwright.sync_api import Page, expect
|
| 19 |
+
import time
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Test configuration
|
| 23 |
+
BASE_URL = "http://localhost:3000"
|
| 24 |
+
API_URL = "http://localhost:8001"
|
| 25 |
+
TEST_USER_EMAIL = f"test_{int(time.time())}@example.com"
|
| 26 |
+
TEST_USER_PASSWORD = "Test1234!"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.fixture(scope="module")
|
| 30 |
+
def browser_context(playwright):
|
| 31 |
+
"""Create browser context for all tests"""
|
| 32 |
+
browser = playwright.chromium.launch(headless=True)
|
| 33 |
+
context = browser.new_context(viewport={"width": 1920, "height": 1080})
|
| 34 |
+
yield context
|
| 35 |
+
context.close()
|
| 36 |
+
browser.close()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@pytest.fixture
|
| 40 |
+
def page(browser_context):
|
| 41 |
+
"""Create new page for each test"""
|
| 42 |
+
page = browser_context.new_page()
|
| 43 |
+
yield page
|
| 44 |
+
page.close()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TestTranslationE2E:
|
| 48 |
+
"""End-to-end translation flow tests"""
|
| 49 |
+
|
| 50 |
+
def test_01_user_signup(self, page: Page):
|
| 51 |
+
"""Test 1: User can sign up with software/hardware background"""
|
| 52 |
+
print(f"\n🧪 Test 1: Signup with email: {TEST_USER_EMAIL}")
|
| 53 |
+
|
| 54 |
+
# Navigate to signup page
|
| 55 |
+
page.goto(f"{BASE_URL}/signup")
|
| 56 |
+
page.wait_for_load_state("networkidle")
|
| 57 |
+
|
| 58 |
+
# Fill signup form
|
| 59 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 60 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 61 |
+
page.fill('textarea[placeholder*="software"]', "Python, JavaScript, ROS2")
|
| 62 |
+
page.fill('textarea[placeholder*="hardware"]', "Arduino, Raspberry Pi")
|
| 63 |
+
|
| 64 |
+
# Submit form
|
| 65 |
+
page.click('button[type="submit"]')
|
| 66 |
+
page.wait_for_timeout(2000)
|
| 67 |
+
|
| 68 |
+
# Verify redirect to homepage or dashboard
|
| 69 |
+
assert "signup" not in page.url.lower() or "login" in page.url.lower()
|
| 70 |
+
print("✅ Signup successful")
|
| 71 |
+
|
| 72 |
+
def test_02_user_login(self, page: Page):
|
| 73 |
+
"""Test 2: User can login with credentials"""
|
| 74 |
+
print(f"\n🧪 Test 2: Login with email: {TEST_USER_EMAIL}")
|
| 75 |
+
|
| 76 |
+
# Navigate to login page
|
| 77 |
+
page.goto(f"{BASE_URL}/login")
|
| 78 |
+
page.wait_for_load_state("networkidle")
|
| 79 |
+
|
| 80 |
+
# Fill login form
|
| 81 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 82 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 83 |
+
|
| 84 |
+
# Submit form
|
| 85 |
+
page.click('button[type="submit"]')
|
| 86 |
+
page.wait_for_timeout(2000)
|
| 87 |
+
|
| 88 |
+
# Verify authentication (should see logout or profile)
|
| 89 |
+
assert "login" not in page.url.lower()
|
| 90 |
+
print("✅ Login successful")
|
| 91 |
+
|
| 92 |
+
def test_03_navigate_to_chapter(self, page: Page):
|
| 93 |
+
"""Test 3: User can navigate to a chapter with translation support"""
|
| 94 |
+
print("\n🧪 Test 3: Navigate to ROS2 Fundamentals chapter")
|
| 95 |
+
|
| 96 |
+
# Login first
|
| 97 |
+
page.goto(f"{BASE_URL}/login")
|
| 98 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 99 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 100 |
+
page.click('button[type="submit"]')
|
| 101 |
+
page.wait_for_timeout(2000)
|
| 102 |
+
|
| 103 |
+
# Navigate to chapter with translation support
|
| 104 |
+
page.goto(f"{BASE_URL}/docs/ros2-fundamentals")
|
| 105 |
+
page.wait_for_load_state("networkidle")
|
| 106 |
+
|
| 107 |
+
# Verify chapter loaded
|
| 108 |
+
assert "ros2" in page.url.lower() or "fundamentals" in page.url.lower()
|
| 109 |
+
|
| 110 |
+
# Verify translation button exists
|
| 111 |
+
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 112 |
+
expect(translate_button).to_be_visible(timeout=5000)
|
| 113 |
+
print("✅ Chapter loaded with translation button")
|
| 114 |
+
|
| 115 |
+
def test_04_translate_to_urdu(self, page: Page):
|
| 116 |
+
"""Test 4: User can translate chapter to Urdu (first time - API call)"""
|
| 117 |
+
print("\n🧪 Test 4: Translate chapter to Urdu")
|
| 118 |
+
|
| 119 |
+
# Login and navigate to chapter
|
| 120 |
+
page.goto(f"{BASE_URL}/login")
|
| 121 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 122 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 123 |
+
page.click('button[type="submit"]')
|
| 124 |
+
page.wait_for_timeout(2000)
|
| 125 |
+
|
| 126 |
+
page.goto(f"{BASE_URL}/docs/intro")
|
| 127 |
+
page.wait_for_load_state("networkidle")
|
| 128 |
+
|
| 129 |
+
# Click translate button
|
| 130 |
+
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 131 |
+
translate_button.click()
|
| 132 |
+
|
| 133 |
+
# Verify loading state appears
|
| 134 |
+
loading_indicator = page.locator('text=Translating')
|
| 135 |
+
expect(loading_indicator).to_be_visible(timeout=2000)
|
| 136 |
+
print("⏳ Loading indicator visible")
|
| 137 |
+
|
| 138 |
+
# Wait for translation to complete (8-10 seconds)
|
| 139 |
+
page.wait_for_timeout(12000)
|
| 140 |
+
|
| 141 |
+
# Verify button changed to "View in English"
|
| 142 |
+
english_button = page.locator('button:has-text("View in English")')
|
| 143 |
+
expect(english_button).to_be_visible(timeout=15000)
|
| 144 |
+
print("✅ Translation completed successfully")
|
| 145 |
+
|
| 146 |
+
# Verify Urdu content is displayed (check for RTL)
|
| 147 |
+
# Note: Actual Urdu verification would need OCR or specific text matching
|
| 148 |
+
page.wait_for_timeout(1000)
|
| 149 |
+
|
| 150 |
+
def test_05_toggle_to_english(self, page: Page):
|
| 151 |
+
"""Test 5: User can toggle back to English instantly"""
|
| 152 |
+
print("\n🧪 Test 5: Toggle back to English")
|
| 153 |
+
|
| 154 |
+
# Login, navigate, and translate first
|
| 155 |
+
page.goto(f"{BASE_URL}/login")
|
| 156 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 157 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 158 |
+
page.click('button[type="submit"]')
|
| 159 |
+
page.wait_for_timeout(2000)
|
| 160 |
+
|
| 161 |
+
page.goto(f"{BASE_URL}/docs/intro")
|
| 162 |
+
page.wait_for_load_state("networkidle")
|
| 163 |
+
|
| 164 |
+
# Translate to Urdu first
|
| 165 |
+
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 166 |
+
if translate_button.is_visible():
|
| 167 |
+
translate_button.click()
|
| 168 |
+
page.wait_for_timeout(12000)
|
| 169 |
+
|
| 170 |
+
# Click "View in English" button
|
| 171 |
+
english_button = page.locator('button:has-text("View in English")')
|
| 172 |
+
start_time = time.time()
|
| 173 |
+
english_button.click()
|
| 174 |
+
|
| 175 |
+
# Verify instant toggle (<100ms target, but allow 1s for UI)
|
| 176 |
+
page.wait_for_timeout(500)
|
| 177 |
+
urdu_button = page.locator('button:has-text("Translate to Urdu")')
|
| 178 |
+
expect(urdu_button).to_be_visible(timeout=2000)
|
| 179 |
+
|
| 180 |
+
elapsed = time.time() - start_time
|
| 181 |
+
print(f"✅ Toggled to English in {elapsed:.2f}s (instant)")
|
| 182 |
+
assert elapsed < 2.0, "Toggle should be instant (<2s)"
|
| 183 |
+
|
| 184 |
+
def test_06_cached_translation(self, page: Page):
|
| 185 |
+
"""Test 6: Second translation loads from cache (<1s)"""
|
| 186 |
+
print("\n🧪 Test 6: Cached translation loading")
|
| 187 |
+
|
| 188 |
+
# Login and navigate
|
| 189 |
+
page.goto(f"{BASE_URL}/login")
|
| 190 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 191 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 192 |
+
page.click('button[type="submit"]')
|
| 193 |
+
page.wait_for_timeout(2000)
|
| 194 |
+
|
| 195 |
+
page.goto(f"{BASE_URL}/docs/intro")
|
| 196 |
+
page.wait_for_load_state("networkidle")
|
| 197 |
+
|
| 198 |
+
# Ensure we're in English view
|
| 199 |
+
english_button = page.locator('button:has-text("View in English")')
|
| 200 |
+
if english_button.is_visible():
|
| 201 |
+
english_button.click()
|
| 202 |
+
page.wait_for_timeout(500)
|
| 203 |
+
|
| 204 |
+
# Click translate again (should hit cache)
|
| 205 |
+
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 206 |
+
start_time = time.time()
|
| 207 |
+
translate_button.click()
|
| 208 |
+
|
| 209 |
+
# Wait for translation to complete
|
| 210 |
+
english_button = page.locator('button:has-text("View in English")')
|
| 211 |
+
expect(english_button).to_be_visible(timeout=5000)
|
| 212 |
+
|
| 213 |
+
elapsed = time.time() - start_time
|
| 214 |
+
print(f"✅ Cache hit in {elapsed:.2f}s")
|
| 215 |
+
|
| 216 |
+
# Verify cache indicator visible
|
| 217 |
+
cache_indicator = page.locator('text=Loaded from cache')
|
| 218 |
+
# Cache indicator might not always be visible, so don't assert
|
| 219 |
+
if cache_indicator.is_visible(timeout=1000):
|
| 220 |
+
print("⚡ Cache indicator displayed")
|
| 221 |
+
|
| 222 |
+
def test_07_submit_feedback(self, page: Page):
|
| 223 |
+
"""Test 7: User can submit translation feedback"""
|
| 224 |
+
print("\n🧪 Test 7: Submit translation feedback")
|
| 225 |
+
|
| 226 |
+
# Login, navigate, and translate
|
| 227 |
+
page.goto(f"{BASE_URL}/login")
|
| 228 |
+
page.fill('input[type="email"]', TEST_USER_EMAIL)
|
| 229 |
+
page.fill('input[type="password"]', TEST_USER_PASSWORD)
|
| 230 |
+
page.click('button[type="submit"]')
|
| 231 |
+
page.wait_for_timeout(2000)
|
| 232 |
+
|
| 233 |
+
page.goto(f"{BASE_URL}/docs/intro")
|
| 234 |
+
page.wait_for_load_state("networkidle")
|
| 235 |
+
|
| 236 |
+
# Translate to Urdu first
|
| 237 |
+
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 238 |
+
if translate_button.is_visible():
|
| 239 |
+
translate_button.click()
|
| 240 |
+
page.wait_for_timeout(12000)
|
| 241 |
+
|
| 242 |
+
# Click "Report Issue" button
|
| 243 |
+
feedback_button = page.locator('button:has-text("Report Issue")')
|
| 244 |
+
if feedback_button.is_visible():
|
| 245 |
+
feedback_button.click()
|
| 246 |
+
page.wait_for_timeout(500)
|
| 247 |
+
|
| 248 |
+
# Fill feedback form
|
| 249 |
+
textarea = page.locator('textarea[placeholder*="issue"]')
|
| 250 |
+
textarea.fill("Test feedback: The term 'API' should remain in English.")
|
| 251 |
+
|
| 252 |
+
# Submit feedback
|
| 253 |
+
submit_button = page.locator('button:has-text("Submit Feedback")')
|
| 254 |
+
submit_button.click()
|
| 255 |
+
page.wait_for_timeout(2000)
|
| 256 |
+
|
| 257 |
+
# Verify success message
|
| 258 |
+
success_message = page.locator('text=Thank you')
|
| 259 |
+
if success_message.is_visible(timeout=3000):
|
| 260 |
+
print("✅ Feedback submitted successfully")
|
| 261 |
+
else:
|
| 262 |
+
print("⚠️ Feedback button found but submission uncertain")
|
| 263 |
+
else:
|
| 264 |
+
print("⚠️ Feedback button not visible (may need Urdu translation first)")
|
| 265 |
+
|
| 266 |
+
def test_08_unauthenticated_user_prompt(self, page: Page):
|
| 267 |
+
"""Test 8: Unauthenticated users see login prompt"""
|
| 268 |
+
print("\n🧪 Test 8: Unauthenticated user experience")
|
| 269 |
+
|
| 270 |
+
# Navigate to chapter without login
|
| 271 |
+
page.goto(f"{BASE_URL}/docs/intro")
|
| 272 |
+
page.wait_for_load_state("networkidle")
|
| 273 |
+
|
| 274 |
+
# Check if translation button is disabled or shows auth prompt
|
| 275 |
+
translate_button = page.locator('button:has-text("Translate to Urdu")')
|
| 276 |
+
|
| 277 |
+
if translate_button.is_visible():
|
| 278 |
+
# Click button (should not translate)
|
| 279 |
+
translate_button.click()
|
| 280 |
+
page.wait_for_timeout(1000)
|
| 281 |
+
|
| 282 |
+
# Should see authentication prompt or redirect to signup
|
| 283 |
+
auth_prompt = page.locator('text=Login required, text=Sign up, text=login')
|
| 284 |
+
if auth_prompt.is_visible(timeout=2000):
|
| 285 |
+
print("✅ Authentication prompt shown to guest users")
|
| 286 |
+
elif "signup" in page.url.lower() or "login" in page.url.lower():
|
| 287 |
+
print("✅ Redirected to signup/login page")
|
| 288 |
+
else:
|
| 289 |
+
print("⚠️ Authentication handling unclear")
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
# Run tests with: pytest backend/tests/e2e/test_translation_flow.py -v -s
|
| 293 |
+
# Note: Requires backend and frontend servers running:
|
| 294 |
+
# Terminal 1: cd backend && python3 main.py
|
| 295 |
+
# Terminal 2: cd frontend && npm start
|
|
@@ -1,126 +1,126 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Integration tests for Translation Feedback API endpoint
|
| 3 |
-
|
| 4 |
-
Tests:
|
| 5 |
-
- test_submit_feedback_authenticated: POST /api/translate/feedback with JWT
|
| 6 |
-
- test_submit_feedback_unauthenticated: POST without JWT returns 401
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import pytest
|
| 10 |
-
from fastapi.testclient import TestClient
|
| 11 |
-
from main import app
|
| 12 |
-
from auth.jwt_utils import create_jwt_token
|
| 13 |
-
import uuid
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
client = TestClient(app)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def test_submit_feedback_authenticated():
|
| 20 |
-
"""Test submitting feedback with valid JWT token"""
|
| 21 |
-
# Arrange
|
| 22 |
-
user_id = str(uuid.uuid4())
|
| 23 |
-
user_email = "test@example.com"
|
| 24 |
-
token = create_jwt_token(user_id, user_email)
|
| 25 |
-
|
| 26 |
-
translation_id = str(uuid.uuid4())
|
| 27 |
-
feedback_data = {
|
| 28 |
-
"translation_id": translation_id,
|
| 29 |
-
"issue_description": "Technical term 'ROS2' was incorrectly translated to Urdu"
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
# Act
|
| 33 |
-
response = client.post(
|
| 34 |
-
"/api/translate/feedback",
|
| 35 |
-
json=feedback_data,
|
| 36 |
-
headers={"Authorization": f"Bearer {token}"}
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
# Assert
|
| 40 |
-
assert response.status_code in [201, 200, 503], f"Expected 201/200/503, got {response.status_code}: {response.text}"
|
| 41 |
-
|
| 42 |
-
if response.status_code == 201:
|
| 43 |
-
data = response.json()
|
| 44 |
-
assert "feedback_id" in data or "id" in data
|
| 45 |
-
print(f"✅ Feedback submitted successfully: {data}")
|
| 46 |
-
elif response.status_code == 503:
|
| 47 |
-
print("⚠️ Database unavailable (mock mode) - endpoint exists but DB not connected")
|
| 48 |
-
else:
|
| 49 |
-
print(f"✅ Feedback endpoint exists: {response.status_code}")
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def test_submit_feedback_unauthenticated():
|
| 53 |
-
"""Test submitting feedback without JWT token returns 401"""
|
| 54 |
-
# Arrange
|
| 55 |
-
feedback_data = {
|
| 56 |
-
"translation_id": str(uuid.uuid4()),
|
| 57 |
-
"issue_description": "Translation issue"
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
# Act
|
| 61 |
-
response = client.post(
|
| 62 |
-
"/api/translate/feedback",
|
| 63 |
-
json=feedback_data
|
| 64 |
-
# No Authorization header
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
# Assert
|
| 68 |
-
assert response.status_code in [401, 404], f"Expected 401 or 404 (endpoint not implemented yet), got {response.status_code}"
|
| 69 |
-
|
| 70 |
-
if response.status_code == 401:
|
| 71 |
-
print("✅ Unauthenticated request properly rejected with 401")
|
| 72 |
-
else:
|
| 73 |
-
print("⚠️ Feedback endpoint not yet implemented (404)")
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def test_submit_feedback_missing_fields():
|
| 77 |
-
"""Test submitting feedback with missing required fields"""
|
| 78 |
-
# Arrange
|
| 79 |
-
user_id = str(uuid.uuid4())
|
| 80 |
-
user_email = "test@example.com"
|
| 81 |
-
token = create_jwt_token(user_id, user_email)
|
| 82 |
-
|
| 83 |
-
incomplete_data = {
|
| 84 |
-
"translation_id": str(uuid.uuid4())
|
| 85 |
-
# Missing issue_description
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
# Act
|
| 89 |
-
response = client.post(
|
| 90 |
-
"/api/translate/feedback",
|
| 91 |
-
json=incomplete_data,
|
| 92 |
-
headers={"Authorization": f"Bearer {token}"}
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
# Assert
|
| 96 |
-
assert response.status_code in [422, 400, 404], f"Expected 422/400/404, got {response.status_code}"
|
| 97 |
-
|
| 98 |
-
if response.status_code in [422, 400]:
|
| 99 |
-
print("✅ Validation error properly handled")
|
| 100 |
-
else:
|
| 101 |
-
print("⚠️ Endpoint not yet implemented")
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def test_submit_feedback_invalid_translation_id():
|
| 105 |
-
"""Test submitting feedback with non-existent translation_id"""
|
| 106 |
-
# Arrange
|
| 107 |
-
user_id = str(uuid.uuid4())
|
| 108 |
-
user_email = "test@example.com"
|
| 109 |
-
token = create_jwt_token(user_id, user_email)
|
| 110 |
-
|
| 111 |
-
feedback_data = {
|
| 112 |
-
"translation_id": str(uuid.uuid4()), # Non-existent translation
|
| 113 |
-
"issue_description": "Test issue"
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
# Act
|
| 117 |
-
response = client.post(
|
| 118 |
-
"/api/translate/feedback",
|
| 119 |
-
json=feedback_data,
|
| 120 |
-
headers={"Authorization": f"Bearer {token}"}
|
| 121 |
-
)
|
| 122 |
-
|
| 123 |
-
# Assert
|
| 124 |
-
# Should either accept (201) or reject with 404 if FK constraint enforced
|
| 125 |
-
assert response.status_code in [201, 404, 400, 503], f"Got {response.status_code}"
|
| 126 |
-
print(f"✅ Invalid translation_id handled: {response.status_code}")
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Integration tests for Translation Feedback API endpoint
|
| 3 |
+
|
| 4 |
+
Tests:
|
| 5 |
+
- test_submit_feedback_authenticated: POST /api/translate/feedback with JWT
|
| 6 |
+
- test_submit_feedback_unauthenticated: POST without JWT returns 401
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
from fastapi.testclient import TestClient
|
| 11 |
+
from main import app
|
| 12 |
+
from auth.jwt_utils import create_jwt_token
|
| 13 |
+
import uuid
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
client = TestClient(app)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_submit_feedback_authenticated():
|
| 20 |
+
"""Test submitting feedback with valid JWT token"""
|
| 21 |
+
# Arrange
|
| 22 |
+
user_id = str(uuid.uuid4())
|
| 23 |
+
user_email = "test@example.com"
|
| 24 |
+
token = create_jwt_token(user_id, user_email)
|
| 25 |
+
|
| 26 |
+
translation_id = str(uuid.uuid4())
|
| 27 |
+
feedback_data = {
|
| 28 |
+
"translation_id": translation_id,
|
| 29 |
+
"issue_description": "Technical term 'ROS2' was incorrectly translated to Urdu"
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# Act
|
| 33 |
+
response = client.post(
|
| 34 |
+
"/api/translate/feedback",
|
| 35 |
+
json=feedback_data,
|
| 36 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Assert
|
| 40 |
+
assert response.status_code in [201, 200, 503], f"Expected 201/200/503, got {response.status_code}: {response.text}"
|
| 41 |
+
|
| 42 |
+
if response.status_code == 201:
|
| 43 |
+
data = response.json()
|
| 44 |
+
assert "feedback_id" in data or "id" in data
|
| 45 |
+
print(f"✅ Feedback submitted successfully: {data}")
|
| 46 |
+
elif response.status_code == 503:
|
| 47 |
+
print("⚠️ Database unavailable (mock mode) - endpoint exists but DB not connected")
|
| 48 |
+
else:
|
| 49 |
+
print(f"✅ Feedback endpoint exists: {response.status_code}")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_submit_feedback_unauthenticated():
|
| 53 |
+
"""Test submitting feedback without JWT token returns 401"""
|
| 54 |
+
# Arrange
|
| 55 |
+
feedback_data = {
|
| 56 |
+
"translation_id": str(uuid.uuid4()),
|
| 57 |
+
"issue_description": "Translation issue"
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
# Act
|
| 61 |
+
response = client.post(
|
| 62 |
+
"/api/translate/feedback",
|
| 63 |
+
json=feedback_data
|
| 64 |
+
# No Authorization header
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Assert
|
| 68 |
+
assert response.status_code in [401, 404], f"Expected 401 or 404 (endpoint not implemented yet), got {response.status_code}"
|
| 69 |
+
|
| 70 |
+
if response.status_code == 401:
|
| 71 |
+
print("✅ Unauthenticated request properly rejected with 401")
|
| 72 |
+
else:
|
| 73 |
+
print("⚠️ Feedback endpoint not yet implemented (404)")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_submit_feedback_missing_fields():
|
| 77 |
+
"""Test submitting feedback with missing required fields"""
|
| 78 |
+
# Arrange
|
| 79 |
+
user_id = str(uuid.uuid4())
|
| 80 |
+
user_email = "test@example.com"
|
| 81 |
+
token = create_jwt_token(user_id, user_email)
|
| 82 |
+
|
| 83 |
+
incomplete_data = {
|
| 84 |
+
"translation_id": str(uuid.uuid4())
|
| 85 |
+
# Missing issue_description
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# Act
|
| 89 |
+
response = client.post(
|
| 90 |
+
"/api/translate/feedback",
|
| 91 |
+
json=incomplete_data,
|
| 92 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Assert
|
| 96 |
+
assert response.status_code in [422, 400, 404], f"Expected 422/400/404, got {response.status_code}"
|
| 97 |
+
|
| 98 |
+
if response.status_code in [422, 400]:
|
| 99 |
+
print("✅ Validation error properly handled")
|
| 100 |
+
else:
|
| 101 |
+
print("⚠️ Endpoint not yet implemented")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def test_submit_feedback_invalid_translation_id():
|
| 105 |
+
"""Test submitting feedback with non-existent translation_id"""
|
| 106 |
+
# Arrange
|
| 107 |
+
user_id = str(uuid.uuid4())
|
| 108 |
+
user_email = "test@example.com"
|
| 109 |
+
token = create_jwt_token(user_id, user_email)
|
| 110 |
+
|
| 111 |
+
feedback_data = {
|
| 112 |
+
"translation_id": str(uuid.uuid4()), # Non-existent translation
|
| 113 |
+
"issue_description": "Test issue"
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
# Act
|
| 117 |
+
response = client.post(
|
| 118 |
+
"/api/translate/feedback",
|
| 119 |
+
json=feedback_data,
|
| 120 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Assert
|
| 124 |
+
# Should either accept (201) or reject with 404 if FK constraint enforced
|
| 125 |
+
assert response.status_code in [201, 404, 400, 503], f"Got {response.status_code}"
|
| 126 |
+
print(f"✅ Invalid translation_id handled: {response.status_code}")
|
|
@@ -1,81 +1,81 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for TranslationFeedback model
|
| 3 |
-
|
| 4 |
-
Tests:
|
| 5 |
-
- test_create_translation_feedback: Verify feedback creation
|
| 6 |
-
- test_feedback_foreign_key_constraint: Verify FK relationship to translations
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import pytest
|
| 10 |
-
from models.translation_feedback import TranslationFeedback
|
| 11 |
-
from models.translation import Translation
|
| 12 |
-
from database.models import User
|
| 13 |
-
from database.db import get_db_session
|
| 14 |
-
import uuid
|
| 15 |
-
from datetime import datetime
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
@pytest.mark.asyncio
|
| 19 |
-
async def test_create_translation_feedback():
|
| 20 |
-
"""Test creating a translation feedback record"""
|
| 21 |
-
# Arrange
|
| 22 |
-
feedback_id = uuid.uuid4()
|
| 23 |
-
translation_id = uuid.uuid4()
|
| 24 |
-
user_id = uuid.uuid4()
|
| 25 |
-
issue_description = "Technical term 'API' was incorrectly translated to Urdu"
|
| 26 |
-
|
| 27 |
-
# Act
|
| 28 |
-
feedback = TranslationFeedback(
|
| 29 |
-
id=feedback_id,
|
| 30 |
-
translation_id=translation_id,
|
| 31 |
-
user_id=user_id,
|
| 32 |
-
issue_description=issue_description,
|
| 33 |
-
created_at=datetime.utcnow()
|
| 34 |
-
)
|
| 35 |
-
|
| 36 |
-
# Assert
|
| 37 |
-
assert feedback.id == feedback_id
|
| 38 |
-
assert feedback.translation_id == translation_id
|
| 39 |
-
assert feedback.user_id == user_id
|
| 40 |
-
assert feedback.issue_description == issue_description
|
| 41 |
-
assert feedback.created_at is not None
|
| 42 |
-
print(f"✅ TranslationFeedback model created: {feedback}")
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
@pytest.mark.asyncio
|
| 46 |
-
async def test_feedback_foreign_key_constraint():
|
| 47 |
-
"""Test that feedback links to translation record via FK"""
|
| 48 |
-
# Arrange
|
| 49 |
-
translation_id = uuid.uuid4()
|
| 50 |
-
user_id = uuid.uuid4()
|
| 51 |
-
|
| 52 |
-
# Act
|
| 53 |
-
feedback = TranslationFeedback(
|
| 54 |
-
id=uuid.uuid4(),
|
| 55 |
-
translation_id=translation_id, # FK to translations table
|
| 56 |
-
user_id=user_id, # FK to users table
|
| 57 |
-
issue_description="Translation quality issue",
|
| 58 |
-
created_at=datetime.utcnow()
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
# Assert
|
| 62 |
-
assert feedback.translation_id == translation_id
|
| 63 |
-
assert hasattr(feedback, 'translation_id'), "Feedback must have translation_id FK"
|
| 64 |
-
assert hasattr(feedback, 'user_id'), "Feedback must have user_id FK"
|
| 65 |
-
print(f"✅ Foreign key constraint verified: translation_id={translation_id}")
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def test_feedback_model_repr():
|
| 69 |
-
"""Test __repr__ method for debugging"""
|
| 70 |
-
feedback = TranslationFeedback(
|
| 71 |
-
id=uuid.uuid4(),
|
| 72 |
-
translation_id=uuid.uuid4(),
|
| 73 |
-
user_id=uuid.uuid4(),
|
| 74 |
-
issue_description="Test issue",
|
| 75 |
-
created_at=datetime.utcnow()
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
repr_str = repr(feedback)
|
| 79 |
-
assert "TranslationFeedback" in repr_str
|
| 80 |
-
assert "issue_description" in repr_str or "translation_id" in repr_str
|
| 81 |
-
print(f"✅ Model repr: {repr_str}")
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit tests for TranslationFeedback model
|
| 3 |
+
|
| 4 |
+
Tests:
|
| 5 |
+
- test_create_translation_feedback: Verify feedback creation
|
| 6 |
+
- test_feedback_foreign_key_constraint: Verify FK relationship to translations
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
from models.translation_feedback import TranslationFeedback
|
| 11 |
+
from models.translation import Translation
|
| 12 |
+
from database.models import User
|
| 13 |
+
from database.db import get_db_session
|
| 14 |
+
import uuid
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@pytest.mark.asyncio
|
| 19 |
+
async def test_create_translation_feedback():
|
| 20 |
+
"""Test creating a translation feedback record"""
|
| 21 |
+
# Arrange
|
| 22 |
+
feedback_id = uuid.uuid4()
|
| 23 |
+
translation_id = uuid.uuid4()
|
| 24 |
+
user_id = uuid.uuid4()
|
| 25 |
+
issue_description = "Technical term 'API' was incorrectly translated to Urdu"
|
| 26 |
+
|
| 27 |
+
# Act
|
| 28 |
+
feedback = TranslationFeedback(
|
| 29 |
+
id=feedback_id,
|
| 30 |
+
translation_id=translation_id,
|
| 31 |
+
user_id=user_id,
|
| 32 |
+
issue_description=issue_description,
|
| 33 |
+
created_at=datetime.utcnow()
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Assert
|
| 37 |
+
assert feedback.id == feedback_id
|
| 38 |
+
assert feedback.translation_id == translation_id
|
| 39 |
+
assert feedback.user_id == user_id
|
| 40 |
+
assert feedback.issue_description == issue_description
|
| 41 |
+
assert feedback.created_at is not None
|
| 42 |
+
print(f"✅ TranslationFeedback model created: {feedback}")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@pytest.mark.asyncio
|
| 46 |
+
async def test_feedback_foreign_key_constraint():
|
| 47 |
+
"""Test that feedback links to translation record via FK"""
|
| 48 |
+
# Arrange
|
| 49 |
+
translation_id = uuid.uuid4()
|
| 50 |
+
user_id = uuid.uuid4()
|
| 51 |
+
|
| 52 |
+
# Act
|
| 53 |
+
feedback = TranslationFeedback(
|
| 54 |
+
id=uuid.uuid4(),
|
| 55 |
+
translation_id=translation_id, # FK to translations table
|
| 56 |
+
user_id=user_id, # FK to users table
|
| 57 |
+
issue_description="Translation quality issue",
|
| 58 |
+
created_at=datetime.utcnow()
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Assert
|
| 62 |
+
assert feedback.translation_id == translation_id
|
| 63 |
+
assert hasattr(feedback, 'translation_id'), "Feedback must have translation_id FK"
|
| 64 |
+
assert hasattr(feedback, 'user_id'), "Feedback must have user_id FK"
|
| 65 |
+
print(f"✅ Foreign key constraint verified: translation_id={translation_id}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def test_feedback_model_repr():
|
| 69 |
+
"""Test __repr__ method for debugging"""
|
| 70 |
+
feedback = TranslationFeedback(
|
| 71 |
+
id=uuid.uuid4(),
|
| 72 |
+
translation_id=uuid.uuid4(),
|
| 73 |
+
user_id=uuid.uuid4(),
|
| 74 |
+
issue_description="Test issue",
|
| 75 |
+
created_at=datetime.utcnow()
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
repr_str = repr(feedback)
|
| 79 |
+
assert "TranslationFeedback" in repr_str
|
| 80 |
+
assert "issue_description" in repr_str or "translation_id" in repr_str
|
| 81 |
+
print(f"✅ Model repr: {repr_str}")
|