experience_enhancer.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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
  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=3000,
  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
  80. metrics = []
  81. for r in results:
  82. m = {}
  83. for key in ["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. if key in r:
  87. m[key] = r[key]
  88. if m:
  89. metrics.append(m)
  90. # Sort by torque and take top/bottom 10 + random 10
  91. sorted_by_torque = sorted(metrics, key=lambda x: x.get("tavg_nm", 0), reverse=True)
  92. top_10 = sorted_by_torque[:10]
  93. bottom_10 = sorted_by_torque[-10:] if len(sorted_by_torque) > 20 else []
  94. # Calculate basic statistics
  95. all_torque = [m.get("tavg_nm", 0) for m in metrics if "tavg_nm" in m]
  96. all_efficiency = [m.get("efficiency_pct", 0) for m in metrics if "efficiency_pct" in m]
  97. stats = {}
  98. if all_torque:
  99. stats["torque"] = {
  100. "min": min(all_torque), "max": max(all_torque),
  101. "mean": sum(all_torque) / len(all_torque),
  102. }
  103. if all_efficiency:
  104. stats["efficiency"] = {
  105. "min": min(all_efficiency), "max": max(all_efficiency),
  106. "mean": sum(all_efficiency) / len(all_efficiency),
  107. }
  108. return {
  109. "total_points": len(results),
  110. "statistics": stats,
  111. "top_performers": top_10,
  112. "bottom_performers": bottom_10,
  113. }
  114. def _fallback_insights(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
  115. """Generate basic insights without AI."""
  116. feasible = [r for r in results if r.get("feasible", True)]
  117. infeasible = [r for r in results if not r.get("feasible", True)]
  118. insights = {
  119. "summary": f"Analyzed {len(results)} points ({len(feasible)} feasible, {len(infeasible)} infeasible).",
  120. "feasibility_rate": len(feasible) / len(results) if results else 0,
  121. "design_rules": [],
  122. "failure_patterns": [],
  123. "extracted_at": datetime.now().isoformat(),
  124. "n_results_analyzed": len(results),
  125. "ai_used": False,
  126. }
  127. # Simple correlation analysis
  128. if len(feasible) >= 5:
  129. # Find parameters that correlate with high torque
  130. for param in ["airgap_mm", "magnet_thickness_mm", "current_a"]:
  131. values = [(r.get(param, 0), r.get("tavg_nm", 0)) for r in feasible if param in r]
  132. if len(values) >= 5:
  133. values.sort(key=lambda x: x[1], reverse=True)
  134. top_params = [v[0] for v in values[:len(values)//3]]
  135. avg_top = sum(top_params) / len(top_params) if top_params else 0
  136. all_avg = sum(v[0] for v in values) / len(values)
  137. if abs(avg_top - all_avg) > 0.1 * abs(all_avg) if all_avg else False:
  138. direction = "higher" if avg_top > all_avg else "lower"
  139. insights["design_rules"].append(
  140. f"High torque designs tend to have {direction} {param} (avg top={avg_top:.2f} vs all={all_avg:.2f})"
  141. )
  142. return insights
  143. def generate_experience_entry(
  144. self,
  145. insights: Dict[str, Any],
  146. project_name: str,
  147. topology: str,
  148. ) -> Dict[str, Any]:
  149. """Generate a structured experience library entry from insights.
  150. Args:
  151. insights: Extracted insights
  152. project_name: Project name
  153. topology: Motor topology
  154. Returns:
  155. Structured experience entry ready for database storage
  156. """
  157. return {
  158. "title": f"{project_name} - {topology} Design Insights",
  159. "topology": topology,
  160. "project": project_name,
  161. "summary": insights.get("summary", ""),
  162. "design_rules": insights.get("design_rules", []),
  163. "failure_patterns": insights.get("failure_patterns", []),
  164. "parameter_sensitivity": insights.get("parameter_sensitivity", {}),
  165. "optimal_regions": insights.get("optimal_regions", []),
  166. "confidence": insights.get("confidence_grade", "C"),
  167. "source": "ai_extracted",
  168. "created_at": datetime.now().isoformat(),
  169. "tags": [topology, "ai-insights", project_name],
  170. }
  171. # Global singleton
  172. _enhancer: Optional[ExperienceEnhancer] = None
  173. def get_experience_enhancer() -> ExperienceEnhancer:
  174. """Get or create global ExperienceEnhancer singleton."""
  175. global _enhancer
  176. if _enhancer is None:
  177. _enhancer = ExperienceEnhancer()
  178. return _enhancer