File size: 10,881 Bytes
76962bf cb505ff b1198f0 76962bf 23bfb17 76962bf cb505ff 76962bf cb505ff 76962bf b1198f0 76962bf b1198f0 76962bf b1198f0 76962bf b1198f0 76962bf b1198f0 76962bf cb505ff 76962bf 23bfb17 cb505ff 23bfb17 cb505ff 23bfb17 cb505ff 76962bf cb505ff 76962bf cb505ff 76962bf cb505ff 76962bf cb505ff 76962bf b1198f0 76962bf 23bfb17 76962bf 23bfb17 76962bf cb505ff 76962bf cb505ff 76962bf cb505ff 76962bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | 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()
|