kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
import os
import json
import logging
from enum import Enum
from typing import Dict, List, Optional, Any
from pydantic import BaseModel, Field, ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict
# Configure logging for this module
logger = logging.getLogger(__name__)
class ModelProvider(str, Enum):
"""モデルプロバイダーの種類"""
HUGGINGFACE = "huggingface"
HUGGINGFACE_API = "huggingface_api"
MLX = "mlx"
GGUF = "gguf"
OLLAMA = "ollama"
class ModelConfig(BaseModel):
"""単一のモデル設定を表すPydanticモデル"""
model_id: str
display_name: str
provider: ModelProvider
api_url: Optional[str] = None
model_name: str
max_tokens: int = 4096
temperature: float = 0.7
timeout: int = 120
is_default: bool = False
supported_domains: List[str]
description: Optional[str] = None
quantization: Optional[str] = None # e.g., "4bit", "8bit"
class ConfigManager:
"""
NullAIプロジェクト全体の構成(モデル、ドメイン、一般的な設定)を管理するクラス。
設定ファイル(JSON形式)から構成をロードし、アクセスを提供します。
"""
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super(ConfigManager, cls).__new__(cls)
return cls._instance
def __init__(self):
if self._initialized:
return
# backend/app/config.pyから見てプロジェクトルートを指すように調整
self.base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
self.models_config_path = os.path.join(self.base_dir, 'models_config.json')
self.domains_config_path = os.path.join(self.base_dir, 'domains_config.json')
self.null_ai_config_path = os.path.join(self.base_dir, 'null_ai_config.json')
self.models: Dict[str, ModelConfig] = {}
self.domains: Dict[str, Any] = {}
self.null_ai_settings: Dict[str, Any] = {}
self._load_configs()
self._initialized = True
def _load_json_file(self, file_path: str) -> Dict[str, Any]:
"""JSONファイルをロードするヘルパーメソッド"""
if not os.path.exists(file_path):
logger.warning(f"Configuration file not found: {file_path}")
return {}
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON from {file_path}: {e}")
return {}
except Exception as e:
logger.error(f"An unexpected error occurred while reading {file_path}: {e}")
return {}
def _save_json_file(self, file_path: str, data: Dict[str, Any]):
"""JSONファイルにデータを保存するヘルパーメソッド"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"Configuration saved successfully to {file_path}")
except Exception as e:
logger.error(f"Failed to save configuration to {file_path}: {e}")
def save_models_config(self):
"""現在のモデル設定をJSONファイルに保存する"""
models_list = [model.dict() for model in self.models.values()]
self._save_json_file(self.models_config_path, {"models": models_list})
def _load_configs(self):
"""すべての設定ファイルをロードする"""
logger.info(f"Loading configurations from {self.base_dir}...")
# null_ai_config.jsonをロード
self.null_ai_settings = self._load_json_file(self.null_ai_config_path)
logger.info(f"Loaded null_ai_config.json: {len(self.null_ai_settings)} items")
# domains_config.jsonをロード
domains_data = self._load_json_file(self.domains_config_path)
if "domains" in domains_data and isinstance(domains_data["domains"], list):
self.domains = {d["domain_id"]: d for d in domains_data["domains"]}
logger.info(f"Loaded domains_config.json: {len(self.domains)} domains")
else:
logger.warning(f"'domains' key not found or not a list in {self.domains_config_path}")
self.domains = {}
# models_config.jsonをロード
models_data = self._load_json_file(self.models_config_path)
if "models" in models_data and isinstance(models_data["models"], list):
for model_dict in models_data["models"]:
try:
model = ModelConfig(**model_dict)
self.models[model.model_id] = model
except ValidationError as e:
logger.error(f"Validation error for model in {self.models_config_path}: {model_dict} - {e}")
except Exception as e:
logger.error(f"Error processing model {model_dict.get('model_id', 'unknown')}: {e}")
logger.info(f"Loaded models_config.json: {len(self.models)} models")
else:
logger.warning(f"'models' key not found or not a list in {self.models_config_path}")
self.models = {}
# null_ai_config.jsonからactive_domain_id, active_master_id, active_apprentice_idをロード
if not self.get_null_ai_setting("active_domain_id") and self.domains:
self.set_null_ai_setting("active_domain_id", list(self.domains.keys())[0]) # デフォルトとして最初のドメインを設定
logger.info("All configurations loaded.")
def set_active_domain(self, domain_id: str) -> bool:
"""アクティブなドメインIDを設定し、null_ai_config.jsonに保存する"""
if domain_id not in self.domains:
logger.error(f"Domain with ID '{domain_id}' not found.")
return False
self.set_null_ai_setting("active_domain_id", domain_id)
logger.info(f"Active domain set to: {domain_id}")
return True
def get_active_domain_id(self) -> Optional[str]:
"""現在アクティブなドメインIDを取得する"""
return self.get_null_ai_setting("active_domain_id")
def add_model(self, model_data: Dict[str, Any]) -> Optional[ModelConfig]:
"""新しいモデルを追加して設定を保存する"""
try:
model = ModelConfig(**model_data)
if model.model_id in self.models:
logger.warning(f"Model with id '{model.model_id}' already exists. Overwriting.")
self.models[model.model_id] = model
self.save_models_config()
logger.info(f"Added/Updated model '{model.model_id}' and saved configuration.")
return model
except ValidationError as e:
logger.error(f"Validation error for new model data: {model_data} - {e}")
return None
def get_model_config(self, model_id: str) -> Optional[ModelConfig]:
"""指定されたモデルIDの構成を取得する"""
return self.models.get(model_id)
def get_default_model_config(self, domain_id: Optional[str] = None) -> Optional[ModelConfig]:
"""
指定されたドメインのデフォルトモデル、またはグローバルデフォルトモデルを取得する。
"""
if domain_id and domain_id in self.domains and "default_model_id" in self.domains[domain_id]:
default_model_id = self.domains[domain_id]["default_model_id"]
model_config = self.get_model_config(default_model_id)
if model_config:
return model_config
logger.warning(f"Default model '{default_model_id}' for domain '{domain_id}' not found in models config.")
# ドメイン固有のデフォルトが見つからない、または指定がない場合、グローバルデフォルトを探す
for model_config in self.models.values():
if model_config.is_default:
return model_config
logger.warning("No default model found in configuration.")
return None
def get_domain_config(self, domain_id: str) -> Optional[Dict[str, Any]]:
"""指定されたドメインIDの構成を取得する"""
return self.domains.get(domain_id)
def get_null_ai_setting(self, key: str, default: Any = None) -> Any:
"""null_ai_config.jsonから設定値を取得する"""
return self.null_ai_settings.get(key, default)
def set_null_ai_setting(self, key: str, value: Any):
"""null_ai_config.jsonに設定値を保存する"""
self.null_ai_settings[key] = value
self._save_json_file(self.null_ai_config_path, self.null_ai_settings)
def reload_configs(self):
"""すべての設定ファイルを再ロードする"""
logger.info("Reloading configurations...")
self.models = {}
self.domains = {}
self.null_ai_settings = {}
self._load_configs()
logger.info("Configurations reloaded.")
def add_model(self, model_data: Dict[str, Any]) -> Optional[ModelConfig]:
"""新しいモデルを追加して設定を保存する"""
try:
model = ModelConfig(**model_data)
if model.model_id in self.models:
logger.warning(f"Model with id '{model.model_id}' already exists. Overwriting.")
self.models[model.model_id] = model
self.save_models_config()
logger.info(f"Added/Updated model '{model.model_id}' and saved configuration.")
return model
except ValidationError as e:
logger.error(f"Validation error for new model data: {model_data} - {e}")
return None
def get_model_config(self, model_id: str) -> Optional[ModelConfig]:
"""指定されたモデルIDの構成を取得する"""
return self.models.get(model_id)
def get_default_model_config(self, domain_id: Optional[str] = None) -> Optional[ModelConfig]:
"""
指定されたドメインのデフォルトモデル、またはグローバルデフォルトモデルを取得する。
"""
if domain_id and domain_id in self.domains and "default_model_id" in self.domains[domain_id]:
default_model_id = self.domains[domain_id]["default_model_id"]
model_config = self.get_model_config(default_model_id)
if model_config:
return model_config
logger.warning(f"Default model '{default_model_id}' for domain '{domain_id}' not found in models config.")
# ドメイン固有のデフォルトが見つからない、または指定がない場合、グローバルデフォルトを探す
for model_config in self.models.values():
if model_config.is_default:
return model_config
logger.warning("No default model found in configuration.")
return None
def get_domain_config(self, domain_id: str) -> Optional[Dict[str, Any]]:
"""指定されたドメインIDの構成を取得する"""
return self.domains.get(domain_id)
def get_null_ai_setting(self, key: str, default: Any = None) -> Any:
"""null_ai_config.jsonから設定値を取得する"""
return self.null_ai_settings.get(key, default)
def reload_configs(self):
"""すべての設定ファイルを再ロードする"""
logger.info("Reloading configurations...")
self.models = {}
self.domains = {}
self.null_ai_settings = {}
self._load_configs()
logger.info("Configurations reloaded.")
# ConfigManagerのシングルトンインスタンス
app_config_manager = ConfigManager()
# ModelRouterのシングルトンインスタンス
from null_ai.model_router import ModelRouter
app_model_router = ModelRouter(app_config_manager)
# DBRouterのシングルトンインスタンス
from null_ai.db_providers import DBRouter, DBProvider
# null_ai_config.jsonからmain_db_pathを読み込む
_db_path = app_config_manager.get_null_ai_setting("main_db_path", "null_ai_knowledge.iath")
_db_provider = app_config_manager.get_null_ai_setting("db_provider", "iath")
app_db_router = DBRouter(
provider_type=DBProvider(_db_provider),
db_path=_db_path
)
class Settings(BaseSettings):
"""アプリケーション設定を管理するクラス"""
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
# データベース設定
DATABASE_URL: str = "sqlite:///./sql_app.db" # デフォルトはSQLite
# Redis設定
REDIS_URL: str = "redis://localhost:6379"
# JWT認証設定
SECRET_KEY: str = "super-secret-key" # 本番環境では強力なキーに変更すること
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # アクセストークンの有効期限 (分)
# CORS設定
CORS_ORIGINS: list[str] = ["http://localhost:5173", "http://127.0.0.1:5173"] # フロントエンドのURL
# デバッグモード
DEBUG: bool = False
# 推論エンジンとDBの設定 (環境変数から読み込む)
DEEPSEEK_API_URL: str = "http://host.docker.internal:11434"
DEEPSEEK_MODEL_NAME: str = "deepseek-r1:32b"
DB_PATH: str = "ilm_athens_medical_db.iath"
# ConfigManagerのインスタンスをSettingsに追加
@property
def app_config(self) -> ConfigManager:
return app_config_manager
settings = Settings()