Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 54 additions & 32 deletions .ai-team/TASK.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,83 @@
# Current Task

- ID: `AR-003`
- Title: `Simplify the assignment plan to module level`
- Status: `done`
- Owner: `zzg`
- Next owner: `user/team`
- ID: `AR-004`
- Title: `Add OA full-text discovery for M05`
- Status: `active`
- Owner: `wdwd200`
- Next owner: `maintainer/reviewer`

## Goal

根据用户反馈,把任务拆解入口从 122 个子任务收敛为模块级分配。保留此前详细版作为附录,但当前团队先按模块认领负责人、边界、主要产出、依赖和验收目标。
在现有 M05 论文搜索链路中,为去重后的论文统一增加 Open Access(OA)全文发现能力。优先通过 Unpaywall 查询合法开放全文,未命中时使用 OpenAlex 作为 fallback;不绕过付费墙,不负责全文解析。

## Acceptance scenarios

- [x] `docs/AutoResearch_任务拆解书.md` 只作为模块级分配入口,覆盖 M00–M17。
- [x] 每个模块说明目标、边界、主要交付物、前置模块、负责人建议和当前状态。
- [x] 文档提供模块负责人验收模板和第一批模块分配建议。
- [x] 原 122 子任务版本保留为 `docs/AutoResearch_任务拆解书_详细版.md`,不丢失后续拆解素材。
- [x] README、Project-to-Act 和 VibeCollab 任务状态保持一致,不改变五业务 Agent 和证据门禁边界。
- [x] 对每篇去重后的 `PaperRecord` 执行统一 OA 查询,而不是只处理某个特定搜索源返回的论文。
- [x] 新增独立的 OA 结果结构,不修改 `PaperRecord`;每篇论文只保留一个最新 OA 状态,并使用 `paper_id` 作为 OA 记录的唯一 `record_id`。
- [x] Unpaywall 命中合法开放全文时,记录 `open_access`、合法全文 URL、可用的 license 和最终来源。
- [x] Unpaywall 未命中时,使用 OpenAlex fallback;fallback 命中时同样返回统一 OA 结果。
- [x] 已完成查询但未发现开放全文时返回 `no_open_fulltext_found`;无法可靠完成查询时返回 `unknown`,不得把“查询失败”误判为“无 OA”。
- [x] OA 查询过程记录 `checked_sources` 和基础 diagnostics,便于排查外部服务失败。
- [x] OA 记录使用现有 `RecordStore` 持久化;同一 `paper_id` 再次检查时更新最新状态,不产生第二条当前 OA 记录。
- [x] 至少覆盖:Unpaywall 命中、OpenAlex fallback 命中、均未命中、外部服务失败、同一论文重复检查更新这 5 个离线测试场景。

## Invariants

- 当前入口不要求团队一次性拆到函数、接口或工单;模块负责人后续再自行细化。
- 详细版是参考材料,不代表所有子任务都已实现或已验收。
- 真实论文试点、生产数据库、Web UI、灾备和性能仍保持原有“外部阻塞/待开发”状态。
- 不提交 token、密钥、私有来源、完整论文、原始提示或虚构外部结果。
- 不绕过付费墙、登录、订阅或访问控制。
- OA 发现只负责“是否存在合法开放全文及其入口”,不负责 PDF 下载、全文解析、ReadingCard 或 Reader 侧逻辑。
- 不把 `unknown` 当作 `no_open_fulltext_found`。
- 不虚构 OA 状态、license、全文 URL、撤稿或更正信息。
- 保持现有五业务 Agent 边界;OA 检查是 M05 内的确定性 service/resolver,不新增业务 Agent。
- 本 PR 不引入 MCP;可参考开源 MCP/Skill 的实现思路,但不改变现有 `PaperSearchService` / connector 架构。
- 不提交 token、密钥、私有来源、完整论文、原始提示或原始工具输出。

## Decisions

- 使用 M00–M17 作为稳定模块 ID,先按模块分配负责人。
- 保留详细拆解版,避免未来重新整理时丢失依赖和验收素材。
- `.project-to-act` 继续作为长期项目事实源;`.ai-team/TASK.md` 只记录本次短周期交付。
- OA enrichment 放在多源检索结果汇总、去重之后,确保 OpenAlex、Crossref、Semantic Scholar 等来源的最终论文都走同一套 OA 处理。
- 使用独立 `OpenAccessRecord`(名称可在实现时按项目命名规范微调),不向公共 `PaperRecord` 追加 OA 字段。
- 每篇论文只维护一个最新 OA 状态;OA 记录直接以 `paper_id` 作为 `RecordStore` 的 `record_id`。
- 第一版状态收敛为:
- `open_access`
- `no_open_fulltext_found`
- `unknown`
- 第一版优先查询 Unpaywall,未命中再查询 OpenAlex。
- `license` 仅记录外部来源明确返回的值;缺失时保持 `None`。
- 撤稿、更正、复杂许可策略、严格检索质量评测不纳入本 PR。

## Completed

- 将原 `docs/AutoResearch_任务拆解书.md` 移为 `docs/AutoResearch_任务拆解书_详细版.md`。
- 新建模块级 `docs/AutoResearch_任务拆解书.md`,包含 18 个模块、执行阶段、负责人模板和首批分配建议。
- 保留 README 任务书入口,并将 AR-003 的状态、范围和下一步写入协作账本。
- 已完成 M05 现状审计,确认当前已有 OpenAlex、Crossref、Semantic Scholar 基础连接器、基础 DOI/标题去重、持久化和 Agent handoff。
- 已确认当前代码没有统一 OA/full-text/license 查询流程。
- 已确认 `PaperRecord` 只有 `full_text_path`,没有 OA 状态、合法全文 URL 或 license 字段。
- 已确认 `RecordStore.put()` 支持通过新的 `kind` 保存 OA 记录,无需为本任务新增数据库表。
- 已完成第一版 OA 数据模型、状态语义、查询顺序和最小测试范围设计。
- 已实现 `OpenAccessProvider` 协议、顺序 `OpenAccessResolver` 和 `paper_open_access` 记录 upsert。
- 已实现可选邮箱配置的 `UnpaywallProvider` 和 `OpenAlexOAProvider` fallback;未配置网络时不启用 OA enrichment。
- 已将 OA enrichment 接入 `PaperSearchService`:去重后先持久化 `PaperRecord`,再调用 resolver。
- 已新增 fake provider 离线测试和 PaperSearchService 集成测试,覆盖命中、fallback 命中、均未命中、服务失败、重复检查更新、去重持久化顺序和未配置 resolver。

## Pending

- 用户/团队需要为模块负责人模板填写实际 owner、分支、目标日期和 evidence ID。
- 模块负责人确认后,再选择需要展开的模块,不默认展开全部模块。
- 真实论文试点仍等待 idea、领域、目标 venue、授权全文和实验资源边界。
- 等待 maintainer/reviewer review 和 merge。
- 根据 review 结果决定是否另行拆分撤稿/更正、筛选/排序或更多检索源 PR。

## Next step

先认领 M01–M04、M05–M07、M08、M09–M10、M11–M12、M13–M15/M17、M16 这 7 个模块包;完成模块边界确认后,再只对当前瓶颈模块做子任务拆解。
等待 maintainer/reviewer review 和 merge;本任务不扩展到 MCP、撤稿/更正、排序或 Reader 功能。

## Verification

- [x] Project-to-Act `--check` — managed 配置有效,唯一账本保持不变。
- [x] 模块文档结构检查 — M00–M17 全部存在,详细版文件可访问。
- [x] `node .ai-team/check.mjs --base main` — AR-003 acceptance 5/5,任务范围和变更同步有效。
- [x] Project-to-Act `--validate` — managed 配置 valid,issues 为空。
- [ ] `.venv\Scripts\python.exe -m pytest -W error -q` — 25 tests passed, but command exited during pytest cache finalization because `.pytest_cache` was inaccessible (`PermissionError: [WinError 5]`)
- [x] `.venv\Scripts\autoresearch.exe doctor`
- [x] `node .ai-team/check.mjs --base origin/main` — `Result: valid`, functional progress 8/8
- [x] `.venv\Scripts\python.exe -m pytest tests/test_open_access_service.py -q` — 7 passed
- [x] `.venv\Scripts\python.exe -m pytest -q` — 25 passed
- [x] `.venv\Scripts\python.exe -m ruff check src tests` — All checks passed
- [x] `.venv\Scripts\python.exe -m pytest -W error -q -p no:cacheprovider` — 25 passed
- [x] `git diff --check` — no whitespace errors; only Windows LF/CRLF warnings

## Handoff note

- From: `zzg`
- To: `user/team`
- Summary: 当前分配入口已经降为模块级;详细拆解保留在附录。先分模块负责人,后续只对真正需要的模块继续细化。
- From: `user/team`
- To: `maintainer/reviewer`
- Summary: AR-004 已完成 OA model、Unpaywall/OpenAlex provider、顺序 resolver、RecordStore upsert、PaperSearchService 去重后 enrichment 和离线/集成测试;等待 review 和 merge。
24 changes: 24 additions & 0 deletions src/autoresearch/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
from autoresearch.handoffs import HandoffService
from autoresearch.knowledge import KnowledgeService
from autoresearch.llm import LLMService
from autoresearch.open_access_service import (
OpenAccessResolver,
OpenAlexOAProvider,
UnpaywallProvider,
)
from autoresearch.profile_service import UserProfileService
from autoresearch.project_service import ProjectService
from autoresearch.reader_service import PaperReaderService
Expand All @@ -66,11 +71,30 @@ def __init__(self, settings: Settings | None = None):
self.settings.template_dir,
self.store,
)
oa_providers = []

if self.settings.unpaywall_email:
oa_providers.append(
UnpaywallProvider(self.settings.unpaywall_email)
)

oa_providers.append(OpenAlexOAProvider())

self.open_access = OpenAccessResolver(
self.store,
oa_providers,
)

self.search = PaperSearchService(
self.store,
self.evidence,
self.knowledge,
network_enabled=self.settings.network_enabled,
open_access=(
self.open_access
if self.settings.network_enabled
else None
),
)
self.reader = PaperReaderService(self.store, self.evidence, self.knowledge)
self.writing = WritingService(
Expand Down
5 changes: 5 additions & 0 deletions src/autoresearch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class Settings(BaseSettings):
template_dir: Path = Field(
default=Path("paper-projects/_template"), alias="AUTORESEARCH_TEMPLATE_DIR"
)
unpaywall_email: str | None = Field(
default=None,
alias="UNPAYWALL_EMAIL",
)
network_enabled: bool = Field(default=False, alias="AUTORESEARCH_NETWORK_ENABLED")

@property
Expand All @@ -55,6 +59,7 @@ def safe_summary(self) -> dict[str, object]:
"network_enabled": self.network_enabled,
"db_path": str(self.db_path),
"checkpoint_path": str(self.checkpoint_path),
"unpaywall_email_configured": bool(self.unpaywall_email),
}


Expand Down
18 changes: 18 additions & 0 deletions src/autoresearch/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ class KnowledgePartition(StrEnum):
PROJECTS = "projects"


class OpenAccessStatus(StrEnum):
OPEN_ACCESS = "open_access"
NO_OPEN_FULLTEXT_FOUND = "no_open_fulltext_found"
UNKNOWN = "unknown"


class ClaimStatus(StrEnum):
UNKNOWN = "unknown"
HYPOTHESIS = "hypothesis"
Expand Down Expand Up @@ -203,6 +209,18 @@ class PaperRecord(BaseModel):
retrieved_at: datetime = Field(default_factory=utc_now)


class OpenAccessRecord(BaseModel):
paper_id: str
project_id: str
status: OpenAccessStatus
fulltext_url: str | None = None
license: str | None = None
source: str | None = None
checked_sources: list[str] = Field(default_factory=list)
diagnostics: list[str] = Field(default_factory=list)
checked_at: datetime = Field(default_factory=utc_now)


class ReadingCard(BaseModel):
card_id: str = Field(default_factory=lambda: new_id("card"))
project_id: str
Expand Down
Loading