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,15 @@
|
|
|
1
|
+
"""Approved claim models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApprovedClaim(BaseModel):
|
|
9
|
+
claim_id: str = Field(description="Stable identifier, e.g. 'c-001'")
|
|
10
|
+
text: str = Field(description="Verbatim approved wording")
|
|
11
|
+
category: str = Field(description="efficacy | safety | tolerability | dosing | positioning | MOA")
|
|
12
|
+
source_document: str = Field(description="Origin document name")
|
|
13
|
+
source_reference: str = Field(description="Precise location, e.g. 'Study 301, Table 2' or 'SmPC Section 5.1'")
|
|
14
|
+
expiry: str | None = Field(default=None, description="Claim approval expiry, ISO 8601")
|
|
15
|
+
restrictions: str | None = Field(default=None, description="Usage restrictions, e.g. 'not for use in EU materials'")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Channel copy models — typed per channel for validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StrictCopyModel(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CopyBlock(StrictCopyModel):
|
|
13
|
+
text: str = Field(description="Copy text")
|
|
14
|
+
claim_ids: list[str] = Field(
|
|
15
|
+
default_factory=list,
|
|
16
|
+
description="Approved claim IDs that ground this text",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EmailCopy(StrictCopyModel):
|
|
21
|
+
subject: CopyBlock = Field(description="Email subject line")
|
|
22
|
+
preheader: CopyBlock = Field(description="Preview text")
|
|
23
|
+
headline: CopyBlock = Field(description="Hero headline")
|
|
24
|
+
body: list[CopyBlock] = Field(description="Body paragraphs")
|
|
25
|
+
cta: CopyBlock = Field(description="Call-to-action button text")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BannerCopy(StrictCopyModel):
|
|
29
|
+
headline: CopyBlock = Field(description="Banner headline, max 8 words")
|
|
30
|
+
sub_headline: CopyBlock | None = Field(default=None, description="Optional sub-headline")
|
|
31
|
+
cta: CopyBlock = Field(description="CTA text, max 3 words")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PosterCopy(StrictCopyModel):
|
|
35
|
+
headline: CopyBlock = Field(description="Poster headline")
|
|
36
|
+
subhead: CopyBlock | None = Field(default=None)
|
|
37
|
+
body: list[CopyBlock] = Field(description="Body paragraphs")
|
|
38
|
+
bullet_points: list[CopyBlock] | None = Field(default=None)
|
|
39
|
+
cta: CopyBlock = Field(description="Call-to-action text")
|
|
40
|
+
footnotes: list[str] | None = Field(default=None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ChannelCopy(StrictCopyModel):
|
|
44
|
+
campaign_brief_id: str = Field(description="Links to the campaign brief")
|
|
45
|
+
channel: str = Field(description="email | banner | poster")
|
|
46
|
+
copy_json: str = Field(description="JSON string of EmailCopy, BannerCopy, or PosterCopy")
|
|
47
|
+
generated_at: str = Field(description="ISO 8601 timestamp")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Audience journey models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JourneyStage(BaseModel):
|
|
9
|
+
stage: str = Field(description="unaware | aware | interested | convinced | acting | advocating")
|
|
10
|
+
objective: str = Field(description="What should happen at this stage")
|
|
11
|
+
key_messages: list[str] = Field(description="claim_id references")
|
|
12
|
+
channels: list[str] = Field(description="Which channels serve this stage")
|
|
13
|
+
content_type: str = Field(description="educational | promotional | reminder")
|
|
14
|
+
kpi: str = Field(description="Measurable outcome for this stage")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AudienceJourney(BaseModel):
|
|
18
|
+
campaign_brief_id: str = Field(description="Links to the campaign brief")
|
|
19
|
+
target_segment: str = Field(description="Target audience segment")
|
|
20
|
+
stages: list[JourneyStage] = Field(description="Journey stages in order")
|
|
21
|
+
generated_at: str = Field(description="ISO 8601 timestamp")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Message architecture models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from ._common import SourceReference
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MessageTier(BaseModel):
|
|
11
|
+
tier: str = Field(description="primary | secondary | supporting")
|
|
12
|
+
message: str = Field(description="The message statement")
|
|
13
|
+
claim_ids: list[str] = Field(description="Approved claim IDs this message draws from")
|
|
14
|
+
audience_stage: str | None = Field(default=None, description="Links to a journey stage")
|
|
15
|
+
rationale: str = Field(description="Why this message matters for the objective")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MessageArchitecture(BaseModel):
|
|
19
|
+
campaign_brief_id: str = Field(description="Links to the campaign brief")
|
|
20
|
+
brand: str = Field(description="Brand name")
|
|
21
|
+
indication: str = Field(description="Approved indication")
|
|
22
|
+
message_tiers: list[MessageTier] = Field(description="Tiered message hierarchy")
|
|
23
|
+
fair_balance_statement: str = Field(description="Required safety/risk language")
|
|
24
|
+
fair_balance_sources: list[SourceReference] = Field(description="Sources for the fair balance statement")
|
|
25
|
+
generated_at: str = Field(description="ISO 8601 timestamp")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""MLR review package model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from ._common import SourceReference
|
|
8
|
+
from .brief import CampaignBrief
|
|
9
|
+
from .validation import ClaimValidationReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RenderedAsset(BaseModel):
|
|
13
|
+
channel: str = Field(description="email | banner | poster")
|
|
14
|
+
file_path: str = Field(description="Path to the rendered file")
|
|
15
|
+
format: str = Field(description="html | svg | pdf")
|
|
16
|
+
editable: bool = Field(description="Whether the output is an editable source file")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MlrPackage(BaseModel):
|
|
20
|
+
campaign_brief_id: str = Field(description="Links to the campaign brief")
|
|
21
|
+
brief: CampaignBrief
|
|
22
|
+
rendered_assets: list[RenderedAsset] = Field(description="All rendered channel assets")
|
|
23
|
+
claim_validation: ClaimValidationReport
|
|
24
|
+
source_evidence: list[SourceReference] = Field(
|
|
25
|
+
description="Deduplicated list of every source cited across all assets"
|
|
26
|
+
)
|
|
27
|
+
mlr_summary: str = Field(description="Markdown summary for reviewer cover sheet")
|
|
28
|
+
reviewer_notes: str | None = Field(default=None)
|
|
29
|
+
generated_at: str = Field(description="ISO 8601 timestamp")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Claim validation and policy check models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClaimValidationResult(BaseModel):
|
|
9
|
+
claim_id: str | None = Field(default=None, description="Matched approved claim ID, if any")
|
|
10
|
+
statement: str = Field(description="The promotional statement as written in the copy")
|
|
11
|
+
status: str = Field(description="approved | needs_review | rejected | not_found")
|
|
12
|
+
matched_claim_text: str | None = Field(default=None, description="Verbatim approved claim it was matched to")
|
|
13
|
+
similarity_score: float | None = Field(default=None, description="0.0-1.0 similarity to nearest approved claim")
|
|
14
|
+
deviation: str | None = Field(
|
|
15
|
+
default=None,
|
|
16
|
+
description="How the copy deviates from approved language",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PolicyCheck(BaseModel):
|
|
21
|
+
check_name: str = Field(description="e.g. fair_balance, isi_present, pi_link, prohibited_language")
|
|
22
|
+
result: str = Field(description="pass | warn | fail")
|
|
23
|
+
detail: str = Field(description="Explanation of the check outcome")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClaimValidationReport(BaseModel):
|
|
27
|
+
campaign_brief_id: str = Field(description="Links to the campaign brief")
|
|
28
|
+
channels_validated: list[str] = Field(description="Which channels were validated")
|
|
29
|
+
claims_checked: list[ClaimValidationResult] = Field(description="Per-statement validation results")
|
|
30
|
+
policy_checks: list[PolicyCheck] = Field(description="Jurisdiction-specific policy checks")
|
|
31
|
+
overall_pass: bool = Field(description="True only if zero failures across all checks")
|
|
32
|
+
generated_at: str = Field(description="ISO 8601 timestamp")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"FDA": {
|
|
3
|
+
"required_elements": ["isi", "pi_ref", "reporting_statement"],
|
|
4
|
+
"fair_balance_required": true,
|
|
5
|
+
"prohibited_patterns": [
|
|
6
|
+
{"pattern": "\\bcure[sd]?\\b", "reason": "Absolute cure claims are prohibited"},
|
|
7
|
+
{"pattern": "\\bguarantee[sd]?\\b", "reason": "Guarantee claims are prohibited"},
|
|
8
|
+
{"pattern": "\\b100%\\s+(safe|effective)\\b", "reason": "Absolute safety/efficacy claims are prohibited"},
|
|
9
|
+
{"pattern": "\\bno\\s+side\\s+effects?\\b", "reason": "Claims of no side effects are prohibited"},
|
|
10
|
+
{"pattern": "\\brisk[- ]free\\b", "reason": "Risk-free claims are prohibited"},
|
|
11
|
+
{"pattern": "\\bmiracle\\b", "reason": "Miracle claims are prohibited"}
|
|
12
|
+
],
|
|
13
|
+
"non_promotional_prohibited": [
|
|
14
|
+
{"pattern": "\\b[A-Z][a-z]+rix\\b", "reason": "Brand names should not appear in headlines of non-promotional materials"}
|
|
15
|
+
],
|
|
16
|
+
"min_safety_ratio": 0.3,
|
|
17
|
+
"notes": "FDA requires fair balance: safety information must be presented with comparable prominence to efficacy claims."
|
|
18
|
+
},
|
|
19
|
+
"EMA": {
|
|
20
|
+
"required_elements": ["pi_ref", "reporting_statement"],
|
|
21
|
+
"fair_balance_required": true,
|
|
22
|
+
"prohibited_patterns": [
|
|
23
|
+
{"pattern": "\\bcure[sd]?\\b", "reason": "Absolute cure claims are prohibited"},
|
|
24
|
+
{"pattern": "\\bguarantee[sd]?\\b", "reason": "Guarantee claims are prohibited"},
|
|
25
|
+
{"pattern": "\\b100%\\s+(safe|effective)\\b", "reason": "Absolute safety/efficacy claims are prohibited"},
|
|
26
|
+
{"pattern": "\\bno\\s+side\\s+effects?\\b", "reason": "Claims of no side effects are prohibited"},
|
|
27
|
+
{"pattern": "\\bbest\\s+in\\s+class\\b", "reason": "Superlative claims require substantiation"}
|
|
28
|
+
],
|
|
29
|
+
"non_promotional_prohibited": [],
|
|
30
|
+
"min_safety_ratio": 0.3,
|
|
31
|
+
"notes": "EMA requires that promotional materials are consistent with the SmPC."
|
|
32
|
+
},
|
|
33
|
+
"MHRA": {
|
|
34
|
+
"required_elements": ["pi_ref", "reporting_statement"],
|
|
35
|
+
"fair_balance_required": true,
|
|
36
|
+
"prohibited_patterns": [
|
|
37
|
+
{"pattern": "\\bcure[sd]?\\b", "reason": "Absolute cure claims are prohibited"},
|
|
38
|
+
{"pattern": "\\bguarantee[sd]?\\b", "reason": "Guarantee claims are prohibited"},
|
|
39
|
+
{"pattern": "\\bno\\s+side\\s+effects?\\b", "reason": "Claims of no side effects are prohibited"}
|
|
40
|
+
],
|
|
41
|
+
"non_promotional_prohibited": [],
|
|
42
|
+
"min_safety_ratio": 0.25,
|
|
43
|
+
"notes": "MHRA ABPI Code requires fair balance and accurate representation of the evidence."
|
|
44
|
+
},
|
|
45
|
+
"HSA": {
|
|
46
|
+
"required_elements": ["pi_ref"],
|
|
47
|
+
"fair_balance_required": true,
|
|
48
|
+
"prohibited_patterns": [
|
|
49
|
+
{"pattern": "\\bcure[sd]?\\b", "reason": "Absolute cure claims are prohibited"},
|
|
50
|
+
{"pattern": "\\bguarantee[sd]?\\b", "reason": "Guarantee claims are prohibited"}
|
|
51
|
+
],
|
|
52
|
+
"non_promotional_prohibited": [],
|
|
53
|
+
"min_safety_ratio": 0.25,
|
|
54
|
+
"notes": "HSA Medicines Advertising Regulations require balanced presentation."
|
|
55
|
+
},
|
|
56
|
+
"PMDA": {
|
|
57
|
+
"required_elements": ["pi_ref"],
|
|
58
|
+
"fair_balance_required": true,
|
|
59
|
+
"prohibited_patterns": [
|
|
60
|
+
{"pattern": "\\bcure[sd]?\\b", "reason": "Absolute cure claims are prohibited"},
|
|
61
|
+
{"pattern": "\\bguarantee[sd]?\\b", "reason": "Guarantee claims are prohibited"}
|
|
62
|
+
],
|
|
63
|
+
"non_promotional_prohibited": [],
|
|
64
|
+
"min_safety_ratio": 0.25,
|
|
65
|
+
"notes": "PMDA requires fair balance per the Pharmaceutical Affairs Law."
|
|
66
|
+
},
|
|
67
|
+
"TGA": {
|
|
68
|
+
"required_elements": ["pi_ref", "reporting_statement"],
|
|
69
|
+
"fair_balance_required": true,
|
|
70
|
+
"prohibited_patterns": [
|
|
71
|
+
{"pattern": "\\bcure[sd]?\\b", "reason": "Absolute cure claims are prohibited"},
|
|
72
|
+
{"pattern": "\\bguarantee[sd]?\\b", "reason": "Guarantee claims are prohibited"},
|
|
73
|
+
{"pattern": "\\bno\\s+side\\s+effects?\\b", "reason": "Claims of no side effects are prohibited"}
|
|
74
|
+
],
|
|
75
|
+
"non_promotional_prohibited": [],
|
|
76
|
+
"min_safety_ratio": 0.25,
|
|
77
|
+
"notes": "TGA Therapeutic Goods Advertising Code requires fair balance."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
2
|
+
viewBox="0 0 {{ width }} {{ height }}" width="{{ width }}" height="{{ height }}">
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.bg { fill: {{ palette.primary | default('#003B5C') }}; }
|
|
6
|
+
.headline-text { fill: {{ palette.text_light | default('#FFFFFF') }}; font-family: {{ typography.heading_family | default('Arial, Helvetica, sans-serif') }}; font-weight: {{ typography.heading_weight | default('bold') }}; }
|
|
7
|
+
.sub-text { fill: {{ palette.text_light | default('#FFFFFF') }}; font-family: {{ typography.body_family | default('Arial, Helvetica, sans-serif') }}; opacity: 0.9; }
|
|
8
|
+
.cta-bg { fill: {{ palette.accent | default('#E8792F') }}; rx: 3; ry: 3; }
|
|
9
|
+
.cta-text { fill: {{ palette.text_light | default('#FFFFFF') }}; font-family: {{ typography.heading_family | default('Arial, Helvetica, sans-serif') }}; font-weight: bold; }
|
|
10
|
+
</style>
|
|
11
|
+
</defs>
|
|
12
|
+
<!-- Background -->
|
|
13
|
+
<rect class="bg" width="{{ width }}" height="{{ height }}" rx="0"/>
|
|
14
|
+
<!-- Logo -->
|
|
15
|
+
{% if logo_path %}
|
|
16
|
+
<image x="12" y="{{ (height - 28) // 2 }}" width="80" height="28" href="{{ logo_path }}"/>
|
|
17
|
+
{% endif %}
|
|
18
|
+
<!-- Headline -->
|
|
19
|
+
<text class="headline-text" x="{{ 100 if logo_path else 16 }}" y="{{ height // 2 - 4 }}" font-size="{{ headline_font_size | default(16) }}">{{ headline }}</text>
|
|
20
|
+
{% if sub_headline %}
|
|
21
|
+
<text class="sub-text" x="{{ 100 if logo_path else 16 }}" y="{{ height // 2 + 14 }}" font-size="11">{{ sub_headline }}</text>
|
|
22
|
+
{% endif %}
|
|
23
|
+
<!-- CTA button -->
|
|
24
|
+
<rect class="cta-bg" x="{{ width - cta_width - 16 }}" y="{{ (height - 28) // 2 }}" width="{{ cta_width }}" height="28"/>
|
|
25
|
+
<text class="cta-text" x="{{ width - cta_width // 2 - 16 }}" y="{{ height // 2 + 5 }}" font-size="12" text-anchor="middle">{{ cta }}</text>
|
|
26
|
+
</svg>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{ language | default('en') }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{ subject }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { margin: 0; padding: 0; font-family: {{ typography.body_family | default('Arial, Helvetica, sans-serif') }}; background-color: {{ palette.background_alt | default('#F7FAFC') }}; color: {{ palette.text | default('#1A202C') }}; }
|
|
9
|
+
.wrapper { max-width: 600px; margin: 0 auto; background: {{ palette.background | default('#FFFFFF') }}; }
|
|
10
|
+
.header { background: {{ palette.primary | default('#003B5C') }}; padding: 20px; text-align: center; }
|
|
11
|
+
.header img { max-width: 180px; height: auto; }
|
|
12
|
+
.hero { padding: 32px 24px; }
|
|
13
|
+
.hero h1 { font-family: {{ typography.heading_family | default('Arial, Helvetica, sans-serif') }}; font-size: {{ typography.sizes.h1 | default('28px') }}; font-weight: {{ typography.heading_weight | default('bold') }}; color: {{ palette.primary | default('#003B5C') }}; margin: 0 0 16px 0; line-height: 1.2; }
|
|
14
|
+
.body-content { padding: 0 24px 24px; font-size: {{ typography.sizes.body | default('14px') }}; line-height: 1.6; }
|
|
15
|
+
.body-content p { margin: 0 0 12px 0; }
|
|
16
|
+
.cta-wrapper { padding: 0 24px 32px; text-align: center; }
|
|
17
|
+
.cta-btn { display: inline-block; padding: 14px 32px; background: {{ palette.accent | default('#E8792F') }}; color: {{ palette.text_light | default('#FFFFFF') }}; text-decoration: none; font-weight: bold; font-size: 16px; border-radius: 4px; }
|
|
18
|
+
.safety-block { padding: 20px 24px; background: {{ palette.background_alt | default('#F7FAFC') }}; border-top: 2px solid {{ palette.safety_highlight | default('#E53E3E') }}; font-size: {{ typography.sizes.small | default('11px') }}; line-height: 1.5; }
|
|
19
|
+
.safety-block h3 { color: {{ palette.safety_highlight | default('#E53E3E') }}; font-size: 12px; margin: 0 0 8px 0; text-transform: uppercase; }
|
|
20
|
+
.legal-footer { padding: 16px 24px; font-size: {{ typography.sizes.legal | default('9px') }}; color: #718096; line-height: 1.4; text-align: center; border-top: 1px solid #E2E8F0; }
|
|
21
|
+
.legal-footer a { color: {{ palette.secondary | default('#00A3E0') }}; }
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div class="wrapper">
|
|
26
|
+
<div class="header">
|
|
27
|
+
{% if logo_path %}<img src="{{ logo_path }}" alt="{{ brand }}">{% endif %}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="hero">
|
|
30
|
+
<h1>{{ headline }}</h1>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="body-content">
|
|
33
|
+
{% for paragraph in body_paragraphs %}
|
|
34
|
+
<p>{{ paragraph }}</p>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
</div>
|
|
37
|
+
<div class="cta-wrapper">
|
|
38
|
+
<a href="#" class="cta-btn">{{ cta }}</a>
|
|
39
|
+
</div>
|
|
40
|
+
{% if safety_text %}
|
|
41
|
+
<div class="safety-block">
|
|
42
|
+
<h3>Important Safety Information</h3>
|
|
43
|
+
<p>{{ safety_text }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
{% endif %}
|
|
46
|
+
<div class="legal-footer">
|
|
47
|
+
{% if pi_ref %}<p>{{ pi_ref }}</p>{% endif %}
|
|
48
|
+
{% if copyright %}<p>{{ copyright }}</p>{% endif %}
|
|
49
|
+
{% if reporting %}<p>{{ reporting }}</p>{% endif %}
|
|
50
|
+
{% if disclaimer %}<p>{{ disclaimer }}</p>{% endif %}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
File without changes
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""create_campaign_brief — persist interview data as a campaign brief."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CreateCampaignBriefArgs(BaseModel):
|
|
11
|
+
# Identity
|
|
12
|
+
campaign_brief_id: str | None = Field(
|
|
13
|
+
default=None,
|
|
14
|
+
description="Resume an existing brief; omit to create new",
|
|
15
|
+
)
|
|
16
|
+
campaign_name: str = Field(description="Short campaign name")
|
|
17
|
+
|
|
18
|
+
# Jurisdiction & mode
|
|
19
|
+
country: str = Field(description="ISO 3166-1 alpha-2 country code")
|
|
20
|
+
policy_jurisdiction: str = Field(description="FDA | EMA | MHRA | HSA | PMDA | TGA")
|
|
21
|
+
mode: str = Field(description="promotional | non_promotional | disease_awareness")
|
|
22
|
+
|
|
23
|
+
# Product
|
|
24
|
+
brand: str = Field(description="Brand name")
|
|
25
|
+
indication: str = Field(description="Approved indication")
|
|
26
|
+
lifecycle_stage: str = Field(
|
|
27
|
+
default="growth",
|
|
28
|
+
description="pre_launch | launch | growth | mature | LOE",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Audience & objectives
|
|
32
|
+
target_segment: str = Field(description="e.g. oncologists, PCPs, nurses, patients")
|
|
33
|
+
behavioral_objective: str = Field(description="What the audience should DO after exposure")
|
|
34
|
+
educational_objective: str | None = Field(default=None)
|
|
35
|
+
desired_kpi: list[str] = Field(description="e.g. ['open_rate>25%', 'HCP_reach_500']")
|
|
36
|
+
|
|
37
|
+
# Content sources
|
|
38
|
+
approved_claims_path: str | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description="Path to claims JSON or PDF; uses fixture if omitted",
|
|
41
|
+
)
|
|
42
|
+
call_to_action: str = Field(description="Primary CTA text")
|
|
43
|
+
|
|
44
|
+
# Channels
|
|
45
|
+
channels: list[str] = Field(description="['email', 'banner', 'poster']")
|
|
46
|
+
asset_dimensions: dict | None = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
description='{"banner": "728x90", "poster": "A4"}',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Brand
|
|
52
|
+
brand_kit_path: str | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description="Path to brand kit directory; uses fixture if omitted",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Localisation
|
|
58
|
+
language: str = Field(default="en")
|
|
59
|
+
localisation_notes: str | None = Field(default=None)
|
|
60
|
+
|
|
61
|
+
# Compliance
|
|
62
|
+
required_safety_content: list[str] | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description="ISI, boxed warning, PI reference",
|
|
65
|
+
)
|
|
66
|
+
required_legal_content: list[str] | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Copyright, disclaimer, reporting statement",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Delivery
|
|
72
|
+
delivery_constraints: str | None = Field(default=None)
|
|
73
|
+
approval_workflow: str = Field(
|
|
74
|
+
default="mlr_standard",
|
|
75
|
+
description="mlr_standard | mlr_expedited | medical_only",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
TOOL: dict[str, Any] = {
|
|
80
|
+
"name": "create_campaign_brief",
|
|
81
|
+
"description": (
|
|
82
|
+
"Create or update a campaign brief from interview data. Validates all "
|
|
83
|
+
"fields, generates a reusable campaign_brief_id, persists the brief to "
|
|
84
|
+
"the campaign store, and returns the brief with any assumptions noted. "
|
|
85
|
+
"The Skill drives the interview conversation — this tool validates and "
|
|
86
|
+
"saves the collected data."
|
|
87
|
+
),
|
|
88
|
+
"args": CreateCampaignBriefArgs,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_VALID_MODES = {"promotional", "non_promotional", "disease_awareness"}
|
|
93
|
+
_VALID_JURISDICTIONS = {"FDA", "EMA", "MHRA", "HSA", "PMDA", "TGA"}
|
|
94
|
+
_VALID_LIFECYCLES = {"pre_launch", "launch", "growth", "mature", "LOE"}
|
|
95
|
+
_VALID_CHANNELS = {"email", "banner", "poster"}
|
|
96
|
+
_VALID_WORKFLOWS = {"mlr_standard", "mlr_expedited", "medical_only"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
100
|
+
import json
|
|
101
|
+
from datetime import datetime, timezone
|
|
102
|
+
|
|
103
|
+
from .._campaign_store import generate_campaign_id, load_brief, save_brief
|
|
104
|
+
|
|
105
|
+
errors: list[str] = []
|
|
106
|
+
assumptions: list[str] = []
|
|
107
|
+
|
|
108
|
+
mode = arguments["mode"]
|
|
109
|
+
if mode not in _VALID_MODES:
|
|
110
|
+
errors.append(f"Invalid mode '{mode}'. Must be one of: {', '.join(sorted(_VALID_MODES))}")
|
|
111
|
+
|
|
112
|
+
jurisdiction = arguments["policy_jurisdiction"]
|
|
113
|
+
if jurisdiction not in _VALID_JURISDICTIONS:
|
|
114
|
+
errors.append(
|
|
115
|
+
f"Invalid jurisdiction '{jurisdiction}'. Must be one of: {', '.join(sorted(_VALID_JURISDICTIONS))}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
lifecycle = arguments.get("lifecycle_stage", "growth")
|
|
119
|
+
if lifecycle not in _VALID_LIFECYCLES:
|
|
120
|
+
errors.append(f"Invalid lifecycle_stage '{lifecycle}'. Must be one of: {', '.join(sorted(_VALID_LIFECYCLES))}")
|
|
121
|
+
|
|
122
|
+
channels = arguments["channels"]
|
|
123
|
+
invalid_channels = set(channels) - _VALID_CHANNELS
|
|
124
|
+
if invalid_channels:
|
|
125
|
+
errors.append(
|
|
126
|
+
f"Invalid channels: {', '.join(sorted(invalid_channels))}. "
|
|
127
|
+
f"Must be from: {', '.join(sorted(_VALID_CHANNELS))}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
workflow = arguments.get("approval_workflow", "mlr_standard")
|
|
131
|
+
if workflow not in _VALID_WORKFLOWS:
|
|
132
|
+
errors.append(f"Invalid approval_workflow '{workflow}'. Must be one of: {', '.join(sorted(_VALID_WORKFLOWS))}")
|
|
133
|
+
|
|
134
|
+
if errors:
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
"type": "text",
|
|
138
|
+
"text": json.dumps({"errors": errors}),
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Resolve or generate campaign_brief_id
|
|
143
|
+
campaign_brief_id = arguments.get("campaign_brief_id")
|
|
144
|
+
if campaign_brief_id:
|
|
145
|
+
existing = load_brief(campaign_brief_id)
|
|
146
|
+
if existing:
|
|
147
|
+
assumptions.append(f"Updating existing brief '{campaign_brief_id}'")
|
|
148
|
+
else:
|
|
149
|
+
campaign_brief_id = generate_campaign_id(arguments["campaign_name"], arguments["brand"])
|
|
150
|
+
|
|
151
|
+
# Handle defaults and assumptions
|
|
152
|
+
safety = arguments.get("required_safety_content")
|
|
153
|
+
if not safety:
|
|
154
|
+
safety = []
|
|
155
|
+
assumptions.append("No safety content specified — will use default ISI from brand kit")
|
|
156
|
+
|
|
157
|
+
legal = arguments.get("required_legal_content")
|
|
158
|
+
if not legal:
|
|
159
|
+
legal = []
|
|
160
|
+
assumptions.append("No legal content specified — will use default from brand kit")
|
|
161
|
+
|
|
162
|
+
if not arguments.get("approved_claims_path"):
|
|
163
|
+
assumptions.append("No claims path specified — will use sample fixture claims")
|
|
164
|
+
|
|
165
|
+
if not arguments.get("brand_kit_path"):
|
|
166
|
+
assumptions.append("No brand kit path specified — will use sample fixture brand kit")
|
|
167
|
+
|
|
168
|
+
brief = {
|
|
169
|
+
"campaign_brief_id": campaign_brief_id,
|
|
170
|
+
"campaign_name": arguments["campaign_name"],
|
|
171
|
+
"country": arguments["country"],
|
|
172
|
+
"policy_jurisdiction": jurisdiction,
|
|
173
|
+
"mode": mode,
|
|
174
|
+
"brand": arguments["brand"],
|
|
175
|
+
"indication": arguments["indication"],
|
|
176
|
+
"lifecycle_stage": lifecycle,
|
|
177
|
+
"target_segment": arguments["target_segment"],
|
|
178
|
+
"behavioral_objective": arguments["behavioral_objective"],
|
|
179
|
+
"educational_objective": arguments.get("educational_objective"),
|
|
180
|
+
"desired_kpi": arguments["desired_kpi"],
|
|
181
|
+
"approved_claims_path": arguments.get("approved_claims_path"),
|
|
182
|
+
"call_to_action": arguments["call_to_action"],
|
|
183
|
+
"channels": channels,
|
|
184
|
+
"asset_dimensions": arguments.get("asset_dimensions"),
|
|
185
|
+
"brand_kit_path": arguments.get("brand_kit_path"),
|
|
186
|
+
"language": arguments.get("language", "en"),
|
|
187
|
+
"localisation_notes": arguments.get("localisation_notes"),
|
|
188
|
+
"required_safety_content": safety,
|
|
189
|
+
"required_legal_content": legal,
|
|
190
|
+
"delivery_constraints": arguments.get("delivery_constraints"),
|
|
191
|
+
"approval_workflow": workflow,
|
|
192
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
save_brief(brief)
|
|
196
|
+
|
|
197
|
+
result = {
|
|
198
|
+
"campaign_brief_id": campaign_brief_id,
|
|
199
|
+
"campaign_name": brief["campaign_name"],
|
|
200
|
+
"brand": brief["brand"],
|
|
201
|
+
"mode": brief["mode"],
|
|
202
|
+
"channels": brief["channels"],
|
|
203
|
+
"brief_path": str(__import__("pathlib").Path(_brief_path(campaign_brief_id))),
|
|
204
|
+
"assumptions": assumptions,
|
|
205
|
+
}
|
|
206
|
+
return [{"type": "text", "text": json.dumps(result, indent=2)}]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _brief_path(campaign_brief_id: str) -> str:
|
|
210
|
+
from .._campaign_store import campaign_dir
|
|
211
|
+
|
|
212
|
+
return str(campaign_dir(campaign_brief_id) / "campaign-brief.json")
|