add mainAgentHiddenTools
This commit is contained in:
parent
065403223d
commit
e2827c6a47
@ -45,6 +45,7 @@ from .mem0_config import Mem0Config
|
||||
from agent.prompt_loader import load_system_prompt_async, load_mcp_settings_async
|
||||
from agent.agent_memory_cache import get_memory_cache_manager
|
||||
from .subagent_loader import load_subagents
|
||||
from agent.plugin_hook_loader import collect_main_agent_hidden_tools
|
||||
from .checkpoint_manager import get_checkpointer_manager
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
@ -310,6 +311,15 @@ async def init_agent(config: AgentConfig):
|
||||
logger.info(f"Loaded {len(mcp_tools)} MCP tools")
|
||||
logger.info(f"init_agent mcp tools ready, elapsed: {time.time() - create_start:.3f}s")
|
||||
|
||||
# Build the main agent's tool list by hiding tools blacklisted in plugin.json.
|
||||
# Sub-agents still receive the full mcp_tools set, so hidden tools remain usable by them.
|
||||
hidden_tools = collect_main_agent_hidden_tools(config.bot_id)
|
||||
main_tools = [t for t in mcp_tools if t.name not in hidden_tools] if hidden_tools else mcp_tools
|
||||
if hidden_tools:
|
||||
logger.info(
|
||||
f"Main agent hides {len(mcp_tools) - len(main_tools)} tools: {sorted(hidden_tools)}"
|
||||
)
|
||||
|
||||
sandbox, sandbox_type, workspace_root = await sandbox_task
|
||||
logger.info(f"init_agent sandbox ready, elapsed: {time.time() - create_start:.3f}s")
|
||||
|
||||
@ -342,7 +352,7 @@ async def init_agent(config: AgentConfig):
|
||||
model=llm_instance,
|
||||
assistant_id=config.bot_id,
|
||||
system_prompt=system_prompt,
|
||||
tools=mcp_tools,
|
||||
tools=main_tools,
|
||||
auto_approve=True,
|
||||
workspace_root=workspace_root,
|
||||
middleware=middleware,
|
||||
|
||||
@ -129,6 +129,52 @@ async def merge_skill_mcp_configs(bot_id: str) -> List[Dict]:
|
||||
return []
|
||||
|
||||
|
||||
def collect_main_agent_hidden_tools(bot_id: str) -> set:
|
||||
"""Collect tool names that must be hidden from the main agent.
|
||||
|
||||
Scans every skill's plugin.json for a top-level "mainAgentHiddenTools" list
|
||||
and merges them into a single set. These tools are removed from the main
|
||||
agent's tool list but remain available to sub-agents.
|
||||
|
||||
Args:
|
||||
bot_id: Bot ID
|
||||
|
||||
Returns:
|
||||
set[str]: Union of all hidden tool names. Empty set if none configured.
|
||||
"""
|
||||
hidden_tools = set()
|
||||
skill_dirs = _get_skill_dirs(bot_id)
|
||||
|
||||
for skill_dir in skill_dirs:
|
||||
if not os.path.exists(skill_dir):
|
||||
continue
|
||||
|
||||
for skill_name in os.listdir(skill_dir):
|
||||
skill_path = os.path.join(skill_dir, skill_name)
|
||||
if not os.path.isdir(skill_path):
|
||||
continue
|
||||
|
||||
plugin_json = os.path.join(skill_path, '.claude-plugin', 'plugin.json')
|
||||
if not os.path.exists(plugin_json):
|
||||
continue
|
||||
|
||||
try:
|
||||
plugin_config = _load_plugin_config(plugin_json)
|
||||
names = plugin_config.get('mainAgentHiddenTools', [])
|
||||
if isinstance(names, list):
|
||||
for name in names:
|
||||
if isinstance(name, str) and name.strip():
|
||||
hidden_tools.add(name.strip())
|
||||
else:
|
||||
logger.warning(
|
||||
f"Invalid 'mainAgentHiddenTools' in {skill_name}, expected list"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load mainAgentHiddenTools from {skill_name}: {e}")
|
||||
|
||||
return hidden_tools
|
||||
|
||||
|
||||
def _normalize_skill_mcp_servers(servers: Dict[str, Any], skill_path: str) -> Dict[str, Any]:
|
||||
"""Normalize relative paths in stdio MCP servers to absolute paths based on the skill directory."""
|
||||
normalized_servers = copy.deepcopy(servers)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "pmda-drug-info",
|
||||
"version": "0.1.0",
|
||||
"description": "PMDA drug information tools for Japanese pharmaceutical package insert queries. Provides drug search, master info, interactions, restrictions, dosing, and full-text chapter retrieval via PostgreSQL + OpenSearch.",
|
||||
"hooks": {
|
||||
"PrePrompt": [
|
||||
@ -9,14 +10,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"mainAgentHiddenTools": [
|
||||
"search_drugs",
|
||||
"list_categories",
|
||||
"list_drugs_in_category",
|
||||
"get_drug_master",
|
||||
"get_drug_interactions",
|
||||
"get_drug_restrictions",
|
||||
"get_drug_dosing",
|
||||
"search_section_text",
|
||||
"list_drug_chapters",
|
||||
"read_drug_chapter"
|
||||
],
|
||||
"mcpServers": {
|
||||
"pmda_drug_info": {
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"command": "python3",
|
||||
"args": [
|
||||
"./pmda_server.py"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"PMDA_PG_HOST": "postgres-db",
|
||||
"PMDA_PG_PORT": "5432",
|
||||
"PMDA_PG_DB": "gptbase",
|
||||
"PMDA_PG_USER": "postgres",
|
||||
"PMDA_PG_PASSWORD": "yRhnjSnhufuxNcCxFtPctXnTbAKS2jT2",
|
||||
"PMDA_OPENSEARCH_URL": "http://admin:admin@opensearch-node:9200",
|
||||
"PMDA_OS_INDEX": "pmda_sections"
|
||||
}
|
||||
}
|
||||
},
|
||||
"category": "Developer Tools"
|
||||
}
|
||||
}
|
||||
|
||||
76
skills/developing/pmda-drug-info/README.md
Normal file
76
skills/developing/pmda-drug-info/README.md
Normal file
@ -0,0 +1,76 @@
|
||||
# pmda-drug-info — Claude Code MCP plugin
|
||||
|
||||
PMDA 添付文書ベース医薬指導 Q&A の MCP plugin(hu-sandbox/pmda v2e 98/100 baseline)。
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
```
|
||||
Claude Code (agent) → MCP stdio → pmda_server.py
|
||||
├─ PG queries (drug_master / interaction / restriction / dosing)
|
||||
└─ OS search (pmda_sections, sudachi C-mode)
|
||||
```
|
||||
|
||||
10 tools: `search_drugs`, `list_categories`, `list_drugs_in_category`,
|
||||
`get_drug_master`, `get_drug_interactions`, `get_drug_restrictions`,
|
||||
`get_drug_dosing`, `search_section_text`, `list_drug_chapters`,
|
||||
`read_drug_chapter`.
|
||||
|
||||
4 sub-agents(`agents/*.md`): `single_drug`, `interaction`,
|
||||
`patient_specific`, `adverse_event`.
|
||||
|
||||
## Plugin は独立配布
|
||||
|
||||
`mygpt.*` への依存なし。PG/OS への接続情報を環境変数で渡すだけで動く。
|
||||
`queries.py` / `db.py` / `os_client.py` / `taxonomy.py` / `drug_category.md`
|
||||
は hu-sandbox/pmda からコピーした自己完結セット。
|
||||
|
||||
## 環境変数
|
||||
|
||||
```
|
||||
PMDA_PG_HOST Postgres ホスト (例: tunnel / pg.example.com)
|
||||
PMDA_PG_PORT Postgres ポート (default 5432)
|
||||
PMDA_PG_DB Postgres DB (例: gptbase)
|
||||
PMDA_PG_USER Postgres ユーザ
|
||||
PMDA_PG_PASSWORD Postgres パスワード
|
||||
|
||||
PMDA_OPENSEARCH_URL OpenSearch URL (例: http://admin:admin@tunnel:9200)
|
||||
PMDA_OS_INDEX OS index 名(default: pmda_sections)
|
||||
```
|
||||
|
||||
## インストール
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
# Plugin ディレクトリを Claude Code に登録(PROJECT 単位で)
|
||||
/plugin install path/to/pmda-drug-info
|
||||
```
|
||||
|
||||
`.claude-plugin/plugin.json` の `mcpServers.pmda_drug_info` が
|
||||
`python ./pmda_server.py` を stdio MCP サーバとして起動する。
|
||||
|
||||
## データ準備
|
||||
|
||||
Plugin は読み取り専用。データ投入は gbase-onprem の folder-connector
|
||||
+ PmdaXmlPipeline(`mygpt/plugins/pmda/pipeline.py`)が一括管理する。
|
||||
|
||||
- PG: aerich migration `migrations/models/263_*_add_pmda_tables.py` で 4 表作成
|
||||
- OS: `pmda_sections` index は `mygpt/plugins/pmda/os_index.py` の DDL を
|
||||
pipeline 初期化時に適用
|
||||
- データ投入: folder-connector で PMDA XML を登録すると 9 ステップ
|
||||
pipeline が OS bulk index + PG fact 抽出を実行
|
||||
|
||||
詳細は `docs/pmda-sync-flow.md` を参照。
|
||||
|
||||
## 動作確認
|
||||
|
||||
```bash
|
||||
# stdio MCP リクエストを手動で投げる
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"initialize"}' | python pmda_server.py
|
||||
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | python pmda_server.py
|
||||
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_drugs","arguments":{"query":"ロサルタン"}}}' | python pmda_server.py
|
||||
```
|
||||
@ -6,26 +6,31 @@ description: Reverse lookup drugs by adverse event name. Find which drugs have r
|
||||
tools: search_section_text, search_drugs, get_drug_master, list_drug_chapters, read_drug_chapter
|
||||
---
|
||||
|
||||
あなたは「副作用 → 該当薬剤の逆引き」専門の sub-agent です。
|
||||
You are a sub-agent specialized in reverse lookup from an adverse event to the drugs that report it.
|
||||
|
||||
【ツール戦略】
|
||||
1. `search_section_text(keyword=副作用名, section_filter="副作用")` で逆引き。
|
||||
total_drugs は必ず本文中に明示する。
|
||||
2. 同義語が必要なケース:
|
||||
## Tool Strategy
|
||||
1. Reverse-lookup with `search_section_text(keyword=<adverse event name>, section_filter="副作用")`. Always state `total_drugs` explicitly in the answer.
|
||||
2. Synonyms are handled automatically — OpenSearch's synonym filter expands them in a single search, e.g.:
|
||||
"Stevens-Johnson" ⇔ "皮膚粘膜眼症候群" / "SJS"
|
||||
"QT延長" ⇔ "Torsades de pointes"
|
||||
"間質性肺炎" ⇔ "肺臓炎"
|
||||
OS の synonym filter が自動展開するので 1 回の検索で OK。
|
||||
3. hit から代表薬を 3〜5 件選び、`read_drug_chapter` で 11.1 重大な副作用 / 11.2 その他の副作用
|
||||
verbatim を引用。
|
||||
4. 因果推論("この薬がこの患者の症状を起こした")は **絶対しない**。
|
||||
情報提示のみ。
|
||||
3. From the hits, pick 3–5 representative drugs and quote "11.1 重大な副作用" / "11.2 その他の副作用" verbatim with `read_drug_chapter`.
|
||||
4. NEVER make causal inferences (e.g. "this drug caused this patient's symptom"). Information presentation only.
|
||||
|
||||
【絶対ルール】
|
||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
||||
## Absolute Rules
|
||||
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||
4. If the information cannot be found, write "添付文書からは確認できません".
|
||||
|
||||
## Citation Requirements (clickable `<CITATION>` tags)
|
||||
Every tool result record ALSO ends with a `CITATION:` line — a pre-built `<CITATION file="..." filename="..." />` clickable tag that the frontend PDF-highlight pipeline depends on. Your FINAL answer (the text returned to the main agent) MUST include these tags, in addition to the `[出典: ...]` text — otherwise the citation is not clickable and the tag is lost.
|
||||
- Copy the record's `CITATION:` line VERBATIM (byte-for-byte) immediately after the fact-grounded paragraph or bullet. NEVER collect tags at the end of the answer.
|
||||
- Do NOT add, modify, reorder, or remove any attribute. Do NOT construct a `<CITATION>` tag yourself.
|
||||
- At most one `<CITATION>` per unique file.
|
||||
- `read_drug_chapter` returns the `<CITATION>` already embedded in its header/footer — copy it as-is.
|
||||
- Records without a `CITATION:` line → emit the `[出典: ...]` text only; never fabricate an empty tag.
|
||||
- An answer that states facts but contains zero `<CITATION>` tags is a failed answer.
|
||||
|
||||
@ -5,24 +5,31 @@ description: Investigate drug-drug interactions between two drugs, or list all i
|
||||
tools: search_drugs, get_drug_master, get_drug_interactions, search_section_text, list_drug_chapters, read_drug_chapter
|
||||
---
|
||||
|
||||
あなたは「薬剤間相互作用」専門の sub-agent です。
|
||||
You are a sub-agent specialized in drug-drug interactions.
|
||||
|
||||
【ツール戦略】
|
||||
- A・B 両薬の yj_code を `search_drugs` で取得。
|
||||
- `get_drug_interactions(drug_a_yj=A, drug_b_yj=B)` で双方向検索(A→B も B→A も拾える)。
|
||||
- ヒットしたら drug_a の側の出典 section(10.1 / 10.2)を `list_drug_chapters` + `read_drug_chapter` で
|
||||
verbatim 取得。drug_b 側にも該当記載があるか確認。
|
||||
- ヒットゼロ → "添付文書上は併用禁忌・併用注意の明確な記載なし" と書く(自由記述/警告等は
|
||||
別途 `search_section_text(keyword=B薬名, section_filter="相互作用")` で念押し)。
|
||||
- 1 薬名のみ与えられた場合は `get_drug_interactions(drug_a_yj=...)` で全相互作用一覧。
|
||||
## Tool Strategy
|
||||
- Get the yj_code of both drugs A and B with `search_drugs`.
|
||||
- Search both directions with `get_drug_interactions(drug_a_yj=A, drug_b_yj=B)` (catches A→B and B→A).
|
||||
- On a hit, retrieve the citing section on drug A's side (10.1 / 10.2) verbatim with `list_drug_chapters` + `read_drug_chapter`. Also check whether drug B's side carries a matching statement.
|
||||
- On zero hits, write "添付文書上は併用禁忌・併用注意の明確な記載なし" (free-text warnings can be double-checked separately with `search_section_text(keyword=<drug B name>, section_filter="相互作用")`).
|
||||
- If only one drug name is given, list all interactions with `get_drug_interactions(drug_a_yj=...)`.
|
||||
|
||||
severity は本文の "併用禁忌" / "併用注意" の語をそのまま転記。
|
||||
Copy the `severity` field verbatim using the source wording "併用禁忌" / "併用注意".
|
||||
|
||||
【絶対ルール】
|
||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
||||
## Absolute Rules
|
||||
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||
4. If the information cannot be found, write "添付文書からは確認できません".
|
||||
|
||||
## Citation Requirements (clickable `<CITATION>` tags)
|
||||
Every tool result record ALSO ends with a `CITATION:` line — a pre-built `<CITATION file="..." filename="..." />` clickable tag that the frontend PDF-highlight pipeline depends on. Your FINAL answer (the text returned to the main agent) MUST include these tags, in addition to the `[出典: ...]` text — otherwise the citation is not clickable and the tag is lost.
|
||||
- Copy the record's `CITATION:` line VERBATIM (byte-for-byte) immediately after the fact-grounded paragraph or bullet. NEVER collect tags at the end of the answer.
|
||||
- Do NOT add, modify, reorder, or remove any attribute. Do NOT construct a `<CITATION>` tag yourself.
|
||||
- At most one `<CITATION>` per unique file.
|
||||
- `read_drug_chapter` returns the `<CITATION>` already embedded in its header/footer — copy it as-is.
|
||||
- Records without a `CITATION:` line → emit the `[出典: ...]` text only; never fabricate an empty tag.
|
||||
- An answer that states facts but contains zero `<CITATION>` tags is a failed answer.
|
||||
|
||||
@ -5,28 +5,36 @@ description: Determine drug administration feasibility and dosage adjustment for
|
||||
tools: search_drugs, get_drug_master, get_drug_restrictions, get_drug_dosing, list_drug_chapters, read_drug_chapter
|
||||
---
|
||||
|
||||
あなたは「特定患者への投与可否・用量調整」専門の sub-agent です。
|
||||
You are a sub-agent specialized in administration feasibility and dosage adjustment for specific patients.
|
||||
|
||||
【ツール戦略】
|
||||
1. 薬名から yj_code を `search_drugs` で取得。
|
||||
2. 患者条件を condition_type に対応付け:
|
||||
- 腎機能 (eGFR/CrCl) → "腎機能障害"
|
||||
- 肝機能 (Child-Pugh) → "肝機能障害"
|
||||
- 妊娠/授乳 → "妊婦"/"授乳婦"
|
||||
- 年齢 (小児/高齢) → "小児等"/"高齢者"
|
||||
- アレルギー既往 → "過敏症"
|
||||
- 合併症 (糖尿病/喘息など) → "疾患"
|
||||
3. `get_drug_restrictions(drug_yj=..., condition_type=...)` で該当 restriction を取得。
|
||||
condition_params の数値(例: {"eGFR_max": 30})を必ず確認。
|
||||
4. `get_drug_dosing(drug_yj=..., patient_segment=...)` で患者層別用量を取得。
|
||||
5. 必要なら原文 `read_drug_chapter` で 9.x 章 verbatim 引用。
|
||||
6. 数値判定(例: eGFR=25 ⇔ eGFR_max=30 → 該当)を agent が責任もって行う。
|
||||
## Tool Strategy
|
||||
1. Get the yj_code from the drug name with `search_drugs`.
|
||||
2. Map the patient condition to a `condition_type`:
|
||||
- Renal function (eGFR/CrCl) → "腎機能障害"
|
||||
- Hepatic function (Child-Pugh) → "肝機能障害"
|
||||
- Pregnancy / lactation → "妊婦" / "授乳婦"
|
||||
- Age (pediatric / elderly) → "小児等" / "高齢者"
|
||||
- Allergy history → "過敏症"
|
||||
- Comorbidity (diabetes, asthma, etc.) → "疾患"
|
||||
3. Get the matching restriction with `get_drug_restrictions(drug_yj=..., condition_type=...)`. Always check the `condition_params` values (e.g. `{"eGFR_max": 30}`).
|
||||
4. Get patient-segment dosing with `get_drug_dosing(drug_yj=..., patient_segment=...)`.
|
||||
5. When needed, quote the 9.x chapter verbatim via `read_drug_chapter`.
|
||||
6. The agent is responsible for the numeric judgment (e.g. eGFR=25 vs eGFR_max=30 → applies).
|
||||
|
||||
【絶対ルール】
|
||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
||||
## Absolute Rules
|
||||
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||
4. If the information cannot be found, write "添付文書からは確認できません".
|
||||
|
||||
## Citation Requirements (clickable `<CITATION>` tags)
|
||||
Every tool result record ALSO ends with a `CITATION:` line — a pre-built `<CITATION file="..." filename="..." />` clickable tag that the frontend PDF-highlight pipeline depends on. Your FINAL answer (the text returned to the main agent) MUST include these tags, in addition to the `[出典: ...]` text — otherwise the citation is not clickable and the tag is lost.
|
||||
- Copy the record's `CITATION:` line VERBATIM (byte-for-byte) immediately after the fact-grounded paragraph or bullet. NEVER collect tags at the end of the answer.
|
||||
- Do NOT add, modify, reorder, or remove any attribute. Do NOT construct a `<CITATION>` tag yourself.
|
||||
- At most one `<CITATION>` per unique file.
|
||||
- `read_drug_chapter` returns the `<CITATION>` already embedded in its header/footer — copy it as-is.
|
||||
- Records without a `CITATION:` line → emit the `[出典: ...]` text only; never fabricate an empty tag.
|
||||
- An answer that states facts but contains zero `<CITATION>` tags is a failed answer.
|
||||
|
||||
@ -5,22 +5,30 @@ description: Answer factual questions about a single drug (brand name, generic n
|
||||
tools: search_drugs, get_drug_master, get_drug_dosing, get_drug_restrictions, list_drug_chapters, read_drug_chapter
|
||||
---
|
||||
|
||||
あなたは「単一薬の事実回答」専門の sub-agent です。
|
||||
You are a sub-agent specialized in factual answers about a single drug.
|
||||
|
||||
【ツール戦略】
|
||||
1. 質問から薬名/yj_code を特定 → `search_drugs` または直接 yj_code が分かれば次へ。
|
||||
2. `get_drug_master(yj_code)` で基本情報(販売名・一般名・薬効分類・規制)を確定。
|
||||
3. 必要に応じて `get_drug_dosing` で用法用量、`get_drug_restrictions(drug_yj=...)` で禁忌・特定患者注意。
|
||||
4. 自由記述や上記テーブルに無い情報(例: 重大な副作用一覧、薬物動態の数値)は
|
||||
`list_drug_chapters(yj_full)` → `read_drug_chapter(yj_full, section_title)` で原文取得。
|
||||
## Tool Strategy
|
||||
1. Identify the drug name / yj_code from the question → use `search_drugs`, or go straight ahead if the yj_code is already known.
|
||||
2. Confirm basic info (brand name, generic name, pharmacological category, regulation) with `get_drug_master(yj_code)`.
|
||||
3. As needed, use `get_drug_dosing` for dosing and `get_drug_restrictions(drug_yj=...)` for contraindications / patient-specific precautions.
|
||||
4. For free-text details not in the fact tables (e.g. the full list of serious adverse reactions, pharmacokinetic values), retrieve the source text with `list_drug_chapters(yj_full)` → `read_drug_chapter(yj_full, section_title)`.
|
||||
|
||||
最終回答は箇条書き or 表で、各事実に出典を付ける。
|
||||
Present the final answer as bullets or a table, attaching a citation to every fact.
|
||||
|
||||
【絶対ルール】
|
||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
||||
## Absolute Rules
|
||||
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||
4. If the information cannot be found, write "添付文書からは確認できません".
|
||||
|
||||
## Citation Requirements (clickable `<CITATION>` tags)
|
||||
Every tool result record ALSO ends with a `CITATION:` line — a pre-built `<CITATION file="..." filename="..." />` clickable tag that the frontend PDF-highlight pipeline depends on. Your FINAL answer (the text returned to the main agent) MUST include these tags, in addition to the `[出典: ...]` text — otherwise the citation is not clickable and the tag is lost.
|
||||
- Copy the record's `CITATION:` line VERBATIM (byte-for-byte) immediately after the fact-grounded paragraph or bullet. NEVER collect tags at the end of the answer.
|
||||
- Do NOT add, modify, reorder, or remove any attribute. Do NOT construct a `<CITATION>` tag yourself.
|
||||
- At most one `<CITATION>` per unique file.
|
||||
- `read_drug_chapter` returns the `<CITATION>` already embedded in its header/footer — copy it as-is.
|
||||
- Records without a `CITATION:` line → emit the `[出典: ...]` text only; never fabricate an empty tag.
|
||||
- An answer that states facts but contains zero `<CITATION>` tags is a failed answer.
|
||||
|
||||
80
skills/developing/pmda-drug-info/db.py
Normal file
80
skills/developing/pmda-drug-info/db.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Postgres 连接 helper。
|
||||
|
||||
配置全部走环境变量,默认指向 docker-compose 起的本地实例:
|
||||
|
||||
PMDA_PG_HOST (默认 localhost)
|
||||
PMDA_PG_PORT (默认 5432)
|
||||
PMDA_PG_DB (默认 pmda)
|
||||
PMDA_PG_USER (默认 pmda)
|
||||
PMDA_PG_PASSWORD (默认 pmda_local_dev — 仅本地开发,生产由 secret 注入)
|
||||
|
||||
`connect()` 返回 psycopg3 connection(autocommit=False)。
|
||||
长时跑批时使用 `pool()` 取 ConnectionPool。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
|
||||
import psycopg
|
||||
from psycopg import Connection
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
PG_HOST = os.environ.get("PMDA_PG_HOST", "localhost")
|
||||
PG_PORT = int(os.environ.get("PMDA_PG_PORT", "5432"))
|
||||
PG_DB = os.environ.get("PMDA_PG_DB", "pmda")
|
||||
PG_USER = os.environ.get("PMDA_PG_USER", "pmda")
|
||||
PG_PASSWORD = os.environ.get("PMDA_PG_PASSWORD", "pmda_local_dev")
|
||||
|
||||
|
||||
def conninfo() -> str:
|
||||
return (
|
||||
f"host={PG_HOST} port={PG_PORT} dbname={PG_DB} "
|
||||
f"user={PG_USER} password={PG_PASSWORD}"
|
||||
)
|
||||
|
||||
|
||||
def connect(*, autocommit: bool = False) -> Connection:
|
||||
"""Open a single connection. Caller is responsible for closing."""
|
||||
return psycopg.connect(conninfo(), autocommit=autocommit)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session(*, autocommit: bool = False) -> Iterator[Connection]:
|
||||
"""`with session() as conn:` — auto close on exit."""
|
||||
conn = connect(autocommit=autocommit)
|
||||
try:
|
||||
yield conn
|
||||
if not autocommit:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
if not autocommit:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
_pool: ConnectionPool | None = None
|
||||
|
||||
|
||||
def pool(min_size: int = 1, max_size: int = 8) -> ConnectionPool:
|
||||
"""Lazy-init module-level pool. Use for batch / agent-loop hot path."""
|
||||
global _pool
|
||||
if _pool is None:
|
||||
_pool = ConnectionPool(
|
||||
conninfo(),
|
||||
min_size=min_size,
|
||||
max_size=max_size,
|
||||
kwargs={"autocommit": False},
|
||||
open=True,
|
||||
)
|
||||
return _pool
|
||||
|
||||
|
||||
def close_pool() -> None:
|
||||
global _pool
|
||||
if _pool is not None:
|
||||
_pool.close()
|
||||
_pool = None
|
||||
206
skills/developing/pmda-drug-info/drug_category.md
Normal file
206
skills/developing/pmda-drug-info/drug_category.md
Normal file
@ -0,0 +1,206 @@
|
||||
|
||||
- 11 中枢神経系用薬
|
||||
- 111 全身麻酔剤
|
||||
- 112 催眠鎮静剤,抗不安剤
|
||||
- 113 抗てんかん剤
|
||||
- 114 解熱鎮痛消炎剤
|
||||
- 115 興奮剤,覚醒剤
|
||||
- 116 抗パーキンソン剤
|
||||
- 117 精神神経用剤
|
||||
- 118 総合感冒剤
|
||||
- 119 その他の中枢神経系用薬
|
||||
- 12 末梢神経用薬
|
||||
- 121 局所麻酔剤
|
||||
- 122 骨格筋弛緩剤
|
||||
- 123 自律神経剤
|
||||
- 124 鎮けい剤
|
||||
- 125 発汗剤,止汗剤
|
||||
- 129 その他の末梢神経系用薬
|
||||
- 13 感覚器用薬
|
||||
- 131 眼科用剤
|
||||
- 132 耳鼻科用剤
|
||||
- 133 鎮暈剤
|
||||
- 139 その他の感覚器官用薬
|
||||
- 19 その他の神経系及び感覚器官用医薬品
|
||||
- 21 循環器官用薬
|
||||
- 211 強心剤
|
||||
- 212 不整脈用剤
|
||||
- 213 利尿剤
|
||||
- 214 血圧降下剤
|
||||
- 215 血管補強剤
|
||||
- 216 血管収縮剤
|
||||
- 217 血管拡張剤
|
||||
- 218 高脂血症用剤
|
||||
- 219 その他の循環器官用薬
|
||||
- 22 呼吸器官用薬
|
||||
- 221 呼吸促進剤
|
||||
- 222 鎮咳剤
|
||||
- 223 去たん剤
|
||||
- 224 鎮咳去たん剤
|
||||
- 225 気管支拡張剤
|
||||
- 226 含嗽剤
|
||||
- 229 その他の呼吸器官用薬
|
||||
- 23 消化器官用薬
|
||||
- 231 止しゃ剤,整腸剤
|
||||
- 232 消化性潰瘍用剤
|
||||
- 233 健胃消化剤
|
||||
- 234 制酸剤
|
||||
- 235 下剤,浣腸剤
|
||||
- 236 利胆剤
|
||||
- 237 複合胃腸剤
|
||||
- 239 その他の消化器官用薬
|
||||
- 24 ホルモン剤(抗ホルモン剤を含む)
|
||||
- 241 脳下垂体ホルモン剤
|
||||
- 242 唾液腺ホルモン剤
|
||||
- 243 甲状腺,副甲状腺ホルモン剤
|
||||
- 244 たん白同化ステロイド剤
|
||||
- 245 副腎ホルモン剤
|
||||
- 246 男性ホルモン剤
|
||||
- 247 卵胞ホルモン及び黄体ホルモン剤
|
||||
- 248 混合ホルモン剤
|
||||
- 249 その他のホルモン剤(抗ホルモン剤を含む)
|
||||
- 25 泌尿生殖器官及び肛門用薬
|
||||
- 251 泌尿器官用剤
|
||||
- 252 生殖器官用剤(性病予防剤を含む。)
|
||||
- 253 子宮収縮剤
|
||||
- 254 避妊剤
|
||||
- 255 痔疾用剤
|
||||
- 259 その他の泌尿生殖器官及び肛門用薬
|
||||
- 26 外皮用薬
|
||||
- 261 外皮用殺菌消毒剤
|
||||
- 262 創傷保護剤
|
||||
- 263 化膿性疾患用剤
|
||||
- 264 鎮痛,鎮痒,収歛,消炎剤
|
||||
- 265 寄生性皮ふ疾患用剤
|
||||
- 266 皮ふ軟化剤(腐しょく剤を含む。)
|
||||
- 267 毛髪用剤(発毛剤,脱毛剤,染毛剤,養毛剤
|
||||
- 268 浴剤
|
||||
- 269 その他の外皮用薬
|
||||
- 27 歯科口腔用薬
|
||||
- 271 歯科用局所麻酔剤
|
||||
- 272 歯髄失活剤
|
||||
- 273 歯科用鎮痛鎮静剤(根管及び齲窩消毒剤を含
|
||||
- 274 歯髄乾屍剤(根管充填剤を含む。)
|
||||
- 275 歯髄覆たく剤
|
||||
- 276 歯科用抗生物質製剤
|
||||
- 279 その他の歯科口腔用薬
|
||||
- 29 その他の個々の器官系用医薬品
|
||||
- 290 その他の個々の器官系用医薬品
|
||||
- 31 ビタミン剤
|
||||
- 311 ビタミンA及びD剤
|
||||
- 312 ビタミンB1剤
|
||||
- 313 ビタミンB剤(ビタミンB1剤を除く。)
|
||||
- 314 ビタミンC剤
|
||||
- 315 ビタミンE剤
|
||||
- 316 ビタミンK剤
|
||||
- 317 混合ビタミン剤(ビタミンA・D混合製剤を除く)
|
||||
- 319 その他のビタミン剤
|
||||
- 32 滋養強壮薬
|
||||
- 321 カルシウム剤
|
||||
- 322 無機質製剤
|
||||
- 323 糖類剤
|
||||
- 324 有機酸製剤
|
||||
- 325 たん白アミノ酸製剤
|
||||
- 326 臓器製剤
|
||||
- 327 乳幼児用剤
|
||||
- 329 その他の滋養強壮薬
|
||||
- 33 血液・体液用薬
|
||||
- 331 血液代用剤
|
||||
- 332 止血剤
|
||||
- 333 血液凝固阻止剤
|
||||
- 339 その他の血液・体液用薬
|
||||
- 34 人工透析用薬
|
||||
- 341 人工腎臓透析用剤
|
||||
- 342 腹膜透析用剤
|
||||
- 349 その他の人工透析用薬
|
||||
- 39 その他の代謝性医薬品
|
||||
- 391 肝臓疾患用剤
|
||||
- 392 解毒剤
|
||||
- 393 習慣性中毒用剤
|
||||
- 394 痛風治療剤
|
||||
- 395 酵素製剤
|
||||
- 396 糖尿病用剤
|
||||
- 397 総合代謝性製剤
|
||||
- 399 他に分類されない代謝性医薬品
|
||||
- 41 細胞賦活用薬
|
||||
- 411 クロロフィル製剤
|
||||
- 412 色素製剤
|
||||
- 419 その他の細胞賦活用薬
|
||||
- 42 腫瘍用薬
|
||||
- 421 アルキル化剤
|
||||
- 422 代謝拮抗剤
|
||||
- 423 抗腫瘍性抗生物質製剤
|
||||
- 424 抗腫瘍性植物成分製剤
|
||||
- 429 その他の腫瘍用薬
|
||||
- 43 放射性医薬品
|
||||
- 430 放射性医薬品
|
||||
- 44 アレルギー用薬
|
||||
- 441 抗ヒスタミン剤
|
||||
- 442 刺激療法剤
|
||||
- 443 非特異性免疫原製剤
|
||||
- 449 その他のアレルギー用薬
|
||||
- 49 その他の組織細胞機能用医薬品
|
||||
- 490 その他の組織細胞機能用医薬品
|
||||
- 51 生薬
|
||||
- 510 生薬
|
||||
- 52 漢方製剤
|
||||
- 520 漢方製剤
|
||||
- 59 その他の生薬及び漢方処方に基づく医薬品
|
||||
- 590 その他の生薬及び漢方処方に基づく医薬品
|
||||
- 61 抗生物質製剤
|
||||
- 611 主としてグラム陽性菌に作用するもの
|
||||
- 612 主としてグラム陰性菌に作用するもの
|
||||
- 613 主としてグラム陽性・陰性菌に作用するもの
|
||||
- 614 主としてグラム陽性菌,マイコプラズマに作用するもの
|
||||
- 615 主としてグラム陽性・陰性菌,リケッチア,クラミジアに作用するもの
|
||||
- 616 主として抗酸菌に作用するもの
|
||||
- 617 主としてカビに作用するもの
|
||||
- 619 その他の抗生物質製剤(複合抗生物質製剤を含む)
|
||||
- 62 化学療法剤
|
||||
- 621 サルファ剤
|
||||
- 622 抗結核剤
|
||||
- 623 抗ハンセン病剤
|
||||
- 624 合成抗菌剤
|
||||
- 625 抗ウイルス剤
|
||||
- 629 その他の化学療法剤
|
||||
- 63 生物学的製剤
|
||||
- 631 ワクチン類
|
||||
- 632 毒素及びトキソイド類
|
||||
- 633 抗毒素類及び抗レプトスピラ血清類
|
||||
- 634 血液製剤類
|
||||
- 635 生物学的試験用製剤類
|
||||
- 636 混合生物学的製剤
|
||||
- 639 その他の生物学的製剤
|
||||
- 64 寄生動物用薬
|
||||
- 641 抗原虫剤
|
||||
- 642 駆虫剤
|
||||
- 649 その他の寄生動物用薬
|
||||
- 69 その他の病原生物に対する医薬品
|
||||
- 690 その他の病原生物に対する医薬品
|
||||
- 71 調剤用薬
|
||||
- 711 賦形剤
|
||||
- 712 軟膏基剤
|
||||
- 713 溶解剤
|
||||
- 714 矯味,矯臭,着色剤
|
||||
- 715 乳化剤
|
||||
- 719 その他の調剤用薬
|
||||
- 72 診断用薬(体外診断用医薬品を除く)
|
||||
- 721 X線造影剤
|
||||
- 722 機能検査用試薬
|
||||
- 729 その他の診断用薬
|
||||
- 73 公衆衛生用薬
|
||||
- 731 防腐剤
|
||||
- 732 防疫用殺菌消毒剤
|
||||
- 733 防虫剤
|
||||
- 734 殺虫剤
|
||||
- 735 殺そ剤
|
||||
- 739 その他の公衆衛生用薬
|
||||
- 79 その他の治療を主目的としない医薬品
|
||||
- 791 ばん創こう
|
||||
- 799 他に分類されない治療を主目的としない医薬品
|
||||
- 81 アルカロイド系麻薬(天然麻薬)
|
||||
- 811 アヘンアルカロイド系麻薬
|
||||
- 812 コカアルカロイド系製剤
|
||||
- 819 その他のアルカロイド系麻薬(天然麻薬)
|
||||
- 82 非アルカロイド系麻薬
|
||||
- 821 合成麻薬
|
||||
@ -2,19 +2,92 @@
|
||||
|
||||
You have access to Japanese pharmaceutical package insert (添付文書) data via the following tools.
|
||||
|
||||
## Tool output format
|
||||
|
||||
Tools return **plain text**, not JSON. Each result has:
|
||||
- A `=== CITATION INSTRUCTIONS ===` header (only when the result carries citable sources).
|
||||
- A `Found N ...:` summary line, then one numbered record block per row.
|
||||
- Inside each block: indented `label: value` fields, an optional `出典: [...]` line, and a
|
||||
`CITATION: <CITATION ... />` line (the pre-built clickable tag).
|
||||
|
||||
When a query matches nothing, the tool instead returns a short English message starting with
|
||||
`No matching ... were found` and **no** citation instructions. In that case tell the user no
|
||||
relevant material was retrieved — **do NOT** invent or emit any `<CITATION>` tag.
|
||||
|
||||
## Core Rules
|
||||
- **Tool calls are mandatory.** Never answer from training knowledge alone. All facts must come from tool results.
|
||||
- Cite sources in the format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`
|
||||
- Fact table rows include a `_citation` field — use it directly.
|
||||
- Cite sources in the format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` — taken from each record's `出典:` line.
|
||||
- Generic citations like `[出典: 薬品マスター]` or `[出典: 添付文書]` are **prohibited**.
|
||||
- For urgent questions (suicide/drug abuse/severe acute symptoms), state: "緊急対応として担当医・薬剤師に直接相談してください"
|
||||
|
||||
## Clickable Citation (<CITATION> tag) — MUST copy the record's `CITATION:` line
|
||||
|
||||
After each fact-grounded paragraph or bullet list, copy that record's **`CITATION:` line VERBATIM**. Do NOT construct the tag yourself.
|
||||
|
||||
### Why verbatim copy
|
||||
|
||||
The tool already built the full `<CITATION ... />` string for you on the `CITATION:` line. It contains:
|
||||
- Generic CITATION core attributes (`file`, `filename`, `page`) for the existing PDF highlight pipeline.
|
||||
- PMDA-specific attributes (`yj_full`, `brand`, `section`) for richer frontend display.
|
||||
|
||||
If you rebuild it yourself, you risk hallucinating `file=` filenames or dropping attributes. **Just copy the `CITATION:` line byte-for-byte** (drop the leading `CITATION: ` label, keep the `<CITATION ... />` tag).
|
||||
|
||||
### Rules
|
||||
|
||||
- Each record's `CITATION:` line is the complete `<CITATION ... />` string.
|
||||
- **Emit it exactly as-is. Do not modify, paraphrase, summarize, reorder, add, or remove any character.**
|
||||
- Do NOT assemble a tag from the `出典:` text or other fields — they are for reference only.
|
||||
- If a record has **no** `CITATION:` line, emit only the `[出典: ...]` text — never invent any CITATION attributes.
|
||||
|
||||
### Multiple citations within the same paragraph
|
||||
|
||||
- Each fact record gets its own `<CITATION>` tag — emit the `CITATION:` line from that record.
|
||||
- Within the same paragraph, if the same `(file, section)` pair would repeat — emit it only once.
|
||||
- Same drug × different sections: one tag per section, back-to-back.
|
||||
- Different drugs: each tag stands alone.
|
||||
|
||||
### Example (LLM-side view)
|
||||
|
||||
Tool returns (plain text):
|
||||
```
|
||||
[1] 〔東洋〕半夏厚朴湯エキス細粒
|
||||
generic: 半夏厚朴湯
|
||||
yj_full: 1399999X9999_1_01
|
||||
出典: [出典: 〔東洋〕半夏厚朴湯エキス細粒 (yj_full=1399999X9999_1_01) / 6. 用法及び用量]
|
||||
CITATION: <CITATION file="abc-uuid" filename="999999_1399999X9999_1_01.xml" page=0 yj_full="1399999X9999_1_01" brand="〔東洋〕半夏厚朴湯エキス細粒" section="6. 用法及び用量" />
|
||||
```
|
||||
|
||||
Your reply (correct):
|
||||
```
|
||||
用法は 1日3回。
|
||||
<CITATION file="abc-uuid" filename="999999_1399999X9999_1_01.xml" page=0 yj_full="1399999X9999_1_01" brand="〔東洋〕半夏厚朴湯エキス細粒" section="6. 用法及び用量" />
|
||||
```
|
||||
|
||||
Your reply (WRONG — reconstructed by hand):
|
||||
```
|
||||
用法は 1日3回。
|
||||
<CITATION file="千里牛香_添付文書.pdf" /> ← hallucinated, missing attributes
|
||||
```
|
||||
|
||||
## Citation Requirements
|
||||
|
||||
- You MUST emit a `<CITATION ... />` tag whenever you use a tool result. Copy the record's `CITATION:` line verbatim — never construct one.
|
||||
- Place each citation IMMEDIATELY AFTER the paragraph or bullet list that uses the fact. NEVER collect citations at the end of the response.
|
||||
- At most one tag per unique file. At least one `<CITATION>` is required whenever the answer is grounded in tool results.
|
||||
- An answer that states tool-grounded facts but contains zero `<CITATION>` tags is a failed answer.
|
||||
|
||||
## When to Use Sub-agents (task tool)
|
||||
- **patient_specific**: Renal/hepatic/pregnancy/elderly/pediatric/allergy conditions × dosing decisions
|
||||
- **interaction**: Pairwise drug interaction investigation
|
||||
- **adverse_event**: Reverse lookup from adverse event name to drugs
|
||||
- **single_drug**: Detailed info not in fact tables (e.g., full adverse event list, pharmacokinetics)
|
||||
|
||||
### Sub-agent citation pass-through (CRITICAL)
|
||||
- A sub-agent's returned text already contains `<CITATION ... />` tags built from the tools it called. The original tag attributes (`file`/`filename`) only exist inside that returned text — you cannot reconstruct them.
|
||||
- You MUST preserve every `<CITATION ... />` tag from the sub-agent output VERBATIM and re-emit it in your final answer, keeping it immediately after the fact it supports.
|
||||
- NEVER strip, summarize away, paraphrase, or merge these tags when integrating sub-agent results.
|
||||
- A final answer that relies on sub-agent facts but contains zero `<CITATION>` tags is a failed answer.
|
||||
|
||||
## Direct Tool Usage (do NOT delegate)
|
||||
- Simple lookups → use tools directly
|
||||
- Multi-drug comparisons → call tools sequentially, output as markdown table
|
||||
|
||||
157
skills/developing/pmda-drug-info/os_client.py
Normal file
157
skills/developing/pmda-drug-info/os_client.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""OpenSearch `pmda_sections` index spec + client helper.
|
||||
|
||||
Mapping 与 wiki-skill 的 sudachi 配置共用 plugin(同一 OS 集群、同一 sudachi
|
||||
core 字典)。每个 doc 对应一份说明书的一个章节节点,冗余存药品 metadata 以避
|
||||
免 JOIN(详见 design.md §2.1.2)。
|
||||
|
||||
环境变量:
|
||||
OS_HOST (默认 http://localhost:9200,与 wiki-skill `_common.py` 一致)
|
||||
PMDA_OS_INDEX (默认 pmda_sections)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from opensearchpy import OpenSearch
|
||||
|
||||
# Plugin env vars: PMDA_OPENSEARCH_URL(推奨)/ OPENSEARCH_URL / OPENSEARCH_HOST
|
||||
OS_HOST = (
|
||||
os.environ.get("PMDA_OPENSEARCH_URL")
|
||||
or os.environ.get("OPENSEARCH_URL")
|
||||
or os.environ.get("OPENSEARCH_HOST")
|
||||
or "http://localhost:9200"
|
||||
)
|
||||
INDEX_NAME = os.environ.get("PMDA_OS_INDEX", "pmda_sections")
|
||||
|
||||
|
||||
# ---- Mapping spec --------------------------------------------------------
|
||||
|
||||
INDEX_BODY: dict = {
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0,
|
||||
"refresh_interval": "1s",
|
||||
},
|
||||
"analysis": {
|
||||
"tokenizer": {
|
||||
"sudachi_tokenizer": {
|
||||
"type": "sudachi_tokenizer",
|
||||
"split_mode": "C",
|
||||
"discard_punctuation": True,
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
# "med_synonyms": {
|
||||
# "type": "synonym",
|
||||
# 初期最小集 — 命中错例后扩充。同义词条之间逗号分隔代表
|
||||
# 等价、空格视为词内字符。
|
||||
# "synonyms": [
|
||||
# "Stevens-Johnson, 皮膚粘膜眼症候群, SJS",
|
||||
# "中毒性表皮壊死融解症, TEN, ライエル症候群",
|
||||
# "QT延長, トルサード, Torsades de pointes",
|
||||
# "間質性肺炎, 肺臓炎",
|
||||
# "横紋筋融解症, ラブドミオリーシス",
|
||||
# "アナフィラキシー, アナフィラキシーショック",
|
||||
# "無顆粒球症, 顆粒球減少症",
|
||||
# ],
|
||||
# },
|
||||
"jp_pos": {
|
||||
"type": "sudachi_part_of_speech",
|
||||
},
|
||||
"jp_stop": {
|
||||
"type": "sudachi_ja_stop",
|
||||
},
|
||||
},
|
||||
"analyzer": {
|
||||
"jp_med": {
|
||||
"type": "custom",
|
||||
# icu_normalizer はデフォルト image に未含、sudachi_
|
||||
# normalizedform で全角半角・正規化はカバーされる。
|
||||
"tokenizer": "sudachi_tokenizer",
|
||||
"filter": [
|
||||
"sudachi_baseform",
|
||||
"sudachi_normalizedform",
|
||||
"jp_pos",
|
||||
"jp_stop",
|
||||
"lowercase",
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"yj_full": {"type": "keyword"},
|
||||
"yj_code": {"type": "keyword"},
|
||||
"l1_code": {"type": "keyword"},
|
||||
"l2_code": {"type": "keyword"},
|
||||
"l2_name": {"type": "keyword"},
|
||||
"category_name": {"type": "keyword"},
|
||||
"brand_names": {"type": "keyword"},
|
||||
"generic_name": {"type": "keyword"},
|
||||
"section_title": {
|
||||
"type": "text",
|
||||
"analyzer": "jp_med",
|
||||
"fields": {"raw": {"type": "keyword"}},
|
||||
},
|
||||
"line_num": {"type": "integer"},
|
||||
"text": {"type": "text", "analyzer": "jp_med"},
|
||||
"revision_date": {"type": "date"},
|
||||
"_md_sha256": {"type": "keyword"},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---- Client --------------------------------------------------------------
|
||||
|
||||
|
||||
def client() -> OpenSearch:
|
||||
"""Return an OpenSearch client bound to OS_HOST."""
|
||||
return OpenSearch(hosts=[OS_HOST], http_compress=True, timeout=60)
|
||||
|
||||
|
||||
# ---- 章節アクセス helpers(PageIndex 退役後の verbatim 取得経路) -------
|
||||
|
||||
|
||||
def list_drug_sections(yj_full: str, *, limit: int = 200) -> list[dict]:
|
||||
"""1 薬の全章節を line_num 昇順で返す。
|
||||
|
||||
各 element: {section_title, line_num, text_len, brand, generic}
|
||||
"""
|
||||
cli = client()
|
||||
resp = cli.search(index=INDEX_NAME, body={
|
||||
"size": min(limit, 500),
|
||||
"_source": ["section_title", "line_num", "text", "brand_names", "generic_name"],
|
||||
"query": {"term": {"yj_full": yj_full}},
|
||||
"sort": [{"line_num": "asc"}],
|
||||
})
|
||||
out = []
|
||||
for h in resp["hits"]["hits"]:
|
||||
s = h["_source"]
|
||||
out.append({
|
||||
"section_title": s.get("section_title", ""),
|
||||
"line_num": s.get("line_num"),
|
||||
"text_len": len(s.get("text", "") or ""),
|
||||
"brand": (s.get("brand_names") or [""])[0],
|
||||
"generic": s.get("generic_name") or "",
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def get_drug_section_text(yj_full: str, section_title: str) -> str:
|
||||
"""指定 (yj_full, section_title) の verbatim 章節 text。見つからなければ ""。"""
|
||||
cli = client()
|
||||
resp = cli.search(index=INDEX_NAME, body={
|
||||
"size": 1,
|
||||
"_source": ["text"],
|
||||
"query": {"bool": {"must": [
|
||||
{"term": {"yj_full": yj_full}},
|
||||
{"term": {"section_title.raw": section_title}},
|
||||
]}},
|
||||
})
|
||||
hits = resp["hits"]["hits"]
|
||||
if not hits:
|
||||
return ""
|
||||
return hits[0]["_source"].get("text", "") or ""
|
||||
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,12 @@
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["auto", "brand", "generic", "yj"],
|
||||
"enum": [
|
||||
"auto",
|
||||
"brand",
|
||||
"generic",
|
||||
"yj"
|
||||
],
|
||||
"description": "Search type. 'auto' searches all fields.",
|
||||
"default": "auto"
|
||||
},
|
||||
@ -21,7 +26,9 @@
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -48,7 +55,9 @@
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
"required": ["l2_code"]
|
||||
"required": [
|
||||
"l2_code"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -62,7 +71,9 @@
|
||||
"description": "12-character YJ code."
|
||||
}
|
||||
},
|
||||
"required": ["yj_code"]
|
||||
"required": [
|
||||
"yj_code"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -145,7 +156,9 @@
|
||||
"default": 20
|
||||
}
|
||||
},
|
||||
"required": ["drug_yj"]
|
||||
"required": [
|
||||
"drug_yj"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -169,7 +182,9 @@
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["keyword"]
|
||||
"required": [
|
||||
"keyword"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -183,7 +198,9 @@
|
||||
"description": "Full YJ code (with revision suffix, e.g., 3399007H1021_1_21)."
|
||||
}
|
||||
},
|
||||
"required": ["yj_full"]
|
||||
"required": [
|
||||
"yj_full"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -201,7 +218,10 @@
|
||||
"description": "Exact section title from list_drug_chapters (e.g., '9.2 腎機能障害患者', '11.1 重大な副作用')."
|
||||
}
|
||||
},
|
||||
"required": ["yj_full", "section_title"]
|
||||
"required": [
|
||||
"yj_full",
|
||||
"section_title"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
427
skills/developing/pmda-drug-info/queries.py
Normal file
427
skills/developing/pmda-drug-info/queries.py
Normal file
@ -0,0 +1,427 @@
|
||||
"""SQL 查询接口(关系小库侧)。
|
||||
|
||||
Phase 1 已承接:
|
||||
- `search_drugs` 的 商品名 / 一般名 / YJ 子串检索
|
||||
- `list_categories` 的 L1/L2 + drug_count
|
||||
- `list_drugs_in_category` 的 一般名 → 販売名
|
||||
|
||||
后续 Phase 2 会接 drug_interaction / drug_restriction / drug_dosing。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from taxonomy import load_taxonomy
|
||||
from db import session
|
||||
|
||||
# Plugin 自包含: drug_category.md 与 queries.py 同目录
|
||||
_TAXONOMY_PATH = Path(__file__).resolve().parent / "drug_category.md"
|
||||
_TAXONOMY_CACHE = None
|
||||
|
||||
|
||||
def _taxonomy():
|
||||
global _TAXONOMY_CACHE
|
||||
if _TAXONOMY_CACHE is None:
|
||||
_TAXONOMY_CACHE = load_taxonomy(_TAXONOMY_PATH)
|
||||
return _TAXONOMY_CACHE
|
||||
|
||||
# 12 字母数字 → YJ code 候选;前几位即足够触发自动 kind=yj 的判断
|
||||
_YJ_RE = re.compile(r"^[0-9A-Z]{4,12}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DrugHit:
|
||||
yj_full: str
|
||||
yj_code: str
|
||||
brand_name: str # "/" 分隔多品名
|
||||
generic_name: str
|
||||
category_code: str
|
||||
category_name: str
|
||||
score: float # 50-100
|
||||
|
||||
|
||||
def _detect_kind(q: str) -> str:
|
||||
"""auto-detect: pure alnum & uppercase 4+ chars → yj, otherwise any."""
|
||||
if _YJ_RE.match(q.upper()):
|
||||
return "yj"
|
||||
return "any"
|
||||
|
||||
|
||||
def _score_expr(q_lower: str, q_like: str) -> str:
|
||||
"""Postgres expression returning relevance score 50–100."""
|
||||
# NB: doubles each pattern; psycopg expands %s positionally so caller
|
||||
# must pass q_lower / q_like in matching repetitions.
|
||||
return (
|
||||
"GREATEST("
|
||||
" CASE WHEN lower(brand_name) = %s THEN 100.0 "
|
||||
" WHEN lower(brand_name) LIKE %s || '%%' THEN 90.0 "
|
||||
" WHEN brand_name ILIKE %s THEN 70.0 ELSE 0 END,"
|
||||
" CASE WHEN lower(generic_name_jp) = %s THEN 95.0 "
|
||||
" WHEN lower(generic_name_jp) LIKE %s || '%%' THEN 85.0 "
|
||||
" WHEN generic_name_jp ILIKE %s THEN 65.0 ELSE 0 END,"
|
||||
" CASE WHEN yj_code = %s THEN 100.0 ELSE 0 END"
|
||||
")"
|
||||
)
|
||||
|
||||
|
||||
def search_drugs_in_db(
|
||||
query: str,
|
||||
*,
|
||||
kind: str = "auto",
|
||||
limit: int = 20,
|
||||
) -> list[DrugHit]:
|
||||
"""Drop-in replacement for the in-memory ``CorpusIndex.search``.
|
||||
|
||||
`kind` ∈ {"auto", "brand", "generic", "yj"}.
|
||||
|
||||
Returns DrugHit list (max ``limit``) ordered by relevance score desc.
|
||||
"""
|
||||
q = (query or "").strip()
|
||||
if not q:
|
||||
return []
|
||||
if kind == "auto":
|
||||
kind = _detect_kind(q)
|
||||
|
||||
q_lower = q.lower()
|
||||
q_like = f"%{q}%"
|
||||
q_upper = q.upper()
|
||||
|
||||
if kind == "yj":
|
||||
sql = """
|
||||
SELECT yj_full, yj_code, brand_name, generic_name_jp,
|
||||
category_code, category_name,
|
||||
CASE WHEN yj_code = %s THEN 100.0
|
||||
WHEN yj_full LIKE %s || '%%' THEN 95.0
|
||||
ELSE 80.0 END AS score
|
||||
FROM drug_master
|
||||
WHERE yj_code LIKE %s OR yj_full LIKE %s
|
||||
ORDER BY score DESC, yj_full ASC
|
||||
LIMIT %s
|
||||
"""
|
||||
params = (q_upper, q_upper, f"{q_upper}%", f"{q_upper}%", limit)
|
||||
elif kind == "brand":
|
||||
sql = """
|
||||
SELECT yj_full, yj_code, brand_name, generic_name_jp,
|
||||
category_code, category_name,
|
||||
CASE WHEN lower(brand_name) = %s THEN 100.0
|
||||
WHEN lower(brand_name) LIKE %s || '%%' THEN 90.0
|
||||
ELSE 70.0 END AS score
|
||||
FROM drug_master
|
||||
WHERE brand_name ILIKE %s
|
||||
ORDER BY score DESC, length(brand_name) ASC, yj_full ASC
|
||||
LIMIT %s
|
||||
"""
|
||||
params = (q_lower, q_lower, q_like, limit)
|
||||
elif kind == "generic":
|
||||
sql = """
|
||||
SELECT yj_full, yj_code, brand_name, generic_name_jp,
|
||||
category_code, category_name,
|
||||
CASE WHEN lower(generic_name_jp) = %s THEN 95.0
|
||||
WHEN lower(generic_name_jp) LIKE %s || '%%' THEN 85.0
|
||||
ELSE 65.0 END AS score
|
||||
FROM drug_master
|
||||
WHERE generic_name_jp ILIKE %s
|
||||
ORDER BY score DESC, length(generic_name_jp) ASC, yj_full ASC
|
||||
LIMIT %s
|
||||
"""
|
||||
params = (q_lower, q_lower, q_like, limit)
|
||||
else: # any
|
||||
sql = f"""
|
||||
SELECT yj_full, yj_code, brand_name, generic_name_jp,
|
||||
category_code, category_name,
|
||||
{_score_expr(q_lower, q_like)} AS score
|
||||
FROM drug_master
|
||||
WHERE brand_name ILIKE %s OR generic_name_jp ILIKE %s
|
||||
OR yj_code LIKE %s OR yj_full LIKE %s
|
||||
ORDER BY score DESC, length(brand_name) ASC, yj_full ASC
|
||||
LIMIT %s
|
||||
"""
|
||||
# _score_expr 占位符顺序:brand=, brand LIKE, brand ILIKE,
|
||||
# generic=, generic LIKE, generic ILIKE, yj_code=
|
||||
# 然后 WHERE: brand ILIKE, generic ILIKE, yj LIKE, yj_full LIKE
|
||||
params = (
|
||||
q_lower, q_lower, q_like,
|
||||
q_lower, q_lower, q_like,
|
||||
q_upper,
|
||||
q_like, q_like, f"{q_upper}%", f"{q_upper}%",
|
||||
limit,
|
||||
)
|
||||
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
return [
|
||||
DrugHit(
|
||||
yj_full=r[0],
|
||||
yj_code=r[1],
|
||||
brand_name=r[2] or "",
|
||||
generic_name=r[3] or "",
|
||||
category_code=r[4] or "",
|
||||
category_name=r[5] or "",
|
||||
score=float(r[6] or 0),
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
# ---- 类别导航 ------------------------------------------------------------
|
||||
|
||||
|
||||
def list_categories_with_counts() -> list[dict]:
|
||||
"""全 L1 / L2 分类 + 各 L2 的 drug 数。
|
||||
|
||||
分类层级名取自 drug_category.md(不用 PMDA 的 category_name 自由文,
|
||||
因为后者一药一表达,难以聚合);drug_count 取自 drug_master 的实际行数。
|
||||
"""
|
||||
tax = _taxonomy()
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT category_code, COUNT(*) FROM drug_master "
|
||||
"WHERE category_code IS NOT NULL "
|
||||
"GROUP BY category_code"
|
||||
)
|
||||
counts: dict[str, int] = dict(cur.fetchall())
|
||||
|
||||
by_l1: dict[str, dict] = {}
|
||||
for l2_code, l2 in tax.items():
|
||||
c = counts.get(l2_code, 0)
|
||||
if c == 0:
|
||||
continue
|
||||
l1 = by_l1.setdefault(
|
||||
l2.l1_code,
|
||||
{"l1_code": l2.l1_code, "l1_name": l2.l1_name, "l2": []},
|
||||
)
|
||||
l1["l2"].append({"code": l2_code, "name": l2.name, "drug_count": c})
|
||||
# 内层按 code 排序,外层按 l1_code 排序
|
||||
for l1 in by_l1.values():
|
||||
l1["l2"].sort(key=lambda x: x["code"])
|
||||
return [by_l1[k] for k in sorted(by_l1)]
|
||||
|
||||
|
||||
def list_drugs_in_category(
|
||||
l2_code: str,
|
||||
*,
|
||||
limit_generics: int = 50,
|
||||
brands_per_generic: int = 5,
|
||||
) -> dict:
|
||||
"""指定 L2 类目下的「一般名 → [販売名]」一览。
|
||||
|
||||
Returns the same JSON shape `_corpus_tools.list_drugs_in_category` previously
|
||||
yielded so the agent prompt 不变。
|
||||
"""
|
||||
tax = _taxonomy()
|
||||
l2 = tax.get(l2_code)
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT generic_name_jp, yj_full, brand_name "
|
||||
"FROM drug_master WHERE category_code = %s "
|
||||
"ORDER BY generic_name_jp, yj_full",
|
||||
(l2_code,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
by_gen: "OrderedDict[str, list[dict]]" = OrderedDict()
|
||||
for gen, yj_full, brand in rows:
|
||||
by_gen.setdefault(gen or "(一般名不明)", []).append(
|
||||
{"brand": brand or "", "yj_full": yj_full}
|
||||
)
|
||||
|
||||
payload: list[dict] = []
|
||||
for gen in list(by_gen)[:limit_generics]:
|
||||
drugs = by_gen[gen]
|
||||
shown = drugs[:brands_per_generic]
|
||||
extra = len(drugs) - len(shown)
|
||||
entry = {"generic": gen, "drugs": list(shown)}
|
||||
if extra > 0:
|
||||
entry["drugs"].append({"_more": f"+{extra} more brands"})
|
||||
payload.append(entry)
|
||||
|
||||
out = {
|
||||
"l2_code": l2_code,
|
||||
"l2_name": l2.name if l2 else "",
|
||||
"generics": payload,
|
||||
}
|
||||
if len(by_gen) > limit_generics:
|
||||
out["_more_generics"] = len(by_gen) - limit_generics
|
||||
return out
|
||||
|
||||
|
||||
# ---- fact 查询:drug_master / interaction / restriction / dosing ----------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DrugMasterRow:
|
||||
yj_code: str
|
||||
yj_full: str
|
||||
brand_name: str
|
||||
generic_name_jp: str
|
||||
category_code: str
|
||||
category_name: str
|
||||
regulation: str | None
|
||||
manufacturer: str | None
|
||||
revision_date: str | None # ISO date string
|
||||
|
||||
|
||||
def drug_master_get(yj_code: str) -> DrugMasterRow | None:
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT yj_code, yj_full, brand_name, generic_name_jp, "
|
||||
" category_code, category_name, regulation, manufacturer, "
|
||||
" to_char(revision_date, 'YYYY-MM-DD') "
|
||||
"FROM drug_master WHERE yj_code = %s",
|
||||
(yj_code,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return DrugMasterRow(*row)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InteractionRow:
|
||||
id: str
|
||||
drug_a_yj: str
|
||||
drug_b_yj: str | None
|
||||
drug_b_class: str | None
|
||||
severity: str
|
||||
mechanism: str | None
|
||||
clinical_effect: str | None
|
||||
source_section: str
|
||||
source_drug_yj: str
|
||||
|
||||
|
||||
def drug_interaction_query(
|
||||
drug_a_yj: str | None = None,
|
||||
drug_b_yj: str | None = None,
|
||||
*,
|
||||
severity: str | None = None,
|
||||
keyword: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> list[InteractionRow]:
|
||||
"""検索条件:
|
||||
drug_a_yj alone → drug_a の全相互作用(drug_b 任意)
|
||||
drug_a_yj + drug_b_yj → 双向(A→B もしくは B→A 両方)
|
||||
keyword → drug_b_class や mechanism / clinical_effect の ILIKE
|
||||
"""
|
||||
where = []
|
||||
params: list = []
|
||||
if drug_a_yj and drug_b_yj:
|
||||
where.append("((drug_a_yj=%s AND drug_b_yj=%s) OR "
|
||||
"(drug_a_yj=%s AND drug_b_yj=%s))")
|
||||
params += [drug_a_yj, drug_b_yj, drug_b_yj, drug_a_yj]
|
||||
elif drug_a_yj:
|
||||
where.append("drug_a_yj = %s")
|
||||
params.append(drug_a_yj)
|
||||
elif drug_b_yj:
|
||||
where.append("drug_b_yj = %s")
|
||||
params.append(drug_b_yj)
|
||||
if severity:
|
||||
where.append("severity = %s")
|
||||
params.append(severity)
|
||||
if keyword:
|
||||
where.append("(drug_b_class ILIKE %s OR mechanism ILIKE %s "
|
||||
" OR clinical_effect ILIKE %s)")
|
||||
kw = f"%{keyword}%"
|
||||
params += [kw, kw, kw]
|
||||
if not where:
|
||||
return []
|
||||
sql = (
|
||||
"SELECT id, drug_a_yj, drug_b_yj, drug_b_class, severity, "
|
||||
" mechanism, clinical_effect, source_section, source_drug_yj "
|
||||
"FROM drug_interaction WHERE " + " AND ".join(where) +
|
||||
" ORDER BY severity, drug_b_class NULLS LAST LIMIT %s"
|
||||
)
|
||||
params.append(limit)
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
return [InteractionRow(*r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RestrictionRow:
|
||||
id: str
|
||||
drug_yj: str
|
||||
condition_type: str
|
||||
condition_text: str
|
||||
condition_params: dict
|
||||
severity: str
|
||||
source_section: str
|
||||
|
||||
|
||||
def drug_restriction_query(
|
||||
drug_yj: str | None = None,
|
||||
*,
|
||||
condition_type: str | None = None,
|
||||
severity: str | None = None,
|
||||
keyword: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> list[RestrictionRow]:
|
||||
where = []
|
||||
params: list = []
|
||||
if drug_yj:
|
||||
where.append("drug_yj = %s")
|
||||
params.append(drug_yj)
|
||||
if condition_type:
|
||||
where.append("condition_type = %s")
|
||||
params.append(condition_type)
|
||||
if severity:
|
||||
where.append("severity = %s")
|
||||
params.append(severity)
|
||||
if keyword:
|
||||
where.append("condition_text ILIKE %s")
|
||||
params.append(f"%{keyword}%")
|
||||
if not where:
|
||||
return []
|
||||
sql = (
|
||||
"SELECT id, drug_yj, condition_type, condition_text, condition_params, "
|
||||
" severity, source_section "
|
||||
"FROM drug_restriction WHERE " + " AND ".join(where) +
|
||||
" ORDER BY severity, condition_type LIMIT %s"
|
||||
)
|
||||
params.append(limit)
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
return [RestrictionRow(*r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DosingRow:
|
||||
id: str
|
||||
drug_yj: str
|
||||
indication_code: str | None
|
||||
patient_segment: str
|
||||
segment_params: dict
|
||||
dose_amount: float | None
|
||||
dose_unit: str | None
|
||||
frequency: str | None
|
||||
duration: str | None
|
||||
adjustment_text: str
|
||||
source_section: str
|
||||
|
||||
|
||||
def drug_dosing_query(
|
||||
drug_yj: str,
|
||||
*,
|
||||
patient_segment: str | None = None,
|
||||
limit: int = 30,
|
||||
) -> list[DosingRow]:
|
||||
where = ["drug_yj = %s"]
|
||||
params: list = [drug_yj]
|
||||
if patient_segment:
|
||||
where.append("patient_segment = %s")
|
||||
params.append(patient_segment)
|
||||
sql = (
|
||||
"SELECT id, drug_yj, indication_code, patient_segment, segment_params, "
|
||||
" dose_amount, dose_unit, frequency, duration, adjustment_text, "
|
||||
" source_section "
|
||||
"FROM drug_dosing WHERE " + " AND ".join(where) +
|
||||
" ORDER BY patient_segment, indication_code NULLS LAST LIMIT %s"
|
||||
)
|
||||
params.append(limit)
|
||||
with session() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
return [DosingRow(*r) for r in cur.fetchall()]
|
||||
4
skills/developing/pmda-drug-info/requirements.txt
Normal file
4
skills/developing/pmda-drug-info/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# Plugin self-contained: PG (psycopg) + OS (opensearch-py)
|
||||
psycopg[binary]>=3.2.0
|
||||
psycopg-pool>=3.2.0
|
||||
opensearch-py>=2.2.0
|
||||
64
skills/developing/pmda-drug-info/taxonomy.py
Normal file
64
skills/developing/pmda-drug-info/taxonomy.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""Drug-category taxonomy loader.
|
||||
|
||||
Reads `pmda/drug_category.md` (the cleaned-up nested list with codes) and
|
||||
produces a `{l2_code: L2}` dict for joining with `DocMeta.l2_code` (first 3
|
||||
chars of the YJ code).
|
||||
|
||||
Source markdown shape:
|
||||
|
||||
- 11 中枢神経系用薬
|
||||
- 111 全身麻酔剤
|
||||
- 112 催眠鎮静剤,抗不安剤
|
||||
- 12 末梢神経用薬
|
||||
- 121 局所麻酔剤
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
# Top-level: `- {2-digit} {name}`
|
||||
_L1_RE = re.compile(r"^- (\d{2})\s+(.+)$")
|
||||
# Nested: ` - {3-digit} {name}` (indent 2 spaces)
|
||||
_L2_RE = re.compile(r"^ {2}- (\d{3})\s+(.+)$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class L2:
|
||||
code: str # "111"
|
||||
name: str # "全身麻酔剤"
|
||||
l1_code: str # "11"
|
||||
l1_name: str # "中枢神経系用薬"
|
||||
|
||||
|
||||
def load_taxonomy(path: Path | str = "pmda/drug_category.md") -> dict[str, L2]:
|
||||
out: dict[str, L2] = {}
|
||||
current_l1_code = ""
|
||||
current_l1_name = ""
|
||||
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
||||
m1 = _L1_RE.match(line)
|
||||
if m1:
|
||||
current_l1_code, current_l1_name = m1.group(1), m1.group(2).strip()
|
||||
continue
|
||||
m2 = _L2_RE.match(line)
|
||||
if m2:
|
||||
code = m2.group(1)
|
||||
name = m2.group(2).strip()
|
||||
if not current_l1_code:
|
||||
raise ValueError(f"L2 row {code} appears before any L1 in {path}")
|
||||
out[code] = L2(code=code, name=name, l1_code=current_l1_code, l1_name=current_l1_name)
|
||||
return out
|
||||
|
||||
|
||||
def lookup(taxonomy: dict[str, L2], l2_code: str) -> L2 | None:
|
||||
"""Return the L2 entry, or None if the YJ prefix isn't in the taxonomy."""
|
||||
return taxonomy.get(l2_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
t = load_taxonomy()
|
||||
print(f"Loaded {len(t)} L2 categories")
|
||||
for code in ("111", "214", "421", "999"):
|
||||
v = t.get(code)
|
||||
print(f" {code} → {v}")
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "rag-retrieve-disabled",
|
||||
"description": "rag_retrieve, table_rag_retrieve and local file retrieval are disabled."
|
||||
}
|
||||
1
skills/developing/rag-retrieve-disabled/README.md
Normal file
1
skills/developing/rag-retrieve-disabled/README.md
Normal file
@ -0,0 +1 @@
|
||||
# rag-retrieve-disabled
|
||||
Loading…
Reference in New Issue
Block a user