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,201 @@
1
+ """Pydantic domain models for territory alignment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Input models
11
+ # ---------------------------------------------------------------------------
12
+
13
+
14
+ class HCP(BaseModel):
15
+ """An HCP loaded from hcps.csv. Extra columns are preserved."""
16
+
17
+ model_config = ConfigDict(extra="allow")
18
+
19
+ hcp_id: str
20
+ name: str
21
+ specialty: str = ""
22
+ segment: str = Field(default="medium", description="high | medium | low")
23
+ tier: int = Field(default=2, ge=1, le=3)
24
+ lat: float | None = None
25
+ lng: float | None = None
26
+ account_id: str = ""
27
+ consent_email: bool = True
28
+ consent_phone: bool = True
29
+ consent_visit: bool = True
30
+ annual_potential: float = 0.0
31
+ product_requirements: list[str] = Field(default_factory=list)
32
+
33
+
34
+ class Rep(BaseModel):
35
+ rep_id: str
36
+ name: str
37
+ base_lat: float = 0.0
38
+ base_lng: float = 0.0
39
+ product_expertise: list[str] = Field(default_factory=list)
40
+ max_weekly_hours: float = 40.0
41
+ max_daily_calls: int = 8
42
+ available_days: list[str] = Field(default_factory=lambda: ["mon", "tue", "wed", "thu", "fri"])
43
+
44
+
45
+ class CurrentAssignment(BaseModel):
46
+ hcp_id: str
47
+ primary_rep: str
48
+ secondary_rep: str = ""
49
+
50
+
51
+ class Constraint(BaseModel):
52
+ type: str
53
+ scope: str
54
+ value: Any
55
+ description: str = ""
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Objective configuration
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ class ObjectiveWeights(BaseModel):
64
+ workload_balance: float = Field(default=0.30, ge=0.0, le=1.0)
65
+ travel_efficiency: float = Field(default=0.25, ge=0.0, le=1.0)
66
+ disruption: float = Field(default=0.25, ge=0.0, le=1.0)
67
+ coverage: float = Field(default=0.20, ge=0.0, le=1.0)
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Scenario levers (ta_align input)
72
+ # ---------------------------------------------------------------------------
73
+
74
+
75
+ class Override(BaseModel):
76
+ hcp_id: str = Field(description="HCP to pin to a specific rep")
77
+ rep_id: str = Field(description="Rep to assign")
78
+ reason: str = ""
79
+
80
+
81
+ class NewHire(BaseModel):
82
+ rep_id: str
83
+ name: str
84
+ base_lat: float
85
+ base_lng: float
86
+ product_expertise: list[str] = Field(default_factory=list)
87
+ max_weekly_hours: float = 40.0
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Output models
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ class AssignmentResult(BaseModel):
96
+ """A single HCP assignment in the output plan."""
97
+
98
+ model_config = ConfigDict(extra="allow")
99
+
100
+ hcp_id: str
101
+ hcp_name: str
102
+ primary_rep: str
103
+ previous_rep: str
104
+ is_changed: bool
105
+ change_reason: str = ""
106
+ estimated_travel_min: float = 0.0
107
+ estimated_annual_visits: int = 0
108
+ segment: str = ""
109
+ tier: int = 0
110
+
111
+
112
+ class TerritorySummary(BaseModel):
113
+ rep_id: str
114
+ rep_name: str
115
+ hcp_count: int
116
+ total_potential: float
117
+ workload_hours_weekly: float
118
+ travel_hours_weekly: float
119
+ segment_high: int = 0
120
+ segment_medium: int = 0
121
+ segment_low: int = 0
122
+ relationships_kept: int = 0
123
+ relationships_new: int = 0
124
+
125
+
126
+ class RawMetrics(BaseModel):
127
+ workload_gini: float
128
+ avg_travel_min: float
129
+ max_travel_min: float
130
+ pct_reassigned: float
131
+ pct_priority_covered: float
132
+
133
+
134
+ class ObjectiveScores(BaseModel):
135
+ workload_balance: float
136
+ travel_efficiency: float
137
+ disruption: float
138
+ coverage: float
139
+ composite: float
140
+ raw: RawMetrics
141
+
142
+
143
+ class UnassignedHCP(BaseModel):
144
+ hcp_id: str
145
+ reason: str
146
+
147
+
148
+ class ScenarioResult(BaseModel):
149
+ scenario_name: str
150
+ assignments: list[AssignmentResult]
151
+ territory_summary: list[TerritorySummary]
152
+ objectives: ObjectiveScores
153
+ unassigned: list[UnassignedHCP]
154
+ weights_used: ObjectiveWeights
155
+ metadata: dict[str, Any] = Field(default_factory=dict)
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Operational mode output
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ class VisitCluster(BaseModel):
164
+ cluster_id: str
165
+ hcp_ids: list[str]
166
+ centroid_lat: float | None = None
167
+ centroid_lng: float | None = None
168
+ estimated_route_km: float = 0.0
169
+ estimated_travel_min: float = 0.0
170
+ hcp_count: int = 0
171
+ suggested_day: str = ""
172
+ travel_warning: str = ""
173
+
174
+
175
+ class VisitStop(BaseModel):
176
+ hcp_id: str
177
+ hcp_name: str
178
+ visit_order: int
179
+ cluster_id: str
180
+ lat: float | None = None
181
+ lng: float | None = None
182
+ travel_km_from_previous: float = 0.0
183
+
184
+
185
+ class RemoteAlternative(BaseModel):
186
+ hcp_id: str
187
+ hcp_name: str
188
+ distance_km: float
189
+ reason: str
190
+
191
+
192
+ class VisitPlan(BaseModel):
193
+ rep_id: str
194
+ rep_name: str
195
+ period: str
196
+ clusters: list[VisitCluster]
197
+ visit_sequence: list[VisitStop]
198
+ remote_alternatives: list[RemoteAlternative]
199
+ total_route_km: float
200
+ total_hcps: int
201
+ remote_count: int
@@ -0,0 +1,125 @@
1
+ """Multi-objective scoring and normalisation for territory alignment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .geo import haversine, travel_minutes
6
+ from .models import (
7
+ HCP,
8
+ AssignmentResult,
9
+ ObjectiveScores,
10
+ ObjectiveWeights,
11
+ RawMetrics,
12
+ Rep,
13
+ )
14
+
15
+
16
+ def score_alignment(
17
+ assignments: list[AssignmentResult],
18
+ hcps: list[HCP],
19
+ reps: list[Rep],
20
+ weights: ObjectiveWeights,
21
+ baseline: RawMetrics | None = None,
22
+ ) -> ObjectiveScores:
23
+ """Score an alignment across four normalised objectives."""
24
+ hcp_map = {h.hcp_id: h for h in hcps}
25
+ rep_map = {r.rep_id: r for r in reps}
26
+
27
+ raw = _compute_raw(assignments, hcp_map, rep_map)
28
+
29
+ if baseline:
30
+ wb = _normalise(raw.workload_gini, baseline.workload_gini)
31
+ te = _normalise(raw.avg_travel_min, baseline.avg_travel_min)
32
+ di = _normalise(raw.pct_reassigned, baseline.pct_reassigned)
33
+ co = _normalise(100.0 - raw.pct_priority_covered, 100.0 - baseline.pct_priority_covered)
34
+ else:
35
+ wb = min(raw.workload_gini, 1.0)
36
+ te = min(raw.avg_travel_min / 60.0, 1.0) if raw.avg_travel_min > 0 else 0.0
37
+ di = min(raw.pct_reassigned / 100.0, 1.0)
38
+ co = min((100.0 - raw.pct_priority_covered) / 100.0, 1.0)
39
+
40
+ composite = (
41
+ weights.workload_balance * wb + weights.travel_efficiency * te + weights.disruption * di + weights.coverage * co
42
+ )
43
+
44
+ return ObjectiveScores(
45
+ workload_balance=round(wb, 4),
46
+ travel_efficiency=round(te, 4),
47
+ disruption=round(di, 4),
48
+ coverage=round(co, 4),
49
+ composite=round(composite, 4),
50
+ raw=raw,
51
+ )
52
+
53
+
54
+ def _compute_raw(
55
+ assignments: list[AssignmentResult],
56
+ hcp_map: dict[str, HCP],
57
+ rep_map: dict[str, Rep],
58
+ ) -> RawMetrics:
59
+ rep_workloads: dict[str, float] = {r: 0.0 for r in rep_map}
60
+ rep_travel: dict[str, list[float]] = {r: [] for r in rep_map}
61
+ reassigned = 0
62
+ priority_total = 0
63
+ priority_covered = 0
64
+
65
+ for a in assignments:
66
+ hcp = hcp_map.get(a.hcp_id)
67
+ rep = rep_map.get(a.primary_rep)
68
+ if not hcp or not rep:
69
+ continue
70
+
71
+ hours_per_visit = 1.0 if hcp.segment == "high" else 0.75 if hcp.segment == "medium" else 0.5
72
+ visits_per_year = 24 if hcp.segment == "high" else 12 if hcp.segment == "medium" else 6
73
+ weekly_hours = (hours_per_visit * visits_per_year) / 48.0
74
+ rep_workloads[a.primary_rep] = rep_workloads.get(a.primary_rep, 0.0) + weekly_hours
75
+
76
+ if hcp.lat is not None and hcp.lng is not None:
77
+ dist = haversine(rep.base_lat, rep.base_lng, hcp.lat, hcp.lng)
78
+ rep_travel.setdefault(a.primary_rep, []).append(travel_minutes(dist))
79
+
80
+ if a.is_changed:
81
+ reassigned += 1
82
+
83
+ if hcp.segment == "high":
84
+ priority_total += 1
85
+ priority_covered += 1
86
+
87
+ all_hcps_high = sum(1 for h in hcp_map.values() if h.segment == "high")
88
+ if all_hcps_high > priority_total:
89
+ priority_total = all_hcps_high
90
+
91
+ workloads = [v for v in rep_workloads.values() if v > 0]
92
+ gini = _gini(workloads) if workloads else 0.0
93
+
94
+ all_travel = [t for ts in rep_travel.values() for t in ts]
95
+ avg_travel = sum(all_travel) / max(len(all_travel), 1)
96
+ max_travel = max(all_travel) if all_travel else 0.0
97
+
98
+ pct_reassigned = (reassigned / max(len(assignments), 1)) * 100.0
99
+ pct_covered = (priority_covered / max(priority_total, 1)) * 100.0
100
+
101
+ return RawMetrics(
102
+ workload_gini=round(gini, 4),
103
+ avg_travel_min=round(avg_travel, 1),
104
+ max_travel_min=round(max_travel, 1),
105
+ pct_reassigned=round(pct_reassigned, 1),
106
+ pct_priority_covered=round(pct_covered, 1),
107
+ )
108
+
109
+
110
+ def _gini(values: list[float]) -> float:
111
+ """Gini coefficient (0 = perfect equality, 1 = max inequality)."""
112
+ if not values or all(v == 0 for v in values):
113
+ return 0.0
114
+ n = len(values)
115
+ sorted_v = sorted(values)
116
+ total = sum(sorted_v)
117
+ weighted_sum = sum((i + 1) * v for i, v in enumerate(sorted_v))
118
+ return round((2.0 * weighted_sum) / (n * total) - (n + 1.0) / n, 4)
119
+
120
+
121
+ def _normalise(value: float, baseline: float) -> float:
122
+ """Normalise a metric relative to baseline. Returns 0-1 range."""
123
+ if baseline <= 0:
124
+ return 0.0 if value <= 0 else 1.0
125
+ return min(max(value / baseline, 0.0), 2.0) / 2.0