Upload 6 files
Browse files- app/config.py +18 -13
- app/exceptions.py +79 -0
- app/logging_config.py +5 -6
- app/main.py +24 -26
- app/schemas.py +41 -26
app/config.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
hf_api_token: str = ""
|
| 17 |
hf_model_id: str = "meta-llama/Llama-3.1-8B-Instruct"
|
| 18 |
-
hf_provider: str | None = None
|
| 19 |
-
hf_timeout_seconds: int = 60
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
|
| 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 |
-
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
|
| 31 |
-
# تهدئة بعض المكتبات الثرثارة
|
| 32 |
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 33 |
-
logging.getLogger("
|
|
|
|
| 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 |
-
|
| 3 |
"""
|
| 4 |
import logging
|
| 5 |
from contextlib import asynccontextmanager
|
| 6 |
|
| 7 |
-
from fastapi import FastAPI
|
| 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
|
| 14 |
-
from app.schemas import ErrorResponse
|
| 15 |
|
| 16 |
settings = get_settings()
|
| 17 |
configure_logging(debug=settings.debug)
|
| 18 |
-
logger = logging.getLogger("
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
@asynccontextmanager
|
| 22 |
async def lifespan(app: FastAPI):
|
| 23 |
-
logger.info(
|
| 24 |
-
|
| 25 |
-
|
| 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="
|
| 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 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 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
|
| 3 |
"""
|
| 4 |
-
from
|
| 5 |
-
|
| 6 |
-
from pydantic import BaseModel, Field
|
| 7 |
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
role: Literal["system", "user", "assistant"]
|
| 11 |
-
content: str = Field(..., min_length=1)
|
| 12 |
|
| 13 |
|
| 14 |
-
class
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 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
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
finish_reason: str | None = None
|
| 30 |
|
| 31 |
|
| 32 |
class HealthResponse(BaseModel):
|
| 33 |
status: Literal["ok", "degraded"]
|
| 34 |
app_name: str
|
| 35 |
-
|
| 36 |
-
|
| 37 |
|
| 38 |
|
| 39 |
-
class
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|