dmChatbotBackend / src /core /graph_cdm.py
github-actions
Auto deploy from GitHub
23bfb17
Raw History Blame Contribute Delete
10.9 kB
from langgraph.graph import StateGraph, END
from src.core.state import AgentState
from src.agents.agents import ResponseValidator, SafetyCheck
from src.tools.fhir_memory import (
get_patient_summary_fhir, save_observation, ingest_fhir_bundle,
get_medications_by_patient, save_medication
)
from src.tools.web_tools import web_search_tool
from langgraph.prebuilt import ToolNode
from langchain_core.messages import ToolMessage
from langchain_core.runnables.config import RunnableConfig
try:
from langgraph.runtime import Runtime
except ImportError:
Runtime = None
import time
from src.utils.logger import setup_logger
from src.core.policy import PlatformGateService, audit_event, gate_update
from src.runtime.execution_context import ExecutionContext
from src.runtime.cdm_runtime import TrendAnalysis
from src.runtime.worker_runtime import WorkerRuntime
from src.skills.adapters import create_worker_adapter
from src.skills.registry import default_skill_registry
logger = setup_logger("CDMPipeline")
def _platform_agent(agent_type):
return agent_type()
def log_step(name: str, output: str = None):
logger.info(f"Executing: {name}")
log_msg = f"➔ CDM Node: {name}"
if output:
log_msg += f"\nOutput: {output}"
return {"logs": [log_msg]}
async def data_fetch_node(state: AgentState):
"""
Fetch FHIR data and medications for CDM context.
Bug 7.3: Include medication context in CDM pipeline.
"""
patient_id = state.get("patient_id", "unknown")
if patient_id == "unknown":
return log_step("Data Fetch", "No Patient ID provided.")
start_time = time.time()
# Fetch FHIR summary
summary = get_patient_summary_fhir.invoke({"patient_id": patient_id})
# Bug 7.3: Fetch medications for context
medications = get_medications_by_patient.invoke({"patient_id": patient_id})
# If summary has active_medications field, populate it
if isinstance(summary, dict) and "active_medications" in summary:
summary["active_medications"] = medications if medications else []
end_time = time.time()
log_output = f"Retrieved summary and medications for {patient_id}"
if medications:
log_output += f" ({len(medications)} active medications)"
log = log_step("FHIR Data Fetch", log_output)
metrics = {
"agent": "DataFetchNode",
"tokens": 0,
"time": round(end_time - start_time, 3)
}
return {
"fhir_data": [summary] if isinstance(summary, dict) else [],
"current_medications": medications if isinstance(medications, list) else [],
"logs": log["logs"],
"metrics": [metrics]
}
async def trend_analyzer_node(state: AgentState):
registry = default_skill_registry()
adapter = create_worker_adapter("cdm-trend-analysis", registry=registry)
context = ExecutionContext(
request_id=state.get("request_id") or "",
trace_id=state.get("trace_id") or "",
session_id=state.get("session_id") or "",
patient_id=state.get("patient_id"),
user_query=state["messages"][-1].content if state.get("messages") else "",
user_role=state.get("user_role"),
)
worker_state = dict(state)
if worker_state.get("user_role") in (None, "unknown"):
worker_state["user_role"] = "patient"
if state.get("runtime_mode", "skill") == "agent":
result = await adapter.execute(worker_state)
else:
worker = WorkerRuntime(registry)
try:
result = await worker.execute_adapter(adapter, worker_state)
finally:
worker.flush_and_destroy()
output = result.output or {}
trend = TrendAnalysis(
summary=output.get("trend_analysis", "") if isinstance(output, dict) else str(output),
metrics=output.get("trend_data", {}) if isinstance(output, dict) else {},
)
metric_trend = dict(result.metrics) if result.metrics else {}
metric_trend["skill_id"] = result.skill_id
metric_trend["onloaded_skill"] = result.skill_id
metric_trend["offloaded_skill"] = result.skill_id
metric_trend["agent"] = "TrendAnalyzer"
return {
"trend_analysis": trend.summary,
"trend_data": trend.metrics,
"cdm_trend_analysis": trend.as_dict(),
"logs": log_step("Trend Analyzer", trend.summary)["logs"],
"metrics": [metric_trend],
"active_skill_id": result.skill_id,
"active_skill_version": result.skill_version,
"skill_trace": (state.get("skill_trace") or []) + [
audit_event({**state, "active_skill_id": result.skill_id, "active_skill_version": result.skill_version}, "skill_onloaded", agent="TrendAnalyzer"),
audit_event({**state, "active_skill_id": result.skill_id, "active_skill_version": result.skill_version}, "skill_executed"),
audit_event({**state, "active_skill_id": result.skill_id, "active_skill_version": result.skill_version}, "skill_offloaded", agent="TrendAnalyzer"),
],
}
async def health_coach_node(state: AgentState, config: RunnableConfig = None):
registry = default_skill_registry()
adapter = create_worker_adapter("cdm-health-coach", registry=registry)
worker_state = dict(state)
if worker_state.get("user_role") in (None, "unknown"):
worker_state["user_role"] = "patient"
if state.get("runtime_mode", "skill") == "agent":
result = await adapter.execute(worker_state, config=config)
else:
worker = WorkerRuntime(registry)
try:
result = await worker.execute_adapter(adapter, worker_state, config=config)
finally:
worker.flush_and_destroy()
content = result.messages[-1].content if result.messages and getattr(result.messages[-1], "content", "") else "Tool calls generated."
metric_coach = dict(result.metrics) if result.metrics else {}
metric_coach["skill_id"] = result.skill_id
metric_coach["onloaded_skill"] = result.skill_id
metric_coach["offloaded_skill"] = result.skill_id
metric_coach["agent"] = "HealthCoach"
return {
"messages": result.messages,
"logs": log_step("Health Coach", content)["logs"],
"metrics": [metric_coach],
"active_skill_id": result.skill_id,
"active_skill_version": result.skill_version,
"skill_plan": [{"skill_id": result.skill_id, "reason": "CDM pipeline"}],
"skill_trace": (state.get("skill_trace") or []) + [
audit_event({**state, "active_skill_id": result.skill_id, "active_skill_version": result.skill_version}, "skill_onloaded", agent="HealthCoach"),
audit_event({**state, "active_skill_id": result.skill_id, "active_skill_version": result.skill_version}, "skill_executed"),
audit_event({**state, "active_skill_id": result.skill_id, "active_skill_version": result.skill_version}, "skill_offloaded", agent="HealthCoach"),
],
}
async def validator_node(state: AgentState):
res = await _platform_agent(ResponseValidator).run(state)
log = log_step("Response Validator", f"Valid: {res.get('is_valid')}")
res.update(log)
res.update(PlatformGateService.record(state, "validation", bool(res.get("is_valid"))))
return res
async def safety_check_node(state: AgentState):
res = await _platform_agent(SafetyCheck).run(state)
log = log_step("Safety Check", f"Safe: {res.get('is_safe')}")
res.update(log)
res.update(PlatformGateService.record(state, "safety", bool(res.get("is_safe"))))
return res
# Tool routing
def route_health_coach(state: AgentState):
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools_node"
return "validator"
def route_after_tools(state: AgentState):
return "health_coach"
def route_after_validator(state: AgentState):
if state.get("is_valid", False):
return "safety_check"
return "health_coach" # Simple retry
# Setup tools
cdm_tools = [web_search_tool, save_observation, ingest_fhir_bundle, get_patient_summary_fhir, get_medications_by_patient, save_medication]
tool_node = ToolNode(cdm_tools)
def _ensure_tool_config(config: RunnableConfig = None) -> dict:
call_config = dict(config) if config else {}
configurable = dict(call_config.get("configurable", {}))
if "__pregel_runtime" not in configurable and Runtime is not None:
try:
configurable["__pregel_runtime"] = Runtime()
except Exception:
pass
call_config["configurable"] = configurable
return call_config
async def tools_node_with_metrics(state: AgentState, config: RunnableConfig = None):
start_time = time.time()
call_config = _ensure_tool_config(config)
res = await tool_node.ainvoke(state, config=call_config)
end_time = time.time()
tool_messages = res if isinstance(res, list) else (res.get("messages", []) if isinstance(res, dict) else [])
tool_details = []
for tm in tool_messages:
if isinstance(tm, ToolMessage):
tool_name = getattr(tm, "name", None) or "CDM Tool"
out = str(getattr(tm, "content", ""))
tool_details.append({
"name": tool_name,
"output": out[:200] if out else "",
"time": round(end_time - start_time, 3)
})
names = [t["name"] for t in tool_details]
tools_str = ", ".join(dict.fromkeys(names)) if names else "CDMTools"
duration = round(end_time - start_time, 3)
metrics = {
"agent": "CDMToolsNode",
"tool_name": tools_str,
"tools": tool_details if tool_details else [{"name": tools_str, "time": duration}],
"tokens": 0,
"time": duration
}
# ToolNode returns a list of messages
return {"messages": res, "metrics": [metrics]}
# Build CDM Graph
builder = StateGraph(AgentState)
builder.add_node("data_fetch", data_fetch_node)
builder.add_node("trend_analyzer", trend_analyzer_node)
builder.add_node("health_coach", health_coach_node)
builder.add_node("tools_node", tools_node_with_metrics)
builder.add_node("validator", validator_node)
builder.add_node("safety_check", safety_check_node)
builder.set_entry_point("data_fetch")
builder.add_edge("data_fetch", "trend_analyzer")
builder.add_edge("trend_analyzer", "health_coach")
builder.add_conditional_edges("health_coach", route_health_coach, {
"tools_node": "tools_node",
"validator": "validator"
})
builder.add_edge("tools_node", "health_coach")
builder.add_conditional_edges("validator", route_after_validator, {
"safety_check": "safety_check",
"health_coach": "health_coach"
})
builder.add_conditional_edges("safety_check", lambda x: "end" if x.get("is_safe") else "health_coach", {
"end": END,
"health_coach": "health_coach"
})
cdm_pipeline = builder.compile()