| name | eop-scan |
| description | Сканира eop.bg за отворени обществени поръчки, категоризира и приоритизира по ROI. Използва се при търсене на нови поръчки за участие. |
| allowed-tools | Read, Write, Glob, Bash(mkdir *), Bash(python *), Agent, mcp__plugin_playwright_playwright__* |
EOP Скенер за обществени поръчки
Чеклист за проследяване
Прогрес на сканиране:
- [ ] Отваряне на eop.bg и прилагане на филтри
- [ ] Извличане на всички отворени поръчки
- [ ] Извличане на метрики от документите (Phase 2)
- [ ] Категоризация по тип
- [ ] Приоритизация по ROI
- [ ] Представяне на резултати
- [ ] Запис на scan summary файл
- [ ] Избор на поръчки за участие
Зареждане на домейн конфигурация
Прочети ./eopowers/domain.md с Read tool. Ако файлът не съществува — съобщи: "Няма домейн конфигурация. Стартирайте /init първо." и спри.
Извлечи от файла:
- Ключова дума за филтър (секция "Търсене → Ключова дума за филтър")
- Положителни ключови думи (секция "Търсене → Положителни ключови думи" — таблица с категории, CPV кодове, ключови думи)
- Отрицателни ключови думи (секция "Търсене → Отрицателни ключови думи" — таблица)
- Приоритет при множество категории (секция "Търсене → Приоритет при множество категории")
- Бележки за категоризация (секция "Търсене → Бележки за категоризация", ако съществува)
- Метрики (секция "Метрики" — всички дефинирани метрики с име, кратко име, единица, regex, минимална стойност)
- Праг за рентабилност (секция "Метрики → Праг за рентабилност")
- Приоритет на файлове за метрики (секция "Метрики → Приоритет на файлове за метрики")
- Допълнителни ключови думи (секция "Търсене → Допълнителни ключови думи за търсене", ако съществува — списък)
- Регион (секция "Регион → Общини", ако съществува — списък от имена на общини)
Използвай тези данни навсякъде по-долу вместо хардкоднати стойности.
Инструкции за сканиране (Playwright)
Стъпка 1: Навигация
Отвори https://app.eop.bg/today с browser_navigate.
Страницата е Angular SPA — изчакай съдържанието да се зареди. Използвай browser_snapshot (НЕ screenshot) за да провериш дали е заредено.
Стъпка 2: Филтри за търсене (мулти-ключова дума)
ВАЖНО: eop.bg третира множество ключови думи като AND (И), не OR (ИЛИ). Търсенето "СМР" + "обновяване" връща 0 резултата. Затова всяка ключова дума се търси ОТДЕЛНО.
Събери всички ключови думи за търсене:
- Основната ключова дума от domain.md (секция "Ключова дума за филтър")
- Всички допълнителни ключови думи от domain.md (секция "Допълнителни ключови думи за търсене")
За ВСЯКА ключова дума изпълни:
- Натисни бутон "Търсене" за да отвориш панела с филтри.
- Ако има вече въведена ключова дума — изтрий я (натисни X до нея).
- В полето "Ключова дума" (textbox "Въведете дума/израз") въведи текущата ключова дума и натисни бутон "Добави".
- Натисни бутон "Приложи" за да филтрираш резултатите.
- Извличай данните от всички страници (Стъпка 3).
- Логирай: "Търсене по '[keyword]': [N] резултата"
Филтърът "отворени за участие" е приложен по подразбиране — не го променяй.
Стъпка 3: Извличане на данни
-
Обходи всички страници с резултати (всяка показва ~10 записа).
-
За навигация между страниците: натисни бутоните с номера в пагинацията (1, 2, 3... N).
ВАЖНО: Използвай exact: true при търсене на бутон по номер, за да избегнеш конфликт (напр. бутон "2" vs "20").
Пример: page.getByRole('button', { name: '2', exact: true })
-
Изчакай 2 секунди след всяка смяна на страница за зареждане на данни.
-
За всеки запис извлечи:
- ID на поръчката (от URL-а, напр.
/today/575004 → 575004)
- Наименование (заглавие на поръчката)
- Възложител
- Начин на възлагане (тип процедура)
- Краен срок
- Прогнозна стойност (ако е видима в листинга)
-
За по-бързо извличане, използвай browser_run_code вместо отделни snapshot-и:
async (page) => {
const items = await page.$$eval('a[href*="/today/"]', links => {
return links
.filter(a => a.parentElement?.tagName?.includes('TENDER') || a.closest('li'))
.map(a => ({
id: a.getAttribute('href').split('/').pop(),
text: a.textContent.replace(/\s+/g, ' ').trim().substring(0, 300)
}))
.filter(item => item.id && item.id.match(/^\d+$/));
});
return JSON.stringify(items);
}
-
Дедупликация: След обхождане на всички ключови думи и всички страници, премахни дублирани записи по ID. Логирай:
- За всяка ключова дума: "Търсене по '[keyword]': [N] резултата"
- Общо: "Общо: [raw] → Уникални: [unique] (дубликати: [dupes])"
-
Регионално групиране: Ако в domain.md има секция "Регион":
- За всяка поръчка, провери дали възложителят съдържа някое от имената на общини (case-insensitive substring match).
- Раздели резултатите на две групи:
- Регионални (buyer match) — показват се първи, Phase 2 enrichment е задължителен
- Извънрегионални — показват се отделно, enrichment е по избор
- Логирай: "Регионални: [N] от [Total] уникални"
Стъпка 4: Обработка на грешки
- При неуспешно зареждане — повтори до 3 пъти с 10 секунди пауза между опитите.
- Таймаут за зареждане на страница: 30 секунди.
- Ако страницата не се зареди след 3 опита — запиши грешката и продължи със следващата.
Стъпка 5: Извличане на метрики (Phase 2 — Enrichment)
След извличане на основните данни и прилагане на keyword филтъра, за ВСЯКА поръчка, която не е изключена:
-
Покажи прогрес: "📐 Анализирам [N]/[Total]: [title]..."
-
Ако ./eopowers/offers/<offer-id>/meta.md вече съществува — пропусни (вече е анализирана).
-
Създай директория:
mkdir -p ./eopowers/offers/<offer-id>/attachments
-
Навигирай до https://app.eop.bg/today/<offer-id> с browser_navigate.
-
Направи browser_snapshot за да намериш секцията "Прикачени файлове".
5b. От snapshot-а извлечи прогнозната стойност — търси текст "Прогнозна стойност (без ДДС)" и стойността до него (формат: EUR X XXX XXX,XX). Парсвай: премахни интервалите между цифрите, замени запетая с точка. Пример: EUR 1 303 052,27 → 1303052.27.
-
Изтегляне на документи (приоритет: Export ZIP):
Метод 1 (предпочитан): Export ZIP
Метод 2 (fallback): Индивидуални файлове
-
Разархивиране и нормализация на пътища:
python3 -c "
import zipfile, os, subprocess, shutil
attachments = './eopowers/offers/<offer-id>/attachments'
# Extract ZIPs
for f in [x for x in os.listdir(attachments) if x.endswith('.zip')]:
zpath = os.path.join(attachments, f)
zipfile.ZipFile(zpath).extractall(attachments)
# Extract RARs
for f in [x for x in os.listdir(attachments) if x.endswith('.rar')]:
rpath = os.path.join(attachments, f)
subprocess.run(['unrar', 'x', '-o+', rpath, attachments + '/'], capture_output=True)
# Normalize Windows backslash paths — flatten all nested files to attachments/
for root, dirs, files in os.walk(attachments):
for fname in files:
src = os.path.join(root, fname)
dst = os.path.join(attachments, fname)
if src != dst and not os.path.exists(dst):
shutil.move(src, dst)
# Remove empty subdirectories
for root, dirs, files in os.walk(attachments, topdown=False):
if root != attachments and not os.listdir(root):
os.rmdir(root)
"
-
Конвертирай .doc файлове в .docx за парсване (НЕ .txt — губи таблици):
for f in ./eopowers/offers/<offer-id>/attachments/*.doc; do
[ -f "$f" ] && libreoffice --headless --convert-to docx --outdir "$(dirname "$f")" "$f" 2>/dev/null
done
-
Парсвай документите за метрики (виж по-долу).
9b. Visual PDF fallback за сканирани документи:
Ако Python скриптът отпечата NEED_VISUAL_PDF_FALLBACK (т.е. не всички метрики са намерени и има PDF файлове), използвай Read tool за визуално четене на PDF:
- Намери PDF файловете в attachments/ (приоритет по file_priority от domain.md).
- За всеки PDF (максимум 2 опита), прочети първите 5 страници с Read tool:
Read: ./eopowers/offers/<offer-id>/attachments/<file>.pdf (pages: "1-5")
- От визуалното съдържание, извлечи липсващите метрики.
- Това е бавна операция — използвай само като последен опит.
За да активираш fallback-а, добави в края на Python скрипта (преди for key in patterns:):
if len(results) < len(patterns):
missing = [k for k in patterns if k not in results]
pdf_candidates = [f for f in files if f.endswith('.pdf')]
if pdf_candidates:
print(f'NEED_VISUAL_PDF_FALLBACK missing={",".join(missing)} files={",".join(os.path.basename(f) for f in pdf_candidates[:2])}')
- Timeout: 120 секунди на поръчка (180 за архиви над 20MB). Ако не успее — запиши "—" за метриките и продължи.
Извличане на метрики от документи
Приоритет на файлове: спазвай реда от секция "Приоритет на файлове за метрики" в domain.md.
Генерирай Python скрипт за извличане на метрики, като адаптираш шаблона по-долу спрямо domain.md:
file_priority функцията — генерирай от секция "Приоритет на файлове за метрики" в domain.md. Всеки ред от списъка става условие с нарастващ приоритет (0, 1, 2...).
patterns dict — попълни от секция "Метрики" в domain.md: ключ = кратко име (lowercase), стойност = regex от domain.md.
min_values dict — попълни от "Минимална стойност" на всяка метрика в domain.md.
python -c "
import re, glob, subprocess, os
offer_id = '<offer-id>'
attachments_dir = f'./eopowers/offers/{offer_id}/attachments'
all_files = glob.glob(f'{attachments_dir}/*.docx')
pdf_files = glob.glob(f'{attachments_dir}/*.pdf')
# ГЕНЕРИРАЙ тази функция от domain.md 'Приоритет на файлове за метрики'
def file_priority(f):
name = os.path.basename(f).lower()
# [ред 1 от списъка] → return 0
# [ред 2 от списъка] → return 1
# ... и т.н.
return 99 # default
files = sorted(all_files, key=file_priority)
files.extend(sorted(pdf_files, key=file_priority))
# ПОПЪЛНИ от domain.md секция 'Метрики' — за всяка метрика:
# ключ = кратко_име.lower(), стойност = regex от domain.md
patterns = {
# 'зп': r'(?:Застроена площ|ЗП)...', ← пример
}
# ПОПЪЛНИ от domain.md — за всяка метрика 'Минимална стойност'
min_values = {
# 'зп': 10, ← пример
}
results = {}
for fpath in files:
try:
if fpath.endswith('.pdf'):
try:
import subprocess as sp
r = sp.run(['pdftotext', '-l', '5', fpath, '-'], capture_output=True, text=True, timeout=15)
text = r.stdout
except FileNotFoundError:
try:
import pdfplumber
with pdfplumber.open(fpath) as pdf:
text = '\n'.join(p.extract_text() or '' for p in pdf.pages[:5])
except ImportError:
continue
elif fpath.endswith('.docx'):
from docx import Document
doc = Document(fpath)
text = '\n'.join(p.text for p in doc.paragraphs)
for table in doc.tables:
for row in table.rows:
text += '\n' + ' '.join(cell.text for cell in row.cells)
else:
with open(fpath, 'r', errors='ignore') as f:
text = f.read()
for key, pat in patterns.items():
if key not in results:
m = re.search(pat, text, re.IGNORECASE)
if m:
val = m.group(1).replace(' ', '').replace(',', '.')
num = float(val)
if num >= min_values.get(key, 0):
results[key] = num
if len(results) == len(patterns):
break
except Exception:
continue
# Print results dynamically for each metric defined in domain.md
for key in patterns:
print(f'{key.upper()}={results.get(key, \"—\")}')
"
Изчисли рентабилност по формулата от domain.md (секция "Праг за рентабилност").
Ако стойността е под прага — маркирай с предупреждението от domain.md.
Категоризация
Използвай таблиците от domain.md (заредени в началото).
Алгоритъм:
- Извлечи заглавието на поръчката.
- Провери дали ОСНОВНИЯТ обхват (заглавие + CPV код) съвпада с отрицателни ключови думи от domain.md → изключи от резултатите. Логирай: "⛔ Изключена: [title] (причина: [matched keyword])".
- Провери CPV кода — ако съвпада с положителни ключови думи таблицата от domain.md, задай категория.
- Ако CPV не е наличен — търси положителни ключови думи в наименованието.
- Приоритет при множество категории: Ако съвпада с повече от една, приложи приоритета от domain.md. Спазвай бележките за категоризация (ако има).
- Ако няма съвпадение с нито една категория — маркирай като "Некатегоризирана".
ROI формула за приоритизация
ROI = (V * Cm * Ct) / (D * Cp)
V = Прогнозна стойност (нормализирана 0-1 спрямо максималната намерена стойност)
Cm = Съответствие с компетенции (1.0 = пълно, 0.5 = частично, 0.2 = несъответствие)
Ct = Времеви буфер (дни до краен срок / 30, максимум 1.0)
D = Сложност (1 = проста, 2 = средна, 3 = сложна; по брой обособени позиции)
Cp = Конкуренция (1 = Договаряне без обявление, 1.5 = Публично състезание, 2 = Открита процедура)
Представи като оценка 1-10 (по-висока = по-атрактивна). Нормализирай крайния резултат: score = min(10, ROI * 10).
За стойността на Cm: прочети ./eopowers/company-profile.md и провери компетенциите. Ако файлът не съществува — използвай 0.5 за всички поръчки.
Представяне на резултати
Покажи резултатите като Markdown таблица, сортирана по ROI (низходящо).
Колоните се генерират динамично от domain.md:
-
Фиксирани колони: #, Поръчка, Възложител, Стойност (EUR), Категория, ROI
-
За всяка метрика в domain.md: добави колона "[Кратко име] ([Единица])"
-
За всяка метрика с праг за рентабилност: добави колона "EUR/[Единица] [Кратко име]"
-
⚠️ — стойност под прага от domain.md
-
"—" — метриката не може да бъде извлечена от документите
Ако няма намерени резултати — съобщи: "Не бяха намерени отворени поръчки с текущите филтри."
Автоматичен запис на резултатите
След представяне на таблицата (преди въпроса за избор), автоматично запиши файл ./eopowers/offers/scan-YYYY-MM-DD.md със следното съдържание:
# Сканиране — YYYY-MM-DD
## Параметри
- Филтър: "[ключова дума от domain.md]"
- Общо резултати: [total] (извлечени: [raw], уникални: [unique])
- Сканирани страници: 1-N
- Изключени (отрицателни ключови думи): [count]
- Включени след филтриране: [count] (от уникалните)
- Обогатени с метрики: [count]
## Изключени поръчки
| ID | Наименование | Причина |
|----|-------------|---------|
| [id] | [title] | [matched negative keyword] |
## Включени по категории
| Категория | Брой |
|-----------|------|
| [category] | [count] |
## Обогатени поръчки (Phase 2 — с метрики)
[full enriched results table — колони генерирани динамично от domain.md]
[Легенда от domain.md — праг за рентабилност и символ за предупреждение]
## Всички необогатени поръчки
| ID | Обект | Възложител |
|----|-------|-----------|
[offers where metrics couldn't be extracted]
Потвърди: "💾 Резултатите са записани в ./eopowers/offers/scan-YYYY-MM-DD.md"
Избор и запис
След представяне на таблицата, попитай:
Кои поръчки искате да разгледате? (въведете номера, разделени със запетая)
За всяка избрана поръчка:
- Създай директория:
mkdir -p ./eopowers/offers/<offer-id>
- Създай
meta.md в директорията:
# Поръчка <offer-id>
- Наименование: [title]
- Възложител: [buyer]
- Прогнозна стойност: [value] EUR
- Краен срок: [deadline]
- Начин на възлагане: [procedure type]
- Категория: [category]
- ROI оценка: [score]
[За всяка метрика от domain.md:]
- [Пълно име на метриката]: [value] [единица]
- EUR/[единица] ([кратко име]): [value]
- URL: https://app.eop.bg/today/<offer-id>
- Дата на сканиране: [current date]
Потвърди записа: "Записани са [N] поръчки в ./eopowers/offers/."