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,99 @@
1
+ """render_banner — merge validated copy into SVG banner template."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class RenderBannerArgs(BaseModel):
11
+ campaign_brief_id: str = Field(description="Links to the campaign")
12
+ dimensions: str = Field(
13
+ default="728x90",
14
+ description="WxH in pixels, e.g. '728x90', '300x250', '160x600'",
15
+ )
16
+
17
+
18
+ TOOL: dict[str, Any] = {
19
+ "name": "render_banner",
20
+ "description": (
21
+ "Render an SVG banner from validated channel copy and brand components. "
22
+ "Refuses to run if validate_claims_and_fair_balance has not passed. "
23
+ "Merges headline, CTA, logo, and brand tokens into the SVG template. "
24
+ "Saves banner.svg to the campaign outputs directory."
25
+ ),
26
+ "args": RenderBannerArgs,
27
+ }
28
+
29
+
30
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
31
+ import json
32
+
33
+ from jinja2 import BaseLoader, Environment
34
+
35
+ from .._campaign_store import load_artifact, save_output
36
+ from .._renderer import (
37
+ check_validation_gate,
38
+ extract_copy_text,
39
+ load_brand_kit,
40
+ load_template,
41
+ )
42
+
43
+ campaign_brief_id = arguments["campaign_brief_id"]
44
+
45
+ gate_error = check_validation_gate(campaign_brief_id, "banner")
46
+ if gate_error:
47
+ return [{"type": "text", "text": json.dumps({"error": gate_error})}]
48
+
49
+ copy_artifact = load_artifact(campaign_brief_id, "copy-banner.json")
50
+ if not copy_artifact:
51
+ return [
52
+ {
53
+ "type": "text",
54
+ "text": json.dumps({"error": "No copy-banner.json found. Run generate_channel_copy for banner first."}),
55
+ }
56
+ ]
57
+
58
+ brand = load_brand_kit(campaign_brief_id)
59
+ copy_text = extract_copy_text(copy_artifact.get("copy", {}), "banner")
60
+
61
+ dims = arguments.get("dimensions", "728x90")
62
+ try:
63
+ width, height = (int(x) for x in dims.split("x"))
64
+ except (ValueError, AttributeError):
65
+ width, height = 728, 90
66
+
67
+ cta_text = copy_text.get("cta", "Learn More")
68
+ cta_width = max(80, len(cta_text) * 8 + 24)
69
+
70
+ headline_font_size = 16 if height >= 90 else 13
71
+
72
+ tpl_content = load_template("banner.svg.j2")
73
+ env = Environment(loader=BaseLoader(), autoescape=True)
74
+ tpl = env.from_string(tpl_content)
75
+
76
+ svg = tpl.render(
77
+ width=width,
78
+ height=height,
79
+ headline=copy_text.get("headline", ""),
80
+ sub_headline=copy_text.get("sub_headline"),
81
+ cta=cta_text,
82
+ cta_width=cta_width,
83
+ headline_font_size=headline_font_size,
84
+ logo_path=brand.get("logo_path", ""),
85
+ palette=brand.get("palette", {}),
86
+ typography=brand.get("typography", {}),
87
+ )
88
+
89
+ path = save_output(campaign_brief_id, "banner.svg", svg)
90
+
91
+ result = {
92
+ "campaign_brief_id": campaign_brief_id,
93
+ "channel": "banner",
94
+ "file_path": str(path),
95
+ "format": "svg",
96
+ "editable": True,
97
+ "dimensions": f"{width}x{height}",
98
+ }
99
+ return [{"type": "text", "text": json.dumps(result, indent=2)}]
@@ -0,0 +1,101 @@
1
+ """render_email — merge validated copy into email HTML template."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class RenderEmailArgs(BaseModel):
11
+ campaign_brief_id: str = Field(description="Links to the campaign")
12
+ template: str | None = Field(
13
+ default=None,
14
+ description="Custom template path; uses default email.html.j2 if omitted",
15
+ )
16
+
17
+
18
+ TOOL: dict[str, Any] = {
19
+ "name": "render_email",
20
+ "description": (
21
+ "Render a self-contained HTML email from validated channel copy and "
22
+ "brand components. Refuses to run if validate_claims_and_fair_balance "
23
+ "has not passed. Merges copy blocks, brand colours, typography, logo, "
24
+ "and legal content into the email template. Saves email.html to the "
25
+ "campaign outputs directory."
26
+ ),
27
+ "args": RenderEmailArgs,
28
+ }
29
+
30
+
31
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
32
+ import json
33
+
34
+ from jinja2 import BaseLoader, Environment
35
+
36
+ from .._campaign_store import load_artifact, save_output
37
+ from .._renderer import (
38
+ check_validation_gate,
39
+ extract_copy_text,
40
+ load_brand_kit,
41
+ load_template,
42
+ )
43
+
44
+ campaign_brief_id = arguments["campaign_brief_id"]
45
+
46
+ gate_error = check_validation_gate(campaign_brief_id, "email")
47
+ if gate_error:
48
+ return [{"type": "text", "text": json.dumps({"error": gate_error})}]
49
+
50
+ copy_artifact = load_artifact(campaign_brief_id, "copy-email.json")
51
+ if not copy_artifact:
52
+ return [
53
+ {
54
+ "type": "text",
55
+ "text": json.dumps({"error": "No copy-email.json found. Run generate_channel_copy for email first."}),
56
+ }
57
+ ]
58
+
59
+ brief = load_artifact(campaign_brief_id, "campaign-brief.json") or {}
60
+ brand = load_brand_kit(campaign_brief_id)
61
+ copy_text = extract_copy_text(copy_artifact.get("copy", {}), "email")
62
+ legal = brand.get("legal", {})
63
+
64
+ template_str = arguments.get("template")
65
+ if template_str:
66
+ from pathlib import Path
67
+
68
+ tpl_content = Path(template_str).read_text()
69
+ else:
70
+ tpl_content = load_template("email.html.j2")
71
+
72
+ env = Environment(loader=BaseLoader(), autoescape=True)
73
+ tpl = env.from_string(tpl_content)
74
+
75
+ html = tpl.render(
76
+ brand=brief.get("brand", ""),
77
+ language=brief.get("language", "en"),
78
+ subject=copy_text.get("subject", ""),
79
+ headline=copy_text.get("headline", ""),
80
+ body_paragraphs=copy_text.get("body_paragraphs", []),
81
+ cta=copy_text.get("cta", ""),
82
+ safety_text=legal.get("isi", ""),
83
+ pi_ref=legal.get("pi_ref", ""),
84
+ copyright=legal.get("copyright", ""),
85
+ reporting=legal.get("reporting_statement", ""),
86
+ disclaimer=legal.get("disclaimer", ""),
87
+ logo_path=brand.get("logo_path", ""),
88
+ palette=brand.get("palette", {}),
89
+ typography=brand.get("typography", {}),
90
+ )
91
+
92
+ path = save_output(campaign_brief_id, "email.html", html)
93
+
94
+ result = {
95
+ "campaign_brief_id": campaign_brief_id,
96
+ "channel": "email",
97
+ "file_path": str(path),
98
+ "format": "html",
99
+ "editable": True,
100
+ }
101
+ return [{"type": "text", "text": json.dumps(result, indent=2)}]
@@ -0,0 +1,222 @@
1
+ """render_poster — build a print-ready PDF poster using ReportLab."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class RenderPosterArgs(BaseModel):
11
+ campaign_brief_id: str = Field(description="Links to the campaign")
12
+ paper_size: str = Field(
13
+ default="A4",
14
+ description="A4 | letter | A3",
15
+ )
16
+
17
+
18
+ TOOL: dict[str, Any] = {
19
+ "name": "render_poster",
20
+ "description": (
21
+ "Render a print-ready PDF poster from validated channel copy and brand "
22
+ "components using ReportLab. Refuses to run if "
23
+ "validate_claims_and_fair_balance has not passed. Includes headline, "
24
+ "body, CTA, safety block, and legal footer with brand styling. "
25
+ "Saves poster.pdf to the campaign outputs directory."
26
+ ),
27
+ "args": RenderPosterArgs,
28
+ }
29
+
30
+ _PAPER_SIZES = {
31
+ "A4": (595.28, 841.89),
32
+ "letter": (612, 792),
33
+ "A3": (841.89, 1190.55),
34
+ }
35
+
36
+
37
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
38
+ import json
39
+
40
+ from .._campaign_store import load_artifact, save_output_bytes
41
+ from .._renderer import (
42
+ check_validation_gate,
43
+ extract_copy_text,
44
+ load_brand_kit,
45
+ )
46
+
47
+ campaign_brief_id = arguments["campaign_brief_id"]
48
+
49
+ gate_error = check_validation_gate(campaign_brief_id, "poster")
50
+ if gate_error:
51
+ return [{"type": "text", "text": json.dumps({"error": gate_error})}]
52
+
53
+ copy_artifact = load_artifact(campaign_brief_id, "copy-poster.json")
54
+ if not copy_artifact:
55
+ return [
56
+ {
57
+ "type": "text",
58
+ "text": json.dumps({"error": "No copy-poster.json found. Run generate_channel_copy for poster first."}),
59
+ }
60
+ ]
61
+
62
+ brand = load_brand_kit(campaign_brief_id)
63
+ copy_text = extract_copy_text(copy_artifact.get("copy", {}), "poster")
64
+ legal = brand.get("legal", {})
65
+ palette = brand.get("palette", {})
66
+
67
+ paper = arguments.get("paper_size", "A4").upper()
68
+ page_w, page_h = _PAPER_SIZES.get(paper, _PAPER_SIZES["A4"])
69
+
70
+ pdf_bytes = _build_pdf(page_w, page_h, copy_text, palette, legal, brand)
71
+
72
+ path = save_output_bytes(campaign_brief_id, "poster.pdf", pdf_bytes)
73
+
74
+ result = {
75
+ "campaign_brief_id": campaign_brief_id,
76
+ "channel": "poster",
77
+ "file_path": str(path),
78
+ "format": "pdf",
79
+ "editable": False,
80
+ "paper_size": paper,
81
+ }
82
+ return [{"type": "text", "text": json.dumps(result, indent=2)}]
83
+
84
+
85
+ def _build_pdf(
86
+ page_w: float,
87
+ page_h: float,
88
+ copy_text: dict,
89
+ palette: dict,
90
+ legal: dict,
91
+ brand: dict,
92
+ ) -> bytes:
93
+ import io
94
+ from xml.sax.saxutils import escape
95
+
96
+ from reportlab.lib.colors import HexColor
97
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
98
+ from reportlab.lib.units import mm
99
+ from reportlab.platypus import (
100
+ Paragraph,
101
+ SimpleDocTemplate,
102
+ Spacer,
103
+ )
104
+
105
+ buf = io.BytesIO()
106
+ margin = 20 * mm
107
+
108
+ doc = SimpleDocTemplate(
109
+ buf,
110
+ pagesize=(page_w, page_h),
111
+ leftMargin=margin,
112
+ rightMargin=margin,
113
+ topMargin=margin,
114
+ bottomMargin=margin,
115
+ )
116
+
117
+ primary = HexColor(palette.get("primary", "#003B5C"))
118
+ accent = HexColor(palette.get("accent", "#E8792F"))
119
+ text_color = HexColor(palette.get("text", "#1A202C"))
120
+ safety_color = HexColor(palette.get("safety_highlight", "#E53E3E"))
121
+
122
+ styles = getSampleStyleSheet()
123
+
124
+ headline_style = ParagraphStyle(
125
+ "Headline",
126
+ parent=styles["Title"],
127
+ fontSize=28,
128
+ textColor=primary,
129
+ spaceAfter=12,
130
+ leading=34,
131
+ )
132
+
133
+ subhead_style = ParagraphStyle(
134
+ "Subhead",
135
+ parent=styles["Heading2"],
136
+ fontSize=18,
137
+ textColor=primary,
138
+ spaceAfter=8,
139
+ leading=22,
140
+ )
141
+
142
+ body_style = ParagraphStyle(
143
+ "Body",
144
+ parent=styles["BodyText"],
145
+ fontSize=12,
146
+ textColor=text_color,
147
+ spaceAfter=8,
148
+ leading=16,
149
+ )
150
+
151
+ cta_style = ParagraphStyle(
152
+ "CTA",
153
+ parent=styles["BodyText"],
154
+ fontSize=14,
155
+ textColor=accent,
156
+ spaceBefore=12,
157
+ spaceAfter=12,
158
+ fontName="Helvetica-Bold",
159
+ )
160
+
161
+ safety_style = ParagraphStyle(
162
+ "Safety",
163
+ parent=styles["BodyText"],
164
+ fontSize=8,
165
+ textColor=safety_color,
166
+ spaceBefore=16,
167
+ spaceAfter=4,
168
+ leading=10,
169
+ )
170
+
171
+ legal_style = ParagraphStyle(
172
+ "Legal",
173
+ parent=styles["BodyText"],
174
+ fontSize=7,
175
+ textColor=HexColor("#718096"),
176
+ spaceBefore=8,
177
+ leading=9,
178
+ )
179
+
180
+ story: list = []
181
+
182
+ headline = copy_text.get("headline", "")
183
+ if headline:
184
+ story.append(Paragraph(escape(headline), headline_style))
185
+
186
+ subhead = copy_text.get("subhead", "")
187
+ if subhead:
188
+ story.append(Paragraph(escape(subhead), subhead_style))
189
+
190
+ story.append(Spacer(1, 8))
191
+
192
+ for para in copy_text.get("body_paragraphs", []):
193
+ if para:
194
+ story.append(Paragraph(escape(para), body_style))
195
+
196
+ for bullet in copy_text.get("bullet_points", []):
197
+ if bullet:
198
+ story.append(Paragraph(f"• {escape(bullet)}", body_style))
199
+
200
+ cta = copy_text.get("cta", "")
201
+ if cta:
202
+ story.append(Paragraph(escape(cta), cta_style))
203
+
204
+ isi = legal.get("isi", "")
205
+ if isi:
206
+ story.append(Paragraph("<b>IMPORTANT SAFETY INFORMATION</b>", safety_style))
207
+ story.append(Paragraph(escape(isi), safety_style))
208
+
209
+ legal_parts = []
210
+ for key in ("pi_ref", "copyright", "reporting_statement", "disclaimer"):
211
+ val = legal.get(key, "")
212
+ if val:
213
+ legal_parts.append(escape(val))
214
+ if legal_parts:
215
+ story.append(Paragraph("<br/>".join(legal_parts), legal_style))
216
+
217
+ for fn in copy_text.get("footnotes", []):
218
+ if fn:
219
+ story.append(Paragraph(escape(fn), legal_style))
220
+
221
+ doc.build(story)
222
+ return buf.getvalue()
@@ -0,0 +1,71 @@
1
+ """retrieve_approved_claims — load and filter approved claims for a campaign."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class RetrieveApprovedClaimsArgs(BaseModel):
11
+ campaign_brief_id: str = Field(description="Links claims to the campaign")
12
+ source: str | None = Field(
13
+ default=None,
14
+ description="Path to claims JSON; uses brief's path or fixture if omitted",
15
+ )
16
+ categories: list[str] | None = Field(
17
+ default=None,
18
+ description="Filter: efficacy | safety | tolerability | dosing | positioning | MOA",
19
+ )
20
+
21
+
22
+ TOOL: dict[str, Any] = {
23
+ "name": "retrieve_approved_claims",
24
+ "description": (
25
+ "Load the approved claims set for a campaign. Reads from a JSON file "
26
+ "containing ApprovedClaim objects. Optionally filter by category. "
27
+ "Persists the claims to the campaign directory for downstream tools. "
28
+ "If no source is specified, uses the path from the campaign brief or "
29
+ "the built-in sample ONCORIX fixture."
30
+ ),
31
+ "args": RetrieveApprovedClaimsArgs,
32
+ }
33
+
34
+
35
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
36
+ import json
37
+ from importlib.resources import files
38
+ from pathlib import Path
39
+
40
+ from .._campaign_store import load_brief, save_artifact
41
+
42
+ campaign_brief_id = arguments["campaign_brief_id"]
43
+ source = arguments.get("source")
44
+ categories = arguments.get("categories")
45
+
46
+ if not source:
47
+ brief = load_brief(campaign_brief_id)
48
+ if brief and brief.get("approved_claims_path"):
49
+ source = brief["approved_claims_path"]
50
+
51
+ claims: list[dict] = []
52
+
53
+ if source and Path(source).is_file():
54
+ claims = json.loads(Path(source).read_text())
55
+ else:
56
+ fixture_path = files("open_pharma_plugins_campaign_studio") / "fixtures" / "sample_approved_claims.json"
57
+ claims = json.loads(fixture_path.read_text())
58
+
59
+ if categories:
60
+ cat_set = {c.lower() for c in categories}
61
+ claims = [c for c in claims if c.get("category", "").lower() in cat_set]
62
+
63
+ save_artifact(campaign_brief_id, "approved-claims.json", claims)
64
+
65
+ result = {
66
+ "campaign_brief_id": campaign_brief_id,
67
+ "total_claims": len(claims),
68
+ "categories": sorted({c.get("category", "") for c in claims}),
69
+ "claims": claims,
70
+ }
71
+ return [{"type": "text", "text": json.dumps(result, indent=2)}]
@@ -0,0 +1,76 @@
1
+ """retrieve_brand_components — load brand kit assets for rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class RetrieveBrandComponentsArgs(BaseModel):
11
+ campaign_brief_id: str = Field(description="Links to the campaign")
12
+ brand_kit_path: str | None = Field(
13
+ default=None,
14
+ description="Override path to brand kit directory; uses brief's path or fixture if omitted",
15
+ )
16
+
17
+
18
+ TOOL: dict[str, Any] = {
19
+ "name": "retrieve_brand_components",
20
+ "description": (
21
+ "Load brand components (logo, product image, colour palette, typography, "
22
+ "and legal content) for a campaign. Returns paths and structured data "
23
+ "for use by renderers. If no path is specified, uses the path from the "
24
+ "campaign brief or the built-in sample brand kit."
25
+ ),
26
+ "args": RetrieveBrandComponentsArgs,
27
+ }
28
+
29
+
30
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
31
+ import json
32
+ from importlib.resources import files
33
+ from pathlib import Path
34
+
35
+ from .._campaign_store import load_brief
36
+
37
+ campaign_brief_id = arguments["campaign_brief_id"]
38
+ kit_path = arguments.get("brand_kit_path")
39
+
40
+ if not kit_path:
41
+ brief = load_brief(campaign_brief_id)
42
+ if brief and brief.get("brand_kit_path"):
43
+ kit_path = brief["brand_kit_path"]
44
+
45
+ if kit_path and Path(kit_path).is_dir():
46
+ kit_dir = Path(kit_path)
47
+ else:
48
+ kit_dir = Path(str(files("open_pharma_plugins_campaign_studio") / "fixtures" / "brand_kit"))
49
+
50
+ palette = _load_json(kit_dir / "palette.json")
51
+ typography = _load_json(kit_dir / "typography.json")
52
+ legal = _load_json(kit_dir / "legal.json")
53
+
54
+ logo_path = kit_dir / "logo.svg"
55
+ product_path = kit_dir / "product.png"
56
+
57
+ result = {
58
+ "campaign_brief_id": campaign_brief_id,
59
+ "brand_kit_path": str(kit_dir),
60
+ "logo_path": str(logo_path) if logo_path.exists() else None,
61
+ "product_image_path": str(product_path) if product_path.exists() else None,
62
+ "palette": palette,
63
+ "typography": typography,
64
+ "legal": legal,
65
+ }
66
+ return [{"type": "text", "text": json.dumps(result, indent=2)}]
67
+
68
+
69
+ def _load_json(path) -> dict:
70
+ import json
71
+ from pathlib import Path
72
+
73
+ p = Path(path)
74
+ if p.exists():
75
+ return json.loads(p.read_text())
76
+ return {}