AhmadA82 commited on
Commit
8ca6a4f
·
verified ·
1 Parent(s): 09e62e4

update-to-qwin3

Browse files
Files changed (6) hide show
  1. .dockerignore.txt +8 -0
  2. .gitattributes.txt +35 -0
  3. Dockerfile.txt +28 -0
  4. app.py +667 -216
  5. monitor.py +29 -70
  6. requirements.txt +5 -1
.dockerignore.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ *.pyc
3
+ __pycache__/
4
+ data/
5
+ .cache/
6
+ .vscode/
7
+ *.log
8
+ tmp/
.gitattributes.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # إضافة حزم أساسية فقط
4
+ RUN apt-get update && apt-get install -y \
5
+ build-essential pkg-config curl \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ # إضافة مستخدم غير root
9
+ RUN useradd -m -u 1000 user
10
+ WORKDIR /home/user/app
11
+ COPY --chown=user . .
12
+
13
+ # مجلد التخزين المؤقت للنموذج
14
+ RUN mkdir -p /home/user/app/data && chown -R user:user /home/user/app/data
15
+
16
+ # إنشاء البيئة الافتراضية
17
+ RUN python -m venv /home/user/venv
18
+ ENV PATH="/home/user/venv/bin:$PATH"
19
+ ENV FORCE_CMAKE=1
20
+ ENV CMAKE_ARGS="-DGGML_CUDA=off"
21
+
22
+ # تثبيت المتطلبات
23
+ RUN pip install --upgrade pip
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ USER user
27
+
28
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,216 +1,667 @@
1
- # إعداد السجل العام للتطبيق
2
- import logging
3
- logging.basicConfig(
4
- level=logging.DEBUG,
5
- format="🪵 [%(asctime)s] [%(levelname)s] %(message)s",
6
- handlers=[
7
- logging.StreamHandler(),
8
- logging.FileHandler("data/app.log")
9
- ]
10
- )
11
- logger = logging.getLogger(__name__)
12
-
13
- from fastapi import FastAPI, HTTPException
14
- from pydantic import BaseModel
15
- from llama_cpp import Llama
16
- import os
17
- import threading
18
- from fastapi.middleware.cors import CORSMiddleware
19
- from monitor import get_current_metrics, start_monitoring_thread
20
- from huggingface_hub import hf_hub_download
21
- from memory import get_history, save_history
22
-
23
- # إعدادات النموذج
24
- MODEL_REPO = "Qwen/Qwen2-1.5B-Instruct-GGUF"
25
- MODEL_FILE = "qwen2-1_5b-instruct-q6_k.gguf"
26
- MODEL_PATH = f"/home/user/app/data/cache/{MODEL_FILE}"
27
- HF_TOKEN = os.getenv("HF_TOKEN")
28
-
29
- SYSTEM_PROMPT = """<|im_start|>system
30
- You are Qwen2, created by Alibaba Cloud. You are a helpful assistant specialized in AI development.
31
- Follow these rules:
32
- 1. Provide accurate and safe answers
33
- 2. For coding tasks, think step-by-step before answering
34
- 3. Keep responses concise but complete
35
- 4. For complex requests, ask clarifying questions<|im_end|>"""
36
-
37
- # تنسيق prompt الدردشة
38
- def format_chat(history, new_message):
39
- messages = [("system", SYSTEM_PROMPT)]
40
-
41
- for item in history[-8:]:
42
- if isinstance(item, list) and len(item) == 2:
43
- user_msg, bot_msg = item
44
- messages.append(("user", user_msg))
45
- messages.append(("assistant", bot_msg))
46
- else:
47
- logger.warning(f"⚠️ عنصر غير متوافق في التاريخ: {item}")
48
-
49
- messages.append(("user", new_message))
50
-
51
- formatted = []
52
- for role, content in messages:
53
- if role == "system":
54
- formatted.append(f"<|im_start|>system\n{content}<|im_end|>")
55
- elif role == "user":
56
- formatted.append(f"<|im_start|>user\n{content}<|im_end|>")
57
- else:
58
- formatted.append(f"<|im_start|>assistant\n{content}<|im_end|>")
59
- formatted.append("<|im_start|>assistant\n")
60
- return "\n".join(formatted)
61
-
62
- # تحميل النموذج إذا لم يكن موجوداً
63
- def load_model():
64
- if not os.path.exists(MODEL_PATH):
65
- os.makedirs("/home/user/app/data/cache", exist_ok=True)
66
- logger.info("📦 تحميل النموذج من Hugging Face Hub...")
67
- try:
68
- hf_hub_download(
69
- repo_id=MODEL_REPO,
70
- filename=MODEL_FILE,
71
- local_dir="/home/user/app/data/cache",
72
- token=HF_TOKEN,
73
- )
74
- logger.info(f"✅ تم تحميل النموذج: {MODEL_FILE}")
75
- except Exception as e:
76
- logger.error(f"❌ فشل تحميل النموذج: {str(e)}")
77
- raise RuntimeError("فشل تحميل النموذج") from e
78
-
79
- if not os.path.exists(MODEL_PATH):
80
- raise RuntimeError("النموذج غير موجود بعد التحميل")
81
-
82
- return Llama(
83
- model_path=MODEL_PATH,
84
- n_ctx=1024,
85
- n_threads=2,
86
- n_gpu_layers=0,
87
- n_batch=64,
88
- use_mlock=False,
89
- verbose=False
90
- )
91
-
92
- # إنشاء تطبيق FastAPI
93
- app = FastAPI()
94
-
95
- # تحميل النموذج في الخلفية
96
- llm = None
97
- model_loading_thread = None
98
-
99
- def load_model_in_background():
100
- global llm
101
- try:
102
- llm = load_model()
103
- logger.info("✅ تم تحميل النموذج بنجاح")
104
- test_prompt = format_chat([], "مرحبا، كيف يمكنك المساعدة؟")
105
- test_output = llm(test_prompt, max_tokens=50)['choices'][0]['text'].strip()
106
- logger.info(f"✅ اختبار النموذج ناجح: {test_output[:100]}...")
107
- except Exception as e:
108
- logger.error(f"❌ فشل تحميل النموذج: {str(e)}")
109
- logger.exception(e)
110
-
111
- # تمكين CORS
112
- app.add_middleware(
113
- CORSMiddleware,
114
- allow_origins=["*"],
115
- allow_credentials=True,
116
- allow_methods=["*"],
117
- allow_headers=["*"],
118
- )
119
-
120
- @app.on_event("startup")
121
- async def startup_event():
122
- global model_loading_thread
123
- logger.info("🚀 بدء تحميل النموذج في الخلفية...")
124
- model_loading_thread = threading.Thread(target=load_model_in_background, daemon=True)
125
- model_loading_thread.start()
126
-
127
- # بدء مراقبة الموارد بعد إنشاء التطبيق
128
- start_monitoring_thread()
129
-
130
- # نموذج الطلب والرد
131
- class ChatRequest(BaseModel):
132
- session_id: str
133
- message: str
134
-
135
- class ChatResponse(BaseModel):
136
- response: str
137
- updated_history: list[list[str]]
138
-
139
- # التحقق من حالة النموذج
140
- @app.get("/model-status")
141
- def model_status():
142
- status = "loaded" if llm else "loading"
143
- thread_alive = model_loading_thread.is_alive() if model_loading_thread else False
144
- return {"status": status, "thread_alive": thread_alive}
145
-
146
- # endpoint عرض المقاييس
147
- @app.get("/metrics")
148
- def read_metrics():
149
- return get_current_metrics()
150
-
151
- # الدردشة الرئيسية
152
- @app.post("/chat", response_model=ChatResponse)
153
- def chat(req: ChatRequest):
154
- global llm
155
- if not llm:
156
- if model_loading_thread and model_loading_thread.is_alive():
157
- raise HTTPException(status_code=503, detail="النموذج قيد التحميل، الرجاء المحاولة لاحقاً")
158
- else:
159
- raise HTTPException(status_code=500, detail="فشل تحميل النموذج")
160
-
161
- logger.info(f"📩 طلب جديد من جلسة {req.session_id}: {req.message}")
162
- try:
163
- history = get_history(req.session_id)
164
- logger.info(f"📜 التاريخ الحالي: {len(history)} رسائل")
165
- prompt = format_chat(history, req.message)
166
-
167
- output = llm(
168
- prompt,
169
- max_tokens=512,
170
- temperature=0.5,
171
- top_p=0.9,
172
- stop=["<|im_end|>", "<|im_start|>"],
173
- echo=False
174
- )
175
-
176
- reply = output['choices'][0]['text'].strip()
177
- logger.info(f"🤖 رد النموذج: {reply[:100]}...")
178
-
179
- updated_history = (history + [[req.message, reply]])[-8:]
180
- save_history(req.session_id, updated_history)
181
-
182
- return ChatResponse(response=reply, updated_history=updated_history)
183
-
184
- except Exception as e:
185
- logger.error(f"❌ خطأ أثناء المعالجة: {str(e)}")
186
- logger.exception(e)
187
- raise HTTPException(status_code=500, detail="حدث خطأ أثناء توليد الرد")
188
-
189
- # root
190
- @app.get("/")
191
- def root():
192
- return {"message": "الخادم يعمل", "status": "ok"}
193
-
194
- # endpoint قراءة سجل مراقبة الموارد
195
- @app.get("/monitor-log")
196
- def read_monitor_log():
197
- import os
198
- # استخدام المسار المطلق للتأكد من العثور على الملف
199
- base_dir = os.path.dirname(os.path.abspath(__file__))
200
- log_path = os.path.join(base_dir, "data", "monitor.log")
201
-
202
- if not os.path.exists(log_path):
203
- # إنشاء ملف فارغ إذا لم يكن موجوداً
204
- open(log_path, "w", encoding="utf-8").close()
205
- logger.warning(f"⚠️ تم إنشاء ملف سجل جديد: {log_path}")
206
-
207
- try:
208
- with open(log_path, "r", encoding="utf-8") as f:
209
- content = f.read()
210
- return {"log": content}
211
- except Exception as e:
212
- logger.error(f"❌ فشل قراءة سجل المراقبة: {str(e)}")
213
- raise HTTPException(
214
- status_code=500,
215
- detail=f"حدث خطأ أثناء قراءة السجل: {str(e)}"
216
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import os
3
+ import json
4
+ import hashlib
5
+ import logging
6
+ import threading
7
+ from pathlib import Path
8
+ from typing import List, Dict, Any, Tuple
9
+
10
+ import numpy as np
11
+ import faiss
12
+ import pickle
13
+ import ast as python_ast
14
+
15
+ from fastapi import FastAPI, HTTPException
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from pydantic import BaseModel
18
+
19
+ from sentence_transformers import SentenceTransformer
20
+ from huggingface_hub import hf_hub_download
21
+
22
+ from monitor import get_current_metrics, start_monitoring_thread
23
+ from memory import get_history, save_history
24
+
25
+ # =========================
26
+ # إعداد السجلّات
27
+ # =========================
28
+ logging.basicConfig(
29
+ level=logging.INFO,
30
+ format="🪵 [%(asctime)s] [%(levelname)s] %(message)s"
31
+ )
32
+ logger = logging.getLogger("app")
33
+
34
+ # =========================
35
+ # ثوابت ومسارات
36
+ # =========================
37
+ DATA_DIR = Path("data")
38
+ CACHE_DIR = DATA_DIR / "cache"
39
+ INDEX_DIR = DATA_DIR / "index"
40
+ FILES_DIR = DATA_DIR / "files" # تخزين النص الكامل لكل ملف
41
+ REPORT_FILE = DATA_DIR / "analysis_report.md"
42
+ GRAPH_FILE = DATA_DIR / "code_graph.json"
43
+ EMB_FILE = INDEX_DIR / "embeddings.faiss"
44
+ META_FILE = INDEX_DIR / "chunks.pkl"
45
+ HASH_MAP_FILE = INDEX_DIR / "hash_map.json"
46
+
47
+ for p in [DATA_DIR, CACHE_DIR, INDEX_DIR, FILES_DIR]:
48
+ p.mkdir(parents=True, exist_ok=True)
49
+
50
+ # Env
51
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
52
+ MODEL_REPO = os.getenv("MODEL_REPO", "Qwen/Qwen3-8B-Instruct")
53
+
54
+ # GGUF المحلي (إن توفر)
55
+ LOCAL_GGUF_REPO = os.getenv("LOCAL_GGUF_REPO", "Triangle104/Qwen3-8B-Q4_K_M-GGUF")
56
+ LOCAL_GGUF_FILE = os.getenv("LOCAL_GGUF_FILE", "qwen3-8b-q4_k_m.gguf")
57
+ LOCAL_GGUF_PATH = CACHE_DIR / LOCAL_GGUF_FILE
58
+
59
+ # تضمين
60
+ EMBED_MODEL_NAME = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
61
+ EMBED_DIM = int(os.getenv("EMBED_DIM", "384"))
62
+
63
+ # تقسيم الشيفرة
64
+ CHUNK_STEP = int(os.getenv("CHUNK_STEP", "40")) # ✅ قابل للتهيئة
65
+ MAX_FILE_BYTES = int(os.getenv("MAX_FILE_BYTES", str(10 * 1024 * 1024))) # 10MB احتياطيًا
66
+
67
+ SYSTEM_PROMPT = """<|im_start|>system
68
+ You are a senior AI code analyst. Analyze projects with hybrid indexing (code graph + retrieval).
69
+ Return structured, accurate, concise answers. Use Arabic + English labels in the final report.
70
+ <|im_end|>"""
71
+
72
+ # =========================
73
+ # الحالة العالمية والقفل
74
+ # =========================
75
+ embed_model: SentenceTransformer | None = None
76
+ faiss_index: faiss.Index | None = None
77
+ all_chunks: List[Tuple[str, str]] = [] # (file_name, chunk_text)
78
+ code_graph: Dict[str, Any] = {"files": {}}
79
+ hash_map: Dict[str, str] = {}
80
+
81
+ index_lock = threading.RLock() # ✅ لتأمين الفهرسة/الاسترجاع
82
+
83
+ # =========================
84
+ # LLM (محلي/سحابي)
85
+ # =========================
86
+ try:
87
+ from llama_cpp import Llama
88
+ except Exception:
89
+ Llama = None
90
+
91
+ llm = None # كائن النموذج المحلي إن توفر
92
+
93
+ def load_local_model_if_configured():
94
+ """تحميل GGUF محليًا إن كان مفعّلًا."""
95
+ global llm
96
+ if Llama is None:
97
+ logger.info("ℹ️ llama_cpp غير متوفر. سيتم الاعتماد على HF Inference عند الحاجة.")
98
+ return
99
+ if not LOCAL_GGUF_PATH.exists():
100
+ try:
101
+ logger.info(f"⬇️ تنزيل GGUF: {LOCAL_GGUF_REPO}/{LOCAL_GGUF_FILE}")
102
+ hf_hub_download(
103
+ repo_id=LOCAL_GGUF_REPO,
104
+ filename=LOCAL_GGUF_FILE,
105
+ local_dir=str(CACHE_DIR),
106
+ token=HF_TOKEN or None
107
+ )
108
+ except Exception as e:
109
+ logger.warning(f"⚠️ تعذر تنزيل GGUF: {e}. سيتجاهل التحميل المحلي.")
110
+ return
111
+ try:
112
+ llm = Llama(
113
+ model_path=str(LOCAL_GGUF_PATH),
114
+ n_ctx=int(os.getenv("N_CTX", "32768")),
115
+ rope_scaling={"type": "yarn", "factor": 4.0},
116
+ n_threads=int(os.getenv("N_THREADS", "2")),
117
+ n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "0")),
118
+ n_batch=int(os.getenv("N_BATCH", "64")),
119
+ use_mlock=False,
120
+ verbose=False
121
+ )
122
+ logger.info("✅ تم تحميل النموذج المحلي (GGUF).")
123
+ except Exception as e:
124
+ llm = None
125
+ logger.warning(f"⚠️ فشل تحميل النموذج المحلي: {e}")
126
+
127
+ def call_local_llm(prompt: str, max_tokens: int = 800) -> str:
128
+ if llm is None or Llama is None:
129
+ return ""
130
+ try:
131
+ res = llm(
132
+ prompt,
133
+ max_tokens=max_tokens,
134
+ temperature=0.4,
135
+ top_p=0.9,
136
+ stop=["<|im_end|>", "<|im_start|>"],
137
+ echo=False
138
+ )
139
+ return res["choices"][0]["text"].strip()
140
+ except Exception as e:
141
+ logger.warning(f"⚠️ local LLM call failed: {e}")
142
+ return ""
143
+
144
+ def call_hf_inference(prompt: str, max_new_tokens: int = 900) -> str:
145
+ import requests
146
+ if not HF_TOKEN:
147
+ raise RuntimeError("HF_TOKEN is not set and local LLM unavailable")
148
+ url = f"https://api-inference.huggingface.co/models/{MODEL_REPO}"
149
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
150
+ payload = {
151
+ "inputs": prompt,
152
+ "parameters": {
153
+ "max_new_tokens": max_new_tokens,
154
+ "temperature": 0.4,
155
+ "top_p": 0.9,
156
+ "return_full_text": False
157
+ }
158
+ }
159
+ r = requests.post(url, headers=headers, json=payload, timeout=120)
160
+ r.raise_for_status()
161
+ data = r.json()
162
+ if isinstance(data, list) and data and "generated_text" in data[0]:
163
+ return data[0]["generated_text"]
164
+ if isinstance(data, dict) and "generated_text" in data:
165
+ return data["generated_text"]
166
+ if isinstance(data, dict) and "error" in data:
167
+ raise RuntimeError(data["error"])
168
+ return json.dumps(data)
169
+
170
+ def call_llm(prompt: str, max_tokens: int = 900) -> str:
171
+ out = call_local_llm(prompt, max_tokens)
172
+ if out:
173
+ return out
174
+ return call_hf_inference(prompt, max_tokens)
175
+
176
+ # =========================
177
+ # أدوات التضمين والفهرسة
178
+ # =========================
179
+ def sha256_text(text: str) -> str:
180
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
181
+
182
+ def init_embed():
183
+ """تهيئة نموذج التضمين والقراءات من القرص."""
184
+ global embed_model, faiss_index, all_chunks, hash_map, code_graph
185
+ embed_model = SentenceTransformer(EMBED_MODEL_NAME)
186
+
187
+ if EMB_FILE.exists() and META_FILE.exists():
188
+ try:
189
+ faiss_index = faiss.read_index(str(EMB_FILE))
190
+ with open(META_FILE, "rb") as f:
191
+ all_chunks = pickle.load(f)
192
+ logger.info(f"✅ تم تحميل الفهرس ({faiss_index.ntotal} متجه) من القرص.")
193
+ except Exception as e:
194
+ logger.warning(f"⚠️ تعذر تحميل الفهرس: {e}. إنشاء فهرس جديد.")
195
+ faiss_index = faiss.IndexFlatL2(EMBED_DIM)
196
+ all_chunks = []
197
+ else:
198
+ faiss_index = faiss.IndexFlatL2(EMBED_DIM)
199
+ all_chunks = []
200
+
201
+ if HASH_MAP_FILE.exists():
202
+ try:
203
+ hash_map = json.loads(HASH_MAP_FILE.read_text(encoding="utf-8"))
204
+ except Exception:
205
+ hash_map = {}
206
+ else:
207
+ hash_map = {}
208
+
209
+ if GRAPH_FILE.exists():
210
+ try:
211
+ code_graph = json.loads(GRAPH_FILE.read_text(encoding="utf-8"))
212
+ except Exception:
213
+ code_graph = {"files": {}}
214
+ else:
215
+ code_graph = {"files": {}}
216
+
217
+ def chunk_code_structured(code: str) -> List[str]:
218
+ """قسّم الشيفرة إلى كتل حسب AST؛ وإن فشل فاقطع أسطريًا."""
219
+ try:
220
+ tree = python_ast.parse(code)
221
+ chunks: List[str] = []
222
+ lines = code.splitlines()
223
+ for node in tree.body:
224
+ s = max(getattr(node, "lineno", 1) - 1, 0)
225
+ e = getattr(node, "end_lineno", s + 1)
226
+ e = max(e, s + 1)
227
+ chunks.append("\n".join(lines[s:e]))
228
+ if chunks:
229
+ return chunks
230
+ except Exception as e:
231
+ # ✅ سجل نوع الاستثناء والموقع إن توفر
232
+ msg = f"{type(e).__name__}: {e}"
233
+ logger.debug(f"AST parse failed; fallback to line-based. Reason: {msg}")
234
+
235
+ step = CHUNK_STEP # ✅ من ENV
236
+ lines = code.splitlines()
237
+ return ["\n".join(lines[i:i + step]) for i in range(0, len(lines), step)]
238
+
239
+ def parse_code_meta(file_name: str, code: str) -> Dict[str, Any]:
240
+ """استخرج رموز/استيرادات/نداءات لملف."""
241
+ meta = {"hash": sha256_text(code), "symbols": [], "calls": [], "imports": []}
242
+ try:
243
+ tree = python_ast.parse(code)
244
+ for node in python_ast.walk(tree):
245
+ if isinstance(node, python_ast.FunctionDef):
246
+ meta["symbols"].append({"name": node.name, "kind": "function", "line": node.lineno})
247
+ elif isinstance(node, python_ast.ClassDef):
248
+ meta["symbols"].append({"name": node.name, "kind": "class", "line": node.lineno})
249
+ elif isinstance(node, python_ast.Assign):
250
+ for t in getattr(node, "targets", []):
251
+ if isinstance(t, python_ast.Name):
252
+ meta["symbols"].append({"name": t.id, "kind": "variable", "line": node.lineno})
253
+ elif isinstance(node, python_ast.Import):
254
+ for alias in node.names:
255
+ meta["imports"].append(alias.name or "")
256
+ elif isinstance(node, python_ast.ImportFrom):
257
+ meta["imports"].append(node.module or "")
258
+ elif isinstance(node, python_ast.Call):
259
+ f = node.func
260
+ if hasattr(f, "id"):
261
+ meta["calls"].append(getattr(f, "id", ""))
262
+ elif hasattr(f, "attr"):
263
+ meta["calls"].append(getattr(f, "attr", ""))
264
+ except Exception as e:
265
+ logger.debug(f"parse_code_meta failed for {file_name}: {type(e).__name__}: {e}")
266
+ return meta
267
+
268
+ def reconstruct_all_vectors() -> np.ndarray:
269
+ """إعادة بناء جميع المتجهات من الفهرس."""
270
+ if faiss_index is None or faiss_index.ntotal == 0:
271
+ return np.array([], dtype=np.float32)
272
+ xs = [faiss_index.reconstruct(i) for i in range(faiss_index.ntotal)]
273
+ return np.array(xs, dtype=np.float32) if xs else np.array([], dtype=np.float32)
274
+
275
+ def persist_index():
276
+ """حفظ الفهرس والميتا والخرائط."""
277
+ faiss.write_index(faiss_index, str(EMB_FILE))
278
+ with open(META_FILE, "wb") as f:
279
+ pickle.dump(all_chunks, f)
280
+ GRAPH_FILE.write_text(json.dumps(code_graph, ensure_ascii=False, indent=2), encoding="utf-8")
281
+ HASH_MAP_FILE.write_text(json.dumps(hash_map, ensure_ascii=False, indent=2), encoding="utf-8")
282
+
283
+ def upsert_file_to_index(file_name: str, content: str):
284
+ """إدراج/تحديث ملف داخل الفهرس والميتا والحفظ على القرص."""
285
+ global faiss_index, all_chunks, code_graph, hash_map
286
+
287
+ # ✅ حفظ نسخة أحدث على HF (القرص المحلي)
288
+ file_path = FILES_DIR / file_name
289
+ file_path.parent.mkdir(parents=True, exist_ok=True)
290
+ file_path.write_text(content, encoding="utf-8")
291
+
292
+ content_hash = sha256_text(content)
293
+ prev_hash = hash_map.get(file_name)
294
+ if prev_hash == content_hash:
295
+ return
296
+
297
+ chunks = chunk_code_structured(content)
298
+ embeds = embed_model.encode(chunks, normalize_embeddings=True)
299
+
300
+ with index_lock:
301
+ # إعادة الدمج (سهل وآمن)
302
+ old_vecs = reconstruct_all_vectors()
303
+ new_vecs = np.array(embeds, dtype=np.float32)
304
+ merged = new_vecs if old_vecs.size == 0 else np.vstack([old_vecs, new_vecs])
305
+ faiss_index = faiss.IndexFlatL2(EMBED_DIM)
306
+ faiss_index.add(merged)
307
+
308
+ all_chunks.extend([(file_name, c) for c in chunks])
309
+ code_graph["files"][file_name] = parse_code_meta(file_name, content)
310
+ hash_map[file_name] = content_hash
311
+ persist_index()
312
+
313
+ def rebuild_index_from_files():
314
+ """إعادة بناء الفهرس بالكامل من محتويات data/files/."""
315
+ global faiss_index, all_chunks, code_graph, hash_map
316
+ with index_lock:
317
+ faiss_index = faiss.IndexFlatL2(EMBED_DIM)
318
+ all_chunks = []
319
+ code_graph = {"files": {}}
320
+ hash_map = {}
321
+
322
+ for p in sorted(FILES_DIR.rglob("*")):
323
+ if not p.is_file():
324
+ continue
325
+ try:
326
+ content = p.read_text(encoding="utf-8")
327
+ except Exception:
328
+ # تجاهل الملفات الثنائية/غير النصية
329
+ continue
330
+ fname = str(p.relative_to(FILES_DIR)).replace("\\", "/")
331
+ chunks = chunk_code_structured(content)
332
+ if not chunks:
333
+ continue
334
+ embeds = embed_model.encode(chunks, normalize_embeddings=True)
335
+ vecs = np.array(embeds, dtype=np.float32)
336
+ if vecs.size:
337
+ faiss_index.add(vecs)
338
+ all_chunks.extend([(fname, c) for c in chunks])
339
+ code_graph["files"][fname] = parse_code_meta(fname, content)
340
+ hash_map[fname] = sha256_text(content)
341
+ persist_index()
342
+
343
+ def retrieve(query: str, k: int = 8) -> List[Tuple[str, str, float]]:
344
+ """استرجاع أفضل k كتل للسياق."""
345
+ if faiss_index is None or faiss_index.ntotal == 0 or embed_model is None:
346
+ return []
347
+ q = embed_model.encode([query], normalize_embeddings=True)
348
+ D, I = faiss_index.search(np.array(q, dtype=np.float32), k)
349
+ out: List[Tuple[str, str, float]] = []
350
+ for score, idx in zip(D[0], I[0]):
351
+ if idx < 0 or idx >= len(all_chunks):
352
+ continue
353
+ file_name, chunk = all_chunks[idx]
354
+ # تعزيز بسيط لو ظهر import/call من الاستعلام
355
+ boost = 1.0
356
+ meta = code_graph["files"].get(file_name, {})
357
+ imports = set(meta.get("imports", []))
358
+ calls = set(meta.get("calls", []))
359
+ if any(tok in query for tok in (list(imports) + list(calls))):
360
+ boost = 0.9
361
+ out.append((file_name, chunk, float(score) * boost))
362
+ out.sort(key=lambda x: x[2])
363
+ return out[:k]
364
+
365
+ def render_graph_overview(limit: int = 100) -> str:
366
+ lines = []
367
+ files = list(code_graph.get("files", {}).items())[:limit]
368
+ for fname, meta in files:
369
+ syms = ", ".join([f"{s.get('kind')}:{s.get('name')}" for s in meta.get("symbols", [])][:8])
370
+ imps = ", ".join(meta.get("imports", [])[:6])
371
+ cls = ", ".join(meta.get("calls", [])[:8])
372
+ lines.append(f"- File: {fname}\n Symbols: {syms}\n Imports: {imps}\n Calls: {cls}")
373
+ return "\n".join(lines)
374
+
375
+ def build_chat_prompt(history: List[List[str]], user_msg: str, extra: str = "") -> str:
376
+ msgs = [("system", SYSTEM_PROMPT)]
377
+ for u, a in history[-8:]:
378
+ msgs.append(("user", u))
379
+ msgs.append(("assistant", a))
380
+ msgs.append(("user", (user_msg or "") + ("\n" + extra if extra else "")))
381
+ out = []
382
+ for role, content in msgs:
383
+ if role == "system":
384
+ out.append(f"<|im_start|>system\n{content}<|im_end|>")
385
+ elif role == "user":
386
+ out.append(f"<|im_start|>user\n{content}<|im_end|>")
387
+ else:
388
+ out.append(f"<|im_start|>assistant\n{content}<|im_end|>")
389
+ out.append("<|im_start|>assistant\n")
390
+ return "\n".join(out)
391
+
392
+ def build_analysis_prompt(query: str, retrieved_docs: List[Tuple[str, str, float]]) -> str:
393
+ graph_overview = render_graph_overview(120)
394
+ ctx = []
395
+ for i, (fname, chunk, score) in enumerate(retrieved_docs, 1):
396
+ ctx.append(f"### Source {i}\n[File] {fname}\n[Score] {score:.4f}\n```\n{chunk}\n```")
397
+ context_block = "\n\n".join(ctx)
398
+ instructions = (
399
+ "المطلوب: تحليل الملفات المسترجعة مع السياق التالي لإنتاج تقرير تحليلي شامل يشمل:\n"
400
+ "1) التسلسل المنطقي (Logical Sequence) وخطوات التنفيذ\n"
401
+ "2) التسلسل الوظيفي (Functional Flow) والمخطط التدفق النصي (Flow Outline)\n"
402
+ "3) التبعيات بين الملفات (Dependencies) والاستدعاءات (Call Relations)\n"
403
+ "4) العلاقات بين الملفات والمتغيرات العامة ومكان تعريفها (Global Vars Map)\n"
404
+ "5) تحديد نقاط الضعف المحتملة (Logic/Security/Performance) إن وجدت\n"
405
+ "6) توصيات اصلاح عملية\n"
406
+ "صيغة المخرجات: Markdown منظم بعناوين عربية + English labels:\n"
407
+ "## نظرة عامة / Overview\n"
408
+ "## خريطة التبعيات / Dependency Map\n"
409
+ "## المخطط التدفق / Flow Outline\n"
410
+ "## تحليل منطقي ووظيفي / Logical & Functional Analysis\n"
411
+ "## المتغيرات العامة / Global Variables\n"
412
+ "## مشاكل محتملة / Potential Issues\n"
413
+ "## توصيات / Recommendations"
414
+ )
415
+ user = f"سؤال التحليل: {query}\n\n[Graph Overview]\n{graph_overview}\n\n[Retrieved Context]\n{context_block}"
416
+ prompt = (
417
+ f"<|im_start|>system\n{SYSTEM_PROMPT}\n<|im_end|>\n"
418
+ f"<|im_start|>user\n{instructions}\n\n{user}\n<|im_end|>\n"
419
+ f"<|im_start|>assistant\n"
420
+ )
421
+ return prompt
422
+
423
+ # =========================
424
+ # نماذج الطلب/الاستجابة
425
+ # =========================
426
+ class ChatRequest(BaseModel):
427
+ session_id: str
428
+ message: str
429
+
430
+ class ChatResponse(BaseModel):
431
+ response: str
432
+ updated_history: list[list[str]]
433
+
434
+ class AnalyzeRequest(BaseModel):
435
+ files: dict[str, str] # name -> content
436
+
437
+ class AnalyzeAndReportRequest(BaseModel):
438
+ session_id: str
439
+ query: str
440
+ top_k: int | None = 10
441
+
442
+ class DiffRequest(BaseModel):
443
+ modified: dict[str, str] = {} # filename -> full new content
444
+ deleted: list[str] = []
445
+
446
+ # =========================
447
+ # (اختياري) هوكس Drive/GitHub
448
+ # =========================
449
+ def maybe_upload_to_drive(local_path: Path):
450
+ """هوك اختياري: ارفع نسخة إلى جوجل درايف إن كان الاتصال مهيّأً."""
451
+ # اتركها فارغة الآن حتى تضيف اعتماد Google Drive.
452
+ # يمكن لاحقًا قراءة ENV مثل DRIVE_ENABLED=1 و OAuth كشفيرة.
453
+ pass
454
+
455
+ def maybe_commit_to_github(local_path: Path, message: str = "auto: update file"):
456
+ """هوك اختياري: قم بكومِت/دَفع تلقائي إن كان الاتصال مهيّأً."""
457
+ # اتركها فارغة الآن لحين ربط GitHub عبر توكن/Repo.
458
+ pass
459
+
460
+ # =========================
461
+ # عمليات التحليل/التقرير
462
+ # =========================
463
+ def analyze_and_report_internal(session_id: str, query: str, k: int = 10) -> str:
464
+ retrieved_docs = retrieve(query, k=k)
465
+ if not retrieved_docs:
466
+ raise HTTPException(status_code=400, detail="لا توجد بيانات مفهرسة بعد. استخدم /analyze-files أولًا.")
467
+ prompt = build_analysis_prompt(query, retrieved_docs)
468
+ report = call_llm(prompt, max_tokens=1400)
469
+ REPORT_FILE.write_text(report, encoding="utf-8")
470
+ history = get_history(session_id)
471
+ updated = (history + [[f"[ANALYZE] {query}", report]])[-8:]
472
+ save_history(session_id, updated)
473
+ return report
474
+
475
+ # =========================
476
+ # تطبيق FastAPI
477
+ # =========================
478
+ app = FastAPI()
479
+ app.add_middleware(
480
+ CORSMiddleware,
481
+ allow_origins=["*"], allow_credentials=True,
482
+ allow_methods=["*"], allow_headers=["*"]
483
+ )
484
+
485
+ @app.on_event("startup")
486
+ async def on_startup():
487
+ load_local_model_if_configured()
488
+ start_monitoring_thread()
489
+ init_embed()
490
+ logger.info("🚀 التطبيق جاهز.")
491
+
492
+ @app.get("/")
493
+ def root():
494
+ return {"message": "الخادم يعمل", "status": "ok"}
495
+
496
+ @app.get("/model-status")
497
+ def model_status():
498
+ status = "local_loaded" if llm else ("hf_ready" if HF_TOKEN else "no_model")
499
+ return {"status": status, "repo": MODEL_REPO, "local": str(LOCAL_GGUF_PATH) if llm else None}
500
+
501
+ @app.get("/metrics")
502
+ def read_metrics():
503
+ return get_current_metrics()
504
+
505
+ @app.get("/monitor-log")
506
+ def read_monitor_log():
507
+ log_path = DATA_DIR / "monitor.log"
508
+ if not log_path.exists():
509
+ log_path.touch()
510
+ return {"log": log_path.read_text(encoding="utf-8")}
511
+
512
+ @app.post("/analyze-files")
513
+ def analyze_files(req: AnalyzeRequest):
514
+ # ✅ حفظ واستبدال الأحدث محليًا + فهرسة
515
+ total_bytes = 0
516
+ for fname, content in req.files.items():
517
+ total_bytes += len(content.encode("utf-8", errors="ignore"))
518
+ if MAX_FILE_BYTES and len(content.encode("utf-8", errors="ignore")) > MAX_FILE_BYTES:
519
+ raise HTTPException(status_code=413, detail=f"الملف {fname} يتجاوز الحجم المسموح.")
520
+
521
+ for fname, content in req.files.items():
522
+ upsert_file_to_index(fname, content)
523
+ # اختياري: رفع نسخة للأرشفة السحابية
524
+ try:
525
+ maybe_upload_to_drive(FILES_DIR / fname)
526
+ except Exception as e:
527
+ logger.warning(f"Drive upload skipped for {fname}: {e}")
528
+ try:
529
+ maybe_commit_to_github(FILES_DIR / fname, "auto: analyze & save")
530
+ except Exception as e:
531
+ logger.warning(f"GitHub commit skipped for {fname}: {e}")
532
+
533
+ return {"status": "Files analyzed and cached", "files_indexed": list(req.files.keys())}
534
+
535
+ @app.post("/diff-files")
536
+ def diff_files(req: DiffRequest):
537
+ """تطبيق تعديلات Git (modified/deleted) مع إعادة بناء نظيفة للفهرس."""
538
+ # 1) حذف الملفات المطلوبة (من القرص ومن الخرائط)
539
+ for fname in req.deleted:
540
+ try:
541
+ # ح��ف من القرص
542
+ fp = FILES_DIR / fname
543
+ if fp.exists():
544
+ fp.unlink()
545
+ except Exception as e:
546
+ logger.warning(f"⚠️ تعذر حذف {fname} من القرص: {e}")
547
+ # تنظيف الخرائط
548
+ hash_map.pop(fname, None)
549
+ code_graph.get("files", {}).pop(fname, None)
550
+
551
+ # 2) كتابة/تحديث الملفات المعدّلة على القرص
552
+ for fname, content in req.modified.items():
553
+ if MAX_FILE_BYTES and len(content.encode("utf-8", errors="ignore")) > MAX_FILE_BYTES:
554
+ raise HTTPException(status_code=413, detail=f"الملف {fname} يتجاوز الحجم المسموح.")
555
+ fp = FILES_DIR / fname
556
+ fp.parent.mkdir(parents=True, exist_ok=True)
557
+ fp.write_text(content, encoding="utf-8")
558
+
559
+ # 3) إعادة بناء الفهرس بالكامل من الملفات الحالية
560
+ rebuild_index_from_files()
561
+
562
+ # 4) (اختياري) رفع النسخ المعدلة للأرشفة
563
+ for fname in req.modified.keys():
564
+ try:
565
+ maybe_upload_to_drive(FILES_DIR / fname)
566
+ except Exception as e:
567
+ logger.warning(f"Drive upload skipped for {fname}: {e}")
568
+ try:
569
+ maybe_commit_to_github(FILES_DIR / fname, "auto: diff-files update")
570
+ except Exception as e:
571
+ logger.warning(f"GitHub commit skipped for {fname}: {e}")
572
+
573
+ return {
574
+ "status": "ok",
575
+ "deleted": req.deleted,
576
+ "modified": list(req.modified.keys()),
577
+ "total_index_vectors": int(faiss_index.ntotal) if faiss_index else 0
578
+ }
579
+
580
+ @app.post("/analyze-and-report")
581
+ def analyze_and_report(req: AnalyzeAndReportRequest):
582
+ report = analyze_and_report_internal(req.session_id, req.query, k=req.top_k or 10)
583
+ return {"status": "ok", "report_path": str(REPORT_FILE), "preview": report[:1200]}
584
+
585
+ def classify_intent(history: List[List[str]], message: str) -> Dict[str, Any]:
586
+ inst = (
587
+ "أعد JSON فقط دون أي نص آخر.\n"
588
+ "المفاتيح: intent (string), confidence (0-1), action (RETRIEVE_ONLY|ANALYZE_AND_REPORT|TRACE_SUBSET|NONE), "
589
+ "targets (list of strings), reason (string).\n"
590
+ "أمثلة:\n"
591
+ "س: ما عمل الملف X؟ → {\"intent\":\"ASK_FILE_ROLE\",\"confidence\":0.9,\"action\":\"RETRIEVE_ONLY\",\"targets\":[\"X\"],\"reason\":\"...\"}\n"
592
+ "س: لماذا لا تعمل ميزة الدخول؟ → {\"intent\":\"WHY_FEATURE_NOT_WORKING\",\"confidence\":0.85,\"action\":\"ANALYZE_AND_REPORT\",\"targets\":[],\"reason\":\"...\"}\n"
593
+ "س: اين يُعرّف المتغير TOKEN وكيف يتغير؟ → {\"intent\":\"CODE_FLOW_TRACE\",\"confidence\":0.8,\"action\":\"TRACE_SUBSET\",\"targets\":[\"TOKEN\"],\"reason\":\"...\"}\n"
594
+ )
595
+ p = (
596
+ f"<|im_start|>system\n{SYSTEM_PROMPT}\n<|im_end|>\n"
597
+ f"<|im_start|>user\n{inst}\nالسؤال: {message}\nأعد JSON فقط.\n<|im_end|>\n"
598
+ f"<|im_start|>assistant\n"
599
+ )
600
+ txt = call_llm(p, max_tokens=200)
601
+ try:
602
+ start = txt.find("{")
603
+ end = txt.rfind("}")
604
+ obj = json.loads(txt[start:end+1]) if start != -1 and end != -1 else {}
605
+ except Exception:
606
+ obj = {}
607
+ if not isinstance(obj, dict):
608
+ obj = {}
609
+ obj.setdefault("intent", "UNKNOWN")
610
+ obj.setdefault("confidence", 0.0)
611
+ obj.setdefault("action", "NONE")
612
+ obj.setdefault("targets", [])
613
+ obj.setdefault("reason", "")
614
+ return obj
615
+
616
+ @app.post("/chat", response_model=ChatResponse)
617
+ def chat(req: ChatRequest):
618
+ history = get_history(req.session_id)
619
+ decision = classify_intent(history, req.message)
620
+ action = decision.get("action", "NONE")
621
+ response_text = ""
622
+
623
+ if action == "ANALYZE_AND_REPORT":
624
+ try:
625
+ report = analyze_and_report_internal(req.session_id, req.message, k=10)
626
+ response_text = "تم إنشاء تقرير تحليلي:\n\n" + report
627
+ except Exception as e:
628
+ raise HTTPException(status_code=500, detail=f"LLM error: {str(e)}")
629
+
630
+ elif action == "RETRIEVE_ONLY":
631
+ retrieved_docs = retrieve(req.message, k=6)
632
+ ctx = []
633
+ for fname, chunk, score in retrieved_docs:
634
+ ctx.append(f"From {fname} (score={score:.4f}):\n{chunk}")
635
+ extra = "\n\n[Context]\n" + "\n\n".join(ctx) + "\n\n" + render_graph_overview(60)
636
+ prompt = build_chat_prompt(history, req.message, extra)
637
+ try:
638
+ response_text = call_llm(prompt, max_tokens=700)
639
+ except Exception as e:
640
+ raise HTTPException(status_code=500, detail=f"LLM error: {str(e)}")
641
+
642
+ elif action == "TRACE_SUBSET":
643
+ targets = decision.get("targets", [])
644
+ key = " ".join(targets) if targets else req.message
645
+ retrieved_docs = retrieve(key, k=10)
646
+ ctx = []
647
+ for fname, chunk, score in retrieved_docs:
648
+ ctx.append(f"From {fname} (score={score:.4f}):\n{chunk}")
649
+ flow_query = req.message + "\nPlease trace variables/functions: " + ", ".join(targets)
650
+ prompt = build_analysis_prompt(flow_query, retrieved_docs)
651
+ try:
652
+ trace_report = call_llm(prompt, max_tokens=1200)
653
+ REPORT_FILE.write_text(trace_report, encoding="utf-8")
654
+ response_text = "تقرير التتبع:\n\n" + trace_report
655
+ except Exception as e:
656
+ raise HTTPException(status_code=500, detail=f"LLM error: {str(e)}")
657
+
658
+ else:
659
+ prompt = build_chat_prompt(history, req.message, "")
660
+ try:
661
+ response_text = call_llm(prompt, max_tokens=600)
662
+ except Exception as e:
663
+ raise HTTPException(status_code=500, detail=f"LLM error: {str(e)}")
664
+
665
+ updated = (history + [[req.message, response_text]])[-8:]
666
+ save_history(req.session_id, updated)
667
+ return ChatResponse(response=response_text, updated_history=updated)
monitor.py CHANGED
@@ -1,94 +1,52 @@
1
  import threading
2
  import time
3
  import psutil
4
- import logging
5
  from collections import deque
 
6
  import os
7
- import tracemalloc
8
- import gc
9
- import sys
10
 
11
- # إنشاء logger
 
 
12
  logger = logging.getLogger("monitor")
13
- logger.setLevel(logging.DEBUG)
14
-
15
- base_dir = os.path.dirname(os.path.abspath(__file__))
16
- log_dir = os.path.join(base_dir, "data")
17
- os.makedirs(log_dir, exist_ok=True)
18
- log_path = os.path.join(log_dir, "monitor.log")
19
-
20
- file_handler = logging.FileHandler(log_path)
21
- file_handler.setLevel(logging.DEBUG)
22
- formatter = logging.Formatter("📁 [%(asctime)s] [%(levelname)s] %(message)s")
23
- file_handler.setFormatter(formatter)
24
- logger.addHandler(file_handler)
25
-
26
- stream_handler = logging.StreamHandler()
27
- stream_handler.setFormatter(logging.Formatter("🪵 [%(asctime)s] [%(levelname)s] %(message)s"))
28
- logger.addHandler(stream_handler)
29
-
30
- # تتبع الذاكرة
31
- tracemalloc.start()
32
- logger.info("🔍 بدأ تتبع الذاكرة")
33
 
 
34
  cpu_history = deque(maxlen=10)
35
  mem_history = deque(maxlen=10)
36
- current_metrics = {'cpu': 0, 'memory': 0}
37
-
38
- def analyze_memory_objects():
39
- try:
40
- objects = gc.get_objects()
41
- logger.info(f"📦 عدد كائنات بايثون: {len(objects)}")
42
- sizes = {}
43
- for obj in objects:
44
- try:
45
- key = type(obj).__name__
46
- sizes[key] = sizes.get(key, 0) + sys.getsizeof(obj)
47
- except Exception:
48
- continue
49
-
50
- top = sorted(sizes.items(), key=lambda x: x[1], reverse=True)[:10]
51
- logger.info("🧮 أعلى 10 أنواع من الكائنات استهلاكًا:")
52
- for name, size in top:
53
- logger.info(f"🔹 {name}: {size / 1024 / 1024:.2f} MB")
54
- except Exception as e:
55
- logger.error(f"❌ خطأ أثناء تحليل كائنات الذاكرة: {str(e)}")
56
 
57
  def monitor_resources():
58
- counter = 0
59
- logger.info("🔁 بدأ تشغيل مراقبة الموارد")
60
  while True:
61
  try:
62
- process = psutil.Process(os.getpid())
63
- mem_info = process.memory_info()
64
  cpu_percent = psutil.cpu_percent(interval=0.5)
65
- mem_percent = psutil.virtual_memory().percent
66
 
67
- current_metrics['cpu'] = cpu_percent
68
- current_metrics['memory'] = mem_percent
69
- cpu_history.append(cpu_percent)
70
- mem_history.append(mem_percent)
71
 
72
- logger.info(f"🧠 RSS: {mem_info.rss / 1024**2:.2f} MB | VMS: {mem_info.vms / 1024**2:.2f} MB")
73
- current, peak = tracemalloc.get_traced_memory()
74
- logger.info(f"📊 tracemalloc: الحالي = {current / 1024**2:.2f} MB | الأعلى = {peak / 1024**2:.2f} MB")
 
 
75
 
76
- # كل دقيقة: تحليل إضافي
77
- counter += 1
78
- if counter % 12 == 0:
79
- snapshot = tracemalloc.take_snapshot()
80
- top_stats = snapshot.statistics('lineno')
81
- logger.info("📌 أعلى 5 أسطر استهلاكًا للذاكرة:")
82
- for stat in top_stats[:5]:
83
- logger.info(f" {stat}")
84
- analyze_memory_objects()
85
 
86
  except Exception as e:
87
- logger.exception(f" استثناء في مراقبة الموارد: {str(e)}")
88
 
89
  time.sleep(5)
90
 
91
  def get_current_metrics():
 
92
  return {
93
  'cpu': current_metrics['cpu'],
94
  'memory': current_metrics['memory'],
@@ -97,6 +55,7 @@ def get_current_metrics():
97
  }
98
 
99
  def start_monitoring_thread():
100
- thread = threading.Thread(target=monitor_resources, daemon=True)
101
- thread.start()
102
- logger.info("✅ تم بدء مراقبة الموارد في خيط منفصل")
 
 
1
  import threading
2
  import time
3
  import psutil
 
4
  from collections import deque
5
+ import logging
6
  import os
 
 
 
7
 
8
+ # ===== إعداد بسيط للسجل (صامت افتراضيًا) =====
9
+ VERBOSE = os.getenv("MONITOR_VERBOSE", "0") == "1"
10
+ LOG_LEVEL = logging.INFO if VERBOSE else logging.WARNING
11
  logger = logging.getLogger("monitor")
12
+ logger.setLevel(LOG_LEVEL)
13
+ if not logger.handlers:
14
+ sh = logging.StreamHandler()
15
+ sh.setLevel(LOG_LEVEL)
16
+ logger.addHandler(sh)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ # ===== مخازن المقاييس (CPU/MEM فقط) =====
19
  cpu_history = deque(maxlen=10)
20
  mem_history = deque(maxlen=10)
21
+ current_metrics = {'cpu': 0.0, 'memory': 0.0}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def monitor_resources():
24
+ """تحديث نسب CPU و Memory كل 5 ثوانٍ فقط."""
25
+ logger.debug("بدأت مراقبة CPU/MEM")
26
  while True:
27
  try:
28
+ # نسبة استعمال CPU للنظام كله (متوسط نصف ثانية)
 
29
  cpu_percent = psutil.cpu_percent(interval=0.5)
 
30
 
31
+ # نسبة استعمال الذاكرة للنظام كله
32
+ mem_percent = psutil.virtual_memory().percent
 
 
33
 
34
+ # تحديث القيم الحالية والتاريخية
35
+ current_metrics['cpu'] = float(cpu_percent)
36
+ current_metrics['memory'] = float(mem_percent)
37
+ cpu_history.append(float(cpu_percent))
38
+ mem_history.append(float(mem_percent))
39
 
40
+ if VERBOSE:
41
+ logger.info(f"CPU: {cpu_percent:.1f}% | MEM: {mem_percent:.1f}%")
 
 
 
 
 
 
 
42
 
43
  except Exception as e:
44
+ logger.exception(f"مشكلة في مراقبة الموارد: {e}")
45
 
46
  time.sleep(5)
47
 
48
  def get_current_metrics():
49
+ """تُستخدم من مسار /metrics لإرجاع القيم للواجهة."""
50
  return {
51
  'cpu': current_metrics['cpu'],
52
  'memory': current_metrics['memory'],
 
55
  }
56
 
57
  def start_monitoring_thread():
58
+ """تشغيل المراقبة في خيط منفصل."""
59
+ t = threading.Thread(target=monitor_resources, daemon=True)
60
+ t.start()
61
+ logger.debug("تم تشغيل خيط مراقبة CPU/MEM")
requirements.txt CHANGED
@@ -4,4 +4,8 @@ huggingface_hub==0.23.0
4
  llama-cpp-python==0.2.77
5
  psutil==5.9.8
6
  aiosqlite==0.20.0
7
- python-multipart==0.0.9
 
 
 
 
 
4
  llama-cpp-python==0.2.77
5
  psutil==5.9.8
6
  aiosqlite==0.20.0
7
+ python-multipart==0.0.9
8
+ sentence-transformers==2.7.0
9
+ faiss-cpu==1.8.0
10
+ networkx==3.3
11
+ transformers==4.42.4