Mark-Lasfar commited on
Commit
7f03ffe
·
1 Parent(s): 361e98a

Fix ChunkedIteratorResult in SQLAlchemyUserDatabase and toggleBtn null error

Browse files
Files changed (6) hide show
  1. api/auth.py +7 -26
  2. api/database.py +3 -3
  3. api/endpoints.py +41 -7
  4. api/models.py +2 -0
  5. api/user_db.py +51 -0
  6. utils/web_search.py +3 -8
api/auth.py CHANGED
@@ -1,24 +1,26 @@
 
1
  # SPDX-FileCopyrightText: Hadad <[email protected]>
2
  # SPDX-License-Identifier: Apache-2.0
3
 
4
  from fastapi_users import FastAPIUsers
5
  from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
6
- from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
7
  from httpx_oauth.clients.google import GoogleOAuth2
8
  from httpx_oauth.clients.github import GitHubOAuth2
9
- from fastapi_users.router.oauth import get_oauth_router
10
- from api.database import User, OAuthAccount, get_user_db
11
- from api.models import UserRead, UserCreate, UserUpdate
12
  from fastapi_users.manager import BaseUserManager, IntegerIDMixin
13
  from fastapi import Depends, Request, FastAPI
14
  from sqlalchemy.ext.asyncio import AsyncSession
15
  from sqlalchemy import select
16
  from fastapi_users.models import UP
17
- from typing import Optional, Dict, Any
18
  import os
19
  import logging
20
  import secrets
21
 
 
 
 
 
22
  # إعداد اللوقينج
23
  logger = logging.getLogger(__name__)
24
 
@@ -55,27 +57,6 @@ GITHUB_REDIRECT_URL = os.getenv("GITHUB_REDIRECT_URL", "https://mgzon-mgzon-app.
55
  google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
56
  github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
57
 
58
- # قاعدة بيانات المستخدم
59
- class CustomSQLAlchemyUserDatabase(SQLAlchemyUserDatabase):
60
- def parse_id(self, value: Any) -> int:
61
- """تحويل الـ ID من string إلى int لتوافق JWTStrategy"""
62
- logger.debug(f"Parsing ID: {value} (type: {type(value)})")
63
- return int(value) if isinstance(value, str) else value
64
-
65
- async def get_by_email(self, email: str) -> Optional[User]:
66
- logger.info(f"Checking for user with email: {email}")
67
- statement = select(self.user_table).where(self.user_table.email == email)
68
- result = await self.session.execute(statement)
69
- return result.scalar_one_or_none()
70
-
71
- async def create(self, create_dict: Dict[str, Any]) -> User:
72
- logger.info(f"Creating user with email: {create_dict.get('email')}")
73
- user = self.user_table(**create_dict)
74
- self.session.add(user)
75
- await self.session.commit()
76
- await self.session.refresh(user)
77
- return user
78
-
79
  # مدير المستخدمين
80
  class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
81
  reset_password_token_secret = SECRET
 
1
+ # api/auth.py
2
  # SPDX-FileCopyrightText: Hadad <[email protected]>
3
  # SPDX-License-Identifier: Apache-2.0
4
 
5
  from fastapi_users import FastAPIUsers
6
  from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
7
+ from fastapi_users.router.oauth import get_oauth_router
8
  from httpx_oauth.clients.google import GoogleOAuth2
9
  from httpx_oauth.clients.github import GitHubOAuth2
 
 
 
10
  from fastapi_users.manager import BaseUserManager, IntegerIDMixin
11
  from fastapi import Depends, Request, FastAPI
12
  from sqlalchemy.ext.asyncio import AsyncSession
13
  from sqlalchemy import select
14
  from fastapi_users.models import UP
15
+ from typing import Optional, Dict
16
  import os
17
  import logging
18
  import secrets
19
 
20
+ from api.user_db import CustomSQLAlchemyUserDatabase, get_user_db # استيراد من user_db.py
21
+ from api.database import User, OAuthAccount
22
+ from api.models import UserRead, UserCreate, UserUpdate
23
+
24
  # إعداد اللوقينج
25
  logger = logging.getLogger(__name__)
26
 
 
57
  google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
58
  github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  # مدير المستخدمين
61
  class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
62
  reset_password_token_secret = SECRET
api/database.py CHANGED
@@ -1,3 +1,4 @@
 
1
  # SPDX-FileCopyrightText: Hadad <[email protected]>
2
  # SPDX-License-Identifier: Apache-2.0
3
 
@@ -10,10 +11,9 @@ from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Boolean, T
10
  from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
11
  from sqlalchemy.ext.declarative import declarative_base
12
  from sqlalchemy.orm import relationship
13
- from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
14
  from fastapi import Depends
15
  import aiosqlite
16
- from api.auth import CustomSQLAlchemyUserDatabase # أضف هذا الـ import
17
 
18
  # إعداد اللوج
19
  logger = logging.getLogger(__name__)
@@ -58,7 +58,7 @@ class OAuthAccount(Base):
58
 
59
  user = relationship("User", back_populates="oauth_accounts", lazy="selectin")
60
 
61
- class User(SQLAlchemyBaseUserTable[int], Base):
62
  __tablename__ = "user"
63
  id = Column(Integer, primary_key=True, index=True)
64
  email = Column(String, unique=True, index=True, nullable=False)
 
1
+ # api/database.py
2
  # SPDX-FileCopyrightText: Hadad <[email protected]>
3
  # SPDX-License-Identifier: Apache-2.0
4
 
 
11
  from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
12
  from sqlalchemy.ext.declarative import declarative_base
13
  from sqlalchemy.orm import relationship
 
14
  from fastapi import Depends
15
  import aiosqlite
16
+ from api.user_db import CustomSQLAlchemyUserDatabase, get_user_db # استيراد من user_db.py
17
 
18
  # إعداد اللوج
19
  logger = logging.getLogger(__name__)
 
58
 
59
  user = relationship("User", back_populates="oauth_accounts", lazy="selectin")
60
 
61
+ class User(Base): # إزالة SQLAlchemyBaseUserTable لأن fastapi-users بيستخدم CustomSQLAlchemyUserDatabase
62
  __tablename__ = "user"
63
  id = Column(Integer, primary_key=True, index=True)
64
  email = Column(String, unique=True, index=True, nullable=False)
api/endpoints.py CHANGED
@@ -141,6 +141,7 @@ async def performance_stats():
141
  "uptime": os.popen("uptime").read().strip()
142
  }
143
 
 
144
  @router.post("/api/chat")
145
  async def chat_endpoint(
146
  request: Request,
@@ -178,10 +179,15 @@ async def chat_endpoint(
178
  preferred_model = user.preferred_model if user else None
179
  model_name, api_endpoint = select_model(req.message, input_type="text", preferred_model=preferred_model)
180
 
 
181
  is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
182
  if not is_available:
183
- logger.error(f"Model {model_name} is not available at {api_endpoint}")
184
- raise HTTPException(status_code=503, detail=f"Model {model_name} is not available. Please try another model.")
 
 
 
 
185
 
186
  system_prompt = enhance_system_prompt(req.system_prompt, req.message, user)
187
 
@@ -209,12 +215,12 @@ async def chat_endpoint(
209
  logger.warning(f"Unexpected non-bytes chunk in audio stream: {chunk}")
210
  if not audio_chunks:
211
  logger.error("No audio data generated.")
212
- raise HTTPException(status_code=500, detail="No audio data generated.")
213
  audio_data = b"".join(audio_chunks)
214
  return StreamingResponse(io.BytesIO(audio_data), media_type="audio/wav")
215
  except Exception as e:
216
  logger.error(f"Audio generation failed: {e}")
217
- raise HTTPException(status_code=500, detail=f"Audio generation failed: {str(e)}")
218
 
219
  response_chunks = []
220
  try:
@@ -225,8 +231,37 @@ async def chat_endpoint(
225
  logger.warning(f"Unexpected non-string chunk in text stream: {chunk}")
226
  response = "".join(response_chunks)
227
  if not response.strip():
228
- logger.error("Empty response generated.")
229
- raise HTTPException(status_code=500, detail="Empty response generated from model.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  logger.info(f"Chat response: {response[:100]}...")
231
  except Exception as e:
232
  logger.error(f"Chat generation failed: {e}")
@@ -246,7 +281,6 @@ async def chat_endpoint(
246
  }
247
 
248
  return {"response": response}
249
-
250
  @router.post("/api/audio-transcription")
251
  async def audio_transcription_endpoint(
252
  request: Request,
 
141
  "uptime": os.popen("uptime").read().strip()
142
  }
143
 
144
+
145
  @router.post("/api/chat")
146
  async def chat_endpoint(
147
  request: Request,
 
179
  preferred_model = user.preferred_model if user else None
180
  model_name, api_endpoint = select_model(req.message, input_type="text", preferred_model=preferred_model)
181
 
182
+ # جرب النموذج الأساسي
183
  is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
184
  if not is_available:
185
+ logger.warning(f"Model {model_name} is not available at {api_endpoint}, trying fallback model.")
186
+ model_name = SECONDARY_MODEL_NAME # جرب النموذج البديل
187
+ is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
188
+ if not is_available:
189
+ logger.error(f"Fallback model {model_name} is not available at {selected_endpoint}")
190
+ raise HTTPException(status_code=503, detail=f"No available models. Tried {MODEL_NAME} and {SECONDARY_MODEL_NAME}.")
191
 
192
  system_prompt = enhance_system_prompt(req.system_prompt, req.message, user)
193
 
 
215
  logger.warning(f"Unexpected non-bytes chunk in audio stream: {chunk}")
216
  if not audio_chunks:
217
  logger.error("No audio data generated.")
218
+ raise HTTPException(status_code=502, detail="No audio data generated. Model may be unavailable.")
219
  audio_data = b"".join(audio_chunks)
220
  return StreamingResponse(io.BytesIO(audio_data), media_type="audio/wav")
221
  except Exception as e:
222
  logger.error(f"Audio generation failed: {e}")
223
+ raise HTTPException(status_code=502, detail=f"Audio generation failed: {str(e)}")
224
 
225
  response_chunks = []
226
  try:
 
231
  logger.warning(f"Unexpected non-string chunk in text stream: {chunk}")
232
  response = "".join(response_chunks)
233
  if not response.strip():
234
+ logger.warning(f"Empty response from {model_name}. Trying fallback model {SECONDARY_MODEL_NAME}.")
235
+ # جرب النموذج البديل
236
+ model_name = SECONDARY_MODEL_NAME
237
+ is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
238
+ if not is_available:
239
+ logger.error(f"Fallback model {model_name} is not available at {selected_endpoint}")
240
+ raise HTTPException(status_code=503, detail=f"No available models. Tried {MODEL_NAME} and {SECONDARY_MODEL_NAME}.")
241
+
242
+ stream = request_generation(
243
+ api_key=api_key,
244
+ api_base=selected_endpoint,
245
+ message=req.message,
246
+ system_prompt=system_prompt,
247
+ model_name=model_name,
248
+ chat_history=req.history,
249
+ temperature=req.temperature,
250
+ max_new_tokens=req.max_new_tokens or 2048,
251
+ deep_search=req.enable_browsing,
252
+ input_type="text",
253
+ output_format=req.output_format
254
+ )
255
+ response_chunks = []
256
+ for chunk in stream:
257
+ if isinstance(chunk, str):
258
+ response_chunks.append(chunk)
259
+ else:
260
+ logger.warning(f"Unexpected non-string chunk in text stream: {chunk}")
261
+ response = "".join(response_chunks)
262
+ if not response.strip():
263
+ logger.error(f"Empty response from fallback model {model_name}.")
264
+ raise HTTPException(status_code=502, detail=f"Empty response from both {MODEL_NAME} and {SECONDARY_MODEL_NAME}.")
265
  logger.info(f"Chat response: {response[:100]}...")
266
  except Exception as e:
267
  logger.error(f"Chat generation failed: {e}")
 
281
  }
282
 
283
  return {"response": response}
 
284
  @router.post("/api/audio-transcription")
285
  async def audio_transcription_endpoint(
286
  request: Request,
api/models.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from pydantic import BaseModel, Field
2
  from typing import List, Optional
3
  from fastapi_users import schemas
@@ -34,6 +35,7 @@ class UserCreate(schemas.BaseUserCreate):
34
 
35
  model_config = {"from_attributes": True}
36
 
 
37
  class UserUpdate(BaseModel):
38
  display_name: Optional[str] = None
39
  preferred_model: Optional[str] = None
 
1
+ # api/models.py
2
  from pydantic import BaseModel, Field
3
  from typing import List, Optional
4
  from fastapi_users import schemas
 
35
 
36
  model_config = {"from_attributes": True}
37
 
38
+ # Pydantic schema for updating user settings
39
  class UserUpdate(BaseModel):
40
  display_name: Optional[str] = None
41
  preferred_model: Optional[str] = None
api/user_db.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # api/user_db.py
2
+ # SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ from typing import Any, AsyncGenerator, Dict, Optional
7
+
8
+ from fastapi import Depends
9
+ from fastapi_users.db import SQLAlchemyUserDatabase
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+ from sqlalchemy import select
12
+
13
+ from api.database import User, OAuthAccount # استيراد جداولك
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class CustomSQLAlchemyUserDatabase(SQLAlchemyUserDatabase[User, int]):
18
+ """
19
+ قاعدة بيانات مخصَّصة لمكتبة fastapi‑users.
20
+ تضيف طريقة parse_id التي تُحوِّل الـ ID من str → int.
21
+ """
22
+
23
+ def parse_id(self, value: Any) -> int:
24
+ logger.debug(f"Parsing user id: {value} (type={type(value)})")
25
+ # إذا كان الـ ID نصًا (من JWT) → حوّله إلى int
26
+ return int(value) if isinstance(value, str) else value
27
+
28
+ # ---------- وظائف مساعدة ----------
29
+ async def get_by_email(self, email: str) -> Optional[User]:
30
+ logger.info(f"Looking for user with email: {email}")
31
+ stmt = select(self.user_table).where(self.user_table.email == email)
32
+ result = await self.session.execute(stmt)
33
+ return result.scalar_one_or_none()
34
+
35
+ async def create(self, create_dict: Dict[str, Any]) -> User:
36
+ logger.info(f"Creating new user: {create_dict.get('email')}")
37
+ user = self.user_table(**create_dict)
38
+ self.session.add(user)
39
+ await self.session.commit()
40
+ await self.session.refresh(user)
41
+ return user
42
+
43
+ # ---------- Dependency يُستَخدم في باقي المشروع ----------
44
+ async def get_user_db(
45
+ session: AsyncSession = Depends(lambda: None) # سيتم استبداله في database.py
46
+ ) -> AsyncGenerator[CustomSQLAlchemyUserDatabase, None]:
47
+ """
48
+ يُستَخدم كـ Depends في جميع المسارات التي تحتاج إلى قاعدة بيانات المستخدم.
49
+ سيتم تمرير الـ AsyncSession الفعلي من `api/database.py`.
50
+ """
51
+ yield CustomSQLAlchemyUserDatabase(session, User, OAuthAccount)
utils/web_search.py CHANGED
@@ -2,7 +2,6 @@ import os
2
  import requests
3
  from bs4 import BeautifulSoup
4
  import logging
5
- import time
6
 
7
  logger = logging.getLogger(__name__)
8
 
@@ -12,11 +11,8 @@ def web_search(query: str) -> str:
12
  google_cse_id = os.getenv("GOOGLE_CSE_ID")
13
  if not google_api_key or not google_cse_id:
14
  return "Web search requires GOOGLE_API_KEY and GOOGLE_CSE_ID to be set."
15
- url = f"https://www.googleapis.com/customsearch/v1?key={google_api_key}&cx={google_cse_id}&q={query}"
16
- headers = {
17
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
18
- }
19
- response = requests.get(url, headers=headers, timeout=10)
20
  response.raise_for_status()
21
  results = response.json().get("items", [])
22
  if not results:
@@ -27,8 +23,7 @@ def web_search(query: str) -> str:
27
  snippet = item.get("snippet", "")
28
  link = item.get("link", "")
29
  try:
30
- time.sleep(2)
31
- page_response = requests.get(link, headers=headers, timeout=10)
32
  page_response.raise_for_status()
33
  soup = BeautifulSoup(page_response.text, "html.parser")
34
  paragraphs = soup.find_all("p")
 
2
  import requests
3
  from bs4 import BeautifulSoup
4
  import logging
 
5
 
6
  logger = logging.getLogger(__name__)
7
 
 
11
  google_cse_id = os.getenv("GOOGLE_CSE_ID")
12
  if not google_api_key or not google_cse_id:
13
  return "Web search requires GOOGLE_API_KEY and GOOGLE_CSE_ID to be set."
14
+ url = f"https://www.googleapis.com/customsearch/v1?key={google_api_key}&cx={google_cse_id}&q={query}+site:https://hager-zon.vercel.app/"
15
+ response = requests.get(url, timeout=10)
 
 
 
16
  response.raise_for_status()
17
  results = response.json().get("items", [])
18
  if not results:
 
23
  snippet = item.get("snippet", "")
24
  link = item.get("link", "")
25
  try:
26
+ page_response = requests.get(link, timeout=5)
 
27
  page_response.raise_for_status()
28
  soup = BeautifulSoup(page_response.text, "html.parser")
29
  paragraphs = soup.find_all("p")