---
title: "RAG (Retrieval-Augmented Generation), Function / Tools Calling, CAG (Cache-Augmented Generation) за LLM модели"
url: "https://romankryvolapov.com/bg/rag-and-function-calling/"
description: "RAG, Function Calling и CAG за локални LLM модели: как работи векторната база данни, с примери на Kotlin, DSPy, LangChain, LangChain4j и llama.cpp."
language: bg
published: 2025-08-19
updated: 2026-11-13
---
Здравейте!\
Ще се опитам да изложа тук опита си с реализацията на RAG и функционални извиквания за LLM модел, стартиран локално на телефон.\
За cloud моделите общата идея не се различава, така че след като прочетете, ще можете да разберете как работи всичко това и за какво се използва.\
Интересното тук е, че успехът до голяма степен зависи от това колко правилно е написан промптът с онова, което искаме от модела, тоест само добър код не върши работа – нужна е и хуманитарна част: да обясним всичко на модела и да се надяваме, че е разбрал правилно.

## Какво е RAG (Retrieval-Augmented Generation)

При обучението в LLM модела се влагат терабайти информация, но за вас, вашия проект, вашите документи и вашата котка той може и да не знае.

Можете да опишете всичко това в заявката към модела, в промпта, но ако информацията е много, размерът на контекста на модела може да свърши, преди да стигнете до въпроса, или просто да ви омръзне да пишете.

За да знае моделът информация, върху която не е бил обучаван, се използва RAG (Retrieval Augmented Generation).

RAG може да има много различни реализации, но общият смисъл е такъв:\
**Към съобщението ви се добавя допълнителна информация**, обикновено в текстов вид, например:

- Резултат от интернет заявка
- Данни от база данни
- Текст от прикачен документ и така нататък

В този пример ще използвам векторна база данни, която се справя отлично с LLM модели.

## Какво е векторна база данни

Преди заявките към модела трябва да „нахраним“ базата данни с вашите данни, за да може после моделът да намери най-подходящите от тях за вашата заявка.

**При добавяне на текст в базата:**

- Текстът се разбива на части
- С помощта на специален embedding модел Gecko за всяка част се генерира вектор със 768 измерения, чийто смисъл максимално съответства на съдържанието на частта от текста
- Векторът заедно с текста се записва в базата

**При търсене** на текст **в базата:**

- Заявката за търсене се преобразува във вектор с помощта на модела Gecko
- В базата се търси вектор, максимално близък до вектора на заявката за търсене
- За този вектор се извлича текстът
- Текстът се добавя към вашата заявка към LLM модела

## Какво е Function Calling

Разликата с RAG е, че в този случай моделът сам решава дали да се обърне към векторната база данни или към друг източник, за да получи информация.\
За целта:

- в промпта се указва, че моделът има достъп до функционални извиквания, описва се форматът им, описват се условията, при които моделът трябва да направи такова извикване
- ако моделът реши да направи такова извикване, той връща служебна информация със заявка за извикване
- парсерът разбира, че трябва да се направи извикване, чете данните от векторната база, добавя ги в опашката със съобщения и изпраща заявката отново.

## Какво е CAG (Cache-Augmented Generation)

Техника за ускоряване и поевтиняване на работата на LLM, основана на повторното използване на резултатите от предишни заявки.

Докато работи, LLM пази междинни представяния на текста (hidden states, attention кешове).

При повторни или подобни заявки не е нужно цялата последователност да се прекарва отново през модела. Вместо това се взема запазеният кеш и моделът доработва само „новата“ част.

Това намалява броя на изчисленията и снижава забавянето.

**Варианти на приложение:**

**Token-level caching:**\
запазват се KV кешовете на вниманието за вече обработените токени (вече се използва в [llama.cpp](/bg/llama-cpp/), Transformers и други).

**Prompt-level caching:**\
резултатите за често срещани промптове се запазват и се използват повторно.

**Semantic caching:**\
съхранение на отговори по смислова близост (например чрез векторно хранилище). Ако новата заявка прилича на стара, връща се запазеният резултат или той се използва като контекст.

**Разлика с обикновения RAG:**\
RAG (Retrieval-Augmented Generation) извлича външни данни от база знания.\
CAG спестява изчисления чрез повторно използване на предишни изпълнения на модела.

## Пример 1 – Реализация на Function Calling в Kotlin на ниско ниво

Тук ще дам пример как работи Function Calling от гледна точка на реализацията.

Първо правим шаблон, който възможно най-подробно обяснява на LLM модела, че има достъп до функционални извиквания и как да работи с тях.

Много е важно да изберем ключова дума, по която после ще определим, че моделът иска да направи функционално извикване; в този пример тя е tool_code, трябва да е уникална и да не се среща в текста.

След това описваме структурите, които искаме да получим; в този пример това е Json, но структурата може да бъде каквато и да е – използвах Json, защото лесно се парсва в клас, но от гледна точка на използването на токени не е оптимален.

Структурата е сложна и има вероятност моделът да сгреши; може да се връща грешка при парсване, да се добавя в опашката със съобщения заедно с описание, че моделът е сгрешил и трябва да поправи заявката си, и да се изпраща на модела за нов опит.

### В този пример използвах 2 функционални извиквания

**get_weather**

```json
{
  "name": "get_weather",
  "arguments": {
    "location": "New York"
 }
}
```

Това е много просто извикване с единствен параметър, при който трудно може да се сгреши.

**search_docs**

```json
{
  "name": "search_docs",
  "arguments": {
    "query": "quantum computing basics",
    "top_k": 5,
    "min_similarity_score": 0.6
  }
}
```

Това е заявка за търсене към базата, в която ще се търсят подходящи документи.

Това извикване може да се постави в цикъл със стартови параметри и да се добави в промпта описание – ако моделът не получи подходящи резултати, може да намали **min_similarity_score** или да промени формулировката на **query** и да направи извикването отново.

Много е важно в промпта да се опишат подробни инструкции за модела за всяка стъпка – моделът няма мотивация и интуиция, за да разбере какво трябва да прави нататък, и ако следващите стъпки не са описани, ще върне празно съобщение.

Важно е също да разбираме, че промптът не е програмен код и правилното му изпълнение има вероятност, която зависи от това колко добре сме описали всички действия и структури.

В края на промпта е важно да се напише, че моделът ще получи резултат във формат Json, чиято структура не е описана в промпта, и моделът сам трябва да разбере как да формулира отговора към потребителя в съответствие с неговата заявка.

Сценарият в промпта има почти безкрайни възможности за настройка, например ако ни трябват данни от база данни, можем да опишем структурата на базата и да поискаме от модела да генерира SQL заявка в съответствие със заявката на потребителя, която след това да изпълним и да получим данните; в този случай е важно да не забравим да ограничим правата на връзката само за четене.

Някои модели са тренирани да изпълняват функционални извиквания с определен формат и ключови думи, в тях може да има и отделна роля за резултата от извикването, например **function**. Аз използвах модел, който не е бил тренирован за това, и той се справяше отлично; мисля, че всеки съвременен повече или по-малко „умен“ модел ще се справи.

**Пълен текст на промпта:**

```kotlin
const val DEFAULT_FUNCTION_CALLING_PROMPT = """
Call tools only when the user explicitly asks to check, verify, look up, find, or search for something. In all other cases, answer directly without calling any tool.

If you decide to invoke a function, output only a JSON object inside a fenced block marked as tool_code without any text before or after it.

Available tools:

search_docs
Description: Searches knowledge and returns the most relevant results as plain text.
Arguments:
query (string) — User string query to search in the vector store.
top_k (integer, default 5) — Number of results to return.
min_similarity_score (float, default 0.6) — Minimum similarity score from 0.0 (no filtering) to 1.0 (exact match).

get_weather
Description: Gets current weather for a location.
Arguments:
location (string) — City or place name.

JSON format inside the fenced block:
tool_code
{
"name": "<tool_name>",
"arguments": {
"<arg_name>": <arg_value>
}
}

Examples:
tool_code
{ "name": "search_docs", "arguments": { "query": "quantum computing basics", "top_k": 5, "min_similarity_score": 0.6 } }

tool_code
{ "name": "get_weather", "arguments": { "location": "New York" } }

Important rules:
Use tools only when explicitly requested by the user to check, verify, look up, find, or search. Otherwise, provide a direct answer.
When calling a tool, the output must be valid JSON with exactly two keys: name and arguments.
Do not include any explanations or extra content outside the tool_code block.

After receiving tool results (the result will be in JSON format):
1. Parse and process the result.
2. Convert it into a simple, clear, and human-readable natural-language response.
3. Always reply to the user with this processed answer as the next message.
4. If no relevant results are found, briefly explain that nothing relevant was found and suggest next steps.
"""
```

В този пример използвах локалните модели **Gemma** и **DeepSeek**, стартирани в **LM Studio**, който има **OpenAI съвместимо API**, тоест моето приложение се свързва с него през **Localhost** и прави заявки; cloud моделите ще се справят още по-добре, тъй като имат много повече параметри и специално API за функционални извиквания, но в примера нарочно поисках да използвам реализацията на възможно най-ниско ниво.

````kotlin
fun main() = runBlocking {

    val messages = mutableListOf(

       	// Добавяме промпта в опашката със съобщения с роля system
        ChatMessage(
            role = "system",
            content = DEFAULT_FUNCTION_CALLING_PROMPT
        ),

        // Добавяме в опашката заявката на потребителя; от тази заявка е ясно,
        // че потребителят моли да се провери времето, моделът знае, че му е достъпна
      	// функцията за проверка на времето, описана в промпта, и я извиква,
      	//  като изпраща в отговор съобщение, започващо с tool_code
        ChatMessage(
            role = "user",
            content = "Check weather in London",
        )
    )
    var iterator = 0
    while (iterator < 3) {
        try {
            chatAnswerWithToolsStream(
                ChatCompletionRequest(
                    model = MODEL_ID,
                    messages = messages,
                    temperature = 0.2
                )
            ).collect { piece ->
                when (piece) {
                    is ChatResult.Debug -> {
                        println("DEBUG MESSAGE:\n${piece.message}")
                    }

                    is ChatResult.Message -> {
                        println("FINAL MESSAGE:\n${piece.message}")
                        iterator = 3
                        return@collect
                    }

                    is ChatResult.SearchDocs -> {
                        println("SEARCH DOCS data:\n$piece")
                        iterator = 3

                        // В този пример ще използвам само проверка на времето,
                        // но добавих още една функция, за да покажа, че може да са много
                        return@collect
                    }

                    is ChatResult.GetWeather -> {
                        println("GET WEATHER result:\n$piece")

                        // Ако моделът е решил да извика функция, добавяме нейното извикване
                        // в опашката със съобщения с роля assistant
                        // Важно е да се добавят всички съобщения в опашката, за да
                        // знае моделът цялата история на диалога
                        messages.add(
                            ChatMessage(
                                role = "assistant",
                                content = piece.message ?: ""
                            )
                        )

                        // правим заявка към API-то за данни за времето
                        val result = getCurrentWeatherByCity(
                            city = piece.location!!,
                        )
                        val jsonResult: String = kotlinxJsonConfig.encodeToString(result)
                        println("GET WEATHER request result:\n$jsonResult")

                        // добавяме в опашката със съобщения с роля user, тъй като този модел
                        // не поддържа специални роли за функционални извиквания
                        messages.add(
                            ChatMessage(
                                role = "user",
                                content = jsonResult
                            )
                        )
                        iterator++
                    }
                }
            }
        } catch (e: Throwable) {
            println(e.message ?: "error")
            break
        }
    }

}

// код за работа с OpenAI API, тук е важно извикването на функцията
// parseToolJson, която ще върне null, ако не е успяла да преобразува отговора във функционално извикване
fun chatAnswerWithToolsStream(req: ChatCompletionRequest): Flow<ChatResult> = flow {
    val response = client.post("$BASE_URL/api/v0/chat/completions") {
        contentType(ContentType.Application.Json)
        setBody(req.copy(stream = true))
    }
    val channel: ByteReadChannel = response.bodyAsChannel()

    val fullBuilder = StringBuilder()

    while (!channel.isClosedForRead) {
        val line: String = channel.readUTF8Line() ?: break
        val payload: String = extractDataLine(line) ?: continue

        val chunk: StreamChatChunk = runCatching {
            kotlinxJsonConfig.decodeFromString(StreamChatChunk.serializer(), payload)
        }.getOrNull() ?: continue

        val piece: String = extractChatDeltaContent(chunk)
        if (piece.isNotEmpty()) {
            fullBuilder.append(piece)
            emit(ChatResult.Debug(message = fullBuilder.toString()))
        }

        if (isFinished(chunk)) break
    }

    val fullMessage: String = fullBuilder.toString().trim()
    val toolResult: ChatResult? = parseToolJson(fullMessage)

    if (toolResult != null) {
        emit(toolResult)
    } else {
        emit(ChatResult.Message(message = fullMessage))
    }
}

fun parseToolJson(input: String): ChatResult? {
    try {
        val json = when {

            // ако отговорът започва с tool_code, разбираме, че това не е отговор към потребителя
            // а функционално извикване и по-нататък следва json с неговия тип и параметри
            input.startsWith("tool_code") -> input.replace("tool_code", "")
                .trim()

            input.startsWith("```tool_code") -> input.replace("```tool_code", "")
                .removeSuffix("```")
                .trim()

            else -> return null
        }
        val rawTool = runCatching {
            gson.fromJson(json, RawTool::class.java)
        }.getOrNull() ?: return null

        val jsonObject = rawTool.arguments?.asJsonObject ?: return null

		// определяме коя точно функция иска да извика моделът
        // и преобразуваме json в модел на данни.
        // Този код може да се подобри, като се върне грешка на модела, ако преобразуването
        // не успее, с описание, че моделът трябва да редактира отговора си и да го поправи
        return when (rawTool.name?.lowercase()) {
            "search_docs" -> {
                gson.fromJson(jsonObject, ChatResult.SearchDocs::class.java).copy(
                    message = json
                )
            }
            "get_weather" -> gson.fromJson(jsonObject, ChatResult.GetWeather::class.java).copy(
                message = json
            )
            else -> null
        }
    } catch (e: Exception) {
        println(e.message ?: "error")
        return null
    }
}
````

Допълнителен код, ако поискате да повторите:

```groovy
ext {
    ktor_version = "3.2.3"
    kotlinx_serialization_json_version = "1.8.1"
    logback_version = "1.4.14"
}

dependencies {
    implementation "io.ktor:ktor-client-core:$ktor_version"
    implementation "io.ktor:ktor-client-cio:$ktor_version"
    implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
    implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
    implementation "io.ktor:ktor-client-logging:$ktor_version"
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_json_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    implementation "com.google.code.gson:gson:2.13.1"
}
```

```kotlin
import io.ktor.client.call.*
import io.ktor.client.request.*

suspend fun getCurrentWeatherByCity(
    apiKey: String = WEATHER_API_KEY,
    city: String,
    units: String = "metric",
    lang: String = "en"
): WeatherResponse {
    return client.get(OWM_BASE) {
        url {
            parameters.append("q", city)
            parameters.append("appid", apiKey)
            parameters.append("units", units)
            parameters.append("lang", lang)
        }
    }.body()
}
```

```kotlin
import com.google.gson.JsonElement
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject


@Serializable
data class ChatMessage(
    @SerialName("role")
    val role: String,
    @SerialName("content")
    val content: String
)

@Serializable
data class ChatCompletionRequest(
    @SerialName("model")
    val model: String,
    @SerialName("messages")
    val messages: List<ChatMessage>,
    @SerialName("temperature")
    val temperature: Double? = null,
    @SerialName("max_tokens")
    val maxTokens: Int? = null,
    @SerialName("stream")
    val stream: Boolean? = null
)

@Serializable
data class StreamChatChunk(
    @SerialName("choices")
    val choices: List<StreamChatChoice>? = null
)


@Serializable
data class ChatChoiceMessage(
    @SerialName("content")
    val content: String? = null
)

@Serializable
data class ChatChoice(
    @SerialName("message")
    val message: ChatChoiceMessage? = null
)

@Serializable
data class ChatResponse(
    @SerialName("choices")
    val choices: List<ChatChoice> = emptyList()
)


@Serializable
data class StreamChatChoice(
    @SerialName("delta")
    val delta: JsonObject? = null,
    @SerialName("finish_reason")
    val finishReason: String? = null
)

data class RawTool(
    val name: String?,
    val arguments: JsonElement?
)

sealed interface ChatResult {

    data class Debug(
        val message: String?,
    ) : ChatResult

    data class Message(
        val message: String?,
    ) : ChatResult

    data class SearchDocs(
        val message: String? = null,
        val query: String?,
        val topK: Int?,
        val minSimilarityScore: Float?
    ) : ChatResult

    data class GetWeather(
        val message: String? = null,
        val location: String?,
    ) : ChatResult

}
```

```kotlin
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive

fun extractDataLine(line: String): String? {
    val t: String = line.trim()
    if (t.isEmpty()) return null
    if (t == "data: [DONE]" || t == "[DONE]") return null
    if (t.startsWith("event:")) return null
    return if (t.startsWith("data:")) t.removePrefix("data:").trim() else t
}

fun extractChatDeltaContent(chunk: StreamChatChunk): String {
    val choice: StreamChatChoice = chunk.choices?.firstOrNull() ?: return ""
    val delta = choice.delta ?: return ""
    val el: JsonElement? = delta["content"]
    return el?.jsonPrimitive?.contentOrNull ?: ""
}

fun extractTextDelta(chunk: StreamTextChunk): String {
    val choice: StreamTextChoice = chunk.choices?.firstOrNull() ?: return ""
    val direct: String? = choice.text
    if (direct != null) return direct
    val delta = choice.delta
    val el: JsonElement? = delta?.get("content") ?: delta?.get("text")
    return el?.jsonPrimitive?.contentOrNull ?: ""
}

fun isFinished(chunk: StreamChatChunk): Boolean {
    val reason: String? = chunk.choices?.firstOrNull()?.finishReason
    return !reason.isNullOrEmpty()
}

fun isFinished(chunk: StreamTextChunk): Boolean {
    val reason: String? = chunk.choices?.firstOrNull()?.finishReason
    return !reason.isNullOrEmpty()
}
```

```kotlin
import com.google.gson.FieldNamingPolicy
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

const val BASE_URL: String = "http://localhost:1234"

const val OWM_BASE = "https://api.openweathermap.org/data/2.5"

const val WEATHER_API_KEY = "..."

const val MODEL_ID = "google/gemma-3n-e4b"

val gson: Gson = GsonBuilder()
    .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
    .create()

val kotlinxJsonConfig: Json = Json {
    ignoreUnknownKeys = true
    prettyPrint = false
}

val client: HttpClient = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(kotlinxJsonConfig)
    }
    install(HttpTimeout) {
        requestTimeoutMillis = 60_000
        connectTimeoutMillis = 10_000
        socketTimeoutMillis = 60_000
    }
}
```

## Пример 2 – Python фреймуърк DSPy

DSPy е open-source Python фреймуърк за „програмиране, а не промптинг“ на LLM: описвате поведението на модулите в кода, а оптимизаторите автоматично подбират подсказките и, при желание, дообучават теглата за избраната метрика. Подходящ е за класификатори, RAG и агенти.\
dspy.ai

[https://dspy.ai](https://dspy.ai)

[https://github.com/stanfordnlp/dspy](https://github.com/stanfordnlp/dspy)

**Signatures:**\
декларативни спецификации на входовете и изходите на модулите.

**Modules:** \
готови стратегии за извикване на LLM (Predict, ChainOfThought, ReAct и др.), от които се сглобяват пайплайни.

- **Predict:** Базов строителен блок. Прави заявка към LLM по зададена сигнатура (input -> output). Използва се за прости задачи като QA, обобщаване, класификация.
- **Signature:** Определя структурата на входните и изходните данни. Може да се използва като „q -> a“ или като Python клас с анотации. Прави кода четим и строг.
- **Chain:** Позволява да се свържат няколко Predict в pipeline. Удобно е, когато има междинни стъпки (извличане на факти → резюме → отговор).
- **ReAct:** Агентен режим: моделът разсъждава и извиква инструменти. Позволява да се свързват API, бази данни, външни функции. Типичен вариант за чат ботове с достъп до инструменти (tools).
- **ChainOfThought (CoT):** Принуждава модела да обяснява стъпките на разсъждението си. Удобно за сложни задачи, математика, логика.
- **Retrieve / RAG:** Интеграция с векторни бази данни. Позволява да се добави търсене в документи към workflow.

**Optimizers (преди teleprompters):**\
алгоритми, които „компилират“ програмата в ефективни подсказки/тегла по метрика; работят дори с 5–10 примера.

**Пример, аналогичен по функции на предишния**:

requirements.txt

```text
dspy==3.0.1
litellm==1.75.8
openai==1.99.9
optuna>=4.5.0
gepa[dspy]==0.0.4
regex>=2025.7.34
diskcache>=5.6.3
json-repair>=0.49.0
magicattr>=0.1.6
backoff>=2.2.1
asyncer==0.0.8
cachetools>=6.1.0
aiohttp>=3.12.15
```

```python
import dspy
import requests
from typing import Dict

dspy.enable_logging()

OPENWEATHER_API_KEY = "..."

# Задаваме доставчик за LLM
lm = dspy.LM(
    "openai/google/gemma-3n-e4b",
    api_base="http://localhost:1234/v1",
    api_key="lm-studio",
    temperature=0
)
dspy.configure(lm=lm)

# Проверяваме дали доставчикът е намерен
print("Provider:\n", type(lm.provider))

# Изпращаме на модела тестови данни, като използваме модула Predict
probe = dspy.Predict("question -> answer")
try:
    result = probe(question="Who are you", max_tokens=1)
    print("Model supported by provider, test answer:\n", result.answer)
except Exception as e:
    print("Model not supported by provider:\n", e)

def get_weather(city: str, units: str = "metric") -> str:
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        "q": city,
        "appid": OPENWEATHER_API_KEY,
        "units": units,
        "lang": "en"
    }
    response = requests.get(url, params=params)
    if response.status_code == 200:
        data: Dict = response.json()
        temp = data["main"]["temp"]
        description = data["weather"][0]["description"]
        return f"Weather in {city}: {temp}°C, {description}"
    else:
        return f"Error fetching weather: {response.text}"


# Добавяме формат на общуване въпрос -> отговор,
# инструмента get_weather и модула ReAct
# Фреймуъркът сам ще генерира промпта в съответствие
# с това кой модул и с какви параметри използваме
agent = dspy.ReAct(
    "question -> answer",
    tools=[get_weather]
)

result = agent(question="Check weather in London")

print("Answer:\n", result.answer)

# Можем да разгледаме историята на общуването с LLM,
# кои tools са били извикани и какъв prompt е бил генериран
history = dspy.inspect_history(n=10)

```

**Изход в конзолата:**

```text
Provider:
 <class 'dspy.clients.openai.OpenAIProvider'>
Model supported by provider, test answer:
 I am Gemma, an open-weights AI assistant. I am a large language model trained by Google DeepMind.
Answer:
 Weather in London: 15.89°C, overcast clouds
```

**Лог**:

- моделът отговаря на прост въпрос без инструменти (`Who are you`). Тук той веднага дава `[[ ## answer ## ]]`.

- системата поставя задача: „Имаш инструменти get_weather/finish“. Моделът мисли и дава първата стъпка: `next_tool_name = get_weather`.

- моделът получава trajectory с резултата от API (`Weather in London: ...`) и трябва да реши какво да прави нататък. Той пише `next_tool_name = finish`.

- системата подава trajectory с двете стъпки (`get_weather` + `finish`). Моделът трябва да даде крайния `[[ ## answer ## ]]`.

**Тестово съобщение:**

```text wrap
System message:

Your input fields are:
1. `question` (str):
Your output fields are:
1. `answer` (str):
All interactions will be structured in the following way, with the appropriate values filled in.
[[ ## question ## ]]
{question}
[[ ## answer ## ]]
{answer}
[[ ## completed ## ]]
In adhering to this structure, your objective is:
 Given the fields `question`, produce the fields `answer`.

User message:

[[ ## question ## ]]
Who are you
Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.

Response:
[[ ## answer ## ]]
I am Gemma, an open-weights AI assistant. I am a large language model trained by Google DeepMind.
[[ ## completed ## ]]
```

**Основно съобщение:**\
тук на модела се обясняват Tools, които може да използва – **get_weather** и **finish**.\
От модела се очакват `next_thought`, `next_tool_name`, `next_tool_args`\
Моделът отговаря с извикване на **\[\[ ## next_tool_name ## \]\] get_weather** с параметри **\[\[ ## next_tool_args ## \]\] {"city": "London"}**

```text wrap
System message:

Your input fields are:
1. `question` (str):
2. `trajectory` (str):
Your output fields are:
1. `next_thought` (str):
2. `next_tool_name` (Literal['get_weather', 'finish']):
3. `next_tool_args` (dict[str, Any]):
All interactions will be structured in the following way, with the appropriate values filled in.
[[ ## question ## ]]
{question}
[[ ## trajectory ## ]]
{trajectory}
[[ ## next_thought ## ]]
{next_thought}
[[ ## next_tool_name ## ]]
{next_tool_name}        # note: the value you produce must exactly match (no extra characters) one of: get_weather; finish
[[ ## next_tool_args ## ]]
{next_tool_args}        # note: the value you produce must adhere to the JSON schema: {"type": "object", "additionalProperties": true}
[[ ## completed ## ]]
In adhering to this structure, your objective is:
        Given the fields `question`, produce the fields `answer`.

        You are an Agent. In each episode, you will be given the fields `question` as input. And you can see your past trajectory so far.
        Your goal is to use one or more of the supplied tools to collect any necessary information for producing `answer`.

        To do this, you will interleave next_thought, next_tool_name, and next_tool_args in each turn, and also when finishing the task.
        After each tool call, you receive a resulting observation, which gets appended to your trajectory.

        When writing next_thought, you may reason about the current situation and plan for future steps.
        When selecting the next_tool_name and its next_tool_args, the tool must be one of:

        (1) get_weather. It takes arguments {'city': {'type': 'string'}, 'units': {'type': 'string', 'default': 'metric'}}.
        (2) finish, whose description is <desc>Marks the task as complete. That is, signals that all information for producing the outputs, i.e. `answer`, are now available to be extracted.</desc>. It takes arguments {}.
        When providing `next_tool_args`, the value inside the field must be in JSON format

User message:

[[ ## question ## ]]
Check weather in London
[[ ## trajectory ## ]]
Respond with the corresponding output fields, starting with the field `[[ ## next_thought ## ]]`, then `[[ ## next_tool_name ## ]]` (must be formatted as a valid Python Literal['get_weather', 'finish']), then `[[ ## next_tool_args ## ]]` (must be formatted as a valid Python dict[str, Any]), and then ending with the marker for `[[ ## completed ## ]]`.

Response:

[[ ## next_thought ## ]]
I need to check the weather in London. I should use the get_weather tool for this.
[[ ## next_tool_name ## ]]
get_weather
[[ ## next_tool_args ## ]]
{"city": "London"}
[[ ## completed ## ]]
```

След това програмата прави извикване към API-то за времето, получава резултата и го добавя в следващото съобщение.\
Моделът решава, че по-нататък трябва да се извика **\[\[ ## next_tool_name ## \]\] finish**\
На този етап моделът все още е агент и е решил кой тул да извика по-нататък.

```text wrap
User message:

[[ ## question ## ]]
Check weather in London
[[ ## trajectory ## ]]
[[ ## thought_0 ## ]]
I need to check the weather in London. I should use the get_weather tool for this.
[[ ## tool_name_0 ## ]]
get_weather
[[ ## tool_args_0 ## ]]
{"city": "London"}

[[ ## observation_0 ## ]]
Weather in London: 23.75°C, overcast clouds
Respond with the corresponding output fields, starting with the field `[[ ## next_thought ## ]]`, then `[[ ## next_tool_name ## ]]` (must be formatted as a valid Python Literal['get_weather', 'finish']), then `[[ ## next_tool_args ## ]]` (must be formatted as a valid Python dict[str, Any]), and then ending with the marker for `[[ ## completed ## ]]`.

Response:

[[ ## next_thought ## ]]
The weather for London has been retrieved. I can now finish the task.
[[ ## next_tool_name ## ]]
finish
[[ ## next_tool_args ## ]]
{}
[[ ## completed ## ]]
```

След това – последното извикване: моделът вече е асистент и дава reasoning + краен отговор.\
Промптът също се е сменил.

```text wrap
System message:

Your input fields are:
1. `question` (str):
2. `trajectory` (str):
Your output fields are:
1. `reasoning` (str):
2. `answer` (str):
All interactions will be structured in the following way, with the appropriate values filled in.
[[ ## question ## ]]
{question}
[[ ## trajectory ## ]]
{trajectory}
[[ ## reasoning ## ]]
{reasoning}
[[ ## answer ## ]]
{answer}
[[ ## completed ## ]]
In adhering to this structure, your objective is:
Given the fields `question`, produce the fields `answer`.

User message:

[[ ## question ## ]]
Check weather in London
[[ ## trajectory ## ]]
[[ ## thought_0 ## ]]
I need to check the weather in London. I should use the get_weather tool for this.
[[ ## tool_name_0 ## ]]
get_weather
[[ ## tool_args_0 ## ]]
{"city": "London"}
[[ ## observation_0 ## ]]
Weather in London: 23.75°C, overcast clouds
[[ ## thought_1 ## ]]
The weather for London has been retrieved. I can now finish the task.
[[ ## tool_name_1 ## ]]
finish
[[ ## tool_args_1 ## ]]
{}
[[ ## observation_1 ## ]]
Completed.
Respond with the corresponding output fields, starting with the field `[[ ## reasoning ## ]]`, then `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.

Response:

[[ ## reasoning ## ]]
The question asks to check the weather in London. I need to use a tool that can provide weather information. The `get_weather` tool is suitable for this task, and I should provide the city as "London". After retrieving the weather information, I will finish the task.
[[ ## answer ## ]]
Weather in London: 23.75°C, overcast clouds
[[ ## completed ## ]]
```

**Защо извикванията с finish са две:**\
Причината е в архитектурата на агентния цикъл в DSPy.

**Извикване с next_tool_name:**\
Тази стъпка се моделира като „действие на агента“. Агентът винаги работи във формат:\
мисля → избирам тул → извиквам тул → получавам observation.\
Дори ако тул = finish, формално това пак се счита за действие.

**Извикване с reasoning + answer:**\
След като агентът „е завършил задачата“, системата прави отделна заявка, в която контекстът включва цялата trajectory.

\
Тази двустъпкова схема е начин да се разделят ролите:\
един формат на общуване, когато моделът е агент, управляващ тулове;\
друг формат, когато моделът е асистент, отговарящ на потребителя.

По-нататък в LangChain ще има пример с едно крайно съобщение.

## Пример 3 – Python фреймуърк LangChain

LangChain е фреймуърк за работа с LLM (Large Language Models), който помага да се изграждат сложни приложения върху моделите, а не просто „въпрос → отговор“.

**Задачата му е да даде удобен слой върху модела, за да може той да:**\
работи с инструменти (например API, бази данни, калкулатор);\
запазва контекст и памет (чатове, история на диалога);\
използва вериги (chains) – последователност от стъпки, при която моделът изпълнява една задача и предава резултата на следващата;\
стартира агенти (agents), които сами решават какви инструменти да използват и в какъв ред;\
се интегрира с популярни LLM доставчици (OpenAI, Anthropic, LM Studio, HuggingFace и др.);\
работи с retrieval и RAG (да извлича знания от документи или векторни бази).

[https://www.langchain.com](https://www.langchain.com)

[https://github.com/langchain-ai/langchain](https://github.com/langchain-ai/langchain)

[https://en.wikipedia.org/wiki/LangChain](https://en.wikipedia.org/wiki/LangChain)

**Пример, аналогичен по функции на предишния**:

requirements.txt

```text
langchain-core==0.3.74
langchain-openai==0.3.30
langchain==0.3.27
openai==1.100.1
requests==2.32.5
```

```python
import requests
from typing import Dict
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.callbacks.base import BaseCallbackHandler

OPENWEATHER_API_KEY = "..."

# Callback за логване
class CustomLogger(BaseCallbackHandler):
    def on_tool_start(self, serialized, input_str, **kwargs):
        print(f"\nTOOL START:\n{serialized.get('name')}\nINPUT:\n{input_str}\n")

    def on_tool_end(self, output, **kwargs):
        print(f"\nTOOL END:\n{output}\n")

    def on_llm_start(self, serialized, prompts, **kwargs):
        print(f"\nLLM START:\n{serialized.get('name')}\nPROMPTS\n{prompts}\n")

    def on_llm_end(self, response, **kwargs):
        print(f"\nLLM END:\n{response}\n")

# Определяме инструмент (tool), който агентът може да извиква
@tool(description="Get current weather for a city.")
def get_weather(city: str) -> str:
    # Формираме заявка към OpenWeather API
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {"q": city, "appid": OPENWEATHER_API_KEY, "units": "metric", "lang": "en"}
    r = requests.get(url, params=params, timeout=15)
    if not r.ok:
        # Ако има грешка – връщаме текста на грешката
        return f"error: {r.status_code} {r.text}"
    # Парсваме JSON отговора
    data: Dict = r.json()
    # Връщаме кратък низ с времето
    return f"Weather in {data.get('name', city)}: {data['main']['temp']}°C, {data['weather'][0]['description']}"

# Свързваме LLM през LM Studio (локален сървър, съвместим с OpenAI API)
llm = ChatOpenAI(
    model="google/gemma-3n-e4b",
    openai_api_base="http://localhost:1234/v1",
    openai_api_key="lm-studio",
    temperature=0,
)

# Зареждаме готов шаблон на промпт ReAct от LangChain Hub
prompt = hub.pull("hwchase17/react")

# Създаваме агент в стил ReAct (моделът мисли + извиква инструменти)
agent = create_react_agent(
  llm,
  tools=[get_weather],
  prompt=prompt
)

# Обгръщаме агента в Executor, за да стартираме и управляваме работата му
executor = AgentExecutor(
    agent=agent,
    tools=[get_weather],
    verbose=True
)

# Стартираме агента с въпрос за времето в Лондон
result = executor.invoke(
    {"input": "Check weather in London"},
    config={"callbacks": [CustomLogger()]}
)
```

**Изход в конзолата:**

```text
> Entering new AgentExecutor chain...
I need to find the weather in London. I will use the get_weather tool to do this.
Action: get_weather
Action Input: LondonWeather in London: 16.16°C, overcast cloudsI have retrieved the weather for London. Now I can provide the answer.
Final Answer: Weather in London: 16.16°C, overcast clouds

> Finished chain.
Weather in London: 16.16°C, overcast clouds
```

**Пълен лог:**

```text wrap
ON_LLM_START:
ChatOpenAI

PROMPTS:
['Human: Answer the following questions as best you can. You have access to the following tools:
get_weather(city: str) -> str - Get current weather for a city.
Use the following format:
	Question: the input question you must answer
	Thought: you should always think about what to do
	Action: the action to take, should be one of [get_weather]
	Action Input: the input to the action
	Observation: the result of the action
	... (this Thought/Action/Action Input/Observation can repeat N times)
	Thought: I now know the final answer
	Final Answer: the final answer to the original input question
Begin!
Question: Check weather in London
Thought:']


ON_LLM_END:
generations=[[
ChatGenerationChunk(text='I need to find the weather in London. I will use the get_weather tool to do this.
	Action: get_weather
	Action Input: London',
	generation_info={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'},
	message=AIMessageChunk(
		content='I need to find the weather in London. I will use the get_weather tool to do this.
		Action: get_weather
		Action Input: London',
		additional_kwargs={}, response_metadata={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'}, id='run--ce1137e0-edde-4be7-95df-8b26c7ddddce'))]]
	llm_output=None
	run=None type='LLMResult'

I need to find the weather in London. I will use the get_weather tool to do this.
Action: get_weather
Action Input: London
```

```text wrap
ON_TOOL_START:
get_weather

INPUT:
London

ON_TOOL_END:
Weather in London: 23.01°C, overcast clouds
```

```text wrap
ON_LLM_START:
ChatOpenAI

PROMPTS:
['Human: Answer the following questions as best you can. You have access to the following tools:
get_weather(city: str) -> str - Get current weather for a city.
Use the following format:
	Question: the input question you must answer
	Thought: you should always think about what to do
	Action: the action to take, should be one of [get_weather]
	Action Input: the input to the action
	Observation: the result of the action
	... (this Thought/Action/Action Input/Observation can repeat N times)
	Thought: I now know the final answer
	Final Answer: the final answer to the original input question
Begin!

Question: Check weather in London
Thought: I need to find the weather in London. I will use the get_weather tool to do this.
Action: get_weather
Action Input: London
Observation: Weather in London: 23.01°C, overcast clouds
Thought: ']

ON_LLM_END:
generations=[[
ChatGenerationChunk(text='I have retrieved the weather for London. Now I can provide the answer.
Final Answer: Weather in London: 23.01°C, overcast clouds', generation_info={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'},
message=AIMessageChunk(
	content='I have retrieved the weather for London. Now I can provide the answer.
	Final Answer: Weather in London: 23.01°C, overcast clouds',
	additional_kwargs={},
	response_metadata={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'}, id='run--342aee15-0171-4549-94c4-a02a620aad7f'))]]
llm_output=None
run=None type='LLMResult'

I have retrieved the weather for London. Now I can provide the answer.
Final Answer: Weather in London: 23.01°C, overcast clouds

> Finished chain.

Process finished with exit code 0
```

## Пример 4 – Java фреймуърк LangChain4j

Тъй като LLM си пробива път от стартъпите към ентърпрайза, появата на решения на Java е само въпрос на време.

Сред популярните решения са:

**Spring AI**\
[https://spring.io/projects/spring-ai](https://spring.io/projects/spring-ai)\
[https://github.com/spring-projects/spring-ai](https://github.com/spring-projects/spring-ai)

**Microsoft Semantic Kernel**\
[https://learn.microsoft.com/en-us/semantic-kernel/overview](https://learn.microsoft.com/en-us/semantic-kernel/overview)\
[https://github.com/microsoft/semantic-kernel](https://github.com/microsoft/semantic-kernel)

**LangChain for Java**\
[https://docs.langchain4j.dev](https://docs.langchain4j.dev)\
[https://github.com/langchain4j/langchain4j](https://github.com/langchain4j/langchain4j)

**Ето пример с LangChain4j, аналогичен по функции на предишните примери:**

```groovy
ext {
    ktor_version   = '3.2.3'
    lc_4_j_version   = '1.3.0'
    logback_version = '1.5.13'
    kotlinx_serialization_json_version = "1.8.1"
}

dependencies {
    implementation "io.ktor:ktor-client-core:$ktor_version"
    implementation "io.ktor:ktor-client-cio:$ktor_version"
    implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
    implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
    implementation "io.ktor:ktor-client-logging:$ktor_version"
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_json_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    implementation "dev.langchain4j:langchain4j:$lc_4_j_version"
    implementation "dev.langchain4j:langchain4j-open-ai:$lc_4_j_version"
    implementation("dev.langchain4j:langchain4j-http-client-jdk:$lc_4_j_version")
}
```

```kotlin
import dev.langchain4j.http.client.jdk.JdkHttpClientBuilder
import dev.langchain4j.model.openai.OpenAiChatModel
import dev.langchain4j.service.AiServices
import kotlinx.coroutines.runBlocking
import java.net.http.HttpClient
import java.time.Duration

fun main() = runBlocking {

    val httpClientBuilder = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .version(HttpClient.Version.HTTP_1_1)

    val jdkClientBuilder = JdkHttpClientBuilder()
        .httpClientBuilder(httpClientBuilder)
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(90))

    val model = OpenAiChatModel.builder()
        .httpClientBuilder(jdkClientBuilder)
        .baseUrl(BASE_URL)
        .apiKey("lm-studio")
        .modelName(MODEL_ID)
        .temperature(0.2)
        .logRequests(true)
        .logResponses(true)
        .timeout(Duration.ofSeconds(90))
        .listeners(listOf(ChatLogger()))
        .build()

    val assistant = AiServices.builder(Assistant::class.java)
        .chatModel(model)
        .tools(WeatherTools)
        .build()

    val result = assistant.chat("Check weather in London")
    println(result)

}
```

```kotlin
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V

interface Assistant {

    @UserMessage("{{input}}")
    fun chat(@V("input") message: String): String

}
```

```kotlin
import dev.langchain4j.agent.tool.Tool
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking

object WeatherTools {

    @Tool(
        name = "current_weather_city",
        value = ["Get current weather for a city"]
    )
    @JvmStatic
    fun currentWeatherByCity(city: String): String = runBlocking {
        client.use { httpClient ->
            val url = URLBuilder(OWM_BASE).apply {
                appendPathSegments("weather")
                parameters.append("q", city)
                parameters.append("appid", WEATHER_API_KEY)
                parameters.append("units", "metric")
                parameters.append("lang", "en")
            }.buildString()

            val response: WeatherResponse = httpClient.get(url).body()
            val name = response.name ?: city
            val temp = response.main?.temp
            val desc = response.weather.firstOrNull()?.description ?: "n/a"
            if (temp == null) {
                "Can't read temperature for $name"
            } else {
                "Weather in $name: $temp °C, $desc"
            }
        }
    }
}
```

```kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

const val BASE_URL: String = "http://localhost:1234/v1"

const val OWM_BASE = "https://api.openweathermap.org/data/2.5"

const val WEATHER_API_KEY = "..."

const val MODEL_ID = "google/gemma-3n-e4b"

val kotlinxJsonConfig: Json = Json {
    ignoreUnknownKeys = true
    prettyPrint = false
}

val client: HttpClient = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(kotlinxJsonConfig)
    }
    install(HttpTimeout) {
        requestTimeoutMillis = 60_000
        connectTimeoutMillis = 10_000
        socketTimeoutMillis = 60_000
    }
}
```

```kotlin
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class WeatherResponse(
    @SerialName("coord") val coord: Coord? = null,
    @SerialName("weather") val weather: List<WeatherItem> = emptyList(),
    @SerialName("base") val base: String? = null,
    @SerialName("main") val main: MainBlock? = null,
    @SerialName("visibility") val visibility: Int? = null,
    @SerialName("wind") val wind: Wind? = null,
    @SerialName("clouds") val clouds: Clouds? = null,
    @SerialName("dt") val dt: Long? = null,
    @SerialName("sys") val sys: Sys? = null,
    @SerialName("timezone") val timezone: Int? = null,
    @SerialName("id") val id: Long? = null,
    @SerialName("name") val name: String? = null,
    @SerialName("cod") val cod: Int? = null
)

@Serializable
data class Coord(
    @SerialName("lon") val lon: Double? = null,
    @SerialName("lat") val lat: Double? = null
)

@Serializable
data class WeatherItem(
    @SerialName("id") val id: Int? = null,
    @SerialName("main") val main: String? = null,
    @SerialName("description") val description: String? = null,
    @SerialName("icon") val icon: String? = null
)

@Serializable
data class MainBlock(
    @SerialName("temp") val temp: Double? = null,
    @SerialName("feels_like") val feelsLike: Double? = null,
    @SerialName("temp_min") val tempMin: Double? = null,
    @SerialName("temp_max") val tempMax: Double? = null,
    @SerialName("pressure") val pressure: Int? = null,
    @SerialName("humidity") val humidity: Int? = null
)

@Serializable
data class Wind(
    @SerialName("speed") val speed: Double? = null,
    @SerialName("deg") val deg: Int? = null,
    @SerialName("gust") val gust: Double? = null
)

@Serializable
data class Clouds(
    @SerialName("all") val all: Int? = null
)

@Serializable
data class Sys(
    @SerialName("type") val type: Int? = null,
    @SerialName("id") val id: Int? = null,
    @SerialName("country") val country: String? = null,
    @SerialName("sunrise") val sunrise: Long? = null,
    @SerialName("sunset") val sunset: Long? = null
)

```

```kotlin
import dev.langchain4j.model.chat.listener.ChatModelErrorContext
import dev.langchain4j.model.chat.listener.ChatModelListener
import dev.langchain4j.model.chat.listener.ChatModelRequestContext
import dev.langchain4j.model.chat.listener.ChatModelResponseContext

class ChatLogger : ChatModelListener {

    override fun onRequest(request: ChatModelRequestContext) {
        println("ON_LLM_START:")
        println(request)
    }

    override fun onResponse(response: ChatModelResponseContext) {
        println("ON_LLM_END:")
        println(response)
    }

    override fun onError(context: ChatModelErrorContext) {
        println("ON_LLM_ERROR:")
        println(context.error().message)
    }

}
```

**Изход в конзолата:**

```text
C:\Users\Roman\.jdks\corretto-21.0.6\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJIdea2025.1\lib\idea_rt.jar=52675" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\Roman\IdeaProjects\LangChain4j_example\build\classes\kotlin\main;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-stdlib\2.2.0\fdfc65fbc42fda253a26f61dac3c0aca335fae96\kotlin-stdlib-2.2.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-classic\1.5.13\e6f82a9ce14a912c7dab831164ec6f10cc3eeb1b\logback-classic-1.5.13.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j\1.3.0\207a49cc2a25b14e61c219057023c97ea6344836\langchain4j-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-open-ai\1.3.0\abd013b6600fefb6db2f2d08cad4970a601bf71a\langchain4j-open-ai-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-http-client-jdk\1.3.0\ade5c4f73164b4b16f191312b80af2d847a26fd6\langchain4j-http-client-jdk-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-cio-jvm\3.2.3\7fb001474733cba10b0e234be7e25c984bac1360\ktor-client-cio-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-content-negotiation-jvm\3.2.3\9ca43332547aaf2e7af0ef9e75b0e55a7fd5d7c9\ktor-client-content-negotiation-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-logging-jvm\3.2.3\7731df6cd3b0da0be75d0d91e66c5c5eab0efb08\ktor-client-logging-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-core-jvm\3.2.3\58361d3e3cb2f4e77c4eedc294b2889fdf82b2ac\ktor-client-core-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-serialization-kotlinx-json-jvm\3.2.3\211555a0b1cad21ccaf31ef4b482374c146e6318\ktor-serialization-kotlinx-json-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-serialization-json-jvm\1.8.1\4de3bace4b175753df5484d2acd74c14bfeb5be9\kotlinx-serialization-json-jvm-1.8.1.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains\annotations\23.0.0\8cc20c07506ec18e0834947b84a864bfc094484e\annotations-23.0.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-core\1.5.13\ccd418c6f1a5a93e57e7b5bb9a3f708dbd563cd8\logback-core-1.5.13.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.slf4j\slf4j-api\2.0.17\d9e58ac9c7779ba3bf8142aff6c830617a7fe60f\slf4j-api-2.0.17.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-core\1.3.0\18fba60f0f2b76b18e2ba362b8ef8076d675e9af\langchain4j-core-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.apache.opennlp\opennlp-tools\2.5.4\11a7671f2169280c0e24b979cb326833b96ca246\opennlp-tools-2.5.4.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-core\2.19.2\50f3b4bd59b9ff51a0ed493e7b5abaf5c39709bf\jackson-core-2.19.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-databind\2.19.2\46509399d28f57ca32c6bb4b0d4e10e8f062051e\jackson-databind-2.19.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-annotations\2.19.2\c5381f11988ae3d424b197a26087d86067b6d7d\jackson-annotations-2.19.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-http-client\1.3.0\c0d03a935c32c5252561ce0004af633477bf0e8a\langchain4j-http-client-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.knuddels\jtokkit\1.1.0\b7370f801db3eb8c7c6a2c2c06231909ac6de0b0\jtokkit-1.1.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-coroutines-slf4j\1.10.2\1271f9d4a929150bb87ab8d1dc1e86d4bcf039f3\kotlinx-coroutines-slf4j-1.10.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jspecify\jspecify\1.0.0\7425a601c1c7ec76645a78d22b8c6a627edee507\jspecify-1.0.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-http-cio-jvm\3.2.3\9ba86bd196344eb2f33f4de7c01b2a76079563d8\ktor-http-cio-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-websockets-jvm\3.2.3\180024bd52774540635a9fdaca021f9392400e60\ktor-websockets-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-network-tls-jvm\3.2.3\d1f24a1e38fd0f940ee8b55c8e11be151491a3de\ktor-network-tls-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-coroutines-core-jvm\1.10.2\4a9f78ef49483748e2c129f3d124b8fa249dafbf\kotlinx-coroutines-core-jvm-1.10.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-serialization-jvm\3.2.3\f3ee77994a1ba0ddc0d65bbb9bd8db2900f40009\ktor-serialization-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-websocket-serialization-jvm\3.2.3\e62e9fdebc262c2b4c556f208628a03740f14046\ktor-websocket-serialization-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-http-jvm\3.2.3\c83599639fc1fc9312e63ed86423ee535d80bc5\ktor-http-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-events-jvm\3.2.3\38a0c405c218e64b0c967ad300ca587cde8154bc\ktor-events-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-sse-jvm\3.2.3\67421e4181c68c6b6d82cb6f8af9c8e44e2838c8\ktor-sse-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-serialization-json-io-jvm\1.8.1\de4e31bfc7ddf8585418f8d4fca649feca574cdf\kotlinx-serialization-json-io-jvm-1.8.1.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-serialization-kotlinx-jvm\3.2.3\316c3d4122d2dcfecd2ae2146060d557a2a4b4\ktor-serialization-kotlinx-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-serialization-core-jvm\1.8.1\510cb839cce9a3e708052d480a6fbf4a7274dfcd\kotlinx-serialization-core-jvm-1.8.1.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-network-jvm\3.2.3\f3eb3f831652f4b5407703f58e4885cd1ae93e92\ktor-network-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-io-jvm\3.2.3\81a70f5d028cb2878fba2107df2101edc7c0240f\ktor-io-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-utils-jvm\3.2.3\5cc6a940a5ca98f7f0c36e0b699c6fbd2f594598\ktor-utils-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-io-core-jvm\0.7.0\1d9595ed390f6304ce90a049e6b2f1fab3b408c4\kotlinx-io-core-jvm-0.7.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-io-bytestring-jvm\0.7.0\ffe5bd231da40d21870250703326113277dbf9c3\kotlinx-io-bytestring-jvm-0.7.0.jar MainKt
ON_LLM_START:
dev.langchain4j.model.chat.listener.ChatModelRequestContext@33d512c1
00:48:03.351 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: http://localhost:1234/v1/chat/completions
- headers: [Authorization: Beare...io], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "google/gemma-3n-e4b",
  "messages" : [ {
    "role" : "user",
    "content" : "Check weather in London"
  } ],
  "temperature" : 0.2,
  "stream" : false,
  "tools" : [ {
    "type" : "function",
    "function" : {
      "name" : "current_weather_city",
      "description" : "Get current weather for a city",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "arg0" : {
            "type" : "string"
          }
        },
        "required" : [ "arg0" ]
      }
    }
  } ]
}

00:48:04.961 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:
- status code: 200
- headers: [connection: keep-alive], [content-length: 741], [content-type: application/json; charset=utf-8], [date: Tue, 19 Aug 2025 21:48:04 GMT], [etag: W/"2e5-JWXcPUg7aNE2xJTlIOO4yntOymw"], [keep-alive: timeout=5], [x-powered-by: Express]
- body: {
  "id": "chatcmpl-ox8omdn9nxl0o4pbihqqi",
  "object": "chat.completion",
  "created": 1755640083,
  "model": "google/gemma-3n-e4b",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "type": "function",
            "id": "458045748",
            "function": {
              "name": "current_weather_city",
              "arguments": "{\"arg0\":\"London\"}"
            }
          }
        ]
      },
      "logprobs": null,
      "finish_reason": "tool_calls"
    }
  ],
  "usage": {
    "prompt_tokens": 399,
    "completion_tokens": 34,
    "total_tokens": 433
  },
  "stats": {},
  "system_fingerprint": "google/gemma-3n-e4b"
}

ON_LLM_END:
dev.langchain4j.model.chat.listener.ChatModelResponseContext@1c481ff2
ON_LLM_START:
dev.langchain4j.model.chat.listener.ChatModelRequestContext@70d2e40b
00:48:05.501 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: http://localhost:1234/v1/chat/completions
- headers: [Authorization: Beare...io], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "google/gemma-3n-e4b",
  "messages" : [ {
    "role" : "user",
    "content" : "Check weather in London"
  }, {
    "role" : "assistant",
    "tool_calls" : [ {
      "id" : "458045748",
      "type" : "function",
      "function" : {
        "name" : "current_weather_city",
        "arguments" : "{\"arg0\":\"London\"}"
      }
    } ]
  }, {
    "role" : "tool",
    "tool_call_id" : "458045748",
    "content" : "Weather in London: 17.76 °C, few clouds"
  } ],
  "temperature" : 0.2,
  "stream" : false,
  "tools" : [ {
    "type" : "function",
    "function" : {
      "name" : "current_weather_city",
      "description" : "Get current weather for a city",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "arg0" : {
            "type" : "string"
          }
        },
        "required" : [ "arg0" ]
      }
    }
  } ]
}

00:48:06.257 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:
- status code: 200
- headers: [connection: keep-alive], [content-length: 554], [content-type: application/json; charset=utf-8], [date: Tue, 19 Aug 2025 21:48:06 GMT], [etag: W/"22a-YKbJMzdQdr5Qv/9Z3J+Mf2kPeAw"], [keep-alive: timeout=5], [x-powered-by: Express]
- body: {
  "id": "chatcmpl-6aslomnvzvh3tu6jo8tfl6",
  "object": "chat.completion",
  "created": 1755640085,
  "model": "google/gemma-3n-e4b",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The weather in London is 17.76 °C with few clouds.",
        "tool_calls": []
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 469,
    "completion_tokens": 18,
    "total_tokens": 487
  },
  "stats": {},
  "system_fingerprint": "google/gemma-3n-e4b"
}

ON_LLM_END:
dev.langchain4j.model.chat.listener.ChatModelResponseContext@2449cff7
The weather in London is 17.76 °C with few clouds.

Process finished with exit code 0
```

## Пример 5 – Java библиотеки от Google за мобилни устройства

### В този пример ще използвам

Двигател: [Google AI Edge MediaPipe](https://github.com/google-ai-edge/mediapipe)

LLM модел: [gemma-3n-E4B-it-int4](https://huggingface.co/google/gemma-3n-E4B-it-litert-preview)

RAG / Functional Calling: [On-Device RAG SDK & On-Device Function Calling SDK](https://github.com/google-ai-edge/ai-edge-apis)

Embedding модел: [Gecko-110m-en](https://huggingface.co/litert-community/Gecko-110m-en/blob/main/Gecko_256_quant.tflite)

### Как трябва да работи всичко това – може да се прочете тук

RAG:

[https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation)

[https://en.wikipedia.org/wiki/Retrieval-augmented_generation](https://en.wikipedia.org/wiki/Retrieval-augmented_generation)

[https://www.ibm.com/think/topics/retrieval-augmented-generation](https://www.ibm.com/think/topics/retrieval-augmented-generation)

Function Calling:

[https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling)

[https://huggingface.co/docs/hugs/guides/function-calling](https://huggingface.co/docs/hugs/guides/function-calling)

[https://platform.openai.com/docs/guides/function-calling](https://platform.openai.com/docs/guides/function-calling)

[https://medium.com/@danushidk507/function-calling-in-llm-e537b286a4fd](https://medium.com/@danushidk507/function-calling-in-llm-e537b286a4fd)

### В този пример ще има 3 класа

**MediaPipeEngineCommon:** тук ще се съхраняват общите компоненти за работа с векторната база

**MediaPipeEngineWithRag:** тук може да се стартира генерация, като към заявката се добавят данни от векторната база

**MediaPipeEngineWithTools:** тук може да се стартира генерация и моделът сам решава дали да направи заявка към векторната база

Ако реши, че е нужно, прави функционално извикване, което се обработва не ръчно, както в предишния пример, а от библиотеката [com.google.ai.edge.localagents:localagents-fc](com.google.ai.edge.localagents:localagents-fc)

**Зависимости в проекта, в новия Gradle формат:**

```toml
localagentsRag = "0.2.0"
localagentsFc = "0.1.0"
tasksGenai = "0.10.25"
tasksText = "0.10.26.1"
tasksVision = "0.10.26.1"
tensorflowLite = "2.17.0"
kotlinxCoroutinesGuava = "1.10.2"

tasks-genai = { module = "com.google.mediapipe:tasks-genai", version.ref = "tasksGenai" }
tasks-text = { module = "com.google.mediapipe:tasks-text", version.ref = "tasksText" }
tasks-vision = { module = "com.google.mediapipe:tasks-vision", version.ref = "tasksVision" }
tensorflow-lite = { module = "org.tensorflow:tensorflow-lite", version.ref = "tensorflowLite" }
localagents-rag = { module = "com.google.ai.edge.localagents:localagents-rag", version.ref = "localagentsRag" }
localagents-fc = { module = "com.google.ai.edge.localagents:localagents-fc", version.ref = "localagentsFc" }
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
```

**MediaPipeEngineCommon:** Клас за работа с векторната база данни, необходим в този пример и за RAG, и за Function Calling

```kotlin
import com.google.ai.edge.localagents.rag.chunking.TextChunker
import com.google.ai.edge.localagents.rag.memory.DefaultSemanticTextMemory
import com.google.ai.edge.localagents.rag.memory.SqliteVectorStore
import com.google.ai.edge.localagents.rag.models.Embedder
import com.google.ai.edge.localagents.rag.prompt.PromptBuilder

interface MediaPipeEngineCommon {

    var chunker: TextChunker
    var embedder: Embedder<String>
    var vectorStore: SqliteVectorStore
    var promptBuilder: PromptBuilder
    var semanticMemory: DefaultSemanticTextMemory

    fun init(
        geckoModelPath: String, // Gecko_256_quant.tflite
        tokenizerModelPath: String, // sentencepiece.model
        useGpuForEmbeddings: Boolean = true,
    )

    fun saveTextToVectorStore(
        text: String,
        chunkOverlap: Int = 20,
        chunkTokenSize: Int = 128,
        chunkMaxSymbolsSize: Int = 1000,
        chunkBySentences: Boolean = false,
    ): String?

    fun readEmbeddingVectors(): List<VectorStoreEntity>

    suspend fun readEmbeddingVectors(
        query: String,
        topK: Int,
        minSimilarityScore: Float,
    ): List<VectorStoreEntity>

    fun makeSQLRequest(query: String): Boolean

}
```

```kotlin
import android.app.Application
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import com.google.ai.edge.localagents.rag.chunking.TextChunker
import com.google.ai.edge.localagents.rag.memory.DefaultSemanticTextMemory
import com.google.ai.edge.localagents.rag.memory.SqliteVectorStore
import com.google.ai.edge.localagents.rag.memory.VectorStoreRecord
import com.google.ai.edge.localagents.rag.models.EmbedData
import com.google.ai.edge.localagents.rag.models.Embedder
import com.google.ai.edge.localagents.rag.models.EmbeddingRequest
import com.google.ai.edge.localagents.rag.models.GeckoEmbeddingModel
import com.google.ai.edge.localagents.rag.prompt.PromptBuilder
import com.google.common.collect.ImmutableList
import com.romankryvolapov.offlineailauncher.common.extensions.toDurationString
import com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logDebug
import com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logError
import kotlinx.coroutines.guava.await
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.Optional

class MediaPipeEngineCommonImpl(
    private val application: Application
) : MediaPipeEngineCommon {

    companion object {
        private const val TAG = "CommonComponentsTag"
        private const val GECKO_EMBEDDING_MODEL_DIMENSION = 768
        private const val PROMPT_TEMPLATE: String =
            "You are an assistant for question-answering tasks. Here are the things I want to remember: {0} Use the things I want to remember, answer the following question the user has: {1}"

    }

    override lateinit var chunker: TextChunker
    override lateinit var embedder: Embedder<String>
    override lateinit var vectorStore: SqliteVectorStore
    override lateinit var promptBuilder: PromptBuilder
    override lateinit var semanticMemory: DefaultSemanticTextMemory

    override fun init(
        geckoModelPath: String,
        tokenizerModelPath: String,
        useGpuForEmbeddings: Boolean,
    ) {
        logDebug("init", TAG)
        chunker = TextChunker()

        // към embedder добавяме пътя до модела Gecko-110m-en
        // използвам версията Gecko_256_quant.tflite, тук 256 е максималният размер на текстовия вход
        // тази версия е оптимална от гледна точка на размера на частите текст и на бързодействието
        // важно – по-нататък в кода подаваме на какви части да се разбива текстът, това зависи от параметрите на модела
        embedder = GeckoEmbeddingModel(
            geckoModelPath,
            Optional.of(tokenizerModelPath),
            useGpuForEmbeddings,
        )

        // тук ще използвам вече готова SQLite база в приложението
        // в нея просто ще се добави още една таблица rag_vector_store
        // с колони text за текста и embeddings за вектора
        val database = File(application.getDatabasePath("database").absolutePath)
        if (!database.exists()) {
            logError("startEngine database not exists", TAG)
        }

        // Може да се направи и собствена реализация на базата данни, която наследява
        // интерфейса VectorStore<String>, но методът getNearestRecords трябва да бъде
        // реализиран правилно и да работи бързо, той търси най-близките вектори
        vectorStore = SqliteVectorStore(
            GECKO_EMBEDDING_MODEL_DIMENSION,
            database.absolutePath
        )
        semanticMemory = DefaultSemanticTextMemory(
            vectorStore,
            embedder
        )
        promptBuilder = PromptBuilder(
            PROMPT_TEMPLATE
        )
        logDebug("init ready", TAG)
    }

    override fun saveTextToVectorStore(
        text: String,

      	// колко назад в текста да се връщаме преди фрагмента, тук 20
        chunkOverlap: Int,

      	// Обърнете внимание, че размерът уж е в токени,
      	// но се използва за chunker и може да не съответства
      	// на размера в токени за embedder, тук chunkTokenSize е 128
        chunkTokenSize: Int,

      	// при разбиване с помощта на chunkBySentences
        // размерът на изреченията може да е голям
      	// ако надхвърли възможностите на embedder модела, той ще върне грешка
      	// за целта се използва отрязване до максималния размер
      	// тук той е 1000 символа
        chunkMaxSymbolsSize: Int,

      	// използване на метода за разбиване по изречения
        chunkBySentences: Boolean,
    ): String? {
        logDebug("saveTextToVectorStore text length: ${text.length}", TAG)
        // таймер, за да разберем колко бързо работи
        val start = System.currentTimeMillis()
        val chunks: List<String> = if (chunkBySentences)
            chunker.chunkBySentences(
                text,
                chunkTokenSize,
            ).filter {
                it.isNotBlank()
            }.map { chunk ->
                if (chunk.length > chunkMaxSymbolsSize) {
                    logError("saveTextToVectorStore crop chunk", TAG)
                    chunk.substring(0, chunkMaxSymbolsSize)
                } else {
                    chunk
                }
            }
        else
            chunker.chunk(
                text,
                chunkTokenSize,
                chunkOverlap
            ).filter {
                it.isNotBlank()
            }.map { chunk ->
                if (chunk.length > chunkMaxSymbolsSize) {
                    logError("saveTextToVectorStore crop chunk", TAG)
                    chunk.substring(0, chunkMaxSymbolsSize)
                } else {
                    chunk
                }
            }
        val end = System.currentTimeMillis()
        val delta = end - start
        logDebug("saveTextToVectorStore chunks delta: ${delta.toDurationString()} size: ${chunks.size}", TAG)
        chunks.forEach {
            logDebug("length: ${it.length}", TAG)
        }
        if (chunks.isEmpty()) {
            logError("saveTextToVectorStore chunks.isEmpty()", TAG)
            return "Chunks is empty"
        }
        return try {
          	// векторът се генерира вътре в semanticMemory
            val result: Boolean? = semanticMemory.recordBatchedMemoryItems(
                ImmutableList.copyOf(chunks)
            )?.get()
            val end = System.currentTimeMillis()
            val delta = end - start
            logDebug("saveTextToVectorStore ready delta: ${delta.toDurationString()} result: $result", TAG)
            null
        } catch (t: Throwable) {
            logError("saveTextToVectorStore failed: ${t.message}", t, TAG)
            t.message
        }
    }

    // търсене по заявката query, ще намери всички части от текста, подобни на заявката
    override suspend fun readEmbeddingVectors(
        query: String,

        // брой резултати от заявката към базата
        topK: Int,

        // доколко векторът на заявката query трябва да е подобен на запис в базата
        // 0.0 = търси във всички записи, сортирай по най-подобните
        // 1.0 = само идеално съвпадение
        // аз използвам стойности 0.6 - 0.8
        minSimilarityScore: Float,
    ): List<VectorStoreEntity> {
        logDebug("readEmbeddingVectors query: $query", TAG)
        val queryEmbedData: EmbedData<String> = EmbedData.create(
            query,
            EmbedData.TaskType.RETRIEVAL_QUERY
        )
        val embeddingRequest: EmbeddingRequest<String> = EmbeddingRequest
            .create(
                listOf(queryEmbedData)
            )
        val vector: ImmutableList<Float> = try {
            embedder.getEmbeddings(embeddingRequest).await()
        } catch (t: Throwable) {
            logError("readEmbeddingVectors: embedding failed: ${t.message}", t, TAG)
            return emptyList()
        }
        logDebug("searchDocsInternal vector size: ${vector.size}", TAG)
        if (vector.isEmpty()) {
            logError("readEmbeddingVectors vector.isEmpty()", TAG)
            return emptyList()
        }
        val hits: ImmutableList<VectorStoreRecord<String>> = try {
            vectorStore.getNearestRecords(
                vector,
                topK,
                minSimilarityScore
            )
        } catch (t: Throwable) {
            logError("readEmbeddingVectors: vector search failed: ${t.message}", t, TAG)
            return emptyList()
        }
        if (hits.isEmpty()) {
            logError("readEmbeddingVectors hits.isEmpty()", TAG)
            return emptyList()
        }
        val result = hits.map {
            VectorStoreEntity(
                id = null,
                text = it.data,
                embedding = it.embeddings
            )
        }
        logDebug("readEmbeddingVectors\nsize: ${result.size}\nresult: $result", TAG)
        return result
    }

    // просто извежда всички записи в базата
    override fun readEmbeddingVectors(): List<VectorStoreEntity> {
        logDebug("readEmbeddingPreview", TAG)
        var cursor: Cursor? = null
        var database: SQLiteDatabase? = null
        return try {
            val databaseFile = File(application.getDatabasePath("database").absolutePath)
            database = SQLiteDatabase.openDatabase(
                databaseFile.absolutePath,
                null,
                SQLiteDatabase.OPEN_READONLY
            )
            cursor = database.rawQuery("SELECT ROWID, text, embeddings FROM rag_vector_store", null)
            val result = mutableListOf<VectorStoreEntity>()
            while (cursor.moveToNext()) {
                val rowId = cursor.getLong(0)
                val text = cursor.getString(1)
                val blob = cursor.getBlob(2)
                val buffer = ByteBuffer.wrap(blob).order(ByteOrder.LITTLE_ENDIAN)
                val floats = mutableListOf<Float>()
                while (buffer.hasRemaining()) {
                    floats.add(buffer.float)
                }
                result.add(
                    VectorStoreEntity(
                        id = rowId,
                        text = text,
                        embedding = floats
                    )
                )
            }
            logDebug("readEmbeddingPreview\nsize: ${result.size}\nresult: $result", TAG)
            result
        } catch (t: Throwable) {
            logError("readEmbeddingPreview failed: ${t.message}", t, TAG)
            emptyList()
        } finally {
            cursor?.close()
            database?.close()
        }
    }

    // може да се напише собствена заявка и тя ще се изпълни,
    // например "DELETE FROM rag_vector_store"
    override fun makeSQLRequest(query: String): Boolean {
        logDebug("makeSQLRequest query: $query", TAG)
        var cursor: Cursor? = null
        var database: SQLiteDatabase? = null
        return try {
            val databaseFile = File(application.getDatabasePath("database").absolutePath)
            database = SQLiteDatabase.openDatabase(
                databaseFile.absolutePath,
                null,
                SQLiteDatabase.OPEN_READWRITE
            )
            cursor = database.rawQuery(query, null)
            val result = cursor.moveToFirst()
            logDebug("makeSQLRequest result: $result", TAG)
            result
        } catch (t: Throwable) {
            logError("makeSQLRequest failed: ${t.message}", t, TAG)
            false
        } finally {
            cursor?.close()
            database?.close()
        }
    }

}
```

**MediaPipeEngineWithRag:** Тук се поддържа само простият механизъм RAG

```kotlin
import kotlinx.coroutines.flow.Flow
import java.io.File

interface MediaPipeEngineWithRag {

    fun startEngine(
        modelFile: File,
        isSupportImages: Boolean = false,
        engineParams: MediaPipeEngineParams,
    )

    fun resetSession()

    fun generateResponse(
        prompt: String,
        topK: Int = 5,
        minSimilarityScore: Float = 0.6F,
    ): Flow<ResultEmittedData<String>>

}
```

```kotlin
import android.app.Application
import com.google.ai.edge.localagents.rag.chains.ChainConfig
import com.google.ai.edge.localagents.rag.chains.RetrievalAndInferenceChain
import com.google.ai.edge.localagents.rag.models.AsyncProgressListener
import com.google.ai.edge.localagents.rag.models.LanguageModelResponse
import com.google.ai.edge.localagents.rag.models.MediaPipeLlmBackend
import com.google.ai.edge.localagents.rag.retrieval.RetrievalConfig
import com.google.ai.edge.localagents.rag.retrieval.RetrievalConfig.TaskType
import com.google.ai.edge.localagents.rag.retrieval.RetrievalRequest
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.google.mediapipe.tasks.genai.llminference.GraphOptions
import com.google.mediapipe.tasks.genai.llminference.LlmInference
import com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptions
import com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.LlmInferenceSessionOptions
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import java.io.File
import java.util.concurrent.Executor

class MediaPipeEngineWithRagImpl(
    private val application: Application,
    private val common: MediaPipeEngineCommon,
) : MediaPipeEngineWithRag {

    companion object {
        private const val TAG = "MediaPipeEngineWithRagTag"
    }

    private var chainConfig: ChainConfig<String>? = null
    private var retrievalAndInferenceChain: RetrievalAndInferenceChain? = null
    private var engineMediaPipe: LlmInference? = null
    private var sessionOptions: LlmInferenceSessionOptions? = null
    private var mediaPipeLanguageModel: MediaPipeLlmBackend? = null
    private var interfaceOptions: LlmInferenceOptions? = null
    private val executor: Executor = MoreExecutors.directExecutor()

    private var future: ListenableFuture<LanguageModelResponse>? = null

    override fun startEngine(
        modelFile: File,
        isSupportImages: Boolean,
        engineParams: MediaPipeEngineParams,
    ) {
        logDebug("startEngine", TAG)
        interfaceOptions = createInterfaceOptions(
            modelFile = modelFile,
            engineParams = engineParams,
            isSupportImages = isSupportImages,
        )
        engineMediaPipe = LlmInference.createFromOptions(
            application,
            interfaceOptions
        )
        if (engineMediaPipe == null) {
            logError("startEngine llmInference == null", TAG)
            return
        }
        sessionOptions = createSessionOptions(
            engineParams = engineParams,
            isSupportImages = isSupportImages,
        )
        mediaPipeLanguageModel = MediaPipeLlmBackend(
            application.applicationContext,
            interfaceOptions,
            sessionOptions,
            executor
        )
        chainConfig = ChainConfig.create(
            mediaPipeLanguageModel,
            common.promptBuilder,
          	// добавяме базата, в която трябва да се проверяват заявките
            common.semanticMemory
        )

        // правим верига с проверка в базата
        retrievalAndInferenceChain = RetrievalAndInferenceChain(
            chainConfig
        )
        Futures.addCallback(
            mediaPipeLanguageModel!!.initialize(),
            object : FutureCallback<Boolean> {
                override fun onSuccess(result: Boolean) {
                    logDebug("mediaPipeLanguageModel initialize onSuccess", TAG)
                }

                override fun onFailure(t: Throwable) {
                    logError(
                        "mediaPipeLanguageModel initialize onFailure: ${t.message}",
                        t,
                        TAG,
                    )
                }
            },
            executor
        )
        logDebug("startEngine ready", TAG)
    }

    override fun resetSession() {
        logDebug("resetSession", TAG)
        try {
            retrievalAndInferenceChain = RetrievalAndInferenceChain(
                chainConfig
            )
            logDebug("Session reset completed", TAG)
        } catch (e: Exception) {
            logError("Failed to reset session: ${e.message}", e, TAG)
        }
        logDebug("resetSession ready", TAG)
    }

    override fun generateResponse(
        prompt: String,

     	// Брой резултати от заявката към базата
        topK: Int,

        // доколко векторът на заявката query трябва да е подобен на запис в базата
        // 0.0 = търси във всички записи, сортирай по най-подобните
        // 1.0 = само идеално съвпадение
        // аз използвам стойности 0.6 - 0.8
        minSimilarityScore: Float,
    ): Flow<ResultEmittedData<String>> = callbackFlow {
        logDebug("generateResponse prompt: $prompt", TAG)
        try {
            if (retrievalAndInferenceChain == null) {
                logError("generateResponse retrievalAndInferenceChain == null", TAG)
                trySend(
                    ResultEmittedData.error(
                        model = null,
                        error = null,
                        title = "MediaPipe engine error",
                        responseCode = null,
                        message = "retrievalAndInferenceChain == null",
                        errorType = ErrorType.ERROR_IN_LOGIC,
                    )
                )
                return@callbackFlow
            }
            val retrievalConfig = RetrievalConfig.create(
                topK,
                minSimilarityScore,
                TaskType.QUESTION_ANSWERING
            )

            // заявката вече включва веригата с проверка
            val retrievalRequest = RetrievalRequest.create(
                prompt,
                retrievalConfig
            )
            logDebug("generateResponse retrievalRequest", TAG)
            val messageBuilder = StringBuilder()
            val listener = AsyncProgressListener<LanguageModelResponse> { partial, done ->
                val delta = partial.text.orEmpty()
                logDebug("generateResponse delta: $delta", TAG)
                if (!done && delta.isNotBlank()) {
                    messageBuilder.append(delta)
                    trySend(
                        ResultEmittedData.loading(
                            model = messageBuilder.toString(),
                        )
                    )
                }
            }
            future = retrievalAndInferenceChain!!.invoke(
                retrievalRequest,
                listener
            )
            future?.addListener({
                val fullText = future?.get()?.text
                if (fullText.isNullOrEmpty()) {
                    logError("generateResponse fullText isNullOrEmpty", TAG)
                    trySend(
                        ResultEmittedData.error(
                            model = null,
                            error = null,
                            title = "MediaPipe engine error",
                            responseCode = null,
                            message = "Empty response",
                            errorType = ErrorType.EXCEPTION
                        )
                    )
                    close()
                    return@addListener
                }
                logDebug("generateResponse fullText: $fullText", TAG)
                trySend(
                    ResultEmittedData.success(
                        model = fullText,
                        message = null,
                        responseCode = null
                    )
                )
                close()
            }, executor)
            logDebug("generateResponse ready", TAG)
        } catch (t: Throwable) {
            logError("generateResponse failed: ${t.message}", t, TAG)
            trySend(
                ResultEmittedData.error(
                    model = null,
                    error = t,
                    title = "MediaPipe engine error",
                    responseCode = null,
                    message = t.message,
                    errorType = ErrorType.EXCEPTION,
                )
            )
        }
    }

    private fun createInterfaceOptions(
        modelFile: File,
        engineParams: MediaPipeEngineParams,
        isSupportImages: Boolean,
    ): LlmInferenceOptions {
        val backend = when (engineParams.backend) {
            MediaPipeBackendParams.CPU -> LlmInference.Backend.CPU
            MediaPipeBackendParams.GPU -> LlmInference.Backend.GPU
        }
        return LlmInferenceOptions.builder().apply {
            setModelPath(modelFile.absolutePath)
            setMaxTokens(engineParams.contextSize)
            setPreferredBackend(backend)
            val maxNumImages = if (isSupportImages) 1 else 0
            setMaxNumImages(maxNumImages)
            if (engineParams.useMaxTopK) setMaxTopK(engineParams.maxTopK)
        }.build()
    }

    private fun createSessionOptions(
        engineParams: MediaPipeEngineParams,
        isSupportImages: Boolean,
    ): LlmInferenceSessionOptions {
        return LlmInferenceSessionOptions.builder().apply {
            if (engineParams.useTopK) setTopK(engineParams.topK)
            if (engineParams.useTopP) setTopP(engineParams.topP)
            if (engineParams.useTemperature) setTemperature(engineParams.temperature)
            if (engineParams.useRandomSeed) setRandomSeed(engineParams.randomSeed)
            setGraphOptions(
                GraphOptions.builder()
                    .setEnableVisionModality(isSupportImages)
                    .build()
            )
        }.build()
    }

    private fun isInGeneration(): Boolean {
        return future != null && future?.isDone != true && future?.isCancelled != true
    }

}
```

**MediaPipeEngineWithTools:** Тук се поддържат извиквания на функции

```kotlin
import kotlinx.coroutines.flow.Flow
import java.io.File

interface MediaPipeEngineWithTools {

    fun startEngine(
        modelFile: File,
        isSupportImages: Boolean = false,
        engineParams: MediaPipeEngineParams,
    )

    fun generateResponse(
        userQuery: String,
        maxSteps: Int = 3,
    ): Flow<ResultEmittedData<String>>

}
```

````kotlin
package com.romankryvolapov.offlineailauncher.mediapipe

import android.app.Application
import com.google.ai.edge.localagents.core.proto.Content
import com.google.ai.edge.localagents.core.proto.FunctionCall
import com.google.ai.edge.localagents.core.proto.FunctionDeclaration
import com.google.ai.edge.localagents.core.proto.FunctionResponse
import com.google.ai.edge.localagents.core.proto.GenerateContentResponse
import com.google.ai.edge.localagents.core.proto.Part
import com.google.ai.edge.localagents.core.proto.Schema
import com.google.ai.edge.localagents.core.proto.Tool
import com.google.ai.edge.localagents.fc.GemmaFormatter
import com.google.ai.edge.localagents.fc.GenerativeModel
import com.google.ai.edge.localagents.fc.LlmInferenceBackend
import com.google.ai.edge.localagents.rag.memory.VectorStoreRecord
import com.google.ai.edge.localagents.rag.models.EmbedData
import com.google.ai.edge.localagents.rag.models.EmbeddingRequest
import com.google.common.collect.ImmutableList
import com.google.mediapipe.tasks.genai.llminference.LlmInference
import com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptions
import com.google.protobuf.Struct
import com.google.protobuf.Value
import com.romankryvolapov.offlineailauncher.common.models.common.ErrorType
import com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logDebug
import com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logError
import com.romankryvolapov.offlineailauncher.common.models.common.ResultEmittedData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.guava.await
import java.io.File

class MediaPipeEngineWithFunctionCallingImpl(
    private val application: Application,
    private val common: MediaPipeEngineCommon,
) : MediaPipeEngineWithFunctionCalling {

    companion object {
        private const val TAG = "MediaPipeEngineWithFunctionCallingsTag"

        private const val DEFAULT_MIN_SIMILARITY_SCORE = 0.8

        private const val TOOLS_CODE = "tool_code"
        private const val RESULTS = "results"

        private const val TOOLS_ACTION_SEARCH_DOCS = "search_docs"
        private const val TOOLS_ACTION_SEARCH_DOCS_DESCRIPTION = "Searches knowledge and returns the most relevant results as plain text."


        private const val TOOLS_PARAM_QUERY = "query"
        private const val TOOLS_PARAM_QUERY_DESCRIPTION = "User query to search in the vector store."

        private const val TOOLS_PARAM_TOP_K = "top_k"
        private const val TOOLS_PARAM_TOP_K_DESCRIPTION = "Number of results to return (default 5)."


        private const val TOOLS_PARAM_MIN_SIMILARITY_SCORE = "min_similarity_score"

        private const val MIN_SIMILARITY_SCORE_DESCRIPTION = """
        Minimum similarity score threshold (float) for filtering search results, from 0.0 (no filtering) to 1.0 (exact match).
        Start with $DEFAULT_MIN_SIMILARITY_SCORE, and if no results are found, lower the value and retry the search.
        """

      	// от този темплейт зависи много
      	// неправилно подбраните параметри ще направят извикването на инструмент невъзможно
        // или след извикването на инструмент генерирането ще спре
      	// за други LLM модели темплейтът може да е различен
        // ```tool_code работи добре за Gemma 3n, изглежда е била обучена върху тази ключова дума
      	// ако чрез инструментите не е намерено нищо, в промпта е указано, че сходството може да се намали
        // инструментите могат да са съвсем различни, например SQL заявка, заявка в интернет, важно е да се опишат правилно
        private val PROMPT_TEMPLATE_WITH_TOOLS = """
        You are an on-device assistant.
        You have access to special tools (also called: "function call", "invoke tool", "use API", "search", "lookup", "query tool")

        If you decide to invoke any of the function, it should be wrapped with ```$TOOLS_CODE```

        You have access to the following tools.
        * `$TOOLS_ACTION_SEARCH_DOCS`: Searches knowledge and returns the most relevant results as plain text.

        WHEN TO USE A TOOL
        - If you do not have enough information to answer with high confidence.
        - If the user explicitly or implicitly asks to check/verify/find out/look up ("check via tools", "verify", "lookup", etc.).

        Tool args:
        $TOOLS_PARAM_QUERY: User string query to search in the vector store.
        $TOOLS_PARAM_TOP_K: Integer number of results to return (default 5).
        $TOOLS_PARAM_MIN_SIMILARITY_SCORE:  Minimum similarity score threshold (float) for filtering search results, from 0.0 (no filtering) to 1.0 (exact match).
        Start with $DEFAULT_MIN_SIMILARITY_SCORE, and if no results are found, lower the value and retry the search.

        Rules for tool call:
        ```$TOOLS_CODE
        $TOOLS_ACTION_SEARCH_DOCS($TOOLS_PARAM_QUERY="<string>", $TOOLS_PARAM_TOP_K=<integer>, $TOOLS_PARAM_MIN_SIMILARITY_SCORE=<float>)
        ```

        Tool response:
        $RESULTS: Plain text results.

        IMPORTANT: After receiving tool results, ALWAYS write a natural-language answer for the user in the very next message.
        If tool results are empty, briefly explain that nothing relevant was found and propose next steps.
       """.trimIndent()

    }

    private var generativeModel: GenerativeModel? = null

    override fun startEngine(
        modelFile: File,
        isSupportImages: Boolean,
        engineParams: MediaPipeEngineParams,
    ) {
        logDebug("startEngine", TAG)
        val interfaceOptions = createInterfaceOptions(
            modelFile = modelFile,
            engineParams = engineParams,
            isSupportImages = isSupportImages,
        )
        val engineMediaPipe = LlmInference.createFromOptions(
            application,
            interfaceOptions
        )
        if (engineMediaPipe == null) {
            logError("startEngine llmInference == null", TAG)
            return
        }
        val searchDocs = FunctionDeclaration.newBuilder()
            .setName(TOOLS_ACTION_SEARCH_DOCS)
            .setDescription(TOOLS_ACTION_SEARCH_DOCS_DESCRIPTION)
            .setParameters(
                Schema.newBuilder()
                    .setType(com.google.ai.edge.localagents.core.proto.Type.OBJECT)
                    .putProperties(
                        TOOLS_PARAM_QUERY,
                        Schema.newBuilder()
                            .setType(com.google.ai.edge.localagents.core.proto.Type.STRING)
                            .setDescription(TOOLS_PARAM_QUERY_DESCRIPTION)
                            .build()
                    )
                    .putProperties(
                        TOOLS_PARAM_TOP_K,
                        Schema.newBuilder()
                            .setType(com.google.ai.edge.localagents.core.proto.Type.INTEGER)
                            .setDescription(TOOLS_PARAM_TOP_K_DESCRIPTION)
                            .build()
                    )
                    .putProperties(
                        TOOLS_PARAM_MIN_SIMILARITY_SCORE,
                        Schema.newBuilder()
                            .setType(com.google.ai.edge.localagents.core.proto.Type.NUMBER)
                            .setDescription(MIN_SIMILARITY_SCORE_DESCRIPTION)
                            .build()
                    )
                    .build()
            )
            .build()
        val systemInstruction = Content.newBuilder()
            .setRole(Gemma3nRoles.SYSTEM.type)
            .addParts(
                Part.newBuilder().setText(
                    PROMPT_TEMPLATE_WITH_TOOLS
                )
            )
            .build()
        val tool = Tool.newBuilder()
            .addFunctionDeclarations(searchDocs)
            .build()
        val inferenceBackend = LlmInferenceBackend(
            engineMediaPipe,
            GemmaFormatter()
        )
        generativeModel = GenerativeModel(
            inferenceBackend,
            systemInstruction,
            listOf(tool),
        )
        logDebug("startEngine ready", TAG)
    }

    override fun generateResponse(
        userQuery: String,
        maxSteps: Int,
    ): Flow<ResultEmittedData<String>> = flow {
        logDebug("generateResponseWithTools userQuery: $userQuery", TAG)
        try {
            val generativeModel = generativeModel ?: run {
                logError("generateResponseWithTools generativeModel is null", TAG)
                emit(
                    ResultEmittedData.error(
                        model = null,
                        error = null,
                        title = "MediaPipe engine error",
                        responseCode = null,
                        message = "Model is not initialized;",
                        errorType = ErrorType.ERROR_IN_LOGIC,
                    )
                )
                return@flow
            }
            val contentPart = Part.newBuilder()
                .setText(userQuery)
                .build()
            val userContent = Content.newBuilder()
                .setRole(Gemma3nRoles.USER.type)
                .addParts(contentPart)
                .build()
            val conversation = mutableListOf(userContent)
            var step = 0

            // за всеки случай тук има цикъл
          	// на модела се изпраща заявка, ако той прецени, че трябва да се извика инструмент,
          	// той пише служебна информация, инструментът се извиква и заявката с резултата се повтаря
          	// ако искате моделът да се опита да намери по-добър резултат от заявката,
          	// променяйки текста на заявката или минималното сходство, напишете го в промпта, за да
            // знае моделът, че извикванията на инструменти може да са много
            while (step < maxSteps) {
                logDebug("generateResponseWithTools step: $step conversation: ${conversation.size}", TAG)
                step++
                val response: GenerateContentResponse = generativeModel.generateContent(
                    conversation
                )
                val responseContent: Content = response.candidatesList.firstOrNull()?.content ?: run {
                    logError("generateResponseWithTools content is null", TAG)
                    emit(
                        ResultEmittedData.error(
                            model = null,
                            error = null,
                            title = "MediaPipe engine error",
                            responseCode = null,
                            message = "Candidates list is null",
                            errorType = ErrorType.ERROR_IN_LOGIC,
                        )
                    )
                    return@flow
                }
                val functionCall: FunctionCall? = responseContent.partsList.firstOrNull {
                    it.hasFunctionCall()
                }?.functionCall

              	// ако моделът е преценил, че не е нужно да се извикват инструменти – просто изпращаме отговора на потребителя
                if (functionCall == null) {
                    val text = extractText(response)
                    if (text.isBlank()) {
                        logError(
                            "generateResponseWithTools text is blank, response: $response",
                            TAG
                        )
                        emit(
                            ResultEmittedData.error(
                                model = null,
                                error = null,
                                title = "MediaPipe engine error",
                                responseCode = null,
                                message = "Empty text",
                                errorType = ErrorType.ERROR_IN_LOGIC,
                            )
                        )
                        return@flow
                    }
                    logDebug("generateResponseWithTools functionCall is null text: $text", TAG)
                    emit(
                        ResultEmittedData.success(
                            model = text,
                            message = null,
                            responseCode = null
                        )
                    )
                    return@flow
                }
                if (functionCall.name != TOOLS_ACTION_SEARCH_DOCS) {
                    logError("generateResponseWithTools wrong name: ${functionCall.name}", TAG)
                    val text = extractText(response)
                    if (text.isBlank()) {
                        logError(
                            "generateResponseWithTools text is blank, response: $response",
                            TAG
                        )
                        emit(
                            ResultEmittedData.error(
                                model = null,
                                error = null,
                                title = "MediaPipe engine error",
                                responseCode = null,
                                message = "Wrong function call",
                                errorType = ErrorType.ERROR_IN_LOGIC,
                            )
                        )
                        return@flow
                    }
                    emit(
                        ResultEmittedData.success(
                            model = text,
                            message = null,
                            responseCode = null
                        )
                    )
                    return@flow
                }
                val args = functionCall.args.fieldsMap

                // моделът връща в параметрите на извикването на инструмента текста на заявката към базата,
                // броя на резултатите и сходството
                // ако нищо не е намерено, в промпта е указано, че сходството може да се намали
                val query = args[TOOLS_PARAM_QUERY]?.stringValue
                val topK = args[TOOLS_PARAM_TOP_K]?.numberValue?.toInt() ?: 5
                val minSimilarityScore = args[TOOLS_PARAM_MIN_SIMILARITY_SCORE]?.numberValue?.toFloat() ?: 0.0F
                if (query.isNullOrEmpty()) {
                    logError("generateResponseWithTools query is null or empty", TAG)
                    val text = extractText(response)
                    if (text.isBlank()) {
                        logError(
                            "generateResponseWithTools text is blank, response: $response",
                            TAG
                        )
                        emit(
                            ResultEmittedData.error(
                                model = null,
                                error = null,
                                title = "MediaPipe engine error",
                                responseCode = null,
                                message = "Wrong function call",
                                errorType = ErrorType.ERROR_IN_LOGIC,
                            )
                        )
                        return@flow
                    }
                    logDebug("generateResponseWithTools query is null or empty text: $text", TAG)
                    emit(
                        ResultEmittedData.success(
                            model = text,
                            message = null,
                            responseCode = null
                        )
                    )
                    return@flow
                }
                val results: String = searchDocsInternal(
                    query,
                    topK,
                    minSimilarityScore
                )
                val respStruct = Struct.newBuilder()
                    .putFields(
                        TOOLS_PARAM_QUERY,
                        Value.newBuilder().setStringValue(query).build()
                    )
                    .putFields(
                        TOOLS_PARAM_TOP_K,
                        Value.newBuilder().setNumberValue(topK.toDouble()).build()
                    )
                    .putFields(
                        TOOLS_PARAM_MIN_SIMILARITY_SCORE,
                        Value.newBuilder().setNumberValue(minSimilarityScore.toDouble()).build()
                    )
                    .putFields(
                        RESULTS,
                        Value.newBuilder().setStringValue(results).build()
                    )
                    .build()
                val functionResponse = FunctionResponse.newBuilder()
                    .setName(TOOLS_ACTION_SEARCH_DOCS)
                    .setResponse(respStruct)
                    .build()
                val functionResponsePart = Part.newBuilder()
                    .setFunctionResponse(functionResponse)
                    .build()
                val toolContent = Content.newBuilder()
                    .setRole(Gemma3nRoles.MODEL.type)
                    .addParts(functionResponsePart)
                    .build()

                // добавяме отговора на модела с извикването на инструмента и резултата от извикването на инструмента
                // във веригата от съобщения и стартираме следващата итерация на цикъла
                // така моделът ще вижда всичките си заявки и всички резултати от извикването на инструмента
                conversation.add(responseContent)
                conversation.add(toolContent)
                logDebug("conversation: $conversation", TAG)

                if (step == maxSteps) {
                    val finalResponse = generativeModel.generateContent(conversation)
                    val text = extractText(finalResponse)
                    if (text.isBlank()) {
                        logError("generateResponseWithTools finalResponse text is blank", TAG)
                        emit(
                            ResultEmittedData.error(
                                title = "MediaPipe engine error",
                                message = "Empty final response",
                                error = null,
                                model = null,
                                responseCode = null,
                                errorType = ErrorType.ERROR_IN_LOGIC,
                            )
                        )
                        return@flow
                    }
                    emit(
                        ResultEmittedData.success(
                            model = text,
                            message = null,
                            responseCode = null
                        )
                    )
                    return@flow
                }
            }
        } catch (t: Throwable) {
            logError("generateResponseWithTools failed: ${t.message}", t, TAG)
            emit(
                ResultEmittedData.error(
                    model = null,
                    error = t,
                    title = "MediaPipe engine error",
                    responseCode = null,
                    message = t.message,
                    errorType = ErrorType.EXCEPTION,
                )
            )
        }
    }

    // търсене във векторната база, като всички параметри ги задава самият модел
    private suspend fun searchDocsInternal(
        query: String,
        topK: Int,
        minSimilarityScore: Float,
    ): String {
        logDebug("searchDocsInternal query: $query topK: $topK minSimilarityScore: $minSimilarityScore", TAG)
        val queryEmbedData: EmbedData<String> = EmbedData.create(
            query,
            EmbedData.TaskType.RETRIEVAL_QUERY
        )
        val embeddingRequest: EmbeddingRequest<String> =
            EmbeddingRequest.create(listOf(queryEmbedData))
        val vector: ImmutableList<Float> = try {
            common.embedder.getEmbeddings(embeddingRequest).await()
        } catch (t: Throwable) {
            logError(
                "searchDocsInternal: embedding failed: ${t.message}", t,
                TAG
            )
            return "No results."
        }
        if (vector.isEmpty()) {
            logError("searchDocsInternal vector.isEmpty()", TAG)
            return "No results."
        }
        val hits: ImmutableList<VectorStoreRecord<String>> = try {
            common.vectorStore.getNearestRecords(
                vector,
                topK,
                minSimilarityScore
            )
        } catch (t: Throwable) {
            logError("searchDocsInternal: failed: ${t.message}", t, TAG)
            return "No results."
        }
        if (hits.isEmpty()) {
            logError("searchDocsInternal hits.isEmpty()", TAG)
            return "No results."
        }
        val result = buildString {
            for (h in hits) {
                appendLine(h.data.trim())
            }
        }.trim()
        logDebug("searchDocsInternal ready size: ${result.length}", TAG)
        return result
    }

    private fun extractText(response: GenerateContentResponse): String {
        response.candidatesList.forEach { candidate ->
            candidate.content.partsList.forEach { part ->
                if (part.text.isNotEmpty()) return part.text
            }
        }
        return ""
    }

    private fun createInterfaceOptions(
        modelFile: File,
        engineParams: MediaPipeEngineParams,
        isSupportImages: Boolean,
    ): LlmInferenceOptions {
        val backend = when (engineParams.backend) {
            MediaPipeBackendParams.CPU -> LlmInference.Backend.CPU
            MediaPipeBackendParams.GPU -> LlmInference.Backend.GPU
        }
        return LlmInferenceOptions.builder().apply {
            setModelPath(modelFile.absolutePath)
            setMaxTokens(engineParams.contextSize)
            setPreferredBackend(backend)
            val maxNumImages = if (isSupportImages) 1 else 0
            setMaxNumImages(maxNumImages)
            if (engineParams.useMaxTopK) setMaxTopK(engineParams.maxTopK)
        }.build()
    }

}
````

Надявам се, че беше интересно.

**Ако искате да повторите, ето използваните помощни класове:**

```kotlin
enum class ErrorType {
    EXCEPTION,
    SERVER_ERROR,
    ERROR_IN_LOGIC,
    SERVER_DATA_ERROR,
    NO_INTERNET_CONNECTION,
    AUTHORIZATION
}

data class ResultEmittedData<out T>(
    val model: T?,
    val error: Any?,
    val status: Status,
    val title: String?,
    val message: String?,
    val responseCode: Int?,
    val errorType: ErrorType?,
) {

    enum class Status {
        SUCCESS,
        ERROR,
        LOADING,
    }

    companion object {
        fun <T> success(
            model: T,
            message: String?,
            responseCode: Int?,
        ): ResultEmittedData<T> =
            ResultEmittedData(
                error = null,
                title = null,
                model = model,
                errorType = null,
                message = message,
                status = Status.SUCCESS,
                responseCode = responseCode,
            )

        fun <T> loading(
            model: T? = null,
            message: String? = null,
        ): ResultEmittedData<T> =
            ResultEmittedData(
                model = model,
                error = null,
                title = null,
                errorType = null,
                message = message,
                responseCode = null,
                status = Status.LOADING,
            )

        fun <T> error(
            model: T?,
            error: Any?,
            title: String?,
            message: String?,
            responseCode: Int?,
            errorType: ErrorType?,
        ): ResultEmittedData<T> =
            ResultEmittedData(
                model = model,
                error = error,
                title = title,
                message = message,
                errorType = errorType,
                status = Status.ERROR,
                responseCode = responseCode,
            )
    }
}

inline fun <T : Any> ResultEmittedData<T>.onLoading(
    action: (
        model: T?,
        message: String?,
    ) -> Unit
): ResultEmittedData<T> {
    if (status == ResultEmittedData.Status.LOADING) action(
        model,
        message
    )
    return this
}

inline fun <T : Any> ResultEmittedData<T>.onSuccess(
    action: (
        model: T,
        message: String?,
        responseCode: Int?,
    ) -> Unit
): ResultEmittedData<T> {
    if (status == ResultEmittedData.Status.SUCCESS && model != null) action(
        model,
        message,
        responseCode,
    )
    return this
}

inline fun <T : Any> ResultEmittedData<T>.onFailure(
    action: (
        model: Any?,
        title: String?,
        message: String?,
        responseCode: Int?,
        errorType: ErrorType?,
    ) -> Unit
): ResultEmittedData<T> {
    if (status == ResultEmittedData.Status.ERROR) action(
        model,
        title,
        message,
        responseCode,
        errorType
    )
    return this
}
```

```kotlin
data class MediaPipeEngineParams(
    val name: String,
    val topK: Int,
    val topP: Float,
    val temperature: Float,
    val randomSeed: Int,
    val contextSize: Int,
    val maxTopK: Int,
    val useTopK: Boolean,
    val useTopP: Boolean,
    val useTemperature: Boolean,
    val useRandomSeed: Boolean,
    val useMaxTopK: Boolean,
    val backend: MediaPipeBackendParams,
)

enum class MediaPipeBackendParams {
    CPU,
    GPU
}

fun Long.toDurationString(): String {
    var msRemaining = this

    val years = msRemaining / (365L * 24 * 60 * 60 * 1000)
    msRemaining %= (365L * 24 * 60 * 60 * 1000)

    val months = msRemaining / (30L * 24 * 60 * 60 * 1000)
    msRemaining %= (30L * 24 * 60 * 60 * 1000)

    val days = msRemaining / (24L * 60 * 60 * 1000)
    msRemaining %= (24L * 60 * 60 * 1000)

    val hours = msRemaining / (60L * 60 * 1000)
    msRemaining %= (60L * 60 * 1000)

    val minutes = msRemaining / (60L * 1000)
    msRemaining %= (60L * 1000)

    val seconds = msRemaining / 1000
    val milliseconds = msRemaining % 1000

    return buildString {
        if (years > 0) append("$years years, ")
        if (months > 0) append("$months months, ")
        if (days > 0) append("$days days, ")
        if (hours > 0) append("$hours hours, ")
        if (minutes > 0) append("$minutes minutes, ")
        if (seconds > 0) append("$seconds seconds, ")
        append("$milliseconds milliseconds")
    }
}
```

```kotlin
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Environment
import android.util.Log
import com.google.firebase.analytics.FirebaseAnalytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date

object LogUtil : KoinComponent {

    private val timeDirectoryName: String

    private const val QUEUE_CAPACITY = 10000
    private const val CURRENT_TAG = "LogUtilExecutionStatusTag"
    private const val LOG_APP_FOLDER_NAME = "app"
    private const val TIME_FORMAT_FOR_LOG = "HH:mm:ss dd-MM-yyyy"
    private const val TIME_FORMAT_FOR_DIRECTORY = "HH-mm-ss_dd-MM-yyyy"
    private const val TAG = "TAG: "
    private const val TIME = "TIME: "
    private const val ERROR_STACKTRACE = "ERROR STACKTRACE: "
    private const val ERROR_MESSAGE = "ERROR: "
    private const val DEBUG_MESSAGE = "MESSAGE: "
    private const val NEW_LINE = "\n"

    private val queue = ArrayDeque<LogData>(QUEUE_CAPACITY)

    private var saveLogsToTxtFileJob: Job? = null

    private val analytics: FirebaseAnalytics by inject()

    @Volatile
    private var isSaveLogsToTxtFile = false

    init {
        Log.d(CURRENT_TAG, "init")
        timeDirectoryName = getCurrentTimeForDirectory()
    }

    fun logDebug(message: String, tag: String) {
        CoroutineScope(Dispatchers.IO).launch {
            if (BuildConfig.DEBUG) {
                Log.d(tag, message)
                enqueue(
                    LogData.DebugMessage(
                        tag = tag,
                        time = System.currentTimeMillis(),
                        message = message,
                    )
                )
                saveLogsToTxtFile()
            }
        }
    }

    fun logError(message: String, tag: String) {
        CoroutineScope(Dispatchers.IO).launch {
            if (BuildConfig.DEBUG) {
                Log.e(tag, message)
                enqueue(
                    LogData.ErrorMessage(
                        tag = tag,
                        time = System.currentTimeMillis(),
                        message = message,
                    )
                )
                saveLogsToTxtFile()
            }
        }
    }

    fun logError(exception: Throwable, tag: String) {
        CoroutineScope(Dispatchers.IO).launch {
            if (BuildConfig.DEBUG) {
                Log.e(tag, exception.message, exception)
                enqueue(
                    LogData.ExceptionMessage(
                        tag = tag,
                        time = System.currentTimeMillis(),
                        exception = exception,
                    )
                )
                saveLogsToTxtFile()
            }
        }
    }

    fun logError(message: String, exception: Throwable, tag: String) {
        CoroutineScope(Dispatchers.IO).launch {
            if (BuildConfig.DEBUG) {
                Log.e(tag, "$message, exception: ${exception.message}", exception)
                enqueue(
                    LogData.ErrorMessageWithException(
                        tag = tag,
                        time = System.currentTimeMillis(),
                        message = message,
                        exception = exception,
                    )
                )
                saveLogsToTxtFile()
            }
        }
    }

    fun logError(message: String, error: String?, tag: String) {
        CoroutineScope(Dispatchers.IO).launch {
            if (BuildConfig.DEBUG) {
                Log.e(tag, "$message, error: $error")
                enqueue(
                    LogData.ErrorMessage(
                        tag = tag,
                        time = System.currentTimeMillis(),
                        message = message,
                    )
                )
                saveLogsToTxtFile()
            }
        }
    }

    @SuppressLint("SimpleDateFormat")
    private fun getTime(time: Long): String {
        return try {
            val date = Date(time)
            val timeString = SimpleDateFormat(TIME_FORMAT_FOR_LOG).format(date)
            timeString.ifEmpty {
                Log.e(CURRENT_TAG, "getTime time.ifEmpty")
                time.toString()
            }
        } catch (e: Exception) {
            Log.e(CURRENT_TAG, "getCurrentTime exception: ${e.message}", e)
            time.toString()
        }
    }

    @SuppressLint("SimpleDateFormat")
    private fun getCurrentTimeForDirectory(): String {
        val time = System.currentTimeMillis()
        return try {
            val date = Date(time)
            val timeString = SimpleDateFormat(TIME_FORMAT_FOR_DIRECTORY).format(date)
            Log.d(CURRENT_TAG, "getCurrentTimeForDirectory time: $time")
            timeString.ifEmpty {
                Log.e(CURRENT_TAG, "getCurrentTimeForDirectory time.ifEmpty")
                time.toString()
            }
        } catch (e: Exception) {
            Log.e(CURRENT_TAG, "getCurrentTimeForDirectory exception: ${e.message}", e)
            time.toString()
        }
    }

    private fun enqueue(message: LogData) {
        try {
            while (queue.size >= QUEUE_CAPACITY) {
                Log.d(CURRENT_TAG, "enqueue removeFirst")
                queue.removeFirst()
            }
            queue.addLast(message)
        } catch (e: Exception) {
            Log.e(CURRENT_TAG, "enqueue exception: ${e.message}", e)
        }
    }

}
```

## Пример 6 – CAG (Cache-Augmented Generation) в LLama.cpp

Тук ще опиша концепцията как може да работи това.\
Конкретно този код работи бавно и не става за пълноценно използване.

**Пример за получаване на контекста на модела като ByteArray.**\
След това полученият контекст може да се запише в база данни или във файл.\
Контекстът може да включва промпта, въпросите и отговорите, добавените документи и така нататък.

```cpp
#include <vector>
#include <cstdint>
#include <stdexcept>
#include <cstddef>
#include "llama.h"

std::vector<uint8_t> get_full_state_raw(llama_context* ctx) {

    // Проверяваме, че указателят към контекста не е равен на nullptr
    if (ctx == nullptr) {
        throw std::invalid_argument("llama_context pointer is null");
    }

    // Получаваме размера на състоянието на модела (в байтове)
    const size_t state_size = llama_state_get_size(ctx);
    if (state_size == 0) {
        throw std::runtime_error("llama_state_get_size returned 0");
    }

    // Заделяме буфер с нужния размер за съхранение на състоянието
    std::vector<uint8_t> out(state_size);

    // Записваме двоичните данни на състоянието в нашия буфер
    const size_t written = llama_state_get_data(ctx, out.data(), out.size());

    // Проверяваме, че размерът съвпада с очаквания
    if (written != state_size) {
        throw std::runtime_error("state size changed during serialization");
    }

    // Връщаме състоянието като масив от байтове
    return out;
}
```

**Пример за възстановяване на контекста от ByteArray.**\
Трябва да се помни, че контекстът може да се възстанови само за същия модел, за който е бил запазен, тъй като включва междинни състояния, а също така не може да се комбинират няколко контекста – все още няма пълноценна модулна архитектура за моделите.

```cpp
#include <vector>
#include <cstdint>
#include <stdexcept>
#include <cstddef>
#include "llama.h"

int set_full_state_raw(llama_context* ctx, const std::vector<uint8_t>& data) {
    // Проверяваме, че контекстът и данните са подадени
    if (ctx == nullptr) {
        throw std::invalid_argument("llama_context pointer is null");
    }
    if (data.empty()) {
        throw std::invalid_argument("data is empty");
    }

    // Възстановяваме състоянието на модела от масива от байтове
    const size_t written = llama_state_set_data(ctx, data.data(), data.size());

    // Връщаме броя реално заредени байтове
    return static_cast<int>(written);
}
```

[Copyright: Roman Kryvolapov](https://t.me/RomanKryvolapov)
