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,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 {}
|