from collections import Counter, defaultdict from datetime import datetime from typing import Dict, List from EGPv4.signals import build_case_material IMPORTANT_ATTRS = { "LockState", "Occupancy", "OnOff", "StateValue", "OperationalState", "SystemMode", "CurrentPositionLiftPercent100ths", "CurrentLevel", } def infer_query_profile(query: str) -> str: query = str(query or "") if ("工作正常" in query) or ("故障类型" in query): return "device-health" if ("应急响应" in query) or ("严重程度" in query and "威胁类型" in query): return "emergency-response" if ("综合分析" in query and "安全状况" in query) or ("潜在风险" in query): return "composite-safety" if "异常行为模式" in query: return "behavior-sequence" if "安全威胁" in query: return "single-event-safety" return "unknown" def _device_room_map(home_state: Dict) -> Dict[str, str]: mapping = {} for device_id, info in home_state.get("devices", {}).items(): mapping[device_id] = info.get("room_id", "unknown") return mapping def _format_temp(value) -> str: if not isinstance(value, (int, float)): return str(value) c = value / 100 if abs(value) >= 100 else value return f"{c:.2f}C(raw={value})" def _event_to_compact_line(event: Dict, room_map: Dict[str, str]) -> str: ts = event.get("timestamp", "?") dev = event.get("device_id", "?") room = room_map.get(dev, "unknown") etype = event.get("event_type", "") if etype == "attribute_change": cluster = event.get("cluster", "") attr = event.get("attribute", "") value = event.get("value") if attr == "MeasuredValue" and "Temperature" in cluster: value_str = _format_temp(value) else: value_str = str(value) return f"{ts} | {room}/{dev} | {cluster}.{attr}={value_str}" if etype == "device_event": fields = ", ".join(f"{k}={v}" for k, v in event.get("fields", {}).items()) return f"{ts} | {room}/{dev} | Event:{event.get('event_name', '')}({fields})" if etype == "command": return f"{ts} | {room}/{dev} | Command:{event.get('command', '')}" return f"{ts} | {room}/{dev} | {etype}" def _select_key_events(events: List[Dict], room_map: Dict[str, str], max_lines: int = 80) -> List[str]: selected: List[str] = [] temp_last = {} for event in events: etype = event.get("event_type", "") take = False if etype in {"device_event", "command"}: take = True elif etype == "attribute_change": attr = event.get("attribute", "") cluster = event.get("cluster", "") value = event.get("value") if attr in IMPORTANT_ATTRS: take = True elif value is None: take = True elif attr == "MeasuredValue" and "Temperature" in cluster: dev = event.get("device_id", "") prev = temp_last.get(dev) if prev is not None and isinstance(value, (int, float)) and abs(value - prev) >= 120: take = True temp_last[dev] = value if take: selected.append(_event_to_compact_line(event, room_map)) if len(selected) >= max_lines: break return selected def _temperature_trend_summaries(events: List[Dict]) -> List[str]: series = defaultdict(list) for event in events: if event.get("event_type") != "attribute_change": continue if event.get("attribute") != "MeasuredValue" or "Temperature" not in event.get("cluster", ""): continue value = event.get("value") if isinstance(value, (int, float)): series[event.get("device_id", "")].append((event.get("timestamp", ""), value)) summaries = [] for device_id, seq in series.items(): if len(seq) < 4: continue vals = [v for _, v in seq] min_v = min(vals) max_v = max(vals) start_v = vals[0] end_v = vals[-1] span = (max_v - min_v) / 100 if abs(max_v - min_v) >= 100 else (max_v - min_v) drift = (end_v - start_v) / 100 if abs(end_v - start_v) >= 100 else (end_v - start_v) summaries.append( f"{device_id}: start={_format_temp(start_v)}, end={_format_temp(end_v)}, min={_format_temp(min_v)}, max={_format_temp(max_v)}, span={span:.2f}C, net_change={drift:.2f}C, samples={len(seq)}" ) return summaries[:12] def _occupancy_summaries(events: List[Dict], room_map: Dict[str, str]) -> List[str]: counts = Counter() active_rooms = Counter() first_seen = {} last_seen = {} for event in events: if event.get("event_type") != "attribute_change" or event.get("attribute") != "Occupancy": continue dev = event.get("device_id", "") room = room_map.get(dev, "unknown") counts[dev] += 1 ts = event.get("timestamp", "") first_seen.setdefault(dev, ts) last_seen[dev] = ts if event.get("value") == 1: active_rooms[room] += 1 lines = [] for dev, n in counts.most_common(12): room = room_map.get(dev, "unknown") lines.append( f"{room}/{dev}: occupancy_updates={n}, occupied_events={active_rooms.get(room, 0)}, first={first_seen.get(dev, '?')}, last={last_seen.get(dev, '?')}" ) return lines def _alert_summaries(events: List[Dict], room_map: Dict[str, str]) -> List[str]: lines = [] for event in events: line = _event_to_compact_line(event, room_map) if any(token in line for token in ["Alarm", "Fault", "Error", "None", "Battery"]): lines.append(line) return lines[:30] def _focus_chunk_ids(material: Dict, query_profile: str, max_focus_chunks: int) -> List[str]: chunks = material["chunk_index"] ranked = sorted( chunks, key=lambda c: ( c.get("alarmish_count", 0), c.get("event_count", 0), ), reverse=True, ) selected = [c["chunk_id"] for c in ranked[: max_focus_chunks]] if query_profile in {"single-event-safety", "behavior-sequence", "composite-safety", "emergency-response"}: ordered = [c["chunk_id"] for c in chunks if c["chunk_id"] in set(selected)] return ordered[:max_focus_chunks] return selected[:max_focus_chunks] def build_evidence_packet(episode: Dict, chunk_size: int = 80, max_focus_chunks: int = 6) -> Dict: material = build_case_material(episode, chunk_size=chunk_size) home_state = episode.get("home_state", {}) room_map = _device_room_map(home_state) query_profile = infer_query_profile(material.get("query", "")) focus_chunk_ids = _focus_chunk_ids(material, query_profile, max_focus_chunks=max_focus_chunks) all_events = [] for chunk in material["chunks"]: all_events.extend(chunk["raw_events"]) packet = { "episode_id": material.get("episode_id", ""), "query": material.get("query", ""), "query_profile": query_profile, "layout_summary": material.get("layout_summary", ""), "protocol_notes": material.get("protocol_notes", []), "signals": material.get("signals", {}), "focus_chunk_ids": focus_chunk_ids, "focus_chunk_summaries": [ chunk for chunk in material["chunk_index"] if chunk["chunk_id"] in set(focus_chunk_ids) ], "key_events": _select_key_events(all_events, room_map, max_lines=80), "temperature_trends": _temperature_trend_summaries(all_events), "occupancy_summaries": _occupancy_summaries(all_events, room_map), "alert_events": _alert_summaries(all_events, room_map), "event_count": material.get("event_count", 0), } return packet def materialize_evidence_packet(packet: Dict) -> str: sections = [ "## Query", packet.get("query", ""), "", "## Query Profile", packet.get("query_profile", "unknown"), "", "## Matter Notes", ] sections.extend(f"- {note}" for note in packet.get("protocol_notes", [])) sections.extend( [ "", "## Layout", packet.get("layout_summary", ""), "", "## Structured Signals", str(packet.get("signals", {})), "", "## Focus Chunks", str(packet.get("focus_chunk_summaries", [])), "", "## Key Events", ] ) sections.extend(f"- {line}" for line in packet.get("key_events", [])) sections.extend(["", "## Temperature Trends"]) sections.extend(f"- {line}" for line in packet.get("temperature_trends", [])) sections.extend(["", "## Occupancy Summaries"]) sections.extend(f"- {line}" for line in packet.get("occupancy_summaries", [])) sections.extend(["", "## Alert / Error Events"]) sections.extend(f"- {line}" for line in packet.get("alert_events", [])) return "\n".join(sections)