humanvprojectceo commited on
Commit
c7fad40
·
verified ·
1 Parent(s): dc0ee32

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +424 -111
app.py CHANGED
@@ -1,16 +1,91 @@
1
  import os
2
  import json
3
  import traceback
4
- from fastapi import FastAPI, HTTPException
 
5
  from fastapi.responses import HTMLResponse, JSONResponse
6
  from pydantic import BaseModel, Field
7
- from typing import List
8
  from google import genai
9
  from google.genai import types
10
 
11
- app = FastAPI(title="Top Danesh Psychological Assessment Platform")
 
 
 
12
 
13
- # گزینه‌های استاندارد ثابت برای MBTI (در جهت‌های ۲+ تا ۲-)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  MBTI_OPTIONS = [
15
  {"score": 2, "text": "کاملاً موافقم", "label": "+۱۰۰٪", "color": 1},
16
  {"score": 1, "text": "موافق هستم", "label": "+۵۰٪", "color": 2},
@@ -19,7 +94,6 @@ MBTI_OPTIONS = [
19
  {"score": -2, "text": "کاملاً مخالفم", "label": "-۱۰۰٪", "color": 5}
20
  ]
21
 
22
- # گزینه‌های استاندارد ثابت برای اضطراب بک (BAI)
23
  BAI_OPTIONS = [
24
  {"score": 0, "text": "اصلاً (در رنج نبودم)", "label": "طبیعی (۰٪)", "color": 1},
25
  {"score": 1, "text": "خفیف (زیاد ناراحتم نکرد)", "label": "خفیف (۳۳٪)", "color": 2},
@@ -27,7 +101,6 @@ BAI_OPTIONS = [
27
  {"score": 3, "text": "شدید (غیر قابل تحمل بود)", "label": "شدید (۱۰۰٪)", "color": 5}
28
  ]
29
 
30
- # گزینه‌های استاندارد برای آزمون اهمال‌کاری تحصیلی سولومون
31
  SOLOMON_OPTIONS = [
32
  {"score": 1, "text": "هرگز", "label": "۱", "color": 1},
33
  {"score": 2, "text": "به ندرت", "label": "۲", "color": 2},
@@ -36,7 +109,7 @@ SOLOMON_OPTIONS = [
36
  {"score": 5, "text": "همیشه", "label": "۵", "color": 5}
37
  ]
38
 
39
- # دیتابیس آزمون اول: ۳۰ سوال MBTI
40
  MBTI_QUESTIONS = [
41
  {"id": 1, "text": "در رویدادهای اجتماعی، من معمولاً با افراد جدید زیادی صحبت می‌کنم، نه فقط با کسانی که می‌شناسم.", "dimension": "EI", "direction": 1},
42
  {"id": 2, "text": "من بیشتر جذب ایده‌های نوآورانه و نظری می‌شوم تا واقعیت‌های ملموس و کاربردی.", "dimension": "SN", "direction": -1},
@@ -70,7 +143,6 @@ MBTI_QUESTIONS = [
70
  {"id": 30, "text": "من ترجیح می‌دهم پروژه‌هایم را به صورت گام به گام و با آرامش پیش ببرم تا اینکه همه کارها را در دقیقه نود جمع کنم.", "dimension": "JP", "direction": 1}
71
  ]
72
 
73
- # آزمون دوم: ۲۱ سوال اضطراب بک (BAI)
74
  BAI_QUESTIONS = [
75
  {"id": 1, "text": "احساس گزگز، بی‌حسی، مورمور شدن یا کرختی در بخش‌هایی از بدنتان.", "dimension": "somatic"},
76
  {"id": 2, "text": "احساس ناگهانی داغ شدن شدید، گرگرفتگی یا افزایش حرارت بدن.", "dimension": "somatic"},
@@ -95,7 +167,6 @@ BAI_QUESTIONS = [
95
  {"id": 21, "text": "عرق کردن شدید که ناشی از دمای محیط یا فعالیت بدنی نباشد.", "dimension": "somatic"}
96
  ]
97
 
98
- # آزمون سوم: ۲۱ سوال افسردگی بک (BDI-II)
99
  BDI_QUESTIONS = [
100
  {
101
  "id": 1,
@@ -295,17 +366,17 @@ BDI_QUESTIONS = [
295
  {"score": 0, "text": "تمرکز فکری من به خوبی گذشته است و حواسم پرت نمی‌شود.", "label": "طبیعی (۰٪)", "color": 1},
296
  {"score": 1, "text": "نمی‌توانم به اندازه قبل، روی کارها یا مطالعه متمرکز بمانم.", "label": "خفیف (۳۳٪)", "color": 2},
297
  {"score": 2, "text": "برای مدت طولانی نمی‌توانم ذهنم را روی یک موضوع مشخص ثابت نگه دارم.", "label": "متوسط (۶۶٪)", "color": 4},
298
- {"score": 3, "text": "احساس می‌کنم اصلاً روی هیچ موضوعی قادر به متمرکز شدن نیستم.", "dimension": "somatic", "label": "شدید (۱۰۰٪)"}
299
  ]
300
  },
301
  {
302
  "id": 20, "text": "میزان احساس خستگی مفرط، بی‌حالی و کوفتگی بدنی روزانه در شما چگونه است؟",
303
  "dimension": "somatic",
304
  "options": [
305
- {"score": 0, "text": "بیش از حد معمول احساس خستگی، کوفتگی یا بی‌حالی نمی‌کنم.", "label": "طبیعی (۰٪)"},
306
- {"score": 1, "text": "زودتر از گذشته احساس خستگی کرده و توان بدنی‌ام تحلیل می‌رود.", "label": "خفیف (۳۳٪)"},
307
- {"score": 2, "text": "خسته‌تر از آن هستم که بتوانم کارهایی را که قبلاً به راحتی انجام می‌دادم، پیش ببرم.", "label": "متوسط (۶۶٪)"},
308
- {"score": 3, "text": "به قدری احساس خستگی مفرط دارم که قادر به شروع کارهای ساده روزانه نیستم.", "label": "شدید (۱۰۰٪)"}
309
  ]
310
  },
311
  {
@@ -320,7 +391,6 @@ BDI_QUESTIONS = [
320
  }
321
  ]
322
 
323
- # آزمون چهارم: ۲۷ سوال اهمال‌کاری تحصیلی سولومون
324
  SOLOMON_QUESTIONS = [
325
  {"id": 1, "text": "هنگام مطالعه برای امتحان مرتباً رویاپردازی می‌کنم و تمرکز کردن برایم دشوار است.", "dimension": "exam"},
326
  {"id": 2, "text": "تا لحظه‌ای که امکان داشته باشد، آماده شدن برای امتحانات را به تأخیر نمی‌اندازم.", "dimension": "exam"},
@@ -351,16 +421,30 @@ SOLOMON_QUESTIONS = [
351
  {"id": 27, "text": "علاقه‌مند هستم که عادت اهمال‌کاری و امروز و فردا کردن در انجام تکالیف را تغییر دهم.", "dimension": "paper"}
352
  ]
353
 
 
 
 
354
  class AnswerItem(BaseModel):
355
  question_id: int
356
  score: int
357
 
358
- class AnalysisRequest(BaseModel):
359
  first_name: str
360
  last_name: str
361
- test_type: str = Field(description="Type of test taken: 'mbti', 'bai', 'bdi', or 'solomon'")
 
362
  answers: List[AnswerItem]
363
 
 
 
 
 
 
 
 
 
 
 
364
  class UniversalAnalysisSchema(BaseModel):
365
  test_type: str = Field(description="The type of test taken ('mbti', 'bai', 'bdi', or 'solomon')")
366
  mbti_type: str = Field(description="MBTI 4-letter type. Leave blank if not MBTI.")
@@ -387,6 +471,16 @@ class UniversalAnalysisSchema(BaseModel):
387
  careers: List[str] = Field(description="CBT practical exercises, career paths or recovery guides (4-5 items in Persian)")
388
  detailed_analysis: str = Field(description="Detailed psychological report with beautifully structured Markdown in Persian (3-4 paragraphs)")
389
 
 
 
 
 
 
 
 
 
 
 
390
  MBTI_SYSTEM_INSTRUCTION = """
391
  شما یک روان‌شناس بالینی ارشد و متخصص حرفه‌ای آزمون‌های شخصیت‌شناسی به ویژه MBTI هستید.
392
  وظیفه شما تحلیل دقیق پاسخ‌های کاربر به آزمون ۳۰ سواله MBTI و تولید یک گزارش جامع، عمیق و علمی به زبان فارسی است.
@@ -413,6 +507,16 @@ SOLOMON_SYSTEM_INSTRUCTION = """
413
  بخش detailed_analysis را با تیترهای مارک‌داون تفکیک و تشریح کنید.
414
  """
415
 
 
 
 
 
 
 
 
 
 
 
416
  def calculate_mbti_raw(answers: List[AnswerItem]):
417
  scores = {"EI": 0, "SN": 0, "TF": 0, "JP": 0}
418
  counts = {"EI": 0, "SN": 0, "TF": 0, "JP": 0}
@@ -494,9 +598,7 @@ def calculate_bdi_raw(answers: List[AnswerItem]):
494
  return {"level": level, "total": total_score, "cognitive": cognitive_pct, "affective": affective_pct, "somatic": somatic_pct}
495
 
496
  def calculate_solomon_raw(answers: List[AnswerItem]):
497
- # گویه‌های نیازمند نمره‌گذاری معکوس طبق پرسشنامه: ۲ - ۴ - ۶ - ۱۱ - ۱۳ - ۱۵ - ۱۶ و ۲۱ - ۲۳ - ۲۵
498
  reverse_ids = {2, 4, 6, 11, 13, 15, 16, 21, 23, 25}
499
-
500
  subscales = {
501
  "exam": {"score": 0, "max": 0},
502
  "assignment": {"score": 0, "max": 0},
@@ -513,7 +615,7 @@ def calculate_solomon_raw(answers: List[AnswerItem]):
513
 
514
  raw_val = max(1, min(5, ans.score))
515
  if q_id in reverse_ids:
516
- actual_score = 6 - raw_val # نمره‌گذاری معکوس: ۵ به ۱، ۴ به ۲، ۳ به ۳، ۲ به ۴، ۱ به ۵
517
  else:
518
  actual_score = raw_val
519
 
@@ -521,7 +623,6 @@ def calculate_solomon_raw(answers: List[AnswerItem]):
521
  subscales[dim]["max"] += 5
522
  total_score += actual_score
523
 
524
- # سطح‌بندی بر اساس نمره کل بین ۲۷ تا ۱۳۵
525
  if total_score <= 67:
526
  level = "اهمال‌کاری ضعیف (مطلوب)"
527
  elif total_score <= 101:
@@ -541,6 +642,9 @@ def calculate_solomon_raw(answers: List[AnswerItem]):
541
  "paper": paper_pct
542
  }
543
 
 
 
 
544
  @app.get("/", response_class=HTMLResponse)
545
  async def get_index():
546
  try:
@@ -549,43 +653,159 @@ async def get_index():
549
  except FileNotFoundError:
550
  raise HTTPException(status_code=404, detail="فایل index.html یافت نشد.")
551
 
 
 
 
 
 
 
 
 
552
  @app.get("/api/questions")
553
  async def get_questions(test_type: str = "mbti"):
554
  if test_type == "bai":
555
- return [
556
- {
557
- "id": q["id"],
558
- "text": q["text"],
559
- "options": BAI_OPTIONS
560
- } for q in BAI_QUESTIONS
561
- ]
562
  elif test_type == "bdi":
563
- return [
564
- {
565
- "id": q["id"],
566
- "text": q["text"],
567
- "options": q["options"]
568
- } for q in BDI_QUESTIONS
569
- ]
570
  elif test_type == "solomon":
571
- return [
572
- {
573
- "id": q["id"],
574
- "text": q["text"],
575
- "options": SOLOMON_OPTIONS
576
- } for q in SOLOMON_QUESTIONS
577
- ]
578
-
579
- return [
580
- {
581
- "id": q["id"],
582
- "text": q["text"],
583
- "options": MBTI_OPTIONS
584
- } for q in MBTI_QUESTIONS
585
- ]
586
-
587
- @app.post("/api/analyze")
588
- async def analyze_personality(payload: AnalysisRequest):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  api_key = (
590
  os.environ.get("model_api") or
591
  os.environ.get("GEMINI_API_KEY") or
@@ -595,21 +815,24 @@ async def analyze_personality(payload: AnalysisRequest):
595
  )
596
 
597
  if not api_key:
598
- return JSONResponse(status_code=500, content={"error": "کلید ارتباطی یافت نشد. تنظیمات سرور را بررسی کنید."})
599
-
600
- if payload.test_type == "bai":
601
- calc = calculate_bai_raw(payload.answers)
 
 
 
602
  system_instruction = BAI_SYSTEM_INSTRUCTION
603
  q_map = {q["id"]: q for q in BAI_QUESTIONS}
604
  answers_text_list = []
605
- for ans in payload.answers:
606
- q = q_map.get(ans.question_id)
607
  if q:
608
- score_str = {0: "اصلا", 1: "خفیف", 2: "متوسط", 3: "شدید"}.get(ans.score, "نامشخص")
609
- answers_text_list.append(f"سوال {ans.question_id}: '{q['text']}' -> پاسخ: {score_str}")
610
  answers_summary = "\n".join(answers_text_list)
611
  prompt_data = f"""
612
- کاربر ارزیابی‌شونده: {payload.first_name} {payload.last_name}
613
  آزمون انجام شده: سنجش بالینی اضطراب بک (BAI)
614
 
615
  محاسبات ریاضی پایه:
@@ -622,19 +845,19 @@ async def analyze_personality(payload: AnalysisRequest):
622
  پاسخ‌های کاربر:
623
  {answers_summary}
624
  """
625
- elif payload.test_type == "bdi":
626
- calc = calculate_bdi_raw(payload.answers)
627
  system_instruction = BDI_SYSTEM_INSTRUCTION
628
  q_map = {q["id"]: q for q in BDI_QUESTIONS}
629
  answers_text_list = []
630
- for ans in payload.answers:
631
- q = q_map.get(ans.question_id)
632
  if q:
633
- selected_opt = next((o["text"] for o in q["options"] if o["score"] == ans.score), "نامشخص")
634
- answers_text_list.append(f"سوال {ans.question_id}: '{q['text']}' -> پاسخ کاربر: {selected_opt} (نمره: {ans.score})")
 
635
  answers_summary = "\n".join(answers_text_list)
636
  prompt_data = f"""
637
- کاربر ارزیابی‌شونده: {payload.first_name} {payload.last_name}
638
  آزمون انجام شده: سنجش بالینی افسردگی بک (BDI-II)
639
 
640
  محاسبات ریاضی پایه:
@@ -647,22 +870,21 @@ async def analyze_personality(payload: AnalysisRequest):
647
  پاسخ‌های تفصیلی کاربر:
648
  {answers_summary}
649
  """
650
- elif payload.test_type == "solomon":
651
- calc = calculate_solomon_raw(payload.answers)
652
  system_instruction = SOLOMON_SYSTEM_INSTRUCTION
653
  q_map = {q["id"]: q for q in SOLOMON_QUESTIONS}
654
  answers_text_list = []
655
- for ans in payload.answers:
656
- q = q_map.get(ans.question_id)
657
  if q:
658
- selected_opt = next((o["text"] for o in SOLOMON_OPTIONS if o["score"] == ans.score), "نامشخص")
659
- answers_text_list.append(f"سوال {ans.question_id}: '{q['text']}' -> پاسخ: {selected_opt} (نمره خام: {ans.score})")
660
  answers_summary = "\n".join(answers_text_list)
661
  prompt_data = f"""
662
- کاربر ارزیابی‌شونده: {payload.first_name} {payload.last_name}
663
  آزمون انجام شده: سنجش علمی اهمال‌کاری تحصیلی سولومون
664
 
665
- محاسبات ریاضی پایه (از این محاسبات دقیق در خروجی JSON استفاده کنید):
666
  - فیلد test_type: مقدار "solomon" قرار گیرد.
667
  - فیلد solomon_level: مقدار "{calc['level']}" قرار گیرد.
668
  - فیلد somatic_score (درصد امتحانات): مقدار {calc['exam']} قرار گیرد.
@@ -676,18 +898,17 @@ async def analyze_personality(payload: AnalysisRequest):
676
  {answers_summary}
677
  """
678
  else:
679
- calc = calculate_mbti_raw(payload.answers)
680
  system_instruction = MBTI_SYSTEM_INSTRUCTION
681
  q_map = {q["id"]: q for q in MBTI_QUESTIONS}
682
  answers_text_list = []
683
- for ans in payload.answers:
684
- q = q_map.get(ans.question_id)
685
  if q:
686
- score_str = {2: "کاملاً موافق", 1: "موافق", 0: "ممتنع", -1: "مخالف", -2: "کاملاً مخالف"}.get(ans.score, "نامشخص")
687
- answers_text_list.append(f"سوال {ans.question_id}: '{q['text']}' -> پاسخ: {score_str}")
688
  answers_summary = "\n".join(answers_text_list)
689
  prompt_data = f"""
690
- کاربر ارزیابی‌شونده: {payload.first_name} {payload.last_name}
691
  آزمون انجام شده: تیپ‌شناسی شخصیت MBTI
692
 
693
  محاسبات ریاضی پایه:
@@ -705,38 +926,130 @@ async def analyze_personality(payload: AnalysisRequest):
705
  client = genai.Client(api_key=api_key)
706
  model = "gemini-3.5-flash"
707
 
708
- try:
709
- generate_content_config = types.GenerateContentConfig(
710
- system_instruction=system_instruction,
711
- response_mime_type="application/json",
712
- response_schema=UniversalAnalysisSchema,
713
- thinking_config=types.ThinkingConfig(
714
- thinking_level="high",
715
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  )
717
- response = client.models.generate_content(
718
- model=model,
719
- contents=prompt_data,
720
- config=generate_content_config,
 
 
 
721
  )
722
- except Exception as primary_err:
723
- print(f"Primary API call failed. Using standard fallback: {str(primary_err)}")
724
- generate_content_config = types.GenerateContentConfig(
725
- system_instruction=system_instruction,
726
- response_mime_type="application/json",
727
- response_schema=UniversalAnalysisSchema,
 
728
  )
729
- response = client.models.generate_content(
730
- model=model,
731
- contents=prompt_data,
732
- config=generate_content_config,
 
 
 
733
  )
734
-
735
- result_json = json.loads(response.text)
736
- return result_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
 
738
  except Exception as e:
739
- print("\n" + "="*50)
740
  traceback.print_exc()
741
- print("="*50 + "\n")
742
- return JSONResponse(status_code=500, content={"error": f"بروز خطا در پردازش مدل هوشمند: {str(e)}"})
 
1
  import os
2
  import json
3
  import traceback
4
+ import datetime
5
+ from fastapi import FastAPI, HTTPException, Cookie, Response, Depends, status
6
  from fastapi.responses import HTMLResponse, JSONResponse
7
  from pydantic import BaseModel, Field
8
+ from typing import List, Optional
9
  from google import genai
10
  from google.genai import types
11
 
12
+ # کتابخانه‌های ارتباط با دیتابیس PostgreSQL
13
+ from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, JSON
14
+ from sqlalchemy.ext.declarative import declarative_base
15
+ from sqlalchemy.orm import sessionmaker, relationship, Session
16
 
17
+ app = FastAPI(title="Top Danesh Psychological Assessment Platform - Counselor Engine")
18
+
19
+ # =====================================================================
20
+ # تنظیمات دیتابیس NEON POSTGRESQL
21
+ # =====================================================================
22
+ DATABASE_URL = "postgresql://neondb_owner:npg_jhAQnM0UHwp3@ep-flat-meadow-aid7y57p-pooler.c-4.us-east-1.aws.neon.tech/neondb?sslmode=require"
23
+
24
+ try:
25
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
26
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
27
+ Base = declarative_base()
28
+ except Exception as db_init_err:
29
+ print(f"خطا در ایجاد موتور دیتابیس: {str(db_init_err)}")
30
+ raise db_init_err
31
+
32
+ # وابستگی تزریق دیتابیس به روت‌ها (Dependency Injection)
33
+ def get_db():
34
+ db = SessionLocal()
35
+ try:
36
+ yield db
37
+ finally:
38
+ db.close()
39
+
40
+ # =====================================================================
41
+ # مدل‌های جدول دیتابیس (ORM)
42
+ # =====================================================================
43
+ class UserDB(Base):
44
+ __tablename__ = "users"
45
+ phone = Column(String(15), primary_key=True, index=True)
46
+ first_name = Column(String(100), nullable=False)
47
+ last_name = Column(String(100), nullable=False)
48
+ created_at = Column(DateTime, default=datetime.datetime.utcnow)
49
+
50
+ submissions = relationship("TestSubmissionDB", back_populates="user", cascade="all, delete-orphan")
51
+
52
+ class TestSubmissionDB(Base):
53
+ __tablename__ = "test_submissions"
54
+ id = Column(Integer, primary_key=True, index=True)
55
+ phone = Column(String(15), ForeignKey("users.phone"), nullable=False)
56
+ test_type = Column(String(50), nullable=False) # mbti, bai, bdi, solomon
57
+ answers = Column(JSON, nullable=False) # ساختار پاسخ‌های خام دریافتی
58
+ calculated_scores = Column(JSON, nullable=False) # مقادیر و درصدهای نمرات محاسبه‌شده در پایتون
59
+ analysis_json = Column(JSON, nullable=True) # ذخیره کش تحلیل هوش مصنوعی پس از اولین فراخوانی
60
+ created_at = Column(DateTime, default=datetime.datetime.utcnow)
61
+
62
+ user = relationship("UserDB", back_populates="submissions")
63
+
64
+ # ایجاد ساختار جداول در صورت عدم وجود در Neon
65
+ try:
66
+ Base.metadata.create_all(bind=engine)
67
+ except Exception as table_creation_err:
68
+ print(f"خطا در ساخت جداول دیتابیس: {str(table_creation_err)}")
69
+
70
+ # =====================================================================
71
+ # تنظیمات احراز هویت مشاور (مبتنی بر Cookie)
72
+ # =====================================================================
73
+ MOSHAVER_USERNAME = os.environ.get("MOSHAVER_USERNAME", "topdanesh_admin")
74
+ MOSHAVER_PASSWORD = os.environ.get("MOSHAVER_PASSWORD", "danesh@2026")
75
+ MOSHAVER_SESSION_TOKEN = os.environ.get("MOSHAVER_TOKEN", "MoshaverSecureToken2026")
76
+
77
+ def verify_moshaver_auth(moshaver_session: Optional[str] = Cookie(None)):
78
+ """اعتبارسنجی ساده و امن دسترسی به پنل مشاور با کوکی"""
79
+ if not moshaver_session or moshaver_session != MOSHAVER_SESSION_TOKEN:
80
+ raise HTTPException(
81
+ status_code=status.HTTP_401_UNAUTHORIZED,
82
+ detail="دسترسی غیرمجاز. لطفا ابتدا وارد پنل مشاور شوید."
83
+ )
84
+ return moshaver_session
85
+
86
+ # =====================================================================
87
+ # گزینه‌های استاندارد ثابت برای آزمون‌ها
88
+ # =====================================================================
89
  MBTI_OPTIONS = [
90
  {"score": 2, "text": "کاملاً موافقم", "label": "+۱۰۰٪", "color": 1},
91
  {"score": 1, "text": "موافق هستم", "label": "+۵۰٪", "color": 2},
 
94
  {"score": -2, "text": "کاملاً مخالفم", "label": "-۱۰۰٪", "color": 5}
95
  ]
96
 
 
97
  BAI_OPTIONS = [
98
  {"score": 0, "text": "اصلاً (در رنج نبودم)", "label": "طبیعی (۰٪)", "color": 1},
99
  {"score": 1, "text": "خفیف (زیاد ناراحتم نکرد)", "label": "خفیف (۳۳٪)", "color": 2},
 
101
  {"score": 3, "text": "شدید (غیر قابل تحمل بود)", "label": "شدید (۱۰۰٪)", "color": 5}
102
  ]
103
 
 
104
  SOLOMON_OPTIONS = [
105
  {"score": 1, "text": "هرگز", "label": "۱", "color": 1},
106
  {"score": 2, "text": "به ندرت", "label": "۲", "color": 2},
 
109
  {"score": 5, "text": "همیشه", "label": "۵", "color": 5}
110
  ]
111
 
112
+ # دیتابیس سوالات آزمون‌ها (بدون تغییر)
113
  MBTI_QUESTIONS = [
114
  {"id": 1, "text": "در رویدادهای اجتماعی، من معمولاً با افراد جدید زیادی صحبت می‌کنم، نه فقط با کسانی که می‌شناسم.", "dimension": "EI", "direction": 1},
115
  {"id": 2, "text": "من بیشتر جذب ایده‌های نوآورانه و نظری می‌شوم تا واقعیت‌های ملموس و کاربردی.", "dimension": "SN", "direction": -1},
 
143
  {"id": 30, "text": "من ترجیح می‌دهم پروژه‌هایم را به صورت گام به گام و با آرامش پیش ببرم تا اینکه همه کارها را در دقیقه نود جمع کنم.", "dimension": "JP", "direction": 1}
144
  ]
145
 
 
146
  BAI_QUESTIONS = [
147
  {"id": 1, "text": "احساس گزگز، بی‌حسی، مورمور شدن یا کرختی در بخش‌هایی از بدنتان.", "dimension": "somatic"},
148
  {"id": 2, "text": "احساس ناگهانی داغ شدن شدید، گرگرفتگی یا افزایش حرارت بدن.", "dimension": "somatic"},
 
167
  {"id": 21, "text": "عرق کردن شدید که ناشی از دمای محیط یا فعالیت بدنی نباشد.", "dimension": "somatic"}
168
  ]
169
 
 
170
  BDI_QUESTIONS = [
171
  {
172
  "id": 1,
 
366
  {"score": 0, "text": "تمرکز فکری من به خوبی گذشته است و حواسم پرت نمی‌شود.", "label": "طبیعی (۰٪)", "color": 1},
367
  {"score": 1, "text": "نمی‌توانم به اندازه قبل، روی کارها یا مطالعه متمرکز بمانم.", "label": "خفیف (۳۳٪)", "color": 2},
368
  {"score": 2, "text": "برای مدت طولانی نمی‌توانم ذهنم را روی یک موضوع مشخص ثابت نگه دارم.", "label": "متوسط (۶۶٪)", "color": 4},
369
+ {"score": 3, "text": "احساس می‌کنم اصلاً روی هیچ موضوعی قادر به متمرکز شدن نیستم.", "label": "شدید (۱۰۰٪)", "color": 5}
370
  ]
371
  },
372
  {
373
  "id": 20, "text": "میزان احساس خستگی مفرط، بی‌حالی و کوفتگی بدنی روزانه در شما چگونه است؟",
374
  "dimension": "somatic",
375
  "options": [
376
+ {"score": 0, "text": "بیش از حد معمول احساس خستگی، کوفتگی یا بی‌حالی نمی‌کنم.", "label": "طبیعی (۰٪)", "color": 1},
377
+ {"score": 1, "text": "زودتر از گذشته احساس خستگی کرده و توان بدنی‌ام تحلیل می‌رود.", "label": "خفیف (۳۳٪)", "color": 2},
378
+ {"score": 2, "text": "خسته‌تر از آن هستم که بتوانم کارهایی را که قبلاً به راحتی انجام می‌دادم، پیش ببرم.", "label": "متوسط (۶۶٪)", "color": 4},
379
+ {"score": 3, "text": "به قدری احساس خستگی مفرط دارم که قادر به شروع کارهای ساده روزانه نیستم.", "label": "شدید (۱۰۰٪)", "color": 5}
380
  ]
381
  },
382
  {
 
391
  }
392
  ]
393
 
 
394
  SOLOMON_QUESTIONS = [
395
  {"id": 1, "text": "هنگام مطالعه برای امتحان مرتباً رویاپردازی می‌کنم و تمرکز کردن برایم دشوار است.", "dimension": "exam"},
396
  {"id": 2, "text": "تا لحظه‌ای که امکان داشته باشد، آماده شدن برای امتحانات را به تأخیر نمی‌اندازم.", "dimension": "exam"},
 
421
  {"id": 27, "text": "علاقه‌مند هستم که عادت اهمال‌کاری و امروز و فردا کردن در انجام تکالیف را تغییر دهم.", "dimension": "paper"}
422
  ]
423
 
424
+ # =====================================================================
425
+ # مدل‌های اعتبارسنجی ورودی Pydantic
426
+ # =====================================================================
427
  class AnswerItem(BaseModel):
428
  question_id: int
429
  score: int
430
 
431
+ class UserSubmissionRequest(BaseModel):
432
  first_name: str
433
  last_name: str
434
+ phone: str = Field(pattern=r"^\+?[0-9]{10,15}$", description="شماره تلفن مراجع برای یکتا بودن پرونده")
435
+ test_type: str = Field(description="نوع آزمون گرفته شده: 'mbti'، 'bai'، 'bdi'، یا 'solomon'")
436
  answers: List[AnswerItem]
437
 
438
+ class LoginRequest(BaseModel):
439
+ username: str
440
+ password: str
441
+
442
+ class TrendAnalysisRequest(BaseModel):
443
+ test_type: str = Field(description="مجموعه آزمونی که تحلیل روند آن مورد نیاز است")
444
+
445
+ # =====================================================================
446
+ # ساختار خروجی تحلیل‌های تکی و روند سیستم هوش مصنوعی
447
+ # =====================================================================
448
  class UniversalAnalysisSchema(BaseModel):
449
  test_type: str = Field(description="The type of test taken ('mbti', 'bai', 'bdi', or 'solomon')")
450
  mbti_type: str = Field(description="MBTI 4-letter type. Leave blank if not MBTI.")
 
471
  careers: List[str] = Field(description="CBT practical exercises, career paths or recovery guides (4-5 items in Persian)")
472
  detailed_analysis: str = Field(description="Detailed psychological report with beautifully structured Markdown in Persian (3-4 paragraphs)")
473
 
474
+ class TrendAnalysisSchema(BaseModel):
475
+ test_type: str = Field(description="نوع آزمون‌های مقایسه شده")
476
+ overall_status: str = Field(description="وضعیت کلی روند مراجع به زبان فارسی (مثال: رو به بهبود چشمگیر، نوسانی، پسرفت خفیف، پایدار بدون تغییر)")
477
+ trend_summary: str = Field(description="خلاصه روند تغییرات در ۲ الی ۳ جمله به زبان فارسی")
478
+ detailed_analysis: str = Field(description="تحلیل عمیق و مقایسه‌ای پیشرفت یا پسرفت در ابعاد مختلف آزمون‌ها با تیتربندی شکیل مارک‌داون فارسی")
479
+ advisor_guide: str = Field(description="راهنمای عملی و راهکارهای درمانی متناسب به تفکیک برای جلسات آینده مشاور جهت مداخله (۴ الی ۵ مورد به زبان فارسی)")
480
+
481
+ # =====================================================================
482
+ # دستورالعمل‌های سیستمی هوش مصنوعی (System Instructions)
483
+ # =====================================================================
484
  MBTI_SYSTEM_INSTRUCTION = """
485
  شما یک روان‌شناس بالینی ارشد و متخصص حرفه‌ای آزمون‌های شخصیت‌شناسی به ویژه MBTI هستید.
486
  وظیفه شما تحلیل دقیق پاسخ‌های کاربر به آزمون ۳۰ سواله MBTI و تولید یک گزارش جامع، عمیق و علمی به زبان فارسی است.
 
507
  بخش detailed_analysis را با تیترهای مارک‌داون تفکیک و تشریح کنید.
508
  """
509
 
510
+ TREND_SYSTEM_INSTRUCTION = """
511
+ شما یک سوپروایزر بالینی ارشد و متخصص روان‌درمانی تحلیلی هستید.
512
+ داده‌های فرستاده شده شامل تاریخچه چندین آزمون متوالی روان‌شناختی یک مراجع با تاریخ و درصدهای دقیق در هر زیرمقیاس است.
513
+ شما باید روند صعودی یا نزولی بهبودی یا پسرفت علائم را با هم مقایسه کرده، الگوهای رفتاری یا تحصیلی مراجع را موشکافی کنید و یک گزارش فوق‌العاده منسجم و تخصصی همراه با راهنمای مداخل�� درمانی برای هدایت جلسات بعدی روان‌شناس/مشاور به زبان فارسی آماده کنید.
514
+ مخاطب مستقیم شما «مشاورِ مراجع» است؛ پس از اصطلاحات روان‌شناسی دقیق و توصیه‌های تخصصی استفاده کنید.
515
+ """
516
+
517
+ # =====================================================================
518
+ # توابع تحلیل محاسباتی و فرمول‌نویسی ریاضی آزمون‌ها (پایتون)
519
+ # =====================================================================
520
  def calculate_mbti_raw(answers: List[AnswerItem]):
521
  scores = {"EI": 0, "SN": 0, "TF": 0, "JP": 0}
522
  counts = {"EI": 0, "SN": 0, "TF": 0, "JP": 0}
 
598
  return {"level": level, "total": total_score, "cognitive": cognitive_pct, "affective": affective_pct, "somatic": somatic_pct}
599
 
600
  def calculate_solomon_raw(answers: List[AnswerItem]):
 
601
  reverse_ids = {2, 4, 6, 11, 13, 15, 16, 21, 23, 25}
 
602
  subscales = {
603
  "exam": {"score": 0, "max": 0},
604
  "assignment": {"score": 0, "max": 0},
 
615
 
616
  raw_val = max(1, min(5, ans.score))
617
  if q_id in reverse_ids:
618
+ actual_score = 6 - raw_val
619
  else:
620
  actual_score = raw_val
621
 
 
623
  subscales[dim]["max"] += 5
624
  total_score += actual_score
625
 
 
626
  if total_score <= 67:
627
  level = "اهمال‌کاری ضعیف (مطلوب)"
628
  elif total_score <= 101:
 
642
  "paper": paper_pct
643
  }
644
 
645
+ # =====================================================================
646
+ # روت‌های وب سرویس و صفحات HTML
647
+ # =====================================================================
648
  @app.get("/", response_class=HTMLResponse)
649
  async def get_index():
650
  try:
 
653
  except FileNotFoundError:
654
  raise HTTPException(status_code=404, detail="فایل index.html یافت نشد.")
655
 
656
+ @app.get("/moshaver", response_class=HTMLResponse)
657
+ async def get_moshaver_panel():
658
+ try:
659
+ with open("moshaver.html", "r", encoding="utf-8") as f:
660
+ return f.read()
661
+ except FileNotFoundError:
662
+ raise HTTPException(status_code=404, detail="فایل moshaver.html یافت نشد.")
663
+
664
  @app.get("/api/questions")
665
  async def get_questions(test_type: str = "mbti"):
666
  if test_type == "bai":
667
+ return [{"id": q["id"], "text": q["text"], "options": BAI_OPTIONS} for q in BAI_QUESTIONS]
 
 
 
 
 
 
668
  elif test_type == "bdi":
669
+ return [{"id": q["id"], "text": q["text"], "options": q["options"]} for q in BDI_QUESTIONS]
 
 
 
 
 
 
670
  elif test_type == "solomon":
671
+ return [{"id": q["id"], "text": q["text"], "options": SOLOMON_OPTIONS} for q in SOLOMON_QUESTIONS]
672
+ return [{"id": q["id"], "text": q["text"], "options": MBTI_OPTIONS} for q in MBTI_QUESTIONS]
673
+
674
+ # =====================================================================
675
+ # ثبت و ذخیره‌سازی داده‌های مراجع در Neon PostgreSQL
676
+ # =====================================================================
677
+ @app.post("/api/submit_test")
678
+ async def submit_test(payload: UserSubmissionRequest, db: Session = Depends(get_db)):
679
+ """ثبت پاسخ‌های مراجع در دیتابیس بدون فراخوانی مستقیم هوش مصنوعی جهت سرعت عمل بالا و صرفه‌جویی در هزینه"""
680
+ try:
681
+ # ۱. بررسی وجود پرونده کاربر با شماره موبایل
682
+ user = db.query(UserDB).filter(UserDB.phone == payload.phone).first()
683
+ if not user:
684
+ user = UserDB(
685
+ phone=payload.phone,
686
+ first_name=payload.first_name,
687
+ last_name=payload.last_name
688
+ )
689
+ db.add(user)
690
+ db.commit()
691
+ db.refresh(user)
692
+ else:
693
+ # به‌روزرسانی مشخصات در صورت تغییر احتمالی نام
694
+ user.first_name = payload.first_name
695
+ user.last_name = payload.last_name
696
+ db.commit()
697
+
698
+ # ۲. محاسبه ریاضی نمرات براساس نوع تست
699
+ if payload.test_type == "bai":
700
+ calc = calculate_bai_raw(payload.answers)
701
+ elif payload.test_type == "bdi":
702
+ calc = calculate_bdi_raw(payload.answers)
703
+ elif payload.test_type == "solomon":
704
+ calc = calculate_solomon_raw(payload.answers)
705
+ else:
706
+ calc = calculate_mbti_raw(payload.answers)
707
+
708
+ # ۳. ذخیره‌سازی سابقه تست
709
+ submission = TestSubmissionDB(
710
+ phone=payload.phone,
711
+ test_type=payload.test_type,
712
+ answers=[ans.dict() for ans in payload.answers],
713
+ calculated_scores=calc,
714
+ analysis_json=None # به طور تعویقی (Lazy) وقتی مشاور کلیک کرد لود خواهد شد
715
+ )
716
+ db.add(submission)
717
+ db.commit()
718
+
719
+ return {
720
+ "success": True,
721
+ "message": "پاسخ‌های شما با موفقیت در دیتابیس یکپارچه تاپ دانش ثبت گردید. پرونده برای بررسی علمی مشاور آماده است."
722
+ }
723
+
724
+ except Exception as e:
725
+ traceback.print_exc()
726
+ return JSONResponse(status_code=500, content={"error": f"خطا در ثبت آزمون: {str(e)}"})
727
+
728
+ # =====================================================================
729
+ # APIهای پنل مدیریت مشاور (احراز هویت شده)
730
+ # =====================================================================
731
+ @app.post("/api/moshaver/login")
732
+ async def moshaver_login(payload: LoginRequest, response: Response):
733
+ """احراز هویت مشاور و تخصیص کوکی نشست امن"""
734
+ if payload.username == MOSHAVER_USERNAME and payload.password == MOSHAVER_PASSWORD:
735
+ # تنظیم کوکی نشست امن برای مرورگر مشاور به مدت ۳۰ روز
736
+ response.set_cookie(
737
+ key="moshaver_session",
738
+ value=MOSHAVER_SESSION_TOKEN,
739
+ max_age=30 * 24 * 60 * 60,
740
+ httponly=True,
741
+ samesite="lax"
742
+ )
743
+ return {"success": True, "message": "لاگین موفقیت‌آمیز بود."}
744
+ else:
745
+ raise HTTPException(status_code=401, detail="نام کاربری یا رمز عبور مشاور اشتباه است.")
746
+
747
+ @app.post("/api/moshaver/logout")
748
+ async def moshaver_logout(response: Response):
749
+ response.delete_cookie("moshaver_session")
750
+ return {"success": True}
751
+
752
+ @app.get("/api/moshaver/users")
753
+ async def get_all_users(db: Session = Depends(get_db), auth: str = Depends(verify_moshaver_auth)):
754
+ """دریافت لیست کلیه مراجعان عضو سامانه همراه با تعداد آزمون‌های هر کدام"""
755
+ users = db.query(UserDB).all()
756
+ output = []
757
+ for u in users:
758
+ test_count = db.query(TestSubmissionDB).filter(TestSubmissionDB.phone == u.phone).count()
759
+ output.append({
760
+ "phone": u.phone,
761
+ "first_name": u.first_name,
762
+ "last_name": u.last_name,
763
+ "created_at": u.created_at.strftime("%Y-%m-%d %H:%M"),
764
+ "test_count": test_count
765
+ })
766
+ return output
767
+
768
+ @app.get("/api/moshaver/user/{phone}/tests")
769
+ async def get_user_test_history(phone: str, db: Session = Depends(get_db), auth: str = Depends(verify_moshaver_auth)):
770
+ """دریافت سوابق آزمون‌های ثبت‌شده برای مراجع خاص به همراه تاریخ"""
771
+ user = db.query(UserDB).filter(UserDB.phone == phone).first()
772
+ if not user:
773
+ raise HTTPException(status_code=404, detail="پرونده مراجع یافت نشد.")
774
+
775
+ subs = db.query(TestSubmissionDB).filter(TestSubmissionDB.phone == phone).order_by(TestSubmissionDB.created_at.desc()).all()
776
+ history = []
777
+ for s in subs:
778
+ history.append({
779
+ "id": s.id,
780
+ "test_type": s.test_type,
781
+ "calculated_scores": s.calculated_scores,
782
+ "is_analyzed": s.analysis_json is not None,
783
+ "created_at": s.created_at.strftime("%Y-%m-%d %H:%M")
784
+ })
785
+ return {
786
+ "user": {
787
+ "first_name": user.first_name,
788
+ "last_name": user.last_name,
789
+ "phone": user.phone
790
+ },
791
+ "history": history
792
+ }
793
+
794
+ # =====================================================================
795
+ # موتور تحلیل تعویقی هوش مصنوعی (Lazy / On-demand Analysis)
796
+ # =====================================================================
797
+ @app.get("/api/moshaver/test/{test_id}/analysis")
798
+ async def get_test_analysis(test_id: int, db: Session = Depends(get_db), auth: str = Depends(verify_moshaver_auth)):
799
+ """ارسال نمرات آزمون به Gemini در صورت عدم وجود کش قبلی و ذخیره نهایی گزارش در دیتابیس"""
800
+ sub = db.query(TestSubmissionDB).filter(TestSubmissionDB.id == test_id).first()
801
+ if not sub:
802
+ raise HTTPException(status_code=404, detail="آزمون مورد نظر یافت نشد.")
803
+
804
+ # در صورتی که تحلیل قبلا ساخته شده، مستقیما بازگردانده می‌شود
805
+ if sub.analysis_json:
806
+ return sub.analysis_json
807
+
808
+ # دریافت کلید API هوش مصنوعی از متغیرهای سیستم
809
  api_key = (
810
  os.environ.get("model_api") or
811
  os.environ.get("GEMINI_API_KEY") or
 
815
  )
816
 
817
  if not api_key:
818
+ raise HTTPException(status_code=500, detail="کلید API مربوط به Google Gemini تنظیم نشده است.")
819
+
820
+ # تولید پرامپت براساس آزمون ثبت شده
821
+ calc = sub.calculated_scores
822
+ user = sub.user
823
+
824
+ if sub.test_type == "bai":
825
  system_instruction = BAI_SYSTEM_INSTRUCTION
826
  q_map = {q["id"]: q for q in BAI_QUESTIONS}
827
  answers_text_list = []
828
+ for ans in sub.answers:
829
+ q = q_map.get(ans["question_id"])
830
  if q:
831
+ score_str = {0: "اصلا", 1: "خفیف", 2: "متوسط", 3: "شدید"}.get(ans["score"], "نامشخص")
832
+ answers_text_list.append(f"سوال {ans['question_id']}: '{q['text']}' -> پاسخ: {score_str}")
833
  answers_summary = "\n".join(answers_text_list)
834
  prompt_data = f"""
835
+ کاربر ارزیابی‌شونده: {user.first_name} {user.last_name}
836
  آزمون انجام شده: سنجش بالینی اضطراب بک (BAI)
837
 
838
  محاسبات ریاضی پایه:
 
845
  پاسخ‌های کاربر:
846
  {answers_summary}
847
  """
848
+ elif sub.test_type == "bdi":
 
849
  system_instruction = BDI_SYSTEM_INSTRUCTION
850
  q_map = {q["id"]: q for q in BDI_QUESTIONS}
851
  answers_text_list = []
852
+ for ans in sub.answers:
853
+ q = q_map.get(ans["question_id"])
854
  if q:
855
+ # یافتن تکست گزینه انتخاب شده
856
+ selected_opt = next((o["text"] for o in q["options"] if o["score"] == ans["score"]), "نامشخص")
857
+ answers_text_list.append(f"سوال {ans['question_id']}: '{q['text']}' -> پاسخ کاربر: {selected_opt} (نمره: {ans['score']})")
858
  answers_summary = "\n".join(answers_text_list)
859
  prompt_data = f"""
860
+ کاربر ارزیابی‌شونده: {user.first_name} {user.last_name}
861
  آزمون انجام شده: سنجش بالینی افسردگی بک (BDI-II)
862
 
863
  محاسبات ریاضی پایه:
 
870
  پاسخ‌های تفصیلی کاربر:
871
  {answers_summary}
872
  """
873
+ elif sub.test_type == "solomon":
 
874
  system_instruction = SOLOMON_SYSTEM_INSTRUCTION
875
  q_map = {q["id"]: q for q in SOLOMON_QUESTIONS}
876
  answers_text_list = []
877
+ for ans in sub.answers:
878
+ q = q_map.get(ans["question_id"])
879
  if q:
880
+ selected_opt = next((o["text"] for o in SOLOMON_OPTIONS if o["score"] == ans["score"]), "نامشخص")
881
+ answers_text_list.append(f"سوال {ans['question_id']}: '{q['text']}' -> پاسخ: {selected_opt} (نمره خام: {ans['score']})")
882
  answers_summary = "\n".join(answers_text_list)
883
  prompt_data = f"""
884
+ کاربر ارزیابی‌شونده: {user.first_name} {user.last_name}
885
  آزمون انجام شده: سنجش علمی اهمال‌کاری تحصیلی سولومون
886
 
887
+ محاسبات ریاضی پایه:
888
  - فیلد test_type: مقدار "solomon" قرار گیرد.
889
  - فیلد solomon_level: مقدار "{calc['level']}" قرار گیرد.
890
  - فیلد somatic_score (درصد امتحانات): مقدار {calc['exam']} قرار گیرد.
 
898
  {answers_summary}
899
  """
900
  else:
 
901
  system_instruction = MBTI_SYSTEM_INSTRUCTION
902
  q_map = {q["id"]: q for q in MBTI_QUESTIONS}
903
  answers_text_list = []
904
+ for ans in sub.answers:
905
+ q = q_map.get(ans["question_id"])
906
  if q:
907
+ score_str = {2: "کاملاً موافق", 1: "موافق", 0: "ممتنع", -1: "مخالف", -2: "کاملاً مخالف"}.get(ans["score"], "نامشخص")
908
+ answers_text_list.append(f"سوال {ans['question_id']}: '{q['text']}' -> پاسخ: {score_str}")
909
  answers_summary = "\n".join(answers_text_list)
910
  prompt_data = f"""
911
+ کاربر ارزیابی‌شونده: {user.first_name} {user.last_name}
912
  آزمون انجام شده: تیپ‌شناسی شخصیت MBTI
913
 
914
  محاسبات ریاضی پایه:
 
926
  client = genai.Client(api_key=api_key)
927
  model = "gemini-3.5-flash"
928
 
929
+ generate_content_config = types.GenerateContentConfig(
930
+ system_instruction=system_instruction,
931
+ response_mime_type="application/json",
932
+ response_schema=UniversalAnalysisSchema,
933
+ )
934
+
935
+ response = client.models.generate_content(
936
+ model=model,
937
+ contents=prompt_data,
938
+ config=generate_content_config,
939
+ )
940
+
941
+ result_json = json.loads(response.text)
942
+
943
+ # ذخیره کش تحلیل در جداول دیتابیس برای بازیابی آنی در مراجعات بعدی
944
+ sub.analysis_json = result_json
945
+ db.commit()
946
+
947
+ return result_json
948
+
949
+ except Exception as e:
950
+ traceback.print_exc()
951
+ raise HTTPException(status_code=500, detail=f"خطا در مدل پردازش هوشمند: {str(e)}")
952
+
953
+ # =====================================================================
954
+ # موتور تحلیل روند پیشرفت/پسرفت مراجع (Trend Analysis Engine)
955
+ # =====================================================================
956
+ @app.post("/api/moshaver/user/{phone}/trend")
957
+ async def generate_trend_analysis(phone: str, payload: TrendAnalysisRequest, db: Session = Depends(get_db), auth: str = Depends(verify_moshaver_auth)):
958
+ """تحلیل روند تغییرات آزمون‌ها با فرستادن توالی زمانی داده‌ها به Gemini (حداقل ۳ آزمون)"""
959
+ user = db.query(UserDB).filter(UserDB.phone == phone).first()
960
+ if not user:
961
+ raise HTTPException(status_code=404, detail="پرونده مراجع یافت نشد.")
962
+
963
+ # استخراج تمام رکوردهای آزمون درخواستی به صورت صعودی (از گذشته تا به ام��وز)
964
+ subs = db.query(TestSubmissionDB).filter(
965
+ TestSubmissionDB.phone == phone,
966
+ TestSubmissionDB.test_type == payload.test_type
967
+ ).order_by(TestSubmissionDB.created_at.asc()).all()
968
+
969
+ if len(subs) < 3:
970
+ raise HTTPException(
971
+ status_code=400,
972
+ detail=f"جهت رسم و تحلیل روند بهبود مراجع، ثبت حداقل ۳ بار آزمون «{payload.test_type.upper()}» الزامی است."
973
+ )
974
+
975
+ # قالب‌بندی متنی سوابق به شکل توالی زمانی برای تحلیل دقیق هوش مصنوعی
976
+ test_history_text = []
977
+ for idx, s in enumerate(subs, 1):
978
+ date_str = s.created_at.strftime("%Y-%m-%d %H:%M")
979
+ scores = s.calculated_scores
980
+
981
+ if payload.test_type == "mbti":
982
+ test_history_text.append(
983
+ f"تست شماره {idx} (تاریخ ثبت: {date_str}):\n"
984
+ f"- تیپ شخصیتی: {scores.get('mbti_type')}\n"
985
+ f"- برون‌گرایی: {scores.get('E')}% | درون‌گرایی: {scores.get('I')}%\n"
986
+ f"- حسی: {scores.get('S')}% | شهودی: {scores.get('N')}%\n"
987
+ f"- منطقی: {scores.get('T')}% | احساسی: {scores.get('F')}%\n"
988
+ f"- ساختارگرا: {scores.get('J')}% | انعطاف‌پذیر: {scores.get('P')}%\n"
989
  )
990
+ elif payload.test_type == "bai":
991
+ test_history_text.append(
992
+ f"تست شماره {idx} (تاریخ ثبت: {date_str}):\n"
993
+ f"- نمره کل اضطراب: {scores.get('total')} از ۶۳ (سطح علائم: {scores.get('level')})\n"
994
+ f"- شدت علائم فیزیولوژیکی/جسمانی: {scores.get('somatic')}%\n"
995
+ f"- شدت علائم ذهنی/شناختی: {scores.get('cognitive')}%\n"
996
+ f"- شدت علائم هراس (Panic): {scores.get('panic')}%\n"
997
  )
998
+ elif payload.test_type == "bdi":
999
+ test_history_text.append(
1000
+ f"تست شماره {idx} (تاریخ ثبت: {date_str}):\n"
1001
+ f"- نمره کل افسردگی: {scores.get('total')} از ۶۳ (سطح خلق: {scores.get('level')})\n"
1002
+ f"- شدت علائم شناختی: {scores.get('cognitive')}%\n"
1003
+ f"- شدت علائم عاطفی/روحی: {scores.get('affective')}%\n"
1004
+ f"- شدت علائم بدنی/جسمی: {scores.get('somatic')}%\n"
1005
  )
1006
+ elif payload.test_type == "solomon":
1007
+ test_history_text.append(
1008
+ f"تست شماره {idx} (تاریخ ثبت: {date_str}):\n"
1009
+ f"- نمره کل اهمال‌کاری: {scores.get('total')} از ۱۳۵ (سطح کلی: {scores.get('level')})\n"
1010
+ f"- اهمال‌کاری امتحانات: {scores.get('exam')}%\n"
1011
+ f"- اهمال‌کاری تکالیف: {scores.get('assignment')}%\n"
1012
+ f"- اهمال‌کاری پروژه‌ها/مقاله‌ها: {scores.get('paper')}%\n"
1013
  )
1014
+
1015
+ history_payload = "\n\n".join(test_history_text)
1016
+
1017
+ prompt_data = f"""
1018
+ نام مراجع ارزیابی‌شونده: {user.first_name} {user.last_name}
1019
+ نوع آزمون مورد مقایسه: {payload.test_type.upper()}
1020
+ تعداد مراحل ارزیابی شده: {len(subs)} مرحله
1021
+
1022
+ تاریخچه داده‌های خام استخراج شده از دیتابیس پرونده مراجع به ترتیب زمانی:
1023
+ {history_payload}
1024
+ """
1025
+
1026
+ api_key = (
1027
+ os.environ.get("model_api") or
1028
+ os.environ.get("GEMINI_API_KEY") or
1029
+ os.environ.get("GOOGLE_API_KEY") or
1030
+ os.environ.get("api_key") or
1031
+ os.environ.get("API_KEY")
1032
+ )
1033
+
1034
+ if not api_key:
1035
+ raise HTTPException(status_code=500, detail="کلید API مربوط به Google Gemini تنظیم نشده است.")
1036
+
1037
+ try:
1038
+ client = genai.Client(api_key=api_key)
1039
+ generate_content_config = types.GenerateContentConfig(
1040
+ system_instruction=TREND_SYSTEM_INSTRUCTION,
1041
+ response_mime_type="application/json",
1042
+ response_schema=TrendAnalysisSchema,
1043
+ )
1044
+
1045
+ response = client.models.generate_content(
1046
+ model="gemini-3.5-flash",
1047
+ contents=prompt_data,
1048
+ config=generate_content_config,
1049
+ )
1050
+
1051
+ return json.loads(response.text)
1052
 
1053
  except Exception as e:
 
1054
  traceback.print_exc()
1055
+ raise HTTPException(status_code=500, detail=f"خطا در تولید تحلیل روند: {str(e)}")