File size: 9,390 Bytes
594ed40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
"""
NullAI システム設定API

モデル、ドメイン、DBなどの設定をUIから動的に変更するためのAPI。
"""
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
import os # For file operations
import logging

from backend.app.config import app_config_manager, app_model_router, ModelConfig as PydanticModelConfig

router = APIRouter()
logger = logging.getLogger(__name__)

# モデル保存ディレクトリ (設定可能にするべきだが、一旦ハードコード)
GGUF_MODELS_DIR = os.path.join(app_config_manager.base_dir, "models", "gguf")
os.makedirs(GGUF_MODELS_DIR, exist_ok=True) # ディレクトリが存在しない場合は作成


# --- APIエンドポイント ---

@router.get("/models", response_model=List[PydanticModelConfig])
async def get_models_config():
    """
    現在利用可能なモデルの設定一覧を取得する。
    """
    models = list(app_config_manager.models.values())
    return models

class DomainConfigResponse(BaseModel): # Renamed to avoid conflict
    domain_id: str
    name: str

# Pydantic models for the new engine management endpoints
class EngineStatusResponse(PydanticModelConfig):
    status: str # 'master' | 'apprentice' | 'available' | 'retired'
    unique_id: Optional[str] = None # For apprentice engines

class EngineSwapRequest(BaseModel):
    apprentice_model_id: str

class EnginePromoteRequest(BaseModel):
    apprentice_model_id: str

class NewApprenticeResponse(BaseModel):
    new_apprentice_id: str
    message: str

@router.get("/domains", response_model=List[DomainConfigResponse])
async def get_domains_config():
    """
    現在利用可能なドメイン(知識DB)の設定一覧を取得する。
    """
    domains = list(app_config_manager.domains.values())
    return [DomainConfigResponse(domain_id=d.get("domain_id"), name=d.get("name")) for d in domains]

@router.post("/models", response_model=PydanticModelConfig)
async def register_new_model(model_data: Dict[str, Any]):
    """
    新しいモデルを登録する。
    """
    new_model = app_config_manager.add_model(model_data)
    if not new_model:
        raise HTTPException(status_code=400, detail="Failed to add new model. Check data format.")
    return new_model

@router.get("/engines", response_model=List[EngineStatusResponse])
async def get_all_engines():
    """
    システムに登録されている全てのエンジン(モデル)とそのステータスを取得する。
    """
    all_models = []
    current_master = app_model_router.get_master_model()
    current_apprentice = app_model_router.get_apprentice_model()
    
    # Assuming app_model_router can provide info about all apprentices (even inactive ones)
    # This might need a new method in app_model_router if not available
    all_managed_engines = app_model_router.get_all_managed_engines() # This method needs to be implemented in ModelRouter
    
    for model_id, model_config in app_config_manager.models.items():
        status = 'available'
        unique_id = None
        
        if current_master and model_id == current_master.model_id:
            status = 'master'
        elif current_apprentice and model_id == current_apprentice.model_id:
            status = 'apprentice'
            # Assuming app_model_router keeps track of unique_id for apprentices
            apprentice_info = next((e for e in all_managed_engines if e['config']['model_id'] == model_id and e['unique_id']), None)
            if apprentice_info:
                unique_id = apprentice_info['unique_id']
        # else: default to 'available' or 'retired' if explicitly managed by router but not active
        
        # Consider models that were once master and now retired
        # This logic needs to be enhanced if app_model_router tracks retired models
        
        all_models.append(
            EngineStatusResponse(
                **model_config.dict(), # Unpack PydanticModelConfig fields
                status=status,
                unique_id=unique_id
            )
        )
    return all_models

class ActiveEngines(BaseModel):
    master_engine_id: str
    apprentice_engine_id: Optional[str] = None

class ActiveEnginesResponse(BaseModel):
    master_engine_id: str
    apprentice_engine_id: Optional[str] = None

@router.get("/engines/active", response_model=ActiveEnginesResponse)
async def get_active_engines():
    """
    現在設定されているマスターエンジンとアプレンティスエンジンのIDを取得する。
    """
    master_model = app_model_router.get_master_model()
    apprentice_model = app_model_router.get_apprentice_model()
    return ActiveEnginesResponse(
        master_engine_id=master_model.model_id if master_model else None,
        apprentice_engine_id=apprentice_model.model_id if apprentice_model else None
    )

@router.post("/engines/active")
async def set_active_engines(engines: ActiveEngines):
    """
    マスターエンジンとアプレンティスエンジンを設定する。
    """
    if not app_model_router.set_master_model(engines.master_engine_id):
        raise HTTPException(status_code=400, detail=f"Master model '{engines.master_engine_id}' not found.")
    if engines.apprentice_engine_id:
        if not app_model_router.set_apprentice_model(engines.apprentice_engine_id):
            raise HTTPException(status_code=400, detail=f"Apprentice model '{engines.apprentice_engine_id}' not found.")
    else:
        app_model_router.set_apprentice_model(None) # Clear apprentice

    return {"status": "success", "message": f"Active engines set to {engines.master_engine_id} (Master) and {engines.apprentice_engine_id or 'None'} (Apprentice)."}

@router.post("/engines/swap", response_model=ActiveEnginesResponse)
async def swap_engines(request: EngineSwapRequest):
    """
    現在の師匠と指定した弟子を入れ替える。
    """
    if not app_model_router.swap_engines(request.apprentice_model_id):
        raise HTTPException(status_code=400, detail=f"Failed to swap engines with apprentice '{request.apprentice_model_id}'. Check logs.")
    
    master_model = app_model_router.get_master_model()
    apprentice_model = app_model_router.get_apprentice_model()
    return ActiveEnginesResponse(
        master_engine_id=master_model.model_id if master_model else None,
        apprentice_engine_id=apprentice_model.model_id if apprentice_model else None
    )

@router.post("/engines/promote", response_model=ActiveEnginesResponse)
async def promote_apprentice(request: EnginePromoteRequest):
    """
    指定した弟子を師匠に昇格させ、現在の師匠を引退させる。
    """
    if not app_model_router.promote_apprentice(request.apprentice_model_id):
        raise HTTPException(status_code=400, detail=f"Failed to promote apprentice '{request.apprentice_model_id}'. Check logs.")
    
    master_model = app_model_router.get_master_model()
    apprentice_model = app_model_router.get_apprentice_model()
    return ActiveEnginesResponse(
        master_engine_id=master_model.model_id if master_model else None,
        apprentice_engine_id=apprentice_model.model_id if apprentice_model else None
    )

@router.post("/engines/apprentice/new", response_model=NewApprenticeResponse)
async def create_new_apprentice_endpoint():
    """
    新しい「空っぽの弟子」推論エンジンを生成し、登録する。
    """
    new_apprentice_data = app_model_router.create_new_apprentice()
    if not new_apprentice_data:
        raise HTTPException(status_code=500, detail="Failed to create new apprentice engine.")
    
    return NewApprenticeResponse(
        new_apprentice_id=new_apprentice_data["config"]["model_id"],
        message=f"New apprentice '{new_apprentice_data['config']['display_name']}' created successfully."
    )

class ActiveDomain(BaseModel):
    domain_id: str

@router.post("/domains/active")
async def set_active_domain(domain: ActiveDomain):
    """
    推論に使用するアクティブなドメイン(知識DB)を設定する。
    """
    if not app_config_manager.set_active_domain(domain.domain_id):
        raise HTTPException(status_code=400, detail=f"Domain '{domain.domain_id}' not found or failed to set.")
    
    # ModelRouterにもアクティブドメインの変更を通知
    app_model_router.set_active_domain_id(domain.domain_id)
    return {"status": "success", "message": f"Active domain set to {domain.domain_id}."}

@router.post("/upload-gguf")
async def upload_gguf_model(file: UploadFile = File(...)):
    """
    GGUFモデルファイルをアップロードする。
    """
    file_path = os.path.join(GGUF_MODELS_DIR, file.filename)
    try:
        with open(file_path, "wb") as buffer:
            while True:
                chunk = await file.read(1024 * 1024) # 1MBずつ読み込み
                if not chunk:
                    break
                buffer.write(chunk)
        logger.info(f"Uploaded GGUF file saved to: {file_path}")
        return {"filename": file.filename, "path": file_path, "message": "GGUF model uploaded successfully."}
    except Exception as e:
        logger.error(f"Failed to upload GGUF file: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to upload file: {e}")