open-pharma-plugins 2.2.0__tar.gz → 2.2.1__tar.gz
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.
- {open_pharma_plugins-2.2.0/src/open_pharma_plugins.egg-info → open_pharma_plugins-2.2.1}/PKG-INFO +1 -1
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_status.py +1 -1
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/__init__.py +3 -3
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/_optimizer.py +95 -74
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/_renderer.py +35 -25
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/_scoring.py +4 -4
- open_pharma_plugins-2.2.1/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/_universe.py +206 -0
- open_pharma_plugins-2.2.1/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/models.py +215 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/tools/load_universe.py +9 -6
- open_pharma_plugins-2.2.1/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/tools/recommend_engagements.py +82 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/tools/render_plan.py +39 -11
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/mcp_framework.py +1 -1
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1/src/open_pharma_plugins.egg-info}/PKG-INFO +1 -1
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/shared/env.py +1 -1
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/shared/filesystem.py +28 -0
- open_pharma_plugins-2.2.0/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/_universe.py +0 -145
- open_pharma_plugins-2.2.0/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/models.py +0 -135
- open_pharma_plugins-2.2.0/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/tools/recommend_engagements.py +0 -80
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/LICENSE +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/README.md +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/pyproject.toml +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/setup.cfg +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/__main__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/_campaign_store.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/_claim_engine.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/_renderer.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/fixtures/brand_kit/legal.json +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/fixtures/brand_kit/logo.svg +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/fixtures/brand_kit/palette.json +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/fixtures/brand_kit/product.png +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/fixtures/brand_kit/typography.json +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/fixtures/sample_approved_claims.json +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/_common.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/brief.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/claims.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/copy.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/journey.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/message.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/mlr.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/models/validation.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/policy/rules.json +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/templates/banner.svg.j2 +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/templates/email.html.j2 +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/create_campaign_brief.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/generate_audience_journey.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/generate_channel_copy.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/generate_message_architecture.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/package_mlr_submission.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/render_banner.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/render_email.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/render_poster.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/retrieve_approved_claims.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/retrieve_brand_components.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/campaign-studio/open_pharma_plugins_campaign_studio/tools/validate_claims_and_fair_balance.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/__main__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_artifacts.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_cache.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_clinical_trials.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_dailymed.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_fda.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_pubmed.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_regulatory.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_runs.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_transport.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_watchlist.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/_web_search.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/models.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_extract_events.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_landscape.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_refresh.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_report.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_scan_news.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_scan_publications.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_scan_regulatory.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_scan_trials.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_timeline.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_track.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/competitive-intelligence/open_pharma_plugins_competitive_intelligence/tools/ci_trial_detail.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/__main__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/_content_store.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/_grounding.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/_html_renderers.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/fixtures/sample_product_message.pdf +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/fixtures/sample_training_deck.pptx +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/models.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/tools/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/tools/get_document_page.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/tools/ingest_document.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/tools/list_documents.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/tools/render_output.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/field-training/open_pharma_plugins_field_training/tools/search_content.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/__main__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/_crm_store.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/batch.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/batch_cli.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/batch_csv.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/fixtures/sample_accounts.csv +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/models.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/get_account.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/list_accounts.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_clinical_trials.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_congresses.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_grants.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_guidelines.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_hco_web.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_hcp_web.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_orcid.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/search_publications.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/hcp-intelligence/open_pharma_plugins_hcp_intelligence/tools/update_account.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/__main__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/fixtures/sample_universe.csv +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/next-best-engagement/open_pharma_plugins_next_best_engagement/tools/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/__main__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/data.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/fixtures/constraints.csv +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/fixtures/current_alignment.csv +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/fixtures/hcps.csv +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/fixtures/reps.csv +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/geo.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/models.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/scoring.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/solver.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/__init__.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/ta_align.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/ta_cluster.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/ta_compare.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/ta_evaluate.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/ta_status.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/capabilities/territory-alignment/open_pharma_plugins_territory_alignment/tools/ta_visualize.py +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/open_pharma_plugins.egg-info/SOURCES.txt +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/open_pharma_plugins.egg-info/dependency_links.txt +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/open_pharma_plugins.egg-info/entry_points.txt +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/open_pharma_plugins.egg-info/requires.txt +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/open_pharma_plugins.egg-info/top_level.txt +0 -0
- {open_pharma_plugins-2.2.0 → open_pharma_plugins-2.2.1}/src/shared/__init__.py +0 -0
{open_pharma_plugins-2.2.0/src/open_pharma_plugins.egg-info → open_pharma_plugins-2.2.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: open-pharma-plugins
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Open Pharma Plugins: Agent Skills + MCP tools for pharmaceutical commercial operations — HCP intelligence, territory alignment, competitive intelligence, field training, engagement planning, and campaign creation.
|
|
5
5
|
Author: EAI Commercial Team
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -77,7 +77,7 @@ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
77
77
|
"openfda": {
|
|
78
78
|
"url": "https://api.fda.gov/drug",
|
|
79
79
|
"auth": "optional API key",
|
|
80
|
-
"rate_limit": "240/min
|
|
80
|
+
"rate_limit": "240/min; 1,000/day per IP without key or 120,000/day per key",
|
|
81
81
|
},
|
|
82
82
|
"dailymed": {"url": "https://dailymed.nlm.nih.gov/dailymed/services/v2", "auth": "none"},
|
|
83
83
|
"pubmed": {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from mcp_framework import build_registry
|
|
2
2
|
|
|
3
|
-
__version__ = "1.0.
|
|
3
|
+
__version__ = "1.0.2"
|
|
4
4
|
|
|
5
5
|
SPECS, get_handler, list_tools = build_registry(__name__, ["tools"])
|
|
6
6
|
|
|
7
7
|
USAGE_NOTE = (
|
|
8
8
|
"Next-Best-Engagement recommends which HCPs to engage, through which "
|
|
9
|
-
"channel, by which rep, subject to capacity, consent,
|
|
10
|
-
"constraints. Load an HCP universe (CSV or built-in fixtures), then "
|
|
9
|
+
"channel, by which rep, subject to visit capacity, explicit consent, "
|
|
10
|
+
"and minimum-gap constraints. Load an HCP universe (CSV or built-in fixtures), then "
|
|
11
11
|
"generate an optimised engagement plan."
|
|
12
12
|
)
|
|
13
13
|
|
|
@@ -7,6 +7,7 @@ from datetime import date, timedelta
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from ._scoring import score_hcp
|
|
10
|
+
from ._universe import fingerprint_universe
|
|
10
11
|
from .models import (
|
|
11
12
|
ConstraintConfig,
|
|
12
13
|
EngagementPlan,
|
|
@@ -24,9 +25,11 @@ def generate_plan(
|
|
|
24
25
|
rep_info: dict[str, dict[str, Any]],
|
|
25
26
|
config: ConstraintConfig,
|
|
26
27
|
today: date | None = None,
|
|
28
|
+
universe_generation: int = 0,
|
|
27
29
|
) -> EngagementPlan:
|
|
28
30
|
"""Generate an optimized engagement plan."""
|
|
29
31
|
today = today or date.today()
|
|
32
|
+
_validate_universe(universe, today)
|
|
30
33
|
period_start = today
|
|
31
34
|
period_end = today + timedelta(days=config.period_days)
|
|
32
35
|
|
|
@@ -35,19 +38,26 @@ def generate_plan(
|
|
|
35
38
|
rep_id: int(info["capacity"] * weeks_in_period) for rep_id, info in rep_info.items()
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
tier_counts: dict[str, int] = {}
|
|
40
|
-
for hcp in universe:
|
|
41
|
-
tier_counts[hcp.tier] = tier_counts.get(hcp.tier, 0) + 1
|
|
42
|
-
|
|
43
|
-
tier_coverage_actual: dict[str, float] = {t: 0.0 for t in tier_counts}
|
|
44
|
-
|
|
41
|
+
tier_coverage_actual = {hcp.tier: 0.0 for hcp in universe}
|
|
45
42
|
scored = []
|
|
46
43
|
for hcp in universe:
|
|
47
44
|
s, rationale = score_hcp(hcp, config, tier_coverage_actual, today)
|
|
48
45
|
scored.append((s, hcp, rationale))
|
|
49
46
|
scored.sort(key=lambda x: x[0], reverse=True)
|
|
50
47
|
|
|
48
|
+
# Coverage targets and metrics use the same pre-allocation action-eligibility
|
|
49
|
+
# boundary as the planning pass: consent/gap checks plus the score threshold.
|
|
50
|
+
eligible_universe = [
|
|
51
|
+
hcp
|
|
52
|
+
for score, hcp, _ in scored
|
|
53
|
+
if _eligibility_reason(hcp, config, today) is None and score >= config.no_action_threshold
|
|
54
|
+
]
|
|
55
|
+
tier_counts: dict[str, int] = {}
|
|
56
|
+
for hcp in eligible_universe:
|
|
57
|
+
tier_counts[hcp.tier] = tier_counts.get(hcp.tier, 0) + 1
|
|
58
|
+
|
|
59
|
+
tier_coverage_actual = {t: 0.0 for t in tier_counts}
|
|
60
|
+
|
|
51
61
|
engagements: list[PlannedEngagement] = []
|
|
52
62
|
unassigned: list[UnassignedHCP] = []
|
|
53
63
|
assigned_hcps: set[str] = set()
|
|
@@ -72,12 +82,21 @@ def generate_plan(
|
|
|
72
82
|
if tier_assigned >= target_count:
|
|
73
83
|
break
|
|
74
84
|
|
|
85
|
+
eligibility_reason = _eligibility_reason(hcp, config, today)
|
|
86
|
+
if eligibility_reason is not None:
|
|
87
|
+
_record_unassigned(hcp, eligibility_reason, unassigned, no_action_reasons)
|
|
88
|
+
assigned_hcps.add(hcp.hcp_id)
|
|
89
|
+
continue
|
|
90
|
+
if s < config.no_action_threshold:
|
|
91
|
+
_record_unassigned(hcp, "below_threshold", unassigned, no_action_reasons)
|
|
92
|
+
assigned_hcps.add(hcp.hcp_id)
|
|
93
|
+
continue
|
|
94
|
+
|
|
75
95
|
result = _try_assign(
|
|
76
96
|
hcp,
|
|
77
97
|
s,
|
|
78
98
|
rationale,
|
|
79
99
|
config,
|
|
80
|
-
hcp_action_counts,
|
|
81
100
|
rep_remaining,
|
|
82
101
|
rep_info,
|
|
83
102
|
period_start,
|
|
@@ -96,8 +115,8 @@ def generate_plan(
|
|
|
96
115
|
|
|
97
116
|
engagements.append(engagement)
|
|
98
117
|
assigned_hcps.add(hcp.hcp_id)
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
if action == "in_person_visit":
|
|
119
|
+
rep_remaining[engagement.rep_id] -= 1
|
|
101
120
|
tier_assigned += 1
|
|
102
121
|
|
|
103
122
|
# Re-score remaining HCPs with updated coverage for Phase 2
|
|
@@ -117,6 +136,10 @@ def generate_plan(
|
|
|
117
136
|
remaining.sort(key=lambda x: x[0], reverse=True)
|
|
118
137
|
|
|
119
138
|
for s, hcp, rationale in remaining:
|
|
139
|
+
eligibility_reason = _eligibility_reason(hcp, config, today)
|
|
140
|
+
if eligibility_reason is not None:
|
|
141
|
+
_record_unassigned(hcp, eligibility_reason, unassigned, no_action_reasons)
|
|
142
|
+
continue
|
|
120
143
|
if s < config.no_action_threshold:
|
|
121
144
|
_record_unassigned(hcp, "below_threshold", unassigned, no_action_reasons)
|
|
122
145
|
continue
|
|
@@ -126,7 +149,6 @@ def generate_plan(
|
|
|
126
149
|
s,
|
|
127
150
|
rationale,
|
|
128
151
|
config,
|
|
129
|
-
hcp_action_counts,
|
|
130
152
|
rep_remaining,
|
|
131
153
|
rep_info,
|
|
132
154
|
period_start,
|
|
@@ -143,11 +165,12 @@ def generate_plan(
|
|
|
143
165
|
|
|
144
166
|
engagements.append(engagement)
|
|
145
167
|
assigned_hcps.add(hcp.hcp_id)
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
if action == "in_person_visit":
|
|
169
|
+
rep_remaining[engagement.rep_id] -= 1
|
|
148
170
|
|
|
149
171
|
metrics = _build_metrics(
|
|
150
|
-
universe,
|
|
172
|
+
len(universe),
|
|
173
|
+
eligible_universe,
|
|
151
174
|
engagements,
|
|
152
175
|
rep_info,
|
|
153
176
|
rep_remaining,
|
|
@@ -159,6 +182,8 @@ def generate_plan(
|
|
|
159
182
|
)
|
|
160
183
|
|
|
161
184
|
return EngagementPlan(
|
|
185
|
+
universe_fingerprint=fingerprint_universe(universe),
|
|
186
|
+
universe_generation=universe_generation,
|
|
162
187
|
period_start=period_start,
|
|
163
188
|
period_end=period_end,
|
|
164
189
|
generated_at=today.isoformat(),
|
|
@@ -171,28 +196,38 @@ def generate_plan(
|
|
|
171
196
|
# ---- internal helpers ----
|
|
172
197
|
|
|
173
198
|
|
|
199
|
+
def _validate_universe(universe: list[UniverseRow], today: date) -> None:
|
|
200
|
+
seen_hcp_ids: set[str] = set()
|
|
201
|
+
for hcp in universe:
|
|
202
|
+
if hcp.hcp_id in seen_hcp_ids:
|
|
203
|
+
raise ValueError(f"Duplicate hcp_id: {hcp.hcp_id}")
|
|
204
|
+
seen_hcp_ids.add(hcp.hcp_id)
|
|
205
|
+
for field_name in ("last_visit_date", "last_email_date", "last_meeting_date"):
|
|
206
|
+
touch_date = getattr(hcp, field_name)
|
|
207
|
+
if touch_date is not None and touch_date > today:
|
|
208
|
+
raise ValueError(f"{field_name} is future-dated relative to the planning date")
|
|
209
|
+
|
|
210
|
+
|
|
174
211
|
def _try_assign(
|
|
175
212
|
hcp: UniverseRow,
|
|
176
213
|
score: float,
|
|
177
214
|
rationale: str,
|
|
178
215
|
config: ConstraintConfig,
|
|
179
|
-
hcp_action_counts: dict[str, list[str]],
|
|
180
216
|
rep_remaining: dict[str, int],
|
|
181
217
|
rep_info: dict[str, dict[str, Any]],
|
|
182
218
|
period_start: date,
|
|
183
219
|
period_end: date,
|
|
184
220
|
today: date,
|
|
185
221
|
) -> tuple[str, PlannedEngagement] | None:
|
|
186
|
-
"""
|
|
187
|
-
|
|
188
|
-
if rep_id
|
|
222
|
+
"""Assign to the declared rep, enforcing capacity only for in-person visits."""
|
|
223
|
+
action = _select_action(hcp, config, today)
|
|
224
|
+
if action == "in_person_visit" and rep_remaining.get(hcp.rep_id, 0) <= 0:
|
|
189
225
|
return None
|
|
190
226
|
|
|
191
|
-
action = _select_action(hcp, config, hcp_action_counts.get(hcp.hcp_id, []), today)
|
|
192
227
|
engagement = _create_engagement(
|
|
193
228
|
hcp,
|
|
194
229
|
action,
|
|
195
|
-
rep_id,
|
|
230
|
+
hcp.rep_id,
|
|
196
231
|
rep_info,
|
|
197
232
|
score,
|
|
198
233
|
rationale,
|
|
@@ -205,13 +240,9 @@ def _try_assign(
|
|
|
205
240
|
def _select_action(
|
|
206
241
|
hcp: UniverseRow,
|
|
207
242
|
config: ConstraintConfig,
|
|
208
|
-
prior_actions: list[str],
|
|
209
243
|
today: date,
|
|
210
244
|
) -> str:
|
|
211
245
|
"""Rule cascade: pick the best action for this HCP."""
|
|
212
|
-
visit_count = sum(1 for a in prior_actions if a == "in_person_visit")
|
|
213
|
-
email_count = sum(1 for a in prior_actions if a == "approved_email")
|
|
214
|
-
|
|
215
246
|
last_visit = hcp.last_visit_date
|
|
216
247
|
days_since_visit = (today - last_visit).days if last_visit else 999
|
|
217
248
|
|
|
@@ -224,51 +255,28 @@ def _select_action(
|
|
|
224
255
|
return "no_action"
|
|
225
256
|
|
|
226
257
|
# Rule 1: High-tier + overdue for visit + phone consent → in-person visit
|
|
227
|
-
if hcp.tier in ("A", "B") and days_since_visit >= 45 and hcp.consent_phone:
|
|
228
|
-
|
|
229
|
-
return "in_person_visit"
|
|
258
|
+
if hcp.tier in ("A", "B") and days_since_visit >= 45 and hcp.consent_phone is True:
|
|
259
|
+
return "in_person_visit"
|
|
230
260
|
|
|
231
261
|
# Rule 2: Recently visited + email consent → email follow-up
|
|
232
|
-
if days_since_visit < 30 and hcp.consent_email:
|
|
233
|
-
|
|
234
|
-
return "approved_email"
|
|
262
|
+
if days_since_visit < 30 and hcp.consent_email is True:
|
|
263
|
+
return "approved_email"
|
|
235
264
|
|
|
236
265
|
# Rule 3: Not recently visited + phone consent → remote meeting
|
|
237
|
-
if days_since_visit >= 30 and hcp.consent_phone:
|
|
238
|
-
|
|
239
|
-
return "remote_meeting"
|
|
266
|
+
if days_since_visit >= 30 and hcp.consent_phone is True:
|
|
267
|
+
return "remote_meeting"
|
|
240
268
|
|
|
241
269
|
# Rule 4: Email fallback
|
|
242
|
-
if hcp.consent_email
|
|
270
|
+
if hcp.consent_email is True:
|
|
243
271
|
return "approved_email"
|
|
244
272
|
|
|
245
273
|
# Rule 5: Phone fallback
|
|
246
|
-
if hcp.consent_phone
|
|
274
|
+
if hcp.consent_phone is True:
|
|
247
275
|
return "in_person_visit"
|
|
248
276
|
|
|
249
277
|
return "no_action"
|
|
250
278
|
|
|
251
279
|
|
|
252
|
-
def _pick_rep(
|
|
253
|
-
hcp: UniverseRow,
|
|
254
|
-
rep_remaining: dict[str, int],
|
|
255
|
-
rep_info: dict[str, dict[str, Any]],
|
|
256
|
-
) -> str | None:
|
|
257
|
-
"""Pick a rep with capacity — prefer the assigned rep, then least-loaded in territory."""
|
|
258
|
-
if hcp.rep_id in rep_remaining and rep_remaining[hcp.rep_id] > 0:
|
|
259
|
-
return hcp.rep_id
|
|
260
|
-
|
|
261
|
-
candidates = [
|
|
262
|
-
(rep_id, rep_remaining.get(rep_id, 0))
|
|
263
|
-
for rep_id, info in rep_info.items()
|
|
264
|
-
if hcp.territory_id in info["territories"] and rep_remaining.get(rep_id, 0) > 0
|
|
265
|
-
]
|
|
266
|
-
if not candidates:
|
|
267
|
-
return None
|
|
268
|
-
candidates.sort(key=lambda x: x[1], reverse=True)
|
|
269
|
-
return candidates[0][0]
|
|
270
|
-
|
|
271
|
-
|
|
272
280
|
def _create_engagement(
|
|
273
281
|
hcp: UniverseRow,
|
|
274
282
|
action: str,
|
|
@@ -292,36 +300,47 @@ def _create_engagement(
|
|
|
292
300
|
|
|
293
301
|
rep_name = rep_info.get(rep_id, {}).get("name", rep_id)
|
|
294
302
|
|
|
295
|
-
data: dict[str, Any] = {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
data: dict[str, Any] = dict(hcp.model_extra or {})
|
|
304
|
+
data.update(
|
|
305
|
+
{
|
|
306
|
+
"hcp_id": hcp.hcp_id,
|
|
307
|
+
"hcp_name": hcp.hcp_name,
|
|
308
|
+
"specialty": hcp.specialty,
|
|
309
|
+
"tier": hcp.tier,
|
|
310
|
+
"territory_id": hcp.territory_id,
|
|
311
|
+
"rep_id": rep_id,
|
|
312
|
+
"rep_name": rep_name,
|
|
313
|
+
"action_type": action,
|
|
314
|
+
"priority": priority,
|
|
315
|
+
"score": round(score, 3),
|
|
316
|
+
"suggested_window_start": period_start,
|
|
317
|
+
"suggested_window_end": period_end,
|
|
318
|
+
"rationale": rationale,
|
|
319
|
+
}
|
|
320
|
+
)
|
|
311
321
|
return PlannedEngagement.model_validate(data)
|
|
312
322
|
|
|
313
323
|
|
|
314
324
|
def _no_action_reason(hcp: UniverseRow, config: ConstraintConfig, today: date) -> str:
|
|
315
325
|
"""Determine why an HCP got no_action."""
|
|
326
|
+
return _eligibility_reason(hcp, config, today) or "channels_exhausted"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _eligibility_reason(hcp: UniverseRow, config: ConstraintConfig, today: date) -> str | None:
|
|
330
|
+
"""Return a fail-closed eligibility reason before score-based filtering."""
|
|
316
331
|
most_recent = max(
|
|
317
332
|
(d for d in [hcp.last_visit_date, hcp.last_email_date, hcp.last_meeting_date] if d),
|
|
318
333
|
default=None,
|
|
319
334
|
)
|
|
320
335
|
if most_recent and (today - most_recent).days < config.min_gap_days:
|
|
321
336
|
return "too_recent"
|
|
337
|
+
if hcp.consent_email is True or hcp.consent_phone is True:
|
|
338
|
+
return None
|
|
339
|
+
if hcp.consent_email is None or hcp.consent_phone is None:
|
|
340
|
+
return "missing_consent"
|
|
322
341
|
if not hcp.consent_email and not hcp.consent_phone:
|
|
323
342
|
return "no_consent"
|
|
324
|
-
return
|
|
343
|
+
return None
|
|
325
344
|
|
|
326
345
|
|
|
327
346
|
def _record_unassigned(
|
|
@@ -335,7 +354,8 @@ def _record_unassigned(
|
|
|
335
354
|
|
|
336
355
|
|
|
337
356
|
def _build_metrics(
|
|
338
|
-
|
|
357
|
+
total_universe: int,
|
|
358
|
+
eligible_universe: list[UniverseRow],
|
|
339
359
|
engagements: list[PlannedEngagement],
|
|
340
360
|
rep_info: dict[str, dict[str, Any]],
|
|
341
361
|
rep_remaining: dict[str, int],
|
|
@@ -345,7 +365,7 @@ def _build_metrics(
|
|
|
345
365
|
tier_counts: dict[str, int],
|
|
346
366
|
target_map: dict[str, float],
|
|
347
367
|
) -> PlanMetrics:
|
|
348
|
-
total_eligible = len(
|
|
368
|
+
total_eligible = len(eligible_universe)
|
|
349
369
|
total_planned = len(engagements)
|
|
350
370
|
|
|
351
371
|
# Coverage by tier
|
|
@@ -389,6 +409,7 @@ def _build_metrics(
|
|
|
389
409
|
coverage_pct = total_planned / total_eligible if total_eligible else 0.0
|
|
390
410
|
|
|
391
411
|
return PlanMetrics(
|
|
412
|
+
total_universe=total_universe,
|
|
392
413
|
total_eligible=total_eligible,
|
|
393
414
|
total_planned=total_planned,
|
|
394
415
|
coverage_pct=round(coverage_pct, 3),
|
|
@@ -7,29 +7,43 @@ import io
|
|
|
7
7
|
from datetime import date
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
|
+
from uuid import uuid4
|
|
10
11
|
|
|
11
|
-
from shared.filesystem import
|
|
12
|
+
from shared.filesystem import ensure_private_dir, exclusive_write_text, remove_files
|
|
12
13
|
|
|
13
|
-
from .models import EngagementPlan
|
|
14
|
+
from .models import EngagementPlan, is_formula_unsafe_text
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def render_csv(plan: EngagementPlan, output_dir: str) -> dict[str, str]:
|
|
17
|
-
"""Write the plan to CSV files. Returns paths of written files."""
|
|
18
|
+
"""Write the plan to a new pair of CSV files. Returns paths of written files."""
|
|
18
19
|
from datetime import datetime, timezone
|
|
19
20
|
|
|
21
|
+
engagements_content = _engagements_csv_content(plan)
|
|
22
|
+
summary_content = _summary_csv_content(plan)
|
|
20
23
|
out = ensure_private_dir(Path(output_dir))
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
for _ in range(10):
|
|
26
|
+
run_id = f"{datetime.now(timezone.utc):%Y%m%d_%H%M%S}_{uuid4().hex}"
|
|
27
|
+
engagements_path = out / f"engagements_{run_id}.csv"
|
|
28
|
+
summary_path = out / f"plan_summary_{run_id}.csv"
|
|
29
|
+
created_paths: list[Path] = []
|
|
30
|
+
try:
|
|
31
|
+
exclusive_write_text(engagements_path, engagements_content)
|
|
32
|
+
created_paths.append(engagements_path)
|
|
33
|
+
exclusive_write_text(summary_path, summary_content)
|
|
34
|
+
created_paths.append(summary_path)
|
|
35
|
+
except FileExistsError:
|
|
36
|
+
remove_files(created_paths)
|
|
37
|
+
except Exception:
|
|
38
|
+
remove_files(created_paths)
|
|
39
|
+
raise
|
|
40
|
+
else:
|
|
41
|
+
return {
|
|
42
|
+
"engagements_csv": str(engagements_path),
|
|
43
|
+
"summary_csv": str(summary_path),
|
|
44
|
+
}
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
"engagements_csv": str(engagements_path),
|
|
31
|
-
"summary_csv": str(summary_path),
|
|
32
|
-
}
|
|
46
|
+
raise FileExistsError("Could not allocate unique plan-render output paths.")
|
|
33
47
|
|
|
34
48
|
|
|
35
49
|
def render_json(plan: EngagementPlan) -> str:
|
|
@@ -54,27 +68,23 @@ _DEFAULT_ENGAGEMENT_COLUMNS = [
|
|
|
54
68
|
]
|
|
55
69
|
|
|
56
70
|
|
|
57
|
-
def
|
|
71
|
+
def _engagements_csv_content(plan: EngagementPlan) -> str:
|
|
58
72
|
buffer = io.StringIO(newline="")
|
|
59
|
-
if not plan.engagements:
|
|
60
|
-
writer = csv.DictWriter(buffer, fieldnames=_DEFAULT_ENGAGEMENT_COLUMNS)
|
|
61
|
-
writer.writeheader()
|
|
62
|
-
atomic_write_text(path, buffer.getvalue())
|
|
63
|
-
return
|
|
64
|
-
|
|
65
73
|
rows = [_engagement_to_row(e) for e in plan.engagements]
|
|
66
|
-
|
|
74
|
+
extra_columns = sorted({key for row in rows for key in row if key not in _DEFAULT_ENGAGEMENT_COLUMNS})
|
|
75
|
+
fieldnames = [*_DEFAULT_ENGAGEMENT_COLUMNS, *extra_columns]
|
|
67
76
|
|
|
68
77
|
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
|
|
69
78
|
writer.writeheader()
|
|
70
79
|
writer.writerows(rows)
|
|
71
|
-
|
|
80
|
+
return buffer.getvalue()
|
|
72
81
|
|
|
73
82
|
|
|
74
|
-
def
|
|
83
|
+
def _summary_csv_content(plan: EngagementPlan) -> str:
|
|
75
84
|
m = plan.metrics
|
|
76
85
|
rows: list[dict[str, Any]] = []
|
|
77
86
|
|
|
87
|
+
rows.append({"section": "overview", "metric": "total_universe", "value": m.total_universe})
|
|
78
88
|
rows.append({"section": "overview", "metric": "total_eligible", "value": m.total_eligible})
|
|
79
89
|
rows.append({"section": "overview", "metric": "total_planned", "value": m.total_planned})
|
|
80
90
|
rows.append({"section": "overview", "metric": "coverage_pct", "value": m.coverage_pct})
|
|
@@ -129,7 +139,7 @@ def _write_summary_csv(plan: EngagementPlan, path: Path) -> None:
|
|
|
129
139
|
writer = csv.DictWriter(buffer, fieldnames=["section", "metric", "value"])
|
|
130
140
|
writer.writeheader()
|
|
131
141
|
writer.writerows({key: _safe_csv_cell(value) for key, value in row.items()} for row in rows)
|
|
132
|
-
|
|
142
|
+
return buffer.getvalue()
|
|
133
143
|
|
|
134
144
|
|
|
135
145
|
def _engagement_to_row(e: Any) -> dict[str, Any]:
|
|
@@ -144,6 +154,6 @@ def _engagement_to_row(e: Any) -> dict[str, Any]:
|
|
|
144
154
|
|
|
145
155
|
def _safe_csv_cell(value: Any) -> Any:
|
|
146
156
|
"""Prevent spreadsheet applications from interpreting exported text as a formula."""
|
|
147
|
-
if isinstance(value, str) and value
|
|
157
|
+
if isinstance(value, str) and is_formula_unsafe_text(value):
|
|
148
158
|
return "'" + value
|
|
149
159
|
return value
|
|
@@ -42,7 +42,7 @@ def score_hcp(
|
|
|
42
42
|
velocity = 0.3
|
|
43
43
|
factors.append(("engagement_velocity", velocity, velocity * w.engagement_velocity))
|
|
44
44
|
|
|
45
|
-
# 4. Channel
|
|
45
|
+
# 4. Channel diversity — reward recent use of more than one channel type
|
|
46
46
|
touches = [
|
|
47
47
|
(hcp.last_visit_date, "visit"),
|
|
48
48
|
(hcp.last_email_date, "email"),
|
|
@@ -50,11 +50,11 @@ def score_hcp(
|
|
|
50
50
|
]
|
|
51
51
|
recent_channels = [ch for d, ch in touches if d and (today - d).days <= 90]
|
|
52
52
|
if len(recent_channels) <= 1:
|
|
53
|
-
|
|
53
|
+
diversity = 0.8 if recent_channels else 1.0
|
|
54
54
|
else:
|
|
55
55
|
unique = len(set(recent_channels))
|
|
56
|
-
|
|
57
|
-
factors.append(("
|
|
56
|
+
diversity = unique / len(recent_channels)
|
|
57
|
+
factors.append(("channel_diversity", diversity, diversity * w.channel_diversity))
|
|
58
58
|
|
|
59
59
|
# 5. Coverage debt — how far is this tier from its target?
|
|
60
60
|
target_map = {
|