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,156 @@
1
+ %PDF-1.4
2
+ %���� ReportLab Generated PDF document (opensource)
3
+ 1 0 obj
4
+ <<
5
+ /F1 2 0 R /F2 3 0 R /F3 6 0 R
6
+ >>
7
+ endobj
8
+ 2 0 obj
9
+ <<
10
+ /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
11
+ >>
12
+ endobj
13
+ 3 0 obj
14
+ <<
15
+ /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
16
+ >>
17
+ endobj
18
+ 4 0 obj
19
+ <<
20
+ /Contents 13 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
21
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
22
+ >> /Rotate 0 /Trans <<
23
+
24
+ >>
25
+ /Type /Page
26
+ >>
27
+ endobj
28
+ 5 0 obj
29
+ <<
30
+ /Contents 14 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
31
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
32
+ >> /Rotate 0 /Trans <<
33
+
34
+ >>
35
+ /Type /Page
36
+ >>
37
+ endobj
38
+ 6 0 obj
39
+ <<
40
+ /BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
41
+ >>
42
+ endobj
43
+ 7 0 obj
44
+ <<
45
+ /Contents 15 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
46
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
47
+ >> /Rotate 0 /Trans <<
48
+
49
+ >>
50
+ /Type /Page
51
+ >>
52
+ endobj
53
+ 8 0 obj
54
+ <<
55
+ /Contents 16 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
56
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
57
+ >> /Rotate 0 /Trans <<
58
+
59
+ >>
60
+ /Type /Page
61
+ >>
62
+ endobj
63
+ 9 0 obj
64
+ <<
65
+ /Contents 17 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
66
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
67
+ >> /Rotate 0 /Trans <<
68
+
69
+ >>
70
+ /Type /Page
71
+ >>
72
+ endobj
73
+ 10 0 obj
74
+ <<
75
+ /PageMode /UseNone /Pages 12 0 R /Type /Catalog
76
+ >>
77
+ endobj
78
+ 11 0 obj
79
+ <<
80
+ /Author (\(anonymous\)) /CreationDate (D:20260819134834+08'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260819134834+08'00') /Producer (ReportLab PDF Library - \(opensource\))
81
+ /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
82
+ >>
83
+ endobj
84
+ 12 0 obj
85
+ <<
86
+ /Count 5 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R ] /Type /Pages
87
+ >>
88
+ endobj
89
+ 13 0 obj
90
+ <<
91
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 675
92
+ >>
93
+ stream
94
+ Gatn"9on!^&;KZOMYG1m,.3VrKY>b:,V#,9X`EWVMb+>e8o-D/W0$l9CE1(2L.[`"%m(Y)4O6)FTcj')DTk`298_%/$L/jAJVIKeFG=ehH10+TB:L(gO`g*)rFtXHiPGBInL1)`Lr/b9^-;n^K`MYB!7eYj\#3:>e'^_WiO`cYE<]#;00ZJ73P$m8VjD>NmoXZ^Yu=(sZK:*)!U1,jT?t2n[>8`CJGS=k!6(':P/.`D\D:2#a)emo.^(X\B<F*rF+&eSG&N)/etF[B,kOk-$f$<fq$ui'=q3J/G_:Hc`1<h6[H:TZYnF^j;=[t&X8nGE10'XQb(P1#S<Y!b(AkFul[@K/KLqYj0_QFp+$nr*$ganJYX,sqhcGd)87cqcV879GLuIZ@cR41k,D6+.[em+nJFRBn1F%)@&RUs-=gc;dpJRG1JlYGDBp1saZ&ANY(X^<h,roYPFO!i,8mm=8-a5Lp^tp[nEOOU=iOSTVq,(FVe*eJ;6kdPZ>M1&O)\,edAm?P1$7r@gK<<+s#*#7lCf&<4CR[St/3)ud,r\L:l]TrAJk8rtn#sgkZ1_LWk.43V>l(l*oJ3%48)nkU*R:nYr>=e=$HQ"Sc]g7[8.D''D'^`tXfIOa<p?pR@AG$25q]OD`3M:e^,B0Uh!%kiRRs1lh]%#dIKPYZ46Z~>endstream
95
+ endobj
96
+ 14 0 obj
97
+ <<
98
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 852
99
+ >>
100
+ stream
101
+ Gat=hhf%4&&BE](/+*QIVW,Sj<G8dqNRA98/R?H8*dBDr!EW$dfd7a>rQ]+J]*^9lMLpG.^:Wjsc:>^gLGlnM!Uq[pcaOUe&*X:384#l9@$^M8pLn>f3JMbkUG8n`lHr0sJbVOl:I6145_I5[oX9O8B]g>J($O6S)E3,ccKelHfnbSuLEWHhOFD4YIcBs;W3?a(`ZH64)NV8IY4:g<^L$Nq/I\M>$/C<d?r;!iUkIi9f:lkt28!\`2(G(o".5E_G9$$`ao#cMojJPZ'W`'D?GPQ/\k(tu+QnG.NRc8D*#KO9X=tF=Qa4ea@)TJf:/bjm'U;]V0=^m36+(Eo2@Z#+E]&O(3+$7BF:N$f):KS7$D%W+_I<h#817kZ]/+-T7A9g>gZ<5GOaos]nMO"75"t-@pb)tT^)q]4I!H1O(QWg4^D`9ATX!Q*5eBqm+1.(h-eq[WLdQcQ:/OauZ!'B^b_hjA5+4R/N^f0SNb;rHoL[EMQ(scj,1NRu-Y//_U?j;<U)aW[F9.dZ.Kl,k<S2$(mlU*c-ppm98S<,\6A4f_+U"2(UK1SuAqVX(\!lopD0Q"Xjm(*YNB?a/7ilT8/JUm7hc@D=s2o"K-4UVo=R,7,h0!,0_]lb5D%AL.mlTr"P;)noP7TV,lnquT.I?"-KP"<"p&h25[;-JoFj9M.9VLqGI+ciV2Gc))>(!U%a&<ukB&2g"&URh"ofauY6<VM1HE5T118e(l2h@5f*h4^d;pUgbG+ughiMH\5mPQZn7Ta&d<lSMpn<'\pL,4MGY>aO"#eN*\qco@iGe=pM;;cr([W+S'RcCoH9c)]ViNtN[(ct6S6>:WK1oh,TJO&ZWCt5G%~>endstream
102
+ endobj
103
+ 15 0 obj
104
+ <<
105
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 677
106
+ >>
107
+ stream
108
+ Gat=(9okbt&A@7.bU!n$8r='$cs)uWX-NBGgN)M/M[P9L_KKZUM>#hN$m]]$fi3h[+_Bg3c+KAJ"<ot(Io[3?@fc$^'-J!H#"ULFTX]WD@e+;$7cZ=W1f"^B[5j(7Se@F>GX:p(k"/8+AIPH,+;$VS,F_)-$7,"a1X?te@h!Y.1FR]=n\u)b_K.adNJ9`KZ/K8D?fH7#V.YcMCXaHT6EoaBOH;dHXL+3q_1rt6cJJ;ZH?DA7J7?bhC?qN2@'&4)@lYCO.OVU/a=pqXa%O=sTVQ<"&)Gm3dBoF[Sf8.KF/%K2,`+<.^'2F@qB.^F"1mj"[:%"U2.m2j8RGr,#0"#oW"H,U0A8hClYThbLg)=;MMLpGGB(9Ec*%^+&$U[nI)hMYTW;gC%+^,aQqTe)YX&YEn$mgcn,;\Aq-mINJV:Cs/"'iuB0SEeHf<PkXhu6tY*!X4[*t<.lpV^+3PF:'$sa\cMSnL4rI4eW,N9su$OuS(k68t&]Js)G/VaX[HrrO\^&8Gb8D%'moqjEd.]o5gSaTfa^<!HYStkjP_YRD%D4/`KfluPN)"W%TL)M<)oHaT[YQ$$J74-;PU<3(kYZ_6T>.%eTdF@,:F&(5;#0$=EHN70q.b<8rSYF+E73`!?rnjA?L`Zq^Atut5n&+PT-jd674^7GC&km`VYIno1~>endstream
109
+ endobj
110
+ 16 0 obj
111
+ <<
112
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 597
113
+ >>
114
+ stream
115
+ GatU.bAQ&g&A7ljk#BueTnnpaa#t,-P6MI-3,#;l4m54*CR_:L3d`3the-Z8DB;8DTHGA>gteBhi<lBn`9(:5^m>[(k5muES3u@/Nq*fibMHN#lnZW%-mf*Fo,VJ%4TLhU&BD'6kZgFg&L%gdQkTUYbXQW9,KVa(NAK2jqIdh4DLi<XBj0>,kH4=/F9@c:-pL2Pn%;s^([2lNVMqV#SaX,<aIR3LA3+!Fc#J<7Ind=b>O!,A^^X*tpp;4"W8<d3;(`1K=^K!k0QUJn:TpoSIR6-;1e#$,g3$/VRQl5pQ%S@p44\IKSI(>NoAE^hbWT*>>cPEoaas6bL^Xe^^Kg?4Er";XIoe7*q);K`^GRXt[F1A;q*Wl6Ah-q?\&%m-($/W1$^rq^DCct?h`Er6^LSdo#0SUlh?(CR@,dS\BcCZq_i?>'Z#/7`=#/*.os'G5k&SUN2E:M0ic@3k_%$;G'U=o:84HT+a"^g.hDK:0OoJj!``g[)G"<.(N?tuTbUlOs=)-30IV;(+rsFa/UtFD/2%*&0%`FGrWig('eSI\Dk-qM$IiRm9A@5jjpQ.C=L0MZ+HMIUhpd<E3NBlq=U@B"7~>endstream
116
+ endobj
117
+ 17 0 obj
118
+ <<
119
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 554
120
+ >>
121
+ stream
122
+ Gat=f;/b/B'SYH?($C]Z,E&*LKh']L%;?O!,&OM1Z%LfLB!tj]^Yb#Z<C\D`9hlnu`]E9E^6^">`T"AlJ<)!?Ht$J'V#ni(*9A!\b/',>4.X#%UR2a;Qtcq#fq`"HG]q=Z3KcL7fhD\&$qQ2fBTlUi\f@f-]&R+>38Ntlr@)NHU4(Ie_9-L+r&^.(.'IsK#?lU<([9?CPru&=JfWhDZI(#dD:5q!K-<m^BM91k%In9c%;P:2^%]57o)<gCSq"ee%U3%6g"&(8#QGjfdMXMXCaH`3N)mM&-=HWa)el+r!O_9qn\L&)lehV-D6TD0kBHO^r?t;VKW1_9-]D`7fX)nOPRIfD4Xf./jC7cZYL2sbana]Pe+Qoa4F'OP.>:`LSE^[U:->oi!l6dXE#id<NScf(-+(QZ[),fBJE?e5S%XM]918uUDcaZnk\j8agGZoM:2g!SBqqcq7*sF-g7bh.Y*D;e%W`6URU1S/YNGa>j9:Riq,HOtWFd!fU#p?nB69&4iFTH!cIVkB?m34]Y58@NeRHHq?B!<sSTA6/$`>3<'*~>endstream
123
+ endobj
124
+ xref
125
+ 0 18
126
+ 0000000000 65535 f
127
+ 0000000061 00000 n
128
+ 0000000112 00000 n
129
+ 0000000219 00000 n
130
+ 0000000331 00000 n
131
+ 0000000526 00000 n
132
+ 0000000721 00000 n
133
+ 0000000798 00000 n
134
+ 0000000993 00000 n
135
+ 0000001188 00000 n
136
+ 0000001383 00000 n
137
+ 0000001453 00000 n
138
+ 0000001734 00000 n
139
+ 0000001818 00000 n
140
+ 0000002584 00000 n
141
+ 0000003527 00000 n
142
+ 0000004295 00000 n
143
+ 0000004983 00000 n
144
+ trailer
145
+ <<
146
+ /ID
147
+ [<c41aa1ae0e9b9080f699bc9a3fa57e7c><c41aa1ae0e9b9080f699bc9a3fa57e7c>]
148
+ % ReportLab generated PDF document -- digest (opensource)
149
+
150
+ /Info 11 0 R
151
+ /Root 10 0 R
152
+ /Size 18
153
+ >>
154
+ startxref
155
+ 5628
156
+ %%EOF
@@ -0,0 +1,265 @@
1
+ """Pydantic output models for field-training learning packages, assessments, and role-play.
2
+
3
+ Every generated content piece traces back to a specific source document and
4
+ page/slide via SourceReference, ensuring compliance with approved messaging.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
10
+
11
+
12
+ class StrictTrainingModel(BaseModel):
13
+ model_config = ConfigDict(extra="forbid")
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Document content store
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ class DocumentPage(StrictTrainingModel):
22
+ """A single page (PDF) or slide (PPTX) of extracted content."""
23
+
24
+ page_number: int = Field(description="1-indexed page or slide number")
25
+ text: str = Field(description="Extracted body text from the page or slide")
26
+ page_type: str = Field(description="Content type: 'page' (PDF) or 'slide' (PPTX)")
27
+ slide_title: str | None = Field(default=None, description="Slide title shape text (PPTX only)")
28
+ speaker_notes: str | None = Field(default=None, description="Presenter notes attached to the slide (PPTX only)")
29
+
30
+
31
+ class IngestedDocument(StrictTrainingModel):
32
+ """A fully ingested document stored in the content index."""
33
+
34
+ document_id: str = Field(description="Stable identifier derived from the file path")
35
+ file_name: str = Field(description="Original file name")
36
+ file_path: str = Field(description="Absolute path to the source file")
37
+ file_type: str = Field(description="File format: 'pdf' or 'pptx'")
38
+ title: str | None = Field(default=None, description="Extracted document title")
39
+ total_pages: int = Field(description="Total number of pages or slides")
40
+ pages: list[DocumentPage] = Field(description="Page-level extracted content")
41
+ ingested_at: str = Field(description="ISO-8601 timestamp of ingestion")
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Source reference (shared across all outputs)
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ class SourceReference(StrictTrainingModel):
50
+ """A traceable reference to a specific passage in an approved source document."""
51
+
52
+ document_id: str = Field(description="ID of the source document in the content store")
53
+ document_name: str = Field(description="File name of the source document")
54
+ page_number: int = Field(description="Page or slide number where the content appears")
55
+ excerpt: str = Field(description="Relevant passage from the source supporting the claim")
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Learning package
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ class KeyMessage(StrictTrainingModel):
64
+ """An approved key message extracted from source documents."""
65
+
66
+ message: str = Field(description="The key message or approved claim")
67
+ category: str = Field(description="Message category: efficacy, safety, dosing, MOA, differentiation, etc.")
68
+ sources: list[SourceReference] = Field(
69
+ min_length=1,
70
+ description="Source references supporting this message",
71
+ )
72
+
73
+
74
+ class LearningObjective(StrictTrainingModel):
75
+ """A learning objective for a training module."""
76
+
77
+ objective: str = Field(description="What the learner should be able to do after training")
78
+ bloom_level: str = Field(description="Bloom's taxonomy level: 'remember', 'understand', or 'apply'")
79
+
80
+
81
+ class TalkingPoint(StrictTrainingModel):
82
+ """A situation-response pair for field rep conversations."""
83
+
84
+ situation: str = Field(description="The conversational context, e.g. 'When an HCP asks about efficacy...'")
85
+ approved_response: str = Field(description="The approved response the rep should deliver")
86
+ supporting_data: str | None = Field(default=None, description="Key data points that support the response")
87
+ sources: list[SourceReference] = Field(
88
+ min_length=1,
89
+ description="Source references grounding the approved response",
90
+ )
91
+
92
+
93
+ class Objection(StrictTrainingModel):
94
+ """A common HCP objection with an approved response."""
95
+
96
+ objection: str = Field(description="The objection an HCP might raise, e.g. 'What about the cardiac safety signal?'")
97
+ approved_response: str = Field(description="The approved response addressing the objection")
98
+ sources: list[SourceReference] = Field(
99
+ min_length=1,
100
+ description="Source references grounding the response",
101
+ )
102
+
103
+
104
+ class LearningModule(StrictTrainingModel):
105
+ """A single training module within a learning package."""
106
+
107
+ title: str = Field(description="Module title")
108
+ product: str | None = Field(default=None, description="Product name if product-specific")
109
+ therapeutic_area: str | None = Field(default=None, description="Therapeutic area covered by the module")
110
+ objectives: list[LearningObjective] = Field(description="Learning objectives for this module")
111
+ key_messages: list[KeyMessage] = Field(description="Approved key messages the rep must know")
112
+ talking_points: list[TalkingPoint] = Field(description="Situation-response talking points for HCP conversations")
113
+ common_objections: list[Objection] = Field(description="Common HCP objections with approved responses")
114
+
115
+
116
+ class LearningPackage(StrictTrainingModel):
117
+ """A complete source-grounded learning package for field reps."""
118
+
119
+ title: str = Field(description="Package title")
120
+ modules: list[LearningModule] = Field(description="Training modules")
121
+ source_documents: list[str] = Field(description="Names of all source documents used to build the package")
122
+ generated_at: str = Field(description="ISO-8601 timestamp of generation")
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Assessment
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ class MCQOption(StrictTrainingModel):
131
+ """A single option in a multiple-choice question."""
132
+
133
+ label: str = Field(description="Option label: A, B, C, or D")
134
+ text: str = Field(description="Option text")
135
+
136
+
137
+ class MCQuestion(StrictTrainingModel):
138
+ """A multiple-choice knowledge check question."""
139
+
140
+ question_id: str = Field(description="Unique question identifier")
141
+ question: str = Field(description="The question text")
142
+ options: list[MCQOption] = Field(description="Answer options (typically 4)")
143
+ correct_answer: str = Field(description="Label of the correct option (e.g. 'B')")
144
+ explanation: str = Field(description="Explanation of why the correct answer is right, citing the source")
145
+ source: SourceReference = Field(description="Source reference for the correct answer")
146
+ difficulty: str = Field(description="Difficulty level: 'easy', 'medium', or 'hard'")
147
+
148
+
149
+ class ScenarioQuestion(StrictTrainingModel):
150
+ """A scenario-based question simulating an HCP interaction."""
151
+
152
+ question_id: str = Field(description="Unique question identifier")
153
+ scenario: str = Field(description="The scenario description, e.g. 'A cardiologist asks you about...'")
154
+ hcp_persona: str = Field(description="HCP persona details, e.g. 'Skeptical oncologist, concerned about toxicity'")
155
+ ideal_response_points: list[str] = Field(description="Key points that an ideal response should cover")
156
+ sources: list[SourceReference] = Field(
157
+ min_length=1,
158
+ description="Source references for the ideal response points",
159
+ )
160
+ difficulty: str = Field(description="Difficulty level: 'easy', 'medium', or 'hard'")
161
+
162
+
163
+ class Assessment(StrictTrainingModel):
164
+ """A complete assessment with MCQ and scenario-based questions."""
165
+
166
+ title: str = Field(description="Assessment title")
167
+ mcq_questions: list[MCQuestion] = Field(description="Multiple-choice knowledge check questions")
168
+ scenario_questions: list[ScenarioQuestion] = Field(description="Scenario-based application questions")
169
+ total_points: int = Field(description="Total points available")
170
+ passing_score_pct: float = Field(description="Passing threshold as a fraction, e.g. 0.8 for 80%")
171
+ source_documents: list[str] = Field(description="Names of source documents the assessment is based on")
172
+ generated_at: str = Field(description="ISO-8601 timestamp of generation")
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Role-play
177
+ # ---------------------------------------------------------------------------
178
+
179
+
180
+ class RoleplayTurn(StrictTrainingModel):
181
+ """A single turn in an interactive role-play conversation."""
182
+
183
+ speaker: str = Field(description="Who spoke: 'hcp' or 'rep'")
184
+ message: str = Field(description="What was said in this turn")
185
+
186
+
187
+ class ClaimEvaluation(StrictTrainingModel):
188
+ """Evaluation of a single claim made during role-play."""
189
+
190
+ claim: str = Field(description="The claim or statement being evaluated")
191
+ status: str = Field(description="Evaluation status: 'correct', 'missed', 'unsupported', or 'inaccurate'")
192
+ source: SourceReference | None = Field(
193
+ default=None,
194
+ description="Source reference if the claim is correct or has a known approved version",
195
+ )
196
+ feedback: str = Field(description="Specific feedback explaining the evaluation")
197
+
198
+
199
+ class RoleplayScorecard(StrictTrainingModel):
200
+ """Post-session scorecard summarizing role-play performance."""
201
+
202
+ hcp_persona: str = Field(description="The HCP persona used in the role-play")
203
+ topic: str = Field(description="Product or topic discussed")
204
+ turns: list[RoleplayTurn] = Field(description="Full conversation transcript")
205
+ claims_evaluated: list[ClaimEvaluation] = Field(description="Evaluation of each claim made by the rep")
206
+ score: float = Field(ge=0.0, le=1.0, description="Overall score from 0.0 to 1.0")
207
+ strengths: list[str] = Field(description="What the rep did well")
208
+ areas_for_improvement: list[str] = Field(description="Specific areas where the rep can improve")
209
+ source_documents: list[str] = Field(description="Source documents used for evaluation")
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Pre-session role-play kit
214
+ # ---------------------------------------------------------------------------
215
+
216
+
217
+ class FacilitatorPrompt(StrictTrainingModel):
218
+ """A facilitator cue for one stage of a role-play session."""
219
+
220
+ stage: str = Field(description="Session stage, such as opening, probe, objection, or close")
221
+ prompt: str = Field(description="Prompt the facilitator delivers to the representative")
222
+ coaching_intent: str = Field(description="Behavior or skill the facilitator should observe")
223
+
224
+
225
+ class EvaluationCriterion(StrictTrainingModel):
226
+ """One weighted criterion in the pre-session evaluation rubric."""
227
+
228
+ criterion: str = Field(description="Capability being evaluated")
229
+ weight_pct: int = Field(ge=1, le=100, description="Percentage weight for this criterion")
230
+ evidence_to_observe: list[str] = Field(description="Observable behaviors that demonstrate the criterion")
231
+
232
+
233
+ class RoleplayKit(StrictTrainingModel):
234
+ """A source-grounded kit used to facilitate and assess a future role-play."""
235
+
236
+ title: str = Field(description="Role-play kit title")
237
+ topic: str = Field(description="Product or approved-message topic")
238
+ hcp_persona: str = Field(description="HCP persona used in the scenario")
239
+ scenario: str = Field(description="Situation the representative will practice")
240
+ objectives: list[LearningObjective] = Field(min_length=1, description="Session learning objectives")
241
+ key_messages: list[KeyMessage] = Field(
242
+ min_length=1,
243
+ description="Approved messages the representative should communicate",
244
+ )
245
+ common_objections: list[Objection] = Field(
246
+ min_length=1,
247
+ description="Likely objections with approved responses",
248
+ )
249
+ facilitator_prompts: list[FacilitatorPrompt] = Field(
250
+ min_length=1,
251
+ description="Prompts that guide the practice conversation",
252
+ )
253
+ evaluation_rubric: list[EvaluationCriterion] = Field(
254
+ min_length=1,
255
+ description="Weighted evaluation criteria",
256
+ )
257
+ source_documents: list[str] = Field(min_length=1, description="Names of all approved documents used")
258
+ generated_at: str = Field(description="ISO-8601 timestamp of generation")
259
+
260
+ @model_validator(mode="after")
261
+ def rubric_totals_100_percent(self) -> RoleplayKit:
262
+ total = sum(item.weight_pct for item in self.evaluation_rubric)
263
+ if total != 100:
264
+ raise ValueError(f"evaluation_rubric weight_pct values must total 100, got {total}")
265
+ return self
File without changes
@@ -0,0 +1,67 @@
1
+ """get_document_page — retrieve the full text of a specific page or slide."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class GetDocumentPageArgs(BaseModel):
11
+ document_id: str = Field(description="Document ID (from list_documents or search_content results)")
12
+ page_number: int = Field(ge=1, description="1-indexed page or slide number")
13
+
14
+
15
+ TOOL: dict[str, Any] = {
16
+ "name": "get_document_page",
17
+ "description": (
18
+ "Get the full text of a specific page (PDF) or slide (PPTX) from an "
19
+ "ingested document. Returns the complete text content, slide title, and "
20
+ "speaker notes (for PPTX). Use this for precise citation when building "
21
+ "learning content or verifying a claim found via search_content."
22
+ ),
23
+ "args": GetDocumentPageArgs,
24
+ }
25
+
26
+
27
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
28
+ import json
29
+
30
+ from .._content_store import load_document
31
+
32
+ document_id = arguments["document_id"]
33
+ page_number = arguments["page_number"]
34
+
35
+ doc = load_document(document_id)
36
+ if doc is None:
37
+ return [{"type": "text", "text": json.dumps({"error": f"Document '{document_id}' not found"})}]
38
+
39
+ page = None
40
+ for p in doc.get("pages", []):
41
+ if p["page_number"] == page_number:
42
+ page = p
43
+ break
44
+
45
+ if page is None:
46
+ return [
47
+ {
48
+ "type": "text",
49
+ "text": json.dumps(
50
+ {
51
+ "error": f"Page {page_number} not found in document '{document_id}' (total pages: {doc.get('total_pages', 0)})"
52
+ }
53
+ ),
54
+ }
55
+ ]
56
+
57
+ output = {
58
+ "document_id": document_id,
59
+ "document_name": doc["file_name"],
60
+ "document_title": doc.get("title", ""),
61
+ "page_number": page["page_number"],
62
+ "page_type": page["page_type"],
63
+ "text": page["text"],
64
+ "slide_title": page.get("slide_title"),
65
+ "speaker_notes": page.get("speaker_notes"),
66
+ }
67
+ return [{"type": "text", "text": json.dumps(output, indent=2)}]
@@ -0,0 +1,147 @@
1
+ """ingest_document — extract structured text from a PDF or PPTX file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class IngestDocumentArgs(BaseModel):
11
+ file_path: str = Field(description="Absolute path to the PDF or PPTX file to ingest")
12
+ document_title: str | None = Field(
13
+ default=None,
14
+ description="Optional title override. If not provided, the title is extracted from the first page/slide.",
15
+ )
16
+
17
+
18
+ TOOL: dict[str, Any] = {
19
+ "name": "ingest_document",
20
+ "description": (
21
+ "Extract structured text from a PDF or PPTX file and store it in the "
22
+ "training content index. Returns a summary with the document ID, title, "
23
+ "page count, and file type. For PPTX files, extracts slide titles, body "
24
+ "text, and speaker notes per slide. For PDF files, extracts the embedded "
25
+ "text layer per page. Use this before search_content or get_document_page."
26
+ ),
27
+ "args": IngestDocumentArgs,
28
+ }
29
+
30
+
31
+ def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
32
+ import json
33
+ import os
34
+ from datetime import datetime, timezone
35
+
36
+ from .._content_store import document_id_from_path, save_document
37
+
38
+ file_path = arguments["file_path"]
39
+ document_title = arguments.get("document_title")
40
+
41
+ if not os.path.isfile(file_path):
42
+ return [{"type": "text", "text": json.dumps({"error": f"File not found: {file_path}"})}]
43
+
44
+ ext = os.path.splitext(file_path)[1].lower()
45
+
46
+ if ext == ".pdf":
47
+ pages, auto_title = _extract_pdf(file_path)
48
+ file_type = "pdf"
49
+ elif ext in (".pptx", ".ppt"):
50
+ pages, auto_title = _extract_pptx(file_path)
51
+ file_type = "pptx"
52
+ else:
53
+ return [{"type": "text", "text": json.dumps({"error": f"Unsupported file type: {ext}. Use .pdf or .pptx"})}]
54
+
55
+ pages = [p for p in pages if p.get("text", "").strip()]
56
+
57
+ title = document_title or auto_title or os.path.basename(file_path)
58
+ doc_id = document_id_from_path(file_path)
59
+
60
+ doc = {
61
+ "document_id": doc_id,
62
+ "file_name": os.path.basename(file_path),
63
+ "file_path": os.path.abspath(file_path),
64
+ "file_type": file_type,
65
+ "title": title,
66
+ "total_pages": len(pages),
67
+ "pages": pages,
68
+ "ingested_at": datetime.now(timezone.utc).isoformat(),
69
+ }
70
+
71
+ save_document(doc)
72
+
73
+ summary = {
74
+ "document_id": doc_id,
75
+ "file_name": doc["file_name"],
76
+ "file_type": file_type,
77
+ "title": title,
78
+ "total_pages": len(pages),
79
+ "ingested_at": doc["ingested_at"],
80
+ }
81
+ return [{"type": "text", "text": json.dumps(summary, indent=2)}]
82
+
83
+
84
+ def _extract_pdf(file_path: str) -> tuple[list[dict], str | None]:
85
+ import pypdfium2 as pdfium
86
+
87
+ doc = pdfium.PdfDocument(file_path)
88
+ pages: list[dict] = []
89
+ auto_title: str | None = None
90
+
91
+ for i in range(len(doc)):
92
+ textpage = doc[i].get_textpage()
93
+ text = textpage.get_text_bounded()
94
+ textpage.close()
95
+ pages.append(
96
+ {
97
+ "page_number": i + 1,
98
+ "text": text,
99
+ "page_type": "page",
100
+ "slide_title": None,
101
+ "speaker_notes": None,
102
+ }
103
+ )
104
+ if i == 0 and text.strip():
105
+ first_line = text.strip().split("\n")[0].strip()
106
+ if first_line and len(first_line) < 200:
107
+ auto_title = first_line
108
+
109
+ doc.close()
110
+ return pages, auto_title
111
+
112
+
113
+ def _extract_pptx(file_path: str) -> tuple[list[dict], str | None]:
114
+ from pptx import Presentation
115
+
116
+ prs = Presentation(file_path)
117
+ pages: list[dict] = []
118
+ auto_title: str | None = None
119
+
120
+ for i, slide in enumerate(prs.slides):
121
+ title = ""
122
+ if slide.shapes.title:
123
+ title = slide.shapes.title.text
124
+
125
+ body_texts: list[str] = []
126
+ for shape in slide.shapes:
127
+ if shape.has_text_frame:
128
+ body_texts.append(shape.text_frame.text)
129
+
130
+ notes = ""
131
+ if slide.has_notes_slide and slide.notes_slide.notes_text_frame:
132
+ notes = slide.notes_slide.notes_text_frame.text
133
+
134
+ pages.append(
135
+ {
136
+ "page_number": i + 1,
137
+ "text": "\n".join(body_texts),
138
+ "page_type": "slide",
139
+ "slide_title": title or None,
140
+ "speaker_notes": notes or None,
141
+ }
142
+ )
143
+
144
+ if i == 0 and title:
145
+ auto_title = title
146
+
147
+ return pages, auto_title