|
| 1 | +# agents/ansible_upgrade/agent.py - CLEAN ReAct Agent (Config Driven) |
| 2 | + |
| 3 | +import logging |
| 4 | +import json |
| 5 | +import uuid |
| 6 | +from typing import Dict, Any, Optional, AsyncGenerator |
| 7 | +from datetime import datetime |
| 8 | + |
| 9 | +from llama_stack_client import LlamaStackClient |
| 10 | +from llama_stack_client.lib.agents.react.agent import ReActAgent |
| 11 | + |
| 12 | +# Import the processor for robust JSON handling |
| 13 | +from .processor import extract_and_validate_analysis |
| 14 | + |
| 15 | +logger = logging.getLogger(__name__) |
| 16 | + |
| 17 | +class AnsibleUpgradeAnalysisAgent: |
| 18 | + def __init__( |
| 19 | + self, |
| 20 | + client: LlamaStackClient, |
| 21 | + config_loader, |
| 22 | + agent_name: str = "ansible_upgrade_analysis" |
| 23 | + ): |
| 24 | + self.client = client |
| 25 | + self.config_loader = config_loader |
| 26 | + self.agent_name = agent_name |
| 27 | + |
| 28 | + # Get agent config exactly like SyncAI |
| 29 | + self.agent_cfg = self._get_agent_config(agent_name) |
| 30 | + self.model = self.agent_cfg.get("model", "granite32-8b") |
| 31 | + self.instructions = self.agent_cfg.get("instructions", "") |
| 32 | + self.tools = self.agent_cfg.get("tools", []) |
| 33 | + self.sampling_params = self.agent_cfg.get("sampling_params", { |
| 34 | + "strategy": {"type": "greedy"}, |
| 35 | + "max_tokens": 4096 |
| 36 | + }) |
| 37 | + self.max_infer_iters = self.agent_cfg.get("max_infer_iters", 1) |
| 38 | + |
| 39 | + # Initialize ReAct agent with config-driven approach |
| 40 | + self._init_react_agent() |
| 41 | + logger.info(f"🔄 Ansible ReAct agent initialized: {self.agent_name}") |
| 42 | + |
| 43 | + def _get_agent_config(self, agent_name: str) -> Dict[str, Any]: |
| 44 | + agents_config = self.config_loader.get_agents_config() |
| 45 | + for agent in agents_config: |
| 46 | + if agent.get("name") == agent_name: |
| 47 | + return agent |
| 48 | + raise ValueError(f"No agent configuration found for {agent_name}") |
| 49 | + |
| 50 | + def _init_react_agent(self): |
| 51 | + try: |
| 52 | + # Use instructions directly from config - no modifications |
| 53 | + self.react_agent = ReActAgent( |
| 54 | + client=self.client, |
| 55 | + model=self.model, |
| 56 | + tools=self.tools, |
| 57 | + instructions=self.instructions, # Pure config instructions |
| 58 | + sampling_params=self.sampling_params, |
| 59 | + max_infer_iters=self.max_infer_iters |
| 60 | + ) |
| 61 | + logger.info("🔄 ReAct agent initialized successfully") |
| 62 | + except Exception as e: |
| 63 | + logger.error(f" Failed to initialize ReAct agent: {e}") |
| 64 | + raise |
| 65 | + |
| 66 | + async def analyze_ansible_upgrade( |
| 67 | + self, |
| 68 | + ansible_data: Dict[str, Any], |
| 69 | + correlation_id: str |
| 70 | + ) -> Dict[str, Any]: |
| 71 | + try: |
| 72 | + content = ansible_data.get("content", "") |
| 73 | + filename = ansible_data.get("filename", "playbook.yml") |
| 74 | + |
| 75 | + if not content.strip(): |
| 76 | + return self._create_error_response("No Ansible content provided", filename, correlation_id) |
| 77 | + |
| 78 | + # Get and format prompt exactly from config |
| 79 | + prompt_template = self.config_loader.config.get("prompts", {}).get("ansible_upgrade_analysis", "") |
| 80 | + |
| 81 | + # Format prompt using config template |
| 82 | + formatted_prompt = prompt_template.format( |
| 83 | + instruction=self.instructions, |
| 84 | + ansible_content=content |
| 85 | + ) |
| 86 | + |
| 87 | + # Use ReAct agent exactly like SyncAI |
| 88 | + session_id = self.react_agent.create_session(f"{self.agent_name}-{correlation_id}") |
| 89 | + |
| 90 | + response = self.react_agent.create_turn( |
| 91 | + messages=[{"role": "user", "content": formatted_prompt}], |
| 92 | + session_id=session_id, |
| 93 | + stream=False |
| 94 | + ) |
| 95 | + |
| 96 | + # Extract response content |
| 97 | + response_content = getattr(response.output_message, 'content', '') if hasattr(response, 'output_message') else "" |
| 98 | + logger.info(f"Raw response length: {len(response_content)}") |
| 99 | + logger.info(f"Raw response preview: {response_content[:300]}...") |
| 100 | + |
| 101 | + # Handle ReAct response - check if it's already proper JSON |
| 102 | + if isinstance(response_content, str): |
| 103 | + try: |
| 104 | + # Try direct JSON parsing (in case agent returns pure JSON) |
| 105 | + parsed_json = json.loads(response_content.strip()) |
| 106 | + if isinstance(parsed_json, dict) and parsed_json.get("success") is not None: |
| 107 | + parsed_json["filename"] = filename |
| 108 | + if "session_info" not in parsed_json: |
| 109 | + parsed_json["session_info"] = { |
| 110 | + "correlation_id": correlation_id, |
| 111 | + "timestamp": datetime.now().isoformat() |
| 112 | + } |
| 113 | + # --- PATCH: Add top-level upgrade block if needed --- |
| 114 | + if parsed_json.get("analysis_type") == "ansible_upgrade_assessment": |
| 115 | + upgrade_requirements = parsed_json.get("upgrade_requirements", {}) |
| 116 | + breaking_changes = ( |
| 117 | + upgrade_requirements.get("structural_changes_needed", []) |
| 118 | + if isinstance(upgrade_requirements, dict) |
| 119 | + else [] |
| 120 | + ) |
| 121 | + current_version = parsed_json.get("current_state", {}).get("estimated_version", "Unknown") |
| 122 | + # --- NON-HARDCODED: get recommended version from model or fallback --- |
| 123 | + recommended_version = ( |
| 124 | + parsed_json.get("recommended_ansible_version") |
| 125 | + or parsed_json.get("recommendations", {}).get("recommended_ansible_version") |
| 126 | + or "2.15" |
| 127 | + ) |
| 128 | + parsed_json["upgrade"] = { |
| 129 | + "breakingChangesCount": len(breaking_changes), |
| 130 | + "currentVersion": current_version, |
| 131 | + "recommendedVersion": recommended_version, |
| 132 | + } |
| 133 | + # --- END PATCH --- |
| 134 | + logger.info(f" Direct JSON parsing successful") |
| 135 | + return parsed_json |
| 136 | + except json.JSONDecodeError: |
| 137 | + # Not pure JSON, continue to processor |
| 138 | + pass |
| 139 | + |
| 140 | + # Process response using the robust processor |
| 141 | + processed_result = extract_and_validate_analysis( |
| 142 | + raw_response=response_content, |
| 143 | + correlation_id=correlation_id, |
| 144 | + ansible_content=content |
| 145 | + ) |
| 146 | + |
| 147 | + # Ensure required fields are set |
| 148 | + processed_result["filename"] = filename |
| 149 | + if "session_info" not in processed_result: |
| 150 | + processed_result["session_info"] = { |
| 151 | + "correlation_id": correlation_id, |
| 152 | + "timestamp": datetime.now().isoformat() |
| 153 | + } |
| 154 | + # --- PATCH: Add top-level upgrade block if needed --- |
| 155 | + if processed_result.get("analysis_type") == "ansible_upgrade_assessment": |
| 156 | + upgrade_requirements = processed_result.get("upgrade_requirements", {}) |
| 157 | + breaking_changes = ( |
| 158 | + upgrade_requirements.get("structural_changes_needed", []) |
| 159 | + if isinstance(upgrade_requirements, dict) |
| 160 | + else [] |
| 161 | + ) |
| 162 | + current_version = processed_result.get("current_state", {}).get("estimated_version", "Unknown") |
| 163 | + # --- NON-HARDCODED: get recommended version from model or fallback --- |
| 164 | + recommended_version = ( |
| 165 | + processed_result.get("recommended_ansible_version") |
| 166 | + or processed_result.get("recommendations", {}).get("recommended_ansible_version") |
| 167 | + or "2.15" |
| 168 | + ) |
| 169 | + processed_result["upgrade"] = { |
| 170 | + "breakingChangesCount": len(breaking_changes), |
| 171 | + "currentVersion": current_version, |
| 172 | + "recommendedVersion": recommended_version, |
| 173 | + } |
| 174 | + # --- END PATCH --- |
| 175 | + |
| 176 | + logger.info(f"Processed result success: {processed_result.get('success')}") |
| 177 | + return processed_result |
| 178 | + |
| 179 | + except Exception as e: |
| 180 | + logger.error(f" Analysis failed: {e}") |
| 181 | + return self._create_error_response(str(e), ansible_data.get("filename", "unknown.yml"), correlation_id) |
| 182 | + |
| 183 | + def _create_error_response(self, error: str, filename: str, correlation_id: str) -> Dict[str, Any]: |
| 184 | + """Create standardized error response""" |
| 185 | + return { |
| 186 | + "success": False, |
| 187 | + "error": error, |
| 188 | + "filename": filename, |
| 189 | + "analysis_type": "ansible_upgrade_assessment", |
| 190 | + "current_state": {}, |
| 191 | + "upgrade_requirements": {}, |
| 192 | + "complexity_assessment": {}, |
| 193 | + "recommendations": {}, |
| 194 | + "session_info": {"correlation_id": correlation_id, "timestamp": datetime.now().isoformat()} |
| 195 | + } |
| 196 | + |
| 197 | + async def analyze_stream(self, ansible_data: Dict[str, Any], correlation_id: str) -> AsyncGenerator[Dict[str, Any], None]: |
| 198 | + yield {"type": "start", "message": "Starting Ansible upgrade analysis", "correlation_id": correlation_id} |
| 199 | + |
| 200 | + try: |
| 201 | + result = await self.analyze_ansible_upgrade(ansible_data, correlation_id) |
| 202 | + yield {"type": "final_result", "data": result, "correlation_id": correlation_id} |
| 203 | + except Exception as e: |
| 204 | + yield {"type": "error", "error": str(e), "correlation_id": correlation_id} |
| 205 | + |
| 206 | + async def health_check(self) -> bool: |
| 207 | + try: |
| 208 | + session_id = self.react_agent.create_session(f"health-{uuid.uuid4().hex[:8]}") |
| 209 | + response = self.react_agent.create_turn( |
| 210 | + messages=[{"role": "user", "content": "Respond with JSON: {\"status\": \"healthy\"}"}], |
| 211 | + session_id=session_id, |
| 212 | + stream=False |
| 213 | + ) |
| 214 | + content = getattr(response.output_message, 'content', '').lower() if hasattr(response, 'output_message') else "" |
| 215 | + return "healthy" in content |
| 216 | + except: |
| 217 | + return False |
| 218 | + |
| 219 | + def get_status(self) -> Dict[str, Any]: |
| 220 | + return { |
| 221 | + "agent_name": self.agent_name, |
| 222 | + "model": self.model, |
| 223 | + "tools": self.tools, |
| 224 | + "status": "ready", |
| 225 | + "pattern": "ReAct (Config Driven)", |
| 226 | + "timestamp": datetime.now().isoformat() |
| 227 | + } |
0 commit comments