open-pharma-plugins 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_framework.py +495 -0
- open_pharma_plugins-2.2.0.dist-info/METADATA +135 -0
- open_pharma_plugins-2.2.0.dist-info/RECORD +136 -0
- open_pharma_plugins-2.2.0.dist-info/WHEEL +5 -0
- open_pharma_plugins-2.2.0.dist-info/entry_points.txt +8 -0
- open_pharma_plugins-2.2.0.dist-info/licenses/LICENSE +202 -0
- open_pharma_plugins-2.2.0.dist-info/top_level.txt +8 -0
- open_pharma_plugins_campaign_studio/__init__.py +14 -0
- open_pharma_plugins_campaign_studio/__main__.py +11 -0
- open_pharma_plugins_campaign_studio/_campaign_store.py +162 -0
- open_pharma_plugins_campaign_studio/_claim_engine.py +262 -0
- open_pharma_plugins_campaign_studio/_renderer.py +119 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/legal.json +21 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/logo.svg +4 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/palette.json +11 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/product.png +1 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/typography.json +14 -0
- open_pharma_plugins_campaign_studio/fixtures/sample_approved_claims.json +119 -0
- open_pharma_plugins_campaign_studio/models/__init__.py +34 -0
- open_pharma_plugins_campaign_studio/models/_common.py +12 -0
- open_pharma_plugins_campaign_studio/models/brief.py +72 -0
- open_pharma_plugins_campaign_studio/models/claims.py +15 -0
- open_pharma_plugins_campaign_studio/models/copy.py +47 -0
- open_pharma_plugins_campaign_studio/models/journey.py +21 -0
- open_pharma_plugins_campaign_studio/models/message.py +25 -0
- open_pharma_plugins_campaign_studio/models/mlr.py +29 -0
- open_pharma_plugins_campaign_studio/models/validation.py +32 -0
- open_pharma_plugins_campaign_studio/policy/rules.json +79 -0
- open_pharma_plugins_campaign_studio/templates/banner.svg.j2 +26 -0
- open_pharma_plugins_campaign_studio/templates/email.html.j2 +54 -0
- open_pharma_plugins_campaign_studio/tools/__init__.py +0 -0
- open_pharma_plugins_campaign_studio/tools/create_campaign_brief.py +212 -0
- open_pharma_plugins_campaign_studio/tools/generate_audience_journey.py +129 -0
- open_pharma_plugins_campaign_studio/tools/generate_channel_copy.py +199 -0
- open_pharma_plugins_campaign_studio/tools/generate_message_architecture.py +121 -0
- open_pharma_plugins_campaign_studio/tools/package_mlr_submission.py +230 -0
- open_pharma_plugins_campaign_studio/tools/render_banner.py +99 -0
- open_pharma_plugins_campaign_studio/tools/render_email.py +101 -0
- open_pharma_plugins_campaign_studio/tools/render_poster.py +222 -0
- open_pharma_plugins_campaign_studio/tools/retrieve_approved_claims.py +71 -0
- open_pharma_plugins_campaign_studio/tools/retrieve_brand_components.py +76 -0
- open_pharma_plugins_campaign_studio/tools/validate_claims_and_fair_balance.py +285 -0
- open_pharma_plugins_competitive_intelligence/__init__.py +13 -0
- open_pharma_plugins_competitive_intelligence/__main__.py +11 -0
- open_pharma_plugins_competitive_intelligence/_artifacts.py +87 -0
- open_pharma_plugins_competitive_intelligence/_cache.py +144 -0
- open_pharma_plugins_competitive_intelligence/_clinical_trials.py +569 -0
- open_pharma_plugins_competitive_intelligence/_dailymed.py +260 -0
- open_pharma_plugins_competitive_intelligence/_fda.py +255 -0
- open_pharma_plugins_competitive_intelligence/_pubmed.py +342 -0
- open_pharma_plugins_competitive_intelligence/_regulatory.py +140 -0
- open_pharma_plugins_competitive_intelligence/_runs.py +278 -0
- open_pharma_plugins_competitive_intelligence/_transport.py +83 -0
- open_pharma_plugins_competitive_intelligence/_watchlist.py +113 -0
- open_pharma_plugins_competitive_intelligence/_web_search.py +331 -0
- open_pharma_plugins_competitive_intelligence/models.py +525 -0
- open_pharma_plugins_competitive_intelligence/tools/__init__.py +0 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_extract_events.py +247 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_landscape.py +195 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_refresh.py +101 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_report.py +402 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_news.py +48 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_publications.py +46 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_regulatory.py +64 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_trials.py +85 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_status.py +106 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_timeline.py +453 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_track.py +121 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_trial_detail.py +55 -0
- open_pharma_plugins_field_training/__init__.py +13 -0
- open_pharma_plugins_field_training/__main__.py +11 -0
- open_pharma_plugins_field_training/_content_store.py +131 -0
- open_pharma_plugins_field_training/_grounding.py +75 -0
- open_pharma_plugins_field_training/_html_renderers.py +546 -0
- open_pharma_plugins_field_training/fixtures/sample_product_message.pdf +156 -0
- open_pharma_plugins_field_training/fixtures/sample_training_deck.pptx +0 -0
- open_pharma_plugins_field_training/models.py +265 -0
- open_pharma_plugins_field_training/tools/__init__.py +0 -0
- open_pharma_plugins_field_training/tools/get_document_page.py +67 -0
- open_pharma_plugins_field_training/tools/ingest_document.py +147 -0
- open_pharma_plugins_field_training/tools/list_documents.py +51 -0
- open_pharma_plugins_field_training/tools/render_output.py +118 -0
- open_pharma_plugins_field_training/tools/search_content.py +57 -0
- open_pharma_plugins_hcp_intelligence/__init__.py +14 -0
- open_pharma_plugins_hcp_intelligence/__main__.py +11 -0
- open_pharma_plugins_hcp_intelligence/_crm_store.py +70 -0
- open_pharma_plugins_hcp_intelligence/batch.py +891 -0
- open_pharma_plugins_hcp_intelligence/batch_cli.py +221 -0
- open_pharma_plugins_hcp_intelligence/batch_csv.py +206 -0
- open_pharma_plugins_hcp_intelligence/fixtures/sample_accounts.csv +27 -0
- open_pharma_plugins_hcp_intelligence/models.py +356 -0
- open_pharma_plugins_hcp_intelligence/tools/__init__.py +0 -0
- open_pharma_plugins_hcp_intelligence/tools/get_account.py +48 -0
- open_pharma_plugins_hcp_intelligence/tools/list_accounts.py +66 -0
- open_pharma_plugins_hcp_intelligence/tools/search_clinical_trials.py +175 -0
- open_pharma_plugins_hcp_intelligence/tools/search_congresses.py +134 -0
- open_pharma_plugins_hcp_intelligence/tools/search_grants.py +181 -0
- open_pharma_plugins_hcp_intelligence/tools/search_guidelines.py +238 -0
- open_pharma_plugins_hcp_intelligence/tools/search_hco_web.py +73 -0
- open_pharma_plugins_hcp_intelligence/tools/search_hcp_web.py +199 -0
- open_pharma_plugins_hcp_intelligence/tools/search_orcid.py +214 -0
- open_pharma_plugins_hcp_intelligence/tools/search_publications.py +207 -0
- open_pharma_plugins_hcp_intelligence/tools/update_account.py +84 -0
- open_pharma_plugins_next_best_engagement/__init__.py +14 -0
- open_pharma_plugins_next_best_engagement/__main__.py +11 -0
- open_pharma_plugins_next_best_engagement/_optimizer.py +400 -0
- open_pharma_plugins_next_best_engagement/_renderer.py +149 -0
- open_pharma_plugins_next_best_engagement/_scoring.py +82 -0
- open_pharma_plugins_next_best_engagement/_universe.py +145 -0
- open_pharma_plugins_next_best_engagement/fixtures/sample_universe.csv +81 -0
- open_pharma_plugins_next_best_engagement/models.py +135 -0
- open_pharma_plugins_next_best_engagement/tools/__init__.py +0 -0
- open_pharma_plugins_next_best_engagement/tools/load_universe.py +47 -0
- open_pharma_plugins_next_best_engagement/tools/recommend_engagements.py +80 -0
- open_pharma_plugins_next_best_engagement/tools/render_plan.py +90 -0
- open_pharma_plugins_territory_alignment/__init__.py +14 -0
- open_pharma_plugins_territory_alignment/__main__.py +11 -0
- open_pharma_plugins_territory_alignment/data.py +300 -0
- open_pharma_plugins_territory_alignment/fixtures/constraints.csv +11 -0
- open_pharma_plugins_territory_alignment/fixtures/current_alignment.csv +81 -0
- open_pharma_plugins_territory_alignment/fixtures/hcps.csv +81 -0
- open_pharma_plugins_territory_alignment/fixtures/reps.csv +9 -0
- open_pharma_plugins_territory_alignment/geo.py +175 -0
- open_pharma_plugins_territory_alignment/models.py +201 -0
- open_pharma_plugins_territory_alignment/scoring.py +125 -0
- open_pharma_plugins_territory_alignment/solver.py +504 -0
- open_pharma_plugins_territory_alignment/tools/__init__.py +0 -0
- open_pharma_plugins_territory_alignment/tools/ta_align.py +138 -0
- open_pharma_plugins_territory_alignment/tools/ta_cluster.py +247 -0
- open_pharma_plugins_territory_alignment/tools/ta_compare.py +173 -0
- open_pharma_plugins_territory_alignment/tools/ta_evaluate.py +119 -0
- open_pharma_plugins_territory_alignment/tools/ta_status.py +34 -0
- open_pharma_plugins_territory_alignment/tools/ta_visualize.py +504 -0
- shared/__init__.py +11 -0
- shared/env.py +217 -0
- shared/filesystem.py +110 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""search_congresses — web search for conference speaking roles and KOL signals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
_CONGRESS_MAP: dict[str, list[str]] = {
|
|
10
|
+
"oncology": ["ASCO", "ESMO", "AACR", "ASH"],
|
|
11
|
+
"hematology": ["ASH", "EHA", "ASCO"],
|
|
12
|
+
"cardiology": ["AHA", "ESC", "ACC", "HRS"],
|
|
13
|
+
"endocrinology": ["ADA", "EASD", "ENDO"],
|
|
14
|
+
"diabetes": ["ADA", "EASD", "ENDO"],
|
|
15
|
+
"neurology": ["AAN", "EAN", "AES"],
|
|
16
|
+
"respiratory": ["ATS", "ERS", "CHEST"],
|
|
17
|
+
"pulmonology": ["ATS", "ERS", "CHEST"],
|
|
18
|
+
"immunology": ["ACR", "EULAR", "AAI"],
|
|
19
|
+
"rheumatology": ["ACR", "EULAR", "AAI"],
|
|
20
|
+
"infectious disease": ["IDWeek", "ECCMID", "CROI"],
|
|
21
|
+
"paediatrics": ["AAP", "ESPID", "EPA"],
|
|
22
|
+
"pediatrics": ["AAP", "ESPID", "EPA"],
|
|
23
|
+
"gastroenterology": ["DDW", "UEG", "AASLD"],
|
|
24
|
+
"hepatology": ["AASLD", "EASL", "DDW"],
|
|
25
|
+
"nephrology": ["ASN", "ERA", "WCN"],
|
|
26
|
+
"dermatology": ["AAD", "EADV", "SID"],
|
|
27
|
+
"ophthalmology": ["AAO", "ARVO", "EURETINA"],
|
|
28
|
+
"urology": ["AUA", "EAU"],
|
|
29
|
+
"psychiatry": ["APA", "EPA", "WPA"],
|
|
30
|
+
"surgery": ["ACS", "EAES", "SAGES"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve_congresses(
|
|
35
|
+
specialty: str | None,
|
|
36
|
+
therapeutic_area: str | None,
|
|
37
|
+
explicit: list[str] | None,
|
|
38
|
+
) -> list[str]:
|
|
39
|
+
if explicit:
|
|
40
|
+
return explicit
|
|
41
|
+
|
|
42
|
+
for key_source in (therapeutic_area, specialty):
|
|
43
|
+
if not key_source:
|
|
44
|
+
continue
|
|
45
|
+
needle = key_source.strip().lower()
|
|
46
|
+
for key, congresses in _CONGRESS_MAP.items():
|
|
47
|
+
if key in needle or needle in key:
|
|
48
|
+
return congresses
|
|
49
|
+
|
|
50
|
+
if specialty:
|
|
51
|
+
return [specialty.strip()]
|
|
52
|
+
return ["medical congress"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SearchCongressesArgs(BaseModel):
|
|
56
|
+
name: str = Field(description="Full name of the healthcare professional")
|
|
57
|
+
specialty: str | None = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="Medical specialty (used to select relevant congresses if congress_names is not provided)",
|
|
60
|
+
)
|
|
61
|
+
therapeutic_area: str | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description=("Therapeutic area to narrow congress search (e.g. 'oncology', 'cardiology', 'diabetes')"),
|
|
64
|
+
)
|
|
65
|
+
country: str | None = Field(default=None, description="Country of practice")
|
|
66
|
+
congress_names: list[str] | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description=(
|
|
69
|
+
"Specific congress names to search (e.g. ['ASCO', 'ESMO']). "
|
|
70
|
+
"If not provided, the tool selects congresses based on specialty "
|
|
71
|
+
"or therapeutic_area."
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
max_results: int = Field(
|
|
75
|
+
default=15,
|
|
76
|
+
ge=1,
|
|
77
|
+
le=30,
|
|
78
|
+
description="Maximum web results to return",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
TOOL: dict[str, Any] = {
|
|
83
|
+
"name": "search_congresses",
|
|
84
|
+
"description": (
|
|
85
|
+
"Search for an HCP's conference speaking roles, poster presentations, "
|
|
86
|
+
"and advisory board visibility at major medical congresses. Results "
|
|
87
|
+
"signal KOL status: invited keynote, symposium speaker, oral "
|
|
88
|
+
"presentation, poster presenter, session chair, or moderator. "
|
|
89
|
+
"Provide specialty or therapeutic_area to auto-select relevant "
|
|
90
|
+
"congresses, or pass explicit congress_names."
|
|
91
|
+
),
|
|
92
|
+
"args": SearchCongressesArgs,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
97
|
+
import json
|
|
98
|
+
from datetime import datetime, timezone
|
|
99
|
+
|
|
100
|
+
from .search_hcp_web import _backend_name, _web_search
|
|
101
|
+
|
|
102
|
+
congresses = _resolve_congresses(
|
|
103
|
+
arguments.get("specialty"),
|
|
104
|
+
arguments.get("therapeutic_area"),
|
|
105
|
+
arguments.get("congress_names"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
congress_clause = " OR ".join(congresses)
|
|
109
|
+
role_clause = (
|
|
110
|
+
"speaker OR lecture OR presentation OR poster OR symposium "
|
|
111
|
+
"OR chair OR moderator OR plenary OR keynote OR abstract"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
query_parts = [
|
|
115
|
+
f'"{arguments["name"]}"',
|
|
116
|
+
f"({congress_clause})",
|
|
117
|
+
f"({role_clause})",
|
|
118
|
+
]
|
|
119
|
+
if arguments.get("country"):
|
|
120
|
+
query_parts.append(arguments["country"])
|
|
121
|
+
|
|
122
|
+
query = " ".join(query_parts)
|
|
123
|
+
max_results = arguments.get("max_results", 15)
|
|
124
|
+
|
|
125
|
+
results = _web_search(query, max_results)
|
|
126
|
+
|
|
127
|
+
output = {
|
|
128
|
+
"query": query,
|
|
129
|
+
"results": results,
|
|
130
|
+
"congresses_searched": congresses,
|
|
131
|
+
"search_backend": _backend_name(),
|
|
132
|
+
"searched_at": datetime.now(timezone.utc).isoformat(),
|
|
133
|
+
}
|
|
134
|
+
return [{"type": "text", "text": json.dumps(output, indent=2)}]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""search_grants — query NIH RePORTER for research grant funding, with web fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchGrantsArgs(BaseModel):
|
|
11
|
+
pi_name: str = Field(description="Full name of the principal investigator (e.g. 'Yvonne Lim')")
|
|
12
|
+
institution: str | None = Field(
|
|
13
|
+
default=None,
|
|
14
|
+
description="Institution or organization name to narrow results",
|
|
15
|
+
)
|
|
16
|
+
keywords: str | None = Field(
|
|
17
|
+
default=None,
|
|
18
|
+
description="Research topic keywords (e.g. 'immunotherapy melanoma')",
|
|
19
|
+
)
|
|
20
|
+
country: str | None = Field(
|
|
21
|
+
default=None,
|
|
22
|
+
description="Country of the PI (used for web fallback when NIH RePORTER returns no results)",
|
|
23
|
+
)
|
|
24
|
+
active_only: bool = Field(
|
|
25
|
+
default=False,
|
|
26
|
+
description="If true, only return currently active grants",
|
|
27
|
+
)
|
|
28
|
+
max_results: int = Field(
|
|
29
|
+
default=20,
|
|
30
|
+
ge=1,
|
|
31
|
+
le=50,
|
|
32
|
+
description="Maximum grants to return",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TOOL: dict[str, Any] = {
|
|
37
|
+
"name": "search_grants",
|
|
38
|
+
"description": (
|
|
39
|
+
"Search for research grant funding awarded to a principal investigator. "
|
|
40
|
+
"Queries NIH RePORTER (covers NIH, NSF, and other US federal agencies) and "
|
|
41
|
+
"falls back to web search for non-US funding bodies. Returns grant number, "
|
|
42
|
+
"title, PI, institution, award amount, dates, activity status, and agency. "
|
|
43
|
+
"Active funding signals an active researcher; amounts indicate research "
|
|
44
|
+
"program scale; co-PIs reveal collaboration networks."
|
|
45
|
+
),
|
|
46
|
+
"args": SearchGrantsArgs,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
51
|
+
import json
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
|
|
54
|
+
pi_name = arguments["pi_name"]
|
|
55
|
+
institution = arguments.get("institution")
|
|
56
|
+
keywords = arguments.get("keywords")
|
|
57
|
+
country = arguments.get("country")
|
|
58
|
+
active_only = arguments.get("active_only", False)
|
|
59
|
+
max_results = arguments.get("max_results", 20)
|
|
60
|
+
|
|
61
|
+
grants, total_count, source = _search_nih_reporter(pi_name, institution, keywords, active_only, max_results)
|
|
62
|
+
|
|
63
|
+
web_fallback_results = None
|
|
64
|
+
if total_count == 0 and country and country.lower() not in ("us", "usa", "united states"):
|
|
65
|
+
web_fallback_results = _web_fallback(pi_name, institution, country)
|
|
66
|
+
source = "web_fallback"
|
|
67
|
+
|
|
68
|
+
query_desc = f"PI: {pi_name}"
|
|
69
|
+
if institution:
|
|
70
|
+
query_desc += f", institution: {institution}"
|
|
71
|
+
if keywords:
|
|
72
|
+
query_desc += f", keywords: {keywords}"
|
|
73
|
+
if active_only:
|
|
74
|
+
query_desc += " (active only)"
|
|
75
|
+
|
|
76
|
+
output: dict[str, Any] = {
|
|
77
|
+
"query": query_desc,
|
|
78
|
+
"total_count": total_count,
|
|
79
|
+
"grants": grants,
|
|
80
|
+
"source": source,
|
|
81
|
+
"searched_at": datetime.now(timezone.utc).isoformat(),
|
|
82
|
+
}
|
|
83
|
+
if web_fallback_results is not None:
|
|
84
|
+
output["web_fallback_results"] = web_fallback_results
|
|
85
|
+
|
|
86
|
+
return [{"type": "text", "text": json.dumps(output, indent=2)}]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _search_nih_reporter(
|
|
90
|
+
pi_name: str,
|
|
91
|
+
institution: str | None,
|
|
92
|
+
keywords: str | None,
|
|
93
|
+
active_only: bool,
|
|
94
|
+
max_results: int,
|
|
95
|
+
) -> tuple[list[dict], int, str]:
|
|
96
|
+
import json
|
|
97
|
+
import urllib.request
|
|
98
|
+
|
|
99
|
+
criteria: dict[str, Any] = {
|
|
100
|
+
"pi_names": [{"any_name": pi_name}],
|
|
101
|
+
}
|
|
102
|
+
if institution:
|
|
103
|
+
criteria["org_names"] = [institution]
|
|
104
|
+
if active_only:
|
|
105
|
+
criteria["is_active"] = True
|
|
106
|
+
if keywords:
|
|
107
|
+
criteria["advanced_text_search"] = {
|
|
108
|
+
"operator": "and",
|
|
109
|
+
"search_field": "terms",
|
|
110
|
+
"search_text": keywords,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
body = json.dumps(
|
|
114
|
+
{
|
|
115
|
+
"criteria": criteria,
|
|
116
|
+
"offset": 0,
|
|
117
|
+
"limit": max_results,
|
|
118
|
+
"sort_field": "project_start_date",
|
|
119
|
+
"sort_order": "desc",
|
|
120
|
+
}
|
|
121
|
+
).encode()
|
|
122
|
+
|
|
123
|
+
req = urllib.request.Request(
|
|
124
|
+
"https://api.reporter.nih.gov/v2/projects/search",
|
|
125
|
+
data=body,
|
|
126
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
131
|
+
data = json.loads(resp.read())
|
|
132
|
+
except Exception:
|
|
133
|
+
return [], 0, "nih_reporter"
|
|
134
|
+
|
|
135
|
+
total_count = data.get("meta", {}).get("total", 0)
|
|
136
|
+
results = data.get("results", [])
|
|
137
|
+
|
|
138
|
+
grants = []
|
|
139
|
+
for r in results:
|
|
140
|
+
pis = r.get("principal_investigators", [])
|
|
141
|
+
pi_full_name = pis[0].get("full_name", "") if pis else ""
|
|
142
|
+
|
|
143
|
+
org = r.get("organization", {})
|
|
144
|
+
org_name = org.get("org_name", "")
|
|
145
|
+
|
|
146
|
+
abstract = r.get("abstract_text") or ""
|
|
147
|
+
if len(abstract) > 500:
|
|
148
|
+
abstract = abstract[:497] + "..."
|
|
149
|
+
|
|
150
|
+
app_id = r.get("appl_id", "")
|
|
151
|
+
|
|
152
|
+
grants.append(
|
|
153
|
+
{
|
|
154
|
+
"project_number": r.get("project_num", ""),
|
|
155
|
+
"title": r.get("project_title", ""),
|
|
156
|
+
"abstract": abstract,
|
|
157
|
+
"pi_name": pi_full_name,
|
|
158
|
+
"institution": org_name,
|
|
159
|
+
"award_amount": r.get("award_amount"),
|
|
160
|
+
"start_date": r.get("project_start_date"),
|
|
161
|
+
"end_date": r.get("project_end_date"),
|
|
162
|
+
"agency_code": r.get("agency_code", ""),
|
|
163
|
+
"is_active": r.get("is_active", False),
|
|
164
|
+
"source_url": f"https://reporter.nih.gov/project-details/{app_id}" if app_id else "",
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return grants, total_count, "nih_reporter"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _web_fallback(pi_name: str, institution: str | None, country: str | None) -> list[dict]:
|
|
172
|
+
from .search_hcp_web import _web_search
|
|
173
|
+
|
|
174
|
+
parts = [f'"{pi_name}"', "(research grant OR funding OR funded by)"]
|
|
175
|
+
if institution:
|
|
176
|
+
parts.append(institution)
|
|
177
|
+
if country:
|
|
178
|
+
parts.append(country)
|
|
179
|
+
query = " ".join(parts)
|
|
180
|
+
|
|
181
|
+
return _web_search(query, 10)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""search_guidelines — guideline authorship (PubMed) + regulatory advisory roles (web)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchGuidelinesArgs(BaseModel):
|
|
11
|
+
name: str = Field(description="Full name of the HCP (e.g. 'Yvonne Lim')")
|
|
12
|
+
specialty: str | None = Field(
|
|
13
|
+
default=None,
|
|
14
|
+
description="Medical specialty to narrow guideline search (e.g. 'Paediatric Medicine')",
|
|
15
|
+
)
|
|
16
|
+
scope: Literal["guidelines", "regulatory", "both"] = Field(
|
|
17
|
+
default="both",
|
|
18
|
+
description=(
|
|
19
|
+
"What to search: 'guidelines' for PubMed guideline/consensus publications, "
|
|
20
|
+
"'regulatory' for FDA/EMA/WHO advisory committee rosters, or 'both'."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
therapeutic_area: str | None = Field(
|
|
24
|
+
default=None,
|
|
25
|
+
description="Therapeutic area or disease to narrow the guideline search (e.g. 'diabetes')",
|
|
26
|
+
)
|
|
27
|
+
max_results: int = Field(
|
|
28
|
+
default=20,
|
|
29
|
+
ge=1,
|
|
30
|
+
le=50,
|
|
31
|
+
description="Maximum results to return per scope (guidelines and regulatory each up to this limit)",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
TOOL: dict[str, Any] = {
|
|
36
|
+
"name": "search_guidelines",
|
|
37
|
+
"description": (
|
|
38
|
+
"Search for an HCP's clinical guideline authorship and regulatory advisory "
|
|
39
|
+
"committee roles. Guideline authorship (via PubMed) and regulatory influence "
|
|
40
|
+
"(FDA/EMA/WHO advisory membership, via web search) are top-tier KOL signals. "
|
|
41
|
+
"Use scope to search guidelines only, regulatory only, or both."
|
|
42
|
+
),
|
|
43
|
+
"args": SearchGuidelinesArgs,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
48
|
+
import json
|
|
49
|
+
from datetime import datetime, timezone
|
|
50
|
+
|
|
51
|
+
scope = arguments.get("scope", "both")
|
|
52
|
+
|
|
53
|
+
guideline_publications: list[dict] = []
|
|
54
|
+
total_guidelines_found = 0
|
|
55
|
+
guideline_query = ""
|
|
56
|
+
|
|
57
|
+
regulatory_results: list[dict] = []
|
|
58
|
+
total_regulatory_results = 0
|
|
59
|
+
regulatory_query = ""
|
|
60
|
+
|
|
61
|
+
if scope in ("guidelines", "both"):
|
|
62
|
+
guideline_query, total_guidelines_found, guideline_publications = _search_guideline_publications(arguments)
|
|
63
|
+
|
|
64
|
+
if scope in ("regulatory", "both"):
|
|
65
|
+
regulatory_query, regulatory_results = _search_regulatory_roles(arguments)
|
|
66
|
+
total_regulatory_results = len(regulatory_results)
|
|
67
|
+
|
|
68
|
+
query = guideline_query or regulatory_query
|
|
69
|
+
if guideline_query and regulatory_query:
|
|
70
|
+
query = f"guidelines: {guideline_query} | regulatory: {regulatory_query}"
|
|
71
|
+
|
|
72
|
+
output = {
|
|
73
|
+
"query": query,
|
|
74
|
+
"guideline_publications": guideline_publications,
|
|
75
|
+
"regulatory_results": regulatory_results,
|
|
76
|
+
"total_guidelines_found": total_guidelines_found,
|
|
77
|
+
"total_regulatory_results": total_regulatory_results,
|
|
78
|
+
"scope": scope,
|
|
79
|
+
"searched_at": datetime.now(timezone.utc).isoformat(),
|
|
80
|
+
}
|
|
81
|
+
return [{"type": "text", "text": json.dumps(output, indent=2)}]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _search_guideline_publications(
|
|
85
|
+
arguments: dict[str, Any],
|
|
86
|
+
) -> tuple[str, int, list[dict]]:
|
|
87
|
+
"""Query PubMed for guideline/consensus publications authored by the HCP."""
|
|
88
|
+
import json
|
|
89
|
+
import urllib.parse
|
|
90
|
+
import urllib.request
|
|
91
|
+
|
|
92
|
+
from shared.env import get_env
|
|
93
|
+
|
|
94
|
+
api_key = get_env("NCBI_API_KEY", "")
|
|
95
|
+
base = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
|
|
96
|
+
|
|
97
|
+
name = arguments["name"]
|
|
98
|
+
pub_type_filter = (
|
|
99
|
+
"(Practice Guideline[pt] OR Guideline[pt] "
|
|
100
|
+
"OR Consensus Development Conference[pt] "
|
|
101
|
+
"OR Consensus Development Conference, NIH[pt])"
|
|
102
|
+
)
|
|
103
|
+
parts = [f"{name}[Author]", pub_type_filter]
|
|
104
|
+
|
|
105
|
+
if arguments.get("therapeutic_area"):
|
|
106
|
+
parts.append(arguments["therapeutic_area"])
|
|
107
|
+
if arguments.get("specialty"):
|
|
108
|
+
parts.append(arguments["specialty"])
|
|
109
|
+
|
|
110
|
+
query = " AND ".join(parts)
|
|
111
|
+
max_results = arguments.get("max_results", 20)
|
|
112
|
+
|
|
113
|
+
search_params = {
|
|
114
|
+
"db": "pubmed",
|
|
115
|
+
"term": query,
|
|
116
|
+
"retmax": str(max_results),
|
|
117
|
+
"retmode": "json",
|
|
118
|
+
"sort": "date",
|
|
119
|
+
}
|
|
120
|
+
if api_key:
|
|
121
|
+
search_params["api_key"] = api_key
|
|
122
|
+
|
|
123
|
+
search_url = f"{base}/esearch.fcgi?{urllib.parse.urlencode(search_params)}"
|
|
124
|
+
with urllib.request.urlopen(search_url, timeout=30) as resp:
|
|
125
|
+
search_data = json.loads(resp.read())
|
|
126
|
+
|
|
127
|
+
result = search_data.get("esearchresult", {})
|
|
128
|
+
total_count = int(result.get("count", 0))
|
|
129
|
+
id_list = result.get("idlist", [])
|
|
130
|
+
|
|
131
|
+
publications: list[dict] = []
|
|
132
|
+
if id_list:
|
|
133
|
+
fetch_params = {
|
|
134
|
+
"db": "pubmed",
|
|
135
|
+
"id": ",".join(id_list),
|
|
136
|
+
"retmode": "xml",
|
|
137
|
+
"rettype": "abstract",
|
|
138
|
+
}
|
|
139
|
+
if api_key:
|
|
140
|
+
fetch_params["api_key"] = api_key
|
|
141
|
+
|
|
142
|
+
fetch_url = f"{base}/efetch.fcgi?{urllib.parse.urlencode(fetch_params)}"
|
|
143
|
+
with urllib.request.urlopen(fetch_url, timeout=60) as resp:
|
|
144
|
+
xml_data = resp.read()
|
|
145
|
+
|
|
146
|
+
publications = _parse_guideline_xml(xml_data)
|
|
147
|
+
|
|
148
|
+
return query, total_count, publications
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse_guideline_xml(xml_bytes: bytes) -> list[dict]:
|
|
152
|
+
"""Parse PubMed efetch XML into guideline publication records."""
|
|
153
|
+
import re
|
|
154
|
+
import xml.etree.ElementTree as ET
|
|
155
|
+
|
|
156
|
+
root = ET.fromstring(xml_bytes)
|
|
157
|
+
pubs = []
|
|
158
|
+
|
|
159
|
+
for article in root.findall(".//PubmedArticle"):
|
|
160
|
+
medline = article.find("MedlineCitation")
|
|
161
|
+
if medline is None:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
pmid_el = medline.find("PMID")
|
|
165
|
+
pmid = pmid_el.text if pmid_el is not None else None
|
|
166
|
+
|
|
167
|
+
art = medline.find("Article")
|
|
168
|
+
if art is None:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
title_el = art.find("ArticleTitle")
|
|
172
|
+
title = "".join(title_el.itertext()) if title_el is not None else ""
|
|
173
|
+
|
|
174
|
+
journal_el = art.find("Journal/Title")
|
|
175
|
+
journal = journal_el.text if journal_el is not None else ""
|
|
176
|
+
|
|
177
|
+
year = None
|
|
178
|
+
for date_path in [
|
|
179
|
+
"Journal/JournalIssue/PubDate/Year",
|
|
180
|
+
"ArticleDate/Year",
|
|
181
|
+
]:
|
|
182
|
+
y_el = art.find(date_path)
|
|
183
|
+
if y_el is not None and y_el.text:
|
|
184
|
+
year = int(y_el.text)
|
|
185
|
+
break
|
|
186
|
+
if year is None:
|
|
187
|
+
medline_date = art.find("Journal/JournalIssue/PubDate/MedlineDate")
|
|
188
|
+
if medline_date is not None and medline_date.text:
|
|
189
|
+
m = re.search(r"(\d{4})", medline_date.text)
|
|
190
|
+
if m:
|
|
191
|
+
year = int(m.group(1))
|
|
192
|
+
|
|
193
|
+
authors = []
|
|
194
|
+
for au in art.findall("AuthorList/Author"):
|
|
195
|
+
last = au.find("LastName")
|
|
196
|
+
fore = au.find("ForeName")
|
|
197
|
+
if last is not None:
|
|
198
|
+
n = last.text or ""
|
|
199
|
+
if fore is not None and fore.text:
|
|
200
|
+
n += f" {fore.text}"
|
|
201
|
+
authors.append(n)
|
|
202
|
+
|
|
203
|
+
pub_types = []
|
|
204
|
+
for pt in art.findall("PublicationTypeList/PublicationType"):
|
|
205
|
+
if pt.text:
|
|
206
|
+
pub_types.append(pt.text)
|
|
207
|
+
|
|
208
|
+
pubs.append(
|
|
209
|
+
{
|
|
210
|
+
"pmid": pmid,
|
|
211
|
+
"title": title,
|
|
212
|
+
"authors": authors,
|
|
213
|
+
"journal": journal,
|
|
214
|
+
"year": year,
|
|
215
|
+
"publication_types": pub_types,
|
|
216
|
+
"source_url": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/" if pmid else "",
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return pubs
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _search_regulatory_roles(arguments: dict[str, Any]) -> tuple[str, list[dict]]:
|
|
224
|
+
"""Web-search for FDA/EMA/WHO advisory committee membership."""
|
|
225
|
+
from .search_hcp_web import _web_search
|
|
226
|
+
|
|
227
|
+
name = arguments["name"]
|
|
228
|
+
max_results = arguments.get("max_results", 20)
|
|
229
|
+
|
|
230
|
+
query = (
|
|
231
|
+
f'"{name}" '
|
|
232
|
+
"(FDA advisory committee OR EMA CHMP OR EMA SAWP OR EMA PDCO "
|
|
233
|
+
"OR WHO expert advisory OR advisory board roster "
|
|
234
|
+
"OR guideline committee member)"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
results = _web_search(query, max_results)
|
|
238
|
+
return query, results
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""search_hco_web — web search tailored for HCO profiling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchHcoWebArgs(BaseModel):
|
|
11
|
+
name: str = Field(description="Name of the healthcare organization")
|
|
12
|
+
country: str | None = Field(default=None, description="Country")
|
|
13
|
+
organization_type: str | None = Field(
|
|
14
|
+
default=None,
|
|
15
|
+
description="Type: hospital, clinic, research institute, medical school, etc.",
|
|
16
|
+
)
|
|
17
|
+
query_focus: str | None = Field(
|
|
18
|
+
default=None,
|
|
19
|
+
description=(
|
|
20
|
+
"Optional focus to append to the search query, e.g. "
|
|
21
|
+
"'departments centres of excellence', 'bed capacity annual report', "
|
|
22
|
+
"'accreditation ranking', 'history founded'"
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
max_results: int = Field(
|
|
26
|
+
default=10,
|
|
27
|
+
ge=1,
|
|
28
|
+
le=20,
|
|
29
|
+
description="Maximum web results to return",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TOOL: dict[str, Any] = {
|
|
34
|
+
"name": "search_hco_web",
|
|
35
|
+
"description": (
|
|
36
|
+
"Search the web for information about a Healthcare Organization (hospital, "
|
|
37
|
+
"clinic, research centre, medical school). Constructs a targeted query to find "
|
|
38
|
+
"the organization's official site, Wikipedia entry, accreditation status, "
|
|
39
|
+
"departments, bed capacity, and institutional history. Use query_focus to "
|
|
40
|
+
"steer toward specific profile sections."
|
|
41
|
+
),
|
|
42
|
+
"args": SearchHcoWebArgs,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
47
|
+
import json
|
|
48
|
+
from datetime import datetime, timezone
|
|
49
|
+
|
|
50
|
+
query_parts = [f'"{arguments["name"]}"']
|
|
51
|
+
if arguments.get("country"):
|
|
52
|
+
query_parts.append(arguments["country"])
|
|
53
|
+
if arguments.get("organization_type"):
|
|
54
|
+
query_parts.append(arguments["organization_type"])
|
|
55
|
+
if arguments.get("query_focus"):
|
|
56
|
+
query_parts.append(arguments["query_focus"])
|
|
57
|
+
else:
|
|
58
|
+
query_parts.append("hospital OR medical centre OR healthcare")
|
|
59
|
+
|
|
60
|
+
query = " ".join(query_parts)
|
|
61
|
+
max_results = arguments.get("max_results", 10)
|
|
62
|
+
|
|
63
|
+
from .search_hcp_web import _backend_name, _web_search
|
|
64
|
+
|
|
65
|
+
results = _web_search(query, max_results)
|
|
66
|
+
|
|
67
|
+
output = {
|
|
68
|
+
"query": query,
|
|
69
|
+
"results": results,
|
|
70
|
+
"search_backend": _backend_name(),
|
|
71
|
+
"searched_at": datetime.now(timezone.utc).isoformat(),
|
|
72
|
+
}
|
|
73
|
+
return [{"type": "text", "text": json.dumps(output, indent=2)}]
|