last
This commit is contained in:
parent
9735aa873c
commit
2e51686c13
63
app.py
63
app.py
|
|
@ -8,9 +8,8 @@ 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
|
||||||
from utils.preprocessing import preprocess_image, preprocess_pipeline, validate_cattle_image
|
from utils.preprocessing import preprocess_image, preprocess_pipeline, validate_cattle_image
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
engine = get_engine()
|
|
||||||
MYSQL_AVAILABLE = engine is not None
|
try:
|
||||||
if MYSQL_AVAILABLE:
|
engine = get_engine()
|
||||||
try:
|
# quick connect test
|
||||||
init_mysql_tables()
|
with engine.connect() as conn:
|
||||||
print("✓ MySQL database initialized")
|
# initialize tables if needed
|
||||||
except Exception as init_err:
|
try:
|
||||||
print(f"⚠️ Warning: Database tables initialization failed: {init_err}")
|
init_mysql_tables()
|
||||||
else:
|
except Exception as init_err:
|
||||||
print("[APP] MySQL disabled: no valid database configuration found")
|
|
||||||
|
print(f"⚠️ Warning: Database tables initialization failed: {init_err}")
|
||||||
|
print("✓ MySQL Database connected successfully")
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -36,19 +36,10 @@ class KnowledgeBase:
|
||||||
)
|
)
|
||||||
|
|
||||||
def _set_fallback_knowledge(self):
|
def _set_fallback_knowledge(self):
|
||||||
try:
|
self.gejala = {}
|
||||||
from utils.mysql_db import get_builtin_expert_knowledge
|
self.penyakit = {}
|
||||||
|
self.aturan = []
|
||||||
knowledge = get_builtin_expert_knowledge()
|
self.gejala_groups = self._empty_groups()
|
||||||
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.penyakit = {}
|
|
||||||
self.aturan = []
|
|
||||||
self.gejala_groups = self._empty_groups()
|
|
||||||
|
|
||||||
def _load_from_database(self):
|
def _load_from_database(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
if DATABASE_URL:
|
||||||
db_user = (os.environ.get('DB_USER') or os.environ.get('MYSQLUSER') or '').strip()
|
url = make_url(DATABASE_URL)
|
||||||
|
if url.drivername == 'mysql':
|
||||||
|
url = url.set(drivername='mysql+pymysql')
|
||||||
|
return str(url)
|
||||||
|
|
||||||
|
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_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()
|
db_name = os.environ.get('DB_NAME') or os.environ.get('MYSQLDATABASE', 'deteksi_pmk')
|
||||||
|
|
||||||
if not db_host or not db_user or not db_name:
|
|
||||||
if DATABASE_URL:
|
|
||||||
try:
|
|
||||||
url = make_url(DATABASE_URL)
|
|
||||||
if url.drivername == 'mysql':
|
|
||||||
url = url.set(drivername='mysql+pymysql')
|
|
||||||
return str(url)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
db_port = _normalize_port(os.environ.get('DB_PORT') or os.environ.get('MYSQLPORT'))
|
|
||||||
|
|
||||||
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:
|
engine = create_engine(DATABASE_URI, echo=False, pool_pre_ping=True)
|
||||||
try:
|
|
||||||
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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue