Merge branch 'feature/no_answer' into dev
This commit is contained in:
commit
9bd40d9bd7
@ -45,6 +45,7 @@ from .mem0_config import Mem0Config
|
|||||||
from agent.prompt_loader import load_system_prompt_async, load_mcp_settings_async
|
from agent.prompt_loader import load_system_prompt_async, load_mcp_settings_async
|
||||||
from agent.agent_memory_cache import get_memory_cache_manager
|
from agent.agent_memory_cache import get_memory_cache_manager
|
||||||
from .subagent_loader import load_subagents
|
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 .checkpoint_manager import get_checkpointer_manager
|
||||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||||
from langgraph.checkpoint.memory import InMemorySaver
|
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"Loaded {len(mcp_tools)} MCP tools")
|
||||||
logger.info(f"init_agent mcp tools ready, elapsed: {time.time() - create_start:.3f}s")
|
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
|
sandbox, sandbox_type, workspace_root = await sandbox_task
|
||||||
logger.info(f"init_agent sandbox ready, elapsed: {time.time() - create_start:.3f}s")
|
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,
|
model=llm_instance,
|
||||||
assistant_id=config.bot_id,
|
assistant_id=config.bot_id,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
tools=mcp_tools,
|
tools=main_tools,
|
||||||
auto_approve=True,
|
auto_approve=True,
|
||||||
workspace_root=workspace_root,
|
workspace_root=workspace_root,
|
||||||
middleware=middleware,
|
middleware=middleware,
|
||||||
|
|||||||
@ -120,6 +120,37 @@ except Exception as e:
|
|||||||
logger.warning(f"Failed to patch mem0 remove_code_blocks: {e}")
|
logger.warning(f"Failed to patch mem0 remove_code_blocks: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Monkey patch: make PGVector.__del__ tolerate cur/conn being None.
|
||||||
|
# This project shares a single psycopg2 pool and explicitly sets vector_store.cur
|
||||||
|
# and vector_store.conn to None after releasing the connection. mem0's original
|
||||||
|
# __del__ calls self.cur.close() without a None check, raising a harmless but noisy
|
||||||
|
# "Exception ignored in __del__: AttributeError" when the instance is garbage
|
||||||
|
# collected. This replacement releases resources only when they still exist.
|
||||||
|
def _safe_pgvector_del(self) -> None:
|
||||||
|
"""Safely close PGVector cursor/connection, tolerating None values."""
|
||||||
|
try:
|
||||||
|
cur = getattr(self, "cur", None)
|
||||||
|
if cur is not None:
|
||||||
|
cur.close()
|
||||||
|
conn = getattr(self, "conn", None)
|
||||||
|
if conn is not None:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
# Never raise from __del__; ignore any teardown errors
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import mem0.vector_stores.pgvector as mem0_pgvector
|
||||||
|
mem0_pgvector.PGVector.__del__ = _safe_pgvector_del
|
||||||
|
logger.info("Successfully patched mem0 PGVector.__del__ to tolerate None cur/conn")
|
||||||
|
except ImportError:
|
||||||
|
# mem0 pgvector module not available; nothing to patch
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to patch mem0 PGVector.__del__: {e}")
|
||||||
|
|
||||||
|
|
||||||
class Mem0Manager:
|
class Mem0Manager:
|
||||||
"""
|
"""
|
||||||
Mem0 connection and instance manager
|
Mem0 connection and instance manager
|
||||||
|
|||||||
@ -129,6 +129,52 @@ async def merge_skill_mcp_configs(bot_id: str) -> List[Dict]:
|
|||||||
return []
|
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]:
|
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."""
|
"""Normalize relative paths in stdio MCP servers to absolute paths based on the skill directory."""
|
||||||
normalized_servers = copy.deepcopy(servers)
|
normalized_servers = copy.deepcopy(servers)
|
||||||
|
|||||||
95
agent/subagent_context_middleware.py
Normal file
95
agent/subagent_context_middleware.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""Middleware that tags logs with the currently executing subagent name.
|
||||||
|
|
||||||
|
Each subagent receives its own instance of this middleware (carrying its name).
|
||||||
|
The middleware writes the name into the request-scoped GlobalContext (`g.subagent`)
|
||||||
|
for the duration of every model call and tool call, so the log Formatter can render
|
||||||
|
which subagent produced each log line. The previous value is restored afterwards so
|
||||||
|
that nested/parallel subagents and the main agent are not affected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Awaitable, Callable
|
||||||
|
|
||||||
|
from langchain.agents.middleware import AgentMiddleware
|
||||||
|
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
||||||
|
from langchain.tools.tool_node import ToolCallRequest
|
||||||
|
|
||||||
|
from utils.log_util.context import g
|
||||||
|
|
||||||
|
logger = logging.getLogger("app")
|
||||||
|
|
||||||
|
# Context key consumed by utils/log_util/logger.py Formatter.
|
||||||
|
_SUBAGENT_KEY = "subagent"
|
||||||
|
|
||||||
|
|
||||||
|
class SubagentContextMiddleware(AgentMiddleware):
|
||||||
|
"""Set `g.subagent` while this subagent's model/tool calls execute."""
|
||||||
|
|
||||||
|
def __init__(self, subagent_name: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._subagent_name = subagent_name
|
||||||
|
|
||||||
|
def _enter(self) -> dict:
|
||||||
|
# Shallow-copy the whole context dict and rebind a PRIVATE copy for this
|
||||||
|
# context. This is load-bearing: GlobalContext mutates a shared dict in
|
||||||
|
# place, and asyncio task copies share that reference, so a plain
|
||||||
|
# `g.subagent = name` would leak across parallel sibling subagents and
|
||||||
|
# race on restore. Replacing the reference isolates each context.
|
||||||
|
try:
|
||||||
|
prev = dict(g.get_context())
|
||||||
|
except LookupError:
|
||||||
|
prev = {}
|
||||||
|
new_ctx = dict(prev)
|
||||||
|
new_ctx[_SUBAGENT_KEY] = self._subagent_name
|
||||||
|
g.update_context(new_ctx)
|
||||||
|
return prev
|
||||||
|
|
||||||
|
def _exit(self, prev: dict) -> None:
|
||||||
|
# Restore by rebinding the previous dict (also a private copy).
|
||||||
|
g.update_context(dict(prev))
|
||||||
|
|
||||||
|
# ----- model call -----
|
||||||
|
def wrap_model_call(
|
||||||
|
self,
|
||||||
|
request: ModelRequest,
|
||||||
|
handler: Callable[[ModelRequest], ModelResponse],
|
||||||
|
) -> ModelResponse:
|
||||||
|
prev = self._enter()
|
||||||
|
try:
|
||||||
|
return handler(request)
|
||||||
|
finally:
|
||||||
|
self._exit(prev)
|
||||||
|
|
||||||
|
async def awrap_model_call(
|
||||||
|
self,
|
||||||
|
request: ModelRequest,
|
||||||
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
||||||
|
) -> ModelResponse:
|
||||||
|
prev = self._enter()
|
||||||
|
try:
|
||||||
|
return await handler(request)
|
||||||
|
finally:
|
||||||
|
self._exit(prev)
|
||||||
|
|
||||||
|
# ----- tool call -----
|
||||||
|
def wrap_tool_call(
|
||||||
|
self,
|
||||||
|
request: ToolCallRequest,
|
||||||
|
handler: Callable[[ToolCallRequest], Any],
|
||||||
|
) -> Any:
|
||||||
|
prev = self._enter()
|
||||||
|
try:
|
||||||
|
return handler(request)
|
||||||
|
finally:
|
||||||
|
self._exit(prev)
|
||||||
|
|
||||||
|
async def awrap_tool_call(
|
||||||
|
self,
|
||||||
|
request: ToolCallRequest,
|
||||||
|
handler: Callable[[ToolCallRequest], Awaitable[Any]],
|
||||||
|
) -> Any:
|
||||||
|
prev = self._enter()
|
||||||
|
try:
|
||||||
|
return await handler(request)
|
||||||
|
finally:
|
||||||
|
self._exit(prev)
|
||||||
@ -25,6 +25,7 @@ from langchain.tools import BaseTool
|
|||||||
from langchain_core.language_models import BaseChatModel
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
|
||||||
from agent.plugin_hook_loader import _get_skill_dirs
|
from agent.plugin_hook_loader import _get_skill_dirs
|
||||||
|
from agent.subagent_context_middleware import SubagentContextMiddleware
|
||||||
|
|
||||||
logger = logging.getLogger('app')
|
logger = logging.getLogger('app')
|
||||||
|
|
||||||
@ -181,6 +182,8 @@ async def load_subagents(
|
|||||||
"system_prompt": parsed["system_prompt"],
|
"system_prompt": parsed["system_prompt"],
|
||||||
"model": model,
|
"model": model,
|
||||||
"tools": filtered_tools,
|
"tools": filtered_tools,
|
||||||
|
# Tag this subagent's model/tool logs with its name.
|
||||||
|
"middleware": [SubagentContextMiddleware(name)],
|
||||||
}
|
}
|
||||||
subagents.append(subagent)
|
subagents.append(subagent)
|
||||||
logger.info(f"Loaded sub-agent '{name}' with {len(filtered_tools)} tools from {parsed['source']}")
|
logger.info(f"Loaded sub-agent '{name}' with {len(filtered_tools)} tools from {parsed['source']}")
|
||||||
|
|||||||
@ -94,3 +94,16 @@ Trace Id: {trace_id}
|
|||||||
- Even when the user writes in a different language, you MUST still reply in [{language}].
|
- Even when the user writes in a different language, you MUST still reply in [{language}].
|
||||||
- Do NOT mix languages. Do NOT fall back to English or any other language under any circumstances.
|
- Do NOT mix languages. Do NOT fall back to English or any other language under any circumstances.
|
||||||
- Technical terms, code identifiers, file paths, and tool names may remain in their original form, but all surrounding text MUST be in [{language}].
|
- Technical terms, code identifiers, file paths, and tool names may remain in their original form, but all surrounding text MUST be in [{language}].
|
||||||
|
|
||||||
|
# Unanswerable Response Specification (MANDATORY)
|
||||||
|
When you genuinely cannot answer because no relevant information was found in the knowledge base / retrieval sources (and self-knowledge fallback is unavailable or insufficient), your reply MUST include the literal sentinel marker `<NO_ANSWER>`.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Output the marker `<NO_ANSWER>` **exactly as written** — it is a fixed ASCII literal. NEVER translate, rewrite, reformat, or wrap it in code blocks.
|
||||||
|
- Place the marker at the **very beginning** of your reply, immediately followed by a polite apology written in [{language}].
|
||||||
|
- NEVER output the marker alone — it MUST be followed by an apology in [{language}] so the user sees a meaningful message.
|
||||||
|
- When you CAN answer (you found relevant information), you MUST NOT output this marker under any circumstances.
|
||||||
|
- The marker is language-independent; only the apology text after it must be in [{language}].
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `<NO_ANSWER>Sorry, I couldn't find that information in the knowledge base.`
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pmda-drug-info",
|
"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.",
|
"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": {
|
"hooks": {
|
||||||
"PrePrompt": [
|
"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": {
|
"mcpServers": {
|
||||||
"pmda_drug_info": {
|
"pmda_drug_info": {
|
||||||
"transport": "stdio",
|
"transport": "stdio",
|
||||||
"command": "python",
|
"command": "python3",
|
||||||
"args": [
|
"args": [
|
||||||
"./pmda_server.py"
|
"./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
|
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.
|
||||||
|
|
||||||
【ツール戦略】
|
## Tool Strategy
|
||||||
1. `search_section_text(keyword=副作用名, section_filter="副作用")` で逆引き。
|
1. Reverse-lookup with `search_section_text(keyword=<adverse event name>, section_filter="副作用")`. Always state `total_drugs` explicitly in the answer.
|
||||||
total_drugs は必ず本文中に明示する。
|
2. Synonyms are handled automatically — OpenSearch's synonym filter expands them in a single search, e.g.:
|
||||||
2. 同義語が必要なケース:
|
|
||||||
"Stevens-Johnson" ⇔ "皮膚粘膜眼症候群" / "SJS"
|
"Stevens-Johnson" ⇔ "皮膚粘膜眼症候群" / "SJS"
|
||||||
"QT延長" ⇔ "Torsades de pointes"
|
"QT延長" ⇔ "Torsades de pointes"
|
||||||
"間質性肺炎" ⇔ "肺臓炎"
|
"間質性肺炎" ⇔ "肺臓炎"
|
||||||
OS の synonym filter が自動展開するので 1 回の検索で OK。
|
3. From the hits, pick 3–5 representative drugs and quote "11.1 重大な副作用" / "11.2 その他の副作用" verbatim with `read_drug_chapter`.
|
||||||
3. hit から代表薬を 3〜5 件選び、`read_drug_chapter` で 11.1 重大な副作用 / 11.2 その他の副作用
|
4. NEVER make causal inferences (e.g. "this drug caused this patient's symptom"). Information presentation only.
|
||||||
verbatim を引用。
|
|
||||||
4. 因果推論("この薬がこの患者の症状を起こした")は **絶対しない**。
|
|
||||||
情報提示のみ。
|
|
||||||
|
|
||||||
【絶対ルール】
|
## Absolute Rules
|
||||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
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
|
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.
|
||||||
|
|
||||||
【ツール戦略】
|
## Tool Strategy
|
||||||
- A・B 両薬の yj_code を `search_drugs` で取得。
|
- Get the yj_code of both drugs A and B with `search_drugs`.
|
||||||
- `get_drug_interactions(drug_a_yj=A, drug_b_yj=B)` で双方向検索(A→B も B→A も拾える)。
|
- Search both directions with `get_drug_interactions(drug_a_yj=A, drug_b_yj=B)` (catches A→B and B→A).
|
||||||
- ヒットしたら drug_a の側の出典 section(10.1 / 10.2)を `list_drug_chapters` + `read_drug_chapter` で
|
- 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.
|
||||||
verbatim 取得。drug_b 側にも該当記載があるか確認。
|
- 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=...)`.
|
||||||
別途 `search_section_text(keyword=B薬名, section_filter="相互作用")` で念押し)。
|
|
||||||
- 1 薬名のみ与えられた場合は `get_drug_interactions(drug_a_yj=...)` で全相互作用一覧。
|
|
||||||
|
|
||||||
severity は本文の "併用禁忌" / "併用注意" の語をそのまま転記。
|
Copy the `severity` field verbatim using the source wording "併用禁忌" / "併用注意".
|
||||||
|
|
||||||
【絶対ルール】
|
## Absolute Rules
|
||||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
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
|
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.
|
||||||
|
|
||||||
【ツール戦略】
|
## Tool Strategy
|
||||||
1. 薬名から yj_code を `search_drugs` で取得。
|
1. Get the yj_code from the drug name with `search_drugs`.
|
||||||
2. 患者条件を condition_type に対応付け:
|
2. Map the patient condition to a `condition_type`:
|
||||||
- 腎機能 (eGFR/CrCl) → "腎機能障害"
|
- Renal function (eGFR/CrCl) → "腎機能障害"
|
||||||
- 肝機能 (Child-Pugh) → "肝機能障害"
|
- Hepatic function (Child-Pugh) → "肝機能障害"
|
||||||
- 妊娠/授乳 → "妊婦"/"授乳婦"
|
- Pregnancy / lactation → "妊婦" / "授乳婦"
|
||||||
- 年齢 (小児/高齢) → "小児等"/"高齢者"
|
- Age (pediatric / elderly) → "小児等" / "高齢者"
|
||||||
- アレルギー既往 → "過敏症"
|
- Allergy history → "過敏症"
|
||||||
- 合併症 (糖尿病/喘息など) → "疾患"
|
- Comorbidity (diabetes, asthma, etc.) → "疾患"
|
||||||
3. `get_drug_restrictions(drug_yj=..., condition_type=...)` で該当 restriction を取得。
|
3. Get the matching restriction with `get_drug_restrictions(drug_yj=..., condition_type=...)`. Always check the `condition_params` values (e.g. `{"eGFR_max": 30}`).
|
||||||
condition_params の数値(例: {"eGFR_max": 30})を必ず確認。
|
4. Get patient-segment dosing with `get_drug_dosing(drug_yj=..., patient_segment=...)`.
|
||||||
4. `get_drug_dosing(drug_yj=..., patient_segment=...)` で患者層別用量を取得。
|
5. When needed, quote the 9.x chapter verbatim via `read_drug_chapter`.
|
||||||
5. 必要なら原文 `read_drug_chapter` で 9.x 章 verbatim 引用。
|
6. The agent is responsible for the numeric judgment (e.g. eGFR=25 vs eGFR_max=30 → applies).
|
||||||
6. 数値判定(例: eGFR=25 ⇔ eGFR_max=30 → 該当)を agent が責任もって行う。
|
|
||||||
|
|
||||||
【絶対ルール】
|
## Absolute Rules
|
||||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
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
|
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.
|
||||||
|
|
||||||
【ツール戦略】
|
## Tool Strategy
|
||||||
1. 質問から薬名/yj_code を特定 → `search_drugs` または直接 yj_code が分かれば次へ。
|
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. `get_drug_master(yj_code)` で基本情報(販売名・一般名・薬効分類・規制)を確定。
|
2. Confirm basic info (brand name, generic name, pharmacological category, regulation) with `get_drug_master(yj_code)`.
|
||||||
3. 必要に応じて `get_drug_dosing` で用法用量、`get_drug_restrictions(drug_yj=...)` で禁忌・特定患者注意。
|
3. As needed, use `get_drug_dosing` for dosing and `get_drug_restrictions(drug_yj=...)` for contraindications / patient-specific precautions.
|
||||||
4. 自由記述や上記テーブルに無い情報(例: 重大な副作用一覧、薬物動態の数値)は
|
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)`.
|
||||||
`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.
|
||||||
|
|
||||||
【絶対ルール】
|
## Absolute Rules
|
||||||
1. ツール呼び出し必須。トレーニング知識・教科書・ガイドラインからの推測は禁止。
|
1. Tool calls are mandatory. Never infer from training knowledge, textbooks, or guidelines.
|
||||||
2. 数値・固有名・条件は本文表現を改変せず逐語引用。
|
2. Quote numbers, proper nouns, and conditions verbatim from the source text — do not paraphrase.
|
||||||
3. 出典は **必ず** `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` の形式。
|
3. Text citation is required, in exactly this format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]`.
|
||||||
- fact 表 row には `_citation` フィールドが入っているので **そのまま転記**。
|
- Fact-table rows include a `_citation` field — copy it verbatim.
|
||||||
- `[出典: 薬品マスター]` `[出典: 添付文書]` 等の汎用出典は **絶対禁止**。
|
- Generic citations such as `[出典: 薬品マスター]` or `[出典: 添付文書]` are PROHIBITED.
|
||||||
- read_drug_chapter で実際に読んだ section 以外の出典を捏造しない。
|
- Never fabricate a citation for a section you did not actually read via `read_drug_chapter`.
|
||||||
4. 該当情報が無ければ "添付文書からは確認できません" と書く。
|
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.
|
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
|
## Core Rules
|
||||||
- **Tool calls are mandatory.** Never answer from training knowledge alone. All facts must come from tool results.
|
- **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>) / <章番号 章タイトル>]`
|
- Cite sources in the format: `[出典: <販売名> (yj_full=<id>) / <章番号 章タイトル>]` — taken from each record's `出典:` line.
|
||||||
- Fact table rows include a `_citation` field — use it directly.
|
|
||||||
- Generic citations like `[出典: 薬品マスター]` or `[出典: 添付文書]` are **prohibited**.
|
- Generic citations like `[出典: 薬品マスター]` or `[出典: 添付文書]` are **prohibited**.
|
||||||
- For urgent questions (suicide/drug abuse/severe acute symptoms), state: "緊急対応として担当医・薬剤師に直接相談してください"
|
- 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)
|
## When to Use Sub-agents (task tool)
|
||||||
- **patient_specific**: Renal/hepatic/pregnancy/elderly/pediatric/allergy conditions × dosing decisions
|
- **patient_specific**: Renal/hepatic/pregnancy/elderly/pediatric/allergy conditions × dosing decisions
|
||||||
- **interaction**: Pairwise drug interaction investigation
|
- **interaction**: Pairwise drug interaction investigation
|
||||||
- **adverse_event**: Reverse lookup from adverse event name to drugs
|
- **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)
|
- **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)
|
## Direct Tool Usage (do NOT delegate)
|
||||||
- Simple lookups → use tools directly
|
- Simple lookups → use tools directly
|
||||||
- Multi-drug comparisons → call tools sequentially, output as markdown table
|
- 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": {
|
"kind": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["auto", "brand", "generic", "yj"],
|
"enum": [
|
||||||
|
"auto",
|
||||||
|
"brand",
|
||||||
|
"generic",
|
||||||
|
"yj"
|
||||||
|
],
|
||||||
"description": "Search type. 'auto' searches all fields.",
|
"description": "Search type. 'auto' searches all fields.",
|
||||||
"default": "auto"
|
"default": "auto"
|
||||||
},
|
},
|
||||||
@ -21,7 +26,9 @@
|
|||||||
"default": 10
|
"default": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["query"]
|
"required": [
|
||||||
|
"query"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -48,7 +55,9 @@
|
|||||||
"default": 50
|
"default": 50
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["l2_code"]
|
"required": [
|
||||||
|
"l2_code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -62,7 +71,9 @@
|
|||||||
"description": "12-character YJ code."
|
"description": "12-character YJ code."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["yj_code"]
|
"required": [
|
||||||
|
"yj_code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -145,7 +156,9 @@
|
|||||||
"default": 20
|
"default": 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["drug_yj"]
|
"required": [
|
||||||
|
"drug_yj"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -169,7 +182,9 @@
|
|||||||
"default": 30
|
"default": 30
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["keyword"]
|
"required": [
|
||||||
|
"keyword"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -183,7 +198,9 @@
|
|||||||
"description": "Full YJ code (with revision suffix, e.g., 3399007H1021_1_21)."
|
"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 重大な副作用')."
|
"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
|
||||||
30
skills/onprem/static-hosting/SKILL.md
Normal file
30
skills/onprem/static-hosting/SKILL.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: static-hosting
|
||||||
|
description: Serve static HTML/CSS/JS/images from robot project directories via the built-in FastAPI static file server. Use when generating web pages, reports, or interactive content for a bot.
|
||||||
|
category: Web Services
|
||||||
|
---
|
||||||
|
|
||||||
|
# Static Hosting
|
||||||
|
|
||||||
|
Host static files (HTML, CSS, JS, images, fonts, etc.) under `/workspace/` and get public URLs.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Write files to `/workspace/`, then run the script to get the public URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 {SKILL_DIR}/scripts/get_url.py <absolute_path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 {SKILL_DIR}/scripts/get_url.py /workspace/index.html
|
||||||
|
# => https://api-dev.gptbase.ai/robot-assets/[bot-id]/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Inside HTML, use **relative paths** to reference other assets (e.g. `href="css/style.css"`)
|
||||||
|
- `/workspace/index.html` is auto-served at the directory URL
|
||||||
|
- All files under `/robot-assets/` are publicly accessible, no authentication
|
||||||
25
skills/onprem/static-hosting/scripts/get_url.py
Normal file
25
skills/onprem/static-hosting/scripts/get_url.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BACKEND_HOST = os.getenv("BACKEND_HOST", "https://onprem-dev.gbase.ai/api")
|
||||||
|
ASSISTANT_ID = os.getenv("ASSISTANT_ID", "")
|
||||||
|
|
||||||
|
if not ASSISTANT_ID:
|
||||||
|
print("Error: ASSISTANT_ID environment variable is not set")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: python3 {sys.argv[0]} <file_path>")
|
||||||
|
print(f"Example: python3 {sys.argv[0]} /workspace/index.html")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
file_path = os.path.abspath(sys.argv[1])
|
||||||
|
|
||||||
|
workspace_root = "/workspace"
|
||||||
|
if not file_path.startswith(workspace_root):
|
||||||
|
print(f"Error: path must be under {workspace_root}, got: {file_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
relative_path = file_path[len(workspace_root):] # e.g. "/css/style.css"
|
||||||
|
base_url = f"{BACKEND_HOST.rstrip('/')}/robot-assets/{ASSISTANT_ID}"
|
||||||
|
print(f"{base_url}{relative_path}")
|
||||||
@ -51,6 +51,13 @@ class Formatter(logging.Formatter):
|
|||||||
record.trace_id = getattr(g, "trace_id")
|
record.trace_id = getattr(g, "trace_id")
|
||||||
except LookupError:
|
except LookupError:
|
||||||
record.trace_id = "N/A"
|
record.trace_id = "N/A"
|
||||||
|
# Handle subagent - default to "main" for the orchestrator / no-context paths.
|
||||||
|
# Catch KeyError too: GlobalContext.__getattr__ raises KeyError on a missing key.
|
||||||
|
if not hasattr(record, "subagent"):
|
||||||
|
try:
|
||||||
|
record.subagent = getattr(g, "subagent")
|
||||||
|
except (KeyError, LookupError):
|
||||||
|
record.subagent = "main"
|
||||||
# Handle user_id
|
# Handle user_id
|
||||||
# if not hasattr(record, "user_id"):
|
# if not hasattr(record, "user_id"):
|
||||||
# record.user_id = getattr(g, "user_id")
|
# record.user_id = getattr(g, "user_id")
|
||||||
@ -65,7 +72,7 @@ class Formatter(logging.Formatter):
|
|||||||
def init_logger_once(name,level):
|
def init_logger_once(name,level):
|
||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
logger.setLevel(level=level)
|
logger.setLevel(level=level)
|
||||||
formatter = Formatter("%(timestamp)s | %(levelname)-5s | %(trace_id)s | %(name)s:%(funcName)s:%(lineno)s - %(message)s", datefmt='%Y-%m-%d %H:%M:%S.%f')
|
formatter = Formatter("%(timestamp)s | %(levelname)-5s | %(trace_id)s | %(subagent)s | %(name)s:%(funcName)s:%(lineno)s - %(message)s", datefmt='%Y-%m-%d %H:%M:%S.%f')
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user