Strategy/projects/files/neuler/neuler_sprint1_plan.md
+

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 ночь)

Choose icon