From 0e73ad6e18fead8313c06dc3a17932e9d54ba7e5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 3 May 2026 00:46:05 +0100 Subject: [PATCH] Split social-read model from local reply lane --- .env.example | 1 + README.md | 2 ++ apps/bot/src/discord-bot-handlers.ts | 4 +-- apps/bot/src/discord-bot.ts | 2 +- packages/config/src/index.ts | 4 +++ scripts/check-voidbot-operations.ps1 | 38 ++++++++++++++++++++++++++++ scripts/start-voidbot-stack.ps1 | 13 ++++++++++ state/evidence.jsonl | 1 + 8 files changed, 62 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 0697c4b..69984ae 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,7 @@ SOURCE_REPO_PATTERNS=* SOURCE_REPO_INCLUDE_PREFIXES=AetheriaLore:Aetheria/ LOCAL_LLM_OLLAMA_BASE_URL=http://127.0.0.1:11434 LOCAL_LLM_OLLAMA_MODEL=qwen3.5:9b +LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL= LOCAL_LLM_OLLAMA_TIMEOUT_MS=120000 LOCAL_LLM_OLLAMA_KEEP_ALIVE=10m LOCAL_LLM_OLLAMA_THINK=low diff --git a/README.md b/README.md index f4aca2c..0f5a15d 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ If you want the remote Ollama generation path too, configure: - `LOCAL_LLM_OLLAMA_BASE_URL` - `LOCAL_LLM_OLLAMA_MODEL` +- `LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL` - `LOCAL_LLM_OLLAMA_TIMEOUT_MS` - `LOCAL_LLM_OLLAMA_KEEP_ALIVE` - `LOCAL_LLM_OLLAMA_THINK` @@ -86,6 +87,7 @@ If you want the remote Ollama generation path too, configure: `LOCAL_LLM_ALLOW_PUBLIC=false` is the sane default. It keeps the local model restricted to admins and the configured owner while you test. `LOCAL_LLM_OLLAMA_THINK=low` is a good default if you want better phrasing and a little more initiative without dragging reply time into the swamp. +`LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL` is optional. Leave it blank to reuse the main local reply model, or set it when you want the room-reading sidecar on a different model than the public/local response lane. If you want a private local persona instead, point `STYLE_PACK_PATH` and `SYSTEM_MESSAGES_PATH` at your own ignored local files. diff --git a/apps/bot/src/discord-bot-handlers.ts b/apps/bot/src/discord-bot-handlers.ts index 71aa99e..4ecaf7e 100644 --- a/apps/bot/src/discord-bot-handlers.ts +++ b/apps/bot/src/discord-bot-handlers.ts @@ -762,7 +762,7 @@ async function inferSituationalSocialRead( channelName: options.guildContext.channelName ?? null, promptExcerpt: truncate(options.prompt, 280), recentMessageCount: input.recentMessages.length, - model: options.config.localLlm.ollamaModel, + model: options.config.localLlm.socialReadOllamaModel, summary: read.summary, roomTone: read.roomTone, speakerCurrentRead: read.speakerCurrentRead, @@ -784,7 +784,7 @@ async function inferSituationalSocialRead( channelId: options.guildContext.channelId, channelName: options.guildContext.channelName ?? null, promptExcerpt: truncate(options.prompt, 280), - model: options.config.localLlm.ollamaModel, + model: options.config.localLlm.socialReadOllamaModel, errorMessage: error instanceof Error ? error.message : "Unexpected situational social read failure.", }, diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index 8b5f6b6..dc7b448 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -150,7 +150,7 @@ export async function startBot(): Promise { const contextBuilder = new ContextBuilder(); const situationalSocialReadInferer = new OllamaSituationalSocialReadInferer({ ollamaBaseUrl: config.localLlm.ollamaBaseUrl, - ollamaModel: config.localLlm.ollamaModel, + ollamaModel: config.localLlm.socialReadOllamaModel, ollamaTimeoutMs: config.localLlm.ollamaTimeoutMs, ollamaKeepAlive: config.localLlm.ollamaKeepAlive, ollamaNumCtx: config.localLlm.ollamaNumCtx, diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index d96b295..3a71d76 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -76,6 +76,7 @@ const envSchema = z.object({ SOURCE_REPO_INCLUDE_PREFIXES: z.string().default("AetheriaLore:Aetheria/"), LOCAL_LLM_OLLAMA_BASE_URL: z.string().url().default("http://127.0.0.1:11434"), LOCAL_LLM_OLLAMA_MODEL: z.string().min(1).default("qwen3.5:9b"), + LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL: optionalNonEmptyString, LOCAL_LLM_OLLAMA_TIMEOUT_MS: z.coerce.number().int().positive().default(120000), LOCAL_LLM_OLLAMA_KEEP_ALIVE: z.string().min(1).default("10m"), LOCAL_LLM_OLLAMA_THINK: z.string().default("false"), @@ -151,6 +152,7 @@ export interface AppConfig { localLlm: { ollamaBaseUrl: string; ollamaModel: string; + socialReadOllamaModel: string; ollamaTimeoutMs: number; ollamaKeepAlive: string; ollamaThink: OllamaThinkMode; @@ -326,6 +328,8 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { localLlm: { ollamaBaseUrl: parsed.LOCAL_LLM_OLLAMA_BASE_URL, ollamaModel: parsed.LOCAL_LLM_OLLAMA_MODEL, + socialReadOllamaModel: + parsed.LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL ?? parsed.LOCAL_LLM_OLLAMA_MODEL, ollamaTimeoutMs: parsed.LOCAL_LLM_OLLAMA_TIMEOUT_MS, ollamaKeepAlive: parsed.LOCAL_LLM_OLLAMA_KEEP_ALIVE, ollamaThink: parseOllamaThinkMode(parsed.LOCAL_LLM_OLLAMA_THINK), diff --git a/scripts/check-voidbot-operations.ps1 b/scripts/check-voidbot-operations.ps1 index 2acc225..44bb620 100644 --- a/scripts/check-voidbot-operations.ps1 +++ b/scripts/check-voidbot-operations.ps1 @@ -822,6 +822,14 @@ try { } $localLlmUrl = if ($config.ContainsKey("LOCAL_LLM_OLLAMA_BASE_URL")) { $config["LOCAL_LLM_OLLAMA_BASE_URL"] } else { "" } $localLlmModel = if ($config.ContainsKey("LOCAL_LLM_OLLAMA_MODEL")) { $config["LOCAL_LLM_OLLAMA_MODEL"] } else { "" } + $localLlmSocialReadModel = if ( + $config.ContainsKey("LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL") -and + -not [string]::IsNullOrWhiteSpace($config["LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL"]) + ) { + $config["LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL"] + } else { + $localLlmModel + } $localLlmEnabled = ($enabledProviders -contains "local_llm") -or ( $config.ContainsKey("LOCAL_LLM_ALLOW_PUBLIC") -and [string]$config["LOCAL_LLM_ALLOW_PUBLIC"] -eq "true" ) @@ -842,16 +850,46 @@ try { models = $localLlmModels } } + + if ([string]::IsNullOrWhiteSpace($localLlmSocialReadModel)) { + Add-Check -Name "local_llm.social_read_model" -Status "warning" -Detail "Social-read sidecar model is blank, so it will fall back to the main local LLM model." -Data @{ + url = $localLlmUrl + model = $localLlmModel + } + } elseif ($localLlmModels -contains $localLlmSocialReadModel) { + $socialReadDetail = if ($localLlmSocialReadModel -eq $localLlmModel) { + "Social-read sidecar reuses the main local LLM model $localLlmSocialReadModel." + } else { + "Social-read sidecar is reachable and has $localLlmSocialReadModel." + } + + Add-Check -Name "local_llm.social_read_model" -Status "passed" -Detail $socialReadDetail -Data @{ + url = $localLlmUrl + model = $localLlmSocialReadModel + } + } else { + Add-Check -Name "local_llm.social_read_model" -Status "failed" -Detail "Local LLM Ollama is reachable but missing social-read model $localLlmSocialReadModel." -Data @{ + url = $localLlmUrl + models = $localLlmModels + } + } } catch { Add-Check -Name "local_llm.model" -Status "failed" -Detail "Local LLM Ollama check failed: $($_.Exception.Message)" -Data @{ url = $localLlmUrl model = $localLlmModel } + Add-Check -Name "local_llm.social_read_model" -Status "failed" -Detail "Social-read Ollama check failed: $($_.Exception.Message)" -Data @{ + url = $localLlmUrl + model = $localLlmSocialReadModel + } } } else { Add-Check -Name "local_llm.model" -Status "passed" -Detail "Local LLM check skipped because the provider is not enabled for this runtime." -Data @{ enabledProviders = $enabledProviders } + Add-Check -Name "local_llm.social_read_model" -Status "passed" -Detail "Social-read model check skipped because the local LLM provider is not enabled for this runtime." -Data @{ + enabledProviders = $enabledProviders + } } $sourceRepoRoot = if ($config.ContainsKey("SOURCE_REPO_ROOT")) { $config["SOURCE_REPO_ROOT"] } else { "" } diff --git a/scripts/start-voidbot-stack.ps1 b/scripts/start-voidbot-stack.ps1 index fcd3068..f4c9307 100644 --- a/scripts/start-voidbot-stack.ps1 +++ b/scripts/start-voidbot-stack.ps1 @@ -478,6 +478,7 @@ $ragOllamaModel = if ($config.ContainsKey("RAG_OLLAMA_MODEL")) { $config["RAG_OL $localLlmEnabled = if ($config.ContainsKey("ENABLED_PROVIDERS")) { $config["ENABLED_PROVIDERS"] -like "*local_llm*" } else { $false } $localLlmBaseUrl = if ($config.ContainsKey("LOCAL_LLM_OLLAMA_BASE_URL")) { $config["LOCAL_LLM_OLLAMA_BASE_URL"] } else { "http://127.0.0.1:11434" } $localLlmModel = if ($config.ContainsKey("LOCAL_LLM_OLLAMA_MODEL")) { $config["LOCAL_LLM_OLLAMA_MODEL"] } else { "qwen3.5:9b" } +$localLlmSocialReadModel = if ($config.ContainsKey("LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL") -and -not [string]::IsNullOrWhiteSpace($config["LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL"])) { $config["LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL"] } else { $localLlmModel } if ($stateStorageBackend -eq "postgres") { $status.stage = "postgres" @@ -514,6 +515,18 @@ if ($localLlmEnabled) { $status.stage = "local_llm" Write-StatusFile -Path $statusPath -Status $status $status.localLlmOllama = Ensure-OllamaEndpoint -BaseUrl $localLlmBaseUrl -Model $localLlmModel -Label "Local LLM Ollama" -LogDir $logDir + + if ($localLlmSocialReadModel -eq $localLlmModel) { + $status.localLlmSocialReadOllama = @{ + url = $localLlmBaseUrl + model = $localLlmSocialReadModel + healthy = $true + mirrorsLocalLlm = $true + } + } else { + $status.localLlmSocialReadOllama = Ensure-OllamaEndpoint -BaseUrl $localLlmBaseUrl -Model $localLlmSocialReadModel -Label "Local LLM social-read Ollama" -LogDir $logDir + } + Write-StatusFile -Path $statusPath -Status $status } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 3d03920..e18cf69 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -32,3 +32,4 @@ {"ts":"2026-05-02T22:16:00+01:00","type":"architecture","status":"ok","note":"Ollama sidecar calls are more reliable under actual JSON-schema structured outputs than bare JSON mode. The situational reader now uses schema-constrained `format` payloads with deterministic temperature and can carry room-read fields plus pronoun evidence in a single no-thinking pass."} {"ts":"2026-05-02T21:52:56Z","type":"implementation","status":"ok","note":"Situational sidecar now emits transcript-wide Ghostlight-style participant reads with durable binary label families plus ephemeral situational state; roster-constrained schema and full configured Ollama timeout are needed to keep multi-person reads viable on a busy remote box."} {"ts":"2026-05-02T22:58:31Z","type":"research","status":"ok","note":"Gemma3 12B Q4_0 on the 4060 Ti box handles the transcript-wide social-read task once the schema sticks to a simpler subset; Ollama chokes on roster-constrained prefixItems/const schema but both Gemma and Qwen work with local roster reconciliation, and Gemma slightly beats Qwen on latency for the tested slice."} +{"ts":"2026-05-02T23:40:40Z","type":"implementation","status":"ok","note":"Split room-read sidecar model selection from the main local reply model so Gemma can drive transcript profiling without changing the public/local answer lane."}