bh-ab commited on
Commit
90a01d3
·
verified ·
1 Parent(s): 0ed2556
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