experience_enhancer.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """Experience Library AI Enhancer (P3-M5).
  2. Uses AI to extract design knowledge, rules, and insights from
  3. historical simulation results, enhancing the experience library.
  4. """
  5. import json
  6. from pathlib import Path
  7. from typing import Dict, List, Optional, Any
  8. from datetime import datetime
  9. from ..config import PROMPTS_DIR, KIMI_MAX_TOKENS
  10. from ..services.ai_client import get_kimi_client
  11. class ExperienceEnhancer:
  12. """AI-powered experience library enhancer.
  13. Extracts:
  14. - Design rules and heuristics from successful cases
  15. - Failure patterns and constraint boundaries
  16. - Parameter sensitivity rankings
  17. - Optimal design regions
  18. - Comparative insights across topologies
  19. """
  20. def __init__(self):
  21. self.ai_client = get_kimi_client()
  22. self._prompt_template = None
  23. def _load_prompt(self) -> str:
  24. if self._prompt_template is None:
  25. prompt_path = PROMPTS_DIR / "experience" / "extract.txt"
  26. if prompt_path.exists():
  27. with open(prompt_path, "r", encoding="utf-8") as f:
  28. self._prompt_template = f.read()
  29. else:
  30. self._prompt_template = self._default_prompt()
  31. return self._prompt_template
  32. def _default_prompt(self) -> str:
  33. return """\u4f60\u662f\u8f74\u5411\u78c1\u901a\u7535\u673a\u8bbe\u8ba1\u77e5\u8bc6\u63d0\u53d6\u4e13\u5bb6\u3002\u4ece\u4eff\u771f\u7ed3\u679c\u6570\u636e\u4e2d\u63d0\u53d6\u8bbe\u8ba1\u89c4\u5219\u3001\u5931\u8d25\u6a21\u5f0f\u3001\u53c2\u6570\u654f\u611f\u6027\u548c\u6700\u4f18\u8bbe\u8ba1\u533a\u57df\u3002\u8f93\u51faJSON\u683c\u5f0f\u3002"""
  34. def extract_insights(
  35. self,
  36. results: List[Dict[str, Any]],
  37. project_context: Optional[Dict[str, Any]] = None,
  38. ) -> Dict[str, Any]:
  39. """Extract design insights from a batch of simulation results.
  40. Args:
  41. results: List of simulation result dicts
  42. project_context: Optional project context
  43. Returns:
  44. Structured insights including rules, patterns, and recommendations
  45. """
  46. if not results:
  47. return {"error": "No results to analyze"}
  48. # Prepare condensed data
  49. condensed = self._condense_results(results)
  50. if project_context:
  51. condensed["project_context"] = project_context
  52. user_message = f"\u4ece\u4ee5\u4e0b\u8f74\u5411\u78c1\u901a\u7535\u673a\u4eff\u771f\u7ed3\u679c\u4e2d\u63d0\u53d6\u8bbe\u8ba1\u77e5\u8bc6\uff1a\n{json.dumps(condensed, ensure_ascii=False, indent=2)}"
  53. if not self.ai_client.is_configured:
  54. return self._fallback_insights(results)
  55. try:
  56. result = self.ai_client.chat_json(
  57. messages=[{"role": "user", "content": user_message}],
  58. system_prompt=self._load_prompt(),
  59. max_tokens=KIMI_MAX_TOKENS,
  60. )
  61. insights = result.get("parsed_json", {})
  62. if not insights:
  63. raw = result.get("raw_content", result.get("content", ""))
  64. try:
  65. start = raw.find("{")
  66. end = raw.rfind("}") + 1
  67. if start >= 0 and end > start:
  68. insights = json.loads(raw[start:end])
  69. except (json.JSONDecodeError, Exception):
  70. insights = {"summary": raw[:500]}
  71. insights["extracted_at"] = datetime.now().isoformat()
  72. insights["n_results_analyzed"] = len(results)
  73. insights["usage"] = result.get("usage", {})
  74. return insights
  75. except Exception as e:
  76. return {"error": f"AI insight extraction failed: {str(e)}", **self._fallback_insights(results)}
  77. def _condense_results(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
  78. """Condense results for AI processing (avoid token overflow)."""
  79. # Extract key metrics. Input params may arrive under Motor-CAD names
  80. # (Airgap, RMSCurrent); translate them to the BC-style keys below via
  81. # the shared single-source map so sensitivity analysis sees inputs.
  82. from src.afmcore.l0.prescreening import MOTORCAD_TO_L0
  83. keys = ["tavg_nm", "efficiency_pct", "total_losses_w", "winding_temp_c",
  84. "airgap_mm", "magnet_thickness_mm", "current_a", "speed_rpm",
  85. "outer_diameter_mm", "inner_diameter_mm", "feasible"]
  86. metrics = []
  87. for r in results:
  88. r_norm = dict(r)
  89. for mc_name, l0_name in MOTORCAD_TO_L0.items():
  90. if mc_name in r_norm and l0_name not in r_norm:
  91. r_norm[l0_name] = r_norm[mc_name]
  92. m = {}
  93. for key in keys:
  94. if key in r_norm:
  95. m[key] = r_norm[key]
  96. if m:
  97. metrics.append(m)
  98. # Sort by torque and take top/bottom 10 + random 10
  99. sorted_by_torque = sorted(metrics, key=lambda x: x.get("tavg_nm", 0), reverse=True)
  100. top_10 = sorted_by_torque[:10]
  101. bottom_10 = sorted_by_torque[-10:] if len(sorted_by_torque) > 20 else []
  102. # Calculate basic statistics
  103. all_torque = [m.get("tavg_nm", 0) for m in metrics if "tavg_nm" in m]
  104. all_efficiency = [m.get("efficiency_pct", 0) for m in metrics if "efficiency_pct" in m]
  105. stats = {}
  106. if all_torque:
  107. stats["torque"] = {
  108. "min": min(all_torque), "max": max(all_torque),
  109. "mean": sum(all_torque) / len(all_torque),
  110. }
  111. if all_efficiency:
  112. stats["efficiency"] = {
  113. "min": min(all_efficiency), "max": max(all_efficiency),
  114. "mean": sum(all_efficiency) / len(all_efficiency),
  115. }
  116. return {
  117. "total_points": len(results),
  118. "statistics": stats,
  119. "top_performers": top_10,
  120. "bottom_performers": bottom_10,
  121. }
  122. def _fallback_insights(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
  123. """Generate basic insights without AI."""
  124. feasible = [r for r in results if r.get("feasible", True)]
  125. infeasible = [r for r in results if not r.get("feasible", True)]
  126. insights = {
  127. "summary": f"Analyzed {len(results)} points ({len(feasible)} feasible, {len(infeasible)} infeasible).",
  128. "feasibility_rate": len(feasible) / len(results) if results else 0,
  129. "design_rules": [],
  130. "failure_patterns": [],
  131. "extracted_at": datetime.now().isoformat(),
  132. "n_results_analyzed": len(results),
  133. "ai_used": False,
  134. }
  135. # Simple correlation analysis
  136. if len(feasible) >= 5:
  137. # Find parameters that correlate with high torque
  138. for param in ["airgap_mm", "magnet_thickness_mm", "current_a"]:
  139. values = [(r.get(param, 0), r.get("tavg_nm", 0)) for r in feasible if param in r]
  140. if len(values) >= 5:
  141. values.sort(key=lambda x: x[1], reverse=True)
  142. top_params = [v[0] for v in values[:len(values)//3]]
  143. avg_top = sum(top_params) / len(top_params) if top_params else 0
  144. all_avg = sum(v[0] for v in values) / len(values)
  145. if abs(avg_top - all_avg) > 0.1 * abs(all_avg) if all_avg else False:
  146. direction = "higher" if avg_top > all_avg else "lower"
  147. insights["design_rules"].append(
  148. f"High torque designs tend to have {direction} {param} (avg top={avg_top:.2f} vs all={all_avg:.2f})"
  149. )
  150. return insights
  151. def generate_experience_entry(
  152. self,
  153. insights: Dict[str, Any],
  154. project_name: str,
  155. topology: str,
  156. ) -> Dict[str, Any]:
  157. """Generate a structured experience library entry from insights.
  158. Args:
  159. insights: Extracted insights
  160. project_name: Project name
  161. topology: Motor topology
  162. Returns:
  163. Structured experience entry ready for database storage
  164. """
  165. return {
  166. "title": f"{project_name} - {topology} Design Insights",
  167. "topology": topology,
  168. "project": project_name,
  169. "summary": insights.get("summary", ""),
  170. "design_rules": insights.get("design_rules", []),
  171. "failure_patterns": insights.get("failure_patterns", []),
  172. "parameter_sensitivity": insights.get("parameter_sensitivity", {}),
  173. "optimal_regions": insights.get("optimal_regions", []),
  174. "confidence": insights.get("confidence_grade", "C"),
  175. "source": "ai_extracted",
  176. "created_at": datetime.now().isoformat(),
  177. "tags": [topology, "ai-insights", project_name],
  178. }
  179. # Global singleton
  180. _enhancer: Optional[ExperienceEnhancer] = None
  181. def get_experience_enhancer() -> ExperienceEnhancer:
  182. """Get or create global ExperienceEnhancer singleton."""
  183. global _enhancer
  184. if _enhancer is None:
  185. _enhancer = ExperienceEnhancer()
  186. return _enhancer