Delete
Browse files- tests/.env.example +0 -25
- tests/Dockerfile +0 -19
- tests/README.md +0 -126
- tests/__init__.py +0 -0
- tests/conftest.py +0 -12
- tests/docker-compose.yml +0 -8
- tests/requirements.txt +0 -7
- tests/test_api.py +0 -82
- tests/test_chat.py +0 -38
- tests/test_health.py +0 -13
tests/.env.example
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
# إعدادات عامة
|
| 2 |
-
APP_NAME=HF Chat Backend
|
| 3 |
-
APP_ENV=development
|
| 4 |
-
DEBUG=false
|
| 5 |
-
|
| 6 |
-
# إعدادات Hugging Face
|
| 7 |
-
# احصل على التوكن من: https://huggingface.co/settings/tokens
|
| 8 |
-
HF_API_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 9 |
-
# معرّف النموذج على Hugging Face (أو رابط Inference Endpoint المخصص)
|
| 10 |
-
HF_MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
|
| 11 |
-
# اتركه فارغاً لاستخدام Hugging Face Inference API الافتراضي
|
| 12 |
-
# أو حدد مزود متوافق مثل: together, fireworks-ai, replicate, sambanova
|
| 13 |
-
HF_PROVIDER=
|
| 14 |
-
HF_TIMEOUT_SECONDS=60
|
| 15 |
-
|
| 16 |
-
# إعدادات التوليد الافتراضية
|
| 17 |
-
DEFAULT_MAX_NEW_TOKENS=512
|
| 18 |
-
DEFAULT_TEMPERATURE=0.7
|
| 19 |
-
DEFAULT_TOP_P=0.95
|
| 20 |
-
|
| 21 |
-
# مفتاح لحماية الباكند نفسه (اتركه فارغاً لتعطيل الحماية)
|
| 22 |
-
BACKEND_API_KEY=
|
| 23 |
-
|
| 24 |
-
# CORS - افصل بفواصل أو استخدم * للسماح للجميع
|
| 25 |
-
CORS_ALLOW_ORIGINS=*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/Dockerfile
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
FROM python:3.12-slim
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
-
PYTHONUNBUFFERED=1
|
| 7 |
-
|
| 8 |
-
COPY requirements.txt .
|
| 9 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
-
|
| 11 |
-
COPY app ./app
|
| 12 |
-
|
| 13 |
-
EXPOSE 8000
|
| 14 |
-
|
| 15 |
-
# Healthcheck بسيط
|
| 16 |
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
| 17 |
-
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
| 18 |
-
|
| 19 |
-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/README.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
| 1 |
-
# HF Chat Backend
|
| 2 |
-
|
| 3 |
-
باكند بسيط ومنظّم مبني بـ **FastAPI** يستدعي نموذج **LLM/Chatbot** مستضاف على **Hugging Face** (عبر Inference API أو Inference Endpoint أو أي مزود متوافق) ويعرضه كـ REST API جاهز للاستخدام من أي تطبيق أو Agent.
|
| 4 |
-
|
| 5 |
-
## البنية
|
| 6 |
-
|
| 7 |
-
```
|
| 8 |
-
hf-chat-backend/
|
| 9 |
-
├── app/
|
| 10 |
-
│ ├── main.py # نقطة الدخول + إعداد FastAPI
|
| 11 |
-
│ ├── config.py # الإعدادات (تُقرأ من .env)
|
| 12 |
-
│ ├── schemas.py # Pydantic Schemas
|
| 13 |
-
│ ├── dependencies.py # Dependency Injection + حماية بمفتاح API
|
| 14 |
-
│ ├── logging_config.py # Logging مهيكل بصيغة JSON
|
| 15 |
-
│ ├── services/
|
| 16 |
-
│ │ └── hf_client.py # طبقة الاتصال بـ Hugging Face
|
| 17 |
-
│ └── routers/
|
| 18 |
-
│ ├── health.py # GET /health
|
| 19 |
-
│ └── chat.py # POST /chat (رد كامل أو بث SSE)
|
| 20 |
-
├── tests/ # اختبارات pytest
|
| 21 |
-
├── requirements.txt
|
| 22 |
-
├── requirements-dev.txt
|
| 23 |
-
├── Dockerfile
|
| 24 |
-
├── docker-compose.yml
|
| 25 |
-
├── .env.example
|
| 26 |
-
└── pyproject.toml
|
| 27 |
-
```
|
| 28 |
-
|
| 29 |
-
## 1) الإعداد المحلي
|
| 30 |
-
|
| 31 |
-
```bash
|
| 32 |
-
cd hf-chat-backend
|
| 33 |
-
python -m venv .venv && source .venv/bin/activate
|
| 34 |
-
pip install -r requirements-dev.txt
|
| 35 |
-
cp .env.example .env
|
| 36 |
-
# عدّل .env وضع HF_API_TOKEN و HF_MODEL_ID الخاصين بك
|
| 37 |
-
```
|
| 38 |
-
|
| 39 |
-
### متغيرات البيئة الأساسية (.env)
|
| 40 |
-
|
| 41 |
-
| المتغير | الوصف |
|
| 42 |
-
|---|---|
|
| 43 |
-
| `HF_API_TOKEN` | التوكن من https://huggingface.co/settings/tokens |
|
| 44 |
-
| `HF_MODEL_ID` | معرّف النموذج، مثال: `meta-llama/Llama-3.1-8B-Instruct` |
|
| 45 |
-
| `HF_PROVIDER` | اتركه فارغاً لاستخدام hf-inference الافتراضي، أو حدد مزود مثل `together`، `fireworks-ai` |
|
| 46 |
-
| `BACKEND_API_KEY` | اختياري: لحماية الباكند نفسه بمفتاح `X-API-Key` |
|
| 47 |
-
|
| 48 |
-
### التشغيل
|
| 49 |
-
|
| 50 |
-
```bash
|
| 51 |
-
uvicorn app.main:app --reload
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
ثم افتح: `http://localhost:8000/docs` لتوثيق Swagger التفاعلي.
|
| 55 |
-
|
| 56 |
-
## 2) الاختبارات والجودة
|
| 57 |
-
|
| 58 |
-
```bash
|
| 59 |
-
pytest -v
|
| 60 |
-
ruff check app tests
|
| 61 |
-
mypy app
|
| 62 |
-
```
|
| 63 |
-
|
| 64 |
-
## 3) الاستخدام عبر Docker
|
| 65 |
-
|
| 66 |
-
```bash
|
| 67 |
-
docker compose up --build
|
| 68 |
-
```
|
| 69 |
-
|
| 70 |
-
## 4) استدعاء الـ API
|
| 71 |
-
|
| 72 |
-
### رد كامل (بدون بث)
|
| 73 |
-
|
| 74 |
-
```bash
|
| 75 |
-
curl -X POST http://localhost:8000/chat \
|
| 76 |
-
-H "Content-Type: application/json" \
|
| 77 |
-
-d '{
|
| 78 |
-
"messages": [
|
| 79 |
-
{"role": "system", "content": "أنت مساعد ذكي ومفيد."},
|
| 80 |
-
{"role": "user", "content": "اشرح لي ما هو FastAPI باختصار"}
|
| 81 |
-
]
|
| 82 |
-
}'
|
| 83 |
-
```
|
| 84 |
-
|
| 85 |
-
### رد ببث لحظي (Server-Sent Events)
|
| 86 |
-
|
| 87 |
-
```bash
|
| 88 |
-
curl -N -X POST http://localhost:8000/chat \
|
| 89 |
-
-H "Content-Type: application/json" \
|
| 90 |
-
-d '{
|
| 91 |
-
"messages": [{"role": "user", "content": "اكتب قصة قصيرة"}],
|
| 92 |
-
"stream": true
|
| 93 |
-
}'
|
| 94 |
-
```
|
| 95 |
-
|
| 96 |
-
### مع حماية بمفتاح API
|
| 97 |
-
|
| 98 |
-
إذا فعّلت `BACKEND_API_KEY` في `.env`، أضف الهيدر:
|
| 99 |
-
|
| 100 |
-
```bash
|
| 101 |
-
-H "X-API-Key: your-secret-key"
|
| 102 |
-
```
|
| 103 |
-
|
| 104 |
-
## 5) النشر على Hugging Face
|
| 105 |
-
|
| 106 |
-
عندك خياران رئيسيان حسب مكان استضافة **النموذج نفسه**:
|
| 107 |
-
|
| 108 |
-
### الخيار أ: النموذج مستضاف بالفعل على HF (Inference API / Endpoint)
|
| 109 |
-
هذا الباكند لا يحتاج لاستضافة النموذج — فقط يستدعيه عبر الشبكة. يمكنك نشر هذا الباكند في أي مكان (Render, Railway, VPS, Docker, Fly.io...) طالما ضبطت `HF_API_TOKEN` و `HF_MODEL_ID`.
|
| 110 |
-
|
| 111 |
-
### الخيار ب: نشر الباكند نفسه كـ Hugging Face Space
|
| 112 |
-
1. أنشئ Space جديد من نوع **Docker** على https://huggingface.co/new-space
|
| 113 |
-
2. ارفع محتويات هذا المجلد كاملة (بما فيها `Dockerfile`)
|
| 114 |
-
3. من إعدادات الـ Space أضف Secret باسم `HF_API_TOKEN` وقيمته توكنك
|
| 115 |
-
4. عدّل `HF_MODEL_ID` حسب النموذج المطلوب (Variable وليس Secret)
|
| 116 |
-
5. Hugging Face Spaces يستخدم المنفذ `7860` افتراضياً — عدّل `Dockerfile` وضع:
|
| 117 |
-
```dockerfile
|
| 118 |
-
EXPOSE 7860
|
| 119 |
-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 120 |
-
```
|
| 121 |
-
|
| 122 |
-
## 6) ملاحظات إنتاجية
|
| 123 |
-
|
| 124 |
-
- **الأمان**: فعّل `BACKEND_API_KEY` دائماً في الإنتاج، ولا تضع `HF_API_TOKEN` في الكود مطلقاً — استخدم Secrets فقط.
|
| 125 |
-
- **Rate Limiting**: هذا المشروع بسيط ومخصص للانطلاق السريع؛ إذا احتجت Rate Limiting متقدم أو RBAC كامل أو Plugins متعددة (PDF, Media, Network...) أخبرني وأبني لك نسخة موسّعة على مراحل.
|
| 126 |
-
- **CORS**: بشكل افتراضي مفتوح للجميع (`*`) — قيّده بدومينات تطبيقك الفعلية في الإنتاج عبر `CORS_ALLOW_ORIGINS`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/__init__.py
DELETED
|
File without changes
|
tests/conftest.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Shared test fixtures.
|
| 3 |
-
"""
|
| 4 |
-
import pytest
|
| 5 |
-
from fastapi.testclient import TestClient
|
| 6 |
-
|
| 7 |
-
from app.main import app
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@pytest.fixture
|
| 11 |
-
def client():
|
| 12 |
-
return TestClient(app)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/docker-compose.yml
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
services:
|
| 2 |
-
api:
|
| 3 |
-
build: .
|
| 4 |
-
ports:
|
| 5 |
-
- "7860:7860"
|
| 6 |
-
env_file:
|
| 7 |
-
- .env
|
| 8 |
-
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/requirements.txt
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
fastapi==0.115.6
|
| 2 |
-
uvicorn[standard]==0.34.0
|
| 3 |
-
pydantic==2.10.4
|
| 4 |
-
pydantic-settings==2.7.1
|
| 5 |
-
huggingface_hub==1.21.0
|
| 6 |
-
python-multipart==0.0.20
|
| 7 |
-
httpx==0.28.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_api.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Tests for health and tools endpoints.
|
| 3 |
-
"""
|
| 4 |
-
import pytest
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
def test_health(client):
|
| 8 |
-
resp = client.get("/health")
|
| 9 |
-
assert resp.status_code == 200
|
| 10 |
-
data = resp.json()
|
| 11 |
-
assert data["status"] == "ok"
|
| 12 |
-
assert data["tools_count"] > 0
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def test_list_tools(client):
|
| 16 |
-
resp = client.get("/tools")
|
| 17 |
-
assert resp.status_code == 200
|
| 18 |
-
data = resp.json()
|
| 19 |
-
assert data["total"] > 0
|
| 20 |
-
names = [t["name"] for t in data["tools"]]
|
| 21 |
-
assert "pdf_merge" in names
|
| 22 |
-
assert "image_resize" in names
|
| 23 |
-
assert "dns_lookup" in names
|
| 24 |
-
assert "chat_completion" in names
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def test_list_tools_by_category(client):
|
| 28 |
-
resp = client.get("/tools/category/document")
|
| 29 |
-
assert resp.status_code == 200
|
| 30 |
-
data = resp.json()
|
| 31 |
-
assert len(data) > 0
|
| 32 |
-
assert all(t["category"] == "document" for t in data)
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def test_openai_schema(client):
|
| 36 |
-
resp = client.get("/tools/openai-schema")
|
| 37 |
-
assert resp.status_code == 200
|
| 38 |
-
data = resp.json()
|
| 39 |
-
assert len(data) > 0
|
| 40 |
-
assert data[0]["type"] == "function"
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def test_unknown_tool(client):
|
| 44 |
-
resp = client.post("/tools/execute", json={"tool": "nonexistent", "params": {}})
|
| 45 |
-
assert resp.status_code == 200
|
| 46 |
-
data = resp.json()
|
| 47 |
-
assert data["success"] is False
|
| 48 |
-
assert "Unknown tool" in data["error"]
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def test_pdf_create(client):
|
| 52 |
-
resp = client.post("/pdf/create", json={"content": "Hello World", "title": "Test"})
|
| 53 |
-
assert resp.status_code == 200
|
| 54 |
-
data = resp.json()
|
| 55 |
-
assert data["success"] is True
|
| 56 |
-
assert data["result"]["file_id"] is not None
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def test_hash_generate(client):
|
| 60 |
-
resp = client.post("/crypto/hash", json={"text": "hello", "algorithm": "sha256"})
|
| 61 |
-
assert resp.status_code == 200
|
| 62 |
-
data = resp.json()
|
| 63 |
-
assert data["success"] is True
|
| 64 |
-
assert len(data["result"]["hash"]) == 64
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def test_password_generate(client):
|
| 68 |
-
resp = client.post("/crypto/password-gen", json={"length": 32})
|
| 69 |
-
assert resp.status_code == 200
|
| 70 |
-
data = resp.json()
|
| 71 |
-
assert data["success"] is True
|
| 72 |
-
assert len(data["result"]["password"]) == 32
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def test_base64(client):
|
| 76 |
-
enc = client.post("/crypto/base64/encode", json={"text": "hello world"})
|
| 77 |
-
assert enc.status_code == 200
|
| 78 |
-
encoded = enc.json()["result"]["encoded"]
|
| 79 |
-
|
| 80 |
-
dec = client.post("/crypto/base64/decode", json={"encoded": encoded})
|
| 81 |
-
assert dec.status_code == 200
|
| 82 |
-
assert dec.json()["result"]["decoded"] == "hello world"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_chat.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
from unittest.mock import AsyncMock
|
| 2 |
-
|
| 3 |
-
from fastapi.testclient import TestClient
|
| 4 |
-
|
| 5 |
-
from app.dependencies import get_hf_service
|
| 6 |
-
from app.main import app
|
| 7 |
-
|
| 8 |
-
client = TestClient(app)
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def _override_hf_service(reply: str = "مرحباً! كيف أقدر أساعدك؟"):
|
| 12 |
-
mock_service = AsyncMock()
|
| 13 |
-
mock_service.chat_completion.return_value = (reply, "stop")
|
| 14 |
-
app.dependency_overrides[get_hf_service] = lambda: mock_service
|
| 15 |
-
return mock_service
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def teardown_function():
|
| 19 |
-
app.dependency_overrides.clear()
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def test_chat_returns_reply_from_model():
|
| 23 |
-
_override_hf_service(reply="أهلاً بك")
|
| 24 |
-
|
| 25 |
-
response = client.post(
|
| 26 |
-
"/chat",
|
| 27 |
-
json={"messages": [{"role": "user", "content": "مرحبا"}]},
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
assert response.status_code == 200
|
| 31 |
-
body = response.json()
|
| 32 |
-
assert body["reply"] == "أهلاً بك"
|
| 33 |
-
assert body["finish_reason"] == "stop"
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def test_chat_rejects_empty_messages():
|
| 37 |
-
response = client.post("/chat", json={"messages": []})
|
| 38 |
-
assert response.status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_health.py
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
from fastapi.testclient import TestClient
|
| 2 |
-
|
| 3 |
-
from app.main import app
|
| 4 |
-
|
| 5 |
-
client = TestClient(app)
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
def test_health_check_returns_ok():
|
| 9 |
-
response = client.get("/health")
|
| 10 |
-
assert response.status_code == 200
|
| 11 |
-
body = response.json()
|
| 12 |
-
assert body["status"] == "ok"
|
| 13 |
-
assert "model" in body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|