Skip to content

Latest commit

 

History

History
245 lines (192 loc) · 20.6 KB

File metadata and controls

245 lines (192 loc) · 20.6 KB

Форма результата веб-поиска: строка -> структура

Лана: blocking_tools._web_search_sync, web_lookup.parse_result, test_web_lookup.py. Отчёт пишется по ходу работы, а не в конце.

Мои файлы: blocking_tools.py, web_lookup.py, test_web_lookup.py. Всё остальное только читал. Git не касался.

Замеры ДО правки (живые данные, только чтение)

Замер 1. Ссылка не выживает в строковой форме

Скрипт _measure_urls.py (разовый, удалён после замера): вытащил все ссылки из stomat_archive.db (117 847 реплик) и stomat_wiki.db (12 784 факта), прогнал каждую через обе формы, которые сегодня склеивает подпроцесс, и сравнил вынутое регуляркой с исходным.

архив: строк со ссылкой 592, ссылок извлечено 628
вики:  ссылок извлечено 5
уникальных ссылок всего: 556
из них со скобкой в самой ссылке: 1

форма tavily ("текст (url)"):        ссылка не восстановлена в 1 из 556
форма ddgs   ("текст\n(Source: url)"): ссылка не восстановлена в 1 из 556

было  https://ru.m.wikipedia.org/wiki/%D0%9E%D0%BA%D1%81%D0%B8%D0%B4_%D1%86%D0%B8%D1%80%D0%BA%D0%BE%D0%BD%D0%B8%D1%8F(IV)
стало https://ru.m.wikipedia.org/wiki/%D0%9E%D0%BA%D1%81%D0%B8%D0%B4_%D1%86%D0%B8%D1%80%D0%BA%D0%BE%D0%BD%D0%B8%D1%8F(IV

Ссылка на «Оксид циркония(IV)» — не выдуманный пример, это реальная ссылка, которую врач присылал в чат, и материал (диоксид циркония) стоматологический. Закрывающая скобка теряется, потому что класс символов в регулярке [^\s\)\]]+ останавливается на первой ). Врач получает ссылку, которая не открывается: утверждение выглядит подтверждённым, а проверить его нельзя.

Доля 1/556 = 0.18% выглядит мелко, но в живом архиве всего 2 ссылки на википедию и скобка у одной из двух: ru.wikipedia именно так разводит стоматологические омонимы — Пломба_(стоматология), Мост_(стоматология), Коронка_(стоматология). То есть класс попадает ровно по тому запросу, ради которого энциклопедия и держится в списке источников (уровень 4 — «определение термина»).

Замер 2. Что именно отдаёт подпроцесс сегодня

Скрипт _measure_shape.py (разовый, удалён): провайдеры подменены фальшивыми модулями, сети нет.

tavily отдал: ['Биодентин закрывает перфорацию. (https://ru.wikipedia.org/wiki/Пломба_(стоматология))']
   тип элемента: str
   разбор вернул url: 'https://ru.wikipedia.org/wiki/Пломба_(стоматология'   <- битая

ddgs отдал: ['МТА даёт лучший прогноз (мета-анализ).\n(Source: https://ru.m.wikipedia.org/wiki/Оксид_циркония(IV))']
   тип элемента: str
   разбор вернул url: 'https://ru.m.wikipedia.org/wiki/Оксид_циркония(IV'    <- битая

Подтверждено: наружу уходят СТРОКИ, в двух разных формах, и ссылка в обеих ломается.

Замер 3. Худшее следствие — обзор из PubMed выбрасывается как реклама клиники

Если в тексте выдержки есть свой маркер со ссылкой (страницы такое содержат: блок «источник», цитата, ссылка партнёра), маркерная регулярка срабатывает на ссылку ИЗ ТЕКСТА, а не на ту, которую вернул провайдер. Дальше по этому хосту считается уровень доверия и работает отсев рекламы:

строковая форма, выдержка из pubmed с чужим маркером внутри:
   принято: []                      | отсеяно как реклама: ['implant-msk.ru: домен рекламный (implant)']
структурная форма той же пары:
   принято: ['pubmed.ncbi.nlm.nih.gov'] | отсеяно как реклама: []

Систематический обзор из PubMed выбрасывается целиком, и врач получает NOTHING_USABLE — «в открытых источниках нашлась только реклама клиник». Это ложь в лицо: обзор был на руках. Честно: частоту этого класса на живых данных я НЕ измерил — в 117 847 репликах архива готовый маркер (Источник: url) внутри текста встречается 0 раз, а страницы провайдеров мне недоступны (сеть в тесте запрещена и правильно). То есть класс показан, но не взвешен.

Мёртвый ли search_engine.py — замер

rg -g '!.git' -g '!*.log' по всему дереву:

perform_search:
  search_engine.py:33       определение
  search_engine_safe.py:10  определение
  web_lookup.py:5,26        упоминания в комментарии
search_engine (как модуль):
  ни одного import ни в одном .py; только комментарии (summarizer.py:9 — «импортировался,
  но не вызывался ни разу», удалён) и отчёты _fix_/_recon_
динамический импорт (importlib / __import__): 17 совпадений, ни одного с search_engine
не-py файлы (start.bat и прочее): 0 совпадений

Вердикт прямой: search_engine.py мёртв целиком — его perform_search не вызывает никто, _run_ddg_sync тоже, модуль никем не импортируется, включая динамический импорт и start.bat. Более того, он опасен на импорте: from ddgs import DDGS и TavilyClient(api_key=...) на уровне модуля, то есть импорт лезет в конфиг за ключом. search_engine_safe.py мёртв так же — его perform_search не вызывается ниоткуда, он лишь обёртка над живым blocking_tools.web_search_async. Не удаляю: решение лида.

Перепроверено мной заново (rg по ВСЕМУ дереву, не только по *.py, с ограничением области): perform_search встречается только двумя определениями плюс упоминания в комментариях и отчётах; строка search_engine не встречается ни в одном import. Вердикт прошлого прогона подтверждаю.

Но мёртвая обёртка ломается о новую форму — замер

search_engine_safe.py:22 склеивает результаты строкой: "\n\n".join(results). Список словарей такой join не принимает. Замер (_measure_shape2.py, сети нет, провайдер подменён):

НОВАЯ форма (dict):  вернул врачу: 'Ошибка поиска.'   ссылка в ответе: False
   в журнале: search subprocess wrapper failed: sequence item 0: expected str instance, dict found
СТАРАЯ форма (str):  вернул врачу: 'Биодентин закрывает перфорацию. (https://pubmed...)'  True

TypeError гасится широким except Exception на строке 23, и врач получает Ошибка поиска. на КАЖДЫЙ успешный поиск — при найденных источниках на руках. Сегодня это не стреляет: обёртку никто не вызывает. Но именно её web_lookup называет обёрткой поиска, то есть она первый кандидат на подключение, и в момент подключения поиск будет отвечать «ошибка» с непустой выдачей. Файл не мой — правку не делал, см. «Для лида».

Правка 2. Чужая ссылка внутри выдержки уезжала в промпт (найдено в этом прогоне)

Ссылку из ПОЛЯ мы теперь не трогаем — но текст выдержки чистился только от той ссылки, которую из него же и вынули. Все ОСТАЛЬНЫЕ ссылки оставались внутри выдержки и уезжали в промпт.

Замер на живых данных (_probe_text_urls.py, боевые базы через mode=ro):

stomat_archive.db: строк со ссылкой 591, из них ссылка в СЕРЕДИНЕ текста 110  (18.6%)

выдержка из pubmed с адресом клиники в середине текста:
   до правки:  рекламная ссылка ВНУТРИ промпта: True    в подписи для врача: False
   после:      рекламная ссылка ВНУТРИ промпта: False   в подписи для врача: False
   строковая форма до правки: True, после: False   (дефект был на ОБОИХ путях)

Последствие для врача: адрес клиники не проходил ни ранжирование по уровню, ни отсев рекламы, и в подписи его нет — но в выдержке он есть, а промпт велит опираться на выдержки. Модель цитирует его как часть систематического обзора. Врач получает ссылку на рекламу, выданную за источник уровня 1.

Правка: _strip_urls в web_lookup.py, применяется на ОБОИХ путях (структурном и строковом) — расхождение путей и есть корень всей ланы. Выдержка, от которой после снятия ссылок не осталось прозы, отбрасывается: источник с номером и пустым текстом даёт модели место, на которое можно сослаться [1] под утверждением, которого в источнике нет.

18.6% — это доля по сообщениям ВРАЧЕЙ в архиве, а не по страницам провайдеров. Страницы провайдеров мне недоступны (сеть в тесте запрещена, и правильно), так что это ближайший доступный замер того, что ссылка внутри прозы на этом домене — рядовой случай, а не край.

Саботаж: 8 диверсий, 7 поймано, 1 НЕ поймана и закрыта

Бэкапы _bak_shape_bt.py.bak / _bak_shape_wl.py.bak, после каждой диверсии восстановление и сверка md5. Итоговые хеши совпадают с точкой до саботажа.

# Что сломано Упало проверок Поймано
1 _web_search_sync (tavily) снова клеит строку f"{content} ({url})" 7 да
2 parse_result: структурный путь не возвращает url из поля, а ищет в тексте 8 да
3 _trim_unbalanced_parens срезает ) безусловно 5 да
4 _as_search_entry не разбирает старые строки (parsed = None) 4 да
5 web_search_async не отсеивает пустышки 2 да
6 _as_search_entry молча глотает порченую нагрузку (снята запись в журнал) 0 НЕТ
7 тот же фильтр с and вместо or — находка без ссылки выбрасывается 2 да
8 _strip_urls превращён в no-op 3 да

Диверсия 2 воспроизвела ровно тот отказ, ради которого лана существует: обзор с pubmed уехал в ads как implant-msk.ru: домен рекламный (implant), и врач получил «в открытых источниках нашлась только реклама клиник» — при обзоре на руках.

Диверсия 6 не была поймана — это исправлено

Снятие _throttled(...) из ветки «ни строка, ни структура» не уронило НИ ОДНОЙ из 94 проверок: PASSED: 94 FAILED: 0, код возврата 0. Ни один тест не подавал в поиск None/int, то есть ветка порченой нагрузки не исполнялась вовсе. Последствие: элемент выбрасывается молча — «поиск нашёл 3, показал 1» без строки в журнале, и разобраться потом нечем.

Закрыто пятью проверками поведения (журнал ловится LogTrap по образцу test_report_targets.py): порченая нагрузка не доезжает до врача, живая находка рядом не теряется, в журнале есть запись, в записи назван ТИП, уровень не ниже WARNING. Повторный прогон той же диверсии — 3 проверки падают.

Слабое место теста, которое диверсия 1 вскрыла

Первый прогон диверсии 1 не просто провалил проверки — он ОБОРВАЛ тест: entry["url"] у строки даёт TypeError, и оставшиеся ~25 проверок не запустились, а сводки PASSED/FAILED в выводе не было вовсе. То есть поломка формы прятала за собой любую другую поломку разбора. Добавлен помощник _entry(seq, index): не структура -> пустой словарь. После этого та же диверсия даёт честные PASSED: 87 FAILED: 7 и все проверки доезжают.

Заодно две проверки вида «в тексте нет http» переписаны на «текст НЕПУСТОЙ и в нём нет http»: на пустом тексте первая форма проходила сама собой, то есть сломанная форма прошла бы как исправная.

Прогоны (после правки, полное дерево не запускалось — только соседи)

test_web_lookup      PASSED: 106  FAILED: 0   (было 66 до ланы, +40)
test_rag_quality     PASSED:  53  FAILED: 0
test_fix_subprocess  PASSED:  59  FAILED: 0
test_import_safety   PASSED: 260  FAILED: 0
test_budget_nesting  PASSED:  29  FAILED: 0
test_observability   PASSED: 133  FAILED: 0
test_fix_cascade     PASSED: 102  FAILED: 0
test_gemini_pacing   PASSED:  18  FAILED: 0
test_validator_policy PASSED: 35  FAILED: 0
test_search_case     PASSED:  33  FAILED: 0

test_isolation даёт PASSED: 106 FAILED: 2, и это НЕ моя лана: test_media_loop_guard.py изолирует базу -- пишет в боевую stomat_bot.db. Файл чужой, не трогал.

Что осталось НЕ проверенным — честно

  • Живого поиска не было ни одного. Все провайдеры подменены фальшивыми модулями, сеть не трогалась. Что tavily и ddgs сегодня реально отдают в полях content/url и body/href, проверено по коду вызова, а не ответом сервиса.
  • Дочерний процесс целиком не запускался. Проверен _web_search_sync прямым вызовом и web_search_async с подменённым _run_json_tool; настоящий python blocking_tools.py web-search со stdin/stdout не прогонялся, то есть сериализация проверена json.dumps/loads в том же процессе, а не через реальный канал.
  • Частота «чужой маркер внутри текста» на страницах провайдеров не измерена. В 117 847 репликах архива готовый (Источник: url) внутри текста встречается 0 раз; 18.6% — это ссылка в середине прозы, близкий, но не тот же самый класс. Класс показан, но не взвешен.
  • Отбрасывание выдержки без прозы — решение с потерей. Источник с рабочей ссылкой, у которого весь текст был одной ссылкой, теперь не показывается врачу вовсе. Считаю это правильным (пустой источник с номером хуже), но это выбор, а не доказанный факт.
  • _strip_urls вырезает и полезные ссылки. Если провайдер вернул выдержку, где ссылка на DOI стоит внутри прозы, врач её в тексте не увидит — только основной источник в подписи. Замера «как часто внутри выдержки лежит ссылка ЛУЧШЕ основной» нет.
  • Ничего из этого не вызывается ботом. Вся лана остаётся мёртвым кодом до подключения: web_search_async не вызывает никто, кроме тестов и мёртвой обёртки.

Для лида

  1. search_engine_safe.py:22 — ломается о новую форму. "\n\n".join(results) на списке словарей даёт TypeError, широкий except на :23 превращает его в Ошибка поиска. для врача при НЕПУСТОЙ выдаче. Файл не мой. Правка на две строки: return "\n\n".join(f"{e['text']} ({e['url']})" if e.get("url") else e["text"] for e in results) — либо, что честнее, выкинуть обёртку и звать web_lookup.prepare, ради которого лана и делалась.
  2. search_engine.py и search_engine_safe.py мёртвы оба (замер выше). search_engine.py вдобавок дёргает конфиг за ключом на уровне модуля. Удаление — решение лида, не делал.
  3. test_isolation ловит чужое нарушение: test_media_loop_guard.py пишет в боевую stomat_bot.db. Это лана media, не моя.
  4. clip_at_sentence в web_lookup.py дублирует _clip_at_sentence в assistant.py — унаследованный комментарий обещает свести их в одно, когда ядро освободится. Не мой файл.