From cd38cbf19db8c1ef80c635cc9926daf1078e2e5e Mon Sep 17 00:00:00 2001 From: zukki259 Date: Sun, 23 Aug 2026 22:10:36 +0900 Subject: [PATCH] Set the search summary model from the settings panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summarizing web search results falls back to DEFAULT_MODEL, so a local Ollama model ends up doing it and the call can fail — which surfaces only as "保存できる要約なし" in the chat. Make SEARCH_SUMMARY_MODEL settable at runtime: add it to the settings allowlist and read it from os.environ on each call (knowledge._summary_model), the way LOCAL_KB_EMBEDDING_MODEL already works. Importing it as a config constant would keep the old value until a restart. Show the current value of the settings that are not secrets. A model name rendered as ••••qwen3 gives no way to tell what is in effect, so the store now marks those keys (NON_SECRET_KEYS) and returns them unmasked; the API field is renamed masked -> display_value and carries a secret flag that the panel uses to pick the input type, replacing the list it kept on its own side. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 +- backend/app/api/settings.py | 5 ++++- backend/app/core/config.py | 1 + backend/app/core/memory_kb/knowledge.py | 12 ++++++++++-- backend/app/core/settings_store.py | 15 ++++++++++++++- docs/features.md | 8 +++++--- frontend/src/api/client.ts | 5 ++++- frontend/src/components/SettingsPanel.vue | 8 +++----- 8 files changed, 42 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0b1d2f3..3455734 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,7 +137,7 @@ LLM に渡すコンテキストは `app/core/memory_kb/memory.py` の `get_conte | `MEMORY_SUMMARIZE_THRESHOLD` | `20` | 長期メモリ化するメッセージ数の閾値 | | `KNOWLEDGE_CONTEXT_LIMIT` | `15` | 蓄積知識をシステムプロンプトに注入する最大件数 | | `TAVILY_API_KEY` | — | Tavily Web 検索の API キー(未設定なら `web_search` は空を返す) | -| `SEARCH_SUMMARY_MODEL` | — | Web 検索結果の要約に使うモデル(未設定なら `DEFAULT_MODEL`) | +| `SEARCH_SUMMARY_MODEL` | — | Web 検索結果の要約に使うモデル(未設定なら `DEFAULT_MODEL`。GUI 設定画面から実行時変更可) | | `TAGGING_MODEL` | — | メッセージの自動タグ付けに使うモデル(未設定なら `DEFAULT_MODEL`) | | `LOCAL_KB_DIR` | `knowledge_base` | ローカル Markdown ナレッジベースのディレクトリ | | `LOCAL_KB_EMBEDDING_MODEL` | `text-embedding-3-small` | ローカル KB の埋め込みモデル(GUI 設定画面から実行時変更可) | diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index f01a663..ff192d4 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -12,7 +12,10 @@ router = APIRouter(prefix="/api/settings", tags=["settings"], dependencies=[Depe class SettingEntry(BaseModel): key: str is_set: bool - masked: str | None + # 秘匿キーはマスク済みの値、それ以外は現在値そのもの。未設定なら None + display_value: str | None + # 入力欄をパスワード扱いにするか(APIキー等は True) + secret: bool overridden: bool diff --git a/backend/app/core/config.py b/backend/app/core/config.py index ab44e91..3e07e34 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -29,6 +29,7 @@ TAVILY_API_KEY: str | None = os.getenv("TAVILY_API_KEY") # 蓄積した知識をシステムプロンプトに注入する際の最大件数 KNOWLEDGE_CONTEXT_LIMIT: int = int(os.getenv("KNOWLEDGE_CONTEXT_LIMIT", "15")) # Web検索結果を構造的な要約にまとめる際に使うモデル(未設定ならDEFAULT_MODELを使用) +# GUI設定画面から実行時に変更できるため、実際の参照は memory_kb/knowledge._summary_model() が os.environ から行う。 SEARCH_SUMMARY_MODEL: str | None = os.getenv("SEARCH_SUMMARY_MODEL") # ローカルMarkdownナレッジベース(話題に応じて関連ファイルをシステムプロンプトに注入) diff --git a/backend/app/core/memory_kb/knowledge.py b/backend/app/core/memory_kb/knowledge.py index c643770..9729a5f 100644 --- a/backend/app/core/memory_kb/knowledge.py +++ b/backend/app/core/memory_kb/knowledge.py @@ -1,5 +1,6 @@ import asyncio import logging +import os import re import httpx @@ -10,7 +11,6 @@ from app.core import llm from app.core.config import ( KNOWLEDGE_CONTEXT_LIMIT, KNOWLEDGE_TOOL_CONTENT_LIMIT, - SEARCH_SUMMARY_MODEL, ) from app.core.memory_kb import local_knowledge from app.db.models import KnowledgeEntry, KnowledgeSource @@ -37,6 +37,14 @@ _SUMMARY_PROMPT = ( _HEADLINE_MARKERS = ("要約:", "要約:") +def _summary_model() -> str | None: + """GUIの設定画面から実行時に変更できるよう、起動時定数ではなく都度 os.environ を見に行く。 + + 未設定(空文字含む)なら None を返し、llm.complete() 側で DEFAULT_MODEL にフォールバックさせる。 + """ + return os.environ.get("SEARCH_SUMMARY_MODEL") or None + + def _split_headline(text: str) -> tuple[str, str]: """LLMの出力を (一行要約, 本文) に分ける。1行目が想定の形式でなければ見出しは空で返す。""" lines = text.strip().splitlines() @@ -87,7 +95,7 @@ async def summarize_results(query: str, results: list[dict], model: str | None = try: text = await llm.complete( [{"role": "user", "content": prompt}], - model=model or SEARCH_SUMMARY_MODEL, + model=model or _summary_model(), ) except Exception: logger.exception("Failed to summarize search results for query=%r", query) diff --git a/backend/app/core/settings_store.py b/backend/app/core/settings_store.py index 9feb7dc..86c1db2 100644 --- a/backend/app/core/settings_store.py +++ b/backend/app/core/settings_store.py @@ -16,8 +16,18 @@ CONFIGURABLE_KEYS = [ "OLLAMA_API_BASE", "TAVILY_API_KEY", "LOCAL_KB_EMBEDDING_MODEL", + "SEARCH_SUMMARY_MODEL", ] +# 秘匿情報ではないキー。値をマスクせずそのまま返し、GUIでも通常のテキスト入力として扱う。 +# モデル名やURLは「今どれが効いているか」が見えないと設定しづらいため。 +NON_SECRET_KEYS = { + "AWS_REGION_NAME", + "OLLAMA_API_BASE", + "LOCAL_KB_EMBEDDING_MODEL", + "SEARCH_SUMMARY_MODEL", +} + # .env由来の起動時の値のスナップショット。clear() で戻す先として使う。 _env_defaults: dict[str, str | None] = {} _snapshot_taken = False @@ -47,15 +57,18 @@ def load_into_env(db: DBSession) -> None: def list_masked(db: DBSession) -> list[dict]: + """GUI設定画面に出す一覧。秘匿キーの値はマスクし、そうでないキーは現在値をそのまま見せる。""" _ensure_snapshot() saved = {row.key: row.value for row in db.query(AppSetting).all()} result = [] for key in CONFIGURABLE_KEYS: current = os.environ.get(key) + secret = key not in NON_SECRET_KEYS result.append({ "key": key, "is_set": bool(current), - "masked": _mask(current) if current else None, + "display_value": (_mask(current) if secret else current) if current else None, + "secret": secret, "overridden": key in saved, }) return result diff --git a/docs/features.md b/docs/features.md index c725d1b..49da50b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -94,7 +94,7 @@ Discord 連携は [discord-bot.md](discord-bot.md)、ノートの操作方法は ## 5. Web 検索と知識の蓄積 - LLM が必要と判断したときだけ `web_search` ツールが走る(Tavily。`TAVILY_API_KEY` 未設定なら空振り)。 -- 結果は `SEARCH_SUMMARY_MODEL` で構造化要約にされ、**2 か所**に保存される。 +- 結果は `SEARCH_SUMMARY_MODEL`(未設定なら `DEFAULT_MODEL`。設定画面から変更可)で構造化要約にされ、**2 か所**に保存される。 - `knowledge_entries` / `knowledge_sources` テーブル → 次回以降、層 1 として全セッションに効く - `knowledge_base/web_search/*.md` → 層 3 の類似度検索の対象になる - `POST /chat/knowledge/regenerate` で、既存の知識エントリを出典 URL から取り直して要約し直せる。 @@ -159,8 +159,10 @@ Discord 連携は [discord-bot.md](discord-bot.md)、ノートの操作方法は 設定画面からラベル/モデル文字列を追加・編集・削除できる(重複は 400 で弾く)。 - 設定画面では API キー類を**再起動なしで**変更できる。 - 変更できるのは許可リストにあるキーだけ(`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / AWS 系 / - `OLLAMA_API_BASE` / `TAVILY_API_KEY` / `LOCAL_KB_EMBEDDING_MODEL`) - - 値は末尾 4 文字だけを見せるマスク表示。空欄で保存すると「変更なし」扱い + `OLLAMA_API_BASE` / `TAVILY_API_KEY` / `LOCAL_KB_EMBEDDING_MODEL` / `SEARCH_SUMMARY_MODEL`) + - API キー類は末尾 4 文字だけを見せるマスク表示。モデル名や URL(`settings_store.NON_SECRET_KEYS`)は + 現在値をそのまま表示し、入力欄も伏せ字にしない(今どれが効いているか分からないと設定しづらいため) + - 空欄で保存すると「変更なし」扱い - 保存すると `app_settings` テーブルに入り `os.environ` に反映される。削除すると `.env` 由来の起動時の値に戻る ## 15-16. ログビューアとシステムモニタ diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 2e7a7b0..02515ef 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -176,7 +176,10 @@ export async function logout(): Promise { export interface SettingEntry { key: string is_set: boolean - masked: string | null + /** 秘匿キーはマスク済みの値、それ以外は現在値そのもの。未設定なら null。 */ + display_value: string | null + /** APIキーなど、入力欄をパスワード扱いにするキーか。 */ + secret: boolean overridden: boolean } diff --git a/frontend/src/components/SettingsPanel.vue b/frontend/src/components/SettingsPanel.vue index 86d512c..870a49d 100644 --- a/frontend/src/components/SettingsPanel.vue +++ b/frontend/src/components/SettingsPanel.vue @@ -46,11 +46,9 @@ const KEY_LABELS: Record = { OLLAMA_API_BASE: 'Ollama API Base URL', TAVILY_API_KEY: 'Tavily APIキー', LOCAL_KB_EMBEDDING_MODEL: '埋め込みモデル(ローカルナレッジベース検索用)', + SEARCH_SUMMARY_MODEL: '要約モデル(Web検索結果のまとめ用)', } -// 秘匿情報ではないため、マスクせず通常のテキスト入力として表示するキー -const PLAIN_TEXT_KEYS = new Set(['LOCAL_KB_EMBEDDING_MODEL', 'OLLAMA_API_BASE', 'AWS_REGION_NAME']) - async function submitPassword() { const ok = await store.unlock(passwordInput.value) if (ok) passwordInput.value = '' @@ -106,8 +104,8 @@ onMounted(() => {