Дизайн расширения web-search
Расширение из двух инструментов: web_search (найти кандидатов-URL в открытом вебе) и
extract_page (прочитать ОДИН URL и дистиллировать ответ на конкретную задачу).
Расположение: ~/.pi/agent/extensions/web-search/
Ключевые дизайн-решения
- Разделение «найти» и «прочитать».
web_searchвозвращает ТОЛЬКО список ссылок
(title + URL + snippet) — компактный «реестр кандидатов», по которому модель сама решает,
какие страницы кормить вextract_page. Контент не тащится через поиск. - Сырой HTML никогда не попадает в главную сессию.
extract_pageсам (детерминированно,
внутри расширения) скачивает страницу и выжимает читаемый текст черезtrafilatura
(fetch.py), затем отдаёт текст одному изолированному model-call'у (runExtractor),
который возвращает только дистиллированный ответ. Главная сессия получает результат, а не
простыню страницы — дёшево по контексту и без риска рекурсии агентов. - Один изолированный model-call вместо вложенного agent-loop'а. Дешевле и
детерминированнее, чем полный цикл «воркера»; рассуждения по извлечению остаются вне
главной сессии. - Бесплатный поисковый бэкенд без ключей. Только DuckDuckGo: сначала библиотека
ddgs, при её недоступности — fallback-скрейпhtml.duckduckgo.com/html/(без новых
зависимостей, толькоlxml). Никакого провайдерского зоопарка. - Python-скрипты как «планирование», а не «самодеятельность». Скрипты только ищут/фетчат
и печатают готовый LLM-friendly вывод; вся интерпретация — на стороне модели. - Windows-безопасность вывода. Оба скрипта оборачивают stdout в UTF-8
(errors="replace"), чтобы не ловитьUnicodeEncodeError/mojibake на cp1252. - Сайт-специфичный парсинг — вне ядра. Никакого «зоопарка» под Ozon/2ch/etc. — такое
живёт в скиллеmanuals, а ядро остаётся универсальным.
Поток данных
Инструмент web_search
Назначение
Найти кандидатов-ссылки в открытом вебе. Используется, когда факты могут быть
устаревшими/неизвестными: версии, изменения API, цены, новости, даты релизов, тексты ошибок,
всё после cutoff'а модели. Только находит URL — за содержимым идёт extract_page. Поиск
бесплатный (DuckDuckGo).
Схема параметров (typebox, additionalProperties: false)
| Параметр | Тип | Описание |
|---|---|---|
query |
string | Поисковый запрос. Поддерживает site:, intitle:, -exclude, "точная фраза" |
limit |
number? | Сколько результатов вернуть (1–10, по умолчанию 5) |
time_filter |
enum? | Свежесть: h=час, d=сутки (по умолчанию), w=неделя, m=месяц, y=год. Для новостей/версий/цен |
Выполнение
onUpdate→ промежуточный статусSearching: <query>.- Запуск
python scripts/search.py <query> -n <limit>(+--tbs qdr:<time_filter>). exit != 0→ throw с хвостом stderr/stdout (до 500 символов).- Возврат stdout как текстового контента;
details: { source: "duckduckgo" }.
search.py
ddgs_search(): предпочитаетddgs.DDGS().text(query, max_results, timelimit)—
структурированные результаты;timelimit=time_filterбез префиксаqdr:.- Fallback: POST на
https://html.duckduckgo.com/html/(UA Firefox,df= маппинг
qdr:*→d|w|m|y), XPath поdiv.web-result→h2/a.result__a(title, href) +
a.result__snippet. Лимит 15 c, результат обрезается доlimit. - CLI:
query,-n/--count(1–10, default 5),-t/--tbs(qdr:h|d|w|m|y). - Если результатов нет →
No results found.иexit 1. - Формат вывода (LLM-friendly, без JSON):
Инструмент extract_page
Назначение
Прочитать одну страницу и вернуть конкретный ответ на задачу. Даёшь URL + точную
одноцелевую задачу — он скачивает страницу, извлекает читаемый текст и дистиллирует только
запрошенное (никогда не dumps всей страницы). Использовать ПОСЛЕ web_search по выбранным
URL. Лучше всего работает на статических страницах: статьи, документация, changelog'и,
release notes, блоги, посты форумов.
Схема параметров (additionalProperties: false)
| Параметр | Тип | Описание |
|---|---|---|
url |
string | URL страницы |
task |
string | Конкретная инструкция, что вытащить. Точная и одноцелевая, напр. «какая последняя стабильная версия и её дата релиза», «извлеки все цены и опции на странице» |
Выполнение
onUpdate→Reading: <url>.python scripts/fetch.py <url>(с прокидываниемsignalдля отмены).- Ошибки:
exit != 0→ throw; stdout начинается сFETCH_ERROR→ throw; пустой текст →
throw «likely JS-heavy or blocked». runExtractor(ctx, { url, task, pageText: pageText.slice(0, 24_000) })— pageText
жёстко обрезается до 24 000 символов перед отправкой модели.- Возврат:
{ content: [text], details: { url, source: "extract_page", error? } }.
fetch.py
fetch():urllib.requestс UA Firefox,Accept: text/html…,Accept-Encoding: gzip, deflate;
timeout 25 c;_gunzip(gzip-магия\x1f\x8b, deflate\x78),_decodeпо
Content-Type: charset=…, fallbackutf-8→cp1252→ replace.- Приоритет —
trafilatura.extract(..., include_links/images/tables=False, output_format="txt"),
печать первых 20 000 символов. - Fallback: lxml →
<title>+text_content()со схлопыванием\n{2,}→ префиксTITLE: …. - Совсем при неудаче — сырой HTML (первые 20 000 символов).
subagent.ts — runExtractor
- Берёт активную модель сессии:
ctx.model; если модели/ключа нет →
ERROR: no active model available to extract./ERROR: no API key for the active model. - Один вызов
complete(model, { systemPrompt, messages }, { apiKey, headers, env })из
@earendil-works/pi-ai/compat; все text-блоки ответа склеиваются и trim'ятся. - Исключение →
isError: true, текстEXTRACTOR_ERROR: <message>. - «Предзнание» окружения зашито в промпт: python-библиотеки (requests 2.34, trafilatura 2.1,
lxml 6.1, bs4 4.15, ddgs 9.14, regex) уже стоят; воркер ничего не ставит и не проверяет.
Фетчит главная сессия (fetch.py), воркер только рассуждает и отвечает.
Промпты (дословно)
Tool description — web_search
promptSnippet: Search the web for candidate links (title+URL+snippet)
promptGuidelines:
Use web_search when the answer depends on info newer than your training cutoff (versions, prices, news, docs, error fixes).web_search returns links/snippets only. To actually read and answer from a page, follow up with extract_page on the chosen URL.
Tool description — extract_page
promptSnippet: Extract the answer to a specific task from a single web page (URL + task)
promptGuidelines:
Use extract_page after web_search returns candidate URLs — give it the URL and a precise task so it returns only the answer.extract_page works on static pages. For JS-heavy or login/anti-bot sites (marketplaces, imageboards) it may fail — tell the user rather than looping.
System prompt экстрактора (subagent.ts)
User-сообщение экстрактора (шаблон)
(pageText обрезан до 24 000 символов на стороне extract_page.ts.)
Промежуточные статусы (onUpdate)
web_search:Searching: ${query}extract_page:Reading: ${url}
Ограничения и поведения по ошибкам
- Статические страницы — да; JS-тяжёлые / login / антибот — скорее всего провал (пустой
текст или FETCH_ERROR) → модель сообщает пользователю, а не зацикливается. pageText≤ 24k символов модели, читаемый текст ≤ 20k символов из fetch.py — верхние
границы контекста осознанно жёсткие.- Отсутствие результатов поиска — явный
exit 1с текстомNo results found. - Отмена — через
signalв fetch; в web_search сигнал не прокидывается.