qa-and-rag-ai-assistant/app.py
2026-01-13 15:38:27 +03:00

233 lines
7.6 KiB
Python

from typing import List, Optional
from pydantic import BaseModel, EmailStr
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Depends
from fastapi.responses import RedirectResponse
from minio import Minio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
import os
import uuid
from config import Config
from database.database import get_db, init_db, User, Profile as DBProfile
from routers import profile, vacancies, chat
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000")
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin")
MINIO_BUCKET = os.getenv("MINIO_BUCKET", "resumes")
minio_client = None
def get_minio_client():
global minio_client
if minio_client is None:
try:
minio_client = Minio(
MINIO_ENDPOINT,
access_key=MINIO_ACCESS_KEY,
secret_key=MINIO_SECRET_KEY,
secure=False
)
if not minio_client.bucket_exists(MINIO_BUCKET):
minio_client.make_bucket(MINIO_BUCKET)
print(f"MinIO connected successfully to {MINIO_ENDPOINT}")
except Exception as e:
print(f"Warning: Could not connect to MinIO at {MINIO_ENDPOINT}: {e}")
print("MinIO features will be disabled. Start MinIO to enable file storage.")
minio_client = False
return minio_client if minio_client else None
class Profile(BaseModel):
name: str
email: EmailStr
position: str
competencies: Optional[str] = None
experience: Optional[str] = None
skills: Optional[str] = None
country: Optional[str] = None
languages: Optional[str] = None
employment_format: Optional[str] = None
rate: Optional[str] = None
relocation: Optional[str] = None
cv_url: Optional[str] = None
class Vacancy(BaseModel):
title: str
salary_range: str
employment_format: str
skills: str
link: str
vacancies_db = [
Vacancy(title="Python Developer", salary_range="2000-4000 USD", employment_format="remote", skills="Python, FastAPI", link="https://example.com/vacancy/1"),
Vacancy(title="Frontend Engineer", salary_range="1500-3000 USD", employment_format="office", skills="React, TypeScript", link="https://example.com/vacancy/2")
]
app = FastAPI()
app.include_router(profile.router)
app.include_router(vacancies.router)
app.include_router(chat.router)
@app.on_event("startup")
async def startup_event():
await init_db()
print("FastAPI application started successfully")
@app.post("/profile")
async def save_profile(
name: str = Form(...),
email: EmailStr = Form(...),
position: str = Form(...),
competencies: Optional[str] = Form(None),
experience: Optional[str] = Form(None),
skills: Optional[str] = Form(None),
country: Optional[str] = Form(None),
languages: Optional[str] = Form(None),
employment_format: Optional[str] = Form(None),
rate: Optional[str] = Form(None),
relocation: Optional[str] = Form(None),
cv: UploadFile = File(...),
db: AsyncSession = Depends(get_db)
):
if cv.content_type != "application/pdf":
raise HTTPException(status_code=400, detail="CV must be a PDF file")
client = get_minio_client()
if not client:
raise HTTPException(status_code=503, detail="File storage (MinIO) is not available. Please start MinIO service.")
file_id = f"{email.replace('@','_')}_{cv.filename}"
client.put_object(
MINIO_BUCKET,
file_id,
cv.file,
length=-1,
part_size=10*1024*1024,
content_type="application/pdf"
)
cv_url = f"/profile/cv/{file_id}"
result = await db.execute(select(DBProfile).where(DBProfile.email == email))
existing_profile = result.scalar_one_or_none()
if existing_profile:
existing_profile.name = name
existing_profile.position = position
existing_profile.competencies = competencies
existing_profile.experience = experience
existing_profile.skills = skills
existing_profile.country = country
existing_profile.languages = languages
existing_profile.employment_format = employment_format
existing_profile.rate = rate
existing_profile.relocation = relocation
existing_profile.cv_url = cv_url
else:
new_profile = DBProfile(
name=name,
email=email,
position=position,
competencies=competencies,
experience=experience,
skills=skills,
country=country,
languages=languages,
employment_format=employment_format,
rate=rate,
relocation=relocation,
cv_url=cv_url
)
db.add(new_profile)
await db.commit()
return {"message": "Profile saved", "cv_url": cv_url}
@app.get("/profile/{email}", response_model=Profile)
async def get_profile(email: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(DBProfile).where(DBProfile.email == email))
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
return Profile(
name=profile.name,
email=profile.email,
position=profile.position,
competencies=profile.competencies,
experience=profile.experience,
skills=profile.skills,
country=profile.country,
languages=profile.languages,
employment_format=profile.employment_format,
rate=profile.rate,
relocation=profile.relocation,
cv_url=profile.cv_url
)
@app.get("/database/tokens")
async def get_tokens(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User))
users = result.scalars().all()
return [{"token": user.token, "status": user.status, "username": user.username} for user in users]
@app.get("/profile/cv/{file_id}")
async def download_cv(file_id: str):
client = get_minio_client()
if not client:
raise HTTPException(status_code=503, detail="File storage (MinIO) is not available. Please start MinIO service.")
try:
response = client.get_object(MINIO_BUCKET, file_id)
return RedirectResponse(response.geturl())
except Exception:
raise HTTPException(status_code=404, detail="CV not found")
@app.get("/vacancies", response_model=List[Vacancy])
async def get_vacancies():
return vacancies_db
@app.get("/login")
async def login_page(db: AsyncSession = Depends(get_db)):
auth_token = str(uuid.uuid4())
bot_name = Config.BOT_NAME
new_user = User(
telegram_id=None,
token=auth_token,
username=None,
status="pending"
)
db.add(new_user)
await db.commit()
bot_url = f"https://t.me/{bot_name}?start={auth_token}"
return {
"message": "Нажмите на кнопку для регистрации",
"url": bot_url,
"check_status_url": f"/check-auth/{auth_token}"
}
@app.get("/check-auth/{token}")
async def check_auth(token: str, db: AsyncSession = Depends(get_db)):
"""Check if user has completed authentication via Telegram bot"""
result = await db.execute(select(User).where(User.token == token))
user = result.scalar_one_or_none()
if not user:
return {"authorized": False, "message": "Invalid token"}
if user.status == "success":
return {
"authorized": True,
"user_data": {
"telegram_id": user.telegram_id,
"username": user.username,
"status": user.status
}
}
return {"authorized": False, "message": "Authentication pending"}