from __future__ import annotations import asyncio from types import SimpleNamespace from beaver.engine.providers.base import LLMProvider, LLMResponse from beaver.skills.assembler.task_assembler import SkillAssembler class RecordingProvider(LLMProvider): def __init__(self) -> None: super().__init__() self.thinking_enabled: bool | None = None async def chat( self, messages: list[dict], tools: list[dict] | None = None, model: str | None = None, max_tokens: int = 4096, temperature: float = 0.7, thinking_enabled: bool | None = None, ) -> LLMResponse: self.thinking_enabled = thinking_enabled return LLMResponse(content='["daily-news"]', provider_name="stub", model="stub-model") def get_default_model(self) -> str: return "stub-model" class SequencedProvider(LLMProvider): def __init__(self, responses: list[str]) -> None: super().__init__() self.responses = list(responses) self.messages: list[list[dict]] = [] async def chat( self, messages: list[dict], tools: list[dict] | None = None, model: str | None = None, max_tokens: int = 4096, temperature: float = 0.7, thinking_enabled: bool | None = None, ) -> LLMResponse: self.messages.append(messages) content = self.responses.pop(0) return LLMResponse(content=content, provider_name="stub", model="stub-model") def get_default_model(self) -> str: return "stub-model" class StaticRetriever: async def retrieve(self, **kwargs): return kwargs["candidates"][: kwargs["top_k"]] class LoaderWithFullSkill: def build_selection_candidates(self) -> list[dict[str, str]]: return [ { "name": "docker-debug", "description": "General container tips.", "version": "v1", "content_hash": "abc", } ] def load_published_skill(self, name: str) -> str | None: if name != "docker-debug": return None return """--- description: General container tips. tools: - search_files --- # Docker Debug Use this skill when doing Docker log triage and container failure analysis. """ def get_skill_record(self, name: str): return SimpleNamespace(version="v1", content_hash="abc", tool_hints=["search_files"]) def test_skill_selection_receives_thinking_mode() -> None: provider = RecordingProvider() assembler = SkillAssembler(loader=SimpleNamespace()) selected = asyncio.run( assembler._select_skill_names( task_description="summarize daily news", candidates=[{"name": "daily-news", "description": "Summarize news"}], provider=provider, model="Qwen3.6-35B", thinking_enabled=False, ) ) assert selected == ["daily-news"] assert provider.thinking_enabled is False def test_skill_assembler_loads_detail_directly_for_small_candidate_sets() -> None: provider = SequencedProvider(['["docker-debug"]']) assembler = SkillAssembler(loader=LoaderWithFullSkill(), retriever=StaticRetriever()) result = asyncio.run( assembler.assemble( task_description="debug a failing Docker container", provider=provider, model="stub-model", ) ) assert [skill.name for skill in result.activated_skills] == ["docker-debug"] assert result.activated_skills[0].tool_hints == ["search_files"] assert [item["stage"] for item in result.llm_interactions] == ["final"] assert len(provider.messages) == 1 first_user_prompt = provider.messages[0][1]["content"] assert "Use this skill when doing Docker log triage" in first_user_prompt def test_skill_assembler_shortlists_before_loading_detail_for_large_candidate_sets() -> None: provider = SequencedProvider(['["docker-debug"]', '["docker-debug"]']) loader = LoaderWithFullSkill() original_candidates = loader.build_selection_candidates loader.build_selection_candidates = lambda: [ *original_candidates(), { "name": "other-skill", "description": "Other workflow.", "version": "v1", "content_hash": "def", }, ] assembler = SkillAssembler( loader=loader, retriever=StaticRetriever(), max_detailed_candidates=1, ) result = asyncio.run( assembler.assemble( task_description="debug a failing Docker container", provider=provider, model="stub-model", ) ) assert [skill.name for skill in result.activated_skills] == ["docker-debug"] assert [item["stage"] for item in result.llm_interactions] == ["shortlist", "final"] assert len(provider.messages) == 2 assert "Use this skill when doing Docker log triage" not in provider.messages[0][1]["content"] assert "Use this skill when doing Docker log triage" in provider.messages[1][1]["content"]