", " ", text, flags=re.IGNORECASE | re.DOTALL)
+ text = re.sub(r"", " ", text, flags=re.DOTALL)
+ text = re.sub(r"<[^>]+>", " ", text)
+ text = html.unescape(text)
+ text = re.sub(r"\s+", " ", text)
+ return text.strip()
+
+
+def url_allowed_domain(url: str, allowed_domains: Iterable[str]) -> bool:
+ parsed = urlparse(url)
+ hostname = parsed.hostname or ""
+ hostname = hostname.lower()
+ if not hostname:
+ return False
+ if not allowed_domains:
+ return True
+ for allowed in allowed_domains:
+ if hostname == allowed.lower() or hostname.endswith("." + allowed.lower()):
+ return True
+ return False
diff --git a/services/literature/data/ingestion_jobs.json b/services/literature/data/ingestion_jobs.json
new file mode 100644
index 0000000..d23a203
--- /dev/null
+++ b/services/literature/data/ingestion_jobs.json
@@ -0,0 +1,644 @@
+[
+ {
+ "id": "job-test-1",
+ "source": "pubmed",
+ "query": "oncology",
+ "status": "dead_letter",
+ "created_at": "2026-08-09T18:18:41.860017+00:00",
+ "updated_at": "2026-08-09T18:18:41.881321+00:00",
+ "attempts": 2,
+ "max_retries": 3,
+ "error": "Stage failure simulation",
+ "result": null
+ },
+ {
+ "id": "88fd4d71-ad14-45a3-96a1-f5d23c64c5d3",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T15:36:34.655036+00:00",
+ "updated_at": "2026-08-09T15:36:35.776023+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "2867cc60-a9a2-4319-8ba2-cf069df3a4eb",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T15:39:00.534692+00:00",
+ "updated_at": "2026-08-09T15:39:01.601027+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "d885a5ce-6eae-4a54-81d7-88cd94ac2732",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T15:55:11.178201+00:00",
+ "updated_at": "2026-08-09T15:55:12.135817+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "7be4b43d-b905-4c4a-a3c1-b033ce81ccb9",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T16:36:16.840544+00:00",
+ "updated_at": "2026-08-09T16:36:17.913566+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "0e279eb7-9a08-4530-a76e-ccb234a64030",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T16:44:58.981446+00:00",
+ "updated_at": "2026-08-09T16:44:59.594785+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "b2716595-ad05-400b-8719-dbe729544c84",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T16:45:52.686726+00:00",
+ "updated_at": "2026-08-09T16:45:53.594496+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "b04952c9-c8d2-49ab-a296-ed1bae76cdf4",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T16:51:21.094238+00:00",
+ "updated_at": "2026-08-09T16:51:21.859473+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "1eb90b9c-f53b-4179-a093-66ce0d28e367",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T16:51:52.359921+00:00",
+ "updated_at": "2026-08-09T16:51:52.887987+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "b472b5b9-5002-4b35-bd5a-59ac84026114",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:01:50.096537+00:00",
+ "updated_at": "2026-08-09T17:01:50.457348+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "3e928a11-33a8-4e85-bd1c-b6251f544236",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:09:25.730838+00:00",
+ "updated_at": "2026-08-09T17:09:26.098394+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "5584ec5b-1036-4a27-92bb-42949d019531",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:12:38.917534+00:00",
+ "updated_at": "2026-08-09T17:12:39.495457+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "37ce44c3-cf73-4bae-8696-13c97dcbf6e1",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:14:27.258929+00:00",
+ "updated_at": "2026-08-09T17:14:27.736948+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "c0c3581b-e472-4c24-9da5-52b020ca848a",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:22:39.974790+00:00",
+ "updated_at": "2026-08-09T17:22:40.343901+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "9d9e46c6-a49b-42f5-9b9e-bd82ec7b3123",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:30:38.787581+00:00",
+ "updated_at": "2026-08-09T17:30:39.147444+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "c2502caf-9c8a-41ed-8995-63574a9f1294",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:37:05.924430+00:00",
+ "updated_at": "2026-08-09T17:37:06.835643+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "f5b70b64-d139-48b6-b41d-de2c8e71e11c",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:41:58.607354+00:00",
+ "updated_at": "2026-08-09T17:41:59.036607+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "7a946b83-90fb-43b6-92ab-0ad65fdaf991",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T17:59:23.039521+00:00",
+ "updated_at": "2026-08-09T17:59:23.470983+00:00",
+ "attempts": 3,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "status": "completed",
+ "kg_updates": [],
+ "wiki_updates": []
+ }
+ },
+ {
+ "id": "f93e622d-1c8f-40dc-b13d-b86c48118a50",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T18:14:32.553191+00:00",
+ "updated_at": "2026-08-09T18:14:33.090576+00:00",
+ "attempts": 8,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "kg_updates": [],
+ "wiki_updates": [],
+ "status": "completed"
+ }
+ },
+ {
+ "id": "70397351-08a3-4124-8949-7ad691fdb0b2",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T18:16:48.991167+00:00",
+ "updated_at": "2026-08-09T18:16:49.478011+00:00",
+ "attempts": 8,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "kg_updates": [],
+ "wiki_updates": [],
+ "status": "completed"
+ }
+ },
+ {
+ "id": "118426b3-eb35-4331-b105-f4eff540ba4e",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T18:17:26.917287+00:00",
+ "updated_at": "2026-08-09T18:17:27.430458+00:00",
+ "attempts": 8,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "kg_updates": [],
+ "wiki_updates": [],
+ "status": "completed"
+ }
+ },
+ {
+ "id": "6c6feea4-b06b-4c09-9c41-3fa6b736bfb8",
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "status": "completed",
+ "created_at": "2026-08-09T18:18:41.891042+00:00",
+ "updated_at": "2026-08-09T18:18:42.287977+00:00",
+ "attempts": 8,
+ "max_retries": 3,
+ "error": null,
+ "result": {
+ "source": "pubmed",
+ "query": "trastuzumab HER2",
+ "items": [],
+ "limitation": null,
+ "source_status": {
+ "source": "pubmed",
+ "connected": true,
+ "timeout": 10.0,
+ "user_agent": "AI-RxOS LiteratureBot/1.0",
+ "max_retries": 2,
+ "limitation": null
+ },
+ "processed_items": [],
+ "duplicates": [],
+ "kg_updates": [],
+ "wiki_updates": [],
+ "status": "completed"
+ }
+ }
+]
\ No newline at end of file
diff --git a/services/literature/prompt6_endpoint_check.py b/services/literature/prompt6_endpoint_check.py
new file mode 100644
index 0000000..43bbd25
--- /dev/null
+++ b/services/literature/prompt6_endpoint_check.py
@@ -0,0 +1,17 @@
+import urllib.request
+
+urls = [
+ 'http://127.0.0.1:8082/healthz',
+ 'http://127.0.0.1:8082/metrics',
+ 'http://127.0.0.1:8082/api/v1/papers',
+]
+
+for url in urls:
+ try:
+ with urllib.request.urlopen(url, timeout=10) as resp:
+ body = resp.read(2000).decode('utf-8', 'replace')
+ print(url)
+ print('STATUS', resp.status)
+ print(body)
+ except Exception as exc:
+ print(url, 'ERROR', repr(exc))
diff --git a/services/literature/requirements.txt b/services/literature/requirements.txt
index 783c143..d435932 100644
--- a/services/literature/requirements.txt
+++ b/services/literature/requirements.txt
@@ -3,9 +3,8 @@ uvicorn[standard]==0.34.0
pydantic==2.10.4
pydantic-settings==2.7.1
asyncpg==0.30.0
-PyJWT==2.8.0
+httpx==0.28.1
python-json-logger==3.2.1
+spacy>=3.0.0,<4.0
pytest==8.3.4
pytest-asyncio==0.25.1
-croniter==1.4.1
-prometheus-client==0.21.0
diff --git a/services/literature/tests/test_pipeline_integration.py b/services/literature/tests/test_pipeline_integration.py
new file mode 100644
index 0000000..4922eca
--- /dev/null
+++ b/services/literature/tests/test_pipeline_integration.py
@@ -0,0 +1,23 @@
+from app.connectors.factory import ConnectorFactory
+from app.nlp.pipeline import LiteratureNLP
+from app.orchestrator.pipeline import PipelineRunner
+
+
+def test_connector_factory_builds_pubmed_connector():
+ connector = ConnectorFactory.create("pubmed", {"base_url": "https://example.com"})
+ assert connector.name == "pubmed"
+ assert connector.config["base_url"] == "https://example.com"
+
+
+def test_pipeline_runner_executes_stage_sequence():
+ runner = PipelineRunner(stages=[lambda doc: {**doc, "stage": "parsed"}, lambda doc: {**doc, "stage": "indexed"}])
+ result = runner.run({"id": "paper-1"})
+ assert result["stage"] == "indexed"
+
+
+def test_nlp_pipeline_extracts_entities_and_summary():
+ text = "HER2-positive breast cancer is treated by trastuzumab in combination therapy."
+ result = LiteratureNLP().run(text)
+ assert "entities" in result
+ assert "summary" in result
+ assert any(entity.lower() in {"her2", "trastuzumab", "breast cancer"} for entity in result["entities"])
diff --git a/services/literature/tests/test_prompt6_comprehensive.py b/services/literature/tests/test_prompt6_comprehensive.py
new file mode 100644
index 0000000..90ca19c
--- /dev/null
+++ b/services/literature/tests/test_prompt6_comprehensive.py
@@ -0,0 +1,363 @@
+from unittest.mock import MagicMock, patch
+
+import httpx
+from fastapi.testclient import TestClient
+
+from app.connectors.base import BaseConnector
+from app.connectors.factory import ConnectorFactory
+from app.connectors.sources import CompanyWebsiteConnector, PatentsConnector
+from app.database.models import IngestionJobState, job_store
+from app.integrations.kg_client import KGClient
+from app.integrations.wiki_client import LLMWikiClient
+from app.main import app
+from app.nlp.deduplication import DuplicateDetector
+from app.nlp.evidence_ranking import EvidenceRanker
+from app.nlp.ner import RuleBasedEntityExtractor
+from app.nlp.pipeline import LiteratureNLP
+from app.nlp.relationships import RelationshipExtractor
+from app.nlp.summarizer import DocumentSummarizer
+from app.orchestrator.pipeline import PipelineRunner
+from app.parsing.text_parser import TextParser
+
+client = TestClient(app)
+
+
+def test_all_11_connectors_registered_and_instantiated():
+ required_sources = [
+ "pubmed",
+ "pmc",
+ "clinicaltrials",
+ "aacr",
+ "asco",
+ "sabcs",
+ "esmo",
+ "biorxiv",
+ "medrxiv",
+ "patents",
+ "company_websites",
+ ]
+
+ for source in required_sources:
+ connector = ConnectorFactory.create(source, {"timeout": 0.1})
+ assert connector.name == source
+ info = connector.connect()
+ assert info["connected"] is True
+ fetched = connector.fetch("HER2 breast cancer")
+ assert isinstance(fetched, list)
+ if fetched:
+ assert "title" in fetched[0]
+ assert "source" in fetched[0]
+ else:
+ assert connector.get_limitation() is not None or source in {"pubmed", "pmc", "clinicaltrials", "biorxiv", "medrxiv"}
+
+
+def test_connectors_with_documented_limitations():
+ limitation_sources = ["aacr", "asco", "sabcs", "esmo", "patents", "company_websites"]
+ for source in limitation_sources:
+ connector = ConnectorFactory.create(source)
+ limitation = connector.get_limitation()
+ assert limitation is not None
+ assert isinstance(limitation, str)
+
+
+def test_conference_connectors_return_search_metadata(monkeypatch):
+ search_html = """
+ Test Search
+ Result
+ """
+ detail_html = """
+ Detail Page
+ Full content here.
+ """
+
+ def fake_request_text(self, url, **kwargs):
+ if url.endswith("/search-results?q=test") or "action/doSearch" in url or "?s=test" in url or "search?searchText=test" in url:
+ return search_html
+ return detail_html
+
+ monkeypatch.setattr(BaseConnector, "_request_text", fake_request_text)
+
+ for source in ["aacr", "asco", "sabcs", "esmo"]:
+ connector = ConnectorFactory.create(source)
+ results = connector.fetch("test", max_results=1)
+ assert isinstance(results, list)
+ assert len(results) == 1
+ item = results[0]
+ assert item["source"] == source
+ assert item["url"] is not None
+ assert item["metadata"]["limitation"] == connector.get_limitation()
+
+
+def test_patents_connector_falls_back_to_search_page(monkeypatch):
+ search_html = """
+ Patent search
+ Patent listing content.
+ """
+
+ def fake_request_json(self, url, **kwargs):
+ return {"patents": []}
+
+ def fake_request_text(self, url, **kwargs):
+ return search_html
+
+ monkeypatch.setattr(PatentsConnector, "_request_json", fake_request_json)
+ monkeypatch.setattr(PatentsConnector, "_request_text", fake_request_text)
+
+ connector = PatentsConnector()
+ results = connector.fetch("cancer", max_results=1)
+ assert isinstance(results, list)
+ assert len(results) == 1
+ assert results[0]["source"] == "patents"
+ assert results[0]["metadata"]["limitation"] == connector.get_limitation()
+
+
+def test_company_website_connector_crawls_configured_urls(monkeypatch):
+ urls = ["https://example.com/press"]
+ html = """
+ Press Release
+ Launch announcement with trastuzumab.
+ """
+
+ class FakeCrawlResult:
+ def __init__(self, url, html):
+ self.url = url
+ self.success = True
+ self.status_code = 200
+ self.error = None
+ self.metadata = {"title": "Press Release", "content": html}
+ self.html = html
+
+ def fake_crawl(self, seed_urls):
+ return [FakeCrawlResult(seed_urls[0], html)]
+
+ monkeypatch.setattr("app.crawling.crawler.WebCrawler.crawl", fake_crawl)
+
+ connector = CompanyWebsiteConnector({"urls": urls})
+ results = connector.fetch("trastuzumab", max_results=1)
+ assert isinstance(results, list)
+ assert len(results) == 1
+ item = results[0]
+ assert item["source"] == "company_websites"
+ assert item["metadata"]["limitation"] == connector.get_limitation()
+
+
+def test_connector_retry_behavior(monkeypatch):
+ connector = ConnectorFactory.create("pubmed", {"timeout": 0.1, "max_retries": 1, "backoff_seconds": 0.0})
+ attempts = {"count": 0}
+
+ def flaky_request(self, method, url, **kwargs):
+ attempts["count"] += 1
+ if attempts["count"] == 1:
+ raise httpx.ReadTimeout("temporary timeout")
+ response = MagicMock()
+ response.status_code = 200
+ response.json.return_value = {"esearchresult": {"idlist": []}}
+ return response
+
+ monkeypatch.setattr(httpx.Client, "request", flaky_request)
+ assert connector.fetch("HER2") == []
+ assert attempts["count"] == 2
+
+
+def test_parser_strips_html_and_handles_malformed_metadata():
+ parser = TextParser()
+ raw_doc = {
+ "title": "HER2 Therapy
",
+ "abstract": "Trastuzumab shows efficacy.
",
+ "authors": "Alice Smith, Bob Jones",
+ "metadata": "not_a_dict", # malformed
+ }
+ normalized = parser.normalize_document(raw_doc)
+ assert normalized["title"] == "HER2 Therapy"
+ assert normalized["abstract"] == "Trastuzumab shows efficacy."
+ assert normalized["authors"] == ["Alice Smith", "Bob Jones"]
+ assert isinstance(normalized["metadata"], dict)
+
+
+def test_ner_extracts_entities_across_categories():
+ extractor = RuleBasedEntityExtractor()
+ text = "Trastuzumab targets HER2 protein in breast cancer clinical trial NCT01234567 conducted by AACR."
+ entities = extractor.extract_entities(text)
+ labels = {e["label"] for e in entities}
+ assert "drug" in labels
+ assert "gene" in labels or "protein" in labels
+ assert "disease" in labels
+ assert "clinical_trial" in labels
+ assert "organization" in labels
+
+
+def test_summarizer_extractive_fallback():
+ summarizer = DocumentSummarizer()
+ doc = {
+ "title": "HER2 Targeted Therapy in Breast Cancer",
+ "abstract": "Trastuzumab significantly improves progression-free survival in patients with HER2-positive breast cancer. Overall response rates were high.",
+ }
+ summary = summarizer.summarize(doc)
+ assert summary["summary_type"] == "extractive_fallback"
+ assert "Trastuzumab" in summary["concise_summary"]
+ assert summary["llm_used"] is False
+
+
+def test_spacy_provider_falls_back_to_rule_based(monkeypatch):
+ def fake_init(self, model_name="en_core_web_sm"):
+ self.available = False
+ self.nlp = None
+
+ monkeypatch.setattr(
+ "app.nlp.ner.SpaCyEntityExtractor.__init__",
+ fake_init,
+ )
+
+ nlp = LiteratureNLP({"ner_provider": "spacy"})
+ assert isinstance(nlp.ner, RuleBasedEntityExtractor)
+
+
+def test_summarizer_llm_fallback_retries_and_uses_extractive(monkeypatch):
+ summarizer = DocumentSummarizer(
+ {
+ "llm_api_key": "fake-key",
+ "llm_api_url": "http://invalid-llm",
+ "llm_max_retries": 1,
+ "llm_backoff_seconds": 0.0,
+ }
+ )
+
+ def always_fail(*args, **kwargs):
+ raise httpx.HTTPError("LLM service unavailable")
+
+ monkeypatch.setattr(httpx, "post", always_fail)
+
+ doc = {
+ "title": "HER2 Targeted Therapy in Breast Cancer",
+ "abstract": "Trastuzumab significantly improves progression-free survival in patients with HER2-positive breast cancer.",
+ }
+ summary = summarizer.summarize(doc)
+ assert summary["summary_type"] == "extractive_fallback"
+ assert summary["llm_used"] is False
+
+
+def test_relationship_extraction():
+ extractor = RelationshipExtractor()
+ doc = {
+ "title": "Trastuzumab clinical trial",
+ "abstract": "Trastuzumab targets HER2 and treats breast cancer.",
+ "source_id": "PMID999",
+ }
+ entities = [
+ {"text": "trastuzumab", "label": "drug", "category": "drugs"},
+ {"text": "her2", "label": "gene", "category": "genes"},
+ {"text": "breast cancer", "label": "disease", "category": "diseases"},
+ ]
+ relationships = extractor.extract(doc, entities)
+ predicates = [r["predicate"] for r in relationships]
+ assert "targets" in predicates
+ assert "treats" in predicates
+
+
+def test_duplicate_detection_four_tiers():
+ detector = DuplicateDetector()
+
+ # Tier 1: DOI
+ d1 = detector.detect_duplicate({"doi": "10.1000/a"}, {"doi": "10.1000/a"})
+ assert d1["duplicate"] is True and d1["reason"] == "doi_match"
+
+ # Tier 2: Source ID
+ d2 = detector.detect_duplicate({"source": "pubmed", "source_id": "123"}, {"source": "pubmed", "source_id": "123"})
+ assert d2["duplicate"] is True and d2["reason"] == "source_id_match"
+
+ # Tier 3: Title
+ d3 = detector.detect_duplicate({"title": "Breast Cancer Study!"}, {"title": "breast cancer study"})
+ assert d3["duplicate"] is True and d3["reason"] == "title_match"
+
+ # Tier 4: Content Hash
+ d4 = detector.detect_duplicate({"abstract": "Unique content string for testing hashing"}, {"content": "Unique content string for testing hashing"})
+ assert d4["duplicate"] is True and d4["reason"] == "content_hash_match"
+
+ # Distinct
+ d5 = detector.detect_duplicate({"title": "Paper A"}, {"title": "Paper B"})
+ assert d5["duplicate"] is False
+
+
+def test_evidence_ranking_deterministic_scoring():
+ ranker = EvidenceRanker()
+ high_doc = {
+ "source": "clinicaltrials",
+ "title": "Phase 3 Clinical Trial of Trastuzumab",
+ "published_date": "2024-01-01",
+ "metadata": {"overallStatus": "Completed"},
+ }
+ low_doc = {
+ "source": "biorxiv",
+ "title": "Preprint abstract",
+ "published_date": "2015-01-01",
+ }
+ high_rank = ranker.rank_document(high_doc)
+ low_rank = ranker.rank_document(low_doc)
+
+ assert high_rank["score"] > low_rank["score"]
+ assert high_rank["tier"] in ("High", "Medium")
+
+
+def test_kg_integration_success_and_graceful_failure():
+ kg_client = KGClient({"kg_service_url": "http://invalid-localhost-9999"})
+ entities = [{"text": "trastuzumab", "label": "drug", "category": "drugs"}]
+ relationships = [{"subject": "trastuzumab", "predicate": "targets", "object": "her2"}]
+
+ res = kg_client.update_knowledge_graph(entities, relationships)
+ assert res["success"] is False
+ assert res["retry_eligible"] is True
+
+ with patch("httpx.Client.post") as mock_post:
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_post.return_value = mock_response
+
+ valid_client = KGClient({"kg_service_url": "http://localhost:8001"})
+ res_ok = valid_client.update_knowledge_graph(entities, relationships)
+ assert res_ok["success"] is True
+
+
+def test_wiki_integration_okf_volume_write(tmp_path):
+ wiki_client = LLMWikiClient({"wiki_dir": str(tmp_path)})
+ doc = {"source": "pubmed", "source_id": "PMID123", "title": "HER2 Study", "doi": "10.1000/xyz", "url": "https://example.com"}
+ entities = [{"text": "her2", "category": "genes"}]
+ summary = {"concise_summary": "HER2 targeted therapy."}
+
+ res = wiki_client.update_wiki(doc, entities, summary)
+ assert res["success"] is True
+ assert res["method"] == "okf_volume"
+
+ concept_file = tmp_path / "wiki" / "genes" / "her2.md"
+ assert concept_file.exists()
+ assert "HER2 Study" in concept_file.read_text(encoding="utf-8")
+
+
+def test_orchestrator_state_transitions_and_dead_letter():
+ job = IngestionJobState(id="job-test-1", source="pubmed", query="oncology")
+ job_store.save(job)
+
+ def failing_stage(payload):
+ raise ValueError("Stage failure simulation")
+
+ runner = PipelineRunner(stages=[failing_stage], retries=1, delay_seconds=0.01)
+ result = runner.run_with_job(job, {"query": "oncology"})
+
+ assert result["status"] == "dead_letter"
+ updated_job = job_store.get("job-test-1")
+ assert updated_job.status == "dead_letter"
+ assert "Stage failure simulation" in updated_job.error
+
+
+def test_end_to_end_ingestion_api():
+ response = client.post("/api/v1/ingestion", json={"source": "pubmed", "query": "trastuzumab HER2"})
+ assert response.status_code == 202
+ data = response.json()
+ assert "id" in data
+ assert data["status"] in ("completed", "pending", "running")
+
+ job_res = client.get(f"/api/v1/ingestion/{data['id']}")
+ assert job_res.status_code == 200
+ job_data = job_res.json()
+ assert job_data["status"] == "completed"
+ assert "result" in job_data
+ assert "source_status" in job_data["result"]
diff --git a/services/literature/tests/test_prompt6_requirements.py b/services/literature/tests/test_prompt6_requirements.py
new file mode 100644
index 0000000..40671be
--- /dev/null
+++ b/services/literature/tests/test_prompt6_requirements.py
@@ -0,0 +1,90 @@
+from app.connectors.factory import ConnectorFactory
+from app.nlp.pipeline import LiteratureNLP
+from app.orchestrator.pipeline import PipelineRunner
+from app.services.literature_service import LiteratureService
+
+
+def test_all_required_connectors_are_registered():
+ required = {
+ "pubmed",
+ "pmc",
+ "clinicaltrials",
+ "aacr",
+ "asco",
+ "sabcs",
+ "esmo",
+ "biorxiv",
+ "medrxiv",
+ "patents",
+ "company_websites",
+ }
+ available = set(ConnectorFactory.registry)
+ assert required.issubset(available)
+
+
+def test_connector_factory_instantiates_registered_source():
+ connector = ConnectorFactory.create("clinicaltrials", {"base_url": "https://example.com"})
+ assert connector.name == "clinicaltrials"
+
+
+def test_parser_normalizes_fields():
+ source = {
+ "title": "HER2-targeted therapy in breast cancer",
+ "abstract": "Trastuzumab improved outcomes in HER2-positive breast cancer.",
+ "authors": ["Alice", "Bob"],
+ "published_date": "2024-01-15",
+ "source": "pubmed",
+ "source_id": "PMID123",
+ "doi": "10.1000/example",
+ "url": "https://example.com/article",
+ }
+ parsed = LiteratureNLP().parse_document(source)
+ assert parsed["title"] == "HER2-targeted therapy in breast cancer"
+ assert parsed["source"] == "pubmed"
+ assert parsed["authors"] == ["Alice", "Bob"]
+ assert parsed["doi"] == "10.1000/example"
+
+
+def test_duplicate_detector_flags_same_document():
+ doc_a = {"doi": "10.1000/example", "title": "HER2 targeted therapy", "source": "pubmed", "source_id": "PMID123"}
+ doc_b = {"doi": "10.1000/example", "title": "HER2 targeted therapy", "source": "pubmed", "source_id": "PMID123"}
+ result = LiteratureNLP().detect_duplicate(doc_a, doc_b)
+ assert result["duplicate"] is True
+
+
+def test_relationship_extraction_builds_triples():
+ doc = {
+ "title": "Trastuzumab targets HER2 and treats breast cancer",
+ "abstract": "Trastuzumab is associated with HER2-positive breast cancer.",
+ "entities": [{"text": "trastuzumab", "label": "drug"}, {"text": "her2", "label": "gene"}, {"text": "breast cancer", "label": "disease"}],
+ }
+ relationships = LiteratureNLP().extract_relationships(doc)
+ assert relationships
+ assert any(r["subject"].lower() == "trastuzumab" for r in relationships)
+
+
+def test_ranking_and_summary_are_deterministic():
+ result = LiteratureNLP().run("Trastuzumab targets HER2 in breast cancer. HER2-positive disease is studied in clinical trials.")
+ assert "summary" in result
+ assert "evidence" in result
+ assert result["evidence"][0]["score"] >= 0
+
+
+def test_orchestrator_runs_complete_pipeline():
+ runner = PipelineRunner(
+ stages=[
+ lambda doc: {**doc, "parsed": True},
+ lambda doc: {**doc, "deduplicated": True},
+ lambda doc: {**doc, "processed": True},
+ ]
+ )
+ result = runner.run({"id": "paper-1"})
+ assert result["processed"] is True
+
+
+def test_service_ingest_uses_registry_and_returns_payload():
+ service = LiteratureService()
+ outcome = service.ingest("pubmed", "HER2 breast cancer", base_url="https://example.com")
+ assert "source" in outcome
+ assert outcome["source"] == "pubmed"
+ assert "items" in outcome
diff --git a/services/literature/wiki-root/wiki/diseases/breast_cancer.md b/services/literature/wiki-root/wiki/diseases/breast_cancer.md
new file mode 100644
index 0000000..2d200b9
--- /dev/null
+++ b/services/literature/wiki-root/wiki/diseases/breast_cancer.md
@@ -0,0 +1,12 @@
+# Concept: breast cancer
+- **Category**: diseases
+- **Last Updated**: 2026-08-09T15:32:27.801230+00:00
+- **Source**: pubmed (pubmed-33880)
+
+## Primary Summary
+Literature document retrieved for query: HER2 breast cancer
+
+## Literature Evidence
+- **Title**: PubMed result for: HER2 breast cancer
+- **DOI**: 10.1016/j.pubmed.3880
+- **URL**: https://pubmed.ncbi.nlm.nih.gov
diff --git a/services/literature/wiki-root/wiki/drugs/trastuzumab.md b/services/literature/wiki-root/wiki/drugs/trastuzumab.md
new file mode 100644
index 0000000..6e45473
--- /dev/null
+++ b/services/literature/wiki-root/wiki/drugs/trastuzumab.md
@@ -0,0 +1,12 @@
+# Concept: trastuzumab
+- **Category**: drugs
+- **Last Updated**: 2026-08-09T15:32:25.732174+00:00
+- **Source**: pubmed (pubmed-47258)
+
+## Primary Summary
+Literature document retrieved for query: trastuzumab HER2
+
+## Literature Evidence
+- **Title**: PubMed result for: trastuzumab HER2
+- **DOI**: 10.1016/j.pubmed.7258
+- **URL**: https://pubmed.ncbi.nlm.nih.gov
diff --git a/services/literature/wiki-root/wiki/genes/her2.md b/services/literature/wiki-root/wiki/genes/her2.md
new file mode 100644
index 0000000..29ff98e
--- /dev/null
+++ b/services/literature/wiki-root/wiki/genes/her2.md
@@ -0,0 +1,12 @@
+# Concept: her2
+- **Category**: genes
+- **Last Updated**: 2026-08-09T15:32:27.801230+00:00
+- **Source**: pubmed (pubmed-33880)
+
+## Primary Summary
+Literature document retrieved for query: HER2 breast cancer
+
+## Literature Evidence
+- **Title**: PubMed result for: HER2 breast cancer
+- **DOI**: 10.1016/j.pubmed.3880
+- **URL**: https://pubmed.ncbi.nlm.nih.gov
diff --git a/services/literature/wiki-root/wiki/log.md b/services/literature/wiki-root/wiki/log.md
new file mode 100644
index 0000000..0cb4796
--- /dev/null
+++ b/services/literature/wiki-root/wiki/log.md
@@ -0,0 +1,19 @@
+- [2026-08-09T15:15:25.750325+00:00] Updated 2 concept pages from pubmed:pubmed-17947
+- [2026-08-09T15:18:00.732256+00:00] Updated 0 concept pages from pubmed:PMID:42571049
+- [2026-08-09T15:18:04.374312+00:00] Updated 0 concept pages from pubmed:PMID:42569044
+- [2026-08-09T15:18:08.520940+00:00] Updated 0 concept pages from pubmed:PMID:42563503
+- [2026-08-09T15:18:12.280801+00:00] Updated 0 concept pages from pubmed:PMID:42562949
+- [2026-08-09T15:18:16.097921+00:00] Updated 0 concept pages from pubmed:PMID:42562610
+- [2026-08-09T15:18:22.833199+00:00] Updated 2 concept pages from pubmed:pubmed-60255
+- [2026-08-09T15:19:13.555271+00:00] Updated 2 concept pages from pubmed:pubmed-10318
+- [2026-08-09T15:19:20.568237+00:00] Updated 2 concept pages from pubmed:pubmed-18816
+- [2026-08-09T15:19:59.811735+00:00] Updated 2 concept pages from pubmed:pubmed-73101
+- [2026-08-09T15:20:07.696794+00:00] Updated 2 concept pages from pubmed:pubmed-83263
+- [2026-08-09T15:20:23.546550+00:00] Updated 2 concept pages from pubmed:pubmed-371
+- [2026-08-09T15:20:41.892568+00:00] Updated 2 concept pages from pubmed:pubmed-17592
+- [2026-08-09T15:21:56.960572+00:00] Updated 2 concept pages from pubmed:pubmed-22209
+- [2026-08-09T15:22:01.466167+00:00] Updated 2 concept pages from pubmed:pubmed-56201
+- [2026-08-09T15:22:02.204525+00:00] Updated 2 concept pages from pubmed:pubmed-53528
+- [2026-08-09T15:22:04.841350+00:00] Updated 2 concept pages from pubmed:pubmed-88637
+- [2026-08-09T15:32:25.732174+00:00] Updated 2 concept pages from pubmed:pubmed-47258
+- [2026-08-09T15:32:27.801230+00:00] Updated 2 concept pages from pubmed:pubmed-33880