neuler_sprint1_plan
Neuler Sprint 1: Literature Scout (Solution A)
Создано: 2026-03-09 05:00 MSK (Феанор, worker ночь)
Для: встречи 09 мар 2026
Горизонт: 7 дней (09–16 мар)
TL;DR
Один CLI-скрипт neuler scout за ~200 строк Python, реиспользующий SDR инфраструктуру. Демо за 7 дней. Всё что нужно — уже написано.
$ neuler scout --topic "operator splitting SGD" --depth S
→ 12 релевантных статей, Markdown-отчёт, 3 knowledge gaps
Что уже готово (SDR → Neuler reuse)
| Компонент | Файл в SDR | Что берём |
|---|---|---|
| OpenAlex search | src/open_deep_research/search/openalex_search.py |
Весь класс |
| SearXNG/Tavily | src/open_deep_research/search/ |
SearXNG adapter |
| Authority scoring | coordinator.py: _extract_authority_metadata() |
DOI + cited_by |
| Query cleaning | coordinator.py: QueryParser.clean_query() |
strip engine names |
| Env loading | .env.benchmark |
BASE_URL + model |
Оценка: ~70% функционала уже написано. Sprint 1 = “склеить” в новый CLI.
Структура Sprint 1 (что писать)
neuler/
├── scout.py # main CLI (80–100 строк) — ПИШЕМ ПЕРВЫМ
├── librarian.py # обёртка над SDR search (60–80 строк) — ПИШЕМ ВТОРЫМ
├── formatter.py # Markdown → report (40–50 строк) — ПИШЕМ ТРЕТЬИМ
└── gap_finder.py # LLM-вызов для knowledge gaps (50–60 строк) — ПИШЕМ ЧЕТВЁРТЫМ
ИТОГО: ~250 строк кода, 1–2 рабочих дня.
День 1 (09 мар, после встречи): scout.py + librarian.py
scout.py — ядро CLI
#!/usr/bin/env python3
"""neuler scout: Literature Scout CLI"""
import argparse
import asyncio
from librarian import Librarian
from formatter import format_report
DEPTHS = {"S": 3, "M": 7, "L": 15} # num search queries
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("--topic", required=True)
parser.add_argument("--depth", choices=["S", "M", "L"], default="S")
parser.add_argument("--output", default=None) # None = stdout
args = parser.parse_args()
lib = Librarian(max_queries=DEPTHS[args.depth])
papers = await lib.search(args.topic)
gaps = await lib.find_gaps(args.topic, papers)
report = format_report(args.topic, papers, gaps)
if args.output:
with open(args.output, "w") as f: f.write(report)
print(f"✅ Отчёт сохранён: {args.output} ({len(papers)} статей, {len(gaps)} gaps)")
else:
print(report)
if __name__ == "__main__":
asyncio.run(main())
librarian.py — search wrapper
"""Librarian: wraps SDR search infrastructure"""
import sys
sys.path.insert(0, "/root/sdr_repo/src")
import asyncio
from open_deep_research.search.openalex_search import OpenAlexSearch
from open_deep_research.coordinator import QueryParser
class Librarian:
def __init__(self, max_queries: int = 5):
self.max_queries = max_queries
self.openalex = OpenAlexSearch()
async def search(self, topic: str) -> list[dict]:
clean = QueryParser.clean_query(topic)
# Параллельный поиск: topic + синонимы
queries = self._expand_topic(clean)[:self.max_queries]
tasks = [self.openalex.search(q) for q in queries]
results = await asyncio.gather(*tasks, return_exceptions=True)
papers = []
seen_dois = set()
for r in results:
if isinstance(r, Exception): continue
for p in (r or []):
doi = p.get("doi", "")
if doi not in seen_dois:
seen_dois.add(doi)
papers.append(p)
# Score by cited_by_count + relevance
papers.sort(key=lambda p: p.get("cited_by_count", 0), reverse=True)
return papers[:20] # top 20
async def find_gaps(self, topic: str, papers: list) -> list[str]:
"""LLM-вызов: найти knowledge gaps из списка статей"""
if not papers: return ["Статьи не найдены — тема слишком узкая?"]
# Реализуется в Day 3 (gap_finder.py)
return ["[gap_finder.py: TODO Day 3]"]
def _expand_topic(self, topic: str) -> list[str]:
"""Генерация поисковых запросов из темы"""
# Простая эвристика: основной запрос + subtopics
return [
topic,
topic + " convergence theory",
topic + " benchmark comparison",
]
День 2 (10 мар): formatter.py + тест на реальном топике
formatter.py
"""Format papers list → readable Markdown report"""
from datetime import datetime
def format_report(topic: str, papers: list, gaps: list) -> str:
lines = [
f"# Literature Scout: {topic}",
f"*Создано: {datetime.now().strftime('%Y-%m-%d %H:%M')} MSK*",
f"*Найдено: {len(papers)} статей, {len(gaps)} knowledge gaps*",
"",
"---", "",
"## Топ статьи", "",
]
for i, p in enumerate(papers[:15], 1):
title = p.get("title", "Unknown")
doi = p.get("doi", "")
year = p.get("publication_year", "?")
cited = p.get("cited_by_count", 0)
abstract = (p.get("abstract", "") or "")[:200]
lines.extend([
f"### {i}. {title} ({year})",
f"**DOI**: {doi} | **Cited**: {cited}",
f"{abstract}...",
"",
])
lines.extend([
"---", "",
"## Knowledge Gaps", "",
*[f"- {gap}" for gap in gaps],
"",
])
return "\n".join(lines)
Тест День 2:
cd /root/neuler
python scout.py --topic "operator splitting SGD" --depth S --output test_report.md
Ожидаем: 5–10 статей, Markdown-отчёт, читаемый результат.
День 3 (11 мар): gap_finder.py + LLM интеграция
"""gap_finder: LLM-вызов для knowledge gap extraction"""
from openai import AsyncOpenAI # Groq compatible
import os, json
async def find_gaps_llm(topic: str, papers: list, model: str = "llama-3.1-8b-instant") -> list[str]:
client = AsyncOpenAI(
base_url=os.getenv("CLOUD_BASE_URL"),
api_key=os.getenv("GROQ_API_KEY")
)
abstracts = "\n".join([
f"- {p.get('title','?')} ({p.get('publication_year','?')}): {(p.get('abstract') or '')[:150]}"
for p in papers[:10]
])
prompt = f"""Тема: {topic}
Найденные статьи:
{abstracts}
Задача: выяви 3–5 knowledge gaps — направлений, которых НЕТ в найденных статьях.
Формат: JSON-список строк. Пример: ["Gap 1", "Gap 2", "Gap 3"]
ТОЛЬКО JSON, без пояснений."""
resp = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=500,
)
try:
return json.loads(resp.choices[0].message.content)
except Exception:
return ["LLM вернул не-JSON — проверь промпт"]
День 4–5 (12–13 мар): eval + полировка
Eval план:
1. Запустить на топике “operator splitting neural network training”
2. Даниил вручную проверяет: есть ли в отчёте Alecsa 2020, Beck-Teboulle 2009, Eisenmann 2024?
3. Coverage score = найдено/ожидаемых * 100%
4. Target: ≥70% для depth=L (15 queries)
Критерии Done:
- neuler scout --topic "X" --depth S работает без ошибок
- Отчёт читаемый, ≥5 релевантных статей
- knowledge_gaps непустые и осмысленные
- Coverage ≥60% на operator splitting benchmark set
Параллельный трек: Eval Framework (опционально)
Если есть время (Альберт?):
# bench/astabench_a.py
KNOWN_PAPERS = [
"10.1007/978-3-319-91578-4_5", # Alecsa 2020
"10.1007/s10107-012-0516-2", # Beck-Teboulle
]
def coverage_score(found_papers: list, known_papers: list = KNOWN_PAPERS) -> float:
found_dois = {p.get("doi", "") for p in found_papers}
return sum(doi in found_dois for doi in known_papers) / len(known_papers)
Timeline
| Дата | Задача | Кто | Done = |
|---|---|---|---|
| 09 мар | scout.py + librarian.py | Даниил | neuler scout запускается |
| 10 мар | formatter.py + первый прогон | Даниил | Читаемый Markdown отчёт |
| 11 мар | gap_finder.py | Даниил | gaps непустые |
| 12–13 мар | eval + coverage | Даниил + Альберт | coverage ≥60% |
| 14–15 мар | полировка + README | Даниил | можно показать команде |
| 16 мар | демо на следующей встрече | all | coverage score + live demo |
Решения, которые нужны на встрече 09 мар
| Вопрос | Опции | Рекомендация |
|---|---|---|
| Solution A или B первым? | A (литература) vs B (гипотезы) | A — инструменты готовы |
| Отдельный repo или в SDR? | Отдельный neuler/ vs sdr_repo/neuler/ |
Отдельный — разные задачи |
| LLM для gap_finder: Groq llama или Claude? | Groq (бесплатно) vs Claude API (качество) | Claude haiku если есть ключ, иначе Groq |
| Eval: ручной или автоматический? | Ручной (быстро) vs auto (масштабируемо) | Ручной для MVP, auto потом |
| Кто пишет код? | Даниил solo vs пара vs команда | Даниил + один PR от Альберта |
Фёанор, 2026-03-09 05:00 MSK (auto, worker ночь)