Skip to content

Commit 1364a83

Browse files
committed
Release v0.7.3
1 parent 251137e commit 1364a83

25 files changed

Lines changed: 2357 additions & 1002 deletions

.github/workflows/ci-tests.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: ci-tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
tests:
15+
runs-on: ubuntu-latest
16+
env:
17+
PYTHONUTF8: "1"
18+
TZ: UTC
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Install dependencies
29+
run: python -m pip install --upgrade pip && pip install -r deps/requirements.txt
30+
31+
- name: Run test suite
32+
run: python -m unittest discover -s tests -p "test_*.py" -v

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,45 @@ Note:
66
- Repository history does not contain a public `0.6.0` version bump.
77
- The preview-first and local-inbox transition work from that period was folded into `0.7.0`.
88

9+
## [0.7.3] - 2026-04-06
10+
11+
### Added
12+
- Added a dedicated CI workflow for automated test execution on `push`, `pull_request`, and manual runs:
13+
- `.github/workflows/ci-tests.yml`
14+
- Added focused search-core and agent-contract tests:
15+
- search-request normalization
16+
- horizon filtering
17+
- no-result / below-threshold handling
18+
- partial-source-failure handling
19+
- agent response snapshot coverage
20+
- Added a versioned agent contract and OpenAPI description:
21+
- `schema_version: "1.1"`
22+
- `docs/openapi/agent_search.openapi.yaml`
23+
24+
### Changed
25+
- Extracted search policy, search models, search request normalization, and search runtime helpers out of the monolithic runtime:
26+
- `app/search_policy.py`
27+
- `app/search_models.py`
28+
- `app/search_request.py`
29+
- `app/search_runtime.py`
30+
- Extracted shared text helpers into `app/text_utils.py`.
31+
- Extracted agent response serialization into `app/agent_contract.py`.
32+
- Moved the local web console base shell from inline Python to a file-based template:
33+
- `app/templates/base.html`
34+
- Agent HTTP / CLI responses now support:
35+
- `include_abstracts`
36+
- `partial_source_failure`
37+
- `meta.source_warnings`
38+
39+
### Fixed
40+
- Retrieval now records per-source failures without discarding usable results from surviving sources.
41+
- Agent error responses now include the same `schema_version` marker as successful responses.
42+
43+
### Docs
44+
- Updated README and Korean README version markers to `v0.7.3`.
45+
- Updated agent manuals for `schema_version`, `include_abstracts`, `partial_source_failure`, and OpenAPI.
46+
- Recorded Phase 1-3 completion in `docs/plans/REPO_EVALUATION_ADOPTION_PLAN_v0.1.md`.
47+
948
## [0.7.2] - 2026-04-03
1049

1150
### Added

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Paper Morning is a research-context paper search engine for medical and health AI work.
88
It turns a project description into search queries, retrieves papers, reranks them by practical relevance, and explains why they matter.
99

10-
- Latest version: **[v0.7.2](VERSION)**
10+
- Latest version: **[v0.7.3](VERSION)**
1111
- License: `GNU AGPLv3` ([LICENSE](LICENSE))
1212
- Privacy policy: [PRIVACY.md](PRIVACY.md)
1313

@@ -61,6 +61,9 @@ What it supports:
6161
- CLI JSON mode: `python app/paper_digest_app.py --agent-search ...`
6262
- brokered local auth via `AGENT_API_TOKEN`
6363
- Gemini or a local/self-hosted OPENAI-compatible backend
64+
- versioned agent JSON contract with `schema_version`
65+
- optional `include_abstracts` and `partial_source_failure` handling
66+
- OpenAPI spec: [`docs/openapi/agent_search.openapi.yaml`](docs/openapi/agent_search.openapi.yaml)
6467

6568
Security model:
6669
- your agent gets only `AGENT_API_TOKEN`
@@ -89,6 +92,7 @@ Shared engine for both human and agent paths:
8992
- Live preview first: [Open Live Web Preview](https://raw.githack.com/jeong87/paper-morning/main/docs/preview/index.html)
9093
- Human local usage: [docs/manuals/MANUAL_FIRSTTIME_EN.md](docs/manuals/MANUAL_FIRSTTIME_EN.md)
9194
- Agent/tool usage: [docs/manuals/MANUAL_AGENT_EN.md](docs/manuals/MANUAL_AGENT_EN.md)
95+
- Agent OpenAPI spec: [docs/openapi/agent_search.openapi.yaml](docs/openapi/agent_search.openapi.yaml)
9296
- Korean README: [docs/manuals/README_KR.md](docs/manuals/README_KR.md)
9397

9498
## Optional Workflows
@@ -199,6 +203,7 @@ Fallback:
199203
- Beginner (English): [docs/manuals/MANUAL_FIRSTTIME_EN.md](docs/manuals/MANUAL_FIRSTTIME_EN.md)
200204
- Full operations (English): [docs/manuals/MANUAL_EN.md](docs/manuals/MANUAL_EN.md)
201205
- Agent/tool integration (English): [docs/manuals/MANUAL_AGENT_EN.md](docs/manuals/MANUAL_AGENT_EN.md)
206+
- Agent OpenAPI spec: [docs/openapi/agent_search.openapi.yaml](docs/openapi/agent_search.openapi.yaml)
202207
- Scoring policy (English): [docs/manuals/SCORING_POLICY_EN.md](docs/manuals/SCORING_POLICY_EN.md)
203208
- Beginner (Korean): [docs/manuals/MANUAL_FIRSTTIME_KR.md](docs/manuals/MANUAL_FIRSTTIME_KR.md)
204209
- Full operations (Korean): [docs/manuals/MANUAL_KR.md](docs/manuals/MANUAL_KR.md)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.7.2
1+
0.7.3

app/agent_contract.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, List
4+
5+
from scoring_policy import LLM_RELEVANCE_MODE_DEFAULT, normalize_relevance_mode
6+
from search_models import DigestStats, Paper, SearchRequest
7+
from text_utils import clean_text
8+
9+
AGENT_SEARCH_SCHEMA_VERSION = "1.1"
10+
11+
12+
def map_agent_status(stats: DigestStats, papers: List[Paper]) -> str:
13+
if papers:
14+
if stats.source_warning_messages:
15+
return "partial_source_failure"
16+
return "ok"
17+
if stats.no_results_reason == "outside_horizon":
18+
return "outside_horizon"
19+
if stats.no_results_reason == "below_threshold":
20+
return "below_threshold"
21+
if stats.no_results_reason == "source_failure":
22+
return "error"
23+
if stats.no_results_reason in {"none_retrieved", "no_candidates"}:
24+
return "no_candidates"
25+
return "error"
26+
27+
28+
def describe_agent_llm_backend(config: Any) -> Dict[str, str]:
29+
if getattr(config, "gemini_api_key", ""):
30+
return {"provider": "gemini", "model": str(getattr(config, "gemini_model", "") or "")}
31+
if (
32+
getattr(config, "enable_openai_compat_fallback", False)
33+
and clean_text(str(getattr(config, "openai_compat_api_base", "") or ""))
34+
and clean_text(str(getattr(config, "openai_compat_model", "") or ""))
35+
):
36+
return {
37+
"provider": "openai_compatible",
38+
"model": str(getattr(config, "openai_compat_model", "") or ""),
39+
}
40+
if getattr(config, "enable_cerebras_fallback", False) and getattr(config, "cerebras_api_key", ""):
41+
return {"provider": "cerebras", "model": str(getattr(config, "cerebras_model", "") or "")}
42+
return {"provider": "none", "model": ""}
43+
44+
45+
def build_agent_search_response(
46+
*,
47+
project_name: str,
48+
llm_projects: List[Dict[str, str]],
49+
generated_topics: List[Dict[str, Any]],
50+
request_config: Any,
51+
search_request: SearchRequest,
52+
stats: DigestStats,
53+
papers: List[Paper],
54+
diagnostics: List[str],
55+
include_abstracts: bool = False,
56+
include_diagnostics: bool = False,
57+
) -> Dict[str, Any]:
58+
primary_topic = generated_topics[0] if generated_topics else {}
59+
backend = describe_agent_llm_backend(request_config)
60+
return {
61+
"schema_version": AGENT_SEARCH_SCHEMA_VERSION,
62+
"status": map_agent_status(stats, papers),
63+
"request": {
64+
"project_name": clean_text(project_name) or llm_projects[0]["name"],
65+
"search_intent": search_request.intent,
66+
"time_horizon": search_request.time_horizon_key,
67+
"top_k": request_config.max_papers,
68+
"output_language": request_config.output_language,
69+
"include_diagnostics": include_diagnostics,
70+
"include_abstracts": include_abstracts,
71+
},
72+
"meta": {
73+
"intent_label": search_request.intent_label,
74+
"requested_horizon_label": search_request.time_horizon_label,
75+
"window_used_label": stats.window_used_label or search_request.time_horizon_label,
76+
"query_plan_label": stats.query_plan_label or "generated topic queries",
77+
"used_provider": backend["provider"],
78+
"used_model": backend["model"],
79+
"sources_queried": [
80+
label
81+
for enabled, label in [
82+
(bool(request_config.arxiv_queries), "arXiv"),
83+
(bool(request_config.pubmed_queries), "PubMed"),
84+
(
85+
request_config.enable_semantic_scholar
86+
and bool(request_config.semantic_scholar_queries),
87+
"Semantic Scholar",
88+
),
89+
(
90+
request_config.enable_google_scholar
91+
and bool(request_config.google_scholar_queries),
92+
"Google Scholar",
93+
),
94+
]
95+
if enabled
96+
],
97+
"scanned_count": stats.post_time_filter_candidates or stats.total_candidates,
98+
"selected_count": len(papers),
99+
"threshold_used": stats.ranking_threshold,
100+
"notice": stats.search_notice,
101+
"partial_source_failure": bool(stats.source_warning_messages),
102+
"source_warning_count": len(stats.source_warning_messages),
103+
"source_warnings": list(stats.source_warning_messages),
104+
},
105+
"topic": {
106+
"name": clean_text(str(primary_topic.get("name", ""))),
107+
"keywords": [
108+
clean_text(str(item))
109+
for item in primary_topic.get("keywords", [])
110+
if clean_text(str(item))
111+
],
112+
"relevance_mode": normalize_relevance_mode(
113+
primary_topic.get("relevance_mode", LLM_RELEVANCE_MODE_DEFAULT)
114+
),
115+
"arxiv_query": clean_text(str(primary_topic.get("arxiv_query", ""))),
116+
"pubmed_query": clean_text(str(primary_topic.get("pubmed_query", ""))),
117+
"semantic_scholar_query": clean_text(
118+
str(primary_topic.get("semantic_scholar_query", ""))
119+
),
120+
"google_scholar_query": clean_text(
121+
str(primary_topic.get("google_scholar_query", ""))
122+
),
123+
},
124+
"papers": [
125+
{
126+
"rank": index,
127+
"id": paper.paper_id,
128+
"title": paper.title,
129+
"authors": ", ".join(paper.authors),
130+
"source": paper.source,
131+
"url": paper.url,
132+
"published_at": paper.published_at_utc.isoformat(),
133+
"relevance_score": paper.score,
134+
"relevance_reason": paper.llm_relevance_text,
135+
"core_point": paper.llm_core_point_text,
136+
"usefulness": paper.llm_usefulness_text,
137+
"evidence_spans": list(paper.llm_evidence_spans or []),
138+
"topic": paper.topic,
139+
"project_name": paper.project_name,
140+
"relevance_mode": paper.relevance_mode,
141+
**({"abstract": paper.abstract} if include_abstracts else {}),
142+
}
143+
for index, paper in enumerate(papers, start=1)
144+
],
145+
"diagnostics": diagnostics,
146+
}

0 commit comments

Comments
 (0)