/
alexefan136
/
flowstack
Обзор
Документация
Войти
/
alexefan136
/
flowstack
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
core/engine/src/services/skill_service.py
175 строк
6 KB
Alexander Efanov
upd fix
31 июл 2026, 19:17
31 июл 2026, 19:17
d146d86
Код
Авторство
О чём код?
"""Бизнес-логика скиллов (CRUD + запуск: render prompt → LLM).""" from __future__ import annotations import uuid from collections.abc import AsyncIterator from typing import Any import structlog from sqlalchemy.ext.asyncio import AsyncSession from src.db.repositories import SkillRepository from src.llm import LLMConfig, get_llm_client from src.primitives import SkillParameter, SkillTemplate from src.schemas import SkillCreate, SkillUpdate logger = structlog.get_logger() class SkillService: """Сервис для работы со скиллами.""" def __init__( self, session: AsyncSession, user_id: uuid.UUID, workspace_id: str, ) -> None: self.session = session self.user_id = user_id self.workspace_id = workspace_id self.skill_repo = SkillRepository(session, user_id, workspace_id) self.llm_client = get_llm_client() # ======================================================================== # CRUD # ======================================================================== async def create(self, data: SkillCreate) -> Any: """Создать скилл.""" skill = await self.skill_repo.create( user_id=self.user_id, workspace_id=self.workspace_id, name=data.name, prompt_template=data.prompt_template, description=data.description, icon=data.icon, category=data.category, parameters=[p.model_dump() for p in data.parameters], default_model=data.default_model, tags=data.tags, is_public=data.is_public, ) logger.info("skill.created", skill_id=str(skill.id), name=data.name) return skill async def get(self, skill_id: uuid.UUID) -> Any | None: return await self.skill_repo.get(skill_id) async def list( self, category: str | None = None, limit: int = 50, offset: int = 0, ) -> list[Any]: return await self.skill_repo.list(category=category, limit=limit, offset=offset) async def update(self, skill_id: uuid.UUID, data: SkillUpdate) -> Any | None: updates = data.model_dump(exclude_unset=True) if not updates: return await self.skill_repo.get(skill_id) return await self.skill_repo.update(skill_id, **updates) async def delete(self, skill_id: uuid.UUID) -> bool: return await self.skill_repo.delete(skill_id) # ======================================================================== # Run # ======================================================================== def _build_template(self, skill: Any) -> SkillTemplate: """Сконвертировать DB-модель скилла в primitives.SkillTemplate.""" parameters = [ SkillParameter( name=p["name"], type=p["type"], label=p.get("label", p["name"]), required=p.get("required", False), default=p.get("default"), options=p.get("options"), ) for p in (skill.parameters or []) ] return SkillTemplate( id=str(skill.id), name=skill.name, prompt_template=skill.prompt_template, parameters=parameters, default_model=skill.default_model, ) async def run( self, skill_id: uuid.UUID, parameters: dict[str, Any], model: str | None = None, ) -> dict[str, Any]: """Запустить скилл (non-streaming): валидация → render → LLM.""" skill = await self.skill_repo.get(skill_id) if skill is None: raise ValueError(f"Skill {skill_id} not found") template = self._build_template(skill) errors = template.validate_parameters(parameters) if errors: raise ValueError(f"Invalid parameters: {'; '.join(errors)}") prompt = template.render(parameters, strict=True) llm_model = model or skill.default_model llm_config = LLMConfig( model=llm_model, max_tokens=8192, temperature=0.7, top_p=0.9, timeout=300.0 ) logger.info("skill.run.start", skill_id=str(skill_id), model=llm_model) response = await self.llm_client.chat_completion( messages=[{"role": "user", "content": prompt}], config=llm_config, ) await self.skill_repo.increment_usage(skill_id) return { "skill_id": str(skill_id), "output": response.get("content", ""), "rendered_prompt": prompt, "tokens_used": response.get("tokens_total", 0), } async def run_stream( self, skill_id: uuid.UUID, parameters: dict[str, Any], model: str | None = None, ) -> AsyncIterator[dict[str, Any]]: """Запустить скилл со стримингом.""" skill = await self.skill_repo.get(skill_id) if skill is None: yield {"type": "error", "error": f"Skill {skill_id} not found"} return template = self._build_template(skill) errors = template.validate_parameters(parameters) if errors: yield {"type": "error", "error": f"Invalid parameters: {'; '.join(errors)}"} return prompt = template.render(parameters, strict=True) llm_model = model or skill.default_model llm_config = LLMConfig( model=llm_model, max_tokens=8192, temperature=0.7, top_p=0.9, timeout=300.0, stream=True, ) async for chunk in self.llm_client.chat_completion_stream( messages=[{"role": "user", "content": prompt}], config=llm_config, ): yield chunk await self.skill_repo.increment_usage(skill_id)