Skip to content

Commit 9bef7e5

Browse files
committed
feat: Integrate LangGraph validation agent as default
- Add LangGraphValidationAgent as default validation agent - Replace problematic LlamaStack validation with reliable ansible-lint integration - Provide structured JSON output instead of raw text - Maintain backward compatibility with existing API endpoints - Add fallback mechanism to original agent if needed - Update requirements.txt with LangGraph dependency All validation endpoints now use LangGraph agent by default: - /api/validate/playbook - /api/validate/playbook/stream - /api/validate/syntax - /api/validate/production Performance: ~1.6s response time, structured JSON output
1 parent dc909b0 commit 9bef7e5

5 files changed

Lines changed: 595 additions & 52 deletions

File tree

agents/validate/lg.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import os
2+
import tempfile
3+
import subprocess
4+
import traceback
5+
import json
6+
import logging
7+
from fastapi import FastAPI, HTTPException
8+
from fastapi.responses import StreamingResponse
9+
from pydantic import BaseModel
10+
from typing import List, Dict, Any
11+
12+
from langgraph.graph import StateGraph, END
13+
14+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
15+
16+
app = FastAPI()
17+
18+
class PlaybookRequest(BaseModel):
19+
playbook_content: str
20+
21+
class LintState(BaseModel):
22+
code: str
23+
lint_summary: str = ""
24+
passed: bool = False
25+
issues: List[str] = []
26+
27+
def run_ansible_lint_subprocess(playbook_code: str) -> dict:
28+
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".yml") as f:
29+
f.write(playbook_code)
30+
fname = f.name
31+
try:
32+
result = subprocess.run(
33+
["ansible-lint", fname],
34+
stdout=subprocess.PIPE,
35+
stderr=subprocess.STDOUT,
36+
universal_newlines=True
37+
)
38+
output = result.stdout
39+
passed = result.returncode == 0
40+
summary = output.strip() if not passed else "No issues found."
41+
issues = output.strip().splitlines() if not passed else []
42+
return {"passed": passed, "summary": summary, "issues": issues}
43+
finally:
44+
try:
45+
os.remove(fname)
46+
except Exception:
47+
pass
48+
49+
def lint_node(state: LintState) -> LintState:
50+
logging.info("[AGENT] Running ansible-lint tool as agent node.")
51+
lint_result = run_ansible_lint_subprocess(state.code)
52+
state.lint_summary = lint_result["summary"]
53+
state.passed = lint_result["passed"]
54+
state.issues = lint_result["issues"]
55+
return state
56+
57+
graph = StateGraph(state_schema=LintState)
58+
graph.add_node("lint", lint_node)
59+
graph.set_entry_point("lint")
60+
graph.add_edge("lint", END)
61+
compiled_graph = graph.compile()
62+
63+
@app.post("/api/validate/playbook/stream")
64+
async def validate_playbook_stream(request: PlaybookRequest):
65+
async def event_stream():
66+
try:
67+
initial_state = LintState(code=request.playbook_content)
68+
# NOTE: For old LangGraph, don't use return_type!
69+
final_state = await compiled_graph.ainvoke(initial_state)
70+
# final_state is a dict, not a LintState object
71+
yield f"data: {json.dumps({'type': 'progress', 'message': 'Lint completed', 'lint_summary': final_state.get('lint_summary', '')[:150]})}\n\n"
72+
result = {
73+
"passed": final_state.get('passed', False),
74+
"lint_summary": final_state.get('lint_summary', ''),
75+
"issues": final_state.get('issues', []),
76+
"code": final_state.get('code', ''),
77+
}
78+
yield f"data: {json.dumps({'type': 'final_result', 'data': result})}\n\n"
79+
yield "data: [DONE]\n\n"
80+
except Exception as e:
81+
tb = traceback.format_exc()
82+
yield f"data: {json.dumps({'type': 'error', 'message': str(e), 'traceback': tb})}\n\n"
83+
return StreamingResponse(event_stream(), media_type="text/event-stream")

0 commit comments

Comments
 (0)