Skip to content

Commit 10cdb25

Browse files
authored
Merge pull request #570 from ShaerWare/server/phase-4.4-finetune
Phase 4.4: Extract finetune endpoints to domain modules
2 parents 9b309a2 + ff6535b commit 10cdb25

3 files changed

Lines changed: 459 additions & 411 deletions

File tree

modules/llm/router_finetune.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
"""LLM fine-tuning endpoints.
2+
3+
Dataset management, training control, and LoRA adapter management.
4+
GPU-only — not registered in cloud deployment mode.
5+
"""
6+
7+
import logging
8+
from typing import Optional
9+
10+
from fastapi import APIRouter, Depends, File, UploadFile
11+
from fastapi.responses import StreamingResponse
12+
from pydantic import BaseModel
13+
14+
from auth_manager import User, require_permission
15+
from finetune_manager import get_finetune_manager
16+
17+
18+
logger = logging.getLogger(__name__)
19+
20+
router = APIRouter(prefix="/admin/finetune", tags=["finetune-llm"])
21+
22+
23+
# ============== Pydantic Models ==============
24+
25+
26+
class DatasetProcessRequest(BaseModel):
27+
owner_name: Optional[str] = None
28+
transcribe_voice: Optional[bool] = None
29+
min_dialog_messages: Optional[int] = None
30+
max_message_length: Optional[int] = None
31+
max_dialog_length: Optional[int] = None
32+
include_groups: Optional[bool] = None
33+
output_name: Optional[str] = None
34+
35+
36+
class GenerateProjectDatasetRequest(BaseModel):
37+
include_tz: bool = True
38+
include_faq: bool = True
39+
include_docs: bool = True
40+
include_escalation: bool = True
41+
include_code: bool = True # Python код и Markdown документация
42+
github_repo_url: Optional[str] = None # URL публичного GitHub/GitLab репозитория
43+
github_branch: str = "main" # Ветка для клонирования
44+
output_name: str = "project_dataset"
45+
46+
47+
class FinetuneConfigRequest(BaseModel):
48+
lora_rank: Optional[int] = None
49+
lora_alpha: Optional[int] = None
50+
batch_size: Optional[int] = None
51+
gradient_accumulation_steps: Optional[int] = None
52+
learning_rate: Optional[float] = None
53+
num_epochs: Optional[int] = None
54+
max_seq_length: Optional[int] = None
55+
output_dir: Optional[str] = None
56+
57+
58+
class AdapterRequest(BaseModel):
59+
adapter: str
60+
61+
62+
# ============== Dataset Endpoints ==============
63+
64+
65+
@router.post("/dataset/upload")
66+
async def admin_upload_dataset(file: UploadFile = File(...)):
67+
"""Загрузить датасет (Telegram export JSON)"""
68+
manager = get_finetune_manager()
69+
content = await file.read()
70+
return await manager.upload_dataset(content, file.filename)
71+
72+
73+
@router.post("/dataset/process")
74+
async def admin_process_dataset(request: Optional[DatasetProcessRequest] = None):
75+
"""Обработать загруженный датасет"""
76+
manager = get_finetune_manager()
77+
config = request.model_dump(exclude_none=True) if request else None
78+
return await manager.process_dataset(config)
79+
80+
81+
@router.get("/dataset/config")
82+
async def admin_get_dataset_config():
83+
"""Получить конфигурацию обработки датасета"""
84+
manager = get_finetune_manager()
85+
return {"config": manager.get_dataset_config()}
86+
87+
88+
@router.post("/dataset/config")
89+
async def admin_set_dataset_config(request: DatasetProcessRequest):
90+
"""Установить конфигурацию обработки датасета"""
91+
manager = get_finetune_manager()
92+
return manager.set_dataset_config(**request.model_dump(exclude_none=True))
93+
94+
95+
@router.get("/dataset/processing-status")
96+
async def admin_get_processing_status():
97+
"""Получить статус обработки датасета"""
98+
manager = get_finetune_manager()
99+
return {"status": manager.get_processing_status()}
100+
101+
102+
@router.get("/dataset/stats")
103+
async def admin_get_dataset_stats():
104+
"""Получить статистику датасета"""
105+
manager = get_finetune_manager()
106+
stats = manager.get_dataset_stats()
107+
return {
108+
"stats": {
109+
"total_sessions": stats.total_sessions,
110+
"total_messages": stats.total_messages,
111+
"total_tokens": stats.total_tokens,
112+
"avg_tokens_per_message": stats.avg_tokens_per_message,
113+
"file_path": stats.file_path,
114+
"file_size_mb": stats.file_size_mb,
115+
"modified": stats.modified,
116+
}
117+
}
118+
119+
120+
@router.get("/dataset/list")
121+
async def admin_list_datasets():
122+
"""Список доступных датасетов"""
123+
manager = get_finetune_manager()
124+
return {"datasets": manager.list_datasets()}
125+
126+
127+
@router.post("/dataset/augment")
128+
async def admin_augment_dataset():
129+
"""Аугментировать датасет"""
130+
manager = get_finetune_manager()
131+
return await manager.augment_dataset()
132+
133+
134+
@router.post("/dataset/generate-project")
135+
async def admin_generate_project_dataset(request: GenerateProjectDatasetRequest):
136+
"""Генерировать датасет из проектных источников (ТЗ, FAQ, документация, эскалации, код, GitHub)"""
137+
manager = get_finetune_manager()
138+
return await manager.generate_project_dataset(
139+
include_tz=request.include_tz,
140+
include_faq=request.include_faq,
141+
include_docs=request.include_docs,
142+
include_escalation=request.include_escalation,
143+
include_code=request.include_code,
144+
github_repo_url=request.github_repo_url,
145+
github_branch=request.github_branch,
146+
output_name=request.output_name,
147+
)
148+
149+
150+
# ============== Training Config Endpoints ==============
151+
152+
153+
@router.get("/config")
154+
async def admin_get_finetune_config():
155+
"""Получить конфигурацию обучения"""
156+
manager = get_finetune_manager()
157+
config = manager.get_config()
158+
return {
159+
"config": {
160+
"base_model": config.base_model,
161+
"lora_rank": config.lora_rank,
162+
"lora_alpha": config.lora_alpha,
163+
"lora_dropout": config.lora_dropout,
164+
"batch_size": config.batch_size,
165+
"gradient_accumulation_steps": config.gradient_accumulation_steps,
166+
"learning_rate": config.learning_rate,
167+
"num_epochs": config.num_epochs,
168+
"warmup_ratio": config.warmup_ratio,
169+
"max_seq_length": config.max_seq_length,
170+
"output_dir": config.output_dir,
171+
},
172+
"presets": {
173+
name: {
174+
"lora_rank": p.lora_rank,
175+
"batch_size": p.batch_size,
176+
"num_epochs": p.num_epochs,
177+
}
178+
for name, p in manager.get_config_presets().items()
179+
},
180+
}
181+
182+
183+
@router.post("/config")
184+
async def admin_set_finetune_config(request: FinetuneConfigRequest):
185+
"""Установить конфигурацию обучения"""
186+
manager = get_finetune_manager()
187+
config = manager.get_config()
188+
189+
# Обновляем только переданные параметры
190+
if request.lora_rank is not None:
191+
config.lora_rank = request.lora_rank
192+
if request.lora_alpha is not None:
193+
config.lora_alpha = request.lora_alpha
194+
if request.batch_size is not None:
195+
config.batch_size = request.batch_size
196+
if request.gradient_accumulation_steps is not None:
197+
config.gradient_accumulation_steps = request.gradient_accumulation_steps
198+
if request.learning_rate is not None:
199+
config.learning_rate = request.learning_rate
200+
if request.num_epochs is not None:
201+
config.num_epochs = request.num_epochs
202+
if request.max_seq_length is not None:
203+
config.max_seq_length = request.max_seq_length
204+
if request.output_dir is not None:
205+
config.output_dir = request.output_dir
206+
207+
return manager.set_config(config)
208+
209+
210+
# ============== Training Control Endpoints ==============
211+
212+
213+
@router.post("/train/start")
214+
async def admin_start_training():
215+
"""Запустить обучение"""
216+
manager = get_finetune_manager()
217+
return await manager.start_training()
218+
219+
220+
@router.post("/train/stop")
221+
async def admin_stop_training():
222+
"""Остановить обучение"""
223+
manager = get_finetune_manager()
224+
return await manager.stop_training()
225+
226+
227+
@router.get("/train/status")
228+
async def admin_get_training_status():
229+
"""Получить статус обучения"""
230+
manager = get_finetune_manager()
231+
status = manager.get_training_status()
232+
return {
233+
"status": {
234+
"is_running": status.is_running,
235+
"current_step": status.current_step,
236+
"total_steps": status.total_steps,
237+
"current_epoch": status.current_epoch,
238+
"total_epochs": status.total_epochs,
239+
"loss": status.loss,
240+
"learning_rate": status.learning_rate,
241+
"elapsed_seconds": status.elapsed_seconds,
242+
"eta_seconds": status.eta_seconds,
243+
"error": status.error,
244+
}
245+
}
246+
247+
248+
@router.get("/train/log")
249+
async def admin_stream_training_log(
250+
user: User = Depends(require_permission("system", "view")),
251+
):
252+
"""SSE streaming лога обучения"""
253+
manager = get_finetune_manager()
254+
255+
async def generate():
256+
async for data in manager.stream_training_log():
257+
yield f"data: {data}\n\n"
258+
259+
return StreamingResponse(
260+
generate(),
261+
media_type="text/event-stream",
262+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
263+
)
264+
265+
266+
# ============== Adapter Endpoints ==============
267+
268+
269+
@router.get("/adapters")
270+
async def admin_list_adapters():
271+
"""Получить список LoRA адаптеров"""
272+
manager = get_finetune_manager()
273+
adapters = manager.list_adapters()
274+
return {
275+
"adapters": [
276+
{
277+
"name": a.name,
278+
"path": a.path,
279+
"size_mb": a.size_mb,
280+
"modified": a.modified,
281+
"active": a.active,
282+
"config": a.config,
283+
}
284+
for a in adapters
285+
],
286+
"active": manager.active_adapter,
287+
}
288+
289+
290+
@router.post("/adapters/activate")
291+
async def admin_activate_adapter(request: AdapterRequest):
292+
"""Активировать LoRA адаптер"""
293+
manager = get_finetune_manager()
294+
return await manager.activate_adapter(request.adapter)
295+
296+
297+
@router.delete("/adapters/{name}")
298+
async def admin_delete_adapter(name: str):
299+
"""Удалить LoRA адаптер"""
300+
manager = get_finetune_manager()
301+
return await manager.delete_adapter(name)

0 commit comments

Comments
 (0)