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.
Files changed (136) hide show
  1. mcp_framework.py +495 -0
  2. open_pharma_plugins-2.2.0.dist-info/METADATA +135 -0
  3. open_pharma_plugins-2.2.0.dist-info/RECORD +136 -0
  4. open_pharma_plugins-2.2.0.dist-info/WHEEL +5 -0
  5. open_pharma_plugins-2.2.0.dist-info/entry_points.txt +8 -0
  6. open_pharma_plugins-2.2.0.dist-info/licenses/LICENSE +202 -0
  7. open_pharma_plugins-2.2.0.dist-info/top_level.txt +8 -0
  8. open_pharma_plugins_campaign_studio/__init__.py +14 -0
  9. open_pharma_plugins_campaign_studio/__main__.py +11 -0
  10. open_pharma_plugins_campaign_studio/_campaign_store.py +162 -0
  11. open_pharma_plugins_campaign_studio/_claim_engine.py +262 -0
  12. open_pharma_plugins_campaign_studio/_renderer.py +119 -0
  13. open_pharma_plugins_campaign_studio/fixtures/brand_kit/legal.json +21 -0
  14. open_pharma_plugins_campaign_studio/fixtures/brand_kit/logo.svg +4 -0
  15. open_pharma_plugins_campaign_studio/fixtures/brand_kit/palette.json +11 -0
  16. open_pharma_plugins_campaign_studio/fixtures/brand_kit/product.png +1 -0
  17. open_pharma_plugins_campaign_studio/fixtures/brand_kit/typography.json +14 -0
  18. open_pharma_plugins_campaign_studio/fixtures/sample_approved_claims.json +119 -0
  19. open_pharma_plugins_campaign_studio/models/__init__.py +34 -0
  20. open_pharma_plugins_campaign_studio/models/_common.py +12 -0
  21. open_pharma_plugins_campaign_studio/models/brief.py +72 -0
  22. open_pharma_plugins_campaign_studio/models/claims.py +15 -0
  23. open_pharma_plugins_campaign_studio/models/copy.py +47 -0
  24. open_pharma_plugins_campaign_studio/models/journey.py +21 -0
  25. open_pharma_plugins_campaign_studio/models/message.py +25 -0
  26. open_pharma_plugins_campaign_studio/models/mlr.py +29 -0
  27. open_pharma_plugins_campaign_studio/models/validation.py +32 -0
  28. open_pharma_plugins_campaign_studio/policy/rules.json +79 -0
  29. open_pharma_plugins_campaign_studio/templates/banner.svg.j2 +26 -0
  30. open_pharma_plugins_campaign_studio/templates/email.html.j2 +54 -0
  31. open_pharma_plugins_campaign_studio/tools/__init__.py +0 -0
  32. open_pharma_plugins_campaign_studio/tools/create_campaign_brief.py +212 -0
  33. open_pharma_plugins_campaign_studio/tools/generate_audience_journey.py +129 -0
  34. open_pharma_plugins_campaign_studio/tools/generate_channel_copy.py +199 -0
  35. open_pharma_plugins_campaign_studio/tools/generate_message_architecture.py +121 -0
  36. open_pharma_plugins_campaign_studio/tools/package_mlr_submission.py +230 -0
  37. open_pharma_plugins_campaign_studio/tools/render_banner.py +99 -0
  38. open_pharma_plugins_campaign_studio/tools/render_email.py +101 -0
  39. open_pharma_plugins_campaign_studio/tools/render_poster.py +222 -0
  40. open_pharma_plugins_campaign_studio/tools/retrieve_approved_claims.py +71 -0
  41. open_pharma_plugins_campaign_studio/tools/retrieve_brand_components.py +76 -0
  42. open_pharma_plugins_campaign_studio/tools/validate_claims_and_fair_balance.py +285 -0
  43. open_pharma_plugins_competitive_intelligence/__init__.py +13 -0
  44. open_pharma_plugins_competitive_intelligence/__main__.py +11 -0
  45. open_pharma_plugins_competitive_intelligence/_artifacts.py +87 -0
  46. open_pharma_plugins_competitive_intelligence/_cache.py +144 -0
  47. open_pharma_plugins_competitive_intelligence/_clinical_trials.py +569 -0
  48. open_pharma_plugins_competitive_intelligence/_dailymed.py +260 -0
  49. open_pharma_plugins_competitive_intelligence/_fda.py +255 -0
  50. open_pharma_plugins_competitive_intelligence/_pubmed.py +342 -0
  51. open_pharma_plugins_competitive_intelligence/_regulatory.py +140 -0
  52. open_pharma_plugins_competitive_intelligence/_runs.py +278 -0
  53. open_pharma_plugins_competitive_intelligence/_transport.py +83 -0
  54. open_pharma_plugins_competitive_intelligence/_watchlist.py +113 -0
  55. open_pharma_plugins_competitive_intelligence/_web_search.py +331 -0
  56. open_pharma_plugins_competitive_intelligence/models.py +525 -0
  57. open_pharma_plugins_competitive_intelligence/tools/__init__.py +0 -0
  58. open_pharma_plugins_competitive_intelligence/tools/ci_extract_events.py +247 -0
  59. open_pharma_plugins_competitive_intelligence/tools/ci_landscape.py +195 -0
  60. open_pharma_plugins_competitive_intelligence/tools/ci_refresh.py +101 -0
  61. open_pharma_plugins_competitive_intelligence/tools/ci_report.py +402 -0
  62. open_pharma_plugins_competitive_intelligence/tools/ci_scan_news.py +48 -0
  63. open_pharma_plugins_competitive_intelligence/tools/ci_scan_publications.py +46 -0
  64. open_pharma_plugins_competitive_intelligence/tools/ci_scan_regulatory.py +64 -0
  65. open_pharma_plugins_competitive_intelligence/tools/ci_scan_trials.py +85 -0
  66. open_pharma_plugins_competitive_intelligence/tools/ci_status.py +106 -0
  67. open_pharma_plugins_competitive_intelligence/tools/ci_timeline.py +453 -0
  68. open_pharma_plugins_competitive_intelligence/tools/ci_track.py +121 -0
  69. open_pharma_plugins_competitive_intelligence/tools/ci_trial_detail.py +55 -0
  70. open_pharma_plugins_field_training/__init__.py +13 -0
  71. open_pharma_plugins_field_training/__main__.py +11 -0
  72. open_pharma_plugins_field_training/_content_store.py +131 -0
  73. open_pharma_plugins_field_training/_grounding.py +75 -0
  74. open_pharma_plugins_field_training/_html_renderers.py +546 -0
  75. open_pharma_plugins_field_training/fixtures/sample_product_message.pdf +156 -0
  76. open_pharma_plugins_field_training/fixtures/sample_training_deck.pptx +0 -0
  77. open_pharma_plugins_field_training/models.py +265 -0
  78. open_pharma_plugins_field_training/tools/__init__.py +0 -0
  79. open_pharma_plugins_field_training/tools/get_document_page.py +67 -0
  80. open_pharma_plugins_field_training/tools/ingest_document.py +147 -0
  81. open_pharma_plugins_field_training/tools/list_documents.py +51 -0
  82. open_pharma_plugins_field_training/tools/render_output.py +118 -0
  83. open_pharma_plugins_field_training/tools/search_content.py +57 -0
  84. open_pharma_plugins_hcp_intelligence/__init__.py +14 -0
  85. open_pharma_plugins_hcp_intelligence/__main__.py +11 -0
  86. open_pharma_plugins_hcp_intelligence/_crm_store.py +70 -0
  87. open_pharma_plugins_hcp_intelligence/batch.py +891 -0
  88. open_pharma_plugins_hcp_intelligence/batch_cli.py +221 -0
  89. open_pharma_plugins_hcp_intelligence/batch_csv.py +206 -0
  90. open_pharma_plugins_hcp_intelligence/fixtures/sample_accounts.csv +27 -0
  91. open_pharma_plugins_hcp_intelligence/models.py +356 -0
  92. open_pharma_plugins_hcp_intelligence/tools/__init__.py +0 -0
  93. open_pharma_plugins_hcp_intelligence/tools/get_account.py +48 -0
  94. open_pharma_plugins_hcp_intelligence/tools/list_accounts.py +66 -0
  95. open_pharma_plugins_hcp_intelligence/tools/search_clinical_trials.py +175 -0
  96. open_pharma_plugins_hcp_intelligence/tools/search_congresses.py +134 -0
  97. open_pharma_plugins_hcp_intelligence/tools/search_grants.py +181 -0
  98. open_pharma_plugins_hcp_intelligence/tools/search_guidelines.py +238 -0
  99. open_pharma_plugins_hcp_intelligence/tools/search_hco_web.py +73 -0
  100. open_pharma_plugins_hcp_intelligence/tools/search_hcp_web.py +199 -0
  101. open_pharma_plugins_hcp_intelligence/tools/search_orcid.py +214 -0
  102. open_pharma_plugins_hcp_intelligence/tools/search_publications.py +207 -0
  103. open_pharma_plugins_hcp_intelligence/tools/update_account.py +84 -0
  104. open_pharma_plugins_next_best_engagement/__init__.py +14 -0
  105. open_pharma_plugins_next_best_engagement/__main__.py +11 -0
  106. open_pharma_plugins_next_best_engagement/_optimizer.py +400 -0
  107. open_pharma_plugins_next_best_engagement/_renderer.py +149 -0
  108. open_pharma_plugins_next_best_engagement/_scoring.py +82 -0
  109. open_pharma_plugins_next_best_engagement/_universe.py +145 -0
  110. open_pharma_plugins_next_best_engagement/fixtures/sample_universe.csv +81 -0
  111. open_pharma_plugins_next_best_engagement/models.py +135 -0
  112. open_pharma_plugins_next_best_engagement/tools/__init__.py +0 -0
  113. open_pharma_plugins_next_best_engagement/tools/load_universe.py +47 -0
  114. open_pharma_plugins_next_best_engagement/tools/recommend_engagements.py +80 -0
  115. open_pharma_plugins_next_best_engagement/tools/render_plan.py +90 -0
  116. open_pharma_plugins_territory_alignment/__init__.py +14 -0
  117. open_pharma_plugins_territory_alignment/__main__.py +11 -0
  118. open_pharma_plugins_territory_alignment/data.py +300 -0
  119. open_pharma_plugins_territory_alignment/fixtures/constraints.csv +11 -0
  120. open_pharma_plugins_territory_alignment/fixtures/current_alignment.csv +81 -0
  121. open_pharma_plugins_territory_alignment/fixtures/hcps.csv +81 -0
  122. open_pharma_plugins_territory_alignment/fixtures/reps.csv +9 -0
  123. open_pharma_plugins_territory_alignment/geo.py +175 -0
  124. open_pharma_plugins_territory_alignment/models.py +201 -0
  125. open_pharma_plugins_territory_alignment/scoring.py +125 -0
  126. open_pharma_plugins_territory_alignment/solver.py +504 -0
  127. open_pharma_plugins_territory_alignment/tools/__init__.py +0 -0
  128. open_pharma_plugins_territory_alignment/tools/ta_align.py +138 -0
  129. open_pharma_plugins_territory_alignment/tools/ta_cluster.py +247 -0
  130. open_pharma_plugins_territory_alignment/tools/ta_compare.py +173 -0
  131. open_pharma_plugins_territory_alignment/tools/ta_evaluate.py +119 -0
  132. open_pharma_plugins_territory_alignment/tools/ta_status.py +34 -0
  133. open_pharma_plugins_territory_alignment/tools/ta_visualize.py +504 -0
  134. shared/__init__.py +11 -0
  135. shared/env.py +217 -0
  136. shared/filesystem.py +110 -0
@@ -0,0 +1,55 @@
1
+ """ci_trial_detail — full trial record with arms, endpoints, and results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from .. import _clinical_trials
11
+ from ..models import TrialDetailRequest
12
+
13
+
14
+ class TrialDetailArgs(BaseModel):
15
+ nct_id: str = Field(description="ClinicalTrials.gov identifier, e.g. 'NCT04567890'")
16
+
17
+
18
+ TOOL: dict[str, Any] = {
19
+ "name": "ci_trial_detail",
20
+ "description": (
21
+ "Fetch the full record for a single clinical trial including arms, "
22
+ "endpoints, eligibility criteria, current status, milestone dates, and "
23
+ "posted results. Use after ci_scan_trials to drill into a specific trial."
24
+ ),
25
+ "args": TrialDetailArgs,
26
+ }
27
+
28
+
29
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
30
+ request = TrialDetailRequest.model_validate(arguments)
31
+ result = _clinical_trials.get_trial_detail(request)
32
+ ledger = result.model_dump(mode="json", exclude={"records"})
33
+ if not result.records:
34
+ payload: dict[str, Any] = {
35
+ "error": result.error.message if result.error else "ClinicalTrials.gov detail failed",
36
+ "coverage": result.status.value,
37
+ "source_ledger": [ledger],
38
+ "limitations": result.limitations,
39
+ }
40
+ return [{"type": "text", "text": json.dumps(payload, indent=2)}]
41
+
42
+ detail = dict(result.records[0])
43
+ trial = dict(detail["trial"])
44
+ interventions = trial.get("interventions", [])
45
+ trial["intervention_details"] = interventions
46
+ trial["interventions"] = [item.get("name", "") for item in interventions]
47
+ detail["trial"] = trial
48
+ detail.update(
49
+ {
50
+ "coverage": result.status.value,
51
+ "source_ledger": [ledger],
52
+ "limitations": result.limitations,
53
+ }
54
+ )
55
+ return [{"type": "text", "text": json.dumps(detail, indent=2)}]
@@ -0,0 +1,13 @@
1
+ from mcp_framework import build_registry
2
+
3
+ __version__ = "1.1.1"
4
+
5
+ SPECS, get_handler, list_tools = build_registry(__name__, ["tools"])
6
+
7
+ USAGE_NOTE = (
8
+ "Field Training turns approved PDF/PPTX file paths into source-grounded "
9
+ "learning packages, assessments, pre-session role-play kits, and "
10
+ "post-session scorecards for field reps."
11
+ )
12
+
13
+ SYSTEM_DEPS = []
@@ -0,0 +1,11 @@
1
+ from pathlib import Path
2
+
3
+ from mcp_framework import run_main
4
+
5
+
6
+ def main() -> None:
7
+ run_main(__package__ or Path(__file__).resolve().parent.name)
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,131 @@
1
+ """Content store for ingested training documents.
2
+
3
+ Stores extracted document content as JSON files in a configurable directory.
4
+ A manifest (_index.json) tracks all ingested documents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ from shared.filesystem import atomic_write_json, contained_path, ensure_private_dir
15
+
16
+
17
+ def _store_dir() -> Path:
18
+ from shared.env import get_env
19
+
20
+ d = Path(
21
+ get_env(
22
+ "OPEN_PHARMA_TRAINING_CONTENT_DIR",
23
+ str(Path.home() / ".open-pharma-plugins" / "training-content"),
24
+ )
25
+ )
26
+ return ensure_private_dir(d)
27
+
28
+
29
+ def document_id_from_path(file_path: str) -> str:
30
+ """Generate a stable document ID from the file path."""
31
+ name = Path(file_path).stem
32
+ short_hash = hashlib.sha256(file_path.encode()).hexdigest()[:8]
33
+ safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)[:40]
34
+ return f"{safe_name}_{short_hash}"
35
+
36
+
37
+ def save_document(doc: dict) -> None:
38
+ """Save an ingested document to the content store."""
39
+ store = _store_dir()
40
+ doc_path = contained_path(store, f"{doc['document_id']}.json")
41
+ atomic_write_json(doc_path, doc)
42
+ _update_index(doc)
43
+
44
+
45
+ def load_document(document_id: str) -> dict | None:
46
+ """Load a document by ID."""
47
+ doc_path = contained_path(_store_dir(), f"{document_id}.json")
48
+ if doc_path.exists():
49
+ return json.loads(doc_path.read_text())
50
+ return None
51
+
52
+
53
+ def list_documents(file_type: str | None = None) -> list[dict]:
54
+ """List all ingested documents from the index."""
55
+ index = _load_index()
56
+ docs = index.get("documents", [])
57
+ if file_type:
58
+ docs = [d for d in docs if d.get("file_type") == file_type.lower()]
59
+ return docs
60
+
61
+
62
+ def search_pages(query: str, document_id: str | None = None, max_results: int = 10) -> list[dict]:
63
+ """Simple keyword search across all ingested documents.
64
+
65
+ Returns ranked results with document info and page content.
66
+ Uses term frequency scoring.
67
+ """
68
+ query_terms = set(query.lower().split())
69
+ if not query_terms:
70
+ return []
71
+
72
+ results = []
73
+
74
+ doc_ids = [document_id] if document_id else [d["document_id"] for d in list_documents()]
75
+
76
+ for doc_id in doc_ids:
77
+ doc = load_document(doc_id)
78
+ if doc is None:
79
+ continue
80
+ for page in doc.get("pages", []):
81
+ text = page.get("text", "")
82
+ text_lower = text.lower()
83
+
84
+ notes = page.get("speaker_notes") or ""
85
+ combined = text_lower + " " + notes.lower()
86
+
87
+ score = sum(combined.count(term) for term in query_terms)
88
+ if score > 0:
89
+ if query.lower() in combined:
90
+ score += 10
91
+ results.append(
92
+ {
93
+ "document_id": doc_id,
94
+ "document_name": doc.get("file_name", ""),
95
+ "page_number": page["page_number"],
96
+ "page_type": page.get("page_type", "page"),
97
+ "slide_title": page.get("slide_title"),
98
+ "text": text[:500],
99
+ "speaker_notes": notes[:300] if notes else None,
100
+ "score": score,
101
+ }
102
+ )
103
+
104
+ results.sort(key=lambda r: r["score"], reverse=True)
105
+ return results[:max_results]
106
+
107
+
108
+ def _load_index() -> dict:
109
+ index_path = contained_path(_store_dir(), "_index.json")
110
+ if index_path.exists():
111
+ return json.loads(index_path.read_text())
112
+ return {"documents": []}
113
+
114
+
115
+ def _update_index(doc: dict) -> None:
116
+ index = _load_index()
117
+ docs = index.get("documents", [])
118
+ docs = [d for d in docs if d.get("document_id") != doc["document_id"]]
119
+ docs.append(
120
+ {
121
+ "document_id": doc["document_id"],
122
+ "file_name": doc.get("file_name", ""),
123
+ "file_type": doc.get("file_type", ""),
124
+ "title": doc.get("title"),
125
+ "total_pages": doc.get("total_pages", 0),
126
+ "ingested_at": doc.get("ingested_at", datetime.now(timezone.utc).isoformat()),
127
+ }
128
+ )
129
+ index["documents"] = docs
130
+ index_path = contained_path(_store_dir(), "_index.json")
131
+ atomic_write_json(index_path, index)
@@ -0,0 +1,75 @@
1
+ """Validate training-output citations against the ingested content store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from ._content_store import list_documents, load_document
10
+ from .models import SourceReference
11
+
12
+
13
+ def validate_output_sources(output: BaseModel) -> list[str]:
14
+ errors: list[str] = []
15
+ declared_names = set(getattr(output, "source_documents", []))
16
+ known_documents = {item.get("file_name", ""): item for item in list_documents()}
17
+
18
+ for name in sorted(declared_names):
19
+ if name not in known_documents:
20
+ errors.append(f"source_documents contains unknown ingested document {name!r}")
21
+
22
+ for reference in _source_references(output):
23
+ if reference.document_name not in declared_names:
24
+ errors.append(
25
+ f"source {reference.document_id!r} names {reference.document_name!r} "
26
+ "but that file is absent from source_documents"
27
+ )
28
+ try:
29
+ document = load_document(reference.document_id)
30
+ except ValueError as exc:
31
+ errors.append(str(exc))
32
+ continue
33
+ if document is None:
34
+ errors.append(f"source document_id {reference.document_id!r} is not ingested")
35
+ continue
36
+ if document.get("file_name") != reference.document_name:
37
+ errors.append(
38
+ f"source {reference.document_id!r} document_name does not match ingested file "
39
+ f"{document.get('file_name')!r}"
40
+ )
41
+ page = next(
42
+ (item for item in document.get("pages", []) if item.get("page_number") == reference.page_number),
43
+ None,
44
+ )
45
+ if page is None:
46
+ errors.append(f"source {reference.document_id!r} has no page or slide {reference.page_number}")
47
+ continue
48
+ excerpt = _normalize(reference.excerpt)
49
+ source_text = _normalize(f"{page.get('text', '')} {page.get('speaker_notes') or ''}")
50
+ if not excerpt or excerpt not in source_text:
51
+ errors.append(
52
+ f"source {reference.document_id!r} page {reference.page_number} excerpt is not present in ingested content"
53
+ )
54
+ return errors
55
+
56
+
57
+ def _source_references(value: object) -> Iterator[SourceReference]:
58
+ if isinstance(value, SourceReference):
59
+ yield value
60
+ return
61
+ if isinstance(value, BaseModel):
62
+ for field in type(value).model_fields:
63
+ yield from _source_references(getattr(value, field))
64
+ return
65
+ if isinstance(value, dict):
66
+ for item in value.values():
67
+ yield from _source_references(item)
68
+ return
69
+ if isinstance(value, (list, tuple, set)):
70
+ for item in value:
71
+ yield from _source_references(item)
72
+
73
+
74
+ def _normalize(text: str) -> str:
75
+ return " ".join(str(text).split()).casefold()