from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import Optional import uuid from datetime import datetime from ..db import get_db from ..models import Assistant, LLMModel, ASRModel, Voice from ..schemas import ( AssistantCreate, AssistantUpdate, AssistantOut ) router = APIRouter(prefix="/assistants", tags=["Assistants"]) def _is_siliconflow_vendor(vendor: Optional[str]) -> bool: return (vendor or "").strip().lower() in {"siliconflow", "硅基流动"} def _resolve_runtime_metadata(db: Session, assistant: Assistant) -> dict: metadata = { "systemPrompt": assistant.prompt or "", "firstTurnMode": assistant.first_turn_mode or "bot_first", "greeting": assistant.opener or "", "generatedOpenerEnabled": bool(assistant.generated_opener_enabled), "output": {"mode": "audio" if assistant.voice_output_enabled else "text"}, "bargeIn": { "enabled": not bool(assistant.bot_cannot_be_interrupted), "minDurationMs": int(assistant.interruption_sensitivity or 500), }, "services": {}, } warnings = [] if assistant.llm_model_id: llm = db.query(LLMModel).filter(LLMModel.id == assistant.llm_model_id).first() if llm: metadata["services"]["llm"] = { "provider": "openai", "model": llm.model_name or llm.name, "apiKey": llm.api_key, "baseUrl": llm.base_url, } else: warnings.append(f"LLM model not found: {assistant.llm_model_id}") if assistant.asr_model_id: asr = db.query(ASRModel).filter(ASRModel.id == assistant.asr_model_id).first() if asr: asr_provider = "siliconflow" if _is_siliconflow_vendor(asr.vendor) else "buffered" metadata["services"]["asr"] = { "provider": asr_provider, "model": asr.model_name or asr.name, "apiKey": asr.api_key if asr_provider == "siliconflow" else None, } else: warnings.append(f"ASR model not found: {assistant.asr_model_id}") if not assistant.voice_output_enabled: metadata["services"]["tts"] = {"enabled": False} elif assistant.voice: voice = db.query(Voice).filter(Voice.id == assistant.voice).first() if voice: tts_provider = "siliconflow" if _is_siliconflow_vendor(voice.vendor) else "edge" metadata["services"]["tts"] = { "enabled": True, "provider": tts_provider, "model": voice.model, "apiKey": voice.api_key if tts_provider == "siliconflow" else None, "voice": voice.voice_key or voice.id, "speed": assistant.speed or voice.speed, } else: # Keep assistant.voice as direct voice identifier fallback metadata["services"]["tts"] = { "enabled": True, "voice": assistant.voice, "speed": assistant.speed or 1.0, } warnings.append(f"Voice resource not found: {assistant.voice}") if assistant.knowledge_base_id: metadata["knowledgeBaseId"] = assistant.knowledge_base_id metadata["knowledge"] = { "enabled": True, "kbId": assistant.knowledge_base_id, "nResults": 5, } return { "assistantId": assistant.id, "sessionStartMetadata": metadata, "sources": { "llmModelId": assistant.llm_model_id, "asrModelId": assistant.asr_model_id, "voiceId": assistant.voice, "knowledgeBaseId": assistant.knowledge_base_id, }, "warnings": warnings, } def assistant_to_dict(assistant: Assistant) -> dict: return { "id": assistant.id, "name": assistant.name, "callCount": assistant.call_count, "firstTurnMode": assistant.first_turn_mode or "bot_first", "opener": assistant.opener or "", "generatedOpenerEnabled": bool(assistant.generated_opener_enabled), "prompt": assistant.prompt or "", "knowledgeBaseId": assistant.knowledge_base_id, "language": assistant.language, "voiceOutputEnabled": assistant.voice_output_enabled, "voice": assistant.voice, "speed": assistant.speed, "hotwords": assistant.hotwords or [], "tools": assistant.tools or [], "botCannotBeInterrupted": bool(assistant.bot_cannot_be_interrupted), "interruptionSensitivity": assistant.interruption_sensitivity, "configMode": assistant.config_mode, "apiUrl": assistant.api_url, "apiKey": assistant.api_key, "llmModelId": assistant.llm_model_id, "asrModelId": assistant.asr_model_id, "embeddingModelId": assistant.embedding_model_id, "rerankModelId": assistant.rerank_model_id, "created_at": assistant.created_at, "updated_at": assistant.updated_at, } def _apply_assistant_update(assistant: Assistant, update_data: dict) -> None: field_map = { "knowledgeBaseId": "knowledge_base_id", "firstTurnMode": "first_turn_mode", "interruptionSensitivity": "interruption_sensitivity", "botCannotBeInterrupted": "bot_cannot_be_interrupted", "configMode": "config_mode", "voiceOutputEnabled": "voice_output_enabled", "generatedOpenerEnabled": "generated_opener_enabled", "apiUrl": "api_url", "apiKey": "api_key", "llmModelId": "llm_model_id", "asrModelId": "asr_model_id", "embeddingModelId": "embedding_model_id", "rerankModelId": "rerank_model_id", } for field, value in update_data.items(): setattr(assistant, field_map.get(field, field), value) # ============ Assistants ============ @router.get("") def list_assistants( page: int = 1, limit: int = 50, db: Session = Depends(get_db) ): """获取助手列表""" query = db.query(Assistant) total = query.count() assistants = query.order_by(Assistant.created_at.desc()) \ .offset((page-1)*limit).limit(limit).all() return { "total": total, "page": page, "limit": limit, "list": [assistant_to_dict(a) for a in assistants] } @router.get("/{id}", response_model=AssistantOut) def get_assistant(id: str, db: Session = Depends(get_db)): """获取单个助手详情""" assistant = db.query(Assistant).filter(Assistant.id == id).first() if not assistant: raise HTTPException(status_code=404, detail="Assistant not found") return assistant_to_dict(assistant) @router.get("/{id}/runtime-config") def get_assistant_runtime_config(id: str, db: Session = Depends(get_db)): """Resolve assistant runtime config for engine WS session.start metadata.""" assistant = db.query(Assistant).filter(Assistant.id == id).first() if not assistant: raise HTTPException(status_code=404, detail="Assistant not found") return _resolve_runtime_metadata(db, assistant) @router.post("", response_model=AssistantOut) def create_assistant(data: AssistantCreate, db: Session = Depends(get_db)): """创建新助手""" assistant = Assistant( id=str(uuid.uuid4())[:8], user_id=1, # 默认用户,后续添加认证 name=data.name, first_turn_mode=data.firstTurnMode, opener=data.opener, generated_opener_enabled=data.generatedOpenerEnabled, prompt=data.prompt, knowledge_base_id=data.knowledgeBaseId, language=data.language, voice_output_enabled=data.voiceOutputEnabled, voice=data.voice, speed=data.speed, hotwords=data.hotwords, tools=data.tools, bot_cannot_be_interrupted=data.botCannotBeInterrupted, interruption_sensitivity=data.interruptionSensitivity, config_mode=data.configMode, api_url=data.apiUrl, api_key=data.apiKey, llm_model_id=data.llmModelId, asr_model_id=data.asrModelId, embedding_model_id=data.embeddingModelId, rerank_model_id=data.rerankModelId, ) db.add(assistant) db.commit() db.refresh(assistant) return assistant_to_dict(assistant) @router.put("/{id}") def update_assistant(id: str, data: AssistantUpdate, db: Session = Depends(get_db)): """更新助手""" assistant = db.query(Assistant).filter(Assistant.id == id).first() if not assistant: raise HTTPException(status_code=404, detail="Assistant not found") update_data = data.model_dump(exclude_unset=True) _apply_assistant_update(assistant, update_data) assistant.updated_at = datetime.utcnow() db.commit() db.refresh(assistant) return assistant_to_dict(assistant) @router.delete("/{id}") def delete_assistant(id: str, db: Session = Depends(get_db)): """删除助手""" assistant = db.query(Assistant).filter(Assistant.id == id).first() if not assistant: raise HTTPException(status_code=404, detail="Assistant not found") db.delete(assistant) db.commit() return {"message": "Deleted successfully"}