gitlink-cli/scripts/research/research.py

199 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""research.py — 科研情报全链路编排器(打通五主线)。
一条命令跑通:选分类(explore) → ③ 热点追踪 → ② 主体画像 → ④ 创新启发 → ⑤ 合规校验 → ① 项目分析。
in-process 复用各 pillar 的函数(非 subprocess共享数据产物按 pillar 分目录 + 顶层 chain.json/chain_report.md。
用法:
python research.py --category 深度学习 --repo owner/repo --out ./out/chain [--limit 20]
python research.py --category 深度学习 # 焦点仓自动取热点榜 top-1
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import collect as c # noqa: F401 (各 pillar 依赖共享)
import hotspot as H
import profile as P
import inspire as I
import repro as R
import lineage as L
def _write(path: str, text: str) -> None:
with open(path, "w", encoding="utf-8") as f:
f.write(text)
def _save(out_root: str, pillar: str, result: dict, report_md: str | None = None) -> None:
if not out_root:
return
d = os.path.join(out_root, pillar)
os.makedirs(d, exist_ok=True)
_write(os.path.join(d, f"{pillar}.json"), json.dumps(result, ensure_ascii=False, indent=2))
if report_md:
_write(os.path.join(d, "report.md"), report_md)
def _focal_from_trending(trending: list[dict]) -> tuple[str, str]:
for r in trending:
full = r.get("repo") or ""
if "/" in full:
o, rr = full.split("/", 1)
if o and rr and o.lower() != "default":
return o, rr
# 回退:允许 default/<repo>
for r in trending:
full = r.get("repo") or ""
if "/" in full:
o, rr = full.split("/", 1)
if o and rr:
return o, rr
return "", ""
def run_chain(category: str, repo: str | None = None, limit: int = 20, out: str = "") -> dict:
summary: dict = {"scenario": "chain", "category": category, "pillars": {}}
# ① 热点追踪
sys.stderr.write(f"[chain] ① 热点追踪:分类 {category}(上限 {limit})…\n"); sys.stderr.flush()
raw = H.collect(category=category, limit=limit)
hot = H.compute(raw)
trending = hot.get("trending_repos") or []
_save(out, "hotspot", hot, getattr(H, "_report_md", lambda x: "")(hot))
# ② 焦点仓
if repo and "/" in repo:
owner, repo_name = repo.split("/", 1)
else:
owner, repo_name = _focal_from_trending(trending)
summary["focal_repo"] = f"{owner}/{repo_name}" if owner else "(未解析到)"
sys.stderr.write(f"[chain] 焦点仓:{owner}/{repo_name}\n"); sys.stderr.flush()
summary["hotspot"] = {
"repo_count": (hot.get("meta") or {}).get("repo_count"),
"trending_top": trending[0]["repo"] if trending else "",
"topic_heat": [t["topic"] for t in (hot.get("topic_heat") or [])[:5]],
}
if not (owner and repo_name):
sys.stderr.write("[chain] ⚠ 未解析到焦点仓,后续画像/启发/合规/分析跳过\n")
_finalize(out, summary, hot)
return summary
ctx = {"category": category,
"topic_heat": [t["topic"] for t in (hot.get("topic_heat") or [])[:6]]}
# ③ 主体画像
sys.stderr.write("[chain] ② 主体画像…\n"); sys.stderr.flush()
proj = P.project_profile(owner, repo_name)
top_login = ((hot.get("core_scholars") or [{}])[0]).get("login") or ""
scholar = P.scholar_profile(top_login) if top_login else None
prof_result = {"scenario": "profile", "mode": "chain",
"project": proj, "scholar": scholar}
prof_md = []
prof_md.append(P.render_report({"mode": "project", "profiles": [proj]}))
if scholar:
prof_md.append(P.render_report({"mode": "scholar", "profiles": [scholar]}))
_save(out, "profile", prof_result, "\n".join(prof_md))
summary["pillars"]["profile"] = {
"repo": proj.get("repo"), "score_total": proj.get("score_total"),
"scholar": top_login or None,
}
# ④ 创新启发
sys.stderr.write("[chain] ③ 创新启发…\n"); sys.stderr.flush()
ins = I.analyze_repo(owner, repo_name, context=ctx)
_save(out, "inspire", ins, I.render_report(ins))
summary["pillars"]["inspire"] = {
"gap_topics": (ins.get("gap_topics") or [])[:5],
"candidates": len(ins.get("candidates") or []),
"innovations": len(ins.get("innovation_points") or []),
"llm_used": ins.get("llm_used"),
}
# ⑤ 合规校验
sys.stderr.write("[chain] ④ 合规校验…\n"); sys.stderr.flush()
try:
rep = R.run(owner, repo_name)
_save(out, "repro", rep, R.render_report(rep))
summary["pillars"]["repro"] = {
"license": rep.get("license"),
"repro_score": rep.get("repro_score"),
"compliance_score": rep.get("compliance_score"),
}
except Exception as e:
sys.stderr.write(f"[chain] repro 失败: {e!r}\n")
# ⑥ 项目分析
sys.stderr.write("[chain] ⑤ 项目分析…\n"); sys.stderr.flush()
try:
lin = L.lineage(owner, repo_name)
_save(out, "lineage", lin, L.render_report(lin))
summary["pillars"]["lineage"] = {
"innovations": len(lin.get("innovation_points") or []),
}
except Exception as e:
sys.stderr.write(f"[chain] lineage 失败: {e!r}\n")
_finalize(out, summary, hot)
return summary
def _finalize(out: str, summary: dict, hot: dict) -> None:
if not out:
return
_write(os.path.join(out, "chain.json"), json.dumps(summary, ensure_ascii=False, indent=2))
_write(os.path.join(out, "chain_report.md"), _chain_report(summary))
sys.stderr.write(f"[chain] ✓ 全链路完成,产物落 {out}\n"); sys.stderr.flush()
def _chain_report(s: dict) -> str:
lines = [
"# 🔬 科研情报全链路报告",
"",
f"> 领域分类:**{s.get('category')}** · 焦点仓:`{s.get('focal_repo')}`",
"",
"## 链路",
"选分类(explore) → ① 热点追踪 → ② 主体画像 → ③ 创新启发 → ④ 合规校验 → ⑤ 项目分析",
"",
]
h = s.get("hotspot") or {}
lines.append("### ① 热点追踪")
lines.append(f"- 榜首:`{h.get('trending_top')}`;领域主题:" +
"".join(f"`{t}`" for t in h.get("topic_heat", [])))
p = s.get("pillars") or {}
if p.get("profile"):
lines.append("\n### ② 主体画像")
pr = p["profile"]
lines.append(f"- `{pr.get('repo')}` 研究维度评分 **{pr.get('score_total')}/40**;核心学者 `{pr.get('scholar')}`")
if p.get("inspire"):
lines.append("\n### ③ 创新启发")
ii = p["inspire"]
lines.append(f"- 缺口 {len(ii.get('gap_topics', []))} 主题、可合作候选 {ii.get('candidates')} 位、创新点 {ii.get('innovations')}LLM 建议 {'' if ii.get('llm_used') else '⏭ 未启用'}")
if p.get("repro"):
lines.append("\n### ④ 合规校验")
rr = p["repro"]
lines.append(f"- 许可证 `{rr.get('license')}`;复现性 {rr.get('repro_score')}/10 · 合规 {rr.get('compliance_score')}/10")
if p.get("lineage"):
lines.append("\n### ⑤ 项目分析")
lines.append(f"- 识别 {p['lineage'].get('innovations')} 个创新点(详见 lineage/report.md")
lines += ["", "各 pillar 完整产物见子目录 `{pillar}/`。", "", "---", "*由 gitlink-research chain 生成*"]
return "\n".join(lines)
def main() -> None:
ap = argparse.ArgumentParser(description="科研情报全链路编排器")
ap.add_argument("--category", "-c", required=True, help="领域分类(中文名或 id如 深度学习/32")
ap.add_argument("--repo", default="", help="焦点仓 owner/repo省略则取热点榜 top-1")
ap.add_argument("--limit", type=int, default=20, help="热点榜上限(默认 20")
ap.add_argument("--out", "-o", default="", help="输出目录")
args = ap.parse_args()
run_chain(args.category, repo=args.repo or None, limit=args.limit, out=args.out)
if __name__ == "__main__":
main()