Mind-Reframe / Web /backend /main.py
Anonymous-EmpathyAI's picture
Update Web/backend/main.py
fb1ada9 verified
Raw
History Blame Contribute Delete
20.7 kB
# File: main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uuid
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import os
import httpx
from fastapi import HTTPException
from typing import Dict, Optional, Any
from datetime import datetime, timedelta
import json
import asyncio
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class SessionInit(BaseModel):
pass
class SessionData(BaseModel):
session_id: str
situation: str
thought: str
distortions: Optional[list] = None
analysis: Optional[dict] = None
candidates: Optional[list] = None
selected_reframe: Optional[dict] = None
created_at: datetime
updated_at: datetime
def clean_expired_sessions():
"""Remove sessions older than SESSION_EXPIRY_MINUTES"""
now = datetime.now()
expired = [
sid for sid, data in sessions_store.items()
if (now - data['updated_at']).seconds > SESSION_EXPIRY_MINUTES * 60
]
for sid in expired:
del sessions_store[sid]
def get_session(session_id: str) -> Optional[dict]:
"""Get session data by ID"""
clean_expired_sessions()
return sessions_store.get(session_id)
def update_session(session_id: str, data: dict):
"""Update session data"""
if session_id in sessions_store:
sessions_store[session_id].update(data)
sessions_store[session_id]['updated_at'] = datetime.now()
else:
raise HTTPException(status_code=404, detail="Session not found")
class DetectionRequest(BaseModel):
session_id: str
situation: str
thought: str
class AnalyzeRequest(BaseModel):
session_id: str
situation: str
thought: str
distortions: list
class ReframeRequest(BaseModel):
session_id: str
situation: str
thought: str
recommended_therapies: list
API_base = 'http://localhost:1812/api/v1/'
sessions_store: Dict[str, dict] = {}
SESSION_EXPIRY_MINUTES = 60
class FeedbackData(BaseModel):
session_id: str
timestamp: str
input: dict
output: dict
ratings: dict
@app.post("/api/feedback")
async def save_feedback(data: FeedbackData):
try:
# Chuyển đổi sang dict
record = data.dict()
# Ghi vào file dataset_feedback.jsonl
# Mode 'a' (append) để ghi nối tiếp, không ghi đè
with open("dataset_feedback.jsonl", "a", encoding="utf-8") as f:
# Ghi mỗi feedback trên 1 dòng (JSONL format)
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return {"status": "success", "message": "Feedback saved"}
except Exception as e:
print(f"Error saving file: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
@app.post("/api/session/init")
async def init_session():
"""Initialize a new session"""
session_id = str(uuid.uuid4())
sessions_store[session_id] = {
'session_id': session_id,
'situation': None,
'thought': None,
'distortions': None,
'analysis': None,
'candidates': None,
'selected_reframe': None,
'created_at': datetime.now(),
'updated_at': datetime.now()
}
return {"session_id": session_id}
def _check_has_distortion(text: str) -> bool:
"""Kiểm tra xem có distortion hay không"""
if not text or len(text.strip()) < 10:
return False
# if '[' in text and ']' in text:
# return True
positive_indicators = [
"all-or-nothing", "mind reading", "catastrophizing",
"overgeneralization", "mental filter", "labeling",
"emotional reasoning", "should statements",
"jumping to conclusions", "fortune telling",
"personalization", "disqualifying"
]
text_lower = text.lower()
if any(indicator in text_lower for indicator in positive_indicators):
return True
negative_indicators = [
"không có distortion",
"không phát hiện distortion",
"no distortion",
"không tìm thấy distortion"
]
return not any(indicator in text_lower for indicator in negative_indicators)
def _extract_distortion_types(text: str) -> list:
"""
Extract các loại distortion từ response
Hỗ trợ format: [Type1, Type2, Type3] hoặc text tự do
"""
import re
bracket_match = re.search(r'\[(.*?)\]', text)
if bracket_match:
content = bracket_match.group(1)
distortions = [d.strip() for d in content.split(',')]
normalized = []
for d in distortions:
d_normalized = d.strip().title()
if 'all-or-nothing' in d.lower():
d_normalized = 'All-or-Nothing Thinking'
elif 'mind reading' in d.lower():
d_normalized = 'Mind Reading'
elif 'catastrophizing' in d.lower():
d_normalized = 'Catastrophizing'
elif 'overgeneralization' in d.lower():
d_normalized = 'Overgeneralization'
elif 'mental filter' in d.lower():
d_normalized = 'Mental Filter'
elif 'jumping to conclusions' in d.lower():
d_normalized = 'Jumping to Conclusions'
elif 'fortune telling' in d.lower():
d_normalized = 'Fortune Telling'
elif 'emotional reasoning' in d.lower():
d_normalized = 'Emotional Reasoning'
elif 'should statements' in d.lower():
d_normalized = 'Should Statements'
elif 'labeling' in d.lower():
d_normalized = 'Labeling'
elif 'personalization' in d.lower():
d_normalized = 'Personalization'
elif 'disqualifying the positive' in d.lower():
d_normalized = 'Disqualifying the Positive'
elif 'magnification' in d.lower():
d_normalized = 'Magnification'
elif 'minimization' in d.lower():
d_normalized = 'Minimization'
elif 'blame' in d.lower():
d_normalized = 'Blame'
normalized.append(d_normalized)
return normalized if normalized else ["Unknown Distortion"]
common_distortions = {
"all-or-nothing": "All-or-Nothing Thinking",
"overgeneralization": "Overgeneralization",
"mental filter": "Mental Filter",
"disqualifying the positive": "Disqualifying the Positive",
"jumping to conclusions": "Jumping to Conclusions",
"mind reading": "Mind Reading",
"fortune telling": "Fortune Telling",
"magnification": "Magnification",
"catastrophizing": "Catastrophizing",
"minimization": "Minimization",
"emotional reasoning": "Emotional Reasoning",
"should statements": "Should Statements",
"labeling": "Labeling",
"personalization": "Personalization",
"blame": "Blame"
}
found_distortions = []
text_lower = text.lower()
for key, distortion_name in common_distortions.items():
if key in text_lower:
found_distortions.append(distortion_name)
seen = set()
unique_distortions = []
for d in found_distortions:
if d not in seen:
seen.add(d)
unique_distortions.append(d)
return unique_distortions if unique_distortions else ["Unknown Distortion"]
def _extract_explanation(text: str) -> str:
"""
Extract phần giải thích từ response
Bỏ qua phần danh sách distortions trong []
"""
import re
text_cleaned = re.sub(r'^\s*\[.*?\]\s*', '', text).strip()
if text_cleaned:
explanation = text_cleaned.strip()
prefixes_to_remove = [
"explanation:",
"giải thích:",
"phân tích:"
# "the speaker"
]
explanation_lower = explanation.lower()
for prefix in prefixes_to_remove:
if explanation_lower.startswith(prefix):
explanation = explanation[len(prefix):].strip()
if explanation:
explanation = explanation[0].upper() + explanation[1:]
break
return explanation
return text.strip()
@app.post("/api/detect")
async def detect_distortion(request: DetectionRequest):
try:
update_session(request.session_id, {
'situation': request.situation,
'thought': request.thought
})
async with httpx.AsyncClient(timeout=30.0) as client:
dot_analysis = await client.post(
f"{API_base}prompts/templates/execute",
json={
"provider": "openai",
"template_name": "dot.analysis",
"model": "gpt-4.1",
"variables": {
"situation": request.situation,
"thought": request.thought
}
},
headers={
"accept": "application/json",
"Content-Type": "application/json"
}
)
dot_analysis.raise_for_status()
result_analysis = dot_analysis.json()
analysis_response = result_analysis.get("result", "") or result_analysis.get("content", "") # Fix: đổi result thành result_analysis
# Bước 2: Gọi dot.detection với kết quả từ dot.analysis
dot_detection = await client.post(
f"{API_base}prompts/templates/execute",
json={
"provider": "openai",
"template_name": "dot.detection",
"model": "gpt-4.1",
"variables": {
"situation": request.situation,
"thought": request.thought,
"dot_analysis": analysis_response
}
},
headers={
"accept": "application/json",
"Content-Type": "application/json"
}
)
dot_detection.raise_for_status()
result_detection = dot_detection.json() # Fix: đổi tên biến để rõ ràng
ai_response = result_detection.get("result", "") or result_detection.get("content", "") # Fix: dùng result_detection
has_distortion = _check_has_distortion(ai_response)
distortion_types = _extract_distortion_types(ai_response)
explanation = _extract_explanation(ai_response)
update_session(request.session_id, {
'distortions': distortion_types if distortion_types else []
})
return {
"session_id": request.session_id,
"has_distortion": has_distortion,
"distortion_types": distortion_types,
"explanation": explanation,
"raw_response": ai_response
}
except httpx.HTTPError as e:
raise HTTPException(
status_code=500,
detail=f"Lỗi khi gọi API phân tích: {str(e)}"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Lỗi không xác định: {str(e)}"
)
def _parse_json_response(text: str) -> dict:
"""Parse JSON từ AI response, xử lý markdown và whitespace"""
import json
import re
try:
# Clean text
text = text.strip()
# Remove markdown code blocks
if text.startswith("```json"):
text = text[7:]
elif text.startswith("```"):
text = text[3:]
if text.endswith("```"):
text = text[:-3]
text = text.strip()
# Parse JSON
return json.loads(text)
except json.JSONDecodeError as e:
# Fallback: try to extract JSON from text
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group())
except:
pass
raise ValueError(f"Không thể parse JSON response: {e}\nText: {text[:200]}")
@app.post("/api/analyze")
async def analyze_thought(request: AnalyzeRequest):
try:
async with httpx.AsyncClient(timeout=30.0) as client:
thera_rcm = await client.post(
f"{API_base}prompts/templates/execute",
json={
"provider": "openai",
"template_name": "dot.thera_rcm",
"model": "gpt-4.1",
"variables": {
"situation": request.situation,
"thought": request.thought,
"distortions_type": request.distortions
}
},
headers={
"accept": "application/json",
"Content-Type": "application/json"
}
)
thera_rcm.raise_for_status()
result = thera_rcm.json()
print(request.situation)
print(request.thought)
print(request.distortions)
ai_text = result.get("result", "") or result.get("content", "")
parsed_result = _parse_json_response(ai_text)
# Validate và add defaults nếu thiếu fields
if "emotional_impact" not in parsed_result:
parsed_result["emotional_impact"] = "Tác động cảm xúc đáng kể"
if "underlying_beliefs" not in parsed_result or not parsed_result["underlying_beliefs"]:
parsed_result["underlying_beliefs"] = ["Niềm tin cốt lõi cần được khám phá thêm"]
if "triggers" not in parsed_result or not parsed_result["triggers"]:
parsed_result["triggers"] = ["Các tình huống tương tự"]
if "recommended_therapies" not in parsed_result or not parsed_result["recommended_therapies"]:
# Default therapies based on common distortions
parsed_result["recommended_therapies"] = ["CBT", "ACT"]
parsed_result["session_id"] = request.session_id
return parsed_result
except json.JSONDecodeError as e:
# Fallback response nếu parse JSON thất bại
return {
"session_id": request.session_id,
"emotional_impact": "Lo âu và căng thẳng đáng kể do các suy nghĩ tiêu cực",
"underlying_beliefs": [
"Tôi cần được chấp nhận bởi người khác",
"Tôi phải tránh thất bại bằng mọi giá",
"Nếu có vấn đề xảy ra, đó là lỗi của tôi"
],
"triggers": [
"Tình huống liên quan đến mối quan hệ",
"Áp lực và kỳ vọng",
"Sự không chắc chắn"
],
"recommended_therapies": ["CBT", "ACT", "DBT"]
}
except httpx.HTTPError as e:
raise HTTPException(
status_code=500,
detail=f"Lỗi khi gọi API phân tích: {str(e)}"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Lỗi không xác định: {str(e)}"
)
# @app.post("/api/reframe")
# async def generate_reframe(request: ReframeRequest):
# reframe_candidates = []
# for therapy in request.recommended_therapies:
# async with httpx.AsyncClient(timeout=30.0) as client:
# response = await client.post(
# f"{API_base}prompts/templates/execute",
# json={
# "provider": "groq",
# "template_name": f"reframe.{therapy.lower()}",
# "model": "openai/gpt-oss-20b",
# "variables": {
# "situation": request.situation,
# "thought": request.thought,
# }
# },
# headers={
# "accept": "application/json",
# "Content-Type": "application/json"
# }
# )
# response.raise_for_status()
# result = response.json()
# reframe = result.get("reframing response", "")
# # parsed_result = _parse_json_response(ai_text)
# reframe_candidates.append({
# "therapy": f"{therapy}",
# "reframe": f"{reframe}",
# "rationale": "demo",
# "evaluation": {"empathy": 5, "logic": 4, "helpfulness": 5}
# })
# return reframe_candidates
@app.post("/api/reframe")
async def generate_reframe(request: ReframeRequest):
async def fetch_reframe(therapy: str) -> Dict[str, Any]:
"""Fetch reframe for a single therapy type"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{API_base}prompts/templates/execute",
json={
"provider": "openai",
"template_name": f"reframe.{therapy.lower()}",
"model": "gpt-4.1",
"variables": {
"situation": request.situation,
"thought": request.thought,
}
},
headers={
"accept": "application/json",
"Content-Type": "application/json"
}
)
response.raise_for_status()
result = response.json()
# Parse JSON string trong trường 'content'
import json
content_str = result.get("content", "{}")
content_json = json.loads(content_str)
# Thử cả 2 key có thể có
reframe = content_json.get("reframing response") or content_json.get("reframing_response", "")
wtw = content_json.get("why_this_works") or content_json.get("why this works", "")
feedback = await client.post(
f"{API_base}prompts/templates/execute",
json={
"provider": "openai",
"template_name": f"dot.thera_supervisor",
"model": "gpt-4.1",
"variables": {
"situation": request.situation,
"thought": request.thought,
"reframe_response": reframe
}
},
headers={
"accept": "application/json",
"Content-Type": "application/json"
}
)
feedback.raise_for_status()
feedback = feedback.json()
print(feedback)
feedback_str = feedback.get("content", "{}")
feedback_json = json.loads(feedback_str)
return {
"therapy": therapy,
"reframe": reframe,
"rationale": wtw,
"evaluation": feedback_json
}
reframe_candidates = await asyncio.gather(
*[fetch_reframe(therapy) for therapy in request.recommended_therapies],
return_exceptions=True
)
print(reframe_candidates)
successful_results = [
result for result in reframe_candidates
if not isinstance(result, Exception)
]
return {
"session_id": request.session_id,
"candidates": successful_results
}
@app.post("/api/save")
async def save_selection(data: dict):
print(f"User saved reframe: {data}")
return {"status": "success"}
app.mount("/assets", StaticFiles(directory="../mind-reframe/dist/assets"), name="assets")
@app.get("/{full_path:path}")
async def serve_react_app(full_path: str):
if full_path.startswith("api/"):
return {"error": "Not Found"}
return FileResponse("../mind-reframe/dist/index.html")