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
mcp_framework.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""Shared MCP-server framework for open-pharma-plugins, built on the SDK's FastMCP.
|
|
2
|
+
|
|
3
|
+
A server is just its tool subpackages plus a thin `__init__.py` (`__version__`, the discovered
|
|
4
|
+
`SPECS`, a few optional hooks). This module provides what every server would otherwise duplicate:
|
|
5
|
+
|
|
6
|
+
- build_registry(import_name, tool_packages) — auto-discover TOOL+handle modules
|
|
7
|
+
- serve(...) — bridge tools onto FastMCP + run stdio
|
|
8
|
+
- run_main(import_name) — console / `python3 <dir>` entry dispatcher
|
|
9
|
+
- tool_schema(model) — Pydantic model -> advertised inputSchema
|
|
10
|
+
|
|
11
|
+
Each tool module exports `TOOL = {"name", "description", "args": <PydanticModel>}` + `handle(dict)`.
|
|
12
|
+
Depends only on the `mcp` SDK (bundles FastMCP + pydantic) + anyio, so servers stay independent.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
__version__ = "2.2.0" # distribution/release-train version; plugin versions are per capability
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import importlib
|
|
21
|
+
import importlib.util
|
|
22
|
+
import inspect
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import pkgutil
|
|
27
|
+
import shutil
|
|
28
|
+
import signal
|
|
29
|
+
import sys
|
|
30
|
+
import warnings
|
|
31
|
+
from typing import Annotated
|
|
32
|
+
|
|
33
|
+
import anyio
|
|
34
|
+
from pydantic.json_schema import GenerateJsonSchema
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"build_registry",
|
|
38
|
+
"serve",
|
|
39
|
+
"run_main",
|
|
40
|
+
"tool_schema",
|
|
41
|
+
"system_report",
|
|
42
|
+
"system_startup_warnings",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Schema: Pydantic model -> the JSON inputSchema advertised to the model.
|
|
47
|
+
class _ToolSchemaGen(GenerateJsonSchema):
|
|
48
|
+
"""Emit LLM-clean tool schemas straight from pydantic, no post-hoc rewriting: drop auto-generated
|
|
49
|
+
`title`s (field- and class-level), unwrap `Optional`'s null branch, and omit `default: null`.
|
|
50
|
+
`field_title_should_be_set`/`nullable_schema`/`default_schema` are documented override points;
|
|
51
|
+
`_update_class_schema` (nested class titles) is private with no public equivalent, so the
|
|
52
|
+
tool_schema tests assert the output stays title-free across pydantic upgrades. Paired with
|
|
53
|
+
union_format='primitive_type_array' so `float | str` -> {"type": ["number", "string"]}."""
|
|
54
|
+
|
|
55
|
+
def field_title_should_be_set(self, schema) -> bool:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _update_class_schema(self, json_schema, *args, **kwargs) -> None:
|
|
59
|
+
super()._update_class_schema(json_schema, *args, **kwargs)
|
|
60
|
+
json_schema.pop("title", None)
|
|
61
|
+
|
|
62
|
+
def nullable_schema(self, schema) -> dict:
|
|
63
|
+
return self.generate_inner(schema["schema"]) # Optional[X] -> X, drop the anyOf null branch
|
|
64
|
+
|
|
65
|
+
def default_schema(self, schema) -> dict:
|
|
66
|
+
js = super().default_schema(schema)
|
|
67
|
+
if js.get("default", ...) is None:
|
|
68
|
+
js.pop("default", None) # omit `default: null`; real defaults are kept
|
|
69
|
+
return js
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def tool_schema(model) -> dict:
|
|
73
|
+
"""Build the advertised MCP inputSchema from a Pydantic model: generated title-free with
|
|
74
|
+
Optional/`default: null` already stripped (see _ToolSchemaGen), then inline `$ref`/`$defs`
|
|
75
|
+
and drop the top-level model description. Per-property `description`s are kept."""
|
|
76
|
+
schema = _inline_defs(model.model_json_schema(schema_generator=_ToolSchemaGen, union_format="primitive_type_array"))
|
|
77
|
+
schema.pop("description", None)
|
|
78
|
+
return schema
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _inline_defs(schema: dict) -> dict:
|
|
82
|
+
"""Replace every `#/$defs/*` `$ref` with the referenced definition (inlined) and drop the
|
|
83
|
+
`$defs` block."""
|
|
84
|
+
defs = schema.pop("$defs", None)
|
|
85
|
+
if not defs:
|
|
86
|
+
return schema
|
|
87
|
+
|
|
88
|
+
def walk(node):
|
|
89
|
+
if isinstance(node, dict):
|
|
90
|
+
ref = node.get("$ref")
|
|
91
|
+
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
|
92
|
+
target = defs[ref.split("/")[-1]]
|
|
93
|
+
sibling = {k: v for k, v in node.items() if k != "$ref"}
|
|
94
|
+
return walk({**target, **sibling})
|
|
95
|
+
return {k: walk(v) for k, v in node.items()}
|
|
96
|
+
if isinstance(node, list):
|
|
97
|
+
return [walk(v) for v in node]
|
|
98
|
+
return node
|
|
99
|
+
|
|
100
|
+
return walk(schema)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Discovery: scan subpackages for tool modules (TOOL dict + handle fn).
|
|
104
|
+
class ToolSpec:
|
|
105
|
+
"""One discovered tool: its advertised schema, Pydantic args model, and handler."""
|
|
106
|
+
|
|
107
|
+
__slots__ = ("name", "description", "input_schema", "args_model", "handle")
|
|
108
|
+
|
|
109
|
+
def __init__(self, name, description, input_schema, args_model, handle):
|
|
110
|
+
self.name = name
|
|
111
|
+
self.description = description
|
|
112
|
+
self.input_schema = input_schema
|
|
113
|
+
self.args_model = args_model
|
|
114
|
+
self.handle = handle
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def meta(self) -> dict:
|
|
118
|
+
return {"name": self.name, "description": self.description, "inputSchema": self.input_schema}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_registry(import_name: str, tool_packages: list[str]):
|
|
122
|
+
"""Auto-discover tool modules (exporting TOOL + handle) under the given subpackages of
|
|
123
|
+
`import_name`. Returns (specs, get_handler, list_tools): a list of ToolSpec, a
|
|
124
|
+
`get_handler(name) -> handle | None`, and a `list_tools() -> [tool meta dict]`. Raises on
|
|
125
|
+
a module that exports only one of TOOL/handle (a typo that would otherwise vanish
|
|
126
|
+
silently) or on a duplicate tool name (an ambiguous registry)."""
|
|
127
|
+
specs: list[ToolSpec] = []
|
|
128
|
+
seen: dict[str, str] = {}
|
|
129
|
+
for pkg_name in tool_packages:
|
|
130
|
+
pkg = importlib.import_module(f"{import_name}.{pkg_name}")
|
|
131
|
+
for info in pkgutil.iter_modules(pkg.__path__):
|
|
132
|
+
full = f"{import_name}.{pkg_name}.{info.name}"
|
|
133
|
+
mod = importlib.import_module(full)
|
|
134
|
+
has_tool, has_handle = hasattr(mod, "TOOL"), hasattr(mod, "handle")
|
|
135
|
+
if has_tool != has_handle:
|
|
136
|
+
missing, present = ("handle", "TOOL") if has_tool else ("TOOL", "handle")
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"{full} exports {present} but not {missing}: a tool module must export both "
|
|
139
|
+
"(or neither, for a helper module)."
|
|
140
|
+
)
|
|
141
|
+
if not has_tool:
|
|
142
|
+
continue
|
|
143
|
+
spec = _spec_from_module(mod)
|
|
144
|
+
if spec.name in seen:
|
|
145
|
+
raise RuntimeError(f"duplicate tool name {spec.name!r}: defined in {seen[spec.name]} and {full}.")
|
|
146
|
+
seen[spec.name] = full
|
|
147
|
+
specs.append(spec)
|
|
148
|
+
handlers = {s.name: s.handle for s in specs}
|
|
149
|
+
|
|
150
|
+
def list_tools() -> list[dict]:
|
|
151
|
+
return [s.meta for s in specs]
|
|
152
|
+
|
|
153
|
+
return specs, handlers.get, list_tools
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _spec_from_module(mod) -> ToolSpec:
|
|
157
|
+
t = mod.TOOL
|
|
158
|
+
model = t["args"]
|
|
159
|
+
return ToolSpec(t["name"], t.get("description", ""), tool_schema(model), model, mod.handle)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Runtime: bridge specs onto FastMCP and run over stdio.
|
|
163
|
+
def _to_content_block(block: dict):
|
|
164
|
+
"""Convert a handler's raw content dict into an SDK content block (text/image;
|
|
165
|
+
anything else falls back to JSON text so a stray block can't crash a call)."""
|
|
166
|
+
import mcp.types as types
|
|
167
|
+
|
|
168
|
+
btype = block.get("type")
|
|
169
|
+
if btype == "image" and "data" in block:
|
|
170
|
+
# Codex MCP image-detail extension ("original" instead of its default "high"):
|
|
171
|
+
# https://github.com/openai/codex/blob/44cb66e4edc061d39ae38de949b47f6f94416553/codex-rs/protocol/src/models.rs#L2106-L2187
|
|
172
|
+
return types.ImageContent(
|
|
173
|
+
type="image",
|
|
174
|
+
data=block["data"],
|
|
175
|
+
mimeType=block.get("mimeType", "image/jpeg"),
|
|
176
|
+
_meta={"codex/imageDetail": "original"},
|
|
177
|
+
)
|
|
178
|
+
if btype == "text":
|
|
179
|
+
return types.TextContent(type="text", text=block.get("text", ""))
|
|
180
|
+
return types.TextContent(type="text", text=json.dumps(block, ensure_ascii=False))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def _run_handle(handle, arguments: dict):
|
|
184
|
+
raw = await anyio.to_thread.run_sync(lambda: handle(arguments))
|
|
185
|
+
return [_to_content_block(b) for b in raw]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _make_wrapper(spec: ToolSpec):
|
|
189
|
+
"""Synthesize the async tool fn FastMCP registers: its signature (from the Pydantic
|
|
190
|
+
args model) drives schema-generation + validation; its body dumps the validated model
|
|
191
|
+
to a plain dict, runs the handle off the event loop, and converts the result."""
|
|
192
|
+
handle = spec.handle
|
|
193
|
+
model = spec.args_model
|
|
194
|
+
params = [
|
|
195
|
+
inspect.Parameter(
|
|
196
|
+
name,
|
|
197
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
198
|
+
default=(inspect.Parameter.empty if field.is_required() else field.default),
|
|
199
|
+
annotation=Annotated[field.annotation, field],
|
|
200
|
+
)
|
|
201
|
+
for name, field in model.model_fields.items()
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
async def wrapper(**kwargs):
|
|
205
|
+
# validated model -> plain dict for the handle(dict) API
|
|
206
|
+
return await _run_handle(handle, model(**kwargs).model_dump(exclude_none=True))
|
|
207
|
+
|
|
208
|
+
wrapper.__signature__ = inspect.Signature(params)
|
|
209
|
+
wrapper.__name__ = spec.name
|
|
210
|
+
return wrapper
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def serve(server_name, version, specs, *, transport=None, on_start=None):
|
|
214
|
+
"""Run an MCP stdio server bridging discovered tool specs onto FastMCP.
|
|
215
|
+
|
|
216
|
+
FastMCP validates inputs against each spec's args model; we override the advertised schema with
|
|
217
|
+
our normalized `tool_schema`. `transport` is an optional (read, write) context-manager factory
|
|
218
|
+
(defaults to the SDK stdio_server; the vision server passes its streaming writer); `on_start`
|
|
219
|
+
runs once after the transport opens."""
|
|
220
|
+
from mcp.server.fastmcp import FastMCP
|
|
221
|
+
from mcp.server.stdio import stdio_server
|
|
222
|
+
|
|
223
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stderr)
|
|
224
|
+
# Ignore SIGPIPE: a client disconnect surfaces as an exception (handled in run_main),
|
|
225
|
+
# not a process kill. Unix-only.
|
|
226
|
+
if hasattr(signal, "SIGPIPE"):
|
|
227
|
+
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
|
228
|
+
|
|
229
|
+
mcp = FastMCP(server_name)
|
|
230
|
+
mcp._mcp_server.version = version # reported in the initialize handshake
|
|
231
|
+
with warnings.catch_warnings():
|
|
232
|
+
# wrapper defaults aren't JSON-serializable; we override .parameters below, so silence it.
|
|
233
|
+
warnings.filterwarnings("ignore", message="Default value .* is not JSON serializable")
|
|
234
|
+
for spec in specs:
|
|
235
|
+
mcp.add_tool(_make_wrapper(spec), name=spec.name, description=spec.description, structured_output=False)
|
|
236
|
+
mcp._tool_manager.get_tool(spec.name).parameters = spec.input_schema
|
|
237
|
+
|
|
238
|
+
low = mcp._mcp_server
|
|
239
|
+
|
|
240
|
+
async def _run():
|
|
241
|
+
tctx = transport or stdio_server
|
|
242
|
+
async with tctx() as (read, write):
|
|
243
|
+
if on_start is not None:
|
|
244
|
+
on_start()
|
|
245
|
+
await low.run(read, write, low.create_initialization_options())
|
|
246
|
+
|
|
247
|
+
asyncio.run(_run())
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# System dependencies: a declarative table -> --check-system report + startup warnings.
|
|
251
|
+
# Some capabilities need a *system* tool pip/uv can't install. A capability declares a SYSTEM_DEPS
|
|
252
|
+
# list (+ optional SYSTEM_DEPS_NOTE); the
|
|
253
|
+
# framework renders --check-system (run_main) and warns at startup about tools missing while their
|
|
254
|
+
# Python extra is installed. Each entry — required: label, tools, hint; the rest are optional and
|
|
255
|
+
# default to the common case (a system binary with no Python gating, warned at startup when missing):
|
|
256
|
+
# label (required) human-readable capability the tool powers
|
|
257
|
+
# tools (required) system binaries (any-of: present if ANY resolves on PATH)
|
|
258
|
+
# hint (required) install command
|
|
259
|
+
# extra pip group that pulls its Python side; omit / None = core, always relevant (shown [core])
|
|
260
|
+
# probe import name to detect that Python side; omit / None = no Python dep (entry always active)
|
|
261
|
+
# startup omit / True = warn at startup when missing; False = report-only (--check-system only)
|
|
262
|
+
def _tool_present(tool: str) -> bool:
|
|
263
|
+
if tool == "__playwright_chromium__":
|
|
264
|
+
# The browser lives in the playwright cache, not on PATH — can't detect it, so report-only.
|
|
265
|
+
return False
|
|
266
|
+
return shutil.which(tool) is not None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _extra_installed(probe) -> bool:
|
|
270
|
+
"""True if the entry's Python side is importable (or it has no Python dep)."""
|
|
271
|
+
return probe is None or importlib.util.find_spec(probe) is not None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _entry_ok(dep: dict) -> bool:
|
|
275
|
+
return any(_tool_present(t) for t in dep["tools"])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def system_report(deps, *, note: str = "") -> str:
|
|
279
|
+
"""Render `--check-system` for a SYSTEM_DEPS table, scoped to installed extras.
|
|
280
|
+
|
|
281
|
+
✓/✗ per entry whose Python extra is importable (plus entries with no Python dep); entries whose
|
|
282
|
+
extra isn't installed collapse into one trailing line. `note` is an optional footer."""
|
|
283
|
+
if not deps:
|
|
284
|
+
return "No system tools required."
|
|
285
|
+
lines = ["System-tool dependency check (pip/uv cannot install these):", ""]
|
|
286
|
+
dormant: list[str] = []
|
|
287
|
+
for dep in deps:
|
|
288
|
+
if not _extra_installed(dep.get("probe")):
|
|
289
|
+
if dep.get("extra"):
|
|
290
|
+
dormant.append(dep["extra"])
|
|
291
|
+
continue
|
|
292
|
+
ok = _entry_ok(dep)
|
|
293
|
+
mark = "✓" if ok else "✗"
|
|
294
|
+
extra = f" [{dep['extra']}]" if dep.get("extra") else " [core]"
|
|
295
|
+
lines.append(f" {mark} {dep['label']}{extra}")
|
|
296
|
+
if not ok:
|
|
297
|
+
real = [t for t in dep["tools"] if t != "__playwright_chromium__"]
|
|
298
|
+
lines.append(f" needs: {' or '.join(real) or 'playwright chromium'}")
|
|
299
|
+
lines.append(f" install: {dep['hint']}")
|
|
300
|
+
if dormant:
|
|
301
|
+
lines.append("")
|
|
302
|
+
lines.append(f" (extras not installed — add to enable: {', '.join(sorted(set(dormant)))})")
|
|
303
|
+
if note:
|
|
304
|
+
lines.append("")
|
|
305
|
+
lines.append(note)
|
|
306
|
+
return "\n".join(lines)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def system_startup_warnings(deps) -> list[str]:
|
|
310
|
+
"""Missing system tools whose Python extra IS installed — what the user will hit at runtime.
|
|
311
|
+
Skips startup=False entries."""
|
|
312
|
+
out = []
|
|
313
|
+
for dep in deps or []:
|
|
314
|
+
if not dep.get("startup", True):
|
|
315
|
+
continue
|
|
316
|
+
if _extra_installed(dep.get("probe")) and not _entry_ok(dep):
|
|
317
|
+
out.append(f"{dep['label']} — install: {dep['hint']}")
|
|
318
|
+
return out
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def usage(import_name: str, note: str = "", *, launchable: bool = False) -> str:
|
|
322
|
+
"""Standard --help / no-tty text for a server, derived from its entry name.
|
|
323
|
+
`note` is the server-specific tail (install hints, required env vars, …).
|
|
324
|
+
`launchable` adds the --launch-app line for capabilities that can start their own app."""
|
|
325
|
+
entry = import_name.replace("_", "-")
|
|
326
|
+
launch_opt = f"Usage: {entry} [--version | --help | --check-system | --setup | --set KEY=VALUE … | --unset KEY …"
|
|
327
|
+
launch_opt += " | --launch-app]\n" if launchable else "]\n"
|
|
328
|
+
text = (
|
|
329
|
+
f"{entry} — MCP server (stdio transport)\n\n"
|
|
330
|
+
f"{launch_opt}"
|
|
331
|
+
" --check-system report system tools pip can't install, + config status\n"
|
|
332
|
+
" --setup interactively write the full config to ~/.open-pharma-plugins/config\n"
|
|
333
|
+
" --set KEY=VALUE non-interactively write config entries (for automation)\n"
|
|
334
|
+
" --unset KEY … non-interactively remove config entries\n"
|
|
335
|
+
)
|
|
336
|
+
if launchable:
|
|
337
|
+
text += (
|
|
338
|
+
" --launch-app bring up the live app (with the bundled addon) this server talks to;\n"
|
|
339
|
+
" pass --launch-app --help for its options\n"
|
|
340
|
+
)
|
|
341
|
+
return f"{text}\n{note}" if note else text
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def config_report(entry: str) -> str:
|
|
345
|
+
"""`--check-system` tail: config location without assuming a model provider."""
|
|
346
|
+
from shared.env import CONFIG_FIELDS, config_file
|
|
347
|
+
|
|
348
|
+
path = config_file()
|
|
349
|
+
lines = [f"User config: {path}" + ("" if os.path.exists(path) else f" (none yet — `{entry} --setup`)")]
|
|
350
|
+
lines.append(f" {len(CONFIG_FIELDS)} capability settings available; run `{entry} --setup` to review them")
|
|
351
|
+
return "\n".join(lines)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _interactive_setup(entry: str) -> None:
|
|
355
|
+
"""Prompt for every catalog field, grouped, and merge the answers into the user config file.
|
|
356
|
+
|
|
357
|
+
Iterates shared.env.CONFIG_FIELDS (the one declarative list) so new vars need no new prompt
|
|
358
|
+
code. Per field: blank keeps the current value, `-` clears it (removed, not written empty)."""
|
|
359
|
+
import getpass
|
|
360
|
+
|
|
361
|
+
from shared.env import CONFIG_FIELDS, config_file, del_config, get_env, set_config
|
|
362
|
+
|
|
363
|
+
print(f"{entry} setup → {config_file()}")
|
|
364
|
+
print("Enter a value, blank to keep the current one, or '-' to clear it. Ctrl-C to abort.")
|
|
365
|
+
values: dict[str, str | None] = {}
|
|
366
|
+
cleared: list[str] = []
|
|
367
|
+
group = None
|
|
368
|
+
try:
|
|
369
|
+
for key, secret, grp, default, desc in CONFIG_FIELDS:
|
|
370
|
+
if grp != group:
|
|
371
|
+
group = grp
|
|
372
|
+
print(f"\n— {grp} —")
|
|
373
|
+
cur = get_env(key)
|
|
374
|
+
if cur:
|
|
375
|
+
shown = "set" if secret else cur
|
|
376
|
+
elif default:
|
|
377
|
+
shown = f"default: {default}"
|
|
378
|
+
else:
|
|
379
|
+
shown = ""
|
|
380
|
+
hint = f" [{shown}]" if shown else ""
|
|
381
|
+
prompt = f" {key} ({desc}){hint}: "
|
|
382
|
+
v = (getpass.getpass(prompt) if secret else input(prompt)).strip()
|
|
383
|
+
if not v:
|
|
384
|
+
continue
|
|
385
|
+
if v == "-":
|
|
386
|
+
cleared.append(key)
|
|
387
|
+
else:
|
|
388
|
+
values[key] = v
|
|
389
|
+
except (EOFError, KeyboardInterrupt):
|
|
390
|
+
print("\naborted.")
|
|
391
|
+
return
|
|
392
|
+
if not values and not cleared:
|
|
393
|
+
print("\nNothing changed.")
|
|
394
|
+
return
|
|
395
|
+
path = set_config(values) if values else config_file()
|
|
396
|
+
if cleared:
|
|
397
|
+
path = del_config(cleared)
|
|
398
|
+
done = sorted(values) + [f"-{k}" for k in cleared]
|
|
399
|
+
print(f"\n✓ saved {', '.join(done)} → {path}")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _check_system_text(pkg) -> str:
|
|
403
|
+
"""`--check-system` text: an explicit check_system() override on the package wins; else
|
|
404
|
+
render its declarative SYSTEM_DEPS (+ optional SYSTEM_DEPS_NOTE footer); else nothing needed."""
|
|
405
|
+
check = getattr(pkg, "check_system", None)
|
|
406
|
+
if check is not None:
|
|
407
|
+
return check()
|
|
408
|
+
deps = getattr(pkg, "SYSTEM_DEPS", None)
|
|
409
|
+
if deps is not None:
|
|
410
|
+
return system_report(deps, note=getattr(pkg, "SYSTEM_DEPS_NOTE", ""))
|
|
411
|
+
return "No system tools required."
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def run_main(import_name: str) -> None:
|
|
415
|
+
"""Console / `python3 <dir>` entry: --version / --check-system / --setup / --help / serve.
|
|
416
|
+
|
|
417
|
+
Reads config off the package: required __version__, SPECS; optional SYSTEM_DEPS (+
|
|
418
|
+
SYSTEM_DEPS_NOTE), check_system() override, on_start(), transport, USAGE_NOTE."""
|
|
419
|
+
pkg = importlib.import_module(import_name)
|
|
420
|
+
argv = sys.argv[1:]
|
|
421
|
+
entry = import_name.replace("_", "-")
|
|
422
|
+
if "--version" in argv:
|
|
423
|
+
print(pkg.__version__)
|
|
424
|
+
return
|
|
425
|
+
if "--check-system" in argv:
|
|
426
|
+
print(_check_system_text(pkg))
|
|
427
|
+
print()
|
|
428
|
+
print(config_report(entry))
|
|
429
|
+
return
|
|
430
|
+
if "--setup" in argv:
|
|
431
|
+
_interactive_setup(entry)
|
|
432
|
+
return
|
|
433
|
+
if "--set" in argv:
|
|
434
|
+
from shared.env import set_config
|
|
435
|
+
|
|
436
|
+
pairs: dict[str, str] = {}
|
|
437
|
+
for a in argv:
|
|
438
|
+
k, sep, v = a.partition("=")
|
|
439
|
+
if sep and not k.startswith("-"):
|
|
440
|
+
pairs[k] = v
|
|
441
|
+
if not pairs:
|
|
442
|
+
print(f"usage: {entry} --set KEY=VALUE [KEY=VALUE …]")
|
|
443
|
+
return
|
|
444
|
+
print(f"✓ wrote {', '.join(sorted(pairs))} → {set_config(pairs)}")
|
|
445
|
+
return
|
|
446
|
+
if "--unset" in argv:
|
|
447
|
+
from shared.env import del_config
|
|
448
|
+
|
|
449
|
+
keys = [a for a in argv if not a.startswith("-") and "=" not in a]
|
|
450
|
+
if not keys:
|
|
451
|
+
print(f"usage: {entry} --unset KEY [KEY …]")
|
|
452
|
+
return
|
|
453
|
+
print(f"✓ removed {', '.join(sorted(keys))} → {del_config(keys)}")
|
|
454
|
+
return
|
|
455
|
+
if "--launch-app" in argv:
|
|
456
|
+
launch = getattr(pkg, "launch_app", None)
|
|
457
|
+
if launch is None:
|
|
458
|
+
print(f"{import_name.replace('_', '-')} has no launchable app.", file=sys.stderr)
|
|
459
|
+
sys.exit(2)
|
|
460
|
+
rest = [a for a in argv if a != "--launch-app"]
|
|
461
|
+
sys.exit(launch(rest) or 0)
|
|
462
|
+
if "--help" in argv or "-h" in argv or sys.stdin.isatty():
|
|
463
|
+
print(
|
|
464
|
+
usage(import_name, getattr(pkg, "USAGE_NOTE", ""), launchable=getattr(pkg, "launch_app", None) is not None)
|
|
465
|
+
)
|
|
466
|
+
if not ({"--help", "-h"} & set(argv)):
|
|
467
|
+
sys.exit(1)
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
log = logging.getLogger(import_name)
|
|
471
|
+
deps = getattr(pkg, "SYSTEM_DEPS", None)
|
|
472
|
+
pkg_on_start = getattr(pkg, "on_start", None)
|
|
473
|
+
|
|
474
|
+
def _on_start():
|
|
475
|
+
# warn (stderr) about missing system tools, then run any capability on_start
|
|
476
|
+
for w in system_startup_warnings(deps):
|
|
477
|
+
log.warning("system tool missing — %s", w)
|
|
478
|
+
if pkg_on_start is not None:
|
|
479
|
+
pkg_on_start()
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
serve(
|
|
483
|
+
import_name,
|
|
484
|
+
pkg.__version__,
|
|
485
|
+
pkg.SPECS,
|
|
486
|
+
transport=getattr(pkg, "transport", None),
|
|
487
|
+
on_start=_on_start,
|
|
488
|
+
)
|
|
489
|
+
except (BrokenPipeError, IOError) as exc:
|
|
490
|
+
log.info("client disconnected (%s), shutting down", type(exc).__name__)
|
|
491
|
+
except KeyboardInterrupt:
|
|
492
|
+
pass
|
|
493
|
+
except Exception:
|
|
494
|
+
log.exception("server crashed unexpectedly")
|
|
495
|
+
sys.exit(1) # surface the crash to the supervising harness with a non-zero status
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-pharma-plugins
|
|
3
|
+
Version: 2.2.0
|
|
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
|
+
Author: EAI Commercial Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/PharmaGenAI/open-pharma-plugins
|
|
8
|
+
Project-URL: Repository, https://github.com/PharmaGenAI/open-pharma-plugins
|
|
9
|
+
Project-URL: Issues, https://github.com/PharmaGenAI/open-pharma-plugins/issues
|
|
10
|
+
Keywords: mcp,pharma,commercial,agent-skills,hcp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: <3.14,>=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp<2,>=1.0.0
|
|
24
|
+
Requires-Dist: anyio<5,>=4.0
|
|
25
|
+
Requires-Dist: pydantic<3,>=2.8
|
|
26
|
+
Provides-Extra: hcp-intelligence
|
|
27
|
+
Requires-Dist: requests<3,>=2.31; extra == "hcp-intelligence"
|
|
28
|
+
Provides-Extra: hcp-intelligence-synth
|
|
29
|
+
Requires-Dist: requests<3,>=2.31; extra == "hcp-intelligence-synth"
|
|
30
|
+
Requires-Dist: openai<4,>=1; extra == "hcp-intelligence-synth"
|
|
31
|
+
Provides-Extra: field-training
|
|
32
|
+
Requires-Dist: pypdfium2<6,>=4; extra == "field-training"
|
|
33
|
+
Requires-Dist: python-pptx<2,>=1; extra == "field-training"
|
|
34
|
+
Provides-Extra: field-training-fixtures
|
|
35
|
+
Requires-Dist: pypdfium2<6,>=4; extra == "field-training-fixtures"
|
|
36
|
+
Requires-Dist: python-pptx<2,>=1; extra == "field-training-fixtures"
|
|
37
|
+
Requires-Dist: reportlab<6,>=4; extra == "field-training-fixtures"
|
|
38
|
+
Provides-Extra: next-best-engagement
|
|
39
|
+
Provides-Extra: territory-alignment
|
|
40
|
+
Provides-Extra: competitive-intelligence
|
|
41
|
+
Requires-Dist: pypdfium2<6,>=4; extra == "competitive-intelligence"
|
|
42
|
+
Requires-Dist: python-docx<2,>=1; extra == "competitive-intelligence"
|
|
43
|
+
Provides-Extra: campaign-studio
|
|
44
|
+
Requires-Dist: jinja2<4,>=3.1; extra == "campaign-studio"
|
|
45
|
+
Requires-Dist: reportlab<6,>=4; extra == "campaign-studio"
|
|
46
|
+
Provides-Extra: all
|
|
47
|
+
Requires-Dist: requests<3,>=2.31; extra == "all"
|
|
48
|
+
Requires-Dist: openai<4,>=1; extra == "all"
|
|
49
|
+
Requires-Dist: pypdfium2<6,>=4; extra == "all"
|
|
50
|
+
Requires-Dist: python-pptx<2,>=1; extra == "all"
|
|
51
|
+
Requires-Dist: python-docx<2,>=1; extra == "all"
|
|
52
|
+
Requires-Dist: reportlab<6,>=4; extra == "all"
|
|
53
|
+
Requires-Dist: jinja2<4,>=3.1; extra == "all"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# Open Pharma Plugins
|
|
57
|
+
|
|
58
|
+
Agent Skills and MCP tools for pharmaceutical commercial operations.
|
|
59
|
+
|
|
60
|
+
> **Public beta.** Validate outputs before operational use. Campaign and field-training artifacts are drafts for qualified medical/legal/regulatory review, not automated approval.
|
|
61
|
+
|
|
62
|
+
## Capabilities
|
|
63
|
+
|
|
64
|
+
| Capability | Tools | Description |
|
|
65
|
+
|---|---:|---|
|
|
66
|
+
| [HCP Intelligence](cookbooks/hcp-intelligence/usage.md) | 11 | Build evidence-backed HCP/HCO profiles from public sources |
|
|
67
|
+
| [Field Training](cookbooks/field-training/usage.md) | 5 | Turn approved PDF/PPTX paths into grounded learning packages, assessments, role-play kits, and scorecards |
|
|
68
|
+
| [Campaign Studio](cookbooks/campaign-studio/usage.md) | 11 | Create, validate, render, and package campaign drafts for MLR review |
|
|
69
|
+
| [Next-Best-Engagement](cookbooks/next-best-engagement/usage.md) | 3 | Score HCPs and produce consent-aware engagement plans |
|
|
70
|
+
| [Territory Alignment](cookbooks/territory-alignment/usage.md) | 6 | Compare HCP-to-rep assignments and plan visit clusters |
|
|
71
|
+
| [Competitive Intelligence](cookbooks/competitive-intelligence/usage.md) | 12 | Collect one evidence run and project reproducible briefings and timelines |
|
|
72
|
+
|
|
73
|
+
## Architecture
|
|
74
|
+
|
|
75
|
+
<p align="center"><img src="docs/assets/architecture.svg" width="960" alt="Architecture diagram"></p>
|
|
76
|
+
|
|
77
|
+
Each source plugin bundles a Skill with one MCP server. The Python wheel contains the six MCP servers and their runtime fixtures; Skills, cookbooks, and marketplace manifests are installed from the repository rather than the wheel.
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
Python 3.10–3.13 is supported. Install one or more MCP servers from the published distribution:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python -m pip install "open-pharma-plugins[territory-alignment,competitive-intelligence]"
|
|
85
|
+
open-pharma-plugins-territory-alignment --version
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For the complete Skill + MCP plugin, download and inspect the guided installer before running it:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
curl -fsSLO https://raw.githubusercontent.com/PharmaGenAI/open-pharma-plugins/main/install.sh
|
|
92
|
+
less install.sh
|
|
93
|
+
bash install.sh
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The installer requires `uv`/`uvx`; it never installs a package manager on your behalf. See [Installation](docs/en/installation.md) for tag-pinned and source-checkout options.
|
|
97
|
+
|
|
98
|
+
## Configuration and local data
|
|
99
|
+
|
|
100
|
+
Copy `.env.example` to `~/.open-pharma-plugins/config` and set only the providers you use. Process environment variables take precedence. Mutable data defaults to private capability directories under `~/.open-pharma-plugins`; files are written with mode `0600` and directories with `0700` where POSIX permissions are available.
|
|
101
|
+
|
|
102
|
+
Common settings include:
|
|
103
|
+
|
|
104
|
+
| Variable | Used by |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `SERPER_API_KEY` / `TAVILY_API_KEY` / `EXA_API_KEY` | Web search |
|
|
107
|
+
| `OPENROUTER_API_KEY` / `OPENROUTER_BASE_URL` | Optional HCP batch profile synthesis |
|
|
108
|
+
| `NCBI_API_KEY` | PubMed |
|
|
109
|
+
| `OPENFDA_API_KEY` | openFDA |
|
|
110
|
+
| `OPEN_PHARMA_*_DIR` | Capability-specific mutable-data locations |
|
|
111
|
+
|
|
112
|
+
API keys are not persisted in competitive-intelligence cache metadata, evidence URLs, runs, or reports. Source queries are still sent to the selected external provider, so do not place secrets or unnecessary personal data in search terms. Reports and timelines can reuse one immutable run instead of repeating provider calls.
|
|
113
|
+
|
|
114
|
+
HCP batch extraction/synthesis defaults to `high` reasoning effort, a 120-second request timeout,
|
|
115
|
+
and zero SDK retries. An installed HCP plugin accepts user-supplied input/output paths through the
|
|
116
|
+
tag-pinned `open-pharma-plugins-hcp-batch` console and produces canonical account JSON,
|
|
117
|
+
`batch_summary.csv`, and `batch_manifest.json`. See the
|
|
118
|
+
[HCP batch guide](docs/en/hcp_batch.md) for dry-run, confirmation, provider, CSV, and resume rules.
|
|
119
|
+
|
|
120
|
+
## Documentation
|
|
121
|
+
|
|
122
|
+
- [Installation](docs/en/installation.md)
|
|
123
|
+
- [Configuration](docs/en/configuration.md)
|
|
124
|
+
- [HCP batch processing and CSV review](docs/en/hcp_batch.md)
|
|
125
|
+
- [Data security and compliance boundaries](docs/en/data_security.md)
|
|
126
|
+
- [Local development](docs/en/local_development.md)
|
|
127
|
+
- [Testing](docs/en/testing.md)
|
|
128
|
+
- [Adding a capability](docs/en/how_to_add_new_capability.md)
|
|
129
|
+
- [Releasing](docs/en/releasing.md)
|
|
130
|
+
- [Manual harness setup](docs/en/manual_harnesses.md)
|
|
131
|
+
- [Chinese documentation](docs/zh/) · [Japanese documentation](docs/jp/)
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
Apache-2.0. See [LICENSE](LICENSE).
|