Vectors from two different embedding models are not comparable — Titan v2 has 1024 dimensions where nomic-embed-text has 768 — and _cosine_similarity zips them, so a mismatch produced quietly meaningless scores rather than an error. Nothing forced a re-embed either: sync_local_kb only looked at the content hash, so switching models left every cached vector in place. local_knowledge_docs now records the model each vector came from, and a row is re-embedded when its content hash OR its model differs. Saving a new LOCAL_KB_EMBEDDING_MODEL from the settings panel starts a background pass over every file; the knowledge base panel also has a button for it. While that pass runs, the per-request sync steps aside so no file is embedded twice. The panel now shows what the embedding model is doing: model name, vector size, how many files are embedded with the current model, how many are still on an older one, a progress bar while a pass runs, and the last error. A "接続テスト" button embeds one short string so a broken or unreachable model can be told apart from one that is merely slow to warm up. Also read TAVILY_API_KEY from os.environ at call time. It was imported as a config constant, which is evaluated before load_into_env() runs, so a key saved from the settings panel was stored but never used — not even after a restart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
import os
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session as DBSession
|
|
|
|
from app.core import settings_store
|
|
from app.core.auth import require_settings_password
|
|
from app.core.memory_kb import local_knowledge
|
|
from app.db.database import get_db
|
|
|
|
router = APIRouter(prefix="/api/settings", tags=["settings"], dependencies=[Depends(require_settings_password)])
|
|
|
|
|
|
class SettingEntry(BaseModel):
|
|
key: str
|
|
is_set: bool
|
|
# 秘匿キーはマスク済みの値、それ以外は現在値そのもの。未設定なら None
|
|
display_value: str | None
|
|
# 入力欄をパスワード扱いにするか(APIキー等は True)
|
|
secret: bool
|
|
overridden: bool
|
|
|
|
|
|
class SaveSettingsRequest(BaseModel):
|
|
values: dict[str, str]
|
|
|
|
|
|
@router.get("", response_model=list[SettingEntry])
|
|
def list_settings(db: DBSession = Depends(get_db)):
|
|
return settings_store.list_masked(db)
|
|
|
|
|
|
@router.put("", response_model=list[SettingEntry])
|
|
async def save_settings(req: SaveSettingsRequest, db: DBSession = Depends(get_db)):
|
|
before_embedding_model = os.environ.get("LOCAL_KB_EMBEDDING_MODEL")
|
|
try:
|
|
for key, value in req.values.items():
|
|
if not value:
|
|
continue # 空欄は「変更なし」として無視する
|
|
settings_store.save(db, key, value)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
_reembed_if_model_changed(before_embedding_model)
|
|
return settings_store.list_masked(db)
|
|
|
|
|
|
@router.delete("/{key}", response_model=list[SettingEntry])
|
|
async def clear_setting(key: str, db: DBSession = Depends(get_db)):
|
|
before_embedding_model = os.environ.get("LOCAL_KB_EMBEDDING_MODEL")
|
|
try:
|
|
settings_store.clear(db, key)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
_reembed_if_model_changed(before_embedding_model)
|
|
return settings_store.list_masked(db)
|
|
|
|
|
|
def _reembed_if_model_changed(before: str | None) -> None:
|
|
"""埋め込みモデルが変わったら、ローカルKBの再埋め込みをバックグラウンドで走らせる。
|
|
|
|
モデルが違うと次元数も意味空間も変わり、古いベクトルは比較しても無意味なので、
|
|
設定を変えた時点で全件やり直す(完了を待たずに応答は返す)。
|
|
"""
|
|
if os.environ.get("LOCAL_KB_EMBEDDING_MODEL") == before:
|
|
return
|
|
local_knowledge.start_reembed_all()
|