bh-ab commited on
Commit
45dd3fd
·
verified ·
1 Parent(s): 01d4633

Upload 6 files

Browse files
Files changed (5) hide show
  1. app/config.py +18 -13
  2. app/exceptions.py +79 -0
  3. app/logging_config.py +5 -6
  4. app/main.py +24 -26
  5. app/schemas.py +41 -26
app/config.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- إعدادات التطبيق - تُقرأ من متغيرات البيئة (.env)
3
  """
4
  from functools import lru_cache
5
 
@@ -7,24 +7,25 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
7
 
8
 
9
  class Settings(BaseSettings):
10
- # إعدادات عامة
11
- app_name: str = "HF Chat Backend"
12
  app_env: str = "development"
13
  debug: bool = False
14
 
15
- # إعدادات Hugging Face
 
 
 
 
 
 
 
 
16
  hf_api_token: str = ""
17
  hf_model_id: str = "meta-llama/Llama-3.1-8B-Instruct"
18
- hf_provider: str | None = None # مثال: "hf-inference" أو "together" أو "fireworks-ai"
19
- hf_timeout_seconds: int = 60
20
 
21
- # إعدادات توليد النص الافتراضية
22
- default_max_new_tokens: int = 512
23
- default_temperature: float = 0.7
24
- default_top_p: float = 0.95
25
-
26
- # أمان بسيط: مفتاح API اختياري لحماية الباكند نفسه
27
- backend_api_key: str = ""
28
 
29
  # CORS
30
  cors_allow_origins: str = "*"
@@ -36,6 +37,10 @@ class Settings(BaseSettings):
36
  extra="ignore",
37
  )
38
 
 
 
 
 
39
 
40
  @lru_cache
41
  def get_settings() -> Settings:
 
1
  """
2
+ All application settings — read from environment variables or .env file.
3
  """
4
  from functools import lru_cache
5
 
 
7
 
8
 
9
  class Settings(BaseSettings):
10
+ app_name: str = "AI Tools Backend"
 
11
  app_env: str = "development"
12
  debug: bool = False
13
 
14
+ # Auth — comma-separated API keys
15
+ api_keys: str = ""
16
+
17
+ # File management
18
+ upload_dir: str = "./uploads"
19
+ output_dir: str = "./outputs"
20
+ max_upload_size_mb: int = 100
21
+
22
+ # AI / LLM (optional — for chat proxy)
23
  hf_api_token: str = ""
24
  hf_model_id: str = "meta-llama/Llama-3.1-8B-Instruct"
25
+ hf_provider: str | None = None
 
26
 
27
+ # Security tools toggle
28
+ pentest_enabled: bool = True
 
 
 
 
 
29
 
30
  # CORS
31
  cors_allow_origins: str = "*"
 
37
  extra="ignore",
38
  )
39
 
40
+ @property
41
+ def api_key_list(self) -> list[str]:
42
+ return [k.strip() for k in self.api_keys.split(",") if k.strip()]
43
+
44
 
45
  @lru_cache
46
  def get_settings() -> Settings:
app/exceptions.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom exceptions + global error handlers.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from typing import Any
8
+
9
+ from fastapi import FastAPI, Request, status
10
+ from fastapi.responses import JSONResponse
11
+
12
+ from app.schemas import ErrorResponse
13
+
14
+ logger = logging.getLogger("ai_tools.errors")
15
+
16
+
17
+ class ToolError(Exception):
18
+ """Base error for tool execution failures."""
19
+
20
+ def __init__(
21
+ self,
22
+ tool_name: str,
23
+ message: str,
24
+ status_code: int = 400,
25
+ detail: Any = None,
26
+ ) -> None:
27
+ self.tool_name = tool_name
28
+ self.message = message
29
+ self.status_code = status_code
30
+ self.detail = detail
31
+
32
+
33
+ class FileTooLargeError(ToolError):
34
+ def __init__(self, tool_name: str, max_mb: int) -> None:
35
+ super().__init__(tool_name, f"File exceeds maximum size of {max_mb}MB", 413)
36
+
37
+
38
+ class UnsupportedFormatError(ToolError):
39
+ def __init__(self, tool_name: str, fmt: str) -> None:
40
+ super().__init__(tool_name, f"Unsupported format: {fmt}", 400)
41
+
42
+
43
+ class SecurityToolDisabledError(ToolError):
44
+ def __init__(self, tool_name: str) -> None:
45
+ super().__init__(tool_name, "Security tools are disabled (pentest_enabled=false)", 403)
46
+
47
+
48
+ class FileNotFoundError(ToolError):
49
+ def __init__(self, tool_name: str, file_id: str) -> None:
50
+ super().__init__(tool_name, f"File not found: {file_id}", 404)
51
+
52
+
53
+ class UnauthorizedError(ToolError):
54
+ def __init__(self) -> None:
55
+ super().__init__("auth", "Invalid or missing API key", 401)
56
+
57
+
58
+ def register_error_handlers(app: FastAPI) -> None:
59
+ @app.exception_handler(ToolError)
60
+ async def tool_error_handler(request: Request, exc: ToolError) -> JSONResponse:
61
+ logger.warning("Tool error [%s]: %s", exc.tool_name, exc.message)
62
+ return JSONResponse(
63
+ status_code=exc.status_code,
64
+ content=ErrorResponse(
65
+ error=exc.tool_name,
66
+ detail=exc.message,
67
+ ).model_dump(),
68
+ )
69
+
70
+ @app.exception_handler(Exception)
71
+ async def unhandled_handler(request: Request, exc: Exception) -> JSONResponse:
72
+ logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
73
+ return JSONResponse(
74
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
75
+ content=ErrorResponse(
76
+ error="internal_server_error",
77
+ detail=str(exc),
78
+ ).model_dump(),
79
+ )
app/logging_config.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- إعداد Logging مهيكل (Structured Logging) بصيغة JSON.
3
  """
4
  import json
5
  import logging
@@ -24,10 +24,9 @@ def configure_logging(debug: bool = False) -> None:
24
  handler = logging.StreamHandler(sys.stdout)
25
  handler.setFormatter(JsonFormatter())
26
 
27
- root_logger = logging.getLogger()
28
- root_logger.handlers = [handler]
29
- root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
30
 
31
- # تهدئة بعض المكتبات الثرثارة
32
  logging.getLogger("httpx").setLevel(logging.WARNING)
33
- logging.getLogger("huggingface_hub").setLevel(logging.INFO)
 
1
  """
2
+ Structured JSON logging configuration.
3
  """
4
  import json
5
  import logging
 
24
  handler = logging.StreamHandler(sys.stdout)
25
  handler.setFormatter(JsonFormatter())
26
 
27
+ root = logging.getLogger()
28
+ root.handlers = [handler]
29
+ root.setLevel(logging.DEBUG if debug else logging.INFO)
30
 
 
31
  logging.getLogger("httpx").setLevel(logging.WARNING)
32
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
app/main.py CHANGED
@@ -1,51 +1,52 @@
1
  """
2
- نقطة الدخول الرئيسية لتطبيق FastAPI.
3
  """
4
  import logging
5
  from contextlib import asynccontextmanager
6
 
7
- from fastapi import FastAPI, Request, status
8
  from fastapi.middleware.cors import CORSMiddleware
9
- from fastapi.responses import JSONResponse
10
 
 
11
  from app.config import get_settings
 
 
 
12
  from app.logging_config import configure_logging
13
- from app.routers import chat, health
14
- from app.schemas import ErrorResponse
15
 
16
  settings = get_settings()
17
  configure_logging(debug=settings.debug)
18
- logger = logging.getLogger("hf_chat_backend.main")
 
 
 
19
 
20
 
21
  @asynccontextmanager
22
  async def lifespan(app: FastAPI):
23
- logger.info(
24
- "Starting %s [env=%s] using model=%s",
25
- settings.app_name,
26
- settings.app_env,
27
- settings.hf_model_id,
28
- )
29
  yield
30
  logger.info("Shutting down %s", settings.app_name)
31
 
32
 
33
  app = FastAPI(
34
  title=settings.app_name,
35
- description="باكند FastAPI بسيط لاستدعاء نموذج LLM مستضاف على Hugging Face",
36
  version="1.0.0",
37
  docs_url="/docs",
38
  redoc_url="/redoc",
39
- openapi_url="/openapi.json",
40
  lifespan=lifespan,
41
  )
42
 
 
43
  origins = (
44
  ["*"]
45
  if settings.cors_allow_origins.strip() == "*"
46
  else [o.strip() for o in settings.cors_allow_origins.split(",") if o.strip()]
47
  )
48
-
49
  app.add_middleware(
50
  CORSMiddleware,
51
  allow_origins=origins,
@@ -54,15 +55,12 @@ app.add_middleware(
54
  allow_headers=["*"],
55
  )
56
 
 
57
 
58
- @app.exception_handler(Exception)
59
- async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
60
- logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
61
- return JSONResponse(
62
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
63
- content=ErrorResponse(error="internal_server_error", detail=str(exc)).model_dump(),
64
- )
65
-
66
-
67
- app.include_router(health.router)
68
- app.include_router(chat.router)
 
1
  """
2
+ AI Tools Backend — Application factory and entry point.
3
  """
4
  import logging
5
  from contextlib import asynccontextmanager
6
 
7
+ from fastapi import FastAPI
8
  from fastapi.middleware.cors import CORSMiddleware
 
9
 
10
+ from app.auth.api_key import verify_api_key
11
  from app.config import get_settings
12
+ from app.core.file_manager import FileManager
13
+ from app.core.plugin_manager import plugin_manager
14
+ from app.exceptions import register_error_handlers
15
  from app.logging_config import configure_logging
16
+ from app.routers import ai_routes, document_routes, health_routes, media_routes, security_routes, tools_routes
 
17
 
18
  settings = get_settings()
19
  configure_logging(debug=settings.debug)
20
+ logger = logging.getLogger("ai_tools.main")
21
+
22
+ # Global file manager instance
23
+ file_manager = FileManager(settings)
24
 
25
 
26
  @asynccontextmanager
27
  async def lifespan(app: FastAPI):
28
+ logger.info("Starting %s", settings.app_name)
29
+ plugin_manager.auto_discover()
30
+ logger.info("Loaded %d tools", plugin_manager.count)
 
 
 
31
  yield
32
  logger.info("Shutting down %s", settings.app_name)
33
 
34
 
35
  app = FastAPI(
36
  title=settings.app_name,
37
+ description="AI Tools Backend — 47+ tools for documents, media, security, and AI",
38
  version="1.0.0",
39
  docs_url="/docs",
40
  redoc_url="/redoc",
 
41
  lifespan=lifespan,
42
  )
43
 
44
+ # CORS
45
  origins = (
46
  ["*"]
47
  if settings.cors_allow_origins.strip() == "*"
48
  else [o.strip() for o in settings.cors_allow_origins.split(",") if o.strip()]
49
  )
 
50
  app.add_middleware(
51
  CORSMiddleware,
52
  allow_origins=origins,
 
55
  allow_headers=["*"],
56
  )
57
 
58
+ register_error_handlers(app)
59
 
60
+ # Register routers
61
+ app.include_router(health_routes.router)
62
+ app.include_router(tools_routes.router)
63
+ app.include_router(document_routes.router)
64
+ app.include_router(media_routes.router)
65
+ app.include_router(security_routes.router)
66
+ app.include_router(ai_routes.router)
 
 
 
 
app/schemas.py CHANGED
@@ -1,41 +1,56 @@
1
  """
2
- Pydantic Schemas الخاصة بطلبات/ردود الدردشة
3
  """
4
- from typing import Literal
5
-
6
- from pydantic import BaseModel, Field
7
 
 
8
 
9
- class ChatMessage(BaseModel):
10
- role: Literal["system", "user", "assistant"]
11
- content: str = Field(..., min_length=1)
12
 
13
 
14
- class ChatRequest(BaseModel):
15
- messages: list[ChatMessage] = Field(
16
- ..., min_length=1, description="سجل المحادثة كاملاً"
17
- )
18
- max_new_tokens: int | None = Field(
19
- default=None, ge=1, le=4096, description="أقصى عدد Tokens للرد"
20
- )
21
- temperature: float | None = Field(default=None, ge=0.0, le=2.0)
22
- top_p: float | None = Field(default=None, ge=0.0, le=1.0)
23
- stream: bool = Field(default=False, description="تفعيل البث اللحظي (SSE)")
24
 
25
 
26
- class ChatResponse(BaseModel):
27
- model: str
28
- reply: str
29
- finish_reason: str | None = None
30
 
31
 
32
  class HealthResponse(BaseModel):
33
  status: Literal["ok", "degraded"]
34
  app_name: str
35
- app_env: str
36
- model: str
37
 
38
 
39
- class ErrorResponse(BaseModel):
40
- error: str
41
- detail: str | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Shared Pydantic schemas for all tool inputs/outputs.
3
  """
4
+ from __future__ import annotations
 
 
5
 
6
+ from typing import Any, Literal
7
 
8
+ from pydantic import BaseModel, Field
 
 
9
 
10
 
11
+ class ToolResult(BaseModel):
12
+ """Unified response from any tool execution."""
13
+ success: bool
14
+ tool: str
15
+ result: Any = None
16
+ error: str | None = None
17
+ file_id: str | None = None
18
+ file_name: str | None = None
 
 
19
 
20
 
21
+ class ErrorResponse(BaseModel):
22
+ error: str
23
+ detail: str | None = None
 
24
 
25
 
26
  class HealthResponse(BaseModel):
27
  status: Literal["ok", "degraded"]
28
  app_name: str
29
+ version: str
30
+ tools_count: int
31
 
32
 
33
+ class ToolInfo(BaseModel):
34
+ name: str
35
+ description: str
36
+ category: str
37
+ input_schema: dict[str, Any]
38
+
39
+
40
+ class ToolListResponse(BaseModel):
41
+ tools: list[ToolInfo]
42
+ total: int
43
+
44
+
45
+ class FileUploadResponse(BaseModel):
46
+ file_id: str
47
+ filename: str
48
+ size: int
49
+ content_type: str
50
+
51
+
52
+ class TaskResponse(BaseModel):
53
+ task_id: str
54
+ status: Literal["pending", "running", "completed", "failed"]
55
+ result: Any = None
56
+ error: str | None = None