Skip to content

Commit 4f17cf7

Browse files
committed
Added upgrade
1 parent 5f07e22 commit 4f17cf7

15 files changed

Lines changed: 1735 additions & 56 deletions

File tree

agents/.DS_Store

0 Bytes
Binary file not shown.

agents/ansible_upgrade/__init__.py

Whitespace-only changes.

agents/ansible_upgrade/agent.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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

Comments
 (0)