medcreatorguard 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- medcreatorguard-0.1.0/LICENSE +21 -0
- medcreatorguard-0.1.0/PKG-INFO +156 -0
- medcreatorguard-0.1.0/README.md +137 -0
- medcreatorguard-0.1.0/medcreatorguard/__init__.py +3 -0
- medcreatorguard-0.1.0/medcreatorguard/analyzer.py +171 -0
- medcreatorguard-0.1.0/medcreatorguard/cli.py +185 -0
- medcreatorguard-0.1.0/medcreatorguard/prompts.py +90 -0
- medcreatorguard-0.1.0/medcreatorguard/safety_rules.py +179 -0
- medcreatorguard-0.1.0/medcreatorguard/schemas.py +79 -0
- medcreatorguard-0.1.0/medcreatorguard.egg-info/PKG-INFO +156 -0
- medcreatorguard-0.1.0/medcreatorguard.egg-info/SOURCES.txt +18 -0
- medcreatorguard-0.1.0/medcreatorguard.egg-info/dependency_links.txt +1 -0
- medcreatorguard-0.1.0/medcreatorguard.egg-info/entry_points.txt +2 -0
- medcreatorguard-0.1.0/medcreatorguard.egg-info/requires.txt +10 -0
- medcreatorguard-0.1.0/medcreatorguard.egg-info/top_level.txt +1 -0
- medcreatorguard-0.1.0/pyproject.toml +32 -0
- medcreatorguard-0.1.0/setup.cfg +4 -0
- medcreatorguard-0.1.0/tests/test_analyzer.py +101 -0
- medcreatorguard-0.1.0/tests/test_safety_rules.py +85 -0
- medcreatorguard-0.1.0/tests/test_schemas.py +86 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MedCreatorGuard Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: medcreatorguard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI safety toolkit for clinicians and health content creators
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: openai>=1.30.0
|
|
10
|
+
Requires-Dist: pydantic>=2.6.0
|
|
11
|
+
Requires-Dist: typer>=0.12.0
|
|
12
|
+
Requires-Dist: rich>=13.7.0
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# MedCreatorGuard
|
|
21
|
+
|
|
22
|
+
**AI safety toolkit for clinicians and health content creators**
|
|
23
|
+
|
|
24
|
+
[](LICENSE)
|
|
25
|
+
[](https://python.org)
|
|
26
|
+
|
|
27
|
+
> 의사이자 AI creator의 관점에서 설계한 의료 콘텐츠 안전성 검토 오픈소스 도구.
|
|
28
|
+
>
|
|
29
|
+
> ---
|
|
30
|
+
>
|
|
31
|
+
> ## What is this?
|
|
32
|
+
>
|
|
33
|
+
> MedCreatorGuard is an open-source AI toolkit that helps clinicians and health creators review medical content for factual accuracy, evidence quality, and patient safety **before publishing**.
|
|
34
|
+
>
|
|
35
|
+
> SNS에 올라오는 건강 콘텐츠 중 많은 부분이:
|
|
36
|
+
> - 효과를 과장하거나 ("완치됩니다", "모든 사람에게 효과")
|
|
37
|
+
> - - 근거 없는 주장을 하거나 ("독소 배출", "간 해독")
|
|
38
|
+
> - - 전문 의료를 불필요하게 여기게 만들거나 ("병원 안 가도 됩니다")
|
|
39
|
+
>
|
|
40
|
+
> - MedCreatorGuard는 이런 문제를 자동으로 감지하고, 더 안전한 표현으로 수정을 제안합니다.
|
|
41
|
+
>
|
|
42
|
+
> - ---
|
|
43
|
+
>
|
|
44
|
+
> ## Features
|
|
45
|
+
>
|
|
46
|
+
> - **Rule-based scan** — API 호출 없이 위험 패턴 즉시 감지 (한국어 + 영어)
|
|
47
|
+
> - - **LLM analysis** — GPT-4o를 이용해 의료 주장 추출, 근거 등급 평가, 안전한 재작성 제안
|
|
48
|
+
> - - **CLI tool** — 터미널에서 바로 사용 가능 (`medguard check`, `medguard rewrite`)
|
|
49
|
+
> - - **Paper-to-post** — 연구 논문 초록 → 안전한 SNS 포스트 자동 변환
|
|
50
|
+
>
|
|
51
|
+
> - ---
|
|
52
|
+
>
|
|
53
|
+
> ## Installation
|
|
54
|
+
>
|
|
55
|
+
> ### Option 1: PyPI (권장)
|
|
56
|
+
> ```bash
|
|
57
|
+
> pip install medcreatorguard
|
|
58
|
+
> ```
|
|
59
|
+
>
|
|
60
|
+
> ### Option 2: GitHub에서 직접 설치
|
|
61
|
+
> ```bash
|
|
62
|
+
> git clone https://github.com/aimekoreaofficial/MedCreatorGuard.git
|
|
63
|
+
> cd MedCreatorGuard
|
|
64
|
+
> pip install -e .
|
|
65
|
+
> ```
|
|
66
|
+
>
|
|
67
|
+
> ### OpenAI API 키 설정
|
|
68
|
+
> ```bash
|
|
69
|
+
> export OPENAI_API_KEY=sk-... # Mac/Linux
|
|
70
|
+
> set OPENAI_API_KEY=sk-... # Windows
|
|
71
|
+
> ```
|
|
72
|
+
>
|
|
73
|
+
> ---
|
|
74
|
+
>
|
|
75
|
+
> ## Quick Start
|
|
76
|
+
>
|
|
77
|
+
> ```bash
|
|
78
|
+
> # 텍스트 직접 분석
|
|
79
|
+
> medguard check --text "마그네슘을 먹으면 불면증이 완치됩니다."
|
|
80
|
+
>
|
|
81
|
+
> # 파일 분석
|
|
82
|
+
> medguard check examples/korean_sleep_caption.txt
|
|
83
|
+
>
|
|
84
|
+
> # 안전한 표현으로 재작성
|
|
85
|
+
> medguard rewrite --text "이 방법만 하면 병원에 가지 않아도 됩니다."
|
|
86
|
+
>
|
|
87
|
+
> # 연구 논문 초록 → SNS 포스트 변환
|
|
88
|
+
> medguard paper-to-post examples/abstract.txt --platform instagram
|
|
89
|
+
>
|
|
90
|
+
> # JSON 형식으로 출력
|
|
91
|
+
> medguard check --text "당뇨가 완치됩니다." --json
|
|
92
|
+
> ```
|
|
93
|
+
>
|
|
94
|
+
> ---
|
|
95
|
+
>
|
|
96
|
+
> ## Example Output
|
|
97
|
+
>
|
|
98
|
+
> ```
|
|
99
|
+
> medguard check --text "마그네슘을 먹으면 불면증이 대부분 해결됩니다."
|
|
100
|
+
>
|
|
101
|
+
> ╭─── MedCreatorGuard Report ───╮
|
|
102
|
+
> │ Risk Score: 🟡 중간 (Medium) │
|
|
103
|
+
> ╰───────────────────────────────╯
|
|
104
|
+
>
|
|
105
|
+
> Detected Claims
|
|
106
|
+
> Claim Type Evidence Risk
|
|
107
|
+
> 마그네슘 섭취가 불면증을 해결한다 treatment_effect C medium
|
|
108
|
+
>
|
|
109
|
+
> ⚠ Risky Phrases
|
|
110
|
+
> • 대부분 해결됩니다
|
|
111
|
+
> 불면증 해결을 과장함
|
|
112
|
+
> → 일부 사람에게 수면 개선에 도움이 될 수 있습니다
|
|
113
|
+
>
|
|
114
|
+
> ╭─ Suggested Safe Rewrite ─╮
|
|
115
|
+
> │ 마그네슘은 일부 사람의 수면 관리에 도움이 될 수 있지만,
|
|
116
|
+
> │ 불면증은 다양한 원인이 있습니다. 증상이 지속되면 의료진과
|
|
117
|
+
> │ 상담하는 것이 좋습니다.
|
|
118
|
+
> ╰───────────────────────────╯
|
|
119
|
+
>
|
|
120
|
+
> ╭─ Suggested Disclaimer ─╮
|
|
121
|
+
> │ 이 콘텐츠는 일반적인 건강 정보 제공 목적이며
|
|
122
|
+
> │ 개인의 진단이나 치료를 대체하지 않습니다.
|
|
123
|
+
> ╰─────────────────────────╯
|
|
124
|
+
> ```
|
|
125
|
+
>
|
|
126
|
+
> ---
|
|
127
|
+
>
|
|
128
|
+
> ## Evidence Levels
|
|
129
|
+
>
|
|
130
|
+
> | Grade | Meaning |
|
|
131
|
+
> |-------|---------|
|
|
132
|
+
> | **A** | 강한 근거 — 메타분석 / 대규모 RCT |
|
|
133
|
+
> | **B** | 중등도 근거 — 소규모 임상 / 관찰연구 |
|
|
134
|
+
> | **C** | 제한적 근거 — 전문가 의견 / 기전 추론 |
|
|
135
|
+
> | **D** | 근거 부족 또는 과장 가능성 |
|
|
136
|
+
>
|
|
137
|
+
> ---
|
|
138
|
+
>
|
|
139
|
+
> ## Development
|
|
140
|
+
>
|
|
141
|
+
> ```bash
|
|
142
|
+
> git clone https://github.com/aimekoreaofficial/MedCreatorGuard.git
|
|
143
|
+
> cd MedCreatorGuard
|
|
144
|
+
> pip install -e ".[dev]"
|
|
145
|
+
> pytest
|
|
146
|
+
> ```
|
|
147
|
+
>
|
|
148
|
+
> ---
|
|
149
|
+
>
|
|
150
|
+
> ## License
|
|
151
|
+
>
|
|
152
|
+
> MIT License — 자유롭게 사용, 수정, 배포 가능합니다.
|
|
153
|
+
>
|
|
154
|
+
> ---
|
|
155
|
+
>
|
|
156
|
+
> *Built by [@aimekoreaofficial](https://github.com/aimekoreaofficial)*
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# MedCreatorGuard
|
|
2
|
+
|
|
3
|
+
**AI safety toolkit for clinicians and health content creators**
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://python.org)
|
|
7
|
+
|
|
8
|
+
> 의사이자 AI creator의 관점에서 설계한 의료 콘텐츠 안전성 검토 오픈소스 도구.
|
|
9
|
+
>
|
|
10
|
+
> ---
|
|
11
|
+
>
|
|
12
|
+
> ## What is this?
|
|
13
|
+
>
|
|
14
|
+
> MedCreatorGuard is an open-source AI toolkit that helps clinicians and health creators review medical content for factual accuracy, evidence quality, and patient safety **before publishing**.
|
|
15
|
+
>
|
|
16
|
+
> SNS에 올라오는 건강 콘텐츠 중 많은 부분이:
|
|
17
|
+
> - 효과를 과장하거나 ("완치됩니다", "모든 사람에게 효과")
|
|
18
|
+
> - - 근거 없는 주장을 하거나 ("독소 배출", "간 해독")
|
|
19
|
+
> - - 전문 의료를 불필요하게 여기게 만들거나 ("병원 안 가도 됩니다")
|
|
20
|
+
>
|
|
21
|
+
> - MedCreatorGuard는 이런 문제를 자동으로 감지하고, 더 안전한 표현으로 수정을 제안합니다.
|
|
22
|
+
>
|
|
23
|
+
> - ---
|
|
24
|
+
>
|
|
25
|
+
> ## Features
|
|
26
|
+
>
|
|
27
|
+
> - **Rule-based scan** — API 호출 없이 위험 패턴 즉시 감지 (한국어 + 영어)
|
|
28
|
+
> - - **LLM analysis** — GPT-4o를 이용해 의료 주장 추출, 근거 등급 평가, 안전한 재작성 제안
|
|
29
|
+
> - - **CLI tool** — 터미널에서 바로 사용 가능 (`medguard check`, `medguard rewrite`)
|
|
30
|
+
> - - **Paper-to-post** — 연구 논문 초록 → 안전한 SNS 포스트 자동 변환
|
|
31
|
+
>
|
|
32
|
+
> - ---
|
|
33
|
+
>
|
|
34
|
+
> ## Installation
|
|
35
|
+
>
|
|
36
|
+
> ### Option 1: PyPI (권장)
|
|
37
|
+
> ```bash
|
|
38
|
+
> pip install medcreatorguard
|
|
39
|
+
> ```
|
|
40
|
+
>
|
|
41
|
+
> ### Option 2: GitHub에서 직접 설치
|
|
42
|
+
> ```bash
|
|
43
|
+
> git clone https://github.com/aimekoreaofficial/MedCreatorGuard.git
|
|
44
|
+
> cd MedCreatorGuard
|
|
45
|
+
> pip install -e .
|
|
46
|
+
> ```
|
|
47
|
+
>
|
|
48
|
+
> ### OpenAI API 키 설정
|
|
49
|
+
> ```bash
|
|
50
|
+
> export OPENAI_API_KEY=sk-... # Mac/Linux
|
|
51
|
+
> set OPENAI_API_KEY=sk-... # Windows
|
|
52
|
+
> ```
|
|
53
|
+
>
|
|
54
|
+
> ---
|
|
55
|
+
>
|
|
56
|
+
> ## Quick Start
|
|
57
|
+
>
|
|
58
|
+
> ```bash
|
|
59
|
+
> # 텍스트 직접 분석
|
|
60
|
+
> medguard check --text "마그네슘을 먹으면 불면증이 완치됩니다."
|
|
61
|
+
>
|
|
62
|
+
> # 파일 분석
|
|
63
|
+
> medguard check examples/korean_sleep_caption.txt
|
|
64
|
+
>
|
|
65
|
+
> # 안전한 표현으로 재작성
|
|
66
|
+
> medguard rewrite --text "이 방법만 하면 병원에 가지 않아도 됩니다."
|
|
67
|
+
>
|
|
68
|
+
> # 연구 논문 초록 → SNS 포스트 변환
|
|
69
|
+
> medguard paper-to-post examples/abstract.txt --platform instagram
|
|
70
|
+
>
|
|
71
|
+
> # JSON 형식으로 출력
|
|
72
|
+
> medguard check --text "당뇨가 완치됩니다." --json
|
|
73
|
+
> ```
|
|
74
|
+
>
|
|
75
|
+
> ---
|
|
76
|
+
>
|
|
77
|
+
> ## Example Output
|
|
78
|
+
>
|
|
79
|
+
> ```
|
|
80
|
+
> medguard check --text "마그네슘을 먹으면 불면증이 대부분 해결됩니다."
|
|
81
|
+
>
|
|
82
|
+
> ╭─── MedCreatorGuard Report ───╮
|
|
83
|
+
> │ Risk Score: 🟡 중간 (Medium) │
|
|
84
|
+
> ╰───────────────────────────────╯
|
|
85
|
+
>
|
|
86
|
+
> Detected Claims
|
|
87
|
+
> Claim Type Evidence Risk
|
|
88
|
+
> 마그네슘 섭취가 불면증을 해결한다 treatment_effect C medium
|
|
89
|
+
>
|
|
90
|
+
> ⚠ Risky Phrases
|
|
91
|
+
> • 대부분 해결됩니다
|
|
92
|
+
> 불면증 해결을 과장함
|
|
93
|
+
> → 일부 사람에게 수면 개선에 도움이 될 수 있습니다
|
|
94
|
+
>
|
|
95
|
+
> ╭─ Suggested Safe Rewrite ─╮
|
|
96
|
+
> │ 마그네슘은 일부 사람의 수면 관리에 도움이 될 수 있지만,
|
|
97
|
+
> │ 불면증은 다양한 원인이 있습니다. 증상이 지속되면 의료진과
|
|
98
|
+
> │ 상담하는 것이 좋습니다.
|
|
99
|
+
> ╰───────────────────────────╯
|
|
100
|
+
>
|
|
101
|
+
> ╭─ Suggested Disclaimer ─╮
|
|
102
|
+
> │ 이 콘텐츠는 일반적인 건강 정보 제공 목적이며
|
|
103
|
+
> │ 개인의 진단이나 치료를 대체하지 않습니다.
|
|
104
|
+
> ╰─────────────────────────╯
|
|
105
|
+
> ```
|
|
106
|
+
>
|
|
107
|
+
> ---
|
|
108
|
+
>
|
|
109
|
+
> ## Evidence Levels
|
|
110
|
+
>
|
|
111
|
+
> | Grade | Meaning |
|
|
112
|
+
> |-------|---------|
|
|
113
|
+
> | **A** | 강한 근거 — 메타분석 / 대규모 RCT |
|
|
114
|
+
> | **B** | 중등도 근거 — 소규모 임상 / 관찰연구 |
|
|
115
|
+
> | **C** | 제한적 근거 — 전문가 의견 / 기전 추론 |
|
|
116
|
+
> | **D** | 근거 부족 또는 과장 가능성 |
|
|
117
|
+
>
|
|
118
|
+
> ---
|
|
119
|
+
>
|
|
120
|
+
> ## Development
|
|
121
|
+
>
|
|
122
|
+
> ```bash
|
|
123
|
+
> git clone https://github.com/aimekoreaofficial/MedCreatorGuard.git
|
|
124
|
+
> cd MedCreatorGuard
|
|
125
|
+
> pip install -e ".[dev]"
|
|
126
|
+
> pytest
|
|
127
|
+
> ```
|
|
128
|
+
>
|
|
129
|
+
> ---
|
|
130
|
+
>
|
|
131
|
+
> ## License
|
|
132
|
+
>
|
|
133
|
+
> MIT License — 자유롭게 사용, 수정, 배포 가능합니다.
|
|
134
|
+
>
|
|
135
|
+
> ---
|
|
136
|
+
>
|
|
137
|
+
> *Built by [@aimekoreaofficial](https://github.com/aimekoreaofficial)*
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Core analyzer — combines rule-based checks with an OpenAI LLM pass."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
|
|
9
|
+
from .schemas import (
|
|
10
|
+
AnalysisReport,
|
|
11
|
+
Claim,
|
|
12
|
+
ClaimType,
|
|
13
|
+
EvidenceLevel,
|
|
14
|
+
RiskLevel,
|
|
15
|
+
RiskyPhrase,
|
|
16
|
+
)
|
|
17
|
+
from .safety_rules import (
|
|
18
|
+
classify_overall_risk,
|
|
19
|
+
detect_risky_patterns,
|
|
20
|
+
generate_disclaimer,
|
|
21
|
+
)
|
|
22
|
+
from .prompts import SYSTEM_ANALYZE, USER_ANALYZE, SYSTEM_PAPER_TO_POST, USER_PAPER_TO_POST
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MedAnalyzer:
|
|
26
|
+
"""Analyze medical/health content for safety, accuracy, and responsible messaging."""
|
|
27
|
+
|
|
28
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str | None = None,
|
|
33
|
+
model: str | None = None,
|
|
34
|
+
use_llm: bool = True,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "")
|
|
37
|
+
self.model = model or self.DEFAULT_MODEL
|
|
38
|
+
self.use_llm = use_llm
|
|
39
|
+
self._client: OpenAI | None = None
|
|
40
|
+
|
|
41
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def analyze(self, text: str) -> AnalysisReport:
|
|
44
|
+
"""Full safety analysis of a health/medical content snippet."""
|
|
45
|
+
if not text or not text.strip():
|
|
46
|
+
raise ValueError("Input text must not be empty")
|
|
47
|
+
|
|
48
|
+
# 1. Rule-based scan (fast, no API)
|
|
49
|
+
rule_matches = detect_risky_patterns(text)
|
|
50
|
+
|
|
51
|
+
# 2. LLM analysis (skipped if use_llm=False or mocked in tests)
|
|
52
|
+
if self.use_llm:
|
|
53
|
+
report = self._call_openai(text)
|
|
54
|
+
else:
|
|
55
|
+
# Offline fallback: build a minimal report from rules only
|
|
56
|
+
risk = classify_overall_risk(
|
|
57
|
+
risky_pattern_count=len(rule_matches),
|
|
58
|
+
high_risk_claim_count=0,
|
|
59
|
+
)
|
|
60
|
+
report = AnalysisReport(
|
|
61
|
+
input_text=text,
|
|
62
|
+
overall_risk=risk,
|
|
63
|
+
disclaimer=generate_disclaimer("general", risk),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# 3. Merge rule-based risky phrases that LLM may have missed
|
|
67
|
+
llm_phrases = {rp.phrase for rp in report.risky_phrases}
|
|
68
|
+
for match in rule_matches:
|
|
69
|
+
if match.matched_text not in llm_phrases:
|
|
70
|
+
report.risky_phrases.append(
|
|
71
|
+
RiskyPhrase(
|
|
72
|
+
phrase=match.matched_text,
|
|
73
|
+
reason=match.reason,
|
|
74
|
+
suggested_rewrite=match.suggested_rewrite,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 4. Re-compute overall risk using combined signals
|
|
79
|
+
high_risk_claims = sum(
|
|
80
|
+
1 for c in report.claims if c.risk_level == RiskLevel.HIGH
|
|
81
|
+
)
|
|
82
|
+
merged_risk = classify_overall_risk(
|
|
83
|
+
risky_pattern_count=len(rule_matches),
|
|
84
|
+
high_risk_claim_count=high_risk_claims,
|
|
85
|
+
)
|
|
86
|
+
# Take the worse of LLM and rule-based estimates
|
|
87
|
+
_order = {RiskLevel.LOW: 0, RiskLevel.MEDIUM: 1, RiskLevel.HIGH: 2}
|
|
88
|
+
if _order[merged_risk] > _order[report.overall_risk]:
|
|
89
|
+
report.overall_risk = merged_risk
|
|
90
|
+
|
|
91
|
+
return report
|
|
92
|
+
|
|
93
|
+
def paper_to_post(
|
|
94
|
+
self,
|
|
95
|
+
abstract: str,
|
|
96
|
+
platform: str = "instagram",
|
|
97
|
+
audience: str = "general_public",
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""Translate a research abstract into a safe social media post."""
|
|
100
|
+
prompt = USER_PAPER_TO_POST.format(
|
|
101
|
+
abstract=abstract,
|
|
102
|
+
platform=platform,
|
|
103
|
+
audience=audience,
|
|
104
|
+
)
|
|
105
|
+
raw = self._call_openai(prompt, system=SYSTEM_PAPER_TO_POST, raw_json=True)
|
|
106
|
+
return raw # type: ignore[return-value]
|
|
107
|
+
|
|
108
|
+
# ── Internal helpers ──────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def _get_client(self) -> OpenAI:
|
|
111
|
+
if self._client is None:
|
|
112
|
+
self._client = OpenAI(api_key=self.api_key)
|
|
113
|
+
return self._client
|
|
114
|
+
|
|
115
|
+
def _call_openai(
|
|
116
|
+
self,
|
|
117
|
+
text: str,
|
|
118
|
+
system: str = SYSTEM_ANALYZE,
|
|
119
|
+
raw_json: bool = False,
|
|
120
|
+
) -> AnalysisReport | dict[str, Any]:
|
|
121
|
+
"""Call OpenAI and parse the response.
|
|
122
|
+
|
|
123
|
+
In tests this method is patched by pytest-mock so no real API call is made.
|
|
124
|
+
"""
|
|
125
|
+
client = self._get_client()
|
|
126
|
+
user_prompt = (
|
|
127
|
+
text if raw_json else USER_ANALYZE.format(text=text)
|
|
128
|
+
)
|
|
129
|
+
response = client.chat.completions.create(
|
|
130
|
+
model=self.model,
|
|
131
|
+
response_format={"type": "json_object"},
|
|
132
|
+
messages=[
|
|
133
|
+
{"role": "system", "content": system},
|
|
134
|
+
{"role": "user", "content": user_prompt},
|
|
135
|
+
],
|
|
136
|
+
temperature=0.1,
|
|
137
|
+
)
|
|
138
|
+
content = response.choices[0].message.content or "{}"
|
|
139
|
+
data = json.loads(content)
|
|
140
|
+
|
|
141
|
+
if raw_json:
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
# Parse into AnalysisReport
|
|
145
|
+
claims = [
|
|
146
|
+
Claim(
|
|
147
|
+
text=c.get("text", ""),
|
|
148
|
+
claim_type=ClaimType(c.get("claim_type", "other")),
|
|
149
|
+
evidence_level=EvidenceLevel(c.get("evidence_level", "D")),
|
|
150
|
+
risk_level=RiskLevel(c.get("risk_level", "medium")),
|
|
151
|
+
concern=c.get("concern", ""),
|
|
152
|
+
suggested_rewrite=c.get("suggested_rewrite"),
|
|
153
|
+
)
|
|
154
|
+
for c in data.get("claims", [])
|
|
155
|
+
]
|
|
156
|
+
risky_phrases = [
|
|
157
|
+
RiskyPhrase(
|
|
158
|
+
phrase=rp.get("phrase", ""),
|
|
159
|
+
reason=rp.get("reason", ""),
|
|
160
|
+
suggested_rewrite=rp.get("suggested_rewrite"),
|
|
161
|
+
)
|
|
162
|
+
for rp in data.get("risky_phrases", [])
|
|
163
|
+
]
|
|
164
|
+
return AnalysisReport(
|
|
165
|
+
input_text=data.get("input_text", text),
|
|
166
|
+
overall_risk=RiskLevel(data.get("overall_risk", "medium")),
|
|
167
|
+
claims=claims,
|
|
168
|
+
risky_phrases=risky_phrases,
|
|
169
|
+
safe_rewrite=data.get("safe_rewrite"),
|
|
170
|
+
disclaimer=data.get("disclaimer", ""),
|
|
171
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""MedCreatorGuard CLI — `medguard` entry point."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich import box
|
|
13
|
+
|
|
14
|
+
from .analyzer import MedAnalyzer
|
|
15
|
+
from .schemas import RiskLevel
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="medguard",
|
|
19
|
+
help="MedCreatorGuard — AI safety toolkit for health content creators",
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
console = Console()
|
|
23
|
+
err_console = Console(stderr=True)
|
|
24
|
+
|
|
25
|
+
_RISK_COLOR = {
|
|
26
|
+
RiskLevel.LOW: "green",
|
|
27
|
+
RiskLevel.MEDIUM: "yellow",
|
|
28
|
+
RiskLevel.HIGH: "red",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_analyzer(model: str) -> MedAnalyzer:
|
|
33
|
+
api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
34
|
+
if not api_key:
|
|
35
|
+
err_console.print(
|
|
36
|
+
"[bold red]Error:[/] OPENAI_API_KEY environment variable is not set.\n"
|
|
37
|
+
" export OPENAI_API_KEY=sk-..."
|
|
38
|
+
)
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
return MedAnalyzer(api_key=api_key, model=model)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@app.command()
|
|
46
|
+
def check(
|
|
47
|
+
file: Optional[Path] = typer.Argument(None, help="Text file to analyze"),
|
|
48
|
+
text: Optional[str] = typer.Option(None, "--text", "-t", help="Inline text to analyze"),
|
|
49
|
+
model: str = typer.Option("gpt-4o-mini", "--model", "-m", help="OpenAI model to use"),
|
|
50
|
+
json_out: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Analyze health content for medical safety issues."""
|
|
53
|
+
if file:
|
|
54
|
+
content = Path(file).read_text(encoding="utf-8")
|
|
55
|
+
elif text:
|
|
56
|
+
content = text
|
|
57
|
+
else:
|
|
58
|
+
# Read from stdin
|
|
59
|
+
if not sys.stdin.isatty():
|
|
60
|
+
content = sys.stdin.read()
|
|
61
|
+
else:
|
|
62
|
+
err_console.print("[red]Error:[/] Provide a file, --text, or pipe stdin.")
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
|
|
65
|
+
analyzer = _get_analyzer(model)
|
|
66
|
+
|
|
67
|
+
with console.status("[bold cyan]Analyzing content…[/]"):
|
|
68
|
+
report = analyzer.analyze(content)
|
|
69
|
+
|
|
70
|
+
if json_out:
|
|
71
|
+
console.print(report.model_dump_json(indent=2))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# ── Pretty output ─────────────────────────────────────────────────────
|
|
75
|
+
risk_color = _RISK_COLOR[report.overall_risk]
|
|
76
|
+
console.print()
|
|
77
|
+
console.print(
|
|
78
|
+
Panel(
|
|
79
|
+
f"[bold]Risk Score:[/] [{risk_color}]{report.risk_label}[/{risk_color}]",
|
|
80
|
+
title="[bold cyan]MedCreatorGuard Report[/]",
|
|
81
|
+
box=box.ROUNDED,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if report.claims:
|
|
86
|
+
table = Table(title="Detected Claims", box=box.SIMPLE, show_lines=True)
|
|
87
|
+
table.add_column("Claim", style="white", max_width=40)
|
|
88
|
+
table.add_column("Type", style="cyan")
|
|
89
|
+
table.add_column("Evidence", style="magenta")
|
|
90
|
+
table.add_column("Risk", style="bold")
|
|
91
|
+
table.add_column("Concern", max_width=35)
|
|
92
|
+
for c in report.claims:
|
|
93
|
+
r_col = f"[{_RISK_COLOR[c.risk_level]}]{c.risk_level.value}[/]"
|
|
94
|
+
table.add_row(c.text, c.claim_type.value, c.evidence_level.value, r_col, c.concern)
|
|
95
|
+
console.print(table)
|
|
96
|
+
|
|
97
|
+
if report.risky_phrases:
|
|
98
|
+
console.print("\n[bold yellow]⚠ Risky Phrases[/]")
|
|
99
|
+
for rp in report.risky_phrases:
|
|
100
|
+
console.print(f" • [yellow]{rp.phrase}[/]\n {rp.reason}")
|
|
101
|
+
if rp.suggested_rewrite:
|
|
102
|
+
console.print(f" → [green]{rp.suggested_rewrite}[/]")
|
|
103
|
+
|
|
104
|
+
if report.safe_rewrite:
|
|
105
|
+
console.print(
|
|
106
|
+
Panel(report.safe_rewrite, title="[green]Suggested Safe Rewrite[/]", box=box.ROUNDED)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
console.print(
|
|
110
|
+
Panel(report.disclaimer, title="[blue]Suggested Disclaimer[/]", box=box.ROUNDED)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def rewrite(
|
|
116
|
+
file: Optional[Path] = typer.Argument(None),
|
|
117
|
+
text: Optional[str] = typer.Option(None, "--text", "-t"),
|
|
118
|
+
audience: str = typer.Option("public", "--audience", "-a", help="public | professional"),
|
|
119
|
+
model: str = typer.Option("gpt-4o-mini", "--model", "-m"),
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Rewrite health content to be safer and more evidence-appropriate."""
|
|
122
|
+
if file:
|
|
123
|
+
content = Path(file).read_text(encoding="utf-8")
|
|
124
|
+
elif text:
|
|
125
|
+
content = text
|
|
126
|
+
else:
|
|
127
|
+
content = sys.stdin.read()
|
|
128
|
+
|
|
129
|
+
analyzer = _get_analyzer(model)
|
|
130
|
+
with console.status("[bold cyan]Rewriting…[/]"):
|
|
131
|
+
report = analyzer.analyze(content)
|
|
132
|
+
|
|
133
|
+
if report.safe_rewrite:
|
|
134
|
+
console.print(Panel(report.safe_rewrite, title="[green]Safe Rewrite[/]", box=box.ROUNDED))
|
|
135
|
+
else:
|
|
136
|
+
console.print("[yellow]Content looks generally safe — no major rewrite needed.[/]")
|
|
137
|
+
|
|
138
|
+
console.print(Panel(report.disclaimer, title="[blue]Disclaimer[/]", box=box.ROUNDED))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command(name="paper-to-post")
|
|
142
|
+
def paper_to_post(
|
|
143
|
+
file: Optional[Path] = typer.Argument(None, help="Abstract text file"),
|
|
144
|
+
abstract: Optional[str] = typer.Option(None, "--abstract", "-a"),
|
|
145
|
+
platform: str = typer.Option("instagram", "--platform", "-p"),
|
|
146
|
+
audience: str = typer.Option("general_public", "--audience"),
|
|
147
|
+
model: str = typer.Option("gpt-4o-mini", "--model", "-m"),
|
|
148
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Translate a research abstract into a safe social media post."""
|
|
151
|
+
if file:
|
|
152
|
+
text = Path(file).read_text(encoding="utf-8")
|
|
153
|
+
elif abstract:
|
|
154
|
+
text = abstract
|
|
155
|
+
else:
|
|
156
|
+
text = sys.stdin.read()
|
|
157
|
+
|
|
158
|
+
analyzer = _get_analyzer(model)
|
|
159
|
+
with console.status("[bold cyan]Converting paper to post…[/]"):
|
|
160
|
+
result = analyzer.paper_to_post(text, platform=platform, audience=audience)
|
|
161
|
+
|
|
162
|
+
if json_out:
|
|
163
|
+
import json
|
|
164
|
+
console.print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
console.print(Panel(result.get("what_it_says", ""), title="[cyan]What the Study Says[/]"))
|
|
168
|
+
|
|
169
|
+
if overclaims := result.get("do_not_overclaim", []):
|
|
170
|
+
console.print("\n[bold red]Do NOT claim:[/]")
|
|
171
|
+
for item in overclaims:
|
|
172
|
+
console.print(f" ✗ {item}")
|
|
173
|
+
|
|
174
|
+
if result.get("instagram_summary"):
|
|
175
|
+
console.print(Panel(result["instagram_summary"], title=f"[magenta]{platform.title()} Caption[/]"))
|
|
176
|
+
|
|
177
|
+
if result.get("public_summary"):
|
|
178
|
+
console.print(Panel(result["public_summary"], title="[green]Public-Friendly Summary[/]"))
|
|
179
|
+
|
|
180
|
+
if result.get("disclaimer"):
|
|
181
|
+
console.print(Panel(result["disclaimer"], title="[blue]Disclaimer[/]"))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
app()
|