Agent Selection

Алгоритм выбора агента для обработки пользовательского запроса. Реализация: bot/app/services/agent_selection.py

Входные параметры

async def select_agent(
    db: "Database",
    *,
    intent: str = "",           # Интент пользователя (web_search, code, sql...)
    process_code: str = "",     # Код процесса (default_chat, daily_digest...)
    scope: str = "direct",      # direct / group
    user_text: str = "",        # Текст запроса (для контекста)
    complexity: str = "simple", # simple / complex / high
    team_id: Optional[str] = None,
    chat_mode: str = "direct",
) -> dict[str, Any]:

intent

Классифицированный интент из классификатора. Примеры:

  • • web_search / web / research
  • • code / sql / db / data
  • • planning / plan / decompose
  • • summarize / summary / digest
  • • review / evaluate
  • • workflow / automate / schedule

process_code

Код процесса, если запрос пришёл через процесс:

  • • default_chat
  • • web_search / web_research_brief
  • • daily_digest
  • • kpi_coach / deadline_report

Алгоритм выбора

1

Поиск по политике

Запрос к agent_selection_policy с matching по intent, process_code, scope:

matched = await db.find_agent_by_policy(
    intent=intent,
    process_code=process_code,
    scope=scope,
)

-- SQL запрос (пример):
SELECT * FROM agent_selection_policy
WHERE is_active = true
  AND (match_intent @> ARRAY[intent] OR match_process_code @> ARRAY[process_code])
  AND scope = ANY(match_scope)
ORDER BY priority DESC
LIMIT 1;
2

Fallback на core_orchestrator

Если агент не найден, используется core_orchestrator:

FALLBACK_AGENT = "core_orchestrator"

if matched and matched.get("agent_code"):
    agent_code = matched["agent_code"]
else:
    agent_code = FALLBACK_AGENT
    selection_reason_parts.append("fallback=core_orchestrator")
3

Загрузка профиля и методов

Загружаются agent_profile, agent_methods, agent_sub_agents:

profile = await db.get_agent_profile(agent_code)
methods = await db.get_agent_methods(agent_code)
sub_agents = await db.get_agent_sub_agents(agent_code)
4

Сборка prompt из фрагментов

Сначала ищутся явно привязанные фрагменты, если нет — авто-подбор:

mapped_fragments = await db.get_agent_prompt_fragments(agent_code)

if not mapped_fragments:
    # Авто-подбор по agent_family и fragment_type
    agent_family = _infer_agent_family(agent_code)
    fragment_type = _infer_fragment_type(intent)
    top_fragments = await db.get_top_prompt_fragments(
        fragment_type=fragment_type,
        agent_family=agent_family,
        limit=3,
    )
5

Сборка system prompt

Функция _assemble_prompt:

def _assemble_prompt(profile, fragments):
    parts = []
    
    # 1. Goal из профиля
    if profile.get("goal"):
        parts.append(profile["goal"])
    
    # 2. Tool constraints
    if profile.get("allowed_tools"):
        parts.append(f"Available tools: {', '.join(allowed)}")
    if profile.get("forbidden_tools"):
        parts.append(f"Forbidden tools: {', '.join(forbidden)}")
    
    # 3. Фрагменты по assembly_mode
    for frag in fragments:
        mode = frag.get("assembly_mode", "append")
        if mode == "prepend": prepend.append(content)
        elif mode == "replace": return content  # Полная замена!
        else: append.append(content)
    
    return "

".join(prepend + parts + append)

Выходные данные

return {
    "selected_agent_code": "web_researcher",
    "selected_fragments": [...],          # Список фрагментов
    "assembled_system_prompt": "...",     # Готовый system prompt
    "system_prompt_hash": "a1b2c3d4",     # SHA256 hash для кэширования
    "fragment_ids": [...],
    "allowed_tools": ["web_search", "rag_search"],
    "forbidden_tools": ["db_query"],
    "preferred_model": "gemma3:4b",
    "default_temperature": 0.4,
    "response_contract": {"format": "markdown", "max_tokens": 4096},
    "execution_mode": "research",         # simple / multi_step / research / code
    "selection_reason": "intent_match=web_search",
    "methods": [...],                     # Доступные методы
    "sub_agents": [...],                  # Sub-agent отношения
    "chat_mode": "both",
}

Маппинг intent → agent_family / fragment_type

_infer_agent_family()

Файл: bot/app/services/agent_selection.py:171

mapping = {
    "core_orchestrator": "orchestrator",
    "web_researcher": "researcher",
    "coder_executor": "coder",
    "sql_analyst": "coder",
    "task_decomposer": "planner",
    "summarizer": "",
    "critic_reviewer": "reviewer",
    "memory_keeper": "",
    "n8n_composer": "coder",
}

_infer_fragment_type()

Файл: bot/app/services/agent_selection.py:186

mapping = {
    "web_search": "research",
    "web": "research",
    "code": "coding",
    "sql": "coding",
    "db": "coding",
    "data": "coding",
    "planning": "planning",
    "summarize": "summarization",
    "review": "review",
    "workflow": "tool_use",
}

Режим исполнения (execution_mode)

Функция _infer_execution_mode() из bot/app/services/agent_selection.py:209

def _infer_execution_mode(intent: str, complexity: str) -> str:
    if complexity in ("complex", "high"):
        return "multi_step"
    
    research_intents = {"web_search", "web", "research"}
    if intent in research_intents:
        return "research"
    
    code_intents = {"code", "sql", "db", "data"}
    if intent in code_intents:
        return "code"
    
    return "simple"

simple

Один LLM вызов, прямой ответ

research

web_search + synthesis

code

code/SQL generation + review

multi_step

Декомпозиция + несколько шагов

Пример flow

Пользователь пишет: "найди информацию о React 19"

1.Classifier определяет intent = web_search
2.select_agent(intent="web_search") ищет в agent_selection_policy
3.Находит: web_researcher с match_intent=['web_search', 'web']
4.Загружает profile, methods=[web_search, web_research_brief, ...]
5.Собирает prompt: goal + "Available tools: web_search, rag_search"
6.Возвращает: selected_agent_code="web_researcher", execution_mode="research"

API Endpoint

POST /internal/agent/select
Authorization: Bearer {INTERNAL_API_TOKEN}
Content-Type: application/json

{
  "intent": "web_search",
  "process_code": "",
  "scope": "direct",
  "user_text": "найди информацию о React 19",
  "complexity": "simple"
}

Связи