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.
@@ -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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
25
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-blue.svg)](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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-blue.svg)](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,3 @@
1
+ """MedCreatorGuard — AI safety toolkit for health content creators."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()