Fix timeout issues: increased to 60s, add retry on CANCEL, default model Medium
This commit is contained in:
parent
ae5907c45f
commit
cabc6b8d85
3 changed files with 252 additions and 92 deletions
55
AGENTS.md
55
AGENTS.md
|
|
@ -95,7 +95,8 @@ web_search НЕ является интерфейсом поисковика и
|
|||
|
||||
**Tool Loop (MainActivity):**
|
||||
- Максимум итераций: 15
|
||||
- Timeout на итерацию: 30 секунд
|
||||
- Timeout на итерацию: 60 секунд
|
||||
- Retry (до 2 попыток) при ошибке "stream was reset: CANCEL"
|
||||
- AI может сделать несколько последовательных поисков если нужно
|
||||
|
||||
### 🌤️ Weather Tool (БЕСПЛАТНОЕ решение)
|
||||
|
|
@ -201,6 +202,8 @@ Kai имеет отличную документацию по tools: https://kai
|
|||
- LEARNING — выводы и паттерны
|
||||
- ERROR — известные ошибки
|
||||
- PREFERENCE — предпочтения пользователя
|
||||
- REMINDER_CAL — напоминания календаря (local mode)
|
||||
- CALENDAR_ERROR — ошибки подключения CalDAV
|
||||
|
||||
**Prompt injection:**
|
||||
```
|
||||
|
|
@ -240,6 +243,29 @@ Kai имеет отличную документацию по tools: https://kai
|
|||
| Safety | ✅ Max iterations (15), timeout (30s), result truncation (2000 chars) |
|
||||
| **OpenUrlTool (RSS)** | ✅ Автоматическое определение и парсинг RSS/Atom |
|
||||
|
||||
**CalDAV Calendar + Local Reminders (Phase 3 extension):**
|
||||
| Задача | Статус |
|
||||
|--------|--------|
|
||||
| iCalDAV зависимость (Apache 2.0) | ⏳ |
|
||||
| CalDavRepository (CRUD) | ⏳ |
|
||||
| UI: drawer_menu → диалог настроек CalDAV | ⏳ |
|
||||
| Настройка синхронизации (15м-сутки) | ⏳ |
|
||||
| caldav_get_events, create, update, delete | ⏳ |
|
||||
| Memory category REMINDER_CAL | ⏳ |
|
||||
| calendar_add_reminder tool | ⏳ |
|
||||
| calendar_get_reminders tool | ⏳ |
|
||||
| Напоминания (любой период: 5мин, 13мин, 2ч, 24ч) | ⏳ |
|
||||
| Счётчик ошибок CalDAV → memory | ⏳ |
|
||||
| UnifiedPush (опционально, на потом) | ⏳ |
|
||||
|
||||
**Memory REMINDER_CAL fields:**
|
||||
- key: название напоминания
|
||||
- value: описание
|
||||
- triggerTime: unix timestamp когда напомнить
|
||||
- status: pending / triggered / expired
|
||||
|
||||
**Trigger logic:** AI проверяет pending напоминания при каждом запросе
|
||||
|
||||
**RSS-ленты (протестировано):**
|
||||
- lenta.ru/rss/ ✅
|
||||
- kommersant.ru/rss/news.xml ✅
|
||||
|
|
@ -265,11 +291,20 @@ Kai имеет отличную документацию по tools: https://kai
|
|||
|
||||
---
|
||||
|
||||
## Unconfirmed Phases (Not Approved)
|
||||
## Active Plan (Phases 1-5)
|
||||
|
||||
Следующие фазы требуют дополнительного планирования:
|
||||
### Phase 3 (Active): CalDAV Calendar + Local Reminders
|
||||
**Статус:** 🔄 В разработке | **Оценка:** 4-5 дней
|
||||
|
||||
### Phase 4: Heartbeat
|
||||
**Два режима:**
|
||||
1. CalDAV — синхронизация с Baikal сервером
|
||||
2. Local — автономная напоминалка в памяти AI (работает БЕЗ интернета)
|
||||
|
||||
**Подробнее:** см. таблицу в разделе Phase 3 выше
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Heartbeat (Scheduled)
|
||||
**Оценка:** 2-3 дня
|
||||
|
||||
Автономная периодическая самопроверка:
|
||||
|
|
@ -315,6 +350,16 @@ app/build/outputs/apk/debug/app-debug.apk
|
|||
### Current Issues
|
||||
- Кнопка STOP не работает (требует streaming mode)
|
||||
|
||||
### Model Selection
|
||||
- **Default:** mistral-medium-latest (быстрее, меньше ошибок)
|
||||
- **Доступные модели:** Large, Medium, Codestral, Pixtral
|
||||
- **OkHttp timeouts:** connect 60s, read 120s, write 60s
|
||||
|
||||
### Error Handling (Исправлено)
|
||||
- Ошибки tool execution (таймауты, network errors) НЕ сохраняются в БД
|
||||
- Показываются пользователю через Toast
|
||||
- Предотвращает "отравление" контекста сообщениями об ошибках
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ВАЖНЫЕ ПРАВИЛА РАЗРАБОТКИ
|
||||
|
|
@ -449,4 +494,4 @@ app/build/outputs/apk/debug/app-debug.apk
|
|||
---
|
||||
|
||||
*Last updated: 2026-04-10*
|
||||
*Version: 1.9*
|
||||
*Version: 1.10*
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.mistral.chat.data.Message
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
|
@ -18,15 +19,28 @@ import kotlin.coroutines.Continuation
|
|||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
data class ChatResponse(
|
||||
val content: String,
|
||||
val usedModel: String,
|
||||
val toolCalls: List<ToolCall>
|
||||
)
|
||||
|
||||
data class ToolCall(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val arguments: JsonObject
|
||||
)
|
||||
|
||||
class MistralClient(private val apiKey: String) {
|
||||
|
||||
private var client = createNewClient()
|
||||
private var toolExecutor: ToolExecutor? = null
|
||||
|
||||
private fun createNewClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(25, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(false)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -35,7 +49,7 @@ class MistralClient(private val apiKey: String) {
|
|||
private val jsonMediaType = "application/json".toMediaType()
|
||||
|
||||
private var currentCall: Call? = null
|
||||
private var currentContinuation: Continuation<Result<Pair<String, String>>>? = null
|
||||
private var currentContinuation: Continuation<Result<ChatResponse>>? = null
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://api.mistral.ai/v1"
|
||||
|
|
@ -54,61 +68,17 @@ class MistralClient(private val apiKey: String) {
|
|||
)
|
||||
|
||||
val AVAILABLE_MODELS = listOf(
|
||||
"mistral-small-latest" to "Mistral Small",
|
||||
"mistral-medium-latest" to "Mistral Medium",
|
||||
"mistral-large-latest" to "Mistral Large",
|
||||
"mistral-medium-latest" to "Mistral Medium",
|
||||
"codestral-latest" to "Codestral",
|
||||
"pixtral-large-latest" to "Pixtral Large"
|
||||
)
|
||||
|
||||
const val DEFAULT_MODEL = "mistral-medium-latest"
|
||||
}
|
||||
|
||||
suspend fun getModels(): Result<List<Pair<String, String>>> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/models")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return@withContext Result.failure(Exception("API error: ${response.code}"))
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
|
||||
|
||||
val models = responseJson
|
||||
.getAsJsonArray("data")
|
||||
?.mapNotNull { obj ->
|
||||
try {
|
||||
val jsonObj = obj.asJsonObject
|
||||
val id = jsonObj.get("id")?.asString ?: return@mapNotNull null
|
||||
if (id in SUPPORTED_MODELS) {
|
||||
val displayName = id
|
||||
.replace("-latest", "")
|
||||
.replace("-12b-2409", "")
|
||||
.replace("-", " ")
|
||||
.split(" ")
|
||||
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
||||
id to displayName
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.distinctBy { it.first }
|
||||
?: emptyList()
|
||||
|
||||
Result.success(models)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is java.io.IOException && !e.message.orEmpty().contains("cancel", ignoreCase = true)) {
|
||||
Result.failure(e)
|
||||
} else {
|
||||
Result.failure(Exception("Request cancelled"))
|
||||
}
|
||||
}
|
||||
fun setToolExecutor(executor: ToolExecutor) {
|
||||
this.toolExecutor = executor
|
||||
}
|
||||
|
||||
fun cancelRequest() {
|
||||
|
|
@ -124,11 +94,55 @@ class MistralClient(private val apiKey: String) {
|
|||
client = createNewClient()
|
||||
}
|
||||
|
||||
suspend fun getModels(): Result<List<Pair<String, String>>> = try {
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/models")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return Result.failure(Exception("API error: ${response.code}"))
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
|
||||
|
||||
val models = responseJson
|
||||
.getAsJsonArray("data")
|
||||
?.mapNotNull { obj ->
|
||||
try {
|
||||
val jsonObj = obj.asJsonObject
|
||||
val id = jsonObj.get("id")?.asString ?: return@mapNotNull null
|
||||
if (id in SUPPORTED_MODELS) {
|
||||
val displayName = id
|
||||
.replace("-latest", "")
|
||||
.replace("-12b-2409", "")
|
||||
.replace("-", " ")
|
||||
.split(" ")
|
||||
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
||||
id to displayName
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.distinctBy { it.first }
|
||||
?: emptyList()
|
||||
|
||||
Result.success(models)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
suspend fun chat(
|
||||
model: String,
|
||||
messages: List<Message>,
|
||||
tools: List<JsonObject>? = null,
|
||||
onChunk: ((String) -> Unit)? = null
|
||||
): Result<Pair<String, String>> = withContext(Dispatchers.IO) {
|
||||
): Result<ChatResponse> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.addProperty("model", model)
|
||||
|
|
@ -138,14 +152,29 @@ class MistralClient(private val apiKey: String) {
|
|||
val messagesArray = JsonArray()
|
||||
messages.forEach { msg ->
|
||||
val msgObj = JsonObject()
|
||||
msgObj.addProperty("role", if (msg.isUser) "user" else "assistant")
|
||||
msgObj.addProperty("role", msg.role)
|
||||
msgObj.addProperty("content", msg.content)
|
||||
messagesArray.add(msgObj)
|
||||
}
|
||||
jsonObject.add("messages", messagesArray)
|
||||
|
||||
if (!tools.isNullOrEmpty()) {
|
||||
val toolsArray = JsonArray()
|
||||
tools.forEach { toolsArray.add(it) }
|
||||
jsonObject.add("tools", toolsArray)
|
||||
jsonObject.addProperty("tool_choice", "auto")
|
||||
}
|
||||
|
||||
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")
|
||||
|
|
@ -155,12 +184,13 @@ class MistralClient(private val apiKey: String) {
|
|||
.build()
|
||||
|
||||
currentCall = client.newCall(request)
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
|
||||
val result = suspendCancellableCoroutine<Result<ChatResponse>> { continuation ->
|
||||
currentContinuation = continuation
|
||||
|
||||
|
||||
currentCall?.enqueue(object : okhttp3.Callback {
|
||||
override fun onFailure(call: okhttp3.Call, e: java.io.IOException) {
|
||||
Log.e("MistralClient", "onFailure: ${e.message}", e)
|
||||
val cont = currentContinuation
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
|
|
@ -175,7 +205,7 @@ class MistralClient(private val apiKey: String) {
|
|||
|
||||
override fun onResponse(call: okhttp3.Call, response: Response) {
|
||||
val cont = currentContinuation
|
||||
|
||||
|
||||
if (cont == null) {
|
||||
response.close()
|
||||
currentCall = null
|
||||
|
|
@ -193,6 +223,8 @@ 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)
|
||||
}
|
||||
|
|
@ -208,6 +240,7 @@ class MistralClient(private val apiKey: String) {
|
|||
|
||||
val choices = responseJson.getAsJsonArray("choices")
|
||||
if (choices == null || choices.size() == 0) {
|
||||
Log.e("MistralClient", "No choices in response: $responseBody")
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(Exception("No response from API")))
|
||||
|
|
@ -216,6 +249,7 @@ class MistralClient(private val apiKey: String) {
|
|||
|
||||
val firstChoice = choices.get(0)?.asJsonObject
|
||||
if (firstChoice == null) {
|
||||
Log.e("MistralClient", "First choice is null")
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(Exception("Empty choice")))
|
||||
|
|
@ -226,10 +260,32 @@ class MistralClient(private val apiKey: String) {
|
|||
val content = message?.get("content")?.asString ?: ""
|
||||
|
||||
val usedModel = responseJson.get("model")?.asString ?: model
|
||||
|
||||
|
||||
val toolCalls = mutableListOf<ToolCall>()
|
||||
val toolCallsElement = message?.get("tool_calls")
|
||||
if (toolCallsElement != null && !toolCallsElement.isJsonNull) {
|
||||
val toolCallsArray = toolCallsElement.asJsonArray
|
||||
for ((index, tcElement) in toolCallsArray.withIndex()) {
|
||||
val tc = tcElement.asJsonObject
|
||||
if (tc != null) {
|
||||
val tcId = tc.get("id")?.asString ?: "call_${System.currentTimeMillis()}_$index"
|
||||
val tcName = tc.get("function")?.asJsonObject?.get("name")?.asString ?: ""
|
||||
val tcArgs = tc.get("function")?.asJsonObject?.get("arguments")?.asString
|
||||
if (tcName.isNotEmpty() && tcArgs != null) {
|
||||
try {
|
||||
val argsJson = gson.fromJson(tcArgs, JsonObject::class.java)
|
||||
toolCalls.add(ToolCall(tcId, tcName, argsJson))
|
||||
} catch (e: Exception) {
|
||||
// Skip invalid arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.success(content to usedModel))
|
||||
cont.resume(Result.success(ChatResponse(content, usedModel, toolCalls)))
|
||||
} catch (e: Exception) {
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
|
|
@ -245,8 +301,14 @@ class MistralClient(private val apiKey: String) {
|
|||
client = createNewClient()
|
||||
}
|
||||
}
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun executeTool(name: String, arguments: JsonObject): String {
|
||||
return toolExecutor?.executeTool(name, arguments)
|
||||
?: """{"status": "error", "message": "Tool executor not initialized"}"""
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.mistral.chat.ui
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
|
|
@ -32,6 +33,7 @@ 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.ChatResponse
|
||||
import com.mistral.chat.api.MistralClient
|
||||
import com.mistral.chat.api.ToolExecutor
|
||||
import com.mistral.chat.data.ChatDatabase
|
||||
|
|
@ -116,6 +118,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
|
||||
gson = Gson()
|
||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// Запрос разрешения на уведомления (Android 13+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = android.Manifest.permission.POST_NOTIFICATIONS
|
||||
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(permission), 1001)
|
||||
}
|
||||
}
|
||||
|
||||
val savedModel = prefs.getString(KEY_SELECTED_MODEL, null)
|
||||
if (savedModel != null) {
|
||||
|
|
@ -879,9 +889,10 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
val hasUserSelectedModel = prefs.contains(KEY_SELECTED_MODEL)
|
||||
if (!hasUserSelectedModel) {
|
||||
runOnUiThread {
|
||||
val codestralIndex = models.indexOfFirst { it.first.contains("codestral", ignoreCase = true) }
|
||||
if (codestralIndex >= 0) {
|
||||
selectedModelName = models[codestralIndex].first
|
||||
// По умолчанию выбираем medium, иначе первую доступную
|
||||
val mediumIndex = models.indexOfFirst { it.first.contains("mistral-medium", ignoreCase = true) }
|
||||
if (mediumIndex >= 0) {
|
||||
selectedModelName = models[mediumIndex].first
|
||||
} else if (models.isNotEmpty()) {
|
||||
selectedModelName = models[0].first
|
||||
}
|
||||
|
|
@ -892,7 +903,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
val hasUserSelectedModel = prefs.contains(KEY_SELECTED_MODEL)
|
||||
if (!hasUserSelectedModel) {
|
||||
runOnUiThread {
|
||||
selectedModelName = MistralClient.AVAILABLE_MODELS.firstOrNull()?.first ?: "mistral-medium-latest"
|
||||
selectedModelName = MistralClient.DEFAULT_MODEL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1102,26 +1113,54 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
|
||||
while (iteration < maxIterations) {
|
||||
iteration++
|
||||
|
||||
val result = withTimeout(30000L) {
|
||||
client?.chat(selectedModel, apiMessages, tools)
|
||||
?: Result.failure(Exception("Client not initialized"))
|
||||
|
||||
var result: Result<ChatResponse>? = null
|
||||
var retryCount = 0
|
||||
val maxRetries = 2
|
||||
|
||||
//Retry при CANCEL ошибке
|
||||
while (retryCount <= maxRetries) {
|
||||
result = withTimeout(60000L) {
|
||||
client?.chat(selectedModel, apiMessages, tools)
|
||||
?: Result.failure(Exception("Client not initialized"))
|
||||
}
|
||||
|
||||
if (!isActive) return@launch
|
||||
|
||||
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
|
||||
|
||||
result.onSuccess { chatResponse ->
|
||||
// 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 (chatResponse.toolCalls.isNotEmpty()) {
|
||||
// Выполняем все tool calls и добавляем результаты в историю
|
||||
for (toolCall in chatResponse.toolCalls) {
|
||||
val toolResult = client?.executeTool(toolCall.name, toolCall.arguments)
|
||||
?: """{"status": "error", "message": "Tool failed"}"""
|
||||
|
||||
apiMessages.add(Message(
|
||||
content = """[${toolCall.name}] result: $toolResult""",
|
||||
isUser = true,
|
||||
role = "user"
|
||||
))
|
||||
// Если tool вернул ошибку - добавляем, но не накапливаем
|
||||
if (!toolResult.contains("error")) {
|
||||
apiMessages.add(Message(
|
||||
content = """[${toolCall.name}] result: $toolResult""",
|
||||
isUser = true,
|
||||
role = "user"
|
||||
))
|
||||
}
|
||||
}
|
||||
// Продолжаем цикл - AI решит нужен ли еще поиск
|
||||
} else {
|
||||
|
|
@ -1129,15 +1168,21 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
finalResponse = chatResponse.content
|
||||
}
|
||||
}.onFailure { error ->
|
||||
finalResponse = "Ошибка: ${error.message}"
|
||||
val errorMsg = error.message ?: "Unknown error"
|
||||
android.util.Log.e("MainActivity", "API error: $errorMsg, iteration: $iteration")
|
||||
|
||||
// При ошибке - показываем её и выходим
|
||||
finalResponse = "Ошибка: $errorMsg"
|
||||
}
|
||||
} // close else block for result == null check
|
||||
|
||||
// Если есть финальный ответ или превышен лимит - выходим
|
||||
if (finalResponse != null || iteration >= maxIterations) {
|
||||
// Выходим только если есть ответ
|
||||
if (finalResponse != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Если прошли все итерации без ответа
|
||||
if (finalResponse == null && iteration >= maxIterations) {
|
||||
finalResponse = "Превышен лимит итераций (${maxIterations}). Попробуйте более конкретный запрос."
|
||||
}
|
||||
|
|
@ -1151,22 +1196,30 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
|
||||
// Проверяем что sessionId не изменился пока работал запрос
|
||||
if (currentSessionId == sessionIdAtStart) {
|
||||
addMessage(Message(content = responseToShow, isUser = false, senderName = selectedModel), sessionIdAtStart)
|
||||
// НЕ добавляем сообщения об ошибках в БД - они портят контекст
|
||||
val isError = responseToShow.contains("Timed out") ||
|
||||
responseToShow.contains("таймаут") ||
|
||||
responseToShow.startsWith("Ошибка:")
|
||||
|
||||
// Прокрутка к началу нового сообщения ИИ
|
||||
recyclerView.post {
|
||||
if (!userScrolledAfterSend) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
layoutManager.scrollToPositionWithOffset(messages.size - 1, 0)
|
||||
if (!isError) {
|
||||
addMessage(Message(content = responseToShow, isUser = false, senderName = selectedModel), sessionIdAtStart)
|
||||
|
||||
// Прокрутка к началу нового сообщения ИИ
|
||||
recyclerView.post {
|
||||
if (!userScrolledAfterSend) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
layoutManager.scrollToPositionWithOffset(messages.size - 1, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!responseToShow.startsWith("Ошибка:")) {
|
||||
// Генерируем название сессии после второго сообщения
|
||||
userMessageCount++
|
||||
if (userMessageCount == 2) {
|
||||
generateSessionTitle()
|
||||
}
|
||||
} else {
|
||||
// Показываем ошибку пользователю через Toast
|
||||
Toast.makeText(this@MainActivity, responseToShow, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue