|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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}...") |
|
|
|
|
|
|
|
|
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_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_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 = {} |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
app_config_manager = ConfigManager() |
|
|
|
|
|
|
|
|
from null_ai.model_router import ModelRouter |
|
|
app_model_router = ModelRouter(app_config_manager) |
|
|
|
|
|
class Settings(BaseSettings): |
|
|
"""アプリケーション設定を管理するクラス""" |
|
|
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') |
|
|
|
|
|
|
|
|
DATABASE_URL: str = "sqlite:///./sql_app.db" |
|
|
|
|
|
|
|
|
REDIS_URL: str = "redis://localhost:6379" |
|
|
|
|
|
|
|
|
SECRET_KEY: str = "super-secret-key" |
|
|
ALGORITHM: str = "HS256" |
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 |
|
|
|
|
|
|
|
|
CORS_ORIGINS: list[str] = ["http://localhost:5173", "http://127.0.0.1:5173"] |
|
|
|
|
|
|
|
|
DEBUG: bool = False |
|
|
|
|
|
|
|
|
DEEPSEEK_API_URL: str = "http://host.docker.internal:11434" |
|
|
DEEPSEEK_MODEL_NAME: str = "deepseek-r1:32b" |
|
|
DB_PATH: str = "ilm_athens_medical_db.iath" |
|
|
|
|
|
|
|
|
@property |
|
|
def app_config(self) -> ConfigManager: |
|
|
return app_config_manager |
|
|
|
|
|
settings = Settings() |
|
|
|