open-pharma-plugins 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_framework.py +495 -0
- open_pharma_plugins-2.2.0.dist-info/METADATA +135 -0
- open_pharma_plugins-2.2.0.dist-info/RECORD +136 -0
- open_pharma_plugins-2.2.0.dist-info/WHEEL +5 -0
- open_pharma_plugins-2.2.0.dist-info/entry_points.txt +8 -0
- open_pharma_plugins-2.2.0.dist-info/licenses/LICENSE +202 -0
- open_pharma_plugins-2.2.0.dist-info/top_level.txt +8 -0
- open_pharma_plugins_campaign_studio/__init__.py +14 -0
- open_pharma_plugins_campaign_studio/__main__.py +11 -0
- open_pharma_plugins_campaign_studio/_campaign_store.py +162 -0
- open_pharma_plugins_campaign_studio/_claim_engine.py +262 -0
- open_pharma_plugins_campaign_studio/_renderer.py +119 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/legal.json +21 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/logo.svg +4 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/palette.json +11 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/product.png +1 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/typography.json +14 -0
- open_pharma_plugins_campaign_studio/fixtures/sample_approved_claims.json +119 -0
- open_pharma_plugins_campaign_studio/models/__init__.py +34 -0
- open_pharma_plugins_campaign_studio/models/_common.py +12 -0
- open_pharma_plugins_campaign_studio/models/brief.py +72 -0
- open_pharma_plugins_campaign_studio/models/claims.py +15 -0
- open_pharma_plugins_campaign_studio/models/copy.py +47 -0
- open_pharma_plugins_campaign_studio/models/journey.py +21 -0
- open_pharma_plugins_campaign_studio/models/message.py +25 -0
- open_pharma_plugins_campaign_studio/models/mlr.py +29 -0
- open_pharma_plugins_campaign_studio/models/validation.py +32 -0
- open_pharma_plugins_campaign_studio/policy/rules.json +79 -0
- open_pharma_plugins_campaign_studio/templates/banner.svg.j2 +26 -0
- open_pharma_plugins_campaign_studio/templates/email.html.j2 +54 -0
- open_pharma_plugins_campaign_studio/tools/__init__.py +0 -0
- open_pharma_plugins_campaign_studio/tools/create_campaign_brief.py +212 -0
- open_pharma_plugins_campaign_studio/tools/generate_audience_journey.py +129 -0
- open_pharma_plugins_campaign_studio/tools/generate_channel_copy.py +199 -0
- open_pharma_plugins_campaign_studio/tools/generate_message_architecture.py +121 -0
- open_pharma_plugins_campaign_studio/tools/package_mlr_submission.py +230 -0
- open_pharma_plugins_campaign_studio/tools/render_banner.py +99 -0
- open_pharma_plugins_campaign_studio/tools/render_email.py +101 -0
- open_pharma_plugins_campaign_studio/tools/render_poster.py +222 -0
- open_pharma_plugins_campaign_studio/tools/retrieve_approved_claims.py +71 -0
- open_pharma_plugins_campaign_studio/tools/retrieve_brand_components.py +76 -0
- open_pharma_plugins_campaign_studio/tools/validate_claims_and_fair_balance.py +285 -0
- open_pharma_plugins_competitive_intelligence/__init__.py +13 -0
- open_pharma_plugins_competitive_intelligence/__main__.py +11 -0
- open_pharma_plugins_competitive_intelligence/_artifacts.py +87 -0
- open_pharma_plugins_competitive_intelligence/_cache.py +144 -0
- open_pharma_plugins_competitive_intelligence/_clinical_trials.py +569 -0
- open_pharma_plugins_competitive_intelligence/_dailymed.py +260 -0
- open_pharma_plugins_competitive_intelligence/_fda.py +255 -0
- open_pharma_plugins_competitive_intelligence/_pubmed.py +342 -0
- open_pharma_plugins_competitive_intelligence/_regulatory.py +140 -0
- open_pharma_plugins_competitive_intelligence/_runs.py +278 -0
- open_pharma_plugins_competitive_intelligence/_transport.py +83 -0
- open_pharma_plugins_competitive_intelligence/_watchlist.py +113 -0
- open_pharma_plugins_competitive_intelligence/_web_search.py +331 -0
- open_pharma_plugins_competitive_intelligence/models.py +525 -0
- open_pharma_plugins_competitive_intelligence/tools/__init__.py +0 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_extract_events.py +247 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_landscape.py +195 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_refresh.py +101 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_report.py +402 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_news.py +48 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_publications.py +46 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_regulatory.py +64 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_trials.py +85 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_status.py +106 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_timeline.py +453 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_track.py +121 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_trial_detail.py +55 -0
- open_pharma_plugins_field_training/__init__.py +13 -0
- open_pharma_plugins_field_training/__main__.py +11 -0
- open_pharma_plugins_field_training/_content_store.py +131 -0
- open_pharma_plugins_field_training/_grounding.py +75 -0
- open_pharma_plugins_field_training/_html_renderers.py +546 -0
- open_pharma_plugins_field_training/fixtures/sample_product_message.pdf +156 -0
- open_pharma_plugins_field_training/fixtures/sample_training_deck.pptx +0 -0
- open_pharma_plugins_field_training/models.py +265 -0
- open_pharma_plugins_field_training/tools/__init__.py +0 -0
- open_pharma_plugins_field_training/tools/get_document_page.py +67 -0
- open_pharma_plugins_field_training/tools/ingest_document.py +147 -0
- open_pharma_plugins_field_training/tools/list_documents.py +51 -0
- open_pharma_plugins_field_training/tools/render_output.py +118 -0
- open_pharma_plugins_field_training/tools/search_content.py +57 -0
- open_pharma_plugins_hcp_intelligence/__init__.py +14 -0
- open_pharma_plugins_hcp_intelligence/__main__.py +11 -0
- open_pharma_plugins_hcp_intelligence/_crm_store.py +70 -0
- open_pharma_plugins_hcp_intelligence/batch.py +891 -0
- open_pharma_plugins_hcp_intelligence/batch_cli.py +221 -0
- open_pharma_plugins_hcp_intelligence/batch_csv.py +206 -0
- open_pharma_plugins_hcp_intelligence/fixtures/sample_accounts.csv +27 -0
- open_pharma_plugins_hcp_intelligence/models.py +356 -0
- open_pharma_plugins_hcp_intelligence/tools/__init__.py +0 -0
- open_pharma_plugins_hcp_intelligence/tools/get_account.py +48 -0
- open_pharma_plugins_hcp_intelligence/tools/list_accounts.py +66 -0
- open_pharma_plugins_hcp_intelligence/tools/search_clinical_trials.py +175 -0
- open_pharma_plugins_hcp_intelligence/tools/search_congresses.py +134 -0
- open_pharma_plugins_hcp_intelligence/tools/search_grants.py +181 -0
- open_pharma_plugins_hcp_intelligence/tools/search_guidelines.py +238 -0
- open_pharma_plugins_hcp_intelligence/tools/search_hco_web.py +73 -0
- open_pharma_plugins_hcp_intelligence/tools/search_hcp_web.py +199 -0
- open_pharma_plugins_hcp_intelligence/tools/search_orcid.py +214 -0
- open_pharma_plugins_hcp_intelligence/tools/search_publications.py +207 -0
- open_pharma_plugins_hcp_intelligence/tools/update_account.py +84 -0
- open_pharma_plugins_next_best_engagement/__init__.py +14 -0
- open_pharma_plugins_next_best_engagement/__main__.py +11 -0
- open_pharma_plugins_next_best_engagement/_optimizer.py +400 -0
- open_pharma_plugins_next_best_engagement/_renderer.py +149 -0
- open_pharma_plugins_next_best_engagement/_scoring.py +82 -0
- open_pharma_plugins_next_best_engagement/_universe.py +145 -0
- open_pharma_plugins_next_best_engagement/fixtures/sample_universe.csv +81 -0
- open_pharma_plugins_next_best_engagement/models.py +135 -0
- open_pharma_plugins_next_best_engagement/tools/__init__.py +0 -0
- open_pharma_plugins_next_best_engagement/tools/load_universe.py +47 -0
- open_pharma_plugins_next_best_engagement/tools/recommend_engagements.py +80 -0
- open_pharma_plugins_next_best_engagement/tools/render_plan.py +90 -0
- open_pharma_plugins_territory_alignment/__init__.py +14 -0
- open_pharma_plugins_territory_alignment/__main__.py +11 -0
- open_pharma_plugins_territory_alignment/data.py +300 -0
- open_pharma_plugins_territory_alignment/fixtures/constraints.csv +11 -0
- open_pharma_plugins_territory_alignment/fixtures/current_alignment.csv +81 -0
- open_pharma_plugins_territory_alignment/fixtures/hcps.csv +81 -0
- open_pharma_plugins_territory_alignment/fixtures/reps.csv +9 -0
- open_pharma_plugins_territory_alignment/geo.py +175 -0
- open_pharma_plugins_territory_alignment/models.py +201 -0
- open_pharma_plugins_territory_alignment/scoring.py +125 -0
- open_pharma_plugins_territory_alignment/solver.py +504 -0
- open_pharma_plugins_territory_alignment/tools/__init__.py +0 -0
- open_pharma_plugins_territory_alignment/tools/ta_align.py +138 -0
- open_pharma_plugins_territory_alignment/tools/ta_cluster.py +247 -0
- open_pharma_plugins_territory_alignment/tools/ta_compare.py +173 -0
- open_pharma_plugins_territory_alignment/tools/ta_evaluate.py +119 -0
- open_pharma_plugins_territory_alignment/tools/ta_status.py +34 -0
- open_pharma_plugins_territory_alignment/tools/ta_visualize.py +504 -0
- shared/__init__.py +11 -0
- shared/env.py +217 -0
- shared/filesystem.py +110 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""list_documents — list all ingested documents in the content store."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ListDocumentsArgs(BaseModel):
|
|
11
|
+
file_type: str | None = Field(
|
|
12
|
+
default=None,
|
|
13
|
+
description="Filter by file type: 'pdf' or 'pptx'. Omit to list all.",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
TOOL: dict[str, Any] = {
|
|
18
|
+
"name": "list_documents",
|
|
19
|
+
"description": (
|
|
20
|
+
"List all documents ingested into the training content store. Shows "
|
|
21
|
+
"document ID, file name, type, page count, title, and ingest date. "
|
|
22
|
+
"Optionally filter by file type. Use this to see what source material "
|
|
23
|
+
"is available before searching or generating training content."
|
|
24
|
+
),
|
|
25
|
+
"args": ListDocumentsArgs,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
30
|
+
import json
|
|
31
|
+
|
|
32
|
+
from .._content_store import list_documents
|
|
33
|
+
|
|
34
|
+
file_type = arguments.get("file_type")
|
|
35
|
+
docs = list_documents(file_type)
|
|
36
|
+
|
|
37
|
+
output = {
|
|
38
|
+
"total": len(docs),
|
|
39
|
+
"documents": [
|
|
40
|
+
{
|
|
41
|
+
"document_id": d["document_id"],
|
|
42
|
+
"file_name": d["file_name"],
|
|
43
|
+
"file_type": d["file_type"],
|
|
44
|
+
"title": d.get("title", ""),
|
|
45
|
+
"total_pages": d["total_pages"],
|
|
46
|
+
"ingested_at": d["ingested_at"],
|
|
47
|
+
}
|
|
48
|
+
for d in docs
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
return [{"type": "text", "text": json.dumps(output, indent=2)}]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""render_output — validate and save structured training output as JSON plus HTML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RenderOutputArgs(BaseModel):
|
|
11
|
+
output_type: str = Field(
|
|
12
|
+
description="Type of output: 'learning_package', 'assessment', 'roleplay_kit', or 'roleplay_scorecard'"
|
|
13
|
+
)
|
|
14
|
+
content_json: str = Field(
|
|
15
|
+
description=(
|
|
16
|
+
"The structured output as a JSON string (LearningPackage, Assessment, RoleplayKit, or RoleplayScorecard)"
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
file_name: str | None = Field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Optional filename stem (without extension). Auto-generated from title + timestamp if omitted.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TOOL: dict[str, Any] = {
|
|
26
|
+
"name": "render_output",
|
|
27
|
+
"description": (
|
|
28
|
+
"Validate and render a learning package, assessment, pre-session role-play kit, "
|
|
29
|
+
"or post-session role-play scorecard as structured JSON and polished, interactive, "
|
|
30
|
+
"self-contained HTML. Saves both files to the private training output directory."
|
|
31
|
+
),
|
|
32
|
+
"args": RenderOutputArgs,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
37
|
+
import json
|
|
38
|
+
from datetime import datetime, timezone
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from pydantic import ValidationError
|
|
42
|
+
|
|
43
|
+
from shared.env import get_env
|
|
44
|
+
from shared.filesystem import atomic_write_json, atomic_write_text, contained_path, ensure_private_dir
|
|
45
|
+
|
|
46
|
+
from .._grounding import validate_output_sources
|
|
47
|
+
from .._html_renderers import RENDERERS
|
|
48
|
+
from ..models import Assessment, LearningPackage, RoleplayKit, RoleplayScorecard
|
|
49
|
+
|
|
50
|
+
output_type = arguments["output_type"]
|
|
51
|
+
content_json = arguments["content_json"]
|
|
52
|
+
file_name = arguments.get("file_name")
|
|
53
|
+
|
|
54
|
+
output_models = {
|
|
55
|
+
"learning_package": LearningPackage,
|
|
56
|
+
"assessment": Assessment,
|
|
57
|
+
"roleplay_kit": RoleplayKit,
|
|
58
|
+
"roleplay_scorecard": RoleplayScorecard,
|
|
59
|
+
}
|
|
60
|
+
if output_type not in output_models:
|
|
61
|
+
valid_types = ", ".join(output_models)
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
"type": "text",
|
|
65
|
+
"text": json.dumps({"error": f"Invalid output_type '{output_type}'. Must be one of: {valid_types}"}),
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(content_json)
|
|
71
|
+
except json.JSONDecodeError as exc:
|
|
72
|
+
return [{"type": "text", "text": json.dumps({"error": f"Invalid JSON: {exc}"})}]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
validated = output_models[output_type].model_validate(data)
|
|
76
|
+
except ValidationError as exc:
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
"type": "text",
|
|
80
|
+
"text": json.dumps({"error": f"Output failed {output_type} schema validation: {exc}"}),
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
grounding_errors = validate_output_sources(validated)
|
|
85
|
+
if grounding_errors:
|
|
86
|
+
return [{"type": "text", "text": json.dumps({"error": "; ".join(grounding_errors)})}]
|
|
87
|
+
data = validated.model_dump(mode="json", exclude_none=True)
|
|
88
|
+
|
|
89
|
+
if not file_name:
|
|
90
|
+
title = data.get("title") or data.get("topic") or output_type
|
|
91
|
+
safe = "".join(character if character.isalnum() or character in "-_ " else "" for character in title)
|
|
92
|
+
safe = safe[:50].strip().replace(" ", "_")
|
|
93
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
94
|
+
file_name = f"{safe}_{timestamp}"
|
|
95
|
+
|
|
96
|
+
content_dir = get_env(
|
|
97
|
+
"OPEN_PHARMA_TRAINING_CONTENT_DIR",
|
|
98
|
+
str(Path.home() / ".open-pharma-plugins" / "training-content"),
|
|
99
|
+
)
|
|
100
|
+
output_dir = ensure_private_dir(Path(content_dir) / "outputs")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
json_path = contained_path(output_dir, f"{file_name}.json")
|
|
104
|
+
html_path = contained_path(output_dir, f"{file_name}.html")
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
return [{"type": "text", "text": json.dumps({"error": str(exc)})}]
|
|
107
|
+
|
|
108
|
+
atomic_write_json(json_path, data)
|
|
109
|
+
atomic_write_text(html_path, RENDERERS[output_type](data))
|
|
110
|
+
|
|
111
|
+
result = {
|
|
112
|
+
"success": True,
|
|
113
|
+
"output_type": output_type,
|
|
114
|
+
"output_dir": str(output_dir),
|
|
115
|
+
"json_path": str(json_path),
|
|
116
|
+
"html_path": str(html_path),
|
|
117
|
+
}
|
|
118
|
+
return [{"type": "text", "text": json.dumps(result, indent=2)}]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""search_content — search across ingested documents for relevant passages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchContentArgs(BaseModel):
|
|
11
|
+
query: str = Field(description="Search query — keywords or phrases to find in the ingested documents")
|
|
12
|
+
document_id: str | None = Field(
|
|
13
|
+
default=None,
|
|
14
|
+
description=(
|
|
15
|
+
"Restrict search to a specific document by ID. Required for path-first workflows; "
|
|
16
|
+
"omit only when the user deliberately requests a global search of the persistent store."
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
max_results: int = Field(
|
|
20
|
+
default=10,
|
|
21
|
+
ge=1,
|
|
22
|
+
le=30,
|
|
23
|
+
description="Maximum number of passages to return",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TOOL: dict[str, Any] = {
|
|
28
|
+
"name": "search_content",
|
|
29
|
+
"description": (
|
|
30
|
+
"Search across all ingested training documents for passages matching a "
|
|
31
|
+
"query. Returns ranked results with document name, page/slide number, "
|
|
32
|
+
"relevant text excerpt, and relevance score. Use this to find source "
|
|
33
|
+
"material for specific topics when building learning packages, assessments, "
|
|
34
|
+
"or preparing for role-play. In a path-first request, pass document_id and "
|
|
35
|
+
"repeat the search for each submitted document; never use an unscoped search."
|
|
36
|
+
),
|
|
37
|
+
"args": SearchContentArgs,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def handle(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
42
|
+
import json
|
|
43
|
+
|
|
44
|
+
from .._content_store import search_pages
|
|
45
|
+
|
|
46
|
+
query = arguments["query"]
|
|
47
|
+
document_id = arguments.get("document_id")
|
|
48
|
+
max_results = arguments.get("max_results", 10)
|
|
49
|
+
|
|
50
|
+
results = search_pages(query, document_id, max_results)
|
|
51
|
+
|
|
52
|
+
output = {
|
|
53
|
+
"query": query,
|
|
54
|
+
"total_results": len(results),
|
|
55
|
+
"results": results,
|
|
56
|
+
}
|
|
57
|
+
return [{"type": "text", "text": json.dumps(output, indent=2)}]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from mcp_framework import build_registry
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.2"
|
|
4
|
+
|
|
5
|
+
SPECS, get_handler, list_tools = build_registry(__name__, ["tools"])
|
|
6
|
+
|
|
7
|
+
USAGE_NOTE = (
|
|
8
|
+
"HCP Intelligence builds evidence-backed profiles for healthcare professionals "
|
|
9
|
+
"and organizations. Call the search tools to gather data from PubMed, "
|
|
10
|
+
"ClinicalTrials.gov, and the web, then synthesize a structured profile "
|
|
11
|
+
"following the schema in the Skill."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
SYSTEM_DEPS = []
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared CRM fixture store used by list_accounts, get_account, update_account.
|
|
2
|
+
|
|
3
|
+
Reads from fixtures/sample_accounts.csv (the account list) and a JSON sidecar
|
|
4
|
+
(enrichment data written by update_account).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
14
|
+
_CSV_PATH = _FIXTURES_DIR / "sample_accounts.csv"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _enrichment_path() -> Path:
|
|
18
|
+
from shared.env import get_env
|
|
19
|
+
from shared.filesystem import contained_path, ensure_private_dir
|
|
20
|
+
|
|
21
|
+
root = ensure_private_dir(
|
|
22
|
+
get_env(
|
|
23
|
+
"OPEN_PHARMA_HCP_DATA_DIR",
|
|
24
|
+
str(Path.home() / ".open-pharma-plugins" / "hcp-intelligence"),
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
return contained_path(root, "enrichment_store.json")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_accounts() -> list[dict[str, str]]:
|
|
31
|
+
with open(_CSV_PATH, newline="", encoding="utf-8") as f:
|
|
32
|
+
rows = list(csv.DictReader(f))
|
|
33
|
+
enrichment = _load_enrichment()
|
|
34
|
+
for row in rows:
|
|
35
|
+
row["status"] = enrichment.get(row["id"], {}).get("status", "pending")
|
|
36
|
+
return rows
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_account_by_id(account_id: str) -> dict | None:
|
|
40
|
+
for row in load_accounts():
|
|
41
|
+
if row["id"] == account_id:
|
|
42
|
+
enrichment = _load_enrichment()
|
|
43
|
+
entry = enrichment.get(account_id, {})
|
|
44
|
+
row["status"] = entry.get("status", "pending")
|
|
45
|
+
row["enrichment"] = entry.get("profile")
|
|
46
|
+
row["last_enriched"] = entry.get("last_enriched")
|
|
47
|
+
return row
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def write_enrichment(account_id: str, profile_json: str, status: str) -> None:
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
|
|
54
|
+
enrichment = _load_enrichment()
|
|
55
|
+
enrichment[account_id] = {
|
|
56
|
+
"status": status,
|
|
57
|
+
"profile": json.loads(profile_json) if profile_json else None,
|
|
58
|
+
"last_enriched": datetime.now(timezone.utc).isoformat(),
|
|
59
|
+
}
|
|
60
|
+
from shared.filesystem import atomic_write_json
|
|
61
|
+
|
|
62
|
+
atomic_write_json(_enrichment_path(), enrichment)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load_enrichment() -> dict:
|
|
66
|
+
path = _enrichment_path()
|
|
67
|
+
if path.exists():
|
|
68
|
+
with open(path, encoding="utf-8") as f:
|
|
69
|
+
return json.load(f)
|
|
70
|
+
return {}
|