Add Foreground Service for background work, clean up logging
This commit is contained in:
parent
cabc6b8d85
commit
dc461ad5dc
4 changed files with 567 additions and 416 deletions
|
|
@ -168,14 +168,6 @@ class MistralClient(private val apiKey: String) {
|
|||
val json = gson.toJson(jsonObject)
|
||||
val body = json.toRequestBody(jsonMediaType)
|
||||
|
||||
Log.d("MistralClient", "Request JSON size: ${json.length} chars")
|
||||
Log.d("MistralClient", "Request: model=$model, msgs=${messages.size}, tools=${tools?.size ?: 0}")
|
||||
|
||||
// Логируем все сообщения
|
||||
messages.forEachIndexed { idx, msg ->
|
||||
Log.d("MistralClient", "Msg[$idx] role=${msg.role}, len=${msg.content.length}, content=${msg.content.take(100)}...")
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/chat/completions")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
|
|
@ -223,8 +215,6 @@ class MistralClient(private val apiKey: String) {
|
|||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
Log.d("MistralClient", "Response: code=${response.code}, len=${responseBody.length}")
|
||||
|
||||
if (onChunk != null) {
|
||||
onChunk(responseBody)
|
||||
}
|
||||
|
|
|
|||
82
app/src/main/java/com/mistral/chat/api/NotificationTool.kt
Normal file
82
app/src/main/java/com/mistral/chat/api/NotificationTool.kt
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
class NotificationTool(private val context: Context) : Tool(
|
||||
name = "send_notification",
|
||||
description = "Отправить уведомление пользователю. Используй когда нужно сообщить важную информацию или напомнить о чём-то.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("title", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Заголовок уведомления"))
|
||||
})
|
||||
add("message", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Текст уведомления"))
|
||||
})
|
||||
})
|
||||
add("required", com.google.gson.JsonArray().apply {
|
||||
add("title")
|
||||
add("message")
|
||||
})
|
||||
}
|
||||
) {
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "mistral_chat_notifications"
|
||||
private const val CHANNEL_NAME = "Chat Notifications"
|
||||
}
|
||||
|
||||
override suspend fun execute(arguments: JsonObject): String {
|
||||
val title = arguments.get("title")?.asString ?: "Уведомление"
|
||||
val message = arguments.get("message")?.asString ?: ""
|
||||
|
||||
if (message.isEmpty()) {
|
||||
return """{"status": "error", "message": "Message cannot be empty"}"""
|
||||
}
|
||||
|
||||
// Проверка разрешения для Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = android.Manifest.permission.POST_NOTIFICATIONS
|
||||
if (context.checkSelfPermission(permission) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
return """{"status": "error", "message": "permission_denied: Уведомления отключены в настройках. Попроси пользователя включить их в настройках приложения."}"""
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Уведомления от Mistral Chat"
|
||||
enableVibration(true)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val notification = android.app.Notification.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(android.app.Notification.BigTextStyle().bigText(message))
|
||||
.setPriority(android.app.Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
|
||||
|
||||
"""{"status": "success", "message": "Уведомление отправлено: $title"}"""
|
||||
} catch (e: Exception) {
|
||||
"""{"status": "error", "message": "${e.message}"}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import android.content.SharedPreferences
|
|||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
|
|
@ -33,7 +34,10 @@ import com.google.android.material.textfield.TextInputEditText
|
|||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.gson.Gson
|
||||
import com.mistral.chat.R
|
||||
import com.mistral.chat.api.CalDavCalendar
|
||||
import com.mistral.chat.api.CalDavClient
|
||||
import com.mistral.chat.api.ChatResponse
|
||||
import com.mistral.chat.api.ApiForegroundService
|
||||
import com.mistral.chat.api.MistralClient
|
||||
import com.mistral.chat.api.ToolExecutor
|
||||
import com.mistral.chat.data.ChatDatabase
|
||||
|
|
@ -173,6 +177,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
loadModels()
|
||||
setupInput()
|
||||
loadProfilesAndSessions()
|
||||
restoreCalDavConnection()
|
||||
|
||||
inputField.postDelayed({
|
||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
|
@ -181,6 +186,36 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
}, 300)
|
||||
}
|
||||
|
||||
private fun restoreCalDavConnection() {
|
||||
val isConnected = encryptedPrefs.getBoolean("caldav_connected", false)
|
||||
|
||||
if (isConnected) {
|
||||
val url = encryptedPrefs.getString("caldav_url", "") ?: ""
|
||||
val username = encryptedPrefs.getString("caldav_username", "") ?: ""
|
||||
val password = encryptedPrefs.getString("caldav_password", "") ?: ""
|
||||
|
||||
if (url.isNotEmpty() && username.isNotEmpty() && password.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
calDavClient = CalDavClient(url, username, password)
|
||||
// Skip connection test - directly fetch calendars
|
||||
val calendarsResult = calDavClient?.getCalendars()
|
||||
calDavCalendars = calendarsResult?.getOrNull() ?: emptyList()
|
||||
|
||||
if (calDavCalendars.isNotEmpty()) {
|
||||
val calendarUrl = calDavCalendars.first().url
|
||||
toolExecutor?.setCalDavClient(calDavClient, calendarUrl)
|
||||
} else {
|
||||
// No calendars - might be connection issue
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Keep connected flag but try next time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDrawer() {
|
||||
navigationView.setNavigationItemSelectedListener(this)
|
||||
|
||||
|
|
@ -228,6 +263,9 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
R.id.action_location -> {
|
||||
showLocationDialog()
|
||||
}
|
||||
R.id.action_calendar -> {
|
||||
showCalendarDialog()
|
||||
}
|
||||
R.id.action_about -> {
|
||||
showAboutDialog()
|
||||
}
|
||||
|
|
@ -321,7 +359,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
val selectedIndex = if (currentSetting) 1 else 0
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.settings)
|
||||
.setTitle(R.string.session_menu_title)
|
||||
.setSingleChoiceItems(options, selectedIndex) { dialog, which ->
|
||||
val newValue = which == 1
|
||||
prefs.edit().putBoolean(KEY_NEW_SESSION_ON_START, newValue).apply()
|
||||
|
|
@ -367,11 +405,50 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
|
||||
private fun showLocationDialog() {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_location, null)
|
||||
val timezoneInput = dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.timezoneInput)
|
||||
val cityInput = dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.cityInput)
|
||||
val timezoneInput = dialogView.findViewById<android.widget.AutoCompleteTextView>(R.id.timezoneInput)
|
||||
val cityInput = dialogView.findViewById<android.widget.AutoCompleteTextView>(R.id.cityInput)
|
||||
|
||||
timezoneInput.setText(getDefaultTimezone())
|
||||
cityInput.setText(getDefaultCity())
|
||||
// Российские города
|
||||
val russianCities = listOf(
|
||||
"Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург", "Казань",
|
||||
"Нижний Новгород", "Челябинск", "Самара", "Омск", "Ростов-на-Дону",
|
||||
"Уфа", "Красноярск", "Воронеж", "Пермь", "Волгоград",
|
||||
"Улан-Удэ", "Иркутск", "Хабаровск", "Ярославль", "Тюмень", "Архангельск"
|
||||
)
|
||||
|
||||
// Мировые столицы
|
||||
val worldCities = listOf(
|
||||
"Лондон", "Париж", "Берлин", "Рим", "Мадрид", "Амстердам", "Брюссель",
|
||||
"Нью-Йорк", "Лос-Анджелес", "Чикаго", "Сан-Франциско", "Майami",
|
||||
"Токио", "Сеул", "Пекин", "Шанхай", "Гонконг", "Сингапур",
|
||||
"Дубай", "Мумбаи", "Дели", "Сидней", "Мельбурн", "Окленд",
|
||||
"Торонто", "Ванкувер", "Монреаль", "Мехико", "Сантьяго", "София"
|
||||
)
|
||||
|
||||
val allCities = russianCities + worldCities
|
||||
val cityAdapter = android.widget.ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, allCities)
|
||||
cityInput.setAdapter(cityAdapter)
|
||||
|
||||
// Timezones - крупнейшие города России и мира
|
||||
val timezones = listOf(
|
||||
"Europe/Moscow", "Europe/Kaliningrad", "Europe/Samara", "Europe/Volgograd",
|
||||
"Asia/Yekaterinburg", "Asia/Omsk", "Asia/Novosibirsk", "Asia/Krasnoyarsk",
|
||||
"Asia/Irkutsk", "Asia/Yakutsk", "Asia/Vladivostok", "Asia/Magadan",
|
||||
"Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Rome", "Europe/Madrid",
|
||||
"Europe/Amsterdam", "Europe/Brussels",
|
||||
"America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles",
|
||||
"America/Toronto", "America/Vancouver", "America/Mexico_City",
|
||||
"Asia/Tokyo", "Asia/Seoul", "Asia/Shanghai", "Asia/Hong_Kong", "Asia/Singapore",
|
||||
"Asia/Dubai", "Asia/Kolkata", "Australia/Sydney", "Australia/Melbourne",
|
||||
"Pacific/Auckland"
|
||||
)
|
||||
|
||||
val tzAdapter = android.widget.ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, timezones)
|
||||
timezoneInput.setAdapter(tzAdapter)
|
||||
|
||||
// Установка текущих значений
|
||||
timezoneInput.setText(getDefaultTimezone(), false)
|
||||
cityInput.setText(getDefaultCity(), false)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.location_title)
|
||||
|
|
@ -394,6 +471,167 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
.show()
|
||||
}
|
||||
|
||||
private var calDavClient: CalDavClient? = null
|
||||
private var calDavCalendars: List<CalDavCalendar> = emptyList()
|
||||
|
||||
private fun showCalendarDialog() {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_calendar, null)
|
||||
val caldavStatusText = dialogView.findViewById<android.widget.TextView>(R.id.caldavStatusText)
|
||||
val caldavUrlInput = dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.caldavUrlInput)
|
||||
val caldavUsernameInput = dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.caldavUsernameInput)
|
||||
val caldavPasswordInput = dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.caldavPasswordInput)
|
||||
val syncIntervalSpinner = dialogView.findViewById<android.widget.AutoCompleteTextView>(R.id.syncIntervalSpinner)
|
||||
|
||||
val prefs = getSharedPreferences("mistral_prefs", MODE_PRIVATE)
|
||||
// Sensitive data: encryptedPrefs, Non-sensitive: regular prefs
|
||||
val caldavUrl = encryptedPrefs.getString("caldav_url", "") ?: ""
|
||||
val caldavUsername = encryptedPrefs.getString("caldav_username", "") ?: ""
|
||||
|
||||
caldavUrlInput.setText(caldavUrl)
|
||||
caldavUsernameInput.setText(caldavUsername)
|
||||
|
||||
// Setup dropdown
|
||||
val intervals = arrayOf("15 минут", "30 минут", "1 час", "3 часа", "6 часов", "12 часов", "1 день")
|
||||
val intervalValues = arrayOf("15", "30", "60", "180", "360", "720", "1440")
|
||||
val adapter = android.widget.ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, intervals)
|
||||
syncIntervalSpinner.setAdapter(adapter)
|
||||
|
||||
val savedInterval = prefs.getString("caldav_sync_interval", "60") ?: "60"
|
||||
val savedIndex = intervalValues.indexOf(savedInterval)
|
||||
if (savedIndex >= 0) syncIntervalSpinner.setText(intervals[savedIndex], false)
|
||||
|
||||
val isConnected = encryptedPrefs.getBoolean("caldav_connected", false)
|
||||
|
||||
val dialogBuilder = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.calendar_title)
|
||||
.setView(dialogView)
|
||||
.setCancelable(true)
|
||||
|
||||
if (isConnected) {
|
||||
dialogBuilder.setPositiveButton(R.string.caldav_disconnect) { _, _ ->
|
||||
disconnectCalDav()
|
||||
}
|
||||
dialogBuilder.setNeutralButton("Синхронизировать") { _, _ ->
|
||||
syncCalDav()
|
||||
}
|
||||
} else {
|
||||
dialogBuilder.setPositiveButton(R.string.caldav_connect) { _, _ ->
|
||||
val url = caldavUrlInput.text.toString().trim()
|
||||
val username = caldavUsernameInput.text.toString().trim()
|
||||
val password = caldavPasswordInput.text.toString()
|
||||
val selectedText = syncIntervalSpinner.text.toString()
|
||||
val selectedIndex = intervals.indexOf(selectedText)
|
||||
val selectedInterval = if (selectedIndex >= 0) intervalValues[selectedIndex] else "60"
|
||||
|
||||
if (url.isNotEmpty() && username.isNotEmpty() && password.isNotEmpty()) {
|
||||
prefs.edit().putString("caldav_sync_interval", selectedInterval).apply()
|
||||
connectCalDav(url, username, password, prefs, caldavStatusText)
|
||||
} else {
|
||||
Toast.makeText(this, "Заполните все поля", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogBuilder.setNegativeButton(R.string.cancel, null)
|
||||
val dialog = dialogBuilder.create()
|
||||
|
||||
// Update status text
|
||||
caldavStatusText.setText(if (isConnected) R.string.caldav_connected else R.string.caldav_disconnected)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun connectCalDav(url: String, username: String, password: String, prefs: android.content.SharedPreferences, statusText: android.widget.TextView) {
|
||||
statusText.setText("Подключение...")
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
calDavClient = CalDavClient(url, username, password)
|
||||
val result = calDavClient?.testConnection()
|
||||
|
||||
if (result?.isSuccess == true) {
|
||||
// Fetch calendars
|
||||
val calendarsResult = calDavClient?.getCalendars()
|
||||
calDavCalendars = calendarsResult?.getOrNull() ?: emptyList()
|
||||
|
||||
// Save to ENCRYPTED storage (IMPORTANT!)
|
||||
encryptedPrefs.edit()
|
||||
.putString("caldav_url", url)
|
||||
.putString("caldav_username", username)
|
||||
.putString("caldav_password", password)
|
||||
.putBoolean("caldav_connected", true)
|
||||
.apply()
|
||||
|
||||
// Update ToolExecutor with CalDAV client
|
||||
val calendarUrl = calDavCalendars.firstOrNull()?.url
|
||||
toolExecutor?.setCalDavClient(calDavClient, calendarUrl)
|
||||
|
||||
val calendarsCount = calDavCalendars.size
|
||||
Toast.makeText(this@MainActivity, "Подключено! Найдено календарей: $calendarsCount", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
encryptedPrefs.edit().putBoolean("caldav_connected", false).apply()
|
||||
Toast.makeText(this@MainActivity, "Ошибка: ${result?.exceptionOrNull()?.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
encryptedPrefs.edit().putBoolean("caldav_connected", false).apply()
|
||||
Toast.makeText(this@MainActivity, "Ошибка: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectCalDav() {
|
||||
calDavClient = null
|
||||
calDavCalendars = emptyList()
|
||||
toolExecutor?.setCalDavClient(null, null)
|
||||
encryptedPrefs.edit()
|
||||
.putBoolean("caldav_connected", false)
|
||||
.apply()
|
||||
Toast.makeText(this, R.string.caldav_disconnected, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun syncCalDav() {
|
||||
val url = encryptedPrefs.getString("caldav_url", "") ?: ""
|
||||
val username = encryptedPrefs.getString("caldav_username", "") ?: ""
|
||||
val password = encryptedPrefs.getString("caldav_password", "") ?: ""
|
||||
|
||||
if (url.isEmpty() || username.isEmpty() || password.isEmpty()) {
|
||||
Toast.makeText(this, "Настройки не найдены", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (calDavClient == null) {
|
||||
calDavClient = CalDavClient(url, username, password)
|
||||
}
|
||||
|
||||
val calendarsResult = calDavClient?.getCalendars()
|
||||
|
||||
if (calendarsResult?.isSuccess == true) {
|
||||
val calendars = calendarsResult.getOrNull() ?: emptyList()
|
||||
|
||||
if (calendars.isNotEmpty()) {
|
||||
val eventsResult = calDavClient?.getEvents(calendars.first().url)
|
||||
val events = eventsResult?.getOrNull() ?: emptyList()
|
||||
|
||||
val msg = if (events.isEmpty()) {
|
||||
"Календари: ${calendars.size}, нет событий"
|
||||
} else {
|
||||
"Календари: ${calendars.size}, событий: ${events.size}"
|
||||
}
|
||||
Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this@MainActivity, "Календари не найдены", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this@MainActivity, "Ошибка: ${calendarsResult?.exceptionOrNull()?.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this@MainActivity, "Ошибка синхронизации: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
hamburgerButton.isVisible = true
|
||||
|
||||
|
|
@ -960,7 +1198,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
}
|
||||
|
||||
private fun getDefaultTimezone(): String {
|
||||
return prefs.getString(KEY_DEFAULT_TIMEZONE, "Europe/Moscow") ?: "Europe/Moscow"
|
||||
return prefs.getString(KEY_DEFAULT_TIMEZONE, "Asia/Irkutsk") ?: "Asia/Irkutsk"
|
||||
}
|
||||
|
||||
private fun setDefaultTimezone(timezone: String) {
|
||||
|
|
@ -968,7 +1206,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
}
|
||||
|
||||
private fun getDefaultCity(): String {
|
||||
return prefs.getString(KEY_DEFAULT_CITY, "Москва") ?: "Москва"
|
||||
return prefs.getString(KEY_DEFAULT_CITY, "Улан-Удэ") ?: "Улан-Удэ"
|
||||
}
|
||||
|
||||
private fun setDefaultCity(city: String) {
|
||||
|
|
@ -1055,6 +1293,15 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
sendButton.isEnabled = false
|
||||
progressIndicator.isVisible = true
|
||||
|
||||
// Start foreground service to keep app alive
|
||||
val activityContext = this
|
||||
ApiForegroundService.start(activityContext)
|
||||
|
||||
// Acquire WakeLock to keep CPU awake during API call
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MistralChat::ApiCallWakeLock")
|
||||
wakeLock.acquire(180000L) // Max 3 minutes
|
||||
|
||||
currentJob = lifecycleScope.launch {
|
||||
try {
|
||||
val profileContext = getSelectedProfileContext()
|
||||
|
|
@ -1108,6 +1355,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
|
||||
// Tool loop - до 15 итераций
|
||||
var iteration = 0
|
||||
var repeatCount = 0
|
||||
var lastToolCalls: List<String> = emptyList()
|
||||
val maxIterations = 15
|
||||
var finalResponse: String? = null
|
||||
|
||||
|
|
@ -1117,10 +1366,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
var result: Result<ChatResponse>? = null
|
||||
var retryCount = 0
|
||||
val maxRetries = 2
|
||||
val apiTimeout = 120000L // 120 seconds for large responses
|
||||
|
||||
//Retry при CANCEL ошибке
|
||||
while (retryCount <= maxRetries) {
|
||||
result = withTimeout(60000L) {
|
||||
result = withTimeout(apiTimeout) {
|
||||
client?.chat(selectedModel, apiMessages, tools)
|
||||
?: Result.failure(Exception("Client not initialized"))
|
||||
}
|
||||
|
|
@ -1130,39 +1380,67 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
val errorMsg = result?.exceptionOrNull()?.message ?: ""
|
||||
if ((errorMsg.contains("CANCEL") || errorMsg.contains("stream was reset")) && retryCount < maxRetries) {
|
||||
retryCount++
|
||||
android.util.Log.w("MainActivity", "Retry $retryCount after CANCEL, iteration $iteration")
|
||||
kotlinx.coroutines.delay(2000L)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isActive) return@launch
|
||||
|
||||
// Handle nullable result - выходим если null
|
||||
if (result == null) {
|
||||
finalResponse = "Ошибка: Не удалось получить ответ от API"
|
||||
} else {
|
||||
val chatResult = result
|
||||
chatResult.onSuccess { chatResponse ->
|
||||
android.util.Log.d("MainActivity", "API response: toolCalls=${chatResponse.toolCalls.size}")
|
||||
if (!isActive) return@launch
|
||||
|
||||
// Handle nullable result - выходим если null
|
||||
if (result == null) {
|
||||
finalResponse = "Ошибка: Не удалось получить ответ от API"
|
||||
} else {
|
||||
val chatResult = result
|
||||
chatResult.onSuccess { chatResponse ->
|
||||
|
||||
if (chatResponse.toolCalls.isNotEmpty()) {
|
||||
// Проверяем на повторяющиеся tool calls (защита от бесконечного цикла)
|
||||
val currentToolCalls = chatResponse.toolCalls.map { "${it.name}:${it.arguments.toString().take(50)}" }
|
||||
|
||||
if (iteration > 1 && lastToolCalls == currentToolCalls) {
|
||||
repeatCount++
|
||||
if (repeatCount >= 2) {
|
||||
// AI повторяет тот же tool 2+ раза - останавливаем
|
||||
finalResponse = "Не удалось выполнить действие. Попробуйте переформулировать запрос."
|
||||
return@onSuccess
|
||||
}
|
||||
} else {
|
||||
repeatCount = 0
|
||||
}
|
||||
lastToolCalls = currentToolCalls
|
||||
|
||||
// Выполняем все tool calls и добавляем результаты в историю
|
||||
var writeOperationCompleted = false
|
||||
var writeOperationMessage = ""
|
||||
|
||||
for (toolCall in chatResponse.toolCalls) {
|
||||
val toolResult = client?.executeTool(toolCall.name, toolCall.arguments)
|
||||
?: """{"status": "error", "message": "Tool failed"}"""
|
||||
|
||||
// Если tool вернул ошибку - добавляем, но не накапливаем
|
||||
if (!toolResult.contains("error")) {
|
||||
apiMessages.add(Message(
|
||||
content = """[${toolCall.name}] result: $toolResult""",
|
||||
isUser = true,
|
||||
role = "user"
|
||||
))
|
||||
// Все результаты добавляем в историю
|
||||
apiMessages.add(Message(
|
||||
content = """[${toolCall.name}] result: $toolResult""",
|
||||
isUser = true,
|
||||
role = "user"
|
||||
))
|
||||
|
||||
// Для write-операций (calendar_add) - после успеха запоминаем
|
||||
if (toolCall.name == "calendar_add_event" && toolResult.contains("success")) {
|
||||
writeOperationCompleted = true
|
||||
writeOperationMessage = "Готово! Событие добавлено в календарь."
|
||||
}
|
||||
}
|
||||
// Продолжаем цикл - AI решит нужен ли еще поиск
|
||||
|
||||
// Если write-операция выполнена - выходим из цикла
|
||||
if (writeOperationCompleted) {
|
||||
finalResponse = writeOperationMessage
|
||||
// Выходим из while цикла
|
||||
return@onSuccess
|
||||
}
|
||||
|
||||
// Продолжаем цикл только если не было write-операции
|
||||
} else {
|
||||
// Нет tool calls - это финальный ответ
|
||||
finalResponse = chatResponse.content
|
||||
|
|
@ -1197,9 +1475,13 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
// Проверяем что sessionId не изменился пока работал запрос
|
||||
if (currentSessionId == sessionIdAtStart) {
|
||||
// НЕ добавляем сообщения об ошибках в БД - они портят контекст
|
||||
val isError = responseToShow.contains("Timed out") ||
|
||||
responseToShow.contains("таймаут") ||
|
||||
responseToShow.startsWith("Ошибка:")
|
||||
// Проверяем более строго - только явные ошибки, а не просто упоминание в тексте
|
||||
val isError = responseToShow.startsWith("Timed out", ignoreCase = true) ||
|
||||
responseToShow.startsWith("Timeout", ignoreCase = true) ||
|
||||
responseToShow.startsWith("таймаут", ignoreCase = true) ||
|
||||
responseToShow.startsWith("Ошибка:", ignoreCase = true) ||
|
||||
responseToShow.startsWith("Error:", ignoreCase = true) ||
|
||||
responseToShow.startsWith("Failed to", ignoreCase = true)
|
||||
|
||||
if (!isError) {
|
||||
addMessage(Message(content = responseToShow, isUser = false, senderName = selectedModel), sessionIdAtStart)
|
||||
|
|
@ -1225,7 +1507,23 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
|
||||
sendButton.isEnabled = true
|
||||
progressIndicator.isVisible = false
|
||||
|
||||
// Release WakeLock
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
|
||||
// Stop foreground service
|
||||
ApiForegroundService.stop(this@MainActivity)
|
||||
} catch (e: Exception) {
|
||||
// Release WakeLock on error
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
|
||||
// Stop foreground service on error
|
||||
ApiForegroundService.stop(activityContext)
|
||||
|
||||
if (!isActive) return@launch
|
||||
android.util.Log.e("MainActivity", "Exception: ${e.message}", e)
|
||||
if (currentSessionId == sessionIdAtStart) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue