Skip to main content
Glama
tvermolaev-source

Deep Research MCP Server

Deep Research MCP Server 🔎

MCP-сервер глубокого поиска. Подключается к Open WebUI (и любому MCP-клиенту) и позволяет запускать полноценный deep-research режим: итеративный веб-поиск через SearXNG + парсинг страниц через Crawl4AI + синтез финального ответа через LLM.

Архитектура и логика итеративного research-цикла портированы с Vane (TypeScript) на Python.


🏗️ Архитектура

┌─────────────────────┐
│   Open WebUI        │ ← фронтенд, видит стрим прогресса
│   (MCP-клиент)      │
└──────────┬──────────┘
           │ MCP (streamable-http / SSE)
           ▼
┌─────────────────────┐
│  Deep Research MCP  │ ← этот сервер
│  server.py          │
└──────────┬──────────┘
           │
    ┌──────┴──────┐
    ▼             ▼
┌────────┐  ┌─────────┐
│SearXNG │  │Crawl4AI │ ← внешние сервисы
│ поиск  │  │ парсинг │
└────────┘  └─────────┘
           │
           ▼
       ┌────────┐
       │  LLM   │ ← OpenAI-совместимый endpoint
       └────────┘   (Ollama / Open WebUI / OpenAI)

Related MCP server: research

✨ Что умеет

  • 🔍 deep_research(query, mode) — главный тулчейн:

    • итеративный цикл планирование → поиск → парсинг → синтез (как в Vane)

    • режимы speed (2 итерации), balanced (6), quality (25)

    • LLM Source Planner — перед стартом цикла отдельный LLM-вызов классифицирует запрос (social / academic / news / videos / general / all) и подбирает релевантные категории SearXNG + адаптивную политику реранкинга.

    • Query Rephraser — каждый длинный пользовательский запрос переформулируется через LLM в SEO-friendly ключевые слова (порт webSearchRetrieverPrompt из Vane).

    • стримит в UI все шаги: план, поисковые запросы, найденные URL, чтение страниц, чанки финального ответа

  • 🌐 web_search(query) — одиночный запрос в SearXNG (для отладки)

  • 📄 scrape_url(url) — одиночный парсинг страницы через Crawl4AI

  • 🧭 Роутинг моделей — отдельные planner / worker модели (сильная для планирования и синтеза, лёгкая для извлечения фактов); опционально worker может ходить на другой endpoint (например, локальный Ollama) — LLM_PLANNER_MODEL / LLM_WORKER_MODEL.

🚀 Быстрый старт

1. Локально (для разработки)

# Клонируем и заходим
cd Deep_Research

# Создаём venv
python3.11 -m venv .venv
source .venv/bin/activate

# Зависимости
pip install -r requirements.txt

# Конфиг
cp .env.example .env
# отредактируй .env: SEARXNG_URL, LLM_BASE_URL, LLM_MODEL, ...

# Запуск
python -m deep_research

Сервер поднимется на http://localhost:8765 (по умолчанию) и начнёт слушать MCP-транспорт streamable-http. Точка входа для клиентов:

  • http://localhost:8765/mcp — MCP-over-HTTP

2. В Docker

docker compose up --build

Поднимаются два контейнера:

  • searxng — на http://localhost:8888 (UI) и http://searxng:8080 (API)

  • deep-research-mcp — на http://localhost:8765/mcp

🔌 Подключение к Open WebUI

Где в UI

  1. Кликни по аватару / имени пользователя (правый верхний угол) → Settings.

  2. В левом меню выбери раздел Tools (он же «Инструменты» в русской локали).

  3. Справа увидишь блок «+ Add MCP Server» — жми туда.

В Open WebUI >= 0.5 нативная поддержка MCP встроена, ничего дополнительно ставить не нужно.

Какую строку вписать в поле URL

Это самое важное — зависит от того, где крутится Open WebUI относительно твоего контейнера deep-research-mcp. Конечная точка у нас всегда одна:

http://<адрес_контейнера>:8765/mcp

Адрес зависит от сценария:

Сценарий

URL для подключения

OW и MCP на одном хосте, оба в Docker, одна сеть

http://deep-research-mcp:8765/mcp (имя сервиса)

OW локально (без Docker), MCP в Docker на той же машине

http://localhost:8765/mcp

OW в Docker, MCP на хосте (или в отдельном контейнере без общей сети)

http://host.docker.internal:8765/mcp

Удалённый сервер/VPS: OW и MCP на разных машинах

http://<публичный_IP_или_домен>:8765/mcp

За reverse-proxy с TLS (nginx/Caddy/Traefik)

https://<домен>/mcp

⚠️ Порт 8765/tcp должен быть открыт в файрволе на хосте, где крутится MCP. В docker-compose.yml у нас он уже проброшен: ports: "8765:8765".

Остальные поля формы

Поле

Значение

Name

Deep Research (любое понятное имя)

Type / Transport

Streamable HTTP — соответствует нашему MCP-транспорту

Authentication

None — для локального/частного использования

Активация

После добавления:

  • в списке тулзов должна появиться строка Deep Research → поставь галочку Enable;

  • по желанию включи «Show in Model Selector», чтобы тул был виден в селекторе модели.

Проверка в чате

Открой новый чат, выбери ту LLM, которая указана у тебя в .env как LLM_MODEL (например, qwen2.5:7b), и спроси:

«Используй deep_research и расскажи про квантовые компьютеры в 2026 году»

Если всё ок, в UI будет стрим прогресса:

🔍 Plan: «Okay, the user wants to know about …» 🌐 Searching for: ["renewable energy 2025", "solar panel efficiency 2025"] 📄 Reading: https://example.com/report ✍️ Synthesizing answer…

Если что-то не работает — чеклист

  1. Не резолвится URL. С машины, где крутится OW, выполни:

    curl -i http://<адрес>:8765/mcp

    Должен прийти HTTP-ответ (не Connection refused, не timeout).

  2. Тул не вызывается моделью. Включи в Admin Panel → Settings → Models → выбранная модель → раздел Capabilities — должна быть галка Tool Calling.

  3. Нет стриминга прогресса, только финальный ответ. Убедись, что в Settings → Tools → MCP Servers выбран тип Streamable HTTP, а не устаревший /sse.

  4. 421 Misdirected Request. Мы уже включили middleware в server.py, который переписывает Host-заголовок — если всё равно возникает, проверь, что контейнер запущен из свежего образа (docker compose pull && docker compose up -d).

  5. OW видит тул, но без описания. Проверь раздел Logs в OW — обычно там видно, прошёл ли MCP-handshake. На нашей стороне смотри docker logs deep-research-mcp.

⚙️ Конфигурация (.env)

Все параметры читаются из переменных окружения. Если переменная не задана, используются эффективные встроенные дефолты — так что сервер работает качественно даже с пустым .env.

Переменная

Дефолт

Описание

SEARXNG_URL

http://searxng:8080

URL SearXNG

SEARXNG_LANGUAGE

ru

Язык поиска (можно en, en-all)

SEARXNG_CATEGORIES

general

Дефолтные SearXNG-категории (используются, если LLM-планировщик не сработал). Допустимые: general news science social videos images files music it map

SEARXNG_ENGINES

google,bing,duckduckgo

Список движков

SEARXNG_SAFESEARCH

0

0/1/2

LLM_BASE_URL

http://localhost:11434/v1

OpenAI-совместимый endpoint

LLM_API_KEY

ollama

API-ключ

LLM_MODEL

qwen2.5:7b

Базовая модель (используется обеими ролями, если роли не заданы)

LLM_PLANNER_MODEL

(пусто → LLM_MODEL)

Сильная модель для планирования источников и финального синтеза

LLM_WORKER_MODEL

(пусто → LLM_MODEL)

Слабая/дешёвая модель для извлечения фактов из чанков

LLM_WORKER_BASE_URL

(пусто → LLM_BASE_URL)

Опц. отдельный endpoint для worker'а (например, локальный Ollama с 3B-моделью)

LLM_WORKER_API_KEY

(пусто → LLM_API_KEY)

Опц. отдельный API-ключ для worker'а

MCP_HOST

0.0.0.0

Хост MCP-сервера

MCP_PORT

8765

Порт

MAX_ITERATIONS_SPEED/BALANCED/QUALITY

2 / 6 / 25

Лимиты итераций по режимам

MAX_PARALLEL_CRAWLS

5

Одновременных парсингов

MAX_RESULTS_PER_QUERY

10

Сколько просить у SearXNG на запрос

CRAWL_TIMEOUT_SEC

60

Таймаут парсинга URL

MIN_RESULT_SCORE

0.0

Фильтр по SearXNG-score. 0 = не фильтровать

RESULTS_TOP_K_PER_QUERY

10

Сколько URL оставлять на запрос после реранкинга

DOMAIN_BOOST_THRESHOLD

2

Буст домена, если он встретился по ≥N запросам

BLOCKED_DOMAINS

(пусто)

Список доменов под безусловный отсев (opt-in)

PRIORITY_DOMAINS

(пусто)

Список доменов-экспертов, +100 к рангу (opt-in)

SOCIAL_DOMAINS

12 источников

Домены соцсетей — поднимаются в режиме «ищи в социальных сетях»

ACADEMIC_DOMAINS

29 источников

Домены научных/академических источников — для режима «факт-чек»

NEWS_DOMAINS

36 источников

Домены мировых СМИ — для режима «новости/статьи»

INTENT_DETECTION

true

Включён ли детектор намерений в запросе пользователя

🧭 Адаптивные режимы поиска

Researcher автоматически распознаёт намерение пользователя по тексту запроса и переключает режим реранкинга. Никаких жёстких блокировок «из коробки» — только мягкие приоритеты: попавшие в приоритет домены поднимаются в топ, остальные не отрезаются.

Что пишет пользователь

Распознанный режим

Что происходит

«ищи в социальных сетях / vk / reddit / twitter»

social

Соцсети поднимаются в топ

«научное подтверждение / факт-чек / peer-reviewed / arxiv»

academic

Академические домены в топе

«новости / статьи в СМИ / press»

news

СМИ в топе

«ищи всё / включи все / без фильтров»

all

Никаких приоритетов и блокировок

обычный запрос без подсказок

neutral

Только то, что задано в PRIORITY_DOMAINS/BLOCKED_DOMAINS

Детектор использует встроенные RU+EN-ключевики (расширяются через INTENT_KEYWORDS_SOCIAL, INTENT_KEYWORDS_ACADEMIC и т.п.). Если INTENT_DETECTION=false — режим всегда neutral.

🔎 Как работает реранкинг

Мы не блокируем домены по умолчанию. Что работает из коробки:

  1. Глобальная дедупликация по URL — один URL не повторяется между запросами и итерациями.

  2. Min-score фильтр (опционально) — только если задать MIN_RESULT_SCORE > 0.

  3. Domain-boost — домен, встретившийся по ≥DOMAIN_BOOST_THRESHOLD разным запросам, получает +10·hits к рангу.

  4. Тонкая настройка через ENVBLOCKED_DOMAINS и PRIORITY_DOMAINS (opt-in).

  5. Top-K на запрос — после реранкинга оставляем RESULTS_TOP_K_PER_QUERY URL.

  6. Адаптивный режим — если пользователь сказал «ищи в X», соответствующие домены получают +100 к рангу (остальные не трогаются).

Адаптивные списки SOCIAL_DOMAINS / ACADEMIC_DOMAINS / NEWS_DOMAINS уже предзаполнены авторитетными источниками мирового уровня (12 / 29 / 36 доменов соответственно) — см. src/deep_research/config.py.

Их критерии: скорость поступления информации, качество журналистики / peer-review, охват и доверие аудитории. Например:

  • SOCIAL_DOMAINS — Twitter/X, Reddit, Facebook, Instagram, LinkedIn, TikTok, Threads, Mastodon, YouTube, VK, Telegram.

  • ACADEMIC_DOMAINS.edu (все университеты мира), arxiv.org, biorxiv.org, scholar.google.com, nature.com, science.org, cell.com, thelancet.com, nejm.org, sciencedirect.com, Springer, Wiley, JSTOR, PLOS, Frontiers, MDPI, IEEE, ACM, …

  • NEWS_DOMAINS — Reuters, AP, AFP, BBC, Guardian, NYT, WaPo, WSJ, FT, Bloomberg, CNN, Al Jazeera, DW, France 24, Le Monde, El País, Spiegel, Asahi, SCMP, Straits Times + научпоп-порталы (Nature, Scientific American, New Scientist, TechCrunch, The Verge, Wired)

    • ведущие русскоязычные СМИ (РИА, ТАСС, РБК, Ведомости, Коммерсантъ, Интерфакс, Лента, Газета).

Чтобы заменить дефолтный набор — просто задайте переменную в .env, она переопределит встроенный список (но не сольётся с ним).

Все домены указаны как суффиксы (.com/....org) или TLD-префиксы (.edu), поэтому внутренние поддомены (m.twitter.com, cs.mit.edu, …) тоже матчатся.

🧠 LLM-планирование источников (до старта цикла)

Помимо keyword-детектора по тексту, в начале каждого исследования Researcher выполняет один отдельный LLM-вызов (через planner-модель), который классифицирует запрос и выбирает SearXNG-категории:

Intent

SearXNG categories

Когда выбирается

social

social

«что обсуждают в твиттере / на реддите / в телеграме»

academic

science (+ general для подстраховки)

«научные статьи / факт-чек / peer-reviewed / arxiv»

news

news

«последние новости / пресс-релизы»

videos

videos (+ general)

«видео на ютубе / обучающие ролики»

general

general

обычный web-поиск

all

general + news + science + social + videos

«ищи всё»

План стримится в UI как plan-событие и сохраняется в self._source_plan, оттуда попадает в SearXNGClient.search_many(categories=…) — поиск сразу идёт по правильным категориям (а не только по «general»).

При любой ошибке LLM (нет endpoint'а, битый JSON) — fallback на SourcePlan.default() с категорией general. Пайплайн не падает.

🧬 Модельный роутинг: planner (сильная) / worker (слабая)

В Researcher'е используются две роли LLM через LLMFactory:

Роль

Что делает

Требования

Пример модели

Planner

_plan_sources() — выбор источника перед циклом• Главный цикл research() — каждый ход (preamble + tools)_synthesize() — финальный ответ

Логика, JSON, следование инструкциям

qwen2.5:14b/32b, llama3.1:70b, gpt-4o, claude-sonnet

Worker

_extract_facts() — извлечение фактов из скрапленного контента (chunked)

Быстро, дёшево, JSON

qwen2.5:3b, llama3.2:3b, phi3:mini, gpt-4o-mini

Если роли не заданы в .env — обе используют базовый LLM_MODEL (полная обратная совместимость).

Пример конфигурации:

# Сильная модель — планирование и синтез
LLM_PLANNER_MODEL=qwen2.5:14b
# Слабая модель — извлечение фактов
LLM_WORKER_MODEL=qwen2.5:3b
# Опционально: worker на отдельном endpoint
LLM_WORKER_BASE_URL=http://localhost:11434/v1
LLM_WORKER_API_KEY=ollama

LLMFactory сам решает — открывать два независимых HTTP-клиента или переиспользовать один, если конфиги planner и worker совпадают.

📜 Полный flow одного исследования (sequence diagram)

Вот что происходит от момента, как ты отправил запрос в Open WebUI, до момента, как ты увидел финальный ответ со ссылками:

Пользователь       Open WebUI           MCP-сервер              LLM (planner)       SearXNG          LLM (worker)       Crawl4AI
     │                  │                     │                       │                 │                  │                │
     │ "расскажи про    │                     │                       │                 │                  │                │
     │  квантовые        │                     │                       │                 │                  │                │
     │  компьютеры 2026" │                     │                       │                 │                  │                │
     ├─────────────────►│ deep_research(      │                       │                 │                  │                │
     │                  │   query, mode)      │                       │                 │                  │                │
     │                  ├────────────────────►│                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ 0. _plan_sources()    │                 │                  │                │
     │                  │                     ├──────────────────────►│                 │                  │                │
     │                  │                     │ SOURCE_PLANNER_PROMPT │                 │                  │                │
     │                  │                     │   + query             │                 │                  │                │
     │                  │                     │                       │ JSON: intent,   │                  │                │
     │                  │                     │                       │   categories    │                  │                │
     │                  │                     │◄──────────────────────┤                 │                  │                │
     │                  │                     │ self._source_plan =   │                 │                  │                │
     │                  │                     │   SourcePlan(...)     │                 │                  │                │
     │                  │                     │ emit_plan("academic") │                 │                  │                │
     │                  │◄───── log ──────────│                       │                 │                  │                │
     │                  │  "plan: academic    │                       │                 │                  │                │
     │                  │   [science]"        │                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ 1. Главный цикл       │                 │                  │                │
     │                  │                     │ tools=[preamble,      │                 │                  │                │
     │                  │                     │         web_search,   │                 │                  │                │
     │                  │                     │         scrape_url,   │                 │                  │                │
     │                  │                     │         done]         │                 │                  │                │
     │                  │                     ├──────────────────────►│                 │                  │                │
     │                  │                     │ system + history      │                 │                  │                │
     │                  │                     │                       │ tool_call(      │                  │                │
     │                  │                     │                       │   preamble)     │                  │                │
     │                  │                     │◄──────────────────────┤                 │                  │                │
     │                  │                     │ emit_plan("Plan:…")   │                 │                  │                │
     │                  │◄───── log ──────────│                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ tool_call(            │                 │                  │                │
     │                  │                     │   web_search)         │                 │                  │                │
     │                  │                     │◄──────────────────────┤                 │                  │                │
     │                  │                     │ search_many(          │                 │                  │                │
     │                  │                     │   queries=[...],      │                 │                  │                │
     │                  │                     │   categories=["science"]              │                  │                │
     │                  │                     ├───────────────────────┼────────────────►│                 │                │
     │                  │                     │                       │                 │ JSON results     │                │
     │                  │                     │◄──────────────────────┼─────────────────┤                  │                │
     │                  │                     │ rank_score + should_drop (FilterPolicy) │                  │                │
     │                  │                     │ emit_search_results   │                 │                  │                │
     │                  │◄───── log ──────────│                       │                 │                  │                │
     │                  │  "search_result:    │                       │                 │                  │                │
     │                  │   5 URLs"           │                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ tool_call(            │                 │                  │                │
     │                  │                     │   scrape_url)         │                 │                  │                │
     │                  │                     │◄──────────────────────┤                 │                  │                │
     │                  │                     │ crawl_many(urls)      │                 │                  │                │
     │                  │                     ├───────────────────────┼─────────────────┼──────────────────┼───────────────►│
     │                  │                     │                       │                 │                  │                │ markdown
     │                  │                     │◄──────────────────────┼─────────────────┼──────────────────┼────────────────┤
     │                  │                     │ _extract_facts() ──► WORKER LLM        │                  │                │
     │                  │                     │                       │                 │   chunked JSON   │                │
     │                  │                     │                       │                 │◄─────────────────┤                │
     │                  │                     │ emit_read_done(url, facts)             │                  │                │
     │                  │◄───── log ──────────│                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ tool_call(done)       │                 │                  │                │
     │                  │                     │◄──────────────────────┤                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ 2. _synthesize() ──► PLANNER LLM      │                  │                │
     │                  │                     │                       │ stream=markdown │                  │                │
     │                  │                     │◄──────────────────────┤                 │                  │                │
     │                  │                     │ emit_synthesis_chunk  │                 │                  │                │
     │                  │◄───── log ──────────│ (стрим чанков ответа) │                 │                  │                │
     │                  │   "## Квантовые…"   │                       │                 │                  │                │
     │                  │◄────────────────────│                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │                     │ emit_done(answer,     │                 │                  │                │
     │                  │                     │          sources)     │                 │                  │                │
     │                  │◄───── log ──────────│                       │                 │                  │                │
     │                  │                     │                       │                 │                  │                │
     │                  │ TextContent(markdown+│                       │                 │                  │                │
     │                  │   sources)          │                       │                 │                  │                │
     │                  │◄────────────────────│                       │                 │                  │                │
     │   видит финал    │                     │                       │                 │                  │                │
     │◄─────────────────┤                     │                       │                 │                  │                │

🔍 Куда какой LLM ходит (всё в одном месте)

Этап

LLM-роль

Модель (если задана)

Что делает

0. _plan_sources()

Planner

LLM_PLANNER_MODEL

Классифицирует запрос, выбирает SearXNG-категории

1. Главный цикл research()

Planner

LLM_PLANNER_MODEL

Каждый ход: preamble + выбор tools + JSON-валидация

2. scrape_url_extract_facts

Worker

LLM_WORKER_MODEL

Извлечение фактов из чанков (механическая работа)

3. _synthesize()

Planner

LLM_PLANNER_MODEL

Финальный markdown-ответ со ссылками

Если роли не заданы — все 4 этапа идут через LLM_MODEL (обратная совместимость).

📊 Что увидит пользователь в Open WebUI

После запуска deep_research в UI приходит стрим событий (через ctx.session.send_log_message):

  1. plan: "source plan: academic (science, general) — нужны научные источники"

  2. plan: "Okay, the user wants to know about quantum computers in 2026…"

  3. search_start: queries=["quantum computing 2026", "quantum supremacy recent"]

  4. search_result: 5-10 URL с заголовками

  5. read_start: urls=[…]

  6. read_done: url + первые 500 символов extracted_facts

  7. synthesis_chunk: чанки markdown-ответа (печатаются как пишутся)

  8. done: финальный ответ + список источников

Все эти шаги прокидываются через MCP-шину (EventBussend_log_message), Open WebUI рисует их как «task steps» в чате.

🛡️ Поведение при ошибках

Сценарий

Что произойдёт

LLM-планировщик недоступен (endpoint не отвечает)

_plan_sources() ловит исключение, self._source_plan остаётся дефолтным (categories=["general"]). Цикл продолжается как раньше.

LLM вернул битый JSON / текст без JSON

_parse_source_plan() возвращает None → план остаётся дефолтным.

LLM вернул невалидные категории/intent

Whitelist-фильтр, fallback на general + categories=["general"].

Worker-LLM недоступен во время _extract_facts

Возвращается исходный chunk текста без извлечения фактов (логируется warning).

Planner и worker идентичны по конфигу

LLMFactory создаёт один HTTP-клиент и переиспользует для обеих ролей — никакого overhead'а.

LLM_PLANNER_MODEL / LLM_WORKER_MODEL пустые

Обе роли используют базовый LLM_MODEL. Полная обратная совместимость.

🧪 Что покрыто тестами (87 тестов, все зелёные)

tests/test_researcher.py       — 1 e2e-тест: цикл plan→search→scrape→done→synthesis
tests/test_llm_factory.py      — 13 тестов: factory shared/distinct, ENV-overrides,
                                      SourcePlan parser (strict JSON / markdown fence /
                                      garbage recovery / invalid categories / intent),
                                      worker_endpoint
tests/test_streaming.py        — pub/sub EventBus, close-unblocks-subscribers
tests/test_filtering.py        — intent detection, FilterPolicy, rank_score, should_drop,
                                      интеграция с Researcher (intgration + planner-arg fix)
tests/test_searxng_client.py   — SearXNGClient search/search_many с categories
tests/test_tools.py            — CrawlClient
tests/test_prompts.py          — 10 тестов: normalize_mode() + get_researcher_system_prompt()
                                      (защита от KeyError на невалидный mode от MCP-клиента)

Локально прогоняется так (Python 3.11+):

python scripts/check_syntax.py    # ast-валидация всех .py-файлов
PYTHONPATH=src pytest tests/ -v   # 87 тестов

В CI (GitHub Actions) гоняется в матрице Python 3.11 + 3.12 + Docker build.

📁 Структура проекта

Deep_Research/
├── docker-compose.yml         # SearXNG + MCP-сервер
├── Dockerfile                 # образ MCP-сервера
├── requirements.txt
├── pyproject.toml
├── .env.example
├── src/deep_research/
│   ├── server.py              # FastMCP-сервер (entrypoint)
│   ├── researcher.py          # главный цикл итеративного поиска
│   ├── intent.py              # детектор намерений по тексту запроса (RU+EN)
│   ├── filter_policy.py       # политики фильтрации/реранкинга по intent
│   ├── llm_client.py          # OpenAI-compatible клиент + streaming + tool calls
│   ├── prompts.py             # промпты (портированы с Vane)
│   ├── streaming.py           # EventBus — стрим событий в UI
│   ├── config.py              # конфигурация из .env
│   ├── types.py               # dataclasses + tool-схемы
│   └── tools/
│       ├── searxng_client.py  # HTTP-клиент SearXNG
│       └── crawl_client.py    # Crawl4AI-обёртка + httpx-fallback
└── tests/
    ├── test_streaming.py
    ├── test_searxng_client.py
    ├── test_tools.py
    ├── test_researcher.py
    ├── test_filtering.py      # intent-детектор + политики + реранкинг + интеграция с Researcher
    └── test_prompts.py        # normalize_mode + get_researcher_system_prompt (10 тестов)

🆕 Что нового в v0.4.3 (current)

  • 🛡️ Защита от KeyError: 'comprehensive' — MCP-клиенты (например, Open WebUI) иногда присылали произвольный mode (любая строка, не входящая в speed/balanced/quality), и пайплайн падал с непонятным traceback в логах. Теперь:

    • В prompts.py добавлена normalize_mode() с whitelist + fallback на balanced.

    • get_researcher_system_prompt() нормализует mode перед индексом _MODE_PROMPTS.

    • Researcher.research() нормализует на входе и фиксирует валидное значение.

    • MCP-тул deep_research_tool в server.py нормализует mode ещё до старта и пишет WARNING: deep_research: unknown mode 'X' from MCP client, using 'balanced'.

  • 🧪 CI стал зелёным — заодно починен долгоживущий баг в tests/test_filtering.py: 5 интеграционных тестов не передавали 4-й позиционный аргумент planner в Researcher._execute_one (появился в v0.4.2 вместе с Query Rephraser). Локально: 87 passed.

  • 🧹 Гигиена.tmp_* добавлен в .gitignore (чтобы ad-hoc scratch-файлы не попадали в коммиты).

🆕 Что нового в v0.4.2

  • 🪄 Query Rephraser — порт webSearchRetrieverPrompt из Vane. Каждый сгенерированный LLM-агентом поисковый запрос перед отправкой в SearXNG переформулируется через LLM в SEO-friendly ключевые слова (2-5 слов, без вопросительных слов и булевых операторов). Управляется через QUERY_REPHRASER=true|false и MAX_QUERY_LENGTH=120. На короткие запросы (<=30 символов, <=5 слов) не действует. При недоступности LLM-rephraser'а — regex-нормализация как fallback.

  • Новые fewshot-примеры в prompts.QUERY_REPHRASER_FEWSHOTS — 5 примеров (вопрос → ключевые слова) для стабилизации выхода модели.

🆕 Что нового в v0.4.1

  • 🕷️ Фикс краулераCrawlClient теперь корректно переиспользует AsyncWebCrawler между вызовами (раньше каждый crawl_one() создавал новый инстанс, что ломало async with semantics). Также добавлена поддержка async withCrawlClient стал полноценным async context manager'ом.

🆕 Что нового в v0.4.0

  • 🧠 LLM-driven Source Planner — перед стартом итеративного цикла Researcher делает один отдельный LLM-вызов (через planner-модель), который классифицирует запрос и подбирает релевантные категории SearXNG + адаптивную политику реранкинга. Стримится в UI как plan-событие, fallback на general при ошибках.

  • 🧭 Роутинг моделей planner / workerLLMFactory создаёт две роли LLM: сильную (planner) для планирования и синтеза, лёгкую (worker) для извлечения фактов. Опционально worker может ходить на отдельный endpoint (например, локальный Ollama с 3B-моделью). Полная обратная совместимость — если роли не заданы, обе используют LLM_MODEL.

  • 🛡️ ASGI-middleware — обход 421 Misdirected Request от uvicorn при Host-заголовке вроде deep-research:8765 или <внешний_IP>:8765. Работает на любой версии uvicorn (не требует host_header_validation=False из 0.32+).

  • 📜 OpenAPI JSON — middleware также отдаёт валидный /openapi.json, /docs и /mcp/openapi.json (минимальный stub), чтобы Open WebUI не получал 404 при авто-детекте.

🆕 Что нового в v0.3.0

  • Адаптивная фильтрация по доменам — Researcher распознаёт намерение пользователя по тексту запроса (social / academic / news / all / neutral) и мягко поднимает нужный тип источников в топ. Никаких автоблоков — другие источники не отсекаются, только получают меньший ранг. Детектор использует RU+EN ключевые слова, расширяемые через INTENT_KEYWORDS_*.

  • Предзаполненные наборы доменов мирового уровняSOCIAL_DOMAINS (12), ACADEMIC_DOMAINS (29), NEWS_DOMAINS (36). Подобраны по скорости поступления информации, качеству журналистики/peer-review, охвату и доверию аудитории. Переопределяются через .env целиком (без слияния с дефолтом).

  • Никаких жёстких блокировок по доменам — убрали авто-блоклист соцсетей. Хотите отсечь конкретный домен — задайте BLOCKED_DOMAINS=… (opt-in).

  • Новые модули:

    • src/deep_research/intent.py — детектор намерений по тексту запроса.

    • src/deep_research/filter_policy.py — политики реранкинга под каждый intent (make_policy, matches_domain, rank_score, should_drop).

  • Расширенные тестыtests/test_filtering.py покрывает детектор, политики, матчинг доменов (включая .edu-TLD) и интеграцию с Researcher. 52 теста, все зелёные.

  • API для MCP/UI — результат web_search теперь содержит поле policy (intent, priority_count, blocked_count), чтобы клиентский UI мог показать, в каком режиме выполнен поиск.

🧬 Портировано с Vane

Vane (TypeScript)

Deep Research MCP (Python)

lib/agents/search/researcher/index.ts

src/deep_research/researcher.py

…/actions/registry.ts

inline в researcher.py

…/actions/plan.ts

промпт __reasoning_preamble в prompts.py

…/actions/search/webSearch.ts

web_search tool

…/actions/search/baseSearch.ts

web_search tool + дедуп по URL

…/actions/scrapeURL.ts

scrape_url tool + _extract_facts

lib/searxng

tools/searxng_client.py

lib/scraper

tools/crawl_client.py (Crawl4AI)

lib/session.emitBlock

streaming.EventBus

� CI/CD и деплой

См. DEPLOY.md — полная инструкция:

  • GitHub Actions: тесты + автосборка Docker-образа

  • Multi-arch образ (amd64 + arm64) в GHCR

  • ./scripts/release.sh patch → push тега → авторелиз

  • ./scripts/deploy.sh v0.1.0 user@server → деплой одной командой

�📜 Лицензия

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Multi-purpose research MCP server integrating web search, deep research, web scraping, research methodology routing, and GPT Researcher report generation.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    A local MCP server for research that bundles SearXNG meta-search and Crawl4AI web scraping, offering tools for search, scraping, structured extraction, and deep research.
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to perform unified web research through a single MCP server, including search, page fetching, recursive crawling, document parsing, YouTube transcript extraction, and deep multi-query research.
    3
    -