feat(admin): presets d'échantillonnage LLM (Speaker/Thinker) — app-side complet
Système de presets de sampling réglables depuis l'admin, applicables au Speaker et au Thinker. Inspiré du tuning Kaz (temp/top-p/top-k/pénalités). Core/config : - SamplingParams (core) : +presencePenalty, +frequencyPenalty - ConfigStore.ModelConfig : +topP/topK/repeatPenalty/presence/frequency/maxTokens +presetName ; SamplingPreset + RuntimeConfig.presets (bibliothèque) ; JSON ; 5 presets d'usine (Équilibré/Kaz chaleureux/Factuel/Analyse/Créatif). Défauts maxTokens : Speaker 450, Thinker 128 (préserve la longueur prod). - ContentProvider : colonnes speaker_*/thinker_* sampling + presets_json (R/W). - KazeiaService : samplingFrom(ModelConfig) → SamplingParams pour Speaker+Thinker (au lieu du hardcodé). maxTokens honoré par le moteur ; le reste prêt. Admin : - KazeiaConfigClient : Sampling + Preset + parse/serialize presets_json + updateSampling/updatePresets - ConfigRepository : updateSampling/updatePresets - ParametersScreen (nav "Paramètres") : éditeurs sliders Speaker/Thinker + apply preset + bibliothèque CRUD ; bandeau honnête « seul Tokens max agit aujourd'hui ». ⚠ Le moteur natif n'honore que maxTokens à ce jour. Spec d'extension JNI sampling pour le dev engine : docs/SAMPLING_ENGINE_SPEC.md (~10 lignes app à brancher après). Vérifié device : colonnes sampling R/W (temp/max/preset_name), presets_json seedés + lus. Build app+admin verts, admin lancé sans crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b11d8a96d4
commit
471fb5d5fb
|
|
@ -93,6 +93,9 @@ private fun MainShell() {
|
||||||
composable(AdminDestination.Prompts.route) {
|
composable(AdminDestination.Prompts.route) {
|
||||||
PromptsScreen()
|
PromptsScreen()
|
||||||
}
|
}
|
||||||
|
composable(AdminDestination.Parameters.route) {
|
||||||
|
com.kazeia.admin.ui.parameters.ParametersScreen()
|
||||||
|
}
|
||||||
composable(AdminDestination.Rag.route) {
|
composable(AdminDestination.Rag.route) {
|
||||||
com.kazeia.admin.ui.rag.RagScreen()
|
com.kazeia.admin.ui.rag.RagScreen()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@ class ConfigRepository(context: Context) {
|
||||||
fun updateDebug(enabled: Boolean): Boolean =
|
fun updateDebug(enabled: Boolean): Boolean =
|
||||||
client.updateConfig(debugEnabled = enabled)
|
client.updateConfig(debugEnabled = enabled)
|
||||||
|
|
||||||
|
/** Applique un jeu de sampling à un rôle ("speaker"|"thinker"), avec le nom
|
||||||
|
* du preset source ("" = réglages custom). */
|
||||||
|
fun updateSampling(role: String, s: KazeiaConfigClient.Sampling, presetName: String): Boolean =
|
||||||
|
client.updateSampling(role, s, presetName)
|
||||||
|
|
||||||
|
/** Remplace toute la bibliothèque de presets (création / édition / suppression). */
|
||||||
|
fun updatePresets(presets: List<KazeiaConfigClient.Preset>): Boolean =
|
||||||
|
client.updatePresets(presets)
|
||||||
|
|
||||||
fun updateSpeakerModel(modelId: String): Boolean =
|
fun updateSpeakerModel(modelId: String): Boolean =
|
||||||
client.updateConfig(speakerModelId = modelId)
|
client.updateConfig(speakerModelId = modelId)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ package com.kazeia.admin.data.source
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client du ContentProvider Kazeia pour les URI `/config` et `/models`.
|
* Client du ContentProvider Kazeia pour les URI `/config` et `/models`.
|
||||||
|
|
@ -17,20 +20,41 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
val MODELS_URI: Uri = Uri.parse("content://$AUTHORITY/models")
|
val MODELS_URI: Uri = Uri.parse("content://$AUTHORITY/models")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Jeu de paramètres d'échantillonnage d'un modèle (ou d'un preset). */
|
||||||
|
data class Sampling(
|
||||||
|
val temperature: Float = 0.7f,
|
||||||
|
val topP: Float = 0.85f,
|
||||||
|
val topK: Int = 40,
|
||||||
|
val repeatPenalty: Float = 1.1f,
|
||||||
|
val presencePenalty: Float = 0.0f,
|
||||||
|
val frequencyPenalty: Float = 0.0f,
|
||||||
|
val maxTokens: Int = 120
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Preset nommé réutilisable, applicable au Speaker ou au Thinker. */
|
||||||
|
data class Preset(val name: String, val sampling: Sampling)
|
||||||
|
|
||||||
data class Config(
|
data class Config(
|
||||||
val cascadeEnabled: Boolean,
|
val cascadeEnabled: Boolean,
|
||||||
val ttsEnabled: Boolean,
|
val ttsEnabled: Boolean,
|
||||||
val speakerModelId: String,
|
val speakerModelId: String,
|
||||||
val speakerSystemPrompt: String,
|
val speakerSystemPrompt: String,
|
||||||
val speakerTemperature: Float,
|
val speakerSampling: Sampling,
|
||||||
|
val speakerPresetName: String,
|
||||||
val thinkerModelId: String,
|
val thinkerModelId: String,
|
||||||
val thinkerSystemPrompt: String,
|
val thinkerSystemPrompt: String,
|
||||||
val thinkerTemperature: Float,
|
val thinkerSampling: Sampling,
|
||||||
|
val thinkerPresetName: String,
|
||||||
|
val presets: List<Preset> = emptyList(),
|
||||||
val ragEnabled: Boolean = false,
|
val ragEnabled: Boolean = false,
|
||||||
val ragThreshold: Float = 0.82f,
|
val ragThreshold: Float = 0.82f,
|
||||||
val ragTopK: Int = 3,
|
val ragTopK: Int = 3,
|
||||||
val debugEnabled: Boolean = false
|
val debugEnabled: Boolean = false
|
||||||
)
|
) {
|
||||||
|
// Compat : anciens écrans qui lisaient juste la température.
|
||||||
|
val speakerTemperature: Float get() = speakerSampling.temperature
|
||||||
|
val thinkerTemperature: Float get() = thinkerSampling.temperature
|
||||||
|
}
|
||||||
|
|
||||||
data class ModelInfo(
|
data class ModelInfo(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
|
@ -44,6 +68,23 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
val notes: String
|
val notes: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun Cursor.floatOr(col: String, d: Float) =
|
||||||
|
getColumnIndex(col).let { if (it >= 0) getFloat(it) else d }
|
||||||
|
private fun Cursor.intOr(col: String, d: Int) =
|
||||||
|
getColumnIndex(col).let { if (it >= 0) getInt(it) else d }
|
||||||
|
private fun Cursor.strOr(col: String, d: String) =
|
||||||
|
getColumnIndex(col).let { if (it >= 0) getString(it) ?: d else d }
|
||||||
|
|
||||||
|
private fun Cursor.sampling(p: String) = Sampling(
|
||||||
|
temperature = floatOr("${p}temperature", 0.7f),
|
||||||
|
topP = floatOr("${p}top_p", 0.85f),
|
||||||
|
topK = intOr("${p}top_k", 40),
|
||||||
|
repeatPenalty = floatOr("${p}repeat_penalty", 1.1f),
|
||||||
|
presencePenalty = floatOr("${p}presence_penalty", 0.0f),
|
||||||
|
frequencyPenalty = floatOr("${p}frequency_penalty", 0.0f),
|
||||||
|
maxTokens = intOr("${p}max_tokens", 120)
|
||||||
|
)
|
||||||
|
|
||||||
fun queryConfig(): Config? = runCatching {
|
fun queryConfig(): Config? = runCatching {
|
||||||
resolver.query(CONFIG_URI, null, null, null, null)?.use { c ->
|
resolver.query(CONFIG_URI, null, null, null, null)?.use { c ->
|
||||||
if (!c.moveToFirst()) return@use null
|
if (!c.moveToFirst()) return@use null
|
||||||
|
|
@ -52,18 +93,58 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
ttsEnabled = c.getInt(c.getColumnIndexOrThrow("tts_enabled")) == 1,
|
ttsEnabled = c.getInt(c.getColumnIndexOrThrow("tts_enabled")) == 1,
|
||||||
speakerModelId = c.getString(c.getColumnIndexOrThrow("speaker_model_id")),
|
speakerModelId = c.getString(c.getColumnIndexOrThrow("speaker_model_id")),
|
||||||
speakerSystemPrompt = c.getString(c.getColumnIndexOrThrow("speaker_system_prompt")),
|
speakerSystemPrompt = c.getString(c.getColumnIndexOrThrow("speaker_system_prompt")),
|
||||||
speakerTemperature = c.getFloat(c.getColumnIndexOrThrow("speaker_temperature")),
|
speakerSampling = c.sampling("speaker_"),
|
||||||
|
speakerPresetName = c.strOr("speaker_preset_name", ""),
|
||||||
thinkerModelId = c.getString(c.getColumnIndexOrThrow("thinker_model_id")),
|
thinkerModelId = c.getString(c.getColumnIndexOrThrow("thinker_model_id")),
|
||||||
thinkerSystemPrompt = c.getString(c.getColumnIndexOrThrow("thinker_system_prompt")),
|
thinkerSystemPrompt = c.getString(c.getColumnIndexOrThrow("thinker_system_prompt")),
|
||||||
thinkerTemperature = c.getFloat(c.getColumnIndexOrThrow("thinker_temperature")),
|
thinkerSampling = c.sampling("thinker_"),
|
||||||
ragEnabled = c.getColumnIndex("rag_enabled").let { if (it >= 0) c.getInt(it) == 1 else false },
|
thinkerPresetName = c.strOr("thinker_preset_name", ""),
|
||||||
ragThreshold = c.getColumnIndex("rag_threshold").let { if (it >= 0) c.getFloat(it) else 0.82f },
|
presets = parsePresets(c.strOr("presets_json", "[]")),
|
||||||
ragTopK = c.getColumnIndex("rag_top_k").let { if (it >= 0) c.getInt(it) else 3 },
|
ragEnabled = c.intOr("rag_enabled", 0) == 1,
|
||||||
debugEnabled = c.getColumnIndex("debug_enabled").let { if (it >= 0) c.getInt(it) == 1 else false }
|
ragThreshold = c.floatOr("rag_threshold", 0.82f),
|
||||||
|
ragTopK = c.intOr("rag_top_k", 3),
|
||||||
|
debugEnabled = c.intOr("debug_enabled", 0) == 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
|
private fun parsePresets(json: String): List<Preset> = runCatching {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).mapNotNull { i ->
|
||||||
|
arr.optJSONObject(i)?.let { o ->
|
||||||
|
Preset(
|
||||||
|
o.optString("name", ""),
|
||||||
|
Sampling(
|
||||||
|
o.optDouble("temperature", 0.7).toFloat(),
|
||||||
|
o.optDouble("top_p", 0.85).toFloat(),
|
||||||
|
o.optInt("top_k", 40),
|
||||||
|
o.optDouble("repeat_penalty", 1.1).toFloat(),
|
||||||
|
o.optDouble("presence_penalty", 0.0).toFloat(),
|
||||||
|
o.optDouble("frequency_penalty", 0.0).toFloat(),
|
||||||
|
o.optInt("max_tokens", 120)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.filter { it.name.isNotBlank() }
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
|
private fun presetsToJson(presets: List<Preset>): String {
|
||||||
|
val arr = JSONArray()
|
||||||
|
presets.forEach { p ->
|
||||||
|
arr.put(JSONObject().apply {
|
||||||
|
put("name", p.name)
|
||||||
|
put("temperature", p.sampling.temperature)
|
||||||
|
put("top_p", p.sampling.topP)
|
||||||
|
put("top_k", p.sampling.topK)
|
||||||
|
put("repeat_penalty", p.sampling.repeatPenalty)
|
||||||
|
put("presence_penalty", p.sampling.presencePenalty)
|
||||||
|
put("frequency_penalty", p.sampling.frequencyPenalty)
|
||||||
|
put("max_tokens", p.sampling.maxTokens)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return arr.toString()
|
||||||
|
}
|
||||||
|
|
||||||
fun queryModels(): List<ModelInfo> = runCatching {
|
fun queryModels(): List<ModelInfo> = runCatching {
|
||||||
resolver.query(MODELS_URI, null, null, null, null)?.use { c ->
|
resolver.query(MODELS_URI, null, null, null, null)?.use { c ->
|
||||||
val out = mutableListOf<ModelInfo>()
|
val out = mutableListOf<ModelInfo>()
|
||||||
|
|
@ -91,10 +172,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
ttsEnabled: Boolean? = null,
|
ttsEnabled: Boolean? = null,
|
||||||
speakerModelId: String? = null,
|
speakerModelId: String? = null,
|
||||||
speakerSystemPrompt: String? = null,
|
speakerSystemPrompt: String? = null,
|
||||||
speakerTemperature: Float? = null,
|
|
||||||
thinkerModelId: String? = null,
|
thinkerModelId: String? = null,
|
||||||
thinkerSystemPrompt: String? = null,
|
thinkerSystemPrompt: String? = null,
|
||||||
thinkerTemperature: Float? = null,
|
|
||||||
ragEnabled: Boolean? = null,
|
ragEnabled: Boolean? = null,
|
||||||
ragThreshold: Float? = null,
|
ragThreshold: Float? = null,
|
||||||
ragTopK: Int? = null,
|
ragTopK: Int? = null,
|
||||||
|
|
@ -105,17 +184,37 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
ttsEnabled?.let { v.put("tts_enabled", it) }
|
ttsEnabled?.let { v.put("tts_enabled", it) }
|
||||||
speakerModelId?.let { v.put("speaker_model_id", it) }
|
speakerModelId?.let { v.put("speaker_model_id", it) }
|
||||||
speakerSystemPrompt?.let { v.put("speaker_system_prompt", it) }
|
speakerSystemPrompt?.let { v.put("speaker_system_prompt", it) }
|
||||||
speakerTemperature?.let { v.put("speaker_temperature", it) }
|
|
||||||
thinkerModelId?.let { v.put("thinker_model_id", it) }
|
thinkerModelId?.let { v.put("thinker_model_id", it) }
|
||||||
thinkerSystemPrompt?.let { v.put("thinker_system_prompt", it) }
|
thinkerSystemPrompt?.let { v.put("thinker_system_prompt", it) }
|
||||||
thinkerTemperature?.let { v.put("thinker_temperature", it) }
|
|
||||||
ragEnabled?.let { v.put("rag_enabled", it) }
|
ragEnabled?.let { v.put("rag_enabled", it) }
|
||||||
ragThreshold?.let { v.put("rag_threshold", it) }
|
ragThreshold?.let { v.put("rag_threshold", it) }
|
||||||
ragTopK?.let { v.put("rag_top_k", it) }
|
ragTopK?.let { v.put("rag_top_k", it) }
|
||||||
debugEnabled?.let { v.put("debug_enabled", it) }
|
debugEnabled?.let { v.put("debug_enabled", it) }
|
||||||
|
return push(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applique un jeu de sampling (+ nom de preset, "" = custom) à un rôle. */
|
||||||
|
fun updateSampling(role: String, s: Sampling, presetName: String): Boolean {
|
||||||
|
val p = "${role}_" // "speaker" | "thinker"
|
||||||
|
val v = ContentValues().apply {
|
||||||
|
put("${p}temperature", s.temperature)
|
||||||
|
put("${p}top_p", s.topP)
|
||||||
|
put("${p}top_k", s.topK)
|
||||||
|
put("${p}repeat_penalty", s.repeatPenalty)
|
||||||
|
put("${p}presence_penalty", s.presencePenalty)
|
||||||
|
put("${p}frequency_penalty", s.frequencyPenalty)
|
||||||
|
put("${p}max_tokens", s.maxTokens)
|
||||||
|
put("${p}preset_name", presetName)
|
||||||
|
}
|
||||||
|
return push(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remplace toute la bibliothèque de presets. */
|
||||||
|
fun updatePresets(presets: List<Preset>): Boolean =
|
||||||
|
push(ContentValues().apply { put("presets_json", presetsToJson(presets)) })
|
||||||
|
|
||||||
|
private fun push(v: ContentValues): Boolean {
|
||||||
if (v.size() == 0) return true
|
if (v.size() == 0) return true
|
||||||
return runCatching {
|
return runCatching { resolver.update(CONFIG_URI, v, null, null) > 0 }.getOrDefault(false)
|
||||||
resolver.update(CONFIG_URI, v, null, null) > 0
|
|
||||||
}.getOrDefault(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.material.icons.outlined.Build
|
||||||
import androidx.compose.material.icons.outlined.GraphicEq
|
import androidx.compose.material.icons.outlined.GraphicEq
|
||||||
import androidx.compose.material.icons.outlined.History
|
import androidx.compose.material.icons.outlined.History
|
||||||
import androidx.compose.material.icons.outlined.People
|
import androidx.compose.material.icons.outlined.People
|
||||||
|
import androidx.compose.material.icons.outlined.Tune
|
||||||
import androidx.compose.material.icons.outlined.RecordVoiceOver
|
import androidx.compose.material.icons.outlined.RecordVoiceOver
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ enum class AdminDestination(
|
||||||
Voices( "voices", com.kazeia.admin.R.string.nav_voices, Icons.Outlined.RecordVoiceOver),
|
Voices( "voices", com.kazeia.admin.R.string.nav_voices, Icons.Outlined.RecordVoiceOver),
|
||||||
Profiles( "profiles", com.kazeia.admin.R.string.nav_profiles, Icons.Outlined.People),
|
Profiles( "profiles", com.kazeia.admin.R.string.nav_profiles, Icons.Outlined.People),
|
||||||
Prompts( "prompts", com.kazeia.admin.R.string.nav_prompts, Icons.AutoMirrored.Outlined.Article),
|
Prompts( "prompts", com.kazeia.admin.R.string.nav_prompts, Icons.AutoMirrored.Outlined.Article),
|
||||||
|
Parameters("parameters", com.kazeia.admin.R.string.nav_parameters, Icons.Outlined.Tune),
|
||||||
Rag( "rag", com.kazeia.admin.R.string.nav_rag, Icons.Outlined.Book),
|
Rag( "rag", com.kazeia.admin.R.string.nav_rag, Icons.Outlined.Book),
|
||||||
History( "history", com.kazeia.admin.R.string.nav_history, Icons.Outlined.History),
|
History( "history", com.kazeia.admin.R.string.nav_history, Icons.Outlined.History),
|
||||||
Telemetry("telemetry", com.kazeia.admin.R.string.nav_telemetry, Icons.Outlined.Analytics),
|
Telemetry("telemetry", com.kazeia.admin.R.string.nav_telemetry, Icons.Outlined.Analytics),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,356 @@
|
||||||
|
package com.kazeia.admin.ui.parameters
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.kazeia.admin.data.repository.ConfigRepository
|
||||||
|
import com.kazeia.admin.data.source.KazeiaConfigClient.Preset
|
||||||
|
import com.kazeia.admin.data.source.KazeiaConfigClient.Sampling
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ParametersScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val repo = remember { ConfigRepository(context) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackbar = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
var loaded by remember { mutableStateOf(false) }
|
||||||
|
var reachable by remember { mutableStateOf(true) }
|
||||||
|
var presets by remember { mutableStateOf<List<Preset>>(emptyList()) }
|
||||||
|
var speaker by remember { mutableStateOf(Sampling()) }
|
||||||
|
var thinker by remember { mutableStateOf(Sampling()) }
|
||||||
|
var speakerPreset by remember { mutableStateOf("") }
|
||||||
|
var thinkerPreset by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
val cfg = repo.config()
|
||||||
|
if (cfg == null) { reachable = false; loaded = true; return }
|
||||||
|
reachable = true
|
||||||
|
presets = cfg.presets
|
||||||
|
speaker = cfg.speakerSampling; speakerPreset = cfg.speakerPresetName
|
||||||
|
thinker = cfg.thinkerSampling; thinkerPreset = cfg.thinkerPresetName
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) { reload() }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Paramètres d'échantillonnage") },
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbar) }
|
||||||
|
) { padding ->
|
||||||
|
if (!loaded) return@Scaffold
|
||||||
|
if (!reachable) {
|
||||||
|
Column(Modifier.fillMaxSize().padding(32.dp), verticalArrangement = Arrangement.Center) {
|
||||||
|
Text("Kazeia patient non joignable.", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Text("Lance l'app Kazeia puis reviens ici.", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
return@Scaffold
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
EngineNoticeCard()
|
||||||
|
|
||||||
|
RoleCard(
|
||||||
|
title = "Speaker", subtitle = "LLM qui répond au patient",
|
||||||
|
sampling = speaker, presetName = speakerPreset, presets = presets,
|
||||||
|
onChange = { speaker = it; speakerPreset = "" },
|
||||||
|
onApplyPreset = { p -> speaker = p.sampling; speakerPreset = p.name },
|
||||||
|
onSave = {
|
||||||
|
val ok = repo.updateSampling("speaker", speaker, speakerPreset)
|
||||||
|
scope.launch { snackbar.showSnackbar(if (ok) "Speaker mis à jour." else "Échec : patient injoignable.", duration = SnackbarDuration.Short) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RoleCard(
|
||||||
|
title = "Thinker", subtitle = "LLM d'analyse (cascade)",
|
||||||
|
sampling = thinker, presetName = thinkerPreset, presets = presets,
|
||||||
|
onChange = { thinker = it; thinkerPreset = "" },
|
||||||
|
onApplyPreset = { p -> thinker = p.sampling; thinkerPreset = p.name },
|
||||||
|
onSave = {
|
||||||
|
val ok = repo.updateSampling("thinker", thinker, thinkerPreset)
|
||||||
|
scope.launch { snackbar.showSnackbar(if (ok) "Thinker mis à jour." else "Échec : patient injoignable.", duration = SnackbarDuration.Short) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PresetLibraryCard(
|
||||||
|
presets = presets,
|
||||||
|
onSave = { updated ->
|
||||||
|
val ok = repo.updatePresets(updated)
|
||||||
|
if (ok) reload()
|
||||||
|
scope.launch { snackbar.showSnackbar(if (ok) "Bibliothèque de presets enregistrée." else "Échec : patient injoignable.", duration = SnackbarDuration.Short) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EngineNoticeCard() {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f)),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text("ℹ️ Effet sur la génération", fontWeight = FontWeight.SemiBold)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
"Aujourd'hui le moteur n'applique que « Tokens max ». Température, top-p/k et " +
|
||||||
|
"pénalités sont enregistrés et prendront effet dès que le moteur expose " +
|
||||||
|
"l'échantillonnage (mise à jour à venir).",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RoleCard(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
sampling: Sampling,
|
||||||
|
presetName: String,
|
||||||
|
presets: List<Preset>,
|
||||||
|
onChange: (Sampling) -> Unit,
|
||||||
|
onApplyPreset: (Preset) -> Unit,
|
||||||
|
onSave: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Appliquer un preset
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded }) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = if (presetName.isNotBlank()) presetName else "Personnalisé",
|
||||||
|
onValueChange = {}, readOnly = true,
|
||||||
|
label = { Text("Preset appliqué") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor(androidx.compose.material3.MenuAnchorType.PrimaryNotEditable, true)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
if (presets.isEmpty()) {
|
||||||
|
DropdownMenuItem(text = { Text("Aucun preset") }, onClick = { expanded = false }, enabled = false)
|
||||||
|
}
|
||||||
|
presets.forEach { p ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(p.name) },
|
||||||
|
onClick = { expanded = false; onApplyPreset(p) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
SamplingEditor(sampling, onChange)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Button(onClick = onSave) { Text("Appliquer au $title") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SamplingEditor(s: Sampling, onChange: (Sampling) -> Unit) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
SliderRow("Température", s.temperature, 0f, 2f) { onChange(s.copy(temperature = it)) }
|
||||||
|
SliderRow("Top-p", s.topP, 0f, 1f) { onChange(s.copy(topP = it)) }
|
||||||
|
IntSliderRow("Top-k", s.topK, 0, 100) { onChange(s.copy(topK = it)) }
|
||||||
|
SliderRow("Pénalité répétition", s.repeatPenalty, 1f, 2f) { onChange(s.copy(repeatPenalty = it)) }
|
||||||
|
SliderRow("Pénalité présence", s.presencePenalty, 0f, 2f) { onChange(s.copy(presencePenalty = it)) }
|
||||||
|
SliderRow("Pénalité fréquence", s.frequencyPenalty, 0f, 2f) { onChange(s.copy(frequencyPenalty = it)) }
|
||||||
|
IntSliderRow("Tokens max ✓", s.maxTokens, 16, 512) { onChange(s.copy(maxTokens = it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SliderRow(label: String, value: Float, min: Float, max: Float, onChange: (Float) -> Unit) {
|
||||||
|
Column {
|
||||||
|
Row {
|
||||||
|
Text(label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
String.format("%.2f", value),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = value.coerceIn(min, max),
|
||||||
|
onValueChange = { onChange((it * 20).roundToInt() / 20f) }, // pas 0.05
|
||||||
|
valueRange = min..max
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IntSliderRow(label: String, value: Int, min: Int, max: Int, onChange: (Int) -> Unit) {
|
||||||
|
Column {
|
||||||
|
Row {
|
||||||
|
Text(label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||||
|
Text("$value", style = MaterialTheme.typography.bodyMedium, fontFamily = FontFamily.Monospace)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = value.coerceIn(min, max).toFloat(),
|
||||||
|
onValueChange = { onChange(it.roundToInt()) },
|
||||||
|
valueRange = min.toFloat()..max.toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PresetLibraryCard(presets: List<Preset>, onSave: (List<Preset>) -> Unit) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text("Bibliothèque de presets", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text("Modèles réutilisables, applicables au Speaker ou au Thinker.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
var editing by remember { mutableStateOf<Int?>(null) } // index en édition
|
||||||
|
|
||||||
|
presets.forEachIndexed { i, p ->
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(p.name, fontWeight = FontWeight.Medium)
|
||||||
|
Text(
|
||||||
|
"t=${"%.2f".format(p.sampling.temperature)} · top-p=${"%.2f".format(p.sampling.topP)} · " +
|
||||||
|
"rep=${"%.2f".format(p.sampling.repeatPenalty)} · max=${p.sampling.maxTokens}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(onClick = { editing = if (editing == i) null else i }) {
|
||||||
|
Text(if (editing == i) "Fermer" else "Éditer")
|
||||||
|
}
|
||||||
|
TextButton(onClick = { onSave(presets.filterIndexed { j, _ -> j != i }) }) {
|
||||||
|
Text("Suppr.", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (editing == i) {
|
||||||
|
var draft by remember(i) { mutableStateOf(p.sampling) }
|
||||||
|
var name by remember(i) { mutableStateOf(p.name) }
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name, onValueChange = { name = it },
|
||||||
|
label = { Text("Nom du preset") }, singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
SamplingEditor(draft) { draft = it }
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
editing = null
|
||||||
|
onSave(presets.toMutableList().also { it[i] = Preset(name.ifBlank { p.name }, draft) })
|
||||||
|
}
|
||||||
|
) { Text("Enregistrer") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nouveau preset
|
||||||
|
var creating by remember { mutableStateOf(false) }
|
||||||
|
if (!creating) {
|
||||||
|
TextButton(onClick = { creating = true }) { Text("+ Nouveau preset") }
|
||||||
|
} else {
|
||||||
|
var newName by remember { mutableStateOf("") }
|
||||||
|
var newSampling by remember { mutableStateOf(Sampling()) }
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newName, onValueChange = { newName = it },
|
||||||
|
label = { Text("Nom du nouveau preset") }, singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
SamplingEditor(newSampling) { newSampling = it }
|
||||||
|
Row {
|
||||||
|
TextButton(onClick = { creating = false }) { Text("Annuler") }
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Button(
|
||||||
|
enabled = newName.isNotBlank(),
|
||||||
|
onClick = {
|
||||||
|
creating = false
|
||||||
|
onSave(presets + Preset(newName.trim(), newSampling))
|
||||||
|
}
|
||||||
|
) { Text("Créer") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<string name="nav_voices">Voix</string>
|
<string name="nav_voices">Voix</string>
|
||||||
<string name="nav_profiles">Profils patients</string>
|
<string name="nav_profiles">Profils patients</string>
|
||||||
<string name="nav_prompts">Prompts</string>
|
<string name="nav_prompts">Prompts</string>
|
||||||
|
<string name="nav_parameters">Paramètres</string>
|
||||||
<string name="nav_rag">Base documentaire</string>
|
<string name="nav_rag">Base documentaire</string>
|
||||||
<string name="nav_history">Historique</string>
|
<string name="nav_history">Historique</string>
|
||||||
<string name="nav_telemetry">Telemetry</string>
|
<string name="nav_telemetry">Telemetry</string>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,32 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
data class ModelConfig(
|
data class ModelConfig(
|
||||||
val modelId: String,
|
val modelId: String,
|
||||||
val systemPrompt: String,
|
val systemPrompt: String,
|
||||||
val temperature: Float
|
// Paramètres d'échantillonnage. `temperature` historique conservé ; les
|
||||||
|
// autres ajoutés par la refonte presets 2026-06-18. ⚠ seul maxTokens est
|
||||||
|
// honoré par le moteur natif actuel — le reste prend effet quand le JNI
|
||||||
|
// expose le sampling (cf. docs/SAMPLING_ENGINE_SPEC.md). presetName = nom
|
||||||
|
// du preset appliqué (vide = réglages custom).
|
||||||
|
val temperature: Float,
|
||||||
|
val topP: Float = 0.85f,
|
||||||
|
val topK: Int = 40,
|
||||||
|
val repeatPenalty: Float = 1.1f,
|
||||||
|
val presencePenalty: Float = 0.0f,
|
||||||
|
val frequencyPenalty: Float = 0.0f,
|
||||||
|
val maxTokens: Int = 120,
|
||||||
|
val presetName: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Modèle d'échantillonnage nommé, réutilisable, applicable au Speaker ou au
|
||||||
|
* Thinker. Géré (CRUD) depuis l'app admin, persisté dans la config. */
|
||||||
|
data class SamplingPreset(
|
||||||
|
val name: String,
|
||||||
|
val temperature: Float,
|
||||||
|
val topP: Float,
|
||||||
|
val topK: Int,
|
||||||
|
val repeatPenalty: Float,
|
||||||
|
val presencePenalty: Float,
|
||||||
|
val frequencyPenalty: Float,
|
||||||
|
val maxTokens: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RuntimeConfig(
|
data class RuntimeConfig(
|
||||||
|
|
@ -69,7 +94,9 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
* panneau de logs + métriques système (CPU/GPU/NPU/RAM). Quand false
|
* panneau de logs + métriques système (CPU/GPU/NPU/RAM). Quand false
|
||||||
* (défaut prod), le bouton est masqué — l'écran reste épuré pour le
|
* (défaut prod), le bouton est masqué — l'écran reste épuré pour le
|
||||||
* patient. Pilotable depuis l'app admin. */
|
* patient. Pilotable depuis l'app admin. */
|
||||||
val debugEnabled: Boolean = false
|
val debugEnabled: Boolean = false,
|
||||||
|
/** Bibliothèque de presets de sampling réutilisables (gérée depuis l'admin). */
|
||||||
|
val presets: List<SamplingPreset> = defaultPresets()
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -111,17 +138,29 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Presets de sampling d'usine — inspirés du tuning Kaz (binôme) + profils
|
||||||
|
* thérapeutiques. Repère pour l'admin ; éditables/supprimables. */
|
||||||
|
fun defaultPresets(): List<SamplingPreset> = listOf(
|
||||||
|
SamplingPreset("Équilibré (défaut)", 0.7f, 0.85f, 40, 1.1f, 0.0f, 0.0f, 120),
|
||||||
|
SamplingPreset("Kaz chaleureux", 0.4f, 0.85f, 40, 1.05f, 0.2f, 0.2f, 256),
|
||||||
|
SamplingPreset("Factuel / froid", 0.3f, 0.70f, 40, 1.2f, 0.1f, 0.1f, 200),
|
||||||
|
SamplingPreset("Analyse (Thinker)", 0.2f, 0.70f, 40, 1.15f, 0.0f, 0.0f, 128),
|
||||||
|
SamplingPreset("Créatif", 0.9f, 0.95f, 60, 1.05f, 0.3f, 0.3f, 200)
|
||||||
|
)
|
||||||
|
|
||||||
fun defaultConfig(): RuntimeConfig = RuntimeConfig(
|
fun defaultConfig(): RuntimeConfig = RuntimeConfig(
|
||||||
cascadeEnabled = false, // mono-Speaker = contrat FROZEN actuel
|
cascadeEnabled = false, // mono-Speaker = contrat FROZEN actuel
|
||||||
speaker = ModelConfig(
|
speaker = ModelConfig(
|
||||||
modelId = ModelRegistry.defaultSpeaker().id,
|
modelId = ModelRegistry.defaultSpeaker().id,
|
||||||
systemPrompt = DEFAULT_SPEAKER_PROMPT,
|
systemPrompt = DEFAULT_SPEAKER_PROMPT,
|
||||||
temperature = 0.7f
|
temperature = 0.7f,
|
||||||
|
maxTokens = 450 // réponse Speaker longue (valeur prod historique)
|
||||||
),
|
),
|
||||||
thinker = ModelConfig(
|
thinker = ModelConfig(
|
||||||
modelId = ModelRegistry.defaultThinker().id,
|
modelId = ModelRegistry.defaultThinker().id,
|
||||||
systemPrompt = DEFAULT_THINKER_PROMPT,
|
systemPrompt = DEFAULT_THINKER_PROMPT,
|
||||||
temperature = 0.0f
|
temperature = 0.0f,
|
||||||
|
maxTokens = 128 // bloc clinique court
|
||||||
),
|
),
|
||||||
ttsEnabled = true,
|
ttsEnabled = true,
|
||||||
sttEngine = "prod",
|
sttEngine = "prod",
|
||||||
|
|
@ -178,19 +217,51 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
root.put("rag_threshold", cfg.ragThreshold.toDouble())
|
root.put("rag_threshold", cfg.ragThreshold.toDouble())
|
||||||
root.put("rag_top_k", cfg.ragTopK)
|
root.put("rag_top_k", cfg.ragTopK)
|
||||||
root.put("debug_enabled", cfg.debugEnabled)
|
root.put("debug_enabled", cfg.debugEnabled)
|
||||||
root.put("speaker", JSONObject().apply {
|
root.put("speaker", modelToJson(cfg.speaker))
|
||||||
put("model_id", cfg.speaker.modelId)
|
root.put("thinker", modelToJson(cfg.thinker))
|
||||||
put("system_prompt", cfg.speaker.systemPrompt)
|
root.put("presets", org.json.JSONArray().apply {
|
||||||
put("temperature", cfg.speaker.temperature)
|
cfg.presets.forEach { p ->
|
||||||
|
put(JSONObject().apply {
|
||||||
|
put("name", p.name)
|
||||||
|
put("temperature", p.temperature)
|
||||||
|
put("top_p", p.topP)
|
||||||
|
put("top_k", p.topK)
|
||||||
|
put("repeat_penalty", p.repeatPenalty)
|
||||||
|
put("presence_penalty", p.presencePenalty)
|
||||||
|
put("frequency_penalty", p.frequencyPenalty)
|
||||||
|
put("max_tokens", p.maxTokens)
|
||||||
})
|
})
|
||||||
root.put("thinker", JSONObject().apply {
|
}
|
||||||
put("model_id", cfg.thinker.modelId)
|
|
||||||
put("system_prompt", cfg.thinker.systemPrompt)
|
|
||||||
put("temperature", cfg.thinker.temperature)
|
|
||||||
})
|
})
|
||||||
return root.toString(2)
|
return root.toString(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun modelToJson(m: ModelConfig) = JSONObject().apply {
|
||||||
|
put("model_id", m.modelId)
|
||||||
|
put("system_prompt", m.systemPrompt)
|
||||||
|
put("temperature", m.temperature)
|
||||||
|
put("top_p", m.topP)
|
||||||
|
put("top_k", m.topK)
|
||||||
|
put("repeat_penalty", m.repeatPenalty)
|
||||||
|
put("presence_penalty", m.presencePenalty)
|
||||||
|
put("frequency_penalty", m.frequencyPenalty)
|
||||||
|
put("max_tokens", m.maxTokens)
|
||||||
|
put("preset_name", m.presetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modelFromJson(js: JSONObject, d: ModelConfig) = ModelConfig(
|
||||||
|
modelId = js.optString("model_id", d.modelId),
|
||||||
|
systemPrompt = js.optString("system_prompt", d.systemPrompt),
|
||||||
|
temperature = js.optDouble("temperature", d.temperature.toDouble()).toFloat(),
|
||||||
|
topP = js.optDouble("top_p", d.topP.toDouble()).toFloat(),
|
||||||
|
topK = js.optInt("top_k", d.topK),
|
||||||
|
repeatPenalty = js.optDouble("repeat_penalty", d.repeatPenalty.toDouble()).toFloat(),
|
||||||
|
presencePenalty = js.optDouble("presence_penalty", d.presencePenalty.toDouble()).toFloat(),
|
||||||
|
frequencyPenalty = js.optDouble("frequency_penalty", d.frequencyPenalty.toDouble()).toFloat(),
|
||||||
|
maxTokens = js.optInt("max_tokens", d.maxTokens),
|
||||||
|
presetName = js.optString("preset_name", d.presetName)
|
||||||
|
)
|
||||||
|
|
||||||
private fun fromJson(text: String): RuntimeConfig {
|
private fun fromJson(text: String): RuntimeConfig {
|
||||||
val root = JSONObject(text)
|
val root = JSONObject(text)
|
||||||
val defaults = defaultConfig()
|
val defaults = defaultConfig()
|
||||||
|
|
@ -204,20 +275,24 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
ragThreshold = root.optDouble("rag_threshold", defaults.ragThreshold.toDouble()).toFloat(),
|
ragThreshold = root.optDouble("rag_threshold", defaults.ragThreshold.toDouble()).toFloat(),
|
||||||
ragTopK = root.optInt("rag_top_k", defaults.ragTopK),
|
ragTopK = root.optInt("rag_top_k", defaults.ragTopK),
|
||||||
debugEnabled = root.optBoolean("debug_enabled", defaults.debugEnabled),
|
debugEnabled = root.optBoolean("debug_enabled", defaults.debugEnabled),
|
||||||
speaker = root.optJSONObject("speaker")?.let { js ->
|
speaker = root.optJSONObject("speaker")?.let { modelFromJson(it, defaults.speaker) } ?: defaults.speaker,
|
||||||
ModelConfig(
|
thinker = root.optJSONObject("thinker")?.let { modelFromJson(it, defaults.thinker) } ?: defaults.thinker,
|
||||||
modelId = js.optString("model_id", defaults.speaker.modelId),
|
presets = root.optJSONArray("presets")?.let { arr ->
|
||||||
systemPrompt = js.optString("system_prompt", defaults.speaker.systemPrompt),
|
(0 until arr.length()).mapNotNull { i ->
|
||||||
temperature = js.optDouble("temperature", defaults.speaker.temperature.toDouble()).toFloat()
|
arr.optJSONObject(i)?.let { js ->
|
||||||
|
SamplingPreset(
|
||||||
|
name = js.optString("name", ""),
|
||||||
|
temperature = js.optDouble("temperature", 0.7).toFloat(),
|
||||||
|
topP = js.optDouble("top_p", 0.85).toFloat(),
|
||||||
|
topK = js.optInt("top_k", 40),
|
||||||
|
repeatPenalty = js.optDouble("repeat_penalty", 1.1).toFloat(),
|
||||||
|
presencePenalty = js.optDouble("presence_penalty", 0.0).toFloat(),
|
||||||
|
frequencyPenalty = js.optDouble("frequency_penalty", 0.0).toFloat(),
|
||||||
|
maxTokens = js.optInt("max_tokens", 120)
|
||||||
)
|
)
|
||||||
} ?: defaults.speaker,
|
}
|
||||||
thinker = root.optJSONObject("thinker")?.let { js ->
|
}.filter { it.name.isNotBlank() }
|
||||||
ModelConfig(
|
} ?: defaults.presets
|
||||||
modelId = js.optString("model_id", defaults.thinker.modelId),
|
|
||||||
systemPrompt = js.optString("system_prompt", defaults.thinker.systemPrompt),
|
|
||||||
temperature = js.optDouble("temperature", defaults.thinker.temperature.toDouble()).toFloat()
|
|
||||||
)
|
|
||||||
} ?: defaults.thinker
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,12 @@ data class SamplingParams(
|
||||||
val temperature: Float = 0.7f,
|
val temperature: Float = 0.7f,
|
||||||
val topP: Float = 0.85f,
|
val topP: Float = 0.85f,
|
||||||
val topK: Int = 40,
|
val topK: Int = 40,
|
||||||
val repetitionPenalty: Float = 1.2f
|
val repetitionPenalty: Float = 1.2f,
|
||||||
|
// Pénalités OpenAI-style (présence/fréquence). Réglables via les presets admin.
|
||||||
|
// ⚠ effectives seulement quand le moteur natif expose le sampling (cf. docs
|
||||||
|
// SAMPLING_ENGINE_SPEC) ; aujourd'hui le JNI n'honore que maxNewTokens.
|
||||||
|
val presencePenalty: Float = 0.0f,
|
||||||
|
val frequencyPenalty: Float = 0.0f
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GenerationResult(
|
data class GenerationResult(
|
||||||
|
|
|
||||||
|
|
@ -1742,9 +1742,7 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
|
||||||
val result = tE.generateWithSystem(
|
val result = tE.generateWithSystem(
|
||||||
prompt = "Message du patient : $patientMessage",
|
prompt = "Message du patient : $patientMessage",
|
||||||
systemPromptOverride = cfg.thinker.systemPrompt,
|
systemPromptOverride = cfg.thinker.systemPrompt,
|
||||||
params = com.kazeia.core.SamplingParams(
|
params = samplingFrom(cfg.thinker),
|
||||||
maxNewTokens = 128, temperature = cfg.thinker.temperature
|
|
||||||
),
|
|
||||||
onToken = null,
|
onToken = null,
|
||||||
tag = "THINKER"
|
tag = "THINKER"
|
||||||
)
|
)
|
||||||
|
|
@ -1947,10 +1945,9 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
|
||||||
}
|
}
|
||||||
!stoppingCriteria.shouldStop(responseBuilder.toString())
|
!stoppingCriteria.shouldStop(responseBuilder.toString())
|
||||||
}
|
}
|
||||||
val sParams = SamplingParams(
|
// Sampling Speaker depuis la config (preset admin). maxTokens honoré par
|
||||||
maxNewTokens = 450,
|
// le moteur ; les autres knobs prendront effet avec l'API sampling natif.
|
||||||
temperature = conversationManager.currentTemperature()
|
val sParams = samplingFrom(cfg.speaker)
|
||||||
)
|
|
||||||
// generateWithSystem propage le prompt système Speaker par tour (bullets
|
// generateWithSystem propage le prompt système Speaker par tour (bullets
|
||||||
// cascade ou hot-reload admin). UnifiedLlmAdapter le gère pour GGUF
|
// cascade ou hot-reload admin). UnifiedLlmAdapter le gère pour GGUF
|
||||||
// (mono-tour si override) comme pour .pte.
|
// (mono-tour si override) comme pour .pte.
|
||||||
|
|
@ -2171,6 +2168,20 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
|
||||||
* libération immédiate.
|
* libération immédiate.
|
||||||
* - Thinker model_id changé en cascade ON → release, sera lazy-loadé.
|
* - Thinker model_id changé en cascade ON → release, sera lazy-loadé.
|
||||||
*/
|
*/
|
||||||
|
/** ModelConfig (preset admin) → SamplingParams pour la génération. ⚠ seul
|
||||||
|
* maxNewTokens est honoré par le moteur natif actuel ; le reste est prêt et
|
||||||
|
* prendra effet dès que le JNI expose le sampling (docs/SAMPLING_ENGINE_SPEC). */
|
||||||
|
private fun samplingFrom(m: com.kazeia.config.ConfigStore.ModelConfig) =
|
||||||
|
com.kazeia.core.SamplingParams(
|
||||||
|
maxNewTokens = m.maxTokens,
|
||||||
|
temperature = m.temperature,
|
||||||
|
topP = m.topP,
|
||||||
|
topK = m.topK,
|
||||||
|
repetitionPenalty = m.repeatPenalty,
|
||||||
|
presencePenalty = m.presencePenalty,
|
||||||
|
frequencyPenalty = m.frequencyPenalty
|
||||||
|
)
|
||||||
|
|
||||||
private fun handleConfigChange(new: com.kazeia.config.ConfigStore.RuntimeConfig) {
|
private fun handleConfigChange(new: com.kazeia.config.ConfigStore.RuntimeConfig) {
|
||||||
val old = runtimeConfig
|
val old = runtimeConfig
|
||||||
runtimeConfig = new
|
runtimeConfig = new
|
||||||
|
|
|
||||||
|
|
@ -258,9 +258,17 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
"stt_engine", "llm_engine", "tts_engine", "rag_enabled",
|
"stt_engine", "llm_engine", "tts_engine", "rag_enabled",
|
||||||
"rag_threshold", "rag_top_k", "debug_enabled",
|
"rag_threshold", "rag_top_k", "debug_enabled",
|
||||||
"speaker_model_id", "speaker_system_prompt", "speaker_temperature",
|
"speaker_model_id", "speaker_system_prompt", "speaker_temperature",
|
||||||
"thinker_model_id", "thinker_system_prompt", "thinker_temperature"
|
"speaker_top_p", "speaker_top_k", "speaker_repeat_penalty",
|
||||||
|
"speaker_presence_penalty", "speaker_frequency_penalty",
|
||||||
|
"speaker_max_tokens", "speaker_preset_name",
|
||||||
|
"thinker_model_id", "thinker_system_prompt", "thinker_temperature",
|
||||||
|
"thinker_top_p", "thinker_top_k", "thinker_repeat_penalty",
|
||||||
|
"thinker_presence_penalty", "thinker_frequency_penalty",
|
||||||
|
"thinker_max_tokens", "thinker_preset_name",
|
||||||
|
"presets_json"
|
||||||
)
|
)
|
||||||
val cursor = MatrixCursor(cols)
|
val cursor = MatrixCursor(cols)
|
||||||
|
val s = cfg.speaker; val t = cfg.thinker
|
||||||
cursor.addRow(arrayOf(
|
cursor.addRow(arrayOf(
|
||||||
if (cfg.cascadeEnabled) 1 else 0,
|
if (cfg.cascadeEnabled) 1 else 0,
|
||||||
if (cfg.ttsEnabled) 1 else 0,
|
if (cfg.ttsEnabled) 1 else 0,
|
||||||
|
|
@ -268,12 +276,31 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
if (cfg.ragEnabled) 1 else 0,
|
if (cfg.ragEnabled) 1 else 0,
|
||||||
cfg.ragThreshold, cfg.ragTopK,
|
cfg.ragThreshold, cfg.ragTopK,
|
||||||
if (cfg.debugEnabled) 1 else 0,
|
if (cfg.debugEnabled) 1 else 0,
|
||||||
cfg.speaker.modelId, cfg.speaker.systemPrompt, cfg.speaker.temperature,
|
s.modelId, s.systemPrompt, s.temperature,
|
||||||
cfg.thinker.modelId, cfg.thinker.systemPrompt, cfg.thinker.temperature
|
s.topP, s.topK, s.repeatPenalty, s.presencePenalty, s.frequencyPenalty, s.maxTokens, s.presetName,
|
||||||
|
t.modelId, t.systemPrompt, t.temperature,
|
||||||
|
t.topP, t.topK, t.repeatPenalty, t.presencePenalty, t.frequencyPenalty, t.maxTokens, t.presetName,
|
||||||
|
presetsToJson(cfg.presets)
|
||||||
))
|
))
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sérialise la bibliothèque de presets en JSON (1 colonne `presets_json`). */
|
||||||
|
private fun presetsToJson(presets: List<com.kazeia.config.ConfigStore.SamplingPreset>): String {
|
||||||
|
val arr = org.json.JSONArray()
|
||||||
|
presets.forEach { p ->
|
||||||
|
arr.put(org.json.JSONObject().apply {
|
||||||
|
put("name", p.name); put("temperature", p.temperature)
|
||||||
|
put("top_p", p.topP); put("top_k", p.topK)
|
||||||
|
put("repeat_penalty", p.repeatPenalty)
|
||||||
|
put("presence_penalty", p.presencePenalty)
|
||||||
|
put("frequency_penalty", p.frequencyPenalty)
|
||||||
|
put("max_tokens", p.maxTokens)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return arr.toString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun modelsCursor(): Cursor {
|
private fun modelsCursor(): Cursor {
|
||||||
val cols = arrayOf(
|
val cols = arrayOf(
|
||||||
"id", "display_name", "pte_path", "tokenizer_path",
|
"id", "display_name", "pte_path", "tokenizer_path",
|
||||||
|
|
@ -364,20 +391,9 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
ragTopK = values.getAsInteger("rag_top_k") ?: cur.ragTopK,
|
ragTopK = values.getAsInteger("rag_top_k") ?: cur.ragTopK,
|
||||||
debugEnabled = if (values.containsKey("debug_enabled"))
|
debugEnabled = if (values.containsKey("debug_enabled"))
|
||||||
values.getAsBoolean("debug_enabled") else cur.debugEnabled,
|
values.getAsBoolean("debug_enabled") else cur.debugEnabled,
|
||||||
speaker = com.kazeia.config.ConfigStore.ModelConfig(
|
speaker = mergeModel(values, "speaker_", cur.speaker),
|
||||||
modelId = values.getAsString("speaker_model_id") ?: cur.speaker.modelId,
|
thinker = mergeModel(values, "thinker_", cur.thinker),
|
||||||
systemPrompt = values.getAsString("speaker_system_prompt")
|
presets = values.getAsString("presets_json")?.let { presetsFromJson(it) } ?: cur.presets
|
||||||
?: cur.speaker.systemPrompt,
|
|
||||||
temperature = values.getAsFloat("speaker_temperature")
|
|
||||||
?: cur.speaker.temperature
|
|
||||||
),
|
|
||||||
thinker = com.kazeia.config.ConfigStore.ModelConfig(
|
|
||||||
modelId = values.getAsString("thinker_model_id") ?: cur.thinker.modelId,
|
|
||||||
systemPrompt = values.getAsString("thinker_system_prompt")
|
|
||||||
?: cur.thinker.systemPrompt,
|
|
||||||
temperature = values.getAsFloat("thinker_temperature")
|
|
||||||
?: cur.thinker.temperature
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
store.save(newCfg)
|
store.save(newCfg)
|
||||||
ctx.contentResolver.notifyChange(uri, null)
|
ctx.contentResolver.notifyChange(uri, null)
|
||||||
|
|
@ -385,6 +401,42 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fusionne les colonnes `<prefix>*` d'un ContentValues dans un ModelConfig
|
||||||
|
* (merge partiel : un champ absent reste inchangé). */
|
||||||
|
private fun mergeModel(
|
||||||
|
v: ContentValues, p: String, cur: com.kazeia.config.ConfigStore.ModelConfig
|
||||||
|
) = com.kazeia.config.ConfigStore.ModelConfig(
|
||||||
|
modelId = v.getAsString("${p}model_id") ?: cur.modelId,
|
||||||
|
systemPrompt = v.getAsString("${p}system_prompt") ?: cur.systemPrompt,
|
||||||
|
temperature = v.getAsFloat("${p}temperature") ?: cur.temperature,
|
||||||
|
topP = v.getAsFloat("${p}top_p") ?: cur.topP,
|
||||||
|
topK = v.getAsInteger("${p}top_k") ?: cur.topK,
|
||||||
|
repeatPenalty = v.getAsFloat("${p}repeat_penalty") ?: cur.repeatPenalty,
|
||||||
|
presencePenalty = v.getAsFloat("${p}presence_penalty") ?: cur.presencePenalty,
|
||||||
|
frequencyPenalty = v.getAsFloat("${p}frequency_penalty") ?: cur.frequencyPenalty,
|
||||||
|
maxTokens = v.getAsInteger("${p}max_tokens") ?: cur.maxTokens,
|
||||||
|
presetName = v.getAsString("${p}preset_name") ?: cur.presetName
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun presetsFromJson(text: String): List<com.kazeia.config.ConfigStore.SamplingPreset> =
|
||||||
|
runCatching {
|
||||||
|
val arr = org.json.JSONArray(text)
|
||||||
|
(0 until arr.length()).mapNotNull { i ->
|
||||||
|
arr.optJSONObject(i)?.let { js ->
|
||||||
|
com.kazeia.config.ConfigStore.SamplingPreset(
|
||||||
|
name = js.optString("name", ""),
|
||||||
|
temperature = js.optDouble("temperature", 0.7).toFloat(),
|
||||||
|
topP = js.optDouble("top_p", 0.85).toFloat(),
|
||||||
|
topK = js.optInt("top_k", 40),
|
||||||
|
repeatPenalty = js.optDouble("repeat_penalty", 1.1).toFloat(),
|
||||||
|
presencePenalty = js.optDouble("presence_penalty", 0.0).toFloat(),
|
||||||
|
frequencyPenalty = js.optDouble("frequency_penalty", 0.0).toFloat(),
|
||||||
|
maxTokens = js.optInt("max_tokens", 120)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.filter { it.name.isNotBlank() }
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
private fun upsertProfile(values: ContentValues): Int {
|
private fun upsertProfile(values: ContentValues): Int {
|
||||||
val ctx = context ?: return 0
|
val ctx = context ?: return 0
|
||||||
val store = com.kazeia.profiles.ProfileStore.get(ctx)
|
val store = com.kazeia.profiles.ProfileStore.get(ctx)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
# Spec — API d'échantillonnage natif (kazeia-engine) pour les presets Kazeia
|
||||||
|
|
||||||
|
**Date** : 2026-06-18
|
||||||
|
**Demandeur** : app Kazeia (refonte presets admin)
|
||||||
|
**Cible** : `kazeia-engine` (lib `libkazeia_engine`, JNI `EngineJni`)
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
L'app admin permet désormais de régler, par modèle (Speaker / Thinker) et via des
|
||||||
|
**presets** nommés, les paramètres d'échantillonnage : `temperature`, `top_p`,
|
||||||
|
`top_k`, `repeat_penalty`, `presence_penalty`, `frequency_penalty`, `max_tokens`.
|
||||||
|
|
||||||
|
Côté app, ces valeurs sont **persistées et transmises** jusqu'au pont JNI
|
||||||
|
(`UnifiedLlmAdapter` → `EngineLlmEngine`/`LlmSession`). **MAIS le JNI actuel
|
||||||
|
n'expose que `maxTok`** :
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
external fun generate(h: Long, sys: String, usr: String, maxTok: Int): String
|
||||||
|
external fun sessionAsk(h: Long, usr: String, maxTok: Int, cb: TokenCallback)
|
||||||
|
external fun generateStream(h: Long, sys: String, usr: String, maxTok: Int, cb: TokenCallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
Donc aujourd'hui **seul `max_tokens` agit** ; température/top-p/k/pénalités sont
|
||||||
|
inertes (l'app l'indique honnêtement dans l'UI : « effectif après MAJ moteur »).
|
||||||
|
|
||||||
|
## Demande — exposer le sampling au JNI
|
||||||
|
|
||||||
|
Ajouter une **variante échantillonnée** des entrées de génération (sans casser les
|
||||||
|
signatures existantes), qui prend une struct de sampling et la câble sur la chaîne
|
||||||
|
de samplers llama.cpp. Le GGUF Qwen3.5-4B (Speaker par défaut) est la cible
|
||||||
|
prioritaire.
|
||||||
|
|
||||||
|
### Signatures proposées
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Struct portée côté natif (ou 7 args primitifs si plus simple en JNI) :
|
||||||
|
// temperature: Float, topP: Float, topK: Int,
|
||||||
|
// repeatPenalty: Float, presencePenalty: Float, frequencyPenalty: Float, maxTokens: Int
|
||||||
|
external fun generateSampled(
|
||||||
|
h: Long, sys: String, usr: String,
|
||||||
|
maxTokens: Int, temperature: Float, topP: Float, topK: Int,
|
||||||
|
repeatPenalty: Float, presencePenalty: Float, frequencyPenalty: Float,
|
||||||
|
cb: TokenCallback
|
||||||
|
)
|
||||||
|
external fun sessionAskSampled(
|
||||||
|
h: Long, usr: String,
|
||||||
|
maxTokens: Int, temperature: Float, topP: Float, topK: Int,
|
||||||
|
repeatPenalty: Float, presencePenalty: Float, frequencyPenalty: Float,
|
||||||
|
cb: TokenCallback
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mapping llama.cpp (sampler chain)
|
||||||
|
|
||||||
|
Construire la chaîne par requête (ou réutiliser un `llama_sampler` reconfiguré) :
|
||||||
|
|
||||||
|
```c
|
||||||
|
auto * chain = llama_sampler_chain_init(llama_sampler_chain_default_params());
|
||||||
|
llama_sampler_chain_add(chain, llama_sampler_init_top_k(top_k));
|
||||||
|
llama_sampler_chain_add(chain, llama_sampler_init_top_p(top_p, 1));
|
||||||
|
llama_sampler_chain_add(chain, llama_sampler_init_penalties(
|
||||||
|
/*penalty_last_n*/ 64, repeat_penalty, frequency_penalty, presence_penalty));
|
||||||
|
llama_sampler_chain_add(chain, llama_sampler_init_temp(temperature));
|
||||||
|
llama_sampler_chain_add(chain, llama_sampler_init_dist(/*seed*/ LLAMA_DEFAULT_SEED));
|
||||||
|
```
|
||||||
|
|
||||||
|
- `temperature <= 0` ⇒ greedy (`llama_sampler_init_greedy`), ignorer les autres.
|
||||||
|
- Conserver le **thinking-off** et le template ChatML actuels inchangés.
|
||||||
|
- `presence/frequency_penalty = 0` ⇒ no-op (comportement neutre).
|
||||||
|
|
||||||
|
### Comportement attendu
|
||||||
|
|
||||||
|
- Valeurs de référence (preset « Kaz chaleureux ») : temp 0.4, top_p 0.85, top_k 40,
|
||||||
|
repeat 1.05, presence 0.2, frequency 0.2, max 256 — doit produire des sorties
|
||||||
|
visiblement moins répétitives / plus chaleureuses que le greedy actuel.
|
||||||
|
- Déterminisme : exposer un `seed` optionnel plus tard si besoin (pas requis v1).
|
||||||
|
|
||||||
|
### `.pte` (NPU) — hors scope v1
|
||||||
|
|
||||||
|
Le runner ExecuTorch `.pte` décode **greedy**. Le sampling y est une évolution
|
||||||
|
distincte (échantillonnage post-logits côté runner). Pour l'instant les presets
|
||||||
|
n'affectent que le chemin GGUF ; documenter la limite suffit.
|
||||||
|
|
||||||
|
## Côté app — déjà prêt
|
||||||
|
|
||||||
|
- `SamplingParams` (com.kazeia.core) porte les 7 knobs.
|
||||||
|
- `UnifiedLlmAdapter.generateWithSystem` reçoit les params ; il suffira de router
|
||||||
|
vers `*Sampled` au lieu de `ask(max)` / `generateStream(max)` quand le JNI existe.
|
||||||
|
- `ConfigStore.ModelConfig` + presets persistent les valeurs ; l'admin les édite.
|
||||||
|
|
||||||
|
Quand le JNI échantillonné est livré : ~10 lignes à changer côté `UnifiedLlmAdapter`
|
||||||
|
(brancher `sessionAskSampled` / `generateSampled`) et retirer la mention
|
||||||
|
« effectif après MAJ moteur » de l'UI admin.
|
||||||
Loading…
Reference in New Issue