Compare commits
2 Commits
67e7fdf04e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b1345b013 | |||
| bae7563adf |
@@ -176,6 +176,7 @@ cython_debug/
|
||||
|
||||
# Custom added
|
||||
*.db
|
||||
chromdb/*
|
||||
model/*
|
||||
models/*
|
||||
chromadb/
|
||||
model/
|
||||
models/
|
||||
.gguf
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Never commit tokens!
|
||||
config.json
|
||||
@@ -0,0 +1,45 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "fcf2c11572af6f390246c056bc905eca609533a0"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
- platform: android
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
- platform: ios
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
- platform: linux
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
- platform: macos
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
- platform: web
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
- platform: windows
|
||||
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.sourceDirectory": "/media/wikmgg/A49CB5969CB56408/Distributed-AI/Mobile/linux"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
psql "postgresql://postgres:Mey%23Birth%40miN2004@localhost:5432"
|
||||
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8313
|
||||
|
||||
livekit-server --dev --bind 0.0.0.0
|
||||
@@ -0,0 +1,22 @@
|
||||
# برای اجرای پروژه با Socket.IO:
|
||||
|
||||
## Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
|
||||
# اجرا با asgi_app
|
||||
uvicorn app.main:asgi_app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
## Flutter
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
|
||||
## مهم:
|
||||
- باید با `app.main:asgi_app` اجرا کنید نه `app.main:app`
|
||||
- این باعث میشود Socket.IO درست کار کند
|
||||
@@ -0,0 +1,153 @@
|
||||
# 🔧 RAG Desktop FFI Isolate Fix - خلاصه تغییرات
|
||||
|
||||
## 🎯 مشکل اصلی
|
||||
|
||||
```
|
||||
error: Cannot invoke native callback from a different isolate
|
||||
```
|
||||
|
||||
**علت**: LlamaCpp از FFI استفاده میکنه که نمیتونه از isolateهای مختلف صدا زده بشه.
|
||||
|
||||
## ✅ تغییرات انجام شده
|
||||
|
||||
### 1. **embedding_service.dart** - حذف Isolate برای Desktop
|
||||
|
||||
```dart
|
||||
// خط 82-84: غیرفعال کردن Isolate برای LlamaCpp
|
||||
if (AppConfig.aiBackend == AIBackendType.llamaCpp && PlatformHelper.isDesktop) {
|
||||
_useIsolate = false; // ✅ اضافه شد
|
||||
}
|
||||
```
|
||||
|
||||
**تغییر در `generateEmbeddings`:**
|
||||
- ✅ حذف کامل `Future.wait()` و parallelization
|
||||
- ✅ پردازش SEQUENTIAL یکی یکی
|
||||
- ✅ اضافه شدن لاگهای debug با emoji
|
||||
|
||||
**تغییر در `generateEmbedding`:**
|
||||
- ✅ اضافه شدن لاگهای مفصل برای trace کردن
|
||||
|
||||
### 2. **llama_cpp_embedding_backend_io.dart** - لاگهای مفصل FFI
|
||||
|
||||
```dart
|
||||
// قبل از FFI call
|
||||
Log.i('🔧 Calling _modelInstance.getEmbeddings()...', 'LlamaCppEmbedding');
|
||||
|
||||
// بعد از FFI call
|
||||
Log.i('✅ FFI call complete, got embedding (dim: ${embedding.length})', 'LlamaCppEmbedding');
|
||||
```
|
||||
|
||||
### 3. **hierarchical_rag_manager.dart** - حذف Future.wait
|
||||
|
||||
**PDF Import (خط 510-541):**
|
||||
```dart
|
||||
// قبل: await Future.wait(indexFutures);
|
||||
// بعد: SEQUENTIAL indexing
|
||||
for (int i = 0; i < allSecondWindows.length; i++) {
|
||||
await _invertedIndex!.indexChunk(...); // یکی یکی
|
||||
if ((i + 1) % 5 == 0) {
|
||||
await Future.delayed(Duration.zero); // yield to UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Text Import (خط 834-865):**
|
||||
- ✅ همین تغییر برای text files
|
||||
|
||||
**Search Method (خط 922-955):**
|
||||
- ✅ اضافه شدن لاگهای مفصل با separator
|
||||
|
||||
**loadEmbeddingModel (خط 126-139):**
|
||||
```dart
|
||||
// Auto-detect: برای LlamaCpp همیشه useIsolate: false
|
||||
final shouldUseIsolate = useIsolate ??
|
||||
(AppConfig.aiBackend == AIBackendType.llamaCpp ? false : true);
|
||||
```
|
||||
|
||||
### 4. **hierarchical_retrieval_service.dart** - لاگهای مفصل
|
||||
|
||||
**تمام Stageها:**
|
||||
```dart
|
||||
// Stage 1, 2, 3:
|
||||
Log.i('🔍 [Stage X] ...', 'HierarchicalRetrieval');
|
||||
Log.i('📊 Generating query embedding...', 'HierarchicalRetrieval');
|
||||
Log.i('✅ Query embedding ready', 'HierarchicalRetrieval');
|
||||
```
|
||||
|
||||
**retrieve method:**
|
||||
```dart
|
||||
Log.i('═══════════════════════════════════════════════', 'HierarchicalRetrieval');
|
||||
Log.i('🚀 Starting 3-Stage Hierarchical Retrieval', 'HierarchicalRetrieval');
|
||||
```
|
||||
|
||||
## 🔍 مسیر کامل لاگها (برای Debug)
|
||||
|
||||
وقتی یک query در `public_lab_chat_screen` با RAG فعال میزنید، باید این لاگها رو ببینید:
|
||||
|
||||
```
|
||||
1. [HierarchicalRAG]
|
||||
═══════════════════════════════════════════════
|
||||
🔎 [HierarchicalRAG] Starting Hybrid Search
|
||||
═══════════════════════════════════════════════
|
||||
📝 Query: "your query"
|
||||
🎯 Max results: 5
|
||||
|
||||
2. [HierarchicalRetrieval]
|
||||
═══════════════════════════════════════════════
|
||||
🚀 Starting 3-Stage Hierarchical Retrieval
|
||||
═══════════════════════════════════════════════
|
||||
|
||||
3. [HierarchicalRetrieval] 🔍 [Stage 1] Document-level search (topK=3)
|
||||
[HierarchicalRetrieval] 📊 Generating query embedding...
|
||||
|
||||
4. [EmbeddingService] 🔍 [generateEmbedding] Starting for text (X chars)
|
||||
[EmbeddingService] 🔧 Calling backend.generateEmbedding...
|
||||
|
||||
5. [LlamaCppEmbedding] 🔍 [LlamaCppBackend] generateEmbedding called (text: X chars)
|
||||
[LlamaCppEmbedding] ⏳ Yielding to event loop before FFI call...
|
||||
[LlamaCppEmbedding] 🔧 Calling _modelInstance.getEmbeddings()...
|
||||
[LlamaCppEmbedding] ✅ FFI call complete, got embedding (dim: 1024)
|
||||
[LlamaCppEmbedding] ⏳ Yielding to event loop after FFI call...
|
||||
|
||||
6. [EmbeddingService] ✅ Generated embedding (dim: 1024)
|
||||
[HierarchicalRetrieval] ✅ Query embedding generated (dim: 1024)
|
||||
|
||||
7. [HierarchicalRetrieval] 🔍 [Stage 2] First Window search...
|
||||
8. [HierarchicalRetrieval] 🔍 [Stage 3] Second Window search...
|
||||
|
||||
9. [HierarchicalRAG] ✅ Hybrid search complete: X results
|
||||
```
|
||||
|
||||
## ❌ اگر ارور بدید، چک کنید:
|
||||
|
||||
1. **"Backend not initialized"**
|
||||
→ embedding model لود نشده
|
||||
→ چک کنید: `AppConfig.llamaCppEmbeddingModelPath`
|
||||
|
||||
2. **"Model not ready"**
|
||||
→ `_isReady` false است
|
||||
→ چک کنید: آیا `autoLoadEmbeddingModel()` اجرا شده؟
|
||||
|
||||
3. **"Cannot invoke native callback from different isolate"**
|
||||
→ هنوز جایی `_useIsolate=true` هست
|
||||
→ چک کنید: لاگ "Isolate mode disabled" رو ببینید
|
||||
|
||||
## 🚀 نتیجه
|
||||
|
||||
- ✅ هیچ isolate spawning برای Desktop
|
||||
- ✅ هیچ Future.wait() parallelization
|
||||
- ✅ همه چیز SEQUENTIAL
|
||||
- ✅ لاگهای کامل برای debug
|
||||
- ✅ Yield to event loop برای responsive UI
|
||||
|
||||
## 📝 تست
|
||||
|
||||
برای تست کردن:
|
||||
1. برنامه رو اجرا کنید
|
||||
2. به `public_lab_chat_screen` برید
|
||||
3. RAG و Bot رو فعال کنید
|
||||
4. یک پیام بفرستید
|
||||
5. لاگها رو چک کنید
|
||||
|
||||
اگر تمام لاگهای بالا رو دیدید و ارور نداد → ✅ موفق!
|
||||
اگر ارور داد → لاگ آخر قبل از ارور رو پیدا کنید و بررسی کنید
|
||||
@@ -0,0 +1,220 @@
|
||||
# 🔧 RAG Desktop FFI Isolate Fix - نسخه نهایی
|
||||
|
||||
## 🎯 ریشه مشکل (Root Cause)
|
||||
|
||||
### مشکل اصلی کشف شد! ✅
|
||||
|
||||
**مشاهده کاربر:**
|
||||
- ✅ Import از `lab_backpack_screen` → موفق
|
||||
- ❌ Search از `public_lab_chat_screen` → ناموفق
|
||||
|
||||
**تحلیل Stack Trace:**
|
||||
```
|
||||
[Unoptimized] EditableTextState._finalizeEditing
|
||||
[Unoptimized] TextInput._handleTextInputInvocation
|
||||
```
|
||||
|
||||
**ریشه مشکل:**
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Main Isolate │
|
||||
│ └─ App Startup │
|
||||
│ └─ LlamaCpp Model Initialized ✅ │
|
||||
│ (FFI bindings created HERE) │
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ TextField Event Isolate (DIFFERENT!) │
|
||||
│ └─ User presses Enter │
|
||||
│ └─ _sendMessage() called │
|
||||
│ └─ hierarchicalRag.search() ❌ │
|
||||
│ └─ generateEmbedding() ❌ │
|
||||
│ └─ FFI call FAILS! │
|
||||
│ "Cannot invoke native │
|
||||
│ callback from different │
|
||||
│ isolate" │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**چرا Import موفق بود؟**
|
||||
- Import از Button → همان isolate اصلی → FFI موفق ✅
|
||||
|
||||
**چرا Search ناموفق بود؟**
|
||||
- Search از TextField onSubmitted → isolate متفاوت → FFI ناموفق ❌
|
||||
|
||||
---
|
||||
|
||||
## ✅ راهحل نهایی (Final Solution)
|
||||
|
||||
### تغییر در `public_lab_chat_screen.dart` (خطوط 232-245)
|
||||
|
||||
```dart
|
||||
// CRITICAL FIX: Force execution on MAIN isolate
|
||||
List<HybridSearchResult> ragResults = [];
|
||||
|
||||
await Future(() async {
|
||||
// این Future تضمین میکنه که کد در main event loop اجرا بشه
|
||||
// همون جایی که LlamaCpp model initialize شده
|
||||
ragResults = await hierarchicalRag.search(
|
||||
query: messageToSend,
|
||||
maxResults: 5,
|
||||
useRRF: true,
|
||||
allowedSources: labSources.toSet(),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### چرا این کار میکنه؟
|
||||
|
||||
`Future(() async { ... })` یک microtask در main event loop ایجاد میکنه و اجرای کد رو به همون isolate که app شروع شده برمیگردونه.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 تست کردن
|
||||
|
||||
### مراحل تست:
|
||||
|
||||
1. **Clean Build:**
|
||||
```bash
|
||||
flutter clean
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
2. **Run Application:**
|
||||
```bash
|
||||
flutter run -d linux # or your desktop platform
|
||||
```
|
||||
|
||||
3. **بررسی Logs در Startup:**
|
||||
```
|
||||
[EmbeddingService] Using LlamaCpp embedding model from config: /path/to/model.gguf
|
||||
[EmbeddingService] Isolate mode disabled for LlamaCpp FFI backend
|
||||
[LlamaCppEmbedding] LlamaCpp embedding model loaded successfully (sync mode)
|
||||
```
|
||||
|
||||
4. **برو به `public_lab_chat_screen`**
|
||||
- Bot toggle را روشن کن
|
||||
- RAG toggle را روشن کن
|
||||
|
||||
5. **یک پیام بفرست و Logs را بررسی کن:**
|
||||
|
||||
### ✅ Logs موفق (اگه کار کنه):
|
||||
|
||||
```
|
||||
[PublicLabChatScreen] 🚀 Starting RAG search with query: "your query"
|
||||
[HierarchicalRAG] 🔎 Starting Hybrid Search
|
||||
[HierarchicalRetrieval] 🚀 Starting 3-Stage Hierarchical Retrieval
|
||||
[HierarchicalRetrieval] 🔍 [Stage 1] Document-level search
|
||||
[EmbeddingService] 🔍 [generateEmbedding] Starting for text (X chars)
|
||||
[EmbeddingService] 🔧 Calling backend.generateEmbedding (NO YIELD)...
|
||||
[LlamaCppEmbedding] 🔍 [LlamaCppBackend] generateEmbedding async wrapper called
|
||||
[LlamaCppEmbedding] 🔧 [SYNC] Calling FFI getEmbeddings()...
|
||||
[LlamaCppEmbedding] ✅ [SYNC] FFI call success (dim: 1024)
|
||||
[EmbeddingService] ✅ Generated embedding (dim: 1024)
|
||||
[HierarchicalRetrieval] ✅ Query embedding generated
|
||||
[HierarchicalRetrieval] 🔍 [Stage 2] First Window search
|
||||
[HierarchicalRetrieval] 🔍 [Stage 3] Second Window search
|
||||
[HierarchicalRAG] ✅ Hybrid search complete: X results
|
||||
[PublicLabChatScreen] ✅ RAG search returned X results
|
||||
```
|
||||
|
||||
### ❌ Logs ناموفق (اگه باز ارور بده):
|
||||
|
||||
اگر باز این ارور رو دیدی:
|
||||
```
|
||||
error: Cannot invoke native callback from a different isolate
|
||||
```
|
||||
|
||||
یعنی `Future(() async { ... })` کافی نبود و باید از یکی از این روشهای جایگزین استفاده کنیم:
|
||||
|
||||
---
|
||||
|
||||
## 🔄 راهحلهای جایگزین (اگر Fix فعلی کار نکرد)
|
||||
|
||||
### گزینه 1: استفاده از SchedulerBinding
|
||||
|
||||
```dart
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
final ragResults = await hierarchicalRag.search(...);
|
||||
// Handle results
|
||||
});
|
||||
```
|
||||
|
||||
### گزینه 2: استفاده از Button به جای TextField onSubmitted
|
||||
|
||||
```dart
|
||||
// Replace TextField onSubmitted with IconButton
|
||||
IconButton(
|
||||
icon: Icon(Icons.send),
|
||||
onPressed: () async {
|
||||
// This runs in main isolate
|
||||
await _sendMessage();
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### گزینه 3: استفاده از Compute با Model Re-initialization
|
||||
|
||||
```dart
|
||||
// Create isolated function
|
||||
Future<List<HybridSearchResult>> _searchInIsolate(String query) async {
|
||||
// Re-initialize LlamaCpp in THIS isolate
|
||||
final backend = LlamaCppEmbeddingBackend();
|
||||
await backend.initialize(modelPath: AppConfig.llamaCppEmbeddingModelPath);
|
||||
|
||||
// Perform search
|
||||
final results = await hierarchicalRag.search(query: query, ...);
|
||||
|
||||
await backend.dispose();
|
||||
return results;
|
||||
}
|
||||
|
||||
// Call with compute
|
||||
final results = await compute(_searchInIsolate, messageToSend);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 خلاصه تمام تغییرات انجام شده
|
||||
|
||||
### 1. `embedding_service.dart`
|
||||
- ✅ غیرفعال کردن Isolate برای Desktop (خط 84)
|
||||
- ✅ پردازش Sequential (خطوط 286-343)
|
||||
- ✅ لاگهای مفصل
|
||||
|
||||
### 2. `llama_cpp_embedding_backend_io.dart`
|
||||
- ✅ متد `_generateEmbeddingSync()` برای FFI خالص (خطوط 25-46)
|
||||
- ✅ لاگهای مفصل برای FFI calls
|
||||
|
||||
### 3. `hierarchical_rag_manager.dart`
|
||||
- ✅ پارامتر `useIsolate` با auto-detection (خطوط 126-139)
|
||||
- ✅ Sequential indexing بدون `Future.wait()` (خطوط 510-541, 834-865)
|
||||
- ✅ لاگهای مفصل در `search()`
|
||||
|
||||
### 4. `hierarchical_retrieval_service.dart`
|
||||
- ✅ لاگهای مفصل برای هر 3 Stage
|
||||
- ✅ Separator logs برای خوانایی بهتر
|
||||
|
||||
### 5. **`public_lab_chat_screen.dart` (FIX اصلی)**
|
||||
- ✅ Import برای `HybridSearchResult` (خط 5)
|
||||
- ✅ **Wrapping RAG search در `Future(() async { ... })`** (خطوط 232-245)
|
||||
- ✅ لاگهای مفصل قبل و بعد از RAG search
|
||||
|
||||
### 6. `RAG_DESKTOP_FIX_SUMMARY.md`
|
||||
- ✅ مستندات کامل تغییرات قبلی
|
||||
|
||||
---
|
||||
|
||||
## 🎯 نتیجهگیری
|
||||
|
||||
این آخرین تلاش برای حل مشکل isolate است. اگر این fix کار کرد:
|
||||
- ✅ مشکل حل شد، RAG search از TextField کار میکنه
|
||||
|
||||
اگر کار نکرد:
|
||||
- باید از یکی از راهحلهای جایگزین استفاده کنیم
|
||||
- احتمالاً باید Button استفاده کنیم به جای TextField onSubmitted
|
||||
- یا باید LlamaCpp رو در Isolate جداگانه initialize کنیم
|
||||
|
||||
**لطفاً برنامه رو اجرا کن و نتیجه رو بهم بگو! 🙏**
|
||||
@@ -0,0 +1,185 @@
|
||||
# Distributed AI Chat (DAI)
|
||||
|
||||
A research project implementing a distributed AI chat system with RAG (Retrieval-Augmented Generation) capabilities, supporting multiple AI models and edge computing.
|
||||
|
||||
## ⚠️ License Notice
|
||||
|
||||
**This is a proprietary research project. All rights reserved.**
|
||||
|
||||
This software is protected by copyright and is made available under a proprietary research license. Unauthorized copying, distribution, or commercial use is strictly prohibited.
|
||||
|
||||
**Key License Terms:**
|
||||
- ✅ **Research & Education**: Free to use for research and educational purposes
|
||||
- ✅ **Contributions Welcome**: You can contribute improvements via pull requests
|
||||
- ❌ **Commercial Use**: Prohibited without explicit written permission
|
||||
- ❌ **Distribution**: You may not distribute or publish this software
|
||||
- ❌ **Standalone Copies**: You may not create independent forks for distribution
|
||||
|
||||
For complete license terms, please see [LICENSE](LICENSE) file.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Architecture](#architecture)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Contact](#contact)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Multiple AI Models**: Support for Gemma, DeepSeek, Llama, and other models
|
||||
- **Distributed Computing**: Local and distributed processing modes
|
||||
- **RAG System**: Retrieval-Augmented Generation with document management
|
||||
- **Edge AI**: On-device AI processing with Flutter Gemma
|
||||
- **Modern UI**: Optimized, fast, and user-friendly interface
|
||||
- **Document Management**: Import and manage documents for RAG queries
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
├── 🎨 frontend/ # UI Layer
|
||||
│ ├── screens/ # Application screens
|
||||
│ ├── widgets/ # Reusable UI components
|
||||
│ └── controllers/ # UI controllers
|
||||
├── 🔧 backend/ # AI Engine Layer
|
||||
│ ├── ai_engine.dart # Base AI engine interface
|
||||
│ ├── gemma_engine.dart # Flutter Gemma implementation
|
||||
│ └── llama_engine.dart # LlamaCpp implementation
|
||||
├── 🌐 network/ # Distributed System
|
||||
│ ├── distributed_manager.dart
|
||||
│ ├── rag_worker.dart
|
||||
│ └── routing_client.dart
|
||||
├── 📚 rag/ # RAG System
|
||||
│ ├── rag_manager.dart
|
||||
│ ├── embedding_service.dart
|
||||
│ └── text_chunker.dart
|
||||
└── 📦 shared/ # Shared utilities
|
||||
├── models.dart
|
||||
└── logger.dart
|
||||
```
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Flutter SDK (>=3.24.0)
|
||||
- Dart SDK (>=3.5.0)
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone [repository-url]
|
||||
cd distributed-ai
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### Running the Application
|
||||
|
||||
```bash
|
||||
# Mobile (Android/iOS)
|
||||
flutter run
|
||||
|
||||
# Desktop
|
||||
flutter run -d windows
|
||||
flutter run -d macos
|
||||
flutter run -d linux
|
||||
|
||||
# Web
|
||||
flutter run -d web
|
||||
```
|
||||
|
||||
### Key Features Usage
|
||||
|
||||
1. **Model Selection**: Browse and select AI models from the home screen
|
||||
2. **Download Models**: Download required models before use
|
||||
3. **Chat Interface**: Start conversations with selected models
|
||||
4. **Document Management**: Import documents in the Backpack screen
|
||||
5. **RAG Queries**: Use RAG system for context-aware responses
|
||||
6. **Distributed Mode**: Toggle between local and distributed processing
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions to this research project!
|
||||
|
||||
**Important**: By contributing, you acknowledge that:
|
||||
- Your contributions become the property of the project owner
|
||||
- You grant a perpetual license to use your contributions
|
||||
- This is a research project, not traditional open-source
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. Fork the repository (for contribution purposes only)
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
For detailed contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
### Contribution Areas
|
||||
|
||||
- 🐛 Bug fixes
|
||||
- ⚡ Performance improvements
|
||||
- 🎨 UI/UX enhancements
|
||||
- 📝 Documentation
|
||||
- 🔧 Code quality improvements
|
||||
- ✨ New features aligned with research goals
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under a **Proprietary Research License**.
|
||||
|
||||
**Copyright (c) 2024 [Your Name/Organization]. All rights reserved.**
|
||||
|
||||
### License Summary
|
||||
|
||||
- **Research & Education**: ✅ Allowed
|
||||
- **Contributions**: ✅ Welcome via pull requests
|
||||
- **Commercial Use**: ❌ Prohibited
|
||||
- **Distribution**: ❌ Prohibited
|
||||
- **Standalone Copies**: ❌ Prohibited
|
||||
|
||||
For complete license terms and conditions, please read the [LICENSE](LICENSE) file.
|
||||
|
||||
### Research Use
|
||||
|
||||
If you use this project in your research:
|
||||
- Please cite the project appropriately
|
||||
- Acknowledge the project in your publications
|
||||
- Share your findings and improvements back to the project
|
||||
|
||||
## 📧 Contact
|
||||
|
||||
For questions, licensing inquiries, or collaboration opportunities:
|
||||
|
||||
- **Email**: [mshafie575@gmail.com]
|
||||
- **Repository**: [https://github.com/wikm360/Distributed-AI]
|
||||
- **Issues**: [https://github.com/wikm360/Distributed-AI/issues]
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Flutter Gemma team for edge AI capabilities
|
||||
- All contributors who have helped improve this project
|
||||
- Research community for feedback and suggestions
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a research project. For commercial licensing or special use cases, please contact the project owner.
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
Analyzing Mobile...
|
||||
|
||||
info • Don't invoke 'print' in production code • lib/core/utils/logger.dart:9:5 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/core/utils/logger.dart:16:5 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/core/utils/logger.dart:23:5 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/core/utils/logger.dart:30:5 • avoid_print
|
||||
info • Use interpolation to compose strings and values • lib/features/chat/data/chat_repository.dart:85:19 • prefer_interpolation_to_compose_strings
|
||||
info • Use interpolation to compose strings and values • lib/features/chat/data/websocket_service.dart:16:19 • prefer_interpolation_to_compose_strings
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/bot_generating_bubble.dart:38:60 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/chat_header.dart:43:51 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/chat_header.dart:63:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/chat_header.dart:64:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/chat_input.dart:58:41 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/chat_input.dart:130:14 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/chat_input.dart:138:18 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps • lib/features/chat/screens/components/chat_input.dart:176:28 • use_build_context_synchronously
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/file_download_view.dart:260:31 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/message_bubble.dart:57:60 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/unread_separator.dart:16:46 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/unread_separator.dart:19:61 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/unread_separator.dart:42:47 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/unread_separator.dart:45:47 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/warning_banner.dart:14:32 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/components/warning_banner.dart:16:51 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • lib/features/chat/screens/lab_chat_screen.dart:111:9 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_chat_screen.dart:898:22 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_chat_screen.dart:902:24 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_chat_screen.dart:922:28 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_rooms_screen.dart:182:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_rooms_screen.dart:513:61 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_rooms_screen.dart:523:52 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_rooms_screen.dart:605:22 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/lab_rooms_screen.dart:607:41 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:176:55 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:182:57 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:193:57 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:998:33 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1004:35 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1036:38 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1101:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1135:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1157:22 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1195:35 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1213:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1214:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1252:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1265:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1278:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/chat/screens/labs_screen.dart:1305:33 • deprecated_member_use
|
||||
info • The private field _strokeWidth could be 'final' • lib/features/chat/screens/room_pdf_viewer_screen.dart:106:10 • prefer_final_fields
|
||||
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/features/chat/screens/room_pdf_viewer_screen.dart:623:31 • deprecated_member_use
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_pdf_service.dart:147:13 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_pdf_service.dart:151:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_pdf_service.dart:154:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_pdf_service.dart:158:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_room_service.dart:240:13 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_room_service.dart:244:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_room_service.dart:247:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/lab_room_service.dart:251:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/websocket_service.dart:54:13 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/websocket_service.dart:58:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/websocket_service.dart:62:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/chat/services/websocket_service.dart:67:7 • avoid_print
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/features/note/screens/note_screen.dart:917:31 • use_build_context_synchronously
|
||||
info • Don't invoke 'print' in production code • lib/features/note/services/note_service.dart:32:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/note/services/note_service.dart:78:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/note/services/note_service.dart:93:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/note/services/note_service.dart:104:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/note/services/note_service.dart:131:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/note/services/note_service.dart:162:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/rag/services/file_manager.dart:36:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/features/rag/services/file_manager.dart:133:7 • avoid_print
|
||||
info • Missing a deprecation message • lib/features/rag/services/hierarchical_chunker.dart:168:3 • provide_deprecation_message
|
||||
info • Unnecessary braces in a string interpolation • lib/features/rag/services/pdf_page_selector.dart:133:34 • unnecessary_brace_in_string_interps
|
||||
info • Unnecessary braces in a string interpolation • lib/features/rag/services/text_preprocessor.dart:149:29 • unnecessary_brace_in_string_interps
|
||||
info • String literals shouldn't be concatenated by the '+' operator • lib/features/rag/services/text_preprocessor.dart:231:40 • prefer_adjacent_string_concatenation
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:297:33 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:298:35 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:299:30 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:304:32 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:316:34 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:324:36 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:367:37 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:368:35 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:369:37 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:383:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:412:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:433:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:456:55 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:462:62 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:524:37 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:525:33 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:526:31 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:545:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:598:41 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:648:33 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:695:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:700:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:709:40 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:715:35 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:746:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:822:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:841:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:887:47 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:903:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:905:60 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:920:51 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:922:60 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:940:46 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:962:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1003:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1005:52 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1023:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1042:35 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1074:39 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1076:58 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1086:58 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1107:52 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1131:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1141:52 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1192:36 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1197:38 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1211:37 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1226:46 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1271:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1296:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1297:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1302:62 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1306:47 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1307:46 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1345:55 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1346:55 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1352:58 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1422:62 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1429:64 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1443:54 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1448:56 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1486:58 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1489:60 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1503:60 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1516:59 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1521:59 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1592:47 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1625:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1633:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1650:58 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1753:52 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/features/settings/screens/settings_screen.dart:1763:56 • deprecated_member_use
|
||||
info • Don't invoke 'print' in production code • lib/main.dart:164:5 • avoid_print
|
||||
info • Don't invoke 'print' in production code • lib/main.dart:166:5 • avoid_print
|
||||
info • The constant name 'qwen25_1_5B_Instruct' isn't a lowerCamelCase identifier • lib/shared/models/models.dart:165:3 • constant_identifier_names
|
||||
info • The constant name 'qwen25_0_5B_instruct' isn't a lowerCamelCase identifier • lib/shared/models/models.dart:181:3 • constant_identifier_names
|
||||
|
||||
150 issues found. (ran in 78.4s)
|
||||
@@ -0,0 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
@@ -0,0 +1,72 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// خواندن key.properties
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.chatbot"
|
||||
compileSdk = 36
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.chatbot"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// getByName("release") {
|
||||
// isMinifyEnabled = false
|
||||
// isShrinkResources = false
|
||||
// signingConfig = signingConfigs.getByName("release")
|
||||
// }
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
getByName("debug") {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Mediapipe
|
||||
-keep class com.google.mediapipe.** { *; }
|
||||
-dontwarn com.google.mediapipe.**
|
||||
|
||||
# Protobuf
|
||||
-keep class com.google.protobuf.** { *; }
|
||||
-dontwarn com.google.protobuf.**
|
||||
|
||||
# AutoValue / JavaPoet
|
||||
-keep class com.google.auto.value.** { *; }
|
||||
-dontwarn com.google.auto.value.**
|
||||
|
||||
# javax.lang.model
|
||||
-dontwarn javax.lang.model.**
|
||||
|
||||
# OkHttp & BouncyCastle & Conscrypt
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.openjsse.**
|
||||
@@ -0,0 +1,65 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:label="DAI"
|
||||
android:name="${applicationName}"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<uses-native-library
|
||||
android:name="libOpenCL.so"
|
||||
android:required="false"/>
|
||||
<uses-native-library android:name="libOpenCL-car.so" android:required="false"/>
|
||||
<uses-native-library android:name="libOpenCL-pixel.so" android:required="false"/>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name="de.julianassmann.flutter_background.IsolateHolderService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.chatbot
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#000000</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#ffffff</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#ffffff</color>
|
||||
<color name="splash_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">dai.wikm.ir</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1,26 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/central") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
// Flutter mirror for engine artifacts
|
||||
maven { url = uri("https://storage.flutter-io.cn/download.flutter.io") }
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
# systemProp.http.proxyHost=127.0.0.1
|
||||
# systemProp.http.proxyPort=12334
|
||||
# systemProp.https.proxyHost=127.0.0.1
|
||||
# systemProp.https.proxyPort=12334
|
||||
# systemProp.http.nonProxyHosts=localhost|127.*|[::1]
|
||||
|
||||
org.gradle.offline=true
|
||||
|
||||
org.gradle.caching=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
@@ -0,0 +1,30 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/central") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
// Flutter mirror for plugin artifacts
|
||||
maven { url = uri("https://storage.flutter-io.cn/download.flutter.io") }
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="511px" height="511px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g><path style="opacity:0.992" fill="#ebebeb" d="M 232.5,-0.5 C 247.5,-0.5 262.5,-0.5 277.5,-0.5C 353.52,7.75086 414.687,42.0842 461,102.5C 488.98,141.46 505.48,184.793 510.5,232.5C 510.5,247.167 510.5,261.833 510.5,276.5C 500.368,364.98 457.368,432.146 381.5,478C 348.877,495.821 314.211,506.655 277.5,510.5C 262.5,510.5 247.5,510.5 232.5,510.5C 151.78,501.407 88.28,463.74 42,397.5C 17.9708,360.577 3.80413,320.243 -0.5,276.5C -0.5,261.833 -0.5,247.167 -0.5,232.5C 9.92391,144.436 52.9239,77.6029 128.5,32C 161.093,14.0272 195.76,3.19385 232.5,-0.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#151516" d="M 245.5,42.5 C 256.332,40.9645 266.332,43.1312 275.5,49C 326.167,78.3333 376.833,107.667 427.5,137C 435.772,142.71 440.605,150.543 442,160.5C 442.667,223.167 442.667,285.833 442,348.5C 440.584,358.51 435.75,366.343 427.5,372C 374.336,402.414 321.336,433.081 268.5,464C 258.316,467.654 248.316,467.32 238.5,463C 186.33,432.414 133.997,402.081 81.5,372C 73.2497,366.343 68.4164,358.51 67,348.5C 66.3333,285.833 66.3333,223.167 67,160.5C 68.9972,148.334 75.4972,139.5 86.5,134C 139.595,103.459 192.595,72.9593 245.5,42.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#ebebeb" d="M 249.5,133.5 C 255.533,132.965 261.199,134.132 266.5,137C 295.167,153.667 323.833,170.333 352.5,187C 356.491,189.989 359.324,193.822 361,198.5C 361.667,235.833 361.667,273.167 361,310.5C 358.608,316.226 354.774,320.726 349.5,324C 321.67,339.747 294.003,355.747 266.5,372C 258.5,376.667 250.5,376.667 242.5,372C 214.997,355.747 187.33,339.747 159.5,324C 154.226,320.726 150.392,316.226 148,310.5C 147.333,273.167 147.333,235.833 148,198.5C 151.5,191 157,185.5 164.5,182C 192.928,165.792 221.261,149.626 249.5,133.5 Z"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1 @@
|
||||
whiteboard-room
|
||||
@@ -0,0 +1,42 @@
|
||||
# Environment Variables Template for Backend
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql+asyncpg://postgres:Mey#Birth@miN2004@localhost:5432/dai_db
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# JWT Settings
|
||||
SECRET_KEY=supersecretkeychangeinproduction
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=100
|
||||
|
||||
# LiveKit Settings
|
||||
LIVEKIT_URL=ws://94.183.170.121:7880
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_SECRET=secret
|
||||
|
||||
# LibreTranslate Settings
|
||||
# Set to true to enable translation feature
|
||||
ENABLE_TRANSLATION=false
|
||||
|
||||
# LibreTranslate URL (only needed if ENABLE_TRANSLATION=true)
|
||||
# For local Docker Compose deployment
|
||||
LIBRETRANSLATE_URL=http://localhost:5000
|
||||
|
||||
# For Docker Compose with service name
|
||||
# LIBRETRANSLATE_URL=http://libretranslate:5000
|
||||
|
||||
# For remote server
|
||||
# LIBRETRANSLATE_URL=http://YOUR_SERVER_IP:5000
|
||||
|
||||
# News Settings
|
||||
NEWS_FETCH_INTERVAL_HOURS=4
|
||||
NEWS_MAX_ITEMS_PER_CATEGORY=50
|
||||
|
||||
# Gitea Settings
|
||||
GITEA_API_URL=http://localhost:3000/api/v1
|
||||
GITEA_ADMIN_TOKEN=
|
||||
GITEA_ADMIN_USERNAME=
|
||||
GITEA_WEBHOOK_SECRET=
|
||||
GIT_REPOS_BASE_DIR=/tmp/git_repos
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.ai.service import AIService
|
||||
from pydantic import BaseModel
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
messages: list # لیست پیامها (مثل [{"role": "user", "content": "سلام"}])
|
||||
|
||||
ai_service = AIService()
|
||||
|
||||
@router.post("/chat/stream")
|
||||
async def chat_stream(request: ChatRequest):
|
||||
"""
|
||||
پاسخها را به صورت استریم (تکهتکه) برای فرانتاند ارسال میکند
|
||||
"""
|
||||
try:
|
||||
# ایجاد استریم و ارسال تکههای پاسخ
|
||||
return StreamingResponse(
|
||||
ai_service.generate_stream(request.messages),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from app.core.config import settings
|
||||
|
||||
class AIService:
|
||||
def __init__(self):
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=settings.AI_API_KEY,
|
||||
base_url=settings.AI_BASE_URL
|
||||
)
|
||||
|
||||
async def generate_stream(self, messages: list):
|
||||
"""
|
||||
پاسخها را به صورت استریم دریافت میکند (با بررسی امنیتی)
|
||||
"""
|
||||
stream = await self.client.chat.completions.create(
|
||||
model=settings.AI_MODEL,
|
||||
messages=messages,
|
||||
stream=True,
|
||||
temperature=0.9,
|
||||
max_tokens=10000
|
||||
)
|
||||
|
||||
async for chunk in stream:
|
||||
if chunk.choices and chunk.choices[0].delta.content is not None:
|
||||
token = chunk.choices[0].delta.content
|
||||
payload = json.dumps({"token": token}, ensure_ascii=False)
|
||||
yield f"data: {payload}\n\n"
|
||||
elif chunk.choices and chunk.choices[0].delta.content is None:
|
||||
continue
|
||||
else:
|
||||
print(f"Empty choices in chunk: {chunk}")
|
||||
yield "data: [DONE]\n\n"
|
||||
@@ -0,0 +1,32 @@
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.database import get_db
|
||||
from app.core.security import create_access_token
|
||||
from app.core.config import settings
|
||||
from app.auth.service import authenticate_user, register_new_user
|
||||
from app.auth.schemas import Token, UserLogin
|
||||
from app.users.schemas import UserCreate, UserResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
|
||||
user = await authenticate_user(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
subject=user.username, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
user = await register_new_user(db, user_in)
|
||||
return user
|
||||
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi import HTTPException, status
|
||||
from app.users.service import get_user_by_username, create_user
|
||||
from app.users.schemas import UserCreate
|
||||
from app.core.security import verify_password, create_access_token
|
||||
from app.auth.schemas import UserLogin
|
||||
|
||||
async def authenticate_user(db: AsyncSession, username: str, password: str):
|
||||
user = await get_user_by_username(db, username)
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
async def register_new_user(db: AsyncSession, user_in: UserCreate):
|
||||
# Check if username already exists
|
||||
user = await get_user_by_username(db, user_in.username)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Username already registered",
|
||||
)
|
||||
|
||||
# Check if email already exists
|
||||
from app.users.service import get_user_by_email
|
||||
existing_email = await get_user_by_email(db, user_in.email)
|
||||
if existing_email:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Email already registered",
|
||||
)
|
||||
|
||||
user = await create_user(db, user_in)
|
||||
return user
|
||||
@@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from tavily import TavilyClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
query: str
|
||||
|
||||
@router.post("/search")
|
||||
async def search_web(request: SearchRequest):
|
||||
try:
|
||||
client = TavilyClient("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")
|
||||
response = client.search(
|
||||
query=request.query,
|
||||
include_answer="basic"
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/process")
|
||||
async def process_bot_message(message: dict):
|
||||
return {"status": "received", "message": "Bot is under construction"}
|
||||
@@ -0,0 +1,64 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
|
||||
class Chat(Base):
|
||||
__tablename__ = "chats"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
lab_id = Column(Integer, ForeignKey("labs.id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
lab = relationship("Lab", back_populates="chats")
|
||||
messages = relationship("ChatMessage", back_populates="chat", cascade="all, delete-orphan")
|
||||
members = relationship("ChatMember", back_populates="chat", cascade="all, delete-orphan")
|
||||
|
||||
class ChatMember(Base):
|
||||
__tablename__ = "chat_members"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
chat_id = Column(Integer, ForeignKey("chats.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
joined_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
last_read_message_id = Column(Integer, ForeignKey("chat_messages.id"), nullable=True)
|
||||
|
||||
chat = relationship("Chat", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
class ChatMessage(Base):
|
||||
__tablename__ = "chat_messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
chat_id = Column(Integer, ForeignKey("chats.id"), nullable=False)
|
||||
lab_id = Column(Integer, ForeignKey("labs.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
content = Column(String, nullable=False)
|
||||
is_bot_message = Column(Boolean, default=False)
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# File attachment fields
|
||||
file_url = Column(String, nullable=True)
|
||||
file_name = Column(String, nullable=True)
|
||||
file_type = Column(String, nullable=True)
|
||||
file_size = Column(Integer, nullable=True)
|
||||
|
||||
# Reply-to field (self-referential)
|
||||
reply_to_id = Column(Integer, ForeignKey("chat_messages.id"), nullable=True)
|
||||
|
||||
chat = relationship("Chat", back_populates="messages")
|
||||
lab = relationship("Lab")
|
||||
user = relationship("User")
|
||||
reply_to = relationship(
|
||||
"ChatMessage",
|
||||
foreign_keys=[reply_to_id],
|
||||
remote_side="ChatMessage.id",
|
||||
uselist=False,
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
@property
|
||||
def sender_name(self) -> str:
|
||||
return self.user.username if self.user else "Unknown"
|
||||
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
import asyncio
|
||||
from app.core.redis import get_redis
|
||||
|
||||
class RedisManager:
|
||||
def __init__(self):
|
||||
self.redis = None
|
||||
|
||||
async def connect(self):
|
||||
self.redis = await get_redis()
|
||||
|
||||
async def publish(self, channel: str, message: str):
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
await self.redis.publish(channel, message)
|
||||
|
||||
async def subscribe(self, channel: str):
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
pubsub = self.redis.pubsub()
|
||||
await pubsub.subscribe(channel)
|
||||
return pubsub
|
||||
|
||||
redis_manager = RedisManager()
|
||||
@@ -0,0 +1,295 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_user
|
||||
from app.chat.models import ChatMessage
|
||||
from app.chat.schemas import MessageResponse, ChatCreate, ChatResponse, ChatMemberResponse
|
||||
from app.chat.redis_manager import redis_manager
|
||||
from app.chat import service as chat_service
|
||||
from app.labs import service as labs_service
|
||||
from app.users.models import User
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from fastapi import UploadFile, File
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class BotMessageRequest(BaseModel):
|
||||
chat_id: int
|
||||
lab_id: int
|
||||
bot_response: str
|
||||
original_sender: str
|
||||
bot_username: str
|
||||
|
||||
# Chat management endpoints
|
||||
@router.post("/{lab_id}/chats", response_model=ChatResponse)
|
||||
async def create_chat(
|
||||
lab_id: int,
|
||||
chat_in: ChatCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new chat in a lab"""
|
||||
# Check if user is lab member
|
||||
is_member = await labs_service.is_lab_member(db, lab_id, current_user.id)
|
||||
if not is_member:
|
||||
raise HTTPException(status_code=403, detail="Must be a lab member to create chats")
|
||||
|
||||
chat = await chat_service.create_chat(db, lab_id, chat_in, current_user.id)
|
||||
|
||||
# Format response
|
||||
members = [ChatMemberResponse.from_orm(member) for member in chat.members]
|
||||
return ChatResponse(
|
||||
id=chat.id,
|
||||
title=chat.title,
|
||||
lab_id=chat.lab_id,
|
||||
created_at=chat.created_at,
|
||||
updated_at=chat.updated_at,
|
||||
members=members
|
||||
)
|
||||
|
||||
@router.get("/{lab_id}/chats", response_model=List[ChatResponse])
|
||||
async def get_lab_chats(
|
||||
lab_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get all chats for a lab"""
|
||||
chat_data = await chat_service.get_lab_chats(db, lab_id)
|
||||
|
||||
# Format response
|
||||
responses = []
|
||||
for chat in chat_data:
|
||||
# Get unread count for the current user
|
||||
unread_count = await chat_service.get_unread_count(db, chat['id'], current_user.id)
|
||||
members = [ChatMemberResponse.from_orm(member) for member in chat['members']]
|
||||
responses.append(ChatResponse(
|
||||
id=chat['id'],
|
||||
title=chat['title'],
|
||||
lab_id=chat['lab_id'],
|
||||
created_at=chat['created_at'],
|
||||
updated_at=chat['updated_at'],
|
||||
last_message=chat['last_message'],
|
||||
unread_count=unread_count,
|
||||
members=members
|
||||
))
|
||||
return responses
|
||||
|
||||
@router.post("/{lab_id}/chats/{chat_id}/join")
|
||||
async def join_chat(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Join a chat"""
|
||||
# Check if user is lab member
|
||||
is_member = await labs_service.is_lab_member(db, lab_id, current_user.id)
|
||||
if not is_member:
|
||||
raise HTTPException(status_code=403, detail="Must be a lab member to join chats")
|
||||
|
||||
# Add user to chat
|
||||
member = await chat_service.add_chat_member(db, chat_id, current_user.id)
|
||||
if member is None:
|
||||
raise HTTPException(status_code=400, detail="Already a chat member")
|
||||
|
||||
return {"status": "success", "message": "Joined chat successfully"}
|
||||
|
||||
@router.get("/{lab_id}/chats/{chat_id}/history", response_model=List[MessageResponse])
|
||||
async def get_chat_history(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
limit: int = 50,
|
||||
before_id: Optional[int] = None,
|
||||
after_id: Optional[int] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get chat message history with pagination"""
|
||||
# Check if user is chat member
|
||||
is_member = await chat_service.is_chat_member(db, chat_id, current_user.id)
|
||||
if not is_member:
|
||||
raise HTTPException(status_code=403, detail="Must be a chat member to see history")
|
||||
|
||||
messages = await chat_service.get_chat_history(db, chat_id, limit=limit, before_id=before_id, after_id=after_id)
|
||||
return messages
|
||||
|
||||
@router.delete("/{lab_id}/chats/{chat_id}")
|
||||
async def delete_chat_endpoint(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Delete a chat entirely"""
|
||||
# Verify owner of lab or just require them to be lab owner / chat owner?
|
||||
# For now, let's verify if they are lab owner (or you can verify chat membership)
|
||||
is_owner = await labs_service.is_lab_owner(db, lab_id, current_user.id)
|
||||
if not is_owner:
|
||||
raise HTTPException(status_code=403, detail="Only lab owner can delete a chat")
|
||||
|
||||
success = await chat_service.delete_chat_full(db, chat_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
return {"status": "success", "message": "Chat deleted successfully"}
|
||||
|
||||
@router.post("/{lab_id}/chats/{chat_id}/read")
|
||||
async def mark_as_read(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
message_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Mark chat messages as read up to message_id"""
|
||||
await chat_service.mark_chat_read(db, chat_id, current_user.id, message_id)
|
||||
|
||||
await db.refresh(current_user)
|
||||
|
||||
# Publish read event to Redis for real-time updates
|
||||
event = {
|
||||
"type": "read_event",
|
||||
"chat_id": chat_id,
|
||||
"user_id": current_user.id,
|
||||
"last_read_message_id": message_id
|
||||
}
|
||||
await redis_manager.publish(f"chat_{chat_id}", json.dumps(event))
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
@router.get("/{lab_id}/chats/{chat_id}/first-unread")
|
||||
async def get_first_unread(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get the ID of the first unread message in the chat"""
|
||||
message_id = await chat_service.get_first_unread_message_id(db, chat_id, current_user.id)
|
||||
return {"message_id": message_id}
|
||||
|
||||
@router.post("/{lab_id}/chats/{chat_id}/bot-response")
|
||||
async def send_bot_response(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
bot_message: BotMessageRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Send bot response to chat"""
|
||||
# Check if user is chat member
|
||||
is_member = await chat_service.is_chat_member(db, chat_id, current_user.id)
|
||||
if not is_member:
|
||||
raise HTTPException(status_code=403, detail="Must be a chat member to send messages")
|
||||
|
||||
# Save bot message to DB
|
||||
content = f"@{bot_message.bot_username}: {bot_message.bot_response}"
|
||||
db_message = await chat_service.save_message(db, chat_id, lab_id, current_user.id, content, is_bot=True)
|
||||
|
||||
# Publish to Redis for other users
|
||||
response = {
|
||||
"type": "bot_message",
|
||||
"sender": bot_message.bot_username,
|
||||
"sender_id": current_user.id,
|
||||
"content": bot_message.bot_response,
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id,
|
||||
"timestamp": db_message.timestamp.isoformat(),
|
||||
"sender_avatar_url": None
|
||||
}
|
||||
await redis_manager.publish(f"chat_{chat_id}", json.dumps(response))
|
||||
|
||||
return {"status": "success", "message": "Bot response sent"}
|
||||
|
||||
@router.post("/{lab_id}/chats/{chat_id}/upload", response_model=MessageResponse)
|
||||
async def upload_chat_file(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Upload a file to chat"""
|
||||
# Check if user is chat member
|
||||
is_member = await chat_service.is_chat_member(db, chat_id, current_user.id)
|
||||
if not is_member:
|
||||
raise HTTPException(status_code=403, detail="Must be a chat member to upload files")
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
upload_dir = "app/static/chat_files"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"{uuid.uuid4()}{file_extension}"
|
||||
file_path = os.path.join(upload_dir, unique_filename)
|
||||
|
||||
# Save file
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
file_size = len(content)
|
||||
file_url = f"/static/chat_files/{unique_filename}"
|
||||
|
||||
# Save message to DB
|
||||
db_message = await chat_service.save_message_with_file(
|
||||
db,
|
||||
chat_id,
|
||||
lab_id,
|
||||
current_user.id,
|
||||
f"Sent a file: {file.filename}",
|
||||
file_url=file_url,
|
||||
file_name=file.filename,
|
||||
file_type=file.content_type,
|
||||
file_size=file_size
|
||||
)
|
||||
|
||||
# Publish to Redis for real-time updates
|
||||
response = {
|
||||
"type": "user_message",
|
||||
"id": db_message.id,
|
||||
"sender": current_user.username,
|
||||
"sender_id": current_user.id,
|
||||
"content": db_message.content,
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id,
|
||||
"timestamp": db_message.timestamp.isoformat(),
|
||||
"file_url": file_url,
|
||||
"file_name": file.filename,
|
||||
"file_type": file.content_type,
|
||||
"file_size": file_size,
|
||||
"sender_avatar_url": current_user.avatar_url
|
||||
}
|
||||
await redis_manager.publish(f"chat_{chat_id}", json.dumps(response))
|
||||
|
||||
return db_message
|
||||
|
||||
@router.delete("/{lab_id}/chats/{chat_id}/messages/{message_id}")
|
||||
async def delete_chat_message(
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
message_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Delete a chat message"""
|
||||
success = await chat_service.delete_message(db, message_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=403, detail="Could not delete message (not yours or already deleted)")
|
||||
|
||||
# Publish delete event to Redis
|
||||
event = {
|
||||
"type": "delete_message",
|
||||
"chat_id": chat_id,
|
||||
"message_id": message_id
|
||||
}
|
||||
await redis_manager.publish(f"chat_{chat_id}", json.dumps(event))
|
||||
|
||||
return {"status": "success"}
|
||||
@@ -0,0 +1,69 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from app.users.schemas import UserResponse
|
||||
|
||||
class ChatCreate(BaseModel):
|
||||
title: str
|
||||
|
||||
class ChatMemberResponse(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
joined_at: datetime
|
||||
last_read_message_id: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, chat_member):
|
||||
return cls(
|
||||
id=chat_member.user.id,
|
||||
username=chat_member.user.username,
|
||||
joined_at=chat_member.joined_at,
|
||||
last_read_message_id=chat_member.last_read_message_id
|
||||
)
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
lab_id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
last_message: Optional[str] = None
|
||||
unread_count: int = 0
|
||||
members: List[ChatMemberResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
content: str
|
||||
bot_enabled: bool = False
|
||||
reply_to_id: Optional[int] = None
|
||||
|
||||
class ReplyPreviewResponse(BaseModel):
|
||||
id: int
|
||||
sender_name: str
|
||||
content: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
id: int
|
||||
content: str
|
||||
timestamp: datetime
|
||||
user: UserResponse
|
||||
lab_id: int
|
||||
chat_id: int
|
||||
is_bot_message: bool = False
|
||||
reply_to_id: Optional[int] = None
|
||||
reply_to: Optional[ReplyPreviewResponse] = None
|
||||
file_url: Optional[str] = None
|
||||
file_name: Optional[str] = None
|
||||
file_type: Optional[str] = None
|
||||
file_size: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,340 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy import desc, func, and_, update
|
||||
from app.chat.models import Chat, ChatMember, ChatMessage
|
||||
from app.chat.schemas import ChatCreate
|
||||
from typing import Optional
|
||||
|
||||
async def create_chat(db: AsyncSession, lab_id: int, chat_in: ChatCreate, user_id: int):
|
||||
db_chat = Chat(
|
||||
lab_id=lab_id,
|
||||
title=chat_in.title
|
||||
)
|
||||
|
||||
db.add(db_chat)
|
||||
await db.commit()
|
||||
await db.refresh(db_chat)
|
||||
|
||||
# Save ID BEFORE second commit (important!)
|
||||
chat_id = db_chat.id
|
||||
|
||||
member = ChatMember(chat_id=chat_id, user_id=user_id)
|
||||
db.add(member)
|
||||
await db.commit()
|
||||
|
||||
# Reload with members safely
|
||||
result = await db.execute(
|
||||
select(Chat)
|
||||
.options(selectinload(Chat.members).selectinload(ChatMember.user))
|
||||
.filter(Chat.id == chat_id)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def get_lab_chats(db: AsyncSession, lab_id: int):
|
||||
"""Get all chats for a lab with last message"""
|
||||
result = await db.execute(
|
||||
select(Chat)
|
||||
.options(
|
||||
selectinload(Chat.members).selectinload(ChatMember.user),
|
||||
selectinload(Chat.messages)
|
||||
)
|
||||
.filter(Chat.lab_id == lab_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
)
|
||||
chats = result.scalars().all()
|
||||
|
||||
# Add last message to each chat
|
||||
chat_responses = []
|
||||
for chat in chats:
|
||||
last_msg = None
|
||||
if chat.messages:
|
||||
sorted_msgs = sorted(chat.messages, key=lambda m: (m.timestamp, m.id), reverse=True)
|
||||
if sorted_msgs:
|
||||
last_msg = sorted_msgs[0].content[:50] + "..." if len(sorted_msgs[0].content) > 50 else sorted_msgs[0].content
|
||||
|
||||
# Calculate unread count if user_id is provided
|
||||
# This part might need further refinement depending on how many chats are loaded
|
||||
chat_data = {
|
||||
'id': chat.id,
|
||||
'title': chat.title,
|
||||
'lab_id': chat.lab_id,
|
||||
'created_at': chat.created_at,
|
||||
'updated_at': chat.updated_at,
|
||||
'last_message': last_msg,
|
||||
'unread_count': 0, # Placeholder, will be updated in router if needed or here
|
||||
'members': chat.members
|
||||
}
|
||||
chat_responses.append(chat_data)
|
||||
|
||||
return chat_responses
|
||||
|
||||
async def get_unread_count(db: AsyncSession, chat_id: int, user_id: int) -> int:
|
||||
"""Get unread message count for a user in a chat"""
|
||||
# Get last read message id
|
||||
member_result = await db.execute(
|
||||
select(ChatMember).filter(
|
||||
ChatMember.chat_id == chat_id,
|
||||
ChatMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
member = member_result.scalars().first()
|
||||
if not member:
|
||||
return 0
|
||||
|
||||
query = select(func.count(ChatMessage.id)).filter(ChatMessage.chat_id == chat_id)
|
||||
if member.last_read_message_id:
|
||||
query = query.filter(ChatMessage.id > member.last_read_message_id)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalar() or 0
|
||||
|
||||
async def get_chat(db: AsyncSession, chat_id: int):
|
||||
"""Get a specific chat with members"""
|
||||
result = await db.execute(
|
||||
select(Chat)
|
||||
.options(selectinload(Chat.members).selectinload(ChatMember.user))
|
||||
.filter(Chat.id == chat_id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
async def delete_chat_full(db: AsyncSession, chat_id: int, user_id: int):
|
||||
"""Delete a chat and all its contents"""
|
||||
# Verify chat exists
|
||||
chat = await get_chat(db, chat_id)
|
||||
if not chat:
|
||||
return False
|
||||
|
||||
# Clear last_read_message_id and reply_to_id to avoid FK violations during cascade delete
|
||||
from app.chat.models import ChatMember, ChatMessage
|
||||
from sqlalchemy import update
|
||||
|
||||
await db.execute(
|
||||
update(ChatMember)
|
||||
.where(ChatMember.chat_id == chat_id)
|
||||
.values(last_read_message_id=None)
|
||||
)
|
||||
await db.execute(
|
||||
update(ChatMessage)
|
||||
.where(ChatMessage.chat_id == chat_id)
|
||||
.values(reply_to_id=None)
|
||||
)
|
||||
|
||||
await db.delete(chat)
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
async def add_chat_member(db: AsyncSession, chat_id: int, user_id: int):
|
||||
"""Add a member to a chat"""
|
||||
# Check if already a member
|
||||
result = await db.execute(
|
||||
select(ChatMember).filter(
|
||||
ChatMember.chat_id == chat_id,
|
||||
ChatMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
if result.scalars().first():
|
||||
return None # Already a member
|
||||
|
||||
member = ChatMember(chat_id=chat_id, user_id=user_id)
|
||||
db.add(member)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
return member
|
||||
|
||||
async def is_chat_member(db: AsyncSession, chat_id: int, user_id: int) -> bool:
|
||||
"""Check if user is a member of a chat"""
|
||||
result = await db.execute(
|
||||
select(ChatMember).filter(
|
||||
ChatMember.chat_id == chat_id,
|
||||
ChatMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
return result.scalars().first() is not None
|
||||
|
||||
async def get_chat_history(db: AsyncSession, chat_id: int, limit: int = 50, before_id: Optional[int] = None, after_id: Optional[int] = None):
|
||||
"""Get messages for a chat with pagination"""
|
||||
query = select(ChatMessage).options(
|
||||
selectinload(ChatMessage.user),
|
||||
selectinload(ChatMessage.reply_to).selectinload(ChatMessage.user),
|
||||
).filter(ChatMessage.chat_id == chat_id)
|
||||
|
||||
if before_id:
|
||||
query = query.filter(ChatMessage.id < before_id)
|
||||
query = query.order_by(ChatMessage.timestamp.desc(), ChatMessage.id.desc())
|
||||
elif after_id:
|
||||
query = query.filter(ChatMessage.id > after_id)
|
||||
query = query.order_by(ChatMessage.timestamp.asc(), ChatMessage.id.asc())
|
||||
else:
|
||||
query = query.order_by(ChatMessage.timestamp.desc(), ChatMessage.id.desc())
|
||||
|
||||
query = query.limit(limit)
|
||||
|
||||
result = await db.execute(query)
|
||||
messages = result.scalars().all()
|
||||
|
||||
# Always return in ascending order for the UI
|
||||
if before_id or not after_id:
|
||||
return sorted(messages, key=lambda m: (m.timestamp, m.id))
|
||||
return messages
|
||||
|
||||
async def mark_chat_read(db: AsyncSession, chat_id: int, user_id: int, last_message_id: int):
|
||||
"""Update last read message ID for a user in a chat"""
|
||||
result = await db.execute(
|
||||
select(ChatMember).filter(
|
||||
ChatMember.chat_id == chat_id,
|
||||
ChatMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
member = result.scalars().first()
|
||||
if member:
|
||||
# Only update if the new message ID is greater than the current one
|
||||
if not member.last_read_message_id or last_message_id > member.last_read_message_id:
|
||||
member.last_read_message_id = last_message_id
|
||||
await db.commit()
|
||||
return member
|
||||
|
||||
async def get_first_unread_message_id(db: AsyncSession, chat_id: int, user_id: int) -> Optional[int]:
|
||||
"""Get the ID of the first unread message"""
|
||||
member_result = await db.execute(
|
||||
select(ChatMember).filter(
|
||||
ChatMember.chat_id == chat_id,
|
||||
ChatMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
member = member_result.scalars().first()
|
||||
if not member or not member.last_read_message_id:
|
||||
# If never read, get the oldest message or None
|
||||
query = select(ChatMessage.id).filter(ChatMessage.chat_id == chat_id).order_by(ChatMessage.timestamp.asc(), ChatMessage.id.asc()).limit(1)
|
||||
result = await db.execute(query)
|
||||
return result.scalar()
|
||||
|
||||
query = select(ChatMessage.id).filter(
|
||||
ChatMessage.chat_id == chat_id,
|
||||
ChatMessage.id > member.last_read_message_id
|
||||
).order_by(ChatMessage.timestamp.asc(), ChatMessage.id.asc()).limit(1)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalar()
|
||||
|
||||
async def save_message(db: AsyncSession, chat_id: int, lab_id: int, user_id: int, content: str, is_bot: bool = False, reply_to_id: Optional[int] = None):
|
||||
"""Save a message to database"""
|
||||
message = ChatMessage(
|
||||
chat_id=chat_id,
|
||||
lab_id=lab_id,
|
||||
user_id=user_id,
|
||||
content=content,
|
||||
is_bot_message=is_bot,
|
||||
reply_to_id=reply_to_id,
|
||||
)
|
||||
db.add(message)
|
||||
|
||||
# Update chat's updated_at timestamp
|
||||
chat_result = await db.execute(select(Chat).filter(Chat.id == chat_id))
|
||||
chat = chat_result.scalars().first()
|
||||
if chat:
|
||||
chat.updated_at = message.timestamp
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
# Load user and reply_to relationships
|
||||
result = await db.execute(
|
||||
select(ChatMessage)
|
||||
.options(
|
||||
selectinload(ChatMessage.user),
|
||||
selectinload(ChatMessage.reply_to).selectinload(ChatMessage.user),
|
||||
)
|
||||
.filter(ChatMessage.id == message.id)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
async def save_message_with_file(
|
||||
db: AsyncSession,
|
||||
chat_id: int,
|
||||
lab_id: int,
|
||||
user_id: int,
|
||||
content: str,
|
||||
file_url: str,
|
||||
file_name: str,
|
||||
file_type: str,
|
||||
file_size: int,
|
||||
reply_to_id: Optional[int] = None,
|
||||
):
|
||||
"""Save a message with a file attachment to database"""
|
||||
message = ChatMessage(
|
||||
chat_id=chat_id,
|
||||
lab_id=lab_id,
|
||||
user_id=user_id,
|
||||
content=content,
|
||||
file_url=file_url,
|
||||
file_name=file_name,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
is_bot_message=False,
|
||||
reply_to_id=reply_to_id,
|
||||
)
|
||||
db.add(message)
|
||||
|
||||
# Update chat's updated_at timestamp
|
||||
chat_result = await db.execute(select(Chat).filter(Chat.id == chat_id))
|
||||
chat = chat_result.scalars().first()
|
||||
if chat:
|
||||
chat.updated_at = message.timestamp
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
# Load user and reply_to relationships
|
||||
result = await db.execute(
|
||||
select(ChatMessage)
|
||||
.options(
|
||||
selectinload(ChatMessage.user),
|
||||
selectinload(ChatMessage.reply_to).selectinload(ChatMessage.user),
|
||||
)
|
||||
.filter(ChatMessage.id == message.id)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
async def delete_message(db: AsyncSession, message_id: int, user_id: int):
|
||||
"""Delete a chat message if it belongs to the user"""
|
||||
result = await db.execute(
|
||||
select(ChatMessage).filter(
|
||||
ChatMessage.id == message_id,
|
||||
ChatMessage.user_id == user_id
|
||||
)
|
||||
)
|
||||
message = result.scalars().first()
|
||||
if not message:
|
||||
return False
|
||||
|
||||
chat_id = message.chat_id
|
||||
|
||||
# 1. Handle replies: set reply_to_id to NULL for messages replying to this one
|
||||
await db.execute(
|
||||
update(ChatMessage)
|
||||
.where(ChatMessage.reply_to_id == message_id)
|
||||
.values(reply_to_id=None)
|
||||
)
|
||||
|
||||
# 2. Handle last_read_message_id in ChatMember
|
||||
# Find the previous message in this chat to point to
|
||||
prev_msg_result = await db.execute(
|
||||
select(ChatMessage.id)
|
||||
.where(ChatMessage.chat_id == chat_id, ChatMessage.id < message_id)
|
||||
.order_by(desc(ChatMessage.id))
|
||||
.limit(1)
|
||||
)
|
||||
prev_msg_id = prev_msg_result.scalar()
|
||||
|
||||
await db.execute(
|
||||
update(ChatMember)
|
||||
.where(ChatMember.last_read_message_id == message_id)
|
||||
.values(last_read_message_id=prev_msg_id)
|
||||
)
|
||||
|
||||
# 3. Delete the message
|
||||
await db.delete(message)
|
||||
await db.commit()
|
||||
return True
|
||||
@@ -0,0 +1,251 @@
|
||||
import json
|
||||
import asyncio
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from socketio import AsyncNamespace, AsyncServer
|
||||
from app.core.database import SessionLocal
|
||||
from app.chat.redis_manager import redis_manager
|
||||
from app.chat import service as chat_service
|
||||
from app.users.service import get_user_by_username
|
||||
from app.chat.models import ChatMessage
|
||||
from app.core.security import settings
|
||||
from jose import jwt, JWTError
|
||||
|
||||
sio = AsyncServer(
|
||||
async_mode='asgi',
|
||||
cors_allowed_origins="*",
|
||||
logger=True,
|
||||
engineio_logger=False,
|
||||
ping_timeout=30,
|
||||
ping_interval=10
|
||||
)
|
||||
|
||||
class ChatNamespace(AsyncNamespace):
|
||||
def __init__(self, namespace='/chat'):
|
||||
super().__init__(namespace)
|
||||
self._session_rooms = {}
|
||||
self._redis_tasks = {}
|
||||
self._redis_pubsubs = {}
|
||||
self._chat_refcounts = {}
|
||||
self._redis_lock = asyncio.Lock()
|
||||
|
||||
async def _ensure_redis_listener(self, chat_id: int):
|
||||
async with self._redis_lock:
|
||||
if chat_id in self._redis_tasks:
|
||||
self._chat_refcounts[chat_id] += 1
|
||||
return
|
||||
|
||||
channel_name = f"chat_{chat_id}"
|
||||
pubsub = await redis_manager.subscribe(channel_name)
|
||||
self._redis_pubsubs[chat_id] = pubsub
|
||||
self._chat_refcounts[chat_id] = 1
|
||||
|
||||
async def listen_to_redis():
|
||||
try:
|
||||
async for message in pubsub.listen():
|
||||
if message.get("type") != "message":
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(message.get("data", "{}"))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
event_type = payload.get("type")
|
||||
if event_type == "read_event":
|
||||
await self.emit("read_event", payload, room=channel_name)
|
||||
elif event_type == "delete_message":
|
||||
await self.emit("delete_message", payload, room=channel_name)
|
||||
else:
|
||||
await self.emit("message", payload, room=channel_name)
|
||||
except Exception as e:
|
||||
print(f"Socket.IO Redis listener error for chat {chat_id}: {e}")
|
||||
|
||||
self._redis_tasks[chat_id] = asyncio.create_task(listen_to_redis())
|
||||
|
||||
async def _release_redis_listener(self, chat_id: int):
|
||||
async with self._redis_lock:
|
||||
if chat_id not in self._chat_refcounts:
|
||||
return
|
||||
self._chat_refcounts[chat_id] -= 1
|
||||
if self._chat_refcounts[chat_id] > 0:
|
||||
return
|
||||
|
||||
channel_name = f"chat_{chat_id}"
|
||||
task = self._redis_tasks.pop(chat_id, None)
|
||||
pubsub = self._redis_pubsubs.pop(chat_id, None)
|
||||
self._chat_refcounts.pop(chat_id, None)
|
||||
|
||||
if task:
|
||||
task.cancel()
|
||||
if pubsub:
|
||||
try:
|
||||
await pubsub.unsubscribe(channel_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def on_connect(self, sid, environ, auth):
|
||||
data = auth or {}
|
||||
lab_id = data.get('lab_id')
|
||||
chat_id = data.get('chat_id')
|
||||
token = data.get('token')
|
||||
|
||||
if not all([lab_id, chat_id, token]):
|
||||
return False
|
||||
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
user = await self._get_user_from_token(token, db)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
is_member = await chat_service.is_chat_member(db, chat_id, user.id)
|
||||
if not is_member:
|
||||
return False
|
||||
|
||||
self._session_rooms[sid] = {
|
||||
'lab_id': lab_id,
|
||||
'chat_id': chat_id,
|
||||
'user_id': user.id,
|
||||
'username': user.username,
|
||||
'bot_username': user.bot_username if user.bot_username else None,
|
||||
'avatar_url': user.avatar_url,
|
||||
}
|
||||
|
||||
room_name = f"chat_{chat_id}"
|
||||
await self.enter_room(sid, room_name)
|
||||
await self._ensure_redis_listener(chat_id)
|
||||
print(f"Socket.IO: User {user.username} connected to chat {chat_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Socket.IO connection error: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def _get_user_from_token(self, token: str, db: AsyncSession):
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
return None
|
||||
except JWTError:
|
||||
return None
|
||||
return await get_user_by_username(db, username)
|
||||
|
||||
async def on_disconnect(self, sid):
|
||||
room_info = self._session_rooms.get(sid)
|
||||
if room_info:
|
||||
room_name = f"chat_{room_info['chat_id']}"
|
||||
await self.leave_room(sid, room_name)
|
||||
await self._release_redis_listener(room_info['chat_id'])
|
||||
print(f"Socket.IO: User {room_info['username']} disconnected from chat {room_info['chat_id']}")
|
||||
del self._session_rooms[sid]
|
||||
|
||||
async def on_message(self, sid, data):
|
||||
room_info = self._session_rooms.get(sid)
|
||||
if not room_info:
|
||||
return
|
||||
|
||||
lab_id = room_info['lab_id']
|
||||
chat_id = room_info['chat_id']
|
||||
user_id = room_info['user_id']
|
||||
username = room_info['username']
|
||||
bot_username = room_info['bot_username']
|
||||
avatar_url = room_info['avatar_url']
|
||||
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
|
||||
is_bot_response = data.get("is_bot_response", False)
|
||||
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
if is_bot_response:
|
||||
if not bot_username:
|
||||
return
|
||||
|
||||
content = data.get("content", "")
|
||||
db_message = await chat_service.save_message(
|
||||
db, chat_id, lab_id, user_id, content, is_bot=True
|
||||
)
|
||||
|
||||
response = {
|
||||
"type": "bot_message",
|
||||
"sender": bot_username,
|
||||
"sender_id": user_id,
|
||||
"content": content,
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id,
|
||||
"timestamp": db_message.timestamp.isoformat(),
|
||||
"is_bot_message": True,
|
||||
"sender_avatar_url": None,
|
||||
}
|
||||
await self.emit('message', response, room=f"chat_{chat_id}")
|
||||
return
|
||||
|
||||
bot_enabled = data.get("bot_enabled", False)
|
||||
reply_to_id = data.get("reply_to_id")
|
||||
if reply_to_id is not None:
|
||||
try:
|
||||
reply_to_id = int(reply_to_id)
|
||||
except (ValueError, TypeError):
|
||||
reply_to_id = None
|
||||
|
||||
has_bot_mention = False
|
||||
clean_message = data.get("content", "")
|
||||
|
||||
if bot_enabled and bot_username:
|
||||
bot_mention_pattern = f"@{bot_username}"
|
||||
has_bot_mention = bot_mention_pattern in clean_message
|
||||
if has_bot_mention:
|
||||
clean_message = clean_message.replace(bot_mention_pattern, "").strip()
|
||||
|
||||
db_message = await chat_service.save_message(
|
||||
db, chat_id, lab_id, user_id, data.get("content", ""),
|
||||
is_bot=False, reply_to_id=reply_to_id
|
||||
)
|
||||
|
||||
reply_to_preview = None
|
||||
if db_message.reply_to is not None:
|
||||
reply_sender = db_message.reply_to.user.username if db_message.reply_to.user else "Unknown"
|
||||
reply_to_preview = {
|
||||
"id": db_message.reply_to.id,
|
||||
"sender_name": reply_sender,
|
||||
"content": db_message.reply_to.content[:200],
|
||||
}
|
||||
|
||||
response = {
|
||||
"type": "user_message",
|
||||
"id": db_message.id,
|
||||
"sender": username,
|
||||
"sender_id": user_id,
|
||||
"content": data.get("content", ""),
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id,
|
||||
"timestamp": db_message.timestamp.isoformat(),
|
||||
"reply_to_id": reply_to_id,
|
||||
"reply_to": reply_to_preview,
|
||||
"sender_avatar_url": avatar_url,
|
||||
}
|
||||
await self.emit('message', response, room=f"chat_{chat_id}")
|
||||
|
||||
if has_bot_mention:
|
||||
bot_request = {
|
||||
"type": "bot_request",
|
||||
"sender": username,
|
||||
"sender_id": user_id,
|
||||
"bot_username": bot_username,
|
||||
"original_message": data.get("content", ""),
|
||||
"clean_message": clean_message,
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id
|
||||
}
|
||||
await self.emit('message', bot_request, to=sid)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Socket.IO message error: {e}")
|
||||
|
||||
chat_namespace = ChatNamespace('/chat')
|
||||
sio.register_namespace(chat_namespace)
|
||||
@@ -0,0 +1,248 @@
|
||||
import json
|
||||
import asyncio
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import update
|
||||
from datetime import datetime, timezone
|
||||
from app.core.database import get_db
|
||||
from app.chat.redis_manager import redis_manager
|
||||
from app.chat import service as chat_service
|
||||
from app.users.service import get_user_by_username
|
||||
from app.users.models import User
|
||||
from app.core.security import settings
|
||||
from jose import jwt, JWTError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Store active WebSocket connections per user
|
||||
active_connections = {} # user_id -> list of websockets
|
||||
|
||||
def get_active_websockets(user_id: int):
|
||||
"""Get all active WebSocket connections for a user"""
|
||||
return active_connections.get(user_id, [])
|
||||
|
||||
def add_active_connection(user_id: int, websocket: WebSocket):
|
||||
"""Add a WebSocket connection for a user"""
|
||||
if user_id not in active_connections:
|
||||
active_connections[user_id] = []
|
||||
active_connections[user_id].append(websocket)
|
||||
|
||||
def remove_active_connection(user_id: int, websocket: WebSocket):
|
||||
"""Remove a WebSocket connection for a user"""
|
||||
if user_id in active_connections:
|
||||
if websocket in active_connections[user_id]:
|
||||
active_connections[user_id].remove(websocket)
|
||||
if not active_connections[user_id]:
|
||||
del active_connections[user_id]
|
||||
|
||||
async def broadcast_user_online_status(user_id: int, lab_id: int, is_online: bool):
|
||||
"""Broadcast user online/offline status to lab members"""
|
||||
status_update = {
|
||||
"type": "user_status",
|
||||
"user_id": user_id,
|
||||
"lab_id": lab_id,
|
||||
"is_online": is_online,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
await redis_manager.publish(f"lab_{lab_id}_status", json.dumps(status_update))
|
||||
|
||||
async def get_user_from_token(token: str, db: AsyncSession):
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
return None
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
user = await get_user_by_username(db, username)
|
||||
return user
|
||||
|
||||
@router.websocket("/ws/{lab_id}/{chat_id}")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
lab_id: int,
|
||||
chat_id: int,
|
||||
token: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
user = await get_user_from_token(token, db)
|
||||
if not user:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
user_id = user.id
|
||||
|
||||
# Set user as online and add connection
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user_id)
|
||||
.values(is_online=True, last_seen=datetime.now(timezone.utc))
|
||||
)
|
||||
await db.commit()
|
||||
add_active_connection(user_id, websocket)
|
||||
|
||||
# Broadcast user came online
|
||||
await broadcast_user_online_status(user_id, lab_id, True)
|
||||
|
||||
redis_task = asyncio.create_task(
|
||||
redis_manager.subscribe(f"chat_{chat_id}", redis_handler)
|
||||
)
|
||||
|
||||
# Subscribe to lab status updates
|
||||
status_task = asyncio.create_task(
|
||||
redis_manager.subscribe(f"lab_{lab_id}_status", status_handler)
|
||||
)
|
||||
|
||||
def status_handler(message):
|
||||
if not websocket.client_connected:
|
||||
return
|
||||
try:
|
||||
import asyncio
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
websocket.send_text(message),
|
||||
asyncio.get_event_loop()
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
def redis_handler(message):
|
||||
if not websocket.client_connected:
|
||||
return
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
websocket.send_text(message),
|
||||
asyncio.get_event_loop()
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Initialize bot user
|
||||
bot_enabled = user.bot_username is not None and user.bot_username != ""
|
||||
|
||||
# Send connection confirmation
|
||||
await websocket.send_text(json.dumps({"type": "connected"}))
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
message_type = data.get("type")
|
||||
|
||||
if message_type == "ping":
|
||||
await websocket.send_text(json.dumps({"type": "pong"}))
|
||||
|
||||
elif message_type == "join":
|
||||
pass
|
||||
|
||||
elif message_type == "bot_request":
|
||||
pass
|
||||
|
||||
elif message_type == "message":
|
||||
# Check if user is lab member
|
||||
is_member = await chat_service.is_lab_member(db, lab_id, user_id)
|
||||
if not is_member:
|
||||
await websocket.send_text(json.dumps({"type": "error", "message": "Not a lab member"}))
|
||||
continue
|
||||
|
||||
# Parse message
|
||||
message_data = data.get("message", {})
|
||||
content = message_data.get("content", "")
|
||||
reply_to_id = message_data.get("reply_to_id")
|
||||
|
||||
if reply_to_id is not None:
|
||||
try:
|
||||
reply_to_id = int(reply_to_id)
|
||||
except (ValueError, TypeError):
|
||||
reply_to_id = None
|
||||
|
||||
# Check if bot is enabled and message mentions user's bot
|
||||
has_bot_mention = False
|
||||
clean_message = message_data["content"]
|
||||
|
||||
if bot_enabled and user.bot_username:
|
||||
bot_mention_pattern = f"@{user.bot_username}"
|
||||
has_bot_mention = bot_mention_pattern in message_data["content"]
|
||||
if has_bot_mention:
|
||||
# Remove bot mention from message
|
||||
clean_message = message_data["content"].replace(bot_mention_pattern, "").strip()
|
||||
|
||||
# Save to DB
|
||||
db_message = await chat_service.save_message(
|
||||
db, chat_id, lab_id, user.id, message_data["content"],
|
||||
is_bot=False, reply_to_id=reply_to_id
|
||||
)
|
||||
|
||||
# Build reply preview if available
|
||||
reply_to_preview = None
|
||||
if db_message.reply_to is not None:
|
||||
reply_sender = db_message.reply_to.user.username if db_message.reply_to.user else "Unknown"
|
||||
reply_to_preview = {
|
||||
"id": db_message.reply_to.id,
|
||||
"sender_name": reply_sender,
|
||||
"content": db_message.reply_to.content[:200],
|
||||
}
|
||||
|
||||
# Construct response
|
||||
response = {
|
||||
"type": "user_message",
|
||||
"id": db_message.id,
|
||||
"sender": user.username,
|
||||
"sender_id": user.id,
|
||||
"content": message_data["content"],
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id,
|
||||
"timestamp": db_message.timestamp.isoformat(),
|
||||
"reply_to_id": reply_to_id,
|
||||
"reply_to": reply_to_preview,
|
||||
"sender_avatar_url": user.avatar_url,
|
||||
}
|
||||
|
||||
# Publish to Redis for other users
|
||||
await redis_manager.publish(f"chat_{chat_id}", json.dumps(response))
|
||||
|
||||
# If bot is enabled and mentioned, process bot message
|
||||
if has_bot_mention:
|
||||
# Send bot request back to the sender for local processing
|
||||
bot_request = {
|
||||
"type": "bot_request",
|
||||
"sender": user.username,
|
||||
"sender_id": user.id,
|
||||
"bot_username": user.bot_username,
|
||||
"original_message": message_data["content"],
|
||||
"clean_message": clean_message,
|
||||
"lab_id": lab_id,
|
||||
"chat_id": chat_id
|
||||
}
|
||||
await websocket.send_text(json.dumps(bot_request))
|
||||
|
||||
except WebSocketDisconnect:
|
||||
redis_task.cancel()
|
||||
status_task.cancel()
|
||||
|
||||
# Set user as offline and remove connection
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user_id)
|
||||
.values(is_online=False, last_seen=datetime.now(timezone.utc))
|
||||
)
|
||||
await db.commit()
|
||||
remove_active_connection(user_id, websocket)
|
||||
|
||||
# Broadcast user went offline
|
||||
await broadcast_user_online_status(user_id, lab_id, False)
|
||||
|
||||
except Exception as e:
|
||||
redis_task.cancel()
|
||||
status_task.cancel()
|
||||
|
||||
# Set user as offline and remove connection
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user_id)
|
||||
.values(is_online=False, last_seen=datetime.now(timezone.utc))
|
||||
)
|
||||
await db.commit()
|
||||
remove_active_connection(user_id, websocket)
|
||||
@@ -0,0 +1,64 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
from functools import lru_cache
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "Distributed AI Chat"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
SECRET_KEY: str = "supersecretkeychangeinproduction"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 43200 # 30 days
|
||||
|
||||
DATABASE_URL: str = "postgresql+asyncpg://postgres:Mey%23Birth%40miN2004@localhost:5432/dai_db"
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
|
||||
# LiveKit Settings
|
||||
LIVEKIT_URL: str = "ws://94.183.170.121:7880"
|
||||
LIVEKIT_API_KEY: str = "devkey"
|
||||
LIVEKIT_API_SECRET: str = "secret"
|
||||
|
||||
# News Settings
|
||||
NEWS_FETCH_INTERVAL_HOURS: int = 4
|
||||
NEWS_MAX_ITEMS_PER_CATEGORY: int = 50
|
||||
NEWS_FEED_URLS: dict = {
|
||||
"Computer Science": "https://sciencedaily.com/rss/computers_math/computer_science.xml",
|
||||
"AI": "https://www.sciencedaily.com/rss/computers_math/artificial_intelligence.xml",
|
||||
"Technology": "https://www.sciencedaily.com/rss/matter_energy/technology.xml",
|
||||
"Quantum Physics":"https://sciencedaily.com/rss/matter_energy/quantum_physics.xml",
|
||||
"Quantum Computing":"https://sciencedaily.com/rss/matter_energy/quantum_computing.xml",
|
||||
"Mathematics":"https://sciencedaily.com/rss/computers_math/mathematics.xml"
|
||||
}
|
||||
|
||||
# LibreTranslate Settings
|
||||
ENABLE_TRANSLATION: bool = False # Set to True to enable translation feature
|
||||
LIBRETRANSLATE_URL: str = "http://localhost:5000"
|
||||
|
||||
# Cloud AI Config
|
||||
AI_API_KEY: str = "17356777-475f-5060-abb8-0f0ca04df768" # کلید API ارائهشده
|
||||
|
||||
AI_BASE_URL: str = "https://arvancloudai.ir/gateway/models/Qwen3-30B-A3B/JXMbt0YuGgdXs2emO40IIqCQswz7IwuUbLmX1uW-4m6wqb_OBQPYUj9tbBYoEViHY0w_UHYv7-ykcN3pM7lqFRqvvlXY4chS8JXWTOw4qULKkd2Jw98ILSGJPbSLrmQBHIYkPErjc8AMCE2eQF94vGbZu5PEKaYtGONxbpZJ_BQhSr-By79PCADPP3wMaGAugY4W1j7NvNgp4U2Db9G_MlnfaVBrKJUTmTwBQyrMgNJOpPlpEycsyYtwXMSqmh-V/v1"
|
||||
AI_MODEL: str = "Qwen3-30B-A3B"
|
||||
|
||||
# AI_BASE_URL: str = "https://arvancloudai.ir/gateway/models/Xerxes-1/t6tYSSaxEeUypQzkFQgozwlCd34729chHZKFs13eBKJsZymUkkLsBqVuMOPGZV2rAGI21B9JvKDQS_oDierPWTLBvp3SHTnknGZ0DzPZl28-Qv7ianVwZ_lUWrn8NQlL_yYjyAHQ4QKfgYA0I52BsCZB94H-klrsXso8BjTXRYc2sPnXDmPVoosepHYu5Vp4tB6nzVWs-dvr6iwUDz_pPX8HfRK6fENs_-awsN86XpjUJMa_ADQ/v1"
|
||||
# AI_MODEL: str ="Xerxes-1"
|
||||
|
||||
# Gitea Configuration
|
||||
GITEA_API_URL: str = "http://localhost:3000/api/v1"
|
||||
GITEA_ADMIN_TOKEN: str = "11e7888696c99a980dac2e74e5422feaf4da10ba"
|
||||
GITEA_ADMIN_USERNAME: str = "wikm"
|
||||
GITEA_WEBHOOK_SECRET: str = ""
|
||||
GIT_REPOS_BASE_DIR: str = "/tmp/git_repos" # Local directory for Git repositories
|
||||
|
||||
# AI_API_KEY: str = "sk-nn1BTh3sxtrg3ouyor8zil2vpmFatYX41HKhorofAj2EK9w9" # کلید API ارائهشده
|
||||
# AI_BASE_URL: str = "https://api.gapgpt.app/v1"
|
||||
# AI_MODEL: str = "gapgpt-qwen-3.5"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=True)
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db():
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,31 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt, JWTError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.users.service import get_user_by_username
|
||||
from app.auth.schemas import TokenData
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
print(payload)
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = await get_user_by_username(db, username=token_data.username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
@@ -0,0 +1,7 @@
|
||||
import redis.asyncio as redis
|
||||
from app.core.config import settings
|
||||
|
||||
redis_client = redis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True)
|
||||
|
||||
async def get_redis():
|
||||
return redis_client
|
||||
@@ -0,0 +1,34 @@
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from app.core.config import settings
|
||||
from app.core.database import SessionLocal
|
||||
from app.news.service import NewsService
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
async def scheduled_news_fetch():
|
||||
logger.info("Running scheduled news fetch")
|
||||
async with SessionLocal() as db:
|
||||
await NewsService.fetch_and_store_news(db)
|
||||
|
||||
def start_scheduler():
|
||||
if not scheduler.running:
|
||||
scheduler.add_job(
|
||||
scheduled_news_fetch,
|
||||
'interval',
|
||||
hours=settings.NEWS_FETCH_INTERVAL_HOURS,
|
||||
id='news_fetch_job',
|
||||
replace_existing=True
|
||||
)
|
||||
scheduler.start()
|
||||
logger.info("Scheduler started")
|
||||
|
||||
# Trigger immediate fetch on startup
|
||||
scheduler.add_job(scheduled_news_fetch, 'date', run_date=None, id='news_fetch_startup')
|
||||
|
||||
def shutdown_scheduler():
|
||||
if scheduler.running:
|
||||
scheduler.shutdown()
|
||||
logger.info("Scheduler shutdown")
|
||||
@@ -0,0 +1,34 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Union, Any
|
||||
import hashlib
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
# Try verifying with the new SHA256 pre-hashing method first
|
||||
sha256_password = hashlib.sha256(plain_password.encode("utf-8")).hexdigest()
|
||||
if pwd_context.verify(sha256_password, hashed_password):
|
||||
return True
|
||||
|
||||
# Fallback: Try verifying with the legacy truncation method
|
||||
# This ensures existing users can still login
|
||||
legacy_password = plain_password.encode("utf-8")[:72].decode("utf-8", errors="ignore")
|
||||
return pwd_context.verify(legacy_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
# Pre-hash with SHA256 to handle long passwords and avoid bcrypt 72-byte limit
|
||||
password = hashlib.sha256(password.encode("utf-8")).hexdigest()
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Gitea Integration Module
|
||||
Handles Git repository management and version control for labs
|
||||
"""
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Git-related database models
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class GitRepository(Base):
|
||||
"""Git repository mapping for labs"""
|
||||
__tablename__ = 'git_repositories'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
lab_id = Column(Integer, ForeignKey('labs.id'), unique=True, nullable=False)
|
||||
gitea_repo_id = Column(Integer, nullable=False)
|
||||
gitea_repo_name = Column(String(255), nullable=False)
|
||||
gitea_clone_url = Column(String(512), nullable=False)
|
||||
last_sync_at = Column(DateTime, default=datetime.utcnow)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationship
|
||||
lab = relationship("Lab", back_populates="git_repository")
|
||||
|
||||
|
||||
class GitCommit(Base):
|
||||
"""Track commits made by users"""
|
||||
__tablename__ = 'git_commits'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
lab_id = Column(Integer, ForeignKey('labs.id'), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
commit_hash = Column(String(40), nullable=False)
|
||||
commit_message = Column(String(1000), nullable=False)
|
||||
branch = Column(String(100), default='main')
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
Git API Router - Endpoints for Git operations
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.gitea.service import GiteaService
|
||||
from app.gitea.models import GitRepository, GitCommit
|
||||
from app.labs.models import Lab, LabMember
|
||||
from app.core.config import get_settings, Settings
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CommitRequest(BaseModel):
|
||||
message: str
|
||||
files: List[str] = []
|
||||
branch: str = "main"
|
||||
|
||||
|
||||
@router.post("/{lab_id}/repository")
|
||||
async def create_lab_repository(
|
||||
lab_id: int,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Create Git repository for lab"""
|
||||
# Check if lab exists and user is owner
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
owner_result = await db.execute(
|
||||
select(LabMember).where(
|
||||
LabMember.lab_id == lab_id,
|
||||
LabMember.user_id == current_user.id,
|
||||
LabMember.role == "owner",
|
||||
)
|
||||
)
|
||||
owner_member = owner_result.scalar_one_or_none()
|
||||
if not owner_member:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
# Check if repository already exists
|
||||
result = await db.execute(
|
||||
select(GitRepository).where(GitRepository.lab_id == lab_id)
|
||||
)
|
||||
existing_repo = result.scalar_one_or_none()
|
||||
if existing_repo:
|
||||
return {"status": "exists", "repository": existing_repo.gitea_repo_name}
|
||||
|
||||
# Create repository in Gitea
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
repo_info = await gitea_service.create_repository(lab_id, lab.name, current_user.username)
|
||||
|
||||
# Store repository mapping in database
|
||||
git_repo = GitRepository(
|
||||
lab_id=lab_id,
|
||||
gitea_repo_id=repo_info["id"],
|
||||
gitea_repo_name=repo_info["name"],
|
||||
gitea_clone_url=repo_info["clone_url"]
|
||||
)
|
||||
db.add(git_repo)
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "repository": repo_info}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create repository for lab {lab_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{lab_id}/repository")
|
||||
async def get_lab_repository(
|
||||
lab_id: int,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get lab repository information"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
result = await db.execute(
|
||||
select(GitRepository).where(GitRepository.lab_id == lab_id)
|
||||
)
|
||||
git_repo = result.scalar_one_or_none()
|
||||
if not git_repo:
|
||||
return {"status": "not_initialized"}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"repository": {
|
||||
"id": git_repo.gitea_repo_id,
|
||||
"name": git_repo.gitea_repo_name,
|
||||
"clone_url": git_repo.gitea_clone_url,
|
||||
"last_sync": git_repo.last_sync_at.isoformat() if git_repo.last_sync_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{lab_id}/commits")
|
||||
async def get_commit_history(
|
||||
lab_id: int,
|
||||
limit: int = 50,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Get commit history for lab"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
gitea_service = GiteaService(settings)
|
||||
commits = await gitea_service.get_commit_history(lab_id, limit)
|
||||
return {"status": "success", "commits": commits}
|
||||
|
||||
|
||||
@router.post("/{lab_id}/commit")
|
||||
async def create_commit(
|
||||
lab_id: int,
|
||||
commit_data: CommitRequest,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Create commit with file changes"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
if not commit_data.message.strip():
|
||||
raise HTTPException(status_code=400, detail="Commit message is required")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
commit_hash = await gitea_service.create_commit(
|
||||
lab_id, current_user.id, commit_data.message, commit_data.files
|
||||
)
|
||||
|
||||
# Store commit record in database
|
||||
commit = GitCommit(
|
||||
lab_id=lab_id,
|
||||
user_id=current_user.id,
|
||||
commit_hash=commit_hash,
|
||||
commit_message=commit_data.message,
|
||||
branch=commit_data.branch
|
||||
)
|
||||
db.add(commit)
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "commit_hash": commit_hash}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create commit for lab {lab_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{lab_id}/push")
|
||||
async def push_changes(
|
||||
lab_id: int,
|
||||
branch: str = "main",
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Push local commits to Gitea"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
await gitea_service.push_to_gitea(db, lab_id, branch)
|
||||
|
||||
# Update last sync time
|
||||
result = await db.execute(
|
||||
select(GitRepository).where(GitRepository.lab_id == lab_id)
|
||||
)
|
||||
git_repo = result.scalar_one_or_none()
|
||||
if git_repo:
|
||||
git_repo.last_sync_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "Pushed to Gitea"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to push lab {lab_id} to Gitea: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{lab_id}/pull")
|
||||
async def pull_changes(
|
||||
lab_id: int,
|
||||
branch: str = "main",
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Pull changes from Gitea"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
await gitea_service.pull_from_gitea(db, lab_id, branch)
|
||||
|
||||
# Update last sync time
|
||||
result = await db.execute(
|
||||
select(GitRepository).where(GitRepository.lab_id == lab_id)
|
||||
)
|
||||
git_repo = result.scalar_one_or_none()
|
||||
if git_repo:
|
||||
git_repo.last_sync_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "Pulled from Gitea"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to pull lab {lab_id} from Gitea: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{lab_id}/diff/{commit_hash}")
|
||||
async def get_file_diff(
|
||||
lab_id: int,
|
||||
commit_hash: str,
|
||||
path: Optional[str] = None,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Get diff for a commit (optionally scoped to a file)"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
diff = await gitea_service.get_file_diff(lab_id, commit_hash, path)
|
||||
return {"status": "success", "diff": diff}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get diff for lab {lab_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{lab_id}/checkout/{commit_hash}")
|
||||
async def checkout_commit(
|
||||
lab_id: int,
|
||||
commit_hash: str,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Checkout a commit and rebuild backpack files"""
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
await gitea_service.checkout_commit(db, lab_id, current_user.id, commit_hash)
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to checkout commit for lab {lab_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{lab_id}/status")
|
||||
async def get_git_status(
|
||||
lab_id: int,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Get git status for a lab repository"""
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
status_info = await gitea_service.get_status(lab_id)
|
||||
return {"status": "success", **status_info}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get status for lab {lab_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{lab_id}/file/{commit_hash}/{file_path:path}")
|
||||
async def get_file_content(
|
||||
lab_id: int,
|
||||
commit_hash: str,
|
||||
file_path: str,
|
||||
current_user = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Get file content at specific commit"""
|
||||
# Check if lab exists and user has access
|
||||
lab = await db.get(Lab, lab_id)
|
||||
if not lab:
|
||||
raise HTTPException(status_code=404, detail="Lab not found")
|
||||
|
||||
try:
|
||||
gitea_service = GiteaService(settings)
|
||||
content = await gitea_service.get_file_content_at_commit(lab_id, commit_hash, file_path)
|
||||
return {"status": "success", "content": content}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get file content for lab {lab_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/test/{lab_id}")
|
||||
async def test_git_integration(
|
||||
lab_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
settings: Settings = Depends(get_settings)
|
||||
):
|
||||
"""Test Git integration for a lab"""
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(GitRepository).where(GitRepository.lab_id == lab_id)
|
||||
)
|
||||
git_repo = result.scalar_one_or_none()
|
||||
if not git_repo:
|
||||
return {"status": "error", "message": "No Git repository found"}
|
||||
|
||||
# Check if local repository exists
|
||||
import os
|
||||
repo_dir = os.path.join(settings.GIT_REPOS_BASE_DIR, f"lab_{lab_id}")
|
||||
repo_exists = os.path.exists(os.path.join(repo_dir, '.git'))
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"lab_id": lab_id,
|
||||
"gitea_repo_id": git_repo.gitea_repo_id,
|
||||
"repo_name": git_repo.gitea_repo_name,
|
||||
"local_repo_exists": repo_exists,
|
||||
"last_sync": git_repo.last_sync_at.isoformat() if git_repo.last_sync_at else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing Git integration for lab {lab_id}: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||