Files changed (12) hide show
  1. .gitattributes +0 -35
  2. .python-version +1 -0
  3. README.md +1 -10
  4. app/db/database.py +4 -39
  5. app/main.py +58 -124
  6. app/security.py +10 -6
  7. db_migrate.py +25 -0
  8. main.py +6 -0
  9. pyproject.toml +23 -0
  10. requirements.txt +0 -1
  11. test_search.py +31 -0
  12. uv.lock +0 -0
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
README.md CHANGED
@@ -1,12 +1,3 @@
1
- ---
2
- title: Smart Reader Backend
3
- emoji: 🐨
4
- colorFrom: pink
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
  # Smart Reader Backend
11
 
12
  This is the FastAPI backend for the Smart Reader application, designed to be deployed on HuggingFace Spaces.
@@ -30,7 +21,7 @@ DATABASE_URL=sqlite:///./smart_reader.db
30
  ```
31
  2. Run the application:
32
  ```bash
33
- uvicorn app.main:app --reload --port 8080
34
  ```
35
 
36
  ## Docker
 
 
 
 
 
 
 
 
 
 
1
  # Smart Reader Backend
2
 
3
  This is the FastAPI backend for the Smart Reader application, designed to be deployed on HuggingFace Spaces.
 
21
  ```
22
  2. Run the application:
23
  ```bash
24
+ uvicorn main:app --reload --port 8080
25
  ```
26
 
27
  ## Docker
app/db/database.py CHANGED
@@ -1,44 +1,12 @@
1
- import os
2
  from sqlalchemy import create_engine
3
  from sqlalchemy.ext.declarative import declarative_base
4
  from sqlalchemy.orm import sessionmaker
5
 
6
- db_url = os.environ.get("DATABASE_URL", "").strip().strip("\"'")
7
-
8
- if db_url.startswith("postgres://"):
9
- db_url = db_url.replace("postgres://", "postgresql://", 1)
10
-
11
- if not db_url or db_url == "DATABASE_URL" or not (db_url.startswith("postgresql://") or db_url.startswith("sqlite://")):
12
- print("[Database Info] DATABASE_URL is missing or invalid. Using default local SQLite database.")
13
- SQLALCHEMY_DATABASE_URL = "sqlite:///./smart_reader.db"
14
- else:
15
- if "supabase" in db_url and "sslmode" not in db_url:
16
- if "?" in db_url:
17
- db_url += "&sslmode=require"
18
- else:
19
- db_url += "?sslmode=require"
20
- SQLALCHEMY_DATABASE_URL = db_url
21
-
22
- if SQLALCHEMY_DATABASE_URL.startswith("sqlite"):
23
- engine = create_engine(
24
- SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
25
- )
26
- else:
27
- try:
28
- temp_engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, connect_args={"connect_timeout": 5})
29
- with temp_engine.connect() as conn:
30
- pass
31
- engine = temp_engine
32
- masked = SQLALCHEMY_DATABASE_URL
33
- if "@" in masked:
34
- p = masked.split("@")
35
- masked = f"{p[0].split(':')[0]}:****@{p[1]}"
36
- print(f"[Database Info] CONNECTED TO REMOTE POSTGRESQL: {masked}")
37
- except Exception as e:
38
- print(f"[Database Warning] Could not connect to PostgreSQL ({e}). Falling back to local SQLite database.")
39
- SQLALCHEMY_DATABASE_URL = "sqlite:///./smart_reader.db"
40
- engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
41
 
 
 
 
42
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
43
 
44
  Base = declarative_base()
@@ -49,6 +17,3 @@ def get_db():
49
  yield db
50
  finally:
51
  db.close()
52
-
53
-
54
-
 
 
1
  from sqlalchemy import create_engine
2
  from sqlalchemy.ext.declarative import declarative_base
3
  from sqlalchemy.orm import sessionmaker
4
 
5
+ SQLALCHEMY_DATABASE_URL = "sqlite:///./smart_reader.db"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ engine = create_engine(
8
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
9
+ )
10
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
11
 
12
  Base = declarative_base()
 
17
  yield db
18
  finally:
19
  db.close()
 
 
 
app/main.py CHANGED
@@ -17,8 +17,8 @@ from app.services.scraper import scrape_article_url
17
  from app.security import get_password_hash, verify_password, create_access_token, get_current_user, ACCESS_TOKEN_EXPIRE_MINUTES
18
  from datetime import timedelta, datetime
19
  import random
20
- import requests
21
- import traceback
22
  from fastapi.security import OAuth2PasswordRequestForm
23
 
24
  # Load environment variables from .env file
@@ -27,21 +27,6 @@ load_dotenv()
27
  # Initialize database tables
28
  models.Base.metadata.create_all(bind=engine)
29
 
30
- # Auto-sync PostgreSQL primary key sequences if connected to Postgres
31
- if not str(engine.url).startswith("sqlite"):
32
- try:
33
- from sqlalchemy import text
34
- with engine.connect() as conn:
35
- for table_name in ["articles", "users", "comments", "comment_ratings"]:
36
- try:
37
- conn.execute(text(f"SELECT setval(pg_get_serial_sequence('{table_name}', 'id'), COALESCE((SELECT MAX(id) FROM {table_name}), 0) + 1, false);"))
38
- conn.commit()
39
- except Exception as seq_err:
40
- print(f"[Sequence Sync Warning] Could not sync sequence for {table_name}: {seq_err}")
41
- print("[Database Info] Primary key sequences synchronized successfully.")
42
- except Exception as e:
43
- print(f"[Database Warning] Sequence sync error: {e}")
44
-
45
  app = FastAPI(title="Smart Reader API", version="1.0.0")
46
 
47
  # --- CORS Middleware Configuration ---
@@ -141,53 +126,29 @@ def generate_ai_summary(content: str) -> str:
141
 
142
  # --- Auth Endpoints ---
143
 
144
- def send_verification_email(to_email: str, code: str) -> bool:
145
  # For development, just print the code.
146
  print(f"\n{'='*40}")
147
  print(f"VERIFICATION CODE FOR {to_email}: {code}")
148
  print(f"{'='*40}\n")
149
 
150
- api_key = os.environ.get("BREVO_API_KEY")
151
- if not api_key:
152
- print("[Brevo Email Error]: BREVO_API_KEY environment variable is not set.")
153
- return False
154
-
155
- url = "https://api.brevo.com/v3/smtp/email"
156
- headers = {
157
- "accept": "application/json",
158
- "api-key": api_key,
159
- "content-type": "application/json"
160
- }
161
- payload = {
162
- "sender": {
163
- "name": "Smart Reader",
164
- "email": "smartreadercommunity1@gmail.com"
165
- },
166
- "to": [
167
- {
168
- "email": to_email
169
- }
170
- ],
171
- "subject": "كود تفعيل حسابك - Smart Reader",
172
- "htmlContent": f"<div dir='rtl' style='font-family: Arial, sans-serif; padding: 25px; background-color: #f4f4f7;'><div style='max-width: 500px; margin: 0 auto; background: #ffffff; padding: 30px; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); text-align: center;'><h2 style='color: #4f46e5; margin-bottom: 10px;'>Smart Reader</h2><p style='color: #4b5563; font-size: 16px;'>كود التفعيل الخاص بحسابك هو:</p><div style='background: #eef2ff; border: 2px dashed #6366f1; border-radius: 12px; padding: 15px; margin: 20px 0;'><span style='color: #4338ca; font-size: 32px; font-weight: bold; letter-spacing: 6px;'>{code}</span></div><p style='color: #9ca3af; font-size: 14px;'>هذا الكود صالح لمدة 10 دقائق فقط.</p></div></div>"
173
- }
174
-
175
- try:
176
- response = requests.post(url, json=payload, headers=headers, timeout=15)
177
- if response.status_code in (200, 201, 202):
178
- print(f"[Brevo Email Success] Status Code: {response.status_code}")
179
- print(f"[Brevo Email Response Body]: {response.text}")
180
- return True
181
- else:
182
- print(f"[Brevo Email Failure] Status Code: {response.status_code}")
183
- print(f"[Brevo Email Response Body]: {response.text}")
184
- print(f"[Brevo Email Response Headers]: {dict(response.headers)}")
185
- print(f"[Brevo Email Exception Traceback]: None (HTTP Status {response.status_code})")
186
- return False
187
- except Exception as e:
188
- print(f"[Brevo Email Exception Failure]: {e}")
189
- print(f"[Brevo Email Exception Traceback]:\n{traceback.format_exc()}")
190
- return False
191
 
192
  @app.post("/api/v1/auth/request-verification")
193
  def request_verification(req: schemas.EmailVerificationRequest, db: Session = Depends(get_db)):
@@ -261,7 +222,7 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
261
 
262
  @app.get("/api/v1/articles", response_model=List[schemas.ArticleResponse])
263
  def get_articles(db: Session = Depends(get_db)):
264
- articles = db.query(models.Article).order_by(models.Article.id.desc()).all()
265
  return articles
266
 
267
  @app.get("/api/v1/ai/articles/{article_id}/summary")
@@ -331,56 +292,30 @@ def rate_comment(comment_id: int, rating_in: schemas.CommentRatingCreate, db: Se
331
  return comment
332
 
333
 
334
- @app.delete("/api/v1/comments/{comment_id}")
335
- def delete_comment(comment_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)):
336
- comment = db.query(models.Comment).filter(models.Comment.id == comment_id).first()
337
- if not comment:
338
- raise HTTPException(status_code=404, detail="التعليق غير موجود")
339
-
340
- if comment.user_id != current_user.id:
341
- raise HTTPException(status_code=403, detail="غير مصرح لك بحذف هذا التعليق")
342
-
343
- # Delete associated ratings
344
- db.query(models.CommentRating).filter(models.CommentRating.comment_id == comment_id).delete()
345
- db.delete(comment)
346
- db.commit()
347
- return {"message": "تم حذف التعليق بنجاح", "id": comment_id}
348
-
349
-
350
  # --- 🚀 Upgraded Scraping Endpoint ---
351
  @app.post("/api/v1/articles/scrape", response_model=schemas.ArticleResponse)
352
  def scrape_and_add_article(req: schemas.ScrapeRequest, db: Session = Depends(get_db)):
353
- print(f"[Scrape] Starting scrape for URL: {req.url}")
354
- try:
355
- scraped_data = scrape_article_url(req.url)
356
- print(f"[Scrape] Result - title: {scraped_data.get('title')}, category: {scraped_data.get('category')}, content_len: {len(scraped_data.get('content',''))}")
357
- except Exception as e:
358
- print(f"[Scrape] Exception during scraping: {e}\n{traceback.format_exc()}")
359
- raise HTTPException(status_code=500, detail=f"خطأ في جلب المقال: {str(e)}")
360
-
361
- if scraped_data.get("category") == "خطأ":
362
- print(f"[Scrape] Scraping failed, returning 422")
363
- raise HTTPException(status_code=422, detail=scraped_data.get("content", "فشل جلب المقال"))
364
-
365
- try:
366
- print(f"[Scrape] Saving to DB...")
367
- db_article = models.Article(
368
- title=scraped_data["title"][:255],
369
- content=scraped_data["content"],
370
- category=scraped_data["category"][:100],
371
- author=scraped_data["author"][:100],
372
- image=(scraped_data["image"] or "")[:500],
373
- summary=""
374
- )
375
- db.add(db_article)
376
- db.commit()
377
- db.refresh(db_article)
378
- print(f"[Scrape] Saved successfully, id={db_article.id}")
379
- return db_article
380
- except Exception as e:
381
- print(f"[Scrape] DB error: {e}\n{traceback.format_exc()}")
382
- db.rollback()
383
- raise HTTPException(status_code=500, detail=f"خطأ في حفظ المقال: {str(e)}")
384
 
385
  # --- 📁 File Upload Endpoint ---
386
  import io
@@ -418,7 +353,11 @@ async def upload_document(file: UploadFile = File(...), db: Session = Depends(ge
418
  raise HTTPException(status_code=400, detail="لم نتمكن من قراءة أي نص من الملف")
419
 
420
  # Add to database as article
 
 
 
421
  db_article = models.Article(
 
422
  title=title,
423
  content=content_text.strip(),
424
  category="مستند مرفوع",
@@ -436,8 +375,6 @@ async def upload_document(file: UploadFile = File(...), db: Session = Depends(ge
436
  except Exception as e:
437
  raise HTTPException(status_code=500, detail=f"حدث خطأ أثناء معالجة الملف: {str(e)}")
438
 
439
- from fastapi.responses import StreamingResponse
440
-
441
  # --- 🚀 Upgraded Chatbot (RAG) Endpoint ---
442
  @app.post("/api/v1/ai/articles/{article_id}/chat")
443
  def chat_with_article(article_id: int, chat_req: schemas.ChatRequest, db: Session = Depends(get_db)):
@@ -447,16 +384,16 @@ def chat_with_article(article_id: int, chat_req: schemas.ChatRequest, db: Sessio
447
 
448
  api_key = os.environ.get("GROQ_API_KEY")
449
  if not api_key or not api_key.strip():
 
450
  user_msg = chat_req.message.lower()
451
  if any(greeting in user_msg for greeting in ["مرحباً", "hello", "hi", "سلام", "هلا"]):
452
- reply_text = "أهلاً بك! أنا مساعدك الذكي لقراءة وتلخيص هذا المقال. كيف يمكنني مساعدتك اليوم؟"
453
- elif any(kw in user_msg for kw in ["لخص", "ملخص", "summary", "summarize"]):
454
- summary_text = article.summary if article.summary else generate_ai_summary(article.content)
455
- reply_text = f"إليك ملخص سريع للمقال:\n{summary_text}"
456
- else:
457
- reply_text = "أهلاً بك! لم يتم ضبط مفتاح Groq API Key في ملف `.env` بعد. يمكنك الحصول عليه مجاناً من https://console.groq.com"
458
 
459
- return {"reply": reply_text}
 
 
 
 
460
 
461
  try:
462
  from groq import Groq
@@ -473,23 +410,20 @@ def chat_with_article(article_id: int, chat_req: schemas.ChatRequest, db: Sessio
473
  المحتوى:
474
  {article.content[:4000]}"""
475
 
476
- # Build messages safely (filtering out empty messages)
477
  messages = [{"role": "system", "content": system_prompt}]
478
  if chat_req.history:
479
  for h in chat_req.history:
480
- if h.content and h.content.strip():
481
- role = "user" if h.role == "user" else "assistant"
482
- messages.append({"role": role, "content": h.content.strip()})
483
- messages.append({"role": "user", "content": chat_req.message.strip()})
484
 
485
  response = client.chat.completions.create(
486
  model="llama-3.3-70b-versatile",
487
  messages=messages,
488
  max_tokens=1024,
489
  )
490
- reply_text = response.choices[0].message.content.strip()
491
- return {"reply": reply_text}
492
-
493
  except Exception as e:
494
  print(f"Groq Chat error: {e}")
495
  return {"reply": f"حدث خطأ أثناء الاتصال بالذكاء الاصطناعي: {str(e)}"}
 
17
  from app.security import get_password_hash, verify_password, create_access_token, get_current_user, ACCESS_TOKEN_EXPIRE_MINUTES
18
  from datetime import timedelta, datetime
19
  import random
20
+ import smtplib
21
+ from email.mime.text import MIMEText
22
  from fastapi.security import OAuth2PasswordRequestForm
23
 
24
  # Load environment variables from .env file
 
27
  # Initialize database tables
28
  models.Base.metadata.create_all(bind=engine)
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  app = FastAPI(title="Smart Reader API", version="1.0.0")
31
 
32
  # --- CORS Middleware Configuration ---
 
126
 
127
  # --- Auth Endpoints ---
128
 
129
+ def send_verification_email(to_email: str, code: str):
130
  # For development, just print the code.
131
  print(f"\n{'='*40}")
132
  print(f"VERIFICATION CODE FOR {to_email}: {code}")
133
  print(f"{'='*40}\n")
134
 
135
+ # Real email sending logic (needs valid SMTP credentials in .env)
136
+ sender_email = os.environ.get("SMTP_EMAIL")
137
+ sender_password = os.environ.get("SMTP_PASSWORD")
138
+
139
+ if sender_email and sender_password:
140
+ try:
141
+ msg = MIMEText(f"كود التفعيل الخاص بك هو: {code}\n\nهذا الكود صالح لمدة 10 دقائق فقط.")
142
+ msg['Subject'] = 'كود تفعيل حسابك - Smart Reader'
143
+ msg['From'] = sender_email
144
+ msg['To'] = to_email
145
+
146
+ server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
147
+ server.login(sender_email, sender_password)
148
+ server.send_message(msg)
149
+ server.quit()
150
+ except Exception as e:
151
+ print(f"Error sending email: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  @app.post("/api/v1/auth/request-verification")
154
  def request_verification(req: schemas.EmailVerificationRequest, db: Session = Depends(get_db)):
 
222
 
223
  @app.get("/api/v1/articles", response_model=List[schemas.ArticleResponse])
224
  def get_articles(db: Session = Depends(get_db)):
225
+ articles = db.query(models.Article).all()
226
  return articles
227
 
228
  @app.get("/api/v1/ai/articles/{article_id}/summary")
 
292
  return comment
293
 
294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  # --- 🚀 Upgraded Scraping Endpoint ---
296
  @app.post("/api/v1/articles/scrape", response_model=schemas.ArticleResponse)
297
  def scrape_and_add_article(req: schemas.ScrapeRequest, db: Session = Depends(get_db)):
298
+ scraped_data = scrape_article_url(req.url)
299
+
300
+ # Auto-generate next ID
301
+ max_id_article = db.query(models.Article).order_by(models.Article.id.desc()).first()
302
+ next_id = (max_id_article.id + 1) if max_id_article else 1
303
+
304
+ db_article = models.Article(
305
+ id=next_id,
306
+ title=scraped_data["title"],
307
+ content=scraped_data["content"],
308
+ category=scraped_data["category"],
309
+ author=scraped_data["author"],
310
+ image=scraped_data["image"],
311
+ summary="" # Generate on demand
312
+ )
313
+
314
+ db.add(db_article)
315
+ db.commit()
316
+ db.refresh(db_article)
317
+
318
+ return db_article
 
 
 
 
 
 
 
 
 
 
319
 
320
  # --- 📁 File Upload Endpoint ---
321
  import io
 
353
  raise HTTPException(status_code=400, detail="لم نتمكن من قراءة أي نص من الملف")
354
 
355
  # Add to database as article
356
+ max_id_article = db.query(models.Article).order_by(models.Article.id.desc()).first()
357
+ next_id = (max_id_article.id + 1) if max_id_article else 1
358
+
359
  db_article = models.Article(
360
+ id=next_id,
361
  title=title,
362
  content=content_text.strip(),
363
  category="مستند مرفوع",
 
375
  except Exception as e:
376
  raise HTTPException(status_code=500, detail=f"حدث خطأ أثناء معالجة الملف: {str(e)}")
377
 
 
 
378
  # --- 🚀 Upgraded Chatbot (RAG) Endpoint ---
379
  @app.post("/api/v1/ai/articles/{article_id}/chat")
380
  def chat_with_article(article_id: int, chat_req: schemas.ChatRequest, db: Session = Depends(get_db)):
 
384
 
385
  api_key = os.environ.get("GROQ_API_KEY")
386
  if not api_key or not api_key.strip():
387
+ # Fallback offline replies
388
  user_msg = chat_req.message.lower()
389
  if any(greeting in user_msg for greeting in ["مرحباً", "hello", "hi", "سلام", "هلا"]):
390
+ return {"reply": "أهلاً بك! أنا مساعدك الذكي لقراءة وتلخيص هذا المقال. كيف يمكنني مساعدتك اليوم؟"}
 
 
 
 
 
391
 
392
+ if any(kw in user_msg for kw in ["لخص", "ملخص", "summary", "summarize"]):
393
+ summary_text = article.summary if article.summary else generate_ai_summary(article.content)
394
+ return {"reply": f"إليك ملخص سريع للمقال:\n{summary_text}"}
395
+
396
+ return {"reply": f"أهلاً بك! لم يتم ضبط مفتاح Groq API Key في ملف `.env` بعد. يمكنك الحصول عليه مجاناً من https://console.groq.com"}
397
 
398
  try:
399
  from groq import Groq
 
410
  المحتوى:
411
  {article.content[:4000]}"""
412
 
413
+ # Build messages from history
414
  messages = [{"role": "system", "content": system_prompt}]
415
  if chat_req.history:
416
  for h in chat_req.history:
417
+ role = "user" if h.role == "user" else "assistant"
418
+ messages.append({"role": role, "content": h.content})
419
+ messages.append({"role": "user", "content": chat_req.message})
 
420
 
421
  response = client.chat.completions.create(
422
  model="llama-3.3-70b-versatile",
423
  messages=messages,
424
  max_tokens=1024,
425
  )
426
+ return {"reply": response.choices[0].message.content.strip()}
 
 
427
  except Exception as e:
428
  print(f"Groq Chat error: {e}")
429
  return {"reply": f"حدث خطأ أثناء الاتصال بالذكاء الاصطناعي: {str(e)}"}
app/security.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  from datetime import datetime, timedelta
3
  from typing import Optional
4
  import jwt
5
- from passlib.context import CryptContext
6
  from fastapi import Depends, HTTPException, status
7
  from fastapi.security import OAuth2PasswordBearer
8
  from sqlalchemy.orm import Session
@@ -10,7 +10,7 @@ from app.db.database import get_db
10
  from app.models import models
11
 
12
  # Password hashing setup
13
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
14
 
15
  # JWT Setup
16
  SECRET_KEY = os.environ.get("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
@@ -19,11 +19,15 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
19
 
20
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
21
 
22
- def verify_password(plain_password, hashed_password):
23
- return pwd_context.verify(plain_password, hashed_password)
 
 
 
24
 
25
- def get_password_hash(password):
26
- return pwd_context.hash(password)
 
27
 
28
  def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
29
  to_encode = data.copy()
 
2
  from datetime import datetime, timedelta
3
  from typing import Optional
4
  import jwt
5
+ import bcrypt
6
  from fastapi import Depends, HTTPException, status
7
  from fastapi.security import OAuth2PasswordBearer
8
  from sqlalchemy.orm import Session
 
10
  from app.models import models
11
 
12
  # Password hashing setup
13
+ # We use bcrypt directly to bypass a known incompatibility between passlib and bcrypt >= 4.0.0
14
 
15
  # JWT Setup
16
  SECRET_KEY = os.environ.get("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
 
19
 
20
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
21
 
22
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
23
+ try:
24
+ return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
25
+ except Exception:
26
+ return False
27
 
28
+ def get_password_hash(password: str) -> str:
29
+ salt = bcrypt.gensalt()
30
+ return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
31
 
32
  def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
33
  to_encode = data.copy()
db_migrate.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+
3
+ def run_migration():
4
+ conn = sqlite3.connect('smart_reader.db')
5
+ cursor = conn.cursor()
6
+
7
+ # Check if 'email' column exists in 'users' table
8
+ cursor.execute("PRAGMA table_info(users)")
9
+ columns = [col[1] for col in cursor.fetchall()]
10
+
11
+ if 'email' not in columns:
12
+ print("Adding 'email' column to users table...")
13
+ cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(100)")
14
+ cursor.execute("CREATE UNIQUE INDEX ix_users_email ON users (email)")
15
+
16
+ if 'hashed_password' not in columns:
17
+ print("Adding 'hashed_password' column to users table...")
18
+ cursor.execute("ALTER TABLE users ADD COLUMN hashed_password VARCHAR(255)")
19
+
20
+ conn.commit()
21
+ conn.close()
22
+ print("Migration finished successfully.")
23
+
24
+ if __name__ == "__main__":
25
+ run_migration()
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from smart-reader-backend!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyproject.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "smart-reader-backend"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "fastapi>=0.115.8",
9
+ "uvicorn>=0.34.0",
10
+ "sqlalchemy>=2.0.38",
11
+ "pydantic>=2.10.6",
12
+ "google-generativeai>=0.8.4",
13
+ "beautifulsoup4>=4.12.3",
14
+ "python-dotenv>=1.0.1",
15
+ "requests>=2.32.3",
16
+ "groq>=0.13.1",
17
+ "python-multipart>=0.0.20",
18
+ "PyPDF2>=3.0.1",
19
+ "python-docx>=1.1.2",
20
+ "passlib>=1.7.4",
21
+ "bcrypt>=4.0.1",
22
+ "PyJWT>=2.8.0",
23
+ ]
requirements.txt CHANGED
@@ -13,4 +13,3 @@ python-docx==1.1.2
13
  passlib==1.7.4
14
  bcrypt==4.0.1
15
  PyJWT==2.8.0
16
- psycopg2-binary==2.9.10
 
13
  passlib==1.7.4
14
  bcrypt==4.0.1
15
  PyJWT==2.8.0
 
test_search.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import urllib.request
2
+ import urllib.parse
3
+ import json
4
+ import re
5
+
6
+ q = "محمد نجيب"
7
+ max_results = 9
8
+
9
+ is_arabic = bool(re.search(r'[\u0600-\u06FF]', q))
10
+ lang = "ar" if is_arabic else "en"
11
+
12
+ url = f"https://{lang}.wikipedia.org/w/api.php?action=query&list=search&srsearch={urllib.parse.quote(q)}&utf8=&format=json&srlimit={max_results}"
13
+ req = urllib.request.Request(url, headers={'User-Agent': 'SmartReader/1.0'})
14
+
15
+ with urllib.request.urlopen(req, timeout=10) as response:
16
+ data = json.loads(response.read().decode('utf-8'))
17
+
18
+ results = []
19
+ for item in data.get('query', {}).get('search', []):
20
+ title = item.get('title', '')
21
+ snippet_html = item.get('snippet', '')
22
+ snippet = re.sub(r'<[^>]+>', '', snippet_html)
23
+ article_url = f"https://{lang}.wikipedia.org/wiki/{urllib.parse.quote(title.replace(' ', '_'))}"
24
+ results.append({
25
+ "title": title,
26
+ "url": article_url,
27
+ "snippet": snippet,
28
+ "source": "ويكيبيديا" if is_arabic else "Wikipedia"
29
+ })
30
+
31
+ print(json.dumps(results, indent=2, ensure_ascii=False))
uv.lock ADDED
The diff for this file is too large to render. See raw diff