Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ build/
# Exclude heavy non-backend folders for super lean docker builds
docs/
tests/
apps/luna_mobile/
apps/eva_mobile/
apps/eva_counselor_mobile/
14 changes: 7 additions & 7 deletions .github/workflows/build-mobile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event.inputs.target == 'all' || github.event.inputs.target == 'android' || github.event.inputs.target == null }}
defaults:
run:
working-directory: apps/luna_mobile
working-directory: apps/eva_mobile

steps:
- name: Checkout repository
Expand Down Expand Up @@ -75,8 +75,8 @@ jobs:
- name: Upload Android APK
uses: actions/upload-artifact@v4
with:
name: luna-mobile-android-${{ github.event.inputs.build_type || 'release' }}-apk
path: apps/luna_mobile/${{ env.APK_PATH }}
name: eva-mobile-android-${{ github.event.inputs.build_type || 'release' }}-apk
path: apps/eva_mobile/${{ env.APK_PATH }}
if-no-files-found: error
retention-days: 14

Expand All @@ -86,7 +86,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event.inputs.target == 'all' || github.event.inputs.target == 'ios' || github.event.inputs.target == null }}
defaults:
run:
working-directory: apps/luna_mobile
working-directory: apps/eva_mobile

steps:
- name: Checkout repository
Expand Down Expand Up @@ -115,7 +115,7 @@ jobs:
fi

BUILD_DIR="build/ios/iphoneos"
IPA_NAME="LunaMobile-iPhone-${BUILD_TYPE}-unsigned.ipa"
IPA_NAME="EvaMobile-iPhone-${BUILD_TYPE}-unsigned.ipa"

echo "Packaging Runner.app into standard iPhone installer (.ipa)..."
cd "$BUILD_DIR"
Expand All @@ -131,7 +131,7 @@ jobs:
- name: Upload iPhone IPA
uses: actions/upload-artifact@v4
with:
name: luna-mobile-iphone-${{ github.event.inputs.build_type || 'release' }}-ipa
path: apps/luna_mobile/${{ env.IPA_PATH }}
name: eva-mobile-iphone-${{ github.event.inputs.build_type || 'release' }}-ipa
path: apps/eva_mobile/${{ env.IPA_PATH }}
if-no-files-found: error
retention-days: 14
26 changes: 19 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ downloads/
eggs/
.eggs/
lib/
!apps/luna_mobile/lib/
!apps/eva_mobile/lib/
!apps/eva_counselor_mobile/lib/
lib64/
parts/
sdist/
Expand Down Expand Up @@ -47,12 +48,20 @@ htmlcov/
.dockerdata/

# Flutter / Mobile
apps/luna_mobile/.dart_tool/
apps/luna_mobile/.fvm/
apps/luna_mobile/.idea/
apps/luna_mobile/.pub-cache/
apps/luna_mobile/.pub/
apps/luna_mobile/build/
apps/eva_mobile/.dart_tool/
apps/eva_mobile/.fvm/
apps/eva_mobile/.idea/
apps/eva_mobile/.pub-cache/
apps/eva_mobile/.pub/
apps/eva_mobile/build/
apps/eva_counselor_mobile/.dart_tool/
apps/eva_counselor_mobile/.fvm/
apps/eva_counselor_mobile/.idea/
apps/eva_counselor_mobile/.pub-cache/
apps/eva_counselor_mobile/.pub/
apps/eva_counselor_mobile/build/
.flutter-plugins
.flutter-plugins-dependencies

# Agents logs
.agents/logs/
Expand All @@ -65,3 +74,6 @@ apps/luna_mobile/build/
# Test audio outputs & pitch exploration
tests/output_*.mp3
tests/samples_pitch/

# User reference screenshots
immg/
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ help:
@echo " make mobile-emu - Jalankan Eva Mobile di Android Emulator (route 10.0.2.2:8888)"
@echo " make mobile-mock - Jalankan Eva Mobile dengan Mock Data lokal"
@echo " make mobile-web - Jalankan Eva Mobile di Chrome Web Simulator"
@echo " make counselor-mock- Jalankan Eva Counselor (Psikolog) dengan Mock Data"
@echo " make counselor-web - Jalankan Eva Counselor di Chrome Web Simulator"
@echo ""
@echo " Testing & Code Quality:"
@echo " make test - Jalankan seluruh test suite (Backend via Docker & Mobile)"
Expand Down Expand Up @@ -134,6 +136,15 @@ mobile-mock:
mobile-web:
@./scripts/run_mobile.sh web $(ARGS)

counselor-mock:
@APP=eva_counselor_mobile ./scripts/run_mobile.sh mock $(ARGS)

counselor-web:
@APP=eva_counselor_mobile ./scripts/run_mobile.sh web $(ARGS)

test-counselor:
cd apps/eva_counselor_mobile && flutter test

# ------------------------------------------------------------------------------
# Testing & Code Quality
# ------------------------------------------------------------------------------
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ docker compose up --build
| **Redis** | `6380` | `6379` | `localhost:6380` |
| **Qdrant Vector DB** | `6333` | `6333` | `http://localhost:6333` |

> ℹ️ *Detail panduan Docker dapat dibaca di [scripts/DOCKER_SETUP.md](file:///home/mashupsoat/Project/luna_ai/scripts/DOCKER_SETUP.md).*
> ℹ️ *Detail panduan Docker dapat dibaca di [scripts/DOCKER_SETUP.md](file:///home/mashupsoat/Project/eva-ecosystem/scripts/DOCKER_SETUP.md).*

---

Expand Down Expand Up @@ -192,7 +192,7 @@ Aplikasi Flutter `eva_mobile` dapat dijalankan di **Perangkat Fisik (HP Android

#### **2. Menjalankan Aplikasi Flutter (Mode Lokal vs Production):**

Secara default, jika dijalankan dengan `flutter run`, aplikasi akan terhubung ke server production (`https://luna.nexacode.dev`). Untuk menghubungkannya ke **server backend lokal laptop**:
Secara default, jika dijalankan dengan `flutter run`, aplikasi akan terhubung ke server production (`https://eva.nexacode.dev`). Untuk menghubungkannya ke **server backend lokal laptop**:

##### **A. Perangkat Fisik Android (Physical Device via USB) & Desktop / Web:**
1. Lakukan reverse port ADB agar port di dalam HP meneruskan request ke laptop via kabel USB:
Expand Down Expand Up @@ -237,7 +237,7 @@ Tambahkan konfigurasi berikut ke `.vscode/launch.json` di root workspace agar da
"version": "0.2.0",
"configurations": [
{
"name": "Luna Mobile (HP Fisik USB - Local)",
"name": "Eva Mobile (HP Fisik USB - Local)",
"request": "launch",
"type": "dart",
"program": "apps/eva_mobile/lib/main.dart",
Expand All @@ -246,7 +246,7 @@ Tambahkan konfigurasi berikut ke `.vscode/launch.json` di root workspace agar da
]
},
{
"name": "Luna Mobile (Android Emulator - Local)",
"name": "Eva Mobile (Android Emulator - Local)",
"request": "launch",
"type": "dart",
"program": "apps/eva_mobile/lib/main.dart",
Expand All @@ -256,7 +256,7 @@ Tambahkan konfigurasi berikut ke `.vscode/launch.json` di root workspace agar da
]
},
{
"name": "Luna Mobile (Production Cloud)",
"name": "Eva Mobile (Production Cloud)",
"request": "launch",
"type": "dart",
"program": "apps/eva_mobile/lib/main.dart"
Expand Down Expand Up @@ -326,15 +326,15 @@ pytest packages/ai/tests
```bash
python3 tests/test_emotion/test_emotion.py
```
*Hasil pengujian emotion recognition akan otomatis ditulis ke [tests/test_emotion/poc_result.md](file:///e:/nexaCode/luna_ai/tests/test_emotion/poc_result.md).*
*Hasil pengujian emotion recognition akan otomatis ditulis ke [tests/test_emotion/poc_result.md](file:///e:/nexaCode/eva-ecosystem/tests/test_emotion/poc_result.md).*

---

## 4. 🔄 Service Communication Flow

```mermaid
graph TD
Client["📱 Luna Mobile (Flutter)"] -->|REST / WebSocket :8888| API["⚡ FastAPI Backend"]
Client["📱 Eva Mobile (Flutter)"] -->|REST / WebSocket :8888| API["⚡ FastAPI Backend"]
LLM["🤖 LLM Agent / Client"] -->|MCP Protocol :8889| MCP["🛠️ FastMCP Server"]

API -->|Read/Write| DB[(🐘 PostgreSQL :5433)]
Expand Down
16 changes: 8 additions & 8 deletions apps/backend/api/app/api/routes/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ async def get_today_conversations(
today_diary = res_diary.scalar_one_or_none()

mood_tag = today_diary.mood_tag if today_diary and today_diary.mood_tag else "Tenang & Nyaman 🌿"
ai_insight = today_diary.ai_insight if today_diary and today_diary.ai_insight else "Pengguna merasa didengarkan dan tenang setelah berdialog bersama LUNA."
ai_insight = today_diary.ai_insight if today_diary and today_diary.ai_insight else "Pengguna merasa didengarkan dan tenang setelah berdialog bersama EVA."

items = []
for c in convs:
Expand Down Expand Up @@ -242,7 +242,7 @@ async def get_today_conversations(

items.append({
"id": str(c.id),
"title": c.title or "Sesi Panggilan Suara LUNA",
"title": c.title or "Sesi Panggilan Suara EVA",
"is_title_generating": "$skeleton" in (c.title or ""),
"date": f"Hari ini, {time_formatted}",
"duration": _format_duration_str(c.started_at, c.last_message_at),
Expand Down Expand Up @@ -297,7 +297,7 @@ async def get_today_conversation_messages(
all_msgs = []
for c in convs:
sorted_msgs = sorted(c.messages, key=lambda m: m.sequence_number) if c.messages else []
sess_title = (c.title or "Sesi Panggilan Suara LUNA").replace("$skeleton", "").strip()
sess_title = (c.title or "Sesi Panggilan Suara EVA").replace("$skeleton", "").strip()
for m in sorted_msgs:
if m.role in ("user", "assistant") and m.content and m.content.strip():
is_user = m.role == "user"
Expand Down Expand Up @@ -466,7 +466,7 @@ async def cached_stream():
has_crisis = any(kw in full_lower for kw in CRISIS_KEYWORDS)

if not user_assistant_msgs:
fallback_summary = "Sesi percakapan curhat bersama LUNA berjalan dengan tenang dan aman."
fallback_summary = "Sesi percakapan curhat bersama EVA berjalan dengan tenang dan aman."

async def empty_stream():
yield f"data: {json.dumps({'token': fallback_summary})}\n\n"
Expand All @@ -485,10 +485,10 @@ async def empty_stream():
system_prompt = LLMMessage(
role="system",
content=(
"Anda adalah konselor psikologis profesional LUNA AI. "
"Anda adalah konselor psikologis profesional EVA AI. "
"Tugas Anda adalah membuat ringkasan percakapan konseling yang sangat ringkas, padat, dan menenangkan (tepat 2-3 kalimat Bahasa Indonesia).\n"
"Pedoman wajib:\n"
"1. Fokus pada esensi perasaan yang dicurahkan pengguna dan pendampingan reflektif dari LUNA.\n"
"1. Fokus pada esensi perasaan yang dicurahkan pengguna dan pendampingan reflektif dari EVA.\n"
"2. Gunakan nada bicara hangat, empatik, tenang, dan suportif.\n"
"3. DILARANG KERAS mengutip mentah kata-kata pengguna dalam tanda kutip.\n"
"4. Jika terdeteksi kondisi krisis berat, berikan narasi suportif yang menekankan ruang aman dan pentingnya pertolongan profesional.\n"
Expand All @@ -511,11 +511,11 @@ async def generate_stream():
logger.error(f"⚠️ [SUMMARY STREAM LLM EXCEPTION]: {e}")
fallback_text = (
"Pengguna meluangkan waktu untuk mengekspresikan beban emosional yang berat dalam sesi ini. "
"LUNA hadir mendampingi secara suportif, memberikan ruang aman untuk bercerita, "
"EVA hadir mendampingi secara suportif, memberikan ruang aman untuk bercerita, "
"dan menegaskan bahwa setiap perasaan berharga serta bantuan selalu tersedia."
if has_crisis
else "Pengguna berbagi refleksi harian dan tantangan perasaan secara terbuka. "
"LUNA memberikan tanggapan yang menenangkan serta ruang aman untuk meredakan ketegangan."
"EVA memberikan tanggapan yang menenangkan serta ruang aman untuk meredakan ketegangan."
)
if not full_tokens:
yield f"data: {json.dumps({'token': fallback_text})}\n\n"
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/api/app/api/routes/diaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _format_diary_entry(d: DiaryEntry | None) -> dict[str, Any]:
if not d:
return {}
raw_events = d.important_events if isinstance(d.important_events, list) else []
events = raw_events if raw_events else ["Sesi refleksi harian tercatat dalam sistem LUNA."]
events = raw_events if raw_events else ["Sesi refleksi harian tercatat dalam sistem EVA."]

scores = d.mental_health_scores or {}
dep = float(scores.get("depression", 0.25))
Expand All @@ -97,19 +97,19 @@ def _format_diary_entry(d: DiaryEntry | None) -> dict[str, Any]:

return {
"id": str(d.id),
"title": d.title or "Refleksi Harian LUNA",
"title": d.title or "Refleksi Harian EVA",
"date": d.entry_date.strftime("%d %B %Y") if isinstance(d.entry_date, date) else str(d.entry_date),
"sessionCount": 0,
"lastSessionTime": "-",
"moodTag": d.mood_tag or "Netral",
"moodEmoji": d.mood_emoji or "😌",
"summary": d.summary or "Catatan harian perkembangan emosional bersama LUNA.",
"summary": d.summary or "Catatan harian perkembangan emosional bersama EVA.",
"riskWarning": {
"detected": True,
"type": "High Risk / Krisis",
"title": "PERINGATAN KRISIS EMOSIONAL",
"level": "RISIKO TINGGI",
"message": "Sistem LUNA mendeteksi akumulasi indikasi krisis emosional tinggi dan stres berat pada percakapan hari ini. Protokol keselamatan aktif untuk rujukan darurat 119 ext 8.",
"message": "Sistem EVA mendeteksi akumulasi indikasi krisis emosional tinggi dan stres berat pada percakapan hari ini. Protokol keselamatan aktif untuk rujukan darurat 119 ext 8.",
} if ("Darurat" in (d.mood_tag or "") or "Stres" in (d.mood_tag or "") or "Cemas" in (d.mood_tag or "") or "Sedih" in (d.mood_tag or "")) else None,
"aiInsight": d.ai_insight or "Analisis AI menunjukkan kondisi stabil.",
"importantEvents": events,
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/api/app/services/analytics_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ async def _get_today_monitoring(user_id: uuid.UUID, db: AsyncSession) -> dict[st
depression_pct = float(scores.get("depression", 0.0))
else:
level = 0
summary = "LUNA belum mencatat percakapanmu hari ini. Ceritakan harimu untuk mulai memantau ritem emosionalmu."
summary = "EVA belum mencatat percakapanmu hari ini. Ceritakan harimu untuk mulai memantau ritem emosionalmu."
desc = "Belum ada interaksi percakapan atau catatan jurnal untuk hari ini. Mulai sesi curhat untuk menganalisis pusat emosionalmu."
stress_pct = 0.0
anxiety_pct = 0.0
Expand Down Expand Up @@ -432,7 +432,7 @@ async def _get_week_monitoring(user_id: uuid.UUID, db: AsyncSession) -> dict[str
stress_avg = 0.0
anx_avg = 0.0
dep_avg = 0.0
summary = "Belum ada riwayat percakapan atau jurnal emosi yang tercatat untuk minggu ini. Mulai sesi curhat bersama LUNA untuk melihat dinamika emosimu."
summary = "Belum ada riwayat percakapan atau jurnal emosi yang tercatat untuk minggu ini. Mulai sesi curhat bersama EVA untuk melihat dinamika emosimu."
desc = "Belum ada data emosional tercatat untuk minggu ini."

risks = [
Expand Down Expand Up @@ -529,7 +529,7 @@ async def _get_month_monitoring(user_id: uuid.UUID, db: AsyncSession) -> dict[st
stress_avg = 0.0
anx_avg = 0.0
dep_avg = 0.0
summary = f"Belum ada data emosional yang tercatat di bulan {month_name}. Ceritakan harimu secara rutin agar LUNA dapat memetakan tren kesehatan mentalmu."
summary = f"Belum ada data emosional yang tercatat di bulan {month_name}. Ceritakan harimu secara rutin agar EVA dapat memetakan tren kesehatan mentalmu."
desc = f"Belum ada data emosional tercatat untuk bulan {month_name}."

risks = [
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/api/app/services/call_persistence_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def load_conversation_history(session: Any) -> None:
now_str = get_wib_now().strftime("%d %b %Y %H:%M")
new_conv = Conversation(
user_id=user.id,
title=f"Panggilan Suara LUNA {now_str}",
title=f"Panggilan Suara EVA {now_str}",
)
db.add(new_conv)
await db.commit()
Expand Down
12 changes: 6 additions & 6 deletions apps/backend/api/app/services/diary_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async def generate_today_diary(user_id: uuid.UUID, db: AsyncSession) -> DiaryEnt
system_prompt = LLMMessage(
role="system",
content=(
"Anda adalah sistem analis kesehatan mental & konseling psikologi LUNA AI. "
"Anda adalah sistem analis kesehatan mental & konseling psikologi EVA AI. "
"Tugas Anda adalah menganalisis seluruh transkrip percakapan harian pengguna hari ini, "
"lalu menghasilkan output JSON VALID HANYA TANPA MARKDOWN (tanpa ```json atau teks tambahan lainnya).\n\n"
"Format JSON yang HARUS dihasilkan secara presisi:\n"
Expand Down Expand Up @@ -126,8 +126,8 @@ async def _save_parsed_diary(
"depression": 0.15,
}
ai_insight = data.get("ai_insight") or "Kondisi emosional pengguna terpantau stabil."
emotional_reflection = data.get("emotional_reflection") or "Pengguna merasa lebih tenang setelah berdialog bersama LUNA."
important_events = data.get("important_events") or [f"[{get_wib_now().strftime('%H:%M')}] Sesi Percakapan LUNA"]
emotional_reflection = data.get("emotional_reflection") or "Pengguna merasa lebih tenang setelah berdialog bersama EVA."
important_events = data.get("important_events") or [f"[{get_wib_now().strftime('%H:%M')}] Sesi Percakapan EVA"]

# Check existing diary entry for today
query = select(DiaryEntry).where(DiaryEntry.user_id == user_id, DiaryEntry.entry_date == entry_date).order_by(DiaryEntry.created_at.desc())
Expand Down Expand Up @@ -171,7 +171,7 @@ async def _upsert_default_diary(user_id: uuid.UUID, entry_date: date, db: AsyncS
default_data = {
"title": f"Refleksi Harian - {entry_date.strftime('%d %b %Y')}",
"summary": "Sesi percakapan hari ini berjalan dengan baik dan memberikan ketenangan emosional.",
"content": "Pengguna telah berinteraksi dengan LUNA AI untuk merefleksikan perasaan dan aktivitas hari ini.",
"content": "Pengguna telah berinteraksi dengan EVA AI untuk merefleksikan perasaan dan aktivitas hari ini.",
"mood_tag": "Netral & Tenang 😌",
"mood_emoji": "😌",
"emotional_level": 4,
Expand All @@ -181,7 +181,7 @@ async def _upsert_default_diary(user_id: uuid.UUID, entry_date: date, db: AsyncS
"depression": 0.15,
},
"ai_insight": "Pengguna merasa didengarkan dan mulai merasa lebih rileks selama percakapan.",
"emotional_reflection": "Refleksi emosi menunjukkan respon positif terhadap konseling LUNA.",
"important_events": [f"[{get_wib_now().strftime('%H:%M')}] Sesi Panggilan Suara & Chat LUNA"],
"emotional_reflection": "Refleksi emosi menunjukkan respon positif terhadap konseling EVA.",
"important_events": [f"[{get_wib_now().strftime('%H:%M')}] Sesi Panggilan Suara & Chat EVA"],
}
return await DiaryGeneratorService._save_parsed_diary(user_id, entry_date, default_data, db)
Loading