This commit is contained in:
livindra 2026-04-28 22:08:16 +07:00
parent 9735aa873c
commit 2e51686c13
3 changed files with 39 additions and 159 deletions

51
app.py
View File

@ -8,8 +8,7 @@ import uuid
import time import time
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables from .env file only for local development. # Load environment variables from .env file
if not os.environ.get('PORT') and not os.environ.get('RAILWAY_ENVIRONMENT') and not os.environ.get('RAILWAY_PROJECT_ID'):
load_dotenv() load_dotenv()
from utils.helpers import load_model, estimate_prediction_confidence from utils.helpers import load_model, estimate_prediction_confidence
@ -40,34 +39,6 @@ def get_symptom_desc(symptom_code):
return kb.gejala.get(symptom_code, symptom_code) return kb.gejala.get(symptom_code, symptom_code)
def _serialize_knowledge_base():
"""Return the current expert knowledge in JSON-serializable form."""
return {
'source': 'mysql' if MYSQL_AVAILABLE else 'builtin',
'gejala': dict(expert_system.get_gejala_list()),
'gejala_groups': dict(expert_system.get_gejala_groups()),
'penyakit': dict(expert_system.kb.penyakit),
'aturan': list(expert_system.kb.aturan),
}
@app.route('/api/expert-knowledge')
def api_expert_knowledge():
"""Expose PMK expert knowledge as JSON."""
return jsonify(_serialize_knowledge_base())
@app.route('/api/gejala')
def api_gejala():
"""Expose the PMK gejala master list as JSON."""
payload = _serialize_knowledge_base()
return jsonify({
'source': payload['source'],
'gejala': payload['gejala'],
'gejala_groups': payload['gejala_groups'],
})
try: try:
from utils.mysql_db import ( from utils.mysql_db import (
save_prediction_mysql, save_prediction_mysql,
@ -80,16 +51,26 @@ try:
get_diagnosis_by_prediction_id, get_diagnosis_by_prediction_id,
get_engine, get_engine,
) )
try:
engine = get_engine() engine = get_engine()
MYSQL_AVAILABLE = engine is not None # quick connect test
if MYSQL_AVAILABLE: with engine.connect() as conn:
# initialize tables if needed
try: try:
init_mysql_tables() init_mysql_tables()
print("✓ MySQL database initialized")
except Exception as init_err: except Exception as init_err:
print(f"⚠️ Warning: Database tables initialization failed: {init_err}") print(f"⚠️ Warning: Database tables initialization failed: {init_err}")
else: print("✓ MySQL Database connected successfully")
print("[APP] MySQL disabled: no valid database configuration found") MYSQL_AVAILABLE = True
except Exception as e:
import traceback
print('❌ MySQL not available at startup:')
print(f" Error: {e}")
print(" Cek: 1) MySQL Server berjalan? 2) .env credentials benar? 3) Database 'deteksi_pmk' ada?")
traceback.print_exc()
MYSQL_AVAILABLE = False
except Exception as import_err: except Exception as import_err:
print(f"❌ Failed to import MySQL utilities: {import_err}") print(f"❌ Failed to import MySQL utilities: {import_err}")
MYSQL_AVAILABLE = False MYSQL_AVAILABLE = False

View File

@ -36,15 +36,6 @@ class KnowledgeBase:
) )
def _set_fallback_knowledge(self): def _set_fallback_knowledge(self):
try:
from utils.mysql_db import get_builtin_expert_knowledge
knowledge = get_builtin_expert_knowledge()
self.gejala = knowledge.get('gejala', {})
self.penyakit = knowledge.get('penyakit', {})
self.aturan = knowledge.get('aturan', [])
self.gejala_groups = knowledge.get('gejala_groups', self._empty_groups())
except Exception:
self.gejala = {} self.gejala = {}
self.penyakit = {} self.penyakit = {}
self.aturan = [] self.aturan = []

View File

@ -17,37 +17,22 @@ from sqlalchemy.exc import SQLAlchemyError
import pytz import pytz
TZ_INDONESIA = pytz.timezone('Asia/Jakarta') TZ_INDONESIA = pytz.timezone('Asia/Jakarta')
DATABASE_URL = os.environ.get('DATABASE_URL', '').strip() DATABASE_URL = os.environ.get('DATABASE_URL')
def _normalize_port(value, default='3306'):
"""Return a numeric MySQL port string or a safe default."""
if value is None:
return default
value = str(value).strip()
return value if value.isdigit() else default
def _build_database_uri(): def _build_database_uri():
"""Build a SQLAlchemy database URI from Railway or local environment variables.""" """Build a SQLAlchemy database URI from Railway or local environment variables."""
db_host = (os.environ.get('DB_HOST') or os.environ.get('MYSQLHOST') or '').strip()
db_user = (os.environ.get('DB_USER') or os.environ.get('MYSQLUSER') or '').strip()
db_password = os.environ.get('DB_PASSWORD') or os.environ.get('MYSQLPASSWORD', '')
db_name = (os.environ.get('DB_NAME') or os.environ.get('MYSQLDATABASE') or '').strip()
if not db_host or not db_user or not db_name:
if DATABASE_URL: if DATABASE_URL:
try:
url = make_url(DATABASE_URL) url = make_url(DATABASE_URL)
if url.drivername == 'mysql': if url.drivername == 'mysql':
url = url.set(drivername='mysql+pymysql') url = url.set(drivername='mysql+pymysql')
return str(url) return str(url)
except Exception:
return None
return None
db_port = _normalize_port(os.environ.get('DB_PORT') or os.environ.get('MYSQLPORT')) db_host = os.environ.get('DB_HOST') or os.environ.get('MYSQLHOST', 'localhost')
db_port = os.environ.get('DB_PORT') or os.environ.get('MYSQLPORT', '3306')
db_user = os.environ.get('DB_USER') or os.environ.get('MYSQLUSER', 'root')
db_password = os.environ.get('DB_PASSWORD') or os.environ.get('MYSQLPASSWORD', '')
db_name = os.environ.get('DB_NAME') or os.environ.get('MYSQLDATABASE', 'deteksi_pmk')
if db_password: if db_password:
return f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" return f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
@ -58,17 +43,9 @@ def _build_database_uri():
DATABASE_URI = _build_database_uri() DATABASE_URI = _build_database_uri()
# Create engine # Create engine
if DATABASE_URI:
try:
engine = create_engine(DATABASE_URI, echo=False, pool_pre_ping=True) engine = create_engine(DATABASE_URI, echo=False, pool_pre_ping=True)
except Exception as e:
print(f"[MYSQL] Database engine unavailable: {e}")
engine = None
else:
engine = None
Base = declarative_base() Base = declarative_base()
Session = sessionmaker(bind=engine) if engine is not None else sessionmaker() Session = sessionmaker(bind=engine)
def _prediction_source_from_features(features): def _prediction_source_from_features(features):
@ -292,50 +269,6 @@ DEFAULT_EXPERT_RULES = [
}, },
] ]
def get_builtin_expert_knowledge():
"""Return expert knowledge from the repository defaults when MySQL is unavailable."""
gejala = OrderedDict()
penyakit = OrderedDict()
aturan = []
grouped_codes = {k: [] for k in _GROUP_TITLES.keys()}
for item in DEFAULT_EXPERT_SYMPTOMS:
gejala[item['code']] = item['description']
category = (item.get('category') or 'umum').lower()
if category not in grouped_codes:
grouped_codes[category] = []
grouped_codes[category].append(item['code'])
for item in DEFAULT_EXPERT_DISEASES:
penyakit[item['code']] = {
'nama': item['name'],
'deskripsi': item['description'],
'solusi': list(item.get('solutions', [])),
}
for item in DEFAULT_EXPERT_RULES:
aturan.append({
'kode': item['code'],
'gejala': list(item.get('symptom_codes', [])),
'hasil': item['result_disease_code'],
'deskripsi': item.get('description', ''),
})
gejala_groups = OrderedDict()
for key, title in _GROUP_TITLES.items():
gejala_groups[key] = {
'title': title,
'codes': grouped_codes.get(key, []),
}
return {
'gejala': dict(gejala),
'penyakit': dict(penyakit),
'aturan': aturan,
'gejala_groups': gejala_groups,
}
_GROUP_TITLES = OrderedDict([ _GROUP_TITLES = OrderedDict([
('umum', 'Gejala Umum / Sistemik'), ('umum', 'Gejala Umum / Sistemik'),
('mulut', 'Gejala Mulut / Oral'), ('mulut', 'Gejala Mulut / Oral'),
@ -352,10 +285,6 @@ def get_engine():
def init_mysql_tables(): def init_mysql_tables():
"""Initialize database tables""" """Initialize database tables"""
if engine is None:
print("[MYSQL] Skipping table initialization because database engine is unavailable")
return False
try: try:
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
migrate_diagnosis_history_schema() migrate_diagnosis_history_schema()
@ -363,7 +292,6 @@ def init_mysql_tables():
seed_expert_knowledge(force=False) seed_expert_knowledge(force=False)
sync_expert_rule_symptom_relations() sync_expert_rule_symptom_relations()
print("Database tables initialized successfully") print("Database tables initialized successfully")
return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Error initializing database tables: {e}") print(f"Error initializing database tables: {e}")
raise raise
@ -371,9 +299,6 @@ def init_mysql_tables():
def migrate_diagnosis_history_schema(): def migrate_diagnosis_history_schema():
"""Remove legacy diagnosis_history columns that are no longer used.""" """Remove legacy diagnosis_history columns that are no longer used."""
if engine is None:
return
try: try:
inspector = inspect(engine) inspector = inspect(engine)
table_names = inspector.get_table_names() table_names = inspector.get_table_names()
@ -393,9 +318,6 @@ def migrate_diagnosis_history_schema():
def migrate_expert_rule_disease_fk(): def migrate_expert_rule_disease_fk():
"""Add the missing foreign key from expert_rules.result_disease_code to expert_diseases.code.""" """Add the missing foreign key from expert_rules.result_disease_code to expert_diseases.code."""
if engine is None:
return
try: try:
inspector = inspect(engine) inspector = inspect(engine)
if 'expert_rules' not in inspector.get_table_names() or 'expert_diseases' not in inspector.get_table_names(): if 'expert_rules' not in inspector.get_table_names() or 'expert_diseases' not in inspector.get_table_names():
@ -420,9 +342,6 @@ def migrate_expert_rule_disease_fk():
def sync_expert_rule_symptom_relations(): def sync_expert_rule_symptom_relations():
"""Backfill the expert_rules_expert_symptoms join table from stored symptom codes.""" """Backfill the expert_rules_expert_symptoms join table from stored symptom codes."""
if engine is None:
return
session = Session() session = Session()
try: try:
session.execute(expert_rules_expert_symptoms.delete()) session.execute(expert_rules_expert_symptoms.delete())
@ -458,14 +377,6 @@ def sync_expert_rule_symptom_relations():
def seed_expert_knowledge(force=False): def seed_expert_knowledge(force=False):
"""Isi data default gejala, penyakit, dan aturan sistem pakar.""" """Isi data default gejala, penyakit, dan aturan sistem pakar."""
if engine is None:
return {
'seeded': False,
'symptoms': 0,
'diseases': 0,
'rules': 0,
}
session = Session() session = Session()
try: try:
existing_symptoms = session.query(ExpertSymptom).count() existing_symptoms = session.query(ExpertSymptom).count()
@ -529,9 +440,6 @@ def seed_expert_knowledge(force=False):
def get_expert_knowledge_mysql(): def get_expert_knowledge_mysql():
"""Ambil knowledge base sistem pakar (gejala, penyakit, aturan) dari MySQL.""" """Ambil knowledge base sistem pakar (gejala, penyakit, aturan) dari MySQL."""
if engine is None:
return None
session = Session() session = Session()
try: try:
symptoms = session.query(ExpertSymptom).order_by(ExpertSymptom.display_order.asc(), ExpertSymptom.code.asc()).all() symptoms = session.query(ExpertSymptom).order_by(ExpertSymptom.display_order.asc(), ExpertSymptom.code.asc()).all()