"""Версионируемые конфигурации агента: создание, активация, чтение.""" import logging from pathlib import Path from sqlalchemy import func, select, update from sqlalchemy.ext.asyncio import AsyncSession from db.models import AgentConfig logger = logging.getLogger(__name__) SEED_PROMPT_PATH = Path(__file__).resolve().parent.parent / "prompts" / "system_prompt.md" def _load_seed_prompt() -> str: try: return SEED_PROMPT_PATH.read_text(encoding="utf-8").strip() except FileNotFoundError: logger.warning("Seed prompt file not found at %s — creating empty v1", SEED_PROMPT_PATH) return "" async def ensure_seed(session: AsyncSession) -> None: """Если таблица пустая, создать v1 из prompts/system_prompt.md и активировать.""" count = (await session.execute(select(func.count(AgentConfig.id)))).scalar_one() if count > 0: return seed_text = _load_seed_prompt() seed = AgentConfig( version=1, name="Исходная версия (из prompts/system_prompt.md)", system_prompt=seed_text, rules_text="", is_active=True, ) session.add(seed) await session.commit() logger.info("Seeded agent_configs with v1 from %s", SEED_PROMPT_PATH.name) async def list_configs(session: AsyncSession) -> list[AgentConfig]: stmt = select(AgentConfig).order_by(AgentConfig.version.desc()) return list((await session.execute(stmt)).scalars().all()) async def get_config(session: AsyncSession, config_id: int) -> AgentConfig | None: return await session.get(AgentConfig, config_id) async def get_active_config(session: AsyncSession) -> AgentConfig | None: stmt = select(AgentConfig).where(AgentConfig.is_active.is_(True)).limit(1) return (await session.execute(stmt)).scalar_one_or_none() async def create_config( session: AsyncSession, system_prompt: str, rules_text: str, name: str | None = None, activate: bool = False, ) -> AgentConfig: """Создать новую версию. При activate=True — сразу сделать активной.""" next_version = (await session.execute(select(func.coalesce(func.max(AgentConfig.version), 0)))).scalar_one() + 1 if activate: await session.execute( update(AgentConfig).where(AgentConfig.is_active.is_(True)).values(is_active=False) ) cfg = AgentConfig( version=next_version, name=(name or "").strip() or None, system_prompt=system_prompt, rules_text=rules_text or "", is_active=activate, ) session.add(cfg) await session.commit() await session.refresh(cfg) return cfg async def activate_config(session: AsyncSession, config_id: int) -> AgentConfig | None: cfg = await session.get(AgentConfig, config_id) if cfg is None: return None await session.execute( update(AgentConfig).where(AgentConfig.is_active.is_(True)).values(is_active=False) ) cfg.is_active = True await session.commit() await session.refresh(cfg) return cfg async def delete_config(session: AsyncSession, config_id: int) -> tuple[bool, str]: """Удалить версию. Нельзя удалить активную. Возвращает (ok, reason_if_fail).""" cfg = await session.get(AgentConfig, config_id) if cfg is None: return False, "not_found" if cfg.is_active: return False, "active" await session.delete(cfg) await session.commit() return True, "" def compose_full_system_prompt(cfg: AgentConfig) -> str: """Собрать из system_prompt + rules_text единую строку для LLM.""" base = (cfg.system_prompt or "").strip() rules = (cfg.rules_text or "").strip() if not rules: return base return f"{base}\n\nДополнительные правила:\n{rules}"