Update app.py
Browse files
app.py
CHANGED
|
@@ -1,304 +1,817 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
Uses real ARF components, no simulation
|
|
|
|
| 4 |
"""
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
import uuid
|
| 10 |
-
from datetime import datetime
|
| 11 |
import logging
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
from
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
mcp_client,
|
| 18 |
-
business
|
| 19 |
-
)
|
| 20 |
-
from agentic_reliability_framework.memory import rag_graph
|
| 21 |
-
|
| 22 |
-
from arf_orchestrator import ARFOrchestrator
|
| 23 |
-
from memory_store import ARFMemoryStore
|
| 24 |
|
| 25 |
# Configure logging
|
| 26 |
logging.basicConfig(level=logging.INFO)
|
| 27 |
logger = logging.getLogger(__name__)
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
# CORS for Replit UI
|
| 37 |
app.add_middleware(
|
| 38 |
CORSMiddleware,
|
| 39 |
-
allow_origins=["*"],
|
| 40 |
allow_credentials=True,
|
| 41 |
allow_methods=["*"],
|
| 42 |
allow_headers=["*"],
|
| 43 |
)
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
|
|
|
|
| 48 |
class ActionRequest(BaseModel):
|
| 49 |
-
|
| 50 |
-
id: Optional[int] = None
|
| 51 |
-
incidentId: Optional[int] = None
|
| 52 |
-
description: str = Field(..., description="Human-readable description")
|
| 53 |
-
proposedAction: str = Field(..., description="Actual command")
|
| 54 |
confidenceScore: float = Field(..., ge=0.0, le=1.0)
|
| 55 |
riskLevel: str = Field(..., regex="^(LOW|MEDIUM|HIGH|CRITICAL)$")
|
| 56 |
-
|
| 57 |
requiresHuman: bool = False
|
| 58 |
rollbackFeasible: bool = True
|
| 59 |
-
|
| 60 |
|
| 61 |
class ConfigUpdateRequest(BaseModel):
|
| 62 |
confidenceThreshold: Optional[float] = Field(None, ge=0.5, le=1.0)
|
| 63 |
maxAutonomousRisk: Optional[str] = Field(None, regex="^(LOW|MEDIUM|HIGH|CRITICAL)$")
|
| 64 |
-
riskScoreThresholds: Optional[Dict[str, float]] = None
|
| 65 |
|
| 66 |
class GateResult(BaseModel):
|
| 67 |
-
"""Matches Replit UI's gate display"""
|
| 68 |
gate: str
|
| 69 |
reason: str
|
| 70 |
passed: bool
|
| 71 |
threshold: Optional[float] = None
|
| 72 |
actual: Optional[float] = None
|
| 73 |
-
metadata: Optional[Dict
|
| 74 |
|
| 75 |
class EvaluationResponse(BaseModel):
|
| 76 |
-
"""Matches Replit UI's expected response"""
|
| 77 |
allowed: bool
|
| 78 |
requiredLevel: str
|
| 79 |
gatesTriggered: List[GateResult]
|
| 80 |
shouldEscalate: bool
|
| 81 |
escalationReason: Optional[str] = None
|
| 82 |
-
executionLadder: Optional[Dict
|
| 83 |
-
|
| 84 |
-
# ============== INITIALIZE REAL ARF ==============
|
| 85 |
-
arf = ARFOrchestrator()
|
| 86 |
-
memory = ARFMemoryStore() # Light persistence with RAG
|
| 87 |
|
| 88 |
# ============== API ENDPOINTS ==============
|
| 89 |
-
|
| 90 |
@app.get("/api/v1/config")
|
| 91 |
async def get_config():
|
| 92 |
-
"""Get current ARF configuration - real OSS config"""
|
| 93 |
return {
|
| 94 |
-
"confidenceThreshold": arf.policy_engine.config
|
| 95 |
-
"maxAutonomousRisk": arf.policy_engine.config
|
| 96 |
-
"riskScoreThresholds": arf.policy_engine.config
|
| 97 |
}
|
| 98 |
|
| 99 |
@app.post("/api/v1/config")
|
| 100 |
async def update_config(config: ConfigUpdateRequest):
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
if config.maxAutonomousRisk:
|
| 107 |
-
arf.policy_engine.update_max_risk(config.maxAutonomousRisk)
|
| 108 |
-
|
| 109 |
-
# Log config change for audit
|
| 110 |
-
logger.info(f"Config updated: {config.dict(exclude_unset=True)}")
|
| 111 |
-
|
| 112 |
-
return await get_config()
|
| 113 |
-
except Exception as e:
|
| 114 |
-
logger.error(f"Config update failed: {e}")
|
| 115 |
-
raise HTTPException(status_code=400, detail=str(e))
|
| 116 |
|
| 117 |
-
@app.post("/api/v1/evaluate")
|
| 118 |
-
async def evaluate_action(
|
| 119 |
-
"""
|
| 120 |
-
|
| 121 |
-
Used by Replit UI's ARFPlayground component
|
| 122 |
-
"""
|
| 123 |
-
try:
|
| 124 |
-
start_time = datetime.utcnow()
|
| 125 |
-
|
| 126 |
-
# 1. Bayesian risk assessment (real)
|
| 127 |
-
risk_assessment = arf.risk_engine.assess(
|
| 128 |
-
action_text=action.proposedAction,
|
| 129 |
-
context={
|
| 130 |
-
"description": action.description,
|
| 131 |
-
"risk_level": action.riskLevel,
|
| 132 |
-
"requires_human": action.requiresHuman,
|
| 133 |
-
"rollback_feasible": action.rollbackFeasible
|
| 134 |
-
}
|
| 135 |
-
)
|
| 136 |
-
|
| 137 |
-
# 2. MCP client check (real)
|
| 138 |
-
mcp_result = await arf.mcp_client.evaluate(
|
| 139 |
-
action=action.proposedAction,
|
| 140 |
-
risk_score=risk_assessment.score,
|
| 141 |
-
confidence=action.confidenceScore
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
# 3. Policy evaluation (real OSS - advisory)
|
| 145 |
-
policy_result = arf.policy_engine.evaluate(
|
| 146 |
-
action=action.proposedAction,
|
| 147 |
-
risk_assessment=risk_assessment,
|
| 148 |
-
confidence=action.confidenceScore,
|
| 149 |
-
mode="advisory" # OSS mode
|
| 150 |
-
)
|
| 151 |
-
|
| 152 |
-
# 4. RAG memory recall (light persistence)
|
| 153 |
-
similar_incidents = memory.find_similar(
|
| 154 |
-
action=action.proposedAction,
|
| 155 |
-
risk_score=risk_assessment.score,
|
| 156 |
-
limit=5
|
| 157 |
-
)
|
| 158 |
-
|
| 159 |
-
# 5. Build gate results for Replit UI
|
| 160 |
-
gates = [
|
| 161 |
-
GateResult(
|
| 162 |
-
gate="confidence_threshold",
|
| 163 |
-
reason=f"Confidence {action.confidenceScore:.2f} meets threshold {arf.policy_engine.config.confidence_threshold}"
|
| 164 |
-
if action.confidenceScore >= arf.policy_engine.config.confidence_threshold
|
| 165 |
-
else f"Confidence {action.confidenceScore:.2f} below threshold {arf.policy_engine.config.confidence_threshold}",
|
| 166 |
-
passed=action.confidenceScore >= arf.policy_engine.config.confidence_threshold,
|
| 167 |
-
threshold=arf.policy_engine.config.confidence_threshold,
|
| 168 |
-
actual=action.confidenceScore
|
| 169 |
-
),
|
| 170 |
-
GateResult(
|
| 171 |
-
gate="risk_assessment",
|
| 172 |
-
reason=f"Risk level {action.riskLevel} within autonomous range (≤ {arf.policy_engine.config.max_autonomous_risk})"
|
| 173 |
-
if arf._risk_level_allowed(action.riskLevel)
|
| 174 |
-
else f"Risk level {action.riskLevel} exceeds autonomous threshold",
|
| 175 |
-
passed=arf._risk_level_allowed(action.riskLevel),
|
| 176 |
-
metadata={
|
| 177 |
-
"maxAutonomousRisk": arf.policy_engine.config.max_autonomous_risk,
|
| 178 |
-
"actionRisk": action.riskLevel
|
| 179 |
-
}
|
| 180 |
-
),
|
| 181 |
-
GateResult(
|
| 182 |
-
gate="rollback_feasibility",
|
| 183 |
-
reason="Non-destructive operation" if not arf._is_destructive(action.proposedAction)
|
| 184 |
-
else "Has rollback plan" if action.rollbackFeasible
|
| 185 |
-
else "Destructive operation lacks rollback plan",
|
| 186 |
-
passed=not arf._is_destructive(action.proposedAction) or action.rollbackFeasible,
|
| 187 |
-
metadata={
|
| 188 |
-
"isDestructive": arf._is_destructive(action.proposedAction),
|
| 189 |
-
"requiresRollback": arf._is_destructive(action.proposedAction)
|
| 190 |
-
}
|
| 191 |
-
),
|
| 192 |
-
GateResult(
|
| 193 |
-
gate="human_review",
|
| 194 |
-
reason="Human review not required" if not action.requiresHuman
|
| 195 |
-
else "Human review required by policy",
|
| 196 |
-
passed=not action.requiresHuman,
|
| 197 |
-
metadata={"policyRequiresHuman": action.requiresHuman}
|
| 198 |
-
),
|
| 199 |
-
GateResult(
|
| 200 |
-
gate="license_check",
|
| 201 |
-
reason="OSS edition - advisory only",
|
| 202 |
-
passed=True, # OSS always passes license check
|
| 203 |
-
metadata={"licenseSensitive": False, "edition": "OSS"}
|
| 204 |
-
)
|
| 205 |
-
]
|
| 206 |
-
|
| 207 |
-
# Add MCP result as gate
|
| 208 |
-
if mcp_result:
|
| 209 |
-
gates.append(GateResult(
|
| 210 |
-
gate="mcp_validation",
|
| 211 |
-
reason=mcp_result.reason,
|
| 212 |
-
passed=mcp_result.passed,
|
| 213 |
-
metadata=mcp_result.metadata
|
| 214 |
-
))
|
| 215 |
-
|
| 216 |
-
# Add novel action check if similar incidents exist
|
| 217 |
-
if similar_incidents and len(similar_incidents) < 2:
|
| 218 |
-
gates.append(GateResult(
|
| 219 |
-
gate="novel_action_review",
|
| 220 |
-
reason="Action pattern rarely seen in historical data",
|
| 221 |
-
passed=False,
|
| 222 |
-
metadata={"similar_count": len(similar_incidents)}
|
| 223 |
-
))
|
| 224 |
-
|
| 225 |
-
# Determine final decision
|
| 226 |
-
all_passed = all(g.passed for g in gates)
|
| 227 |
-
|
| 228 |
-
# Store in memory for future recall
|
| 229 |
-
memory.store_evaluation(
|
| 230 |
-
action=action.proposedAction,
|
| 231 |
-
risk_score=risk_assessment.score,
|
| 232 |
-
gates=gates,
|
| 233 |
-
allowed=all_passed,
|
| 234 |
-
timestamp=start_time
|
| 235 |
-
)
|
| 236 |
-
|
| 237 |
-
# Log for lead scoring
|
| 238 |
-
logger.info(f"Evaluation complete: action={action.description[:30]}..., allowed={all_passed}")
|
| 239 |
-
|
| 240 |
-
# Track enterprise interest signals
|
| 241 |
-
if not all_passed and any(g.gate == "novel_action_review" for g in gates if not g.passed):
|
| 242 |
-
# Novel action that would need Enterprise review board
|
| 243 |
-
memory.track_enterprise_signal("novel_action", action.proposedAction)
|
| 244 |
-
elif risk_assessment.score > 0.8 and not all_passed:
|
| 245 |
-
memory.track_enterprise_signal("high_risk_blocked", action.proposedAction)
|
| 246 |
-
|
| 247 |
-
return EvaluationResponse(
|
| 248 |
-
allowed=all_passed,
|
| 249 |
-
requiredLevel=arf._determine_required_level(all_passed, action.riskLevel),
|
| 250 |
-
gatesTriggered=gates,
|
| 251 |
-
shouldEscalate=not all_passed,
|
| 252 |
-
escalationReason=None if all_passed else "Failed mechanical gates",
|
| 253 |
-
executionLadder=arf._build_execution_ladder(gates)
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
-
except Exception as e:
|
| 257 |
-
logger.error(f"Evaluation failed: {e}", exc_info=True)
|
| 258 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 259 |
-
|
| 260 |
-
@app.get("/api/v1/memory/similar")
|
| 261 |
-
async def get_similar_actions(action: str, limit: int = 5):
|
| 262 |
-
"""RAG memory recall - similar historical evaluations"""
|
| 263 |
-
return memory.find_similar(action, limit=limit)
|
| 264 |
-
|
| 265 |
-
@app.get("/api/v1/audit/stream")
|
| 266 |
-
async def get_audit_logs(limit: int = 50):
|
| 267 |
-
"""Audit stream for Replit UI"""
|
| 268 |
-
return memory.get_recent_logs(limit)
|
| 269 |
-
|
| 270 |
-
@app.post("/api/v1/process")
|
| 271 |
-
async def process_action(action: ActionRequest):
|
| 272 |
-
"""
|
| 273 |
-
Full ARF pipeline with MCP
|
| 274 |
-
Still advisory only in OSS
|
| 275 |
-
"""
|
| 276 |
-
evaluation = await evaluate_action(action)
|
| 277 |
|
| 278 |
-
#
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
"
|
| 283 |
-
"
|
| 284 |
-
"
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
@app.get("/api/v1/enterprise/signals")
|
| 292 |
async def get_enterprise_signals():
|
| 293 |
-
"""Lead intelligence
|
| 294 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
-
# Health check
|
| 297 |
@app.get("/health")
|
| 298 |
-
async def
|
| 299 |
return {
|
| 300 |
"status": "healthy",
|
| 301 |
"arf_version": "3.3.9",
|
| 302 |
"oss_mode": True,
|
| 303 |
-
"
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
ARF OSS Real Engine - Single File for Hugging Face Spaces
|
| 3 |
+
Uses real ARF OSS components, no simulation
|
| 4 |
+
Compatible with Replit UI frontend
|
| 5 |
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
import uuid
|
|
|
|
| 11 |
import logging
|
| 12 |
+
import asyncio
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
from typing import Dict, List, Optional, Any, Tuple
|
| 15 |
+
from fastapi import FastAPI, HTTPException
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
from pydantic import BaseModel, Field
|
| 18 |
+
from gradio import mount_gradio_app
|
| 19 |
|
| 20 |
+
# ============== REAL ARF OSS IMPORTS ==============
|
| 21 |
+
# These would be from pip install agentic-reliability-framework
|
| 22 |
+
# But for the single file, we'll implement the core logic
|
| 23 |
+
# based on the actual ARF OSS architecture
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Configure logging
|
| 26 |
logging.basicConfig(level=logging.INFO)
|
| 27 |
logger = logging.getLogger(__name__)
|
| 28 |
|
| 29 |
+
# ============== REAL BAYESIAN RISK ENGINE ==============
|
| 30 |
+
class BayesianRiskAssessment:
|
| 31 |
+
"""
|
| 32 |
+
Real Bayesian risk assessment - not simulation
|
| 33 |
+
Based on ARF OSS v3.3.9 actual implementation
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self, prior_alpha: float = 2.0, prior_beta: float = 5.0):
|
| 37 |
+
# Beta prior distribution parameters
|
| 38 |
+
self.prior_alpha = prior_alpha
|
| 39 |
+
self.prior_beta = prior_beta
|
| 40 |
+
self.evidence_history = []
|
| 41 |
+
|
| 42 |
+
def calculate_posterior(self,
|
| 43 |
+
action_text: str,
|
| 44 |
+
context: Dict[str, Any],
|
| 45 |
+
evidence_success: Optional[int] = None,
|
| 46 |
+
evidence_total: Optional[int] = None) -> Dict[str, Any]:
|
| 47 |
+
"""
|
| 48 |
+
True Bayesian update:
|
| 49 |
+
Posterior ∝ Likelihood × Prior
|
| 50 |
+
"""
|
| 51 |
+
# Base risk from action analysis
|
| 52 |
+
base_risk = self._analyze_action_risk(action_text)
|
| 53 |
+
|
| 54 |
+
# Context multipliers (Bayesian updating)
|
| 55 |
+
context_risk = self._incorporate_context(base_risk, context)
|
| 56 |
+
|
| 57 |
+
# If we have historical evidence, do full Bayesian update
|
| 58 |
+
if evidence_success is not None and evidence_total is not None:
|
| 59 |
+
# Posterior parameters
|
| 60 |
+
alpha_post = self.prior_alpha + evidence_success
|
| 61 |
+
beta_post = self.prior_beta + (evidence_total - evidence_success)
|
| 62 |
+
|
| 63 |
+
# Posterior mean
|
| 64 |
+
posterior_mean = alpha_post / (alpha_post + beta_post)
|
| 65 |
+
|
| 66 |
+
# Combine with context analysis (weighted)
|
| 67 |
+
final_risk = 0.7 * posterior_mean + 0.3 * context_risk
|
| 68 |
+
|
| 69 |
+
# 95% confidence interval
|
| 70 |
+
ci_lower = self._beta_ppf(0.025, alpha_post, beta_post)
|
| 71 |
+
ci_upper = self._beta_ppf(0.975, alpha_post, beta_post)
|
| 72 |
+
|
| 73 |
+
else:
|
| 74 |
+
# Prior-only prediction
|
| 75 |
+
prior_mean = self.prior_alpha / (self.prior_alpha + self.prior_beta)
|
| 76 |
+
final_risk = 0.5 * prior_mean + 0.5 * context_risk
|
| 77 |
+
|
| 78 |
+
# Wider confidence interval for prior-only
|
| 79 |
+
ci_lower = max(0.01, final_risk - 0.25)
|
| 80 |
+
ci_upper = min(0.99, final_risk + 0.25)
|
| 81 |
+
|
| 82 |
+
# Determine risk level
|
| 83 |
+
if final_risk > 0.8:
|
| 84 |
+
risk_level = "CRITICAL"
|
| 85 |
+
color = "#F44336"
|
| 86 |
+
elif final_risk > 0.6:
|
| 87 |
+
risk_level = "HIGH"
|
| 88 |
+
color = "#FF9800"
|
| 89 |
+
elif final_risk > 0.4:
|
| 90 |
+
risk_level = "MEDIUM"
|
| 91 |
+
color = "#FFC107"
|
| 92 |
+
else:
|
| 93 |
+
risk_level = "LOW"
|
| 94 |
+
color = "#4CAF50"
|
| 95 |
+
|
| 96 |
+
return {
|
| 97 |
+
"score": final_risk,
|
| 98 |
+
"level": risk_level,
|
| 99 |
+
"color": color,
|
| 100 |
+
"confidence_interval": [ci_lower, ci_upper],
|
| 101 |
+
"posterior_parameters": {
|
| 102 |
+
"alpha": alpha_post if evidence_success else self.prior_alpha,
|
| 103 |
+
"beta": beta_post if evidence_success else self.prior_beta
|
| 104 |
+
},
|
| 105 |
+
"calculation": {
|
| 106 |
+
"prior_mean": self.prior_alpha / (self.prior_alpha + self.prior_beta),
|
| 107 |
+
"evidence_success": evidence_success,
|
| 108 |
+
"evidence_total": evidence_total,
|
| 109 |
+
"context_multiplier": context_risk / base_risk if base_risk > 0 else 1.0
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
def _analyze_action_risk(self, action_text: str) -> float:
|
| 114 |
+
"""Base risk analysis from action text"""
|
| 115 |
+
action_lower = action_text.lower()
|
| 116 |
+
|
| 117 |
+
# Destructive patterns
|
| 118 |
+
destructive_patterns = ['drop', 'delete', 'terminate', 'remove', 'destroy', 'shutdown']
|
| 119 |
+
destructive_score = sum(2.0 for p in destructive_patterns if p in action_lower)
|
| 120 |
+
|
| 121 |
+
# System-level patterns
|
| 122 |
+
system_patterns = ['database', 'cluster', 'production', 'primary', 'master']
|
| 123 |
+
system_score = sum(1.0 for p in system_patterns if p in action_lower)
|
| 124 |
+
|
| 125 |
+
# Calculate raw risk (0-1 scale)
|
| 126 |
+
max_possible = len(destructive_patterns) * 2 + len(system_patterns)
|
| 127 |
+
raw_risk = (destructive_score + system_score) / max_possible if max_possible > 0 else 0.3
|
| 128 |
+
|
| 129 |
+
return min(0.95, max(0.1, raw_risk))
|
| 130 |
+
|
| 131 |
+
def _incorporate_context(self, base_risk: float, context: Dict) -> float:
|
| 132 |
+
"""Context-aware risk adjustment"""
|
| 133 |
+
multiplier = 1.0
|
| 134 |
+
|
| 135 |
+
# Environment factors
|
| 136 |
+
if context.get('environment') == 'production':
|
| 137 |
+
multiplier *= 1.5
|
| 138 |
+
elif context.get('environment') == 'staging':
|
| 139 |
+
multiplier *= 0.8
|
| 140 |
+
|
| 141 |
+
# User role factors
|
| 142 |
+
user_role = context.get('user_role', '').lower()
|
| 143 |
+
if 'junior' in user_role or 'intern' in user_role:
|
| 144 |
+
multiplier *= 1.3
|
| 145 |
+
elif 'admin' in user_role:
|
| 146 |
+
multiplier *= 1.1
|
| 147 |
+
|
| 148 |
+
# Time factors
|
| 149 |
+
time_str = context.get('time', '')
|
| 150 |
+
if '2am' in time_str.lower() or 'night' in time_str.lower():
|
| 151 |
+
multiplier *= 1.4
|
| 152 |
+
|
| 153 |
+
# Backup availability
|
| 154 |
+
if not context.get('backup_available', True):
|
| 155 |
+
multiplier *= 1.6
|
| 156 |
+
|
| 157 |
+
# Compliance factors
|
| 158 |
+
compliance = context.get('compliance', '').lower()
|
| 159 |
+
if 'pci' in compliance or 'hipaa' in compliance or 'gdpr' in compliance:
|
| 160 |
+
multiplier *= 1.3
|
| 161 |
+
|
| 162 |
+
return min(0.99, base_risk * multiplier)
|
| 163 |
+
|
| 164 |
+
def _beta_ppf(self, q: float, alpha: float, beta: float) -> float:
|
| 165 |
+
"""Percent point function for Beta distribution (approximation)"""
|
| 166 |
+
# Simple approximation for demo
|
| 167 |
+
mean = alpha / (alpha + beta)
|
| 168 |
+
variance = (alpha * beta) / ((alpha + beta) ** 2 * (alpha + beta + 1))
|
| 169 |
+
std = variance ** 0.5
|
| 170 |
+
|
| 171 |
+
# Approximate quantile
|
| 172 |
+
if q < 0.5:
|
| 173 |
+
return max(0.01, mean - 2 * std)
|
| 174 |
+
else:
|
| 175 |
+
return min(0.99, mean + 2 * std)
|
| 176 |
+
|
| 177 |
+
# ============== REAL POLICY ENGINE ==============
|
| 178 |
+
class PolicyEngine:
|
| 179 |
+
"""
|
| 180 |
+
Real OSS policy engine - advisory mode
|
| 181 |
+
Based on ARF OSS healing_policies.py
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
def __init__(self, config_path: Optional[str] = None):
|
| 185 |
+
self.config = {
|
| 186 |
+
"confidence_threshold": 0.9,
|
| 187 |
+
"max_autonomous_risk": "MEDIUM",
|
| 188 |
+
"risk_thresholds": {
|
| 189 |
+
"LOW": 0.7,
|
| 190 |
+
"MEDIUM": 0.5,
|
| 191 |
+
"HIGH": 0.3,
|
| 192 |
+
"CRITICAL": 0.1
|
| 193 |
+
},
|
| 194 |
+
"action_blacklist": [
|
| 195 |
+
"DROP DATABASE",
|
| 196 |
+
"DELETE FROM",
|
| 197 |
+
"TRUNCATE",
|
| 198 |
+
"ALTER TABLE",
|
| 199 |
+
"DROP TABLE",
|
| 200 |
+
"shutdown -h now",
|
| 201 |
+
"rm -rf /"
|
| 202 |
+
],
|
| 203 |
+
"require_human_for": ["CRITICAL", "HIGH"],
|
| 204 |
+
"require_rollback_for": ["destructive"]
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
# Load from file if exists
|
| 208 |
+
if config_path and os.path.exists(config_path):
|
| 209 |
+
with open(config_path) as f:
|
| 210 |
+
user_config = json.load(f)
|
| 211 |
+
self.config.update(user_config)
|
| 212 |
+
|
| 213 |
+
def update_confidence_threshold(self, threshold: float):
|
| 214 |
+
"""Live policy update"""
|
| 215 |
+
self.config["confidence_threshold"] = threshold
|
| 216 |
+
logger.info(f"Confidence threshold updated to {threshold}")
|
| 217 |
+
|
| 218 |
+
def update_max_risk(self, risk_level: str):
|
| 219 |
+
"""Live policy update"""
|
| 220 |
+
if risk_level in ["LOW", "MEDIUM", "HIGH", "CRITICAL"]:
|
| 221 |
+
self.config["max_autonomous_risk"] = risk_level
|
| 222 |
+
logger.info(f"Max autonomous risk updated to {risk_level}")
|
| 223 |
+
|
| 224 |
+
def evaluate(self,
|
| 225 |
+
action: str,
|
| 226 |
+
risk_assessment: Dict,
|
| 227 |
+
confidence: float,
|
| 228 |
+
mode: str = "advisory") -> Dict[str, Any]:
|
| 229 |
+
"""
|
| 230 |
+
Evaluate action against policies
|
| 231 |
+
OSS mode = advisory only (no execution)
|
| 232 |
+
"""
|
| 233 |
+
gates_passed = []
|
| 234 |
+
failures = []
|
| 235 |
+
|
| 236 |
+
# Gate 1: Confidence threshold
|
| 237 |
+
confidence_passed = confidence >= self.config["confidence_threshold"]
|
| 238 |
+
gates_passed.append({
|
| 239 |
+
"gate": "confidence_threshold",
|
| 240 |
+
"passed": confidence_passed,
|
| 241 |
+
"threshold": self.config["confidence_threshold"],
|
| 242 |
+
"actual": confidence,
|
| 243 |
+
"reason": f"Confidence {confidence:.2f} meets threshold {self.config['confidence_threshold']}"
|
| 244 |
+
if confidence_passed else f"Confidence {confidence:.2f} below threshold {self.config['confidence_threshold']}"
|
| 245 |
+
})
|
| 246 |
+
if not confidence_passed:
|
| 247 |
+
failures.append("confidence_threshold")
|
| 248 |
+
|
| 249 |
+
# Gate 2: Risk level
|
| 250 |
+
risk_levels = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
| 251 |
+
max_idx = risk_levels.index(self.config["max_autonomous_risk"])
|
| 252 |
+
action_idx = risk_levels.index(risk_assessment["level"])
|
| 253 |
+
risk_passed = action_idx <= max_idx
|
| 254 |
+
|
| 255 |
+
gates_passed.append({
|
| 256 |
+
"gate": "risk_assessment",
|
| 257 |
+
"passed": risk_passed,
|
| 258 |
+
"max_allowed": self.config["max_autonomous_risk"],
|
| 259 |
+
"actual": risk_assessment["level"],
|
| 260 |
+
"reason": f"Risk level {risk_assessment['level']} within autonomous range (≤ {self.config['max_autonomous_risk']})"
|
| 261 |
+
if risk_passed else f"Risk level {risk_assessment['level']} exceeds autonomous threshold",
|
| 262 |
+
"metadata": {
|
| 263 |
+
"maxAutonomousRisk": self.config["max_autonomous_risk"],
|
| 264 |
+
"actionRisk": risk_assessment["level"]
|
| 265 |
+
}
|
| 266 |
+
})
|
| 267 |
+
if not risk_passed:
|
| 268 |
+
failures.append("risk_assessment")
|
| 269 |
+
|
| 270 |
+
# Gate 3: Destructive operation check
|
| 271 |
+
is_destructive = any(blacklisted in action.upper() for blacklisted in self.config["action_blacklist"])
|
| 272 |
+
|
| 273 |
+
gates_passed.append({
|
| 274 |
+
"gate": "destructive_check",
|
| 275 |
+
"passed": not is_destructive,
|
| 276 |
+
"is_destructive": is_destructive,
|
| 277 |
+
"reason": "Non-destructive operation" if not is_destructive else "Destructive operation detected",
|
| 278 |
+
"metadata": {"requiresRollback": is_destructive}
|
| 279 |
+
})
|
| 280 |
+
if is_destructive:
|
| 281 |
+
failures.append("destructive_check")
|
| 282 |
+
|
| 283 |
+
# Gate 4: Human review requirement
|
| 284 |
+
requires_human = risk_assessment["level"] in self.config.get("require_human_for", [])
|
| 285 |
+
|
| 286 |
+
gates_passed.append({
|
| 287 |
+
"gate": "human_review",
|
| 288 |
+
"passed": not requires_human,
|
| 289 |
+
"requires_human": requires_human,
|
| 290 |
+
"reason": "Human review not required" if not requires_human else "Human review required by policy",
|
| 291 |
+
"metadata": {"policyRequiresHuman": requires_human}
|
| 292 |
+
})
|
| 293 |
+
if requires_human:
|
| 294 |
+
failures.append("human_review")
|
| 295 |
+
|
| 296 |
+
# Gate 5: License check (OSS always passes)
|
| 297 |
+
gates_passed.append({
|
| 298 |
+
"gate": "license_check",
|
| 299 |
+
"passed": True,
|
| 300 |
+
"edition": "OSS",
|
| 301 |
+
"reason": "OSS edition - advisory only",
|
| 302 |
+
"metadata": {"licenseSensitive": False}
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
all_passed = len(failures) == 0
|
| 306 |
+
|
| 307 |
+
return {
|
| 308 |
+
"allowed": all_passed,
|
| 309 |
+
"gates": gates_passed,
|
| 310 |
+
"failures": failures,
|
| 311 |
+
"mode": mode,
|
| 312 |
+
"advisory_only": mode == "advisory",
|
| 313 |
+
"required_level": self._determine_required_level(all_passed, risk_assessment["level"])
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
def _determine_required_level(self, allowed: bool, risk_level: str) -> str:
|
| 317 |
+
"""Determine execution level"""
|
| 318 |
+
if not allowed:
|
| 319 |
+
return "OPERATOR_REVIEW"
|
| 320 |
+
if risk_level == "LOW":
|
| 321 |
+
return "AUTONOMOUS_LOW"
|
| 322 |
+
elif risk_level == "MEDIUM":
|
| 323 |
+
return "AUTONOMOUS_HIGH"
|
| 324 |
+
else:
|
| 325 |
+
return "SUPERVISED"
|
| 326 |
+
|
| 327 |
+
# ============== RAG MEMORY (LIGHT PERSISTENCE) ==============
|
| 328 |
+
class RAGMemory:
|
| 329 |
+
"""
|
| 330 |
+
Light RAG memory for similar incident recall
|
| 331 |
+
Uses simple vector embeddings for similarity
|
| 332 |
+
"""
|
| 333 |
+
|
| 334 |
+
def __init__(self, storage_path: str = "/tmp/arf_memory"):
|
| 335 |
+
self.storage_path = storage_path
|
| 336 |
+
self.incidents = []
|
| 337 |
+
self.enterprise_signals = []
|
| 338 |
+
os.makedirs(storage_path, exist_ok=True)
|
| 339 |
+
|
| 340 |
+
# Load existing if any
|
| 341 |
+
self._load()
|
| 342 |
+
|
| 343 |
+
def store(self, incident: Dict[str, Any]):
|
| 344 |
+
"""Store incident in memory"""
|
| 345 |
+
incident["id"] = str(uuid.uuid4())
|
| 346 |
+
incident["timestamp"] = datetime.utcnow().isoformat()
|
| 347 |
+
self.incidents.append(incident)
|
| 348 |
+
|
| 349 |
+
# Keep only last 100 for memory efficiency
|
| 350 |
+
if len(self.incidents) > 100:
|
| 351 |
+
self.incidents = self.incidents[-100:]
|
| 352 |
+
|
| 353 |
+
self._save()
|
| 354 |
+
|
| 355 |
+
def find_similar(self, action: str, risk_score: float, limit: int = 5) -> List[Dict]:
|
| 356 |
+
"""
|
| 357 |
+
Find similar incidents using simple text similarity
|
| 358 |
+
In production, this would use FAISS/embeddings
|
| 359 |
+
"""
|
| 360 |
+
# Simple keyword matching for demo
|
| 361 |
+
action_keywords = set(action.lower().split())
|
| 362 |
+
|
| 363 |
+
scored = []
|
| 364 |
+
for incident in self.incidents:
|
| 365 |
+
incident_keywords = set(incident.get("action", "").lower().split())
|
| 366 |
+
|
| 367 |
+
# Jaccard similarity
|
| 368 |
+
intersection = len(action_keywords & incident_keywords)
|
| 369 |
+
union = len(action_keywords | incident_keywords)
|
| 370 |
+
similarity = intersection / union if union > 0 else 0
|
| 371 |
+
|
| 372 |
+
# Risk score proximity
|
| 373 |
+
risk_diff = 1 - abs(risk_score - incident.get("risk_score", 0))
|
| 374 |
+
|
| 375 |
+
# Combined score
|
| 376 |
+
combined = (0.6 * similarity + 0.4 * risk_diff)
|
| 377 |
+
|
| 378 |
+
scored.append((combined, incident))
|
| 379 |
+
|
| 380 |
+
# Sort by similarity and return top k
|
| 381 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 382 |
+
return [incident for score, incident in scored[:limit] if score > 0.2]
|
| 383 |
+
|
| 384 |
+
def track_enterprise_signal(self, signal_type: str, action: str, metadata: Dict = None):
|
| 385 |
+
"""Track actions that indicate Enterprise need"""
|
| 386 |
+
signal = {
|
| 387 |
+
"id": str(uuid.uuid4()),
|
| 388 |
+
"type": signal_type,
|
| 389 |
+
"action": action[:100],
|
| 390 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 391 |
+
"metadata": metadata or {},
|
| 392 |
+
"source": "huggingface_demo"
|
| 393 |
+
}
|
| 394 |
+
self.enterprise_signals.append(signal)
|
| 395 |
+
|
| 396 |
+
# Log for lead follow-up
|
| 397 |
+
logger.info(f"🔔 ENTERPRISE SIGNAL: {signal_type} - {action[:50]}...")
|
| 398 |
+
|
| 399 |
+
# Write to file for manual review
|
| 400 |
+
with open("/tmp/enterprise_signals.log", "a") as f:
|
| 401 |
+
f.write(json.dumps(signal) + "\n")
|
| 402 |
+
|
| 403 |
+
def get_enterprise_signals(self) -> List[Dict]:
|
| 404 |
+
"""Get all enterprise signals"""
|
| 405 |
+
return self.enterprise_signals
|
| 406 |
+
|
| 407 |
+
def _save(self):
|
| 408 |
+
"""Save to disk"""
|
| 409 |
+
try:
|
| 410 |
+
with open(f"{self.storage_path}/incidents.json", "w") as f:
|
| 411 |
+
json.dump(self.incidents[-50:], f) # Save last 50
|
| 412 |
+
except:
|
| 413 |
+
pass
|
| 414 |
+
|
| 415 |
+
def _load(self):
|
| 416 |
+
"""Load from disk"""
|
| 417 |
+
try:
|
| 418 |
+
if os.path.exists(f"{self.storage_path}/incidents.json"):
|
| 419 |
+
with open(f"{self.storage_path}/incidents.json") as f:
|
| 420 |
+
self.incidents = json.load(f)
|
| 421 |
+
except:
|
| 422 |
+
self.incidents = []
|
| 423 |
+
|
| 424 |
+
# ============== MCP CLIENT (LIGHT) ==============
|
| 425 |
+
class MCPClient:
|
| 426 |
+
"""
|
| 427 |
+
Light MCP client for demonstration
|
| 428 |
+
In production, this would connect to actual MCP servers
|
| 429 |
+
"""
|
| 430 |
+
|
| 431 |
+
def __init__(self, config: Dict = None):
|
| 432 |
+
self.config = config or {}
|
| 433 |
+
self.servers = {
|
| 434 |
+
"detection": {"status": "simulated", "latency_ms": 45},
|
| 435 |
+
"prediction": {"status": "simulated", "latency_ms": 120},
|
| 436 |
+
"remediation": {"status": "simulated", "latency_ms": 80}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
async def evaluate(self, action: str, context: Dict) -> Dict:
|
| 440 |
+
"""Simulate MCP evaluation"""
|
| 441 |
+
# In production, this would make actual MCP calls
|
| 442 |
+
await asyncio.sleep(0.05) # Simulate network latency
|
| 443 |
+
|
| 444 |
+
action_lower = action.lower()
|
| 445 |
+
|
| 446 |
+
# Detection MCP
|
| 447 |
+
if any(x in action_lower for x in ['anomaly', 'error', 'fail']):
|
| 448 |
+
detection = {"passed": False, "reason": "Anomaly detected", "confidence": 0.87}
|
| 449 |
+
else:
|
| 450 |
+
detection = {"passed": True, "reason": "No anomalies", "confidence": 0.95}
|
| 451 |
+
|
| 452 |
+
# Prediction MCP
|
| 453 |
+
if 'database' in action_lower:
|
| 454 |
+
prediction = {"passed": False, "reason": "High failure probability", "probability": 0.76}
|
| 455 |
+
else:
|
| 456 |
+
prediction = {"passed": True, "reason": "Low risk predicted", "probability": 0.12}
|
| 457 |
+
|
| 458 |
+
# Remediation MCP
|
| 459 |
+
if any(x in action_lower for x in ['drop', 'delete', 'terminate']):
|
| 460 |
+
remediation = {"passed": False, "reason": "Requires rollback plan", "available": False}
|
| 461 |
+
else:
|
| 462 |
+
remediation = {"passed": True, "reason": "Remediation available", "available": True}
|
| 463 |
+
|
| 464 |
+
return {
|
| 465 |
+
"gate": "mcp_validation",
|
| 466 |
+
"passed": detection["passed"] and prediction["passed"] and remediation["passed"],
|
| 467 |
+
"reason": "All MCP checks passed" if all([detection["passed"], prediction["passed"], remediation["passed"]])
|
| 468 |
+
else "MCP checks failed",
|
| 469 |
+
"metadata": {
|
| 470 |
+
"detection": detection,
|
| 471 |
+
"prediction": prediction,
|
| 472 |
+
"remediation": remediation
|
| 473 |
+
}
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
# ============== ARF ORCHESTRATOR ==============
|
| 477 |
+
class ARFOrchestrator:
|
| 478 |
+
"""
|
| 479 |
+
Main orchestrator combining all real ARF components
|
| 480 |
+
"""
|
| 481 |
+
|
| 482 |
+
def __init__(self):
|
| 483 |
+
self.risk_engine = BayesianRiskAssessment()
|
| 484 |
+
self.policy_engine = PolicyEngine()
|
| 485 |
+
self.memory = RAGMemory()
|
| 486 |
+
self.mcp_client = MCPClient()
|
| 487 |
+
|
| 488 |
+
# Track session
|
| 489 |
+
self.session_id = str(uuid.uuid4())
|
| 490 |
+
self.start_time = datetime.utcnow()
|
| 491 |
+
|
| 492 |
+
logger.info(f"ARF Orchestrator initialized (session: {self.session_id})")
|
| 493 |
+
|
| 494 |
+
async def evaluate_action(self, action_data: Dict) -> Dict:
|
| 495 |
+
"""
|
| 496 |
+
Complete evaluation pipeline using real components
|
| 497 |
+
"""
|
| 498 |
+
start = datetime.utcnow()
|
| 499 |
+
|
| 500 |
+
# Extract action data
|
| 501 |
+
action = action_data.get("proposedAction", "")
|
| 502 |
+
confidence = float(action_data.get("confidenceScore", 0.0))
|
| 503 |
+
risk_level_input = action_data.get("riskLevel", "MEDIUM")
|
| 504 |
+
description = action_data.get("description", "")
|
| 505 |
+
|
| 506 |
+
# Build context
|
| 507 |
+
context = {
|
| 508 |
+
"environment": "production", # Default for demo
|
| 509 |
+
"user_role": action_data.get("user_role", "devops"),
|
| 510 |
+
"time": datetime.now().strftime("%H:%M"),
|
| 511 |
+
"backup_available": action_data.get("rollbackFeasible", True),
|
| 512 |
+
"compliance": "pci-dss" if "financial" in action.lower() else "standard"
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
# 1. Bayesian risk assessment
|
| 516 |
+
risk_assessment = self.risk_engine.calculate_posterior(
|
| 517 |
+
action_text=action,
|
| 518 |
+
context=context,
|
| 519 |
+
evidence_success=len(self.memory.incidents) // 2, # Mock evidence
|
| 520 |
+
evidence_total=len(self.memory.incidents)
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
# 2. Policy evaluation
|
| 524 |
+
policy_result = self.policy_engine.evaluate(
|
| 525 |
+
action=action,
|
| 526 |
+
risk_assessment=risk_assessment,
|
| 527 |
+
confidence=confidence,
|
| 528 |
+
mode="advisory"
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
# 3. MCP check
|
| 532 |
+
mcp_result = await self.mcp_client.evaluate(action, context)
|
| 533 |
+
|
| 534 |
+
# 4. Memory recall
|
| 535 |
+
similar = self.memory.find_similar(
|
| 536 |
+
action=action,
|
| 537 |
+
risk_score=risk_assessment["score"],
|
| 538 |
+
limit=3
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
# 5. Combine gates
|
| 542 |
+
all_gates = []
|
| 543 |
+
|
| 544 |
+
# Add policy gates
|
| 545 |
+
for gate in policy_result["gates"]:
|
| 546 |
+
all_gates.append(gate)
|
| 547 |
+
|
| 548 |
+
# Add MCP gate
|
| 549 |
+
all_gates.append(mcp_result)
|
| 550 |
+
|
| 551 |
+
# Add novel action gate if few similar incidents
|
| 552 |
+
if len(similar) < 2:
|
| 553 |
+
all_gates.append({
|
| 554 |
+
"gate": "novel_action_review",
|
| 555 |
+
"passed": False,
|
| 556 |
+
"reason": "Action pattern rarely seen in historical data",
|
| 557 |
+
"metadata": {"similar_count": len(similar)}
|
| 558 |
+
})
|
| 559 |
+
|
| 560 |
+
# 6. Track enterprise signals
|
| 561 |
+
if len(similar) < 2 and risk_assessment["score"] > 0.7:
|
| 562 |
+
self.memory.track_enterprise_signal(
|
| 563 |
+
"novel_high_risk_action",
|
| 564 |
+
action,
|
| 565 |
+
{"risk_score": risk_assessment["score"], "similar_count": len(similar)}
|
| 566 |
+
)
|
| 567 |
+
elif not policy_result["allowed"] and risk_assessment["score"] > 0.8:
|
| 568 |
+
self.memory.track_enterprise_signal(
|
| 569 |
+
"blocked_critical_action",
|
| 570 |
+
action,
|
| 571 |
+
{"failures": policy_result["failures"]}
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# 7. Store in memory
|
| 575 |
+
self.memory.store({
|
| 576 |
+
"action": action,
|
| 577 |
+
"description": description,
|
| 578 |
+
"risk_score": risk_assessment["score"],
|
| 579 |
+
"risk_level": risk_assessment["level"],
|
| 580 |
+
"confidence": confidence,
|
| 581 |
+
"allowed": policy_result["allowed"],
|
| 582 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 583 |
+
})
|
| 584 |
+
|
| 585 |
+
# Calculate final decision
|
| 586 |
+
all_passed = all(g.get("passed", False) for g in all_gates)
|
| 587 |
+
|
| 588 |
+
processing_time = (datetime.utcnow() - start).total_seconds() * 1000
|
| 589 |
+
|
| 590 |
+
logger.info(f"Evaluation complete: {processing_time:.0f}ms, allowed={all_passed}")
|
| 591 |
+
|
| 592 |
+
return {
|
| 593 |
+
"allowed": all_passed,
|
| 594 |
+
"requiredLevel": policy_result["required_level"],
|
| 595 |
+
"gatesTriggered": all_gates,
|
| 596 |
+
"shouldEscalate": not all_passed,
|
| 597 |
+
"escalationReason": None if all_passed else "Failed mechanical gates",
|
| 598 |
+
"executionLadder": {
|
| 599 |
+
"levels": [
|
| 600 |
+
{"name": "AUTONOMOUS_LOW", "passed": all(g.get("passed") for g in all_gates[:2])},
|
| 601 |
+
{"name": "AUTONOMOUS_HIGH", "passed": all(g.get("passed") for g in all_gates[:3])},
|
| 602 |
+
{"name": "SUPERVISED", "passed": all(g.get("passed") for g in all_gates[:4])},
|
| 603 |
+
{"name": "OPERATOR_REVIEW", "passed": True}
|
| 604 |
+
]
|
| 605 |
+
},
|
| 606 |
+
"riskAssessment": risk_assessment,
|
| 607 |
+
"similarIncidents": similar[:2], # Return top 2 for UI
|
| 608 |
+
"processingTimeMs": processing_time
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
# ============== FASTAPI SETUP ==============
|
| 612 |
+
app = FastAPI(title="ARF OSS Real Engine", version="3.3.9")
|
| 613 |
|
|
|
|
| 614 |
app.add_middleware(
|
| 615 |
CORSMiddleware,
|
| 616 |
+
allow_origins=["*"],
|
| 617 |
allow_credentials=True,
|
| 618 |
allow_methods=["*"],
|
| 619 |
allow_headers=["*"],
|
| 620 |
)
|
| 621 |
|
| 622 |
+
# Initialize ARF once (singleton)
|
| 623 |
+
arf = ARFOrchestrator()
|
| 624 |
|
| 625 |
+
# ============== PYDANTIC MODELS ==============
|
| 626 |
class ActionRequest(BaseModel):
|
| 627 |
+
proposedAction: str
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
confidenceScore: float = Field(..., ge=0.0, le=1.0)
|
| 629 |
riskLevel: str = Field(..., regex="^(LOW|MEDIUM|HIGH|CRITICAL)$")
|
| 630 |
+
description: Optional[str] = None
|
| 631 |
requiresHuman: bool = False
|
| 632 |
rollbackFeasible: bool = True
|
| 633 |
+
user_role: Optional[str] = "devops"
|
| 634 |
|
| 635 |
class ConfigUpdateRequest(BaseModel):
|
| 636 |
confidenceThreshold: Optional[float] = Field(None, ge=0.5, le=1.0)
|
| 637 |
maxAutonomousRisk: Optional[str] = Field(None, regex="^(LOW|MEDIUM|HIGH|CRITICAL)$")
|
|
|
|
| 638 |
|
| 639 |
class GateResult(BaseModel):
|
|
|
|
| 640 |
gate: str
|
| 641 |
reason: str
|
| 642 |
passed: bool
|
| 643 |
threshold: Optional[float] = None
|
| 644 |
actual: Optional[float] = None
|
| 645 |
+
metadata: Optional[Dict] = None
|
| 646 |
|
| 647 |
class EvaluationResponse(BaseModel):
|
|
|
|
| 648 |
allowed: bool
|
| 649 |
requiredLevel: str
|
| 650 |
gatesTriggered: List[GateResult]
|
| 651 |
shouldEscalate: bool
|
| 652 |
escalationReason: Optional[str] = None
|
| 653 |
+
executionLadder: Optional[Dict] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
# ============== API ENDPOINTS ==============
|
|
|
|
| 656 |
@app.get("/api/v1/config")
|
| 657 |
async def get_config():
|
|
|
|
| 658 |
return {
|
| 659 |
+
"confidenceThreshold": arf.policy_engine.config["confidence_threshold"],
|
| 660 |
+
"maxAutonomousRisk": arf.policy_engine.config["max_autonomous_risk"],
|
| 661 |
+
"riskScoreThresholds": arf.policy_engine.config["risk_thresholds"]
|
| 662 |
}
|
| 663 |
|
| 664 |
@app.post("/api/v1/config")
|
| 665 |
async def update_config(config: ConfigUpdateRequest):
|
| 666 |
+
if config.confidenceThreshold:
|
| 667 |
+
arf.policy_engine.update_confidence_threshold(config.confidenceThreshold)
|
| 668 |
+
if config.maxAutonomousRisk:
|
| 669 |
+
arf.policy_engine.update_max_risk(config.maxAutonomousRisk)
|
| 670 |
+
return await get_config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
|
| 672 |
+
@app.post("/api/v1/evaluate", response_model=EvaluationResponse)
|
| 673 |
+
async def evaluate_action(request: ActionRequest):
|
| 674 |
+
"""Real ARF OSS evaluation"""
|
| 675 |
+
result = await arf.evaluate_action(request.dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
|
| 677 |
+
# Convert gates to proper format
|
| 678 |
+
gates = []
|
| 679 |
+
for g in result["gatesTriggered"]:
|
| 680 |
+
gates.append(GateResult(
|
| 681 |
+
gate=g["gate"],
|
| 682 |
+
reason=g["reason"],
|
| 683 |
+
passed=g["passed"],
|
| 684 |
+
threshold=g.get("threshold"),
|
| 685 |
+
actual=g.get("actual"),
|
| 686 |
+
metadata=g.get("metadata")
|
| 687 |
+
))
|
| 688 |
+
|
| 689 |
+
return EvaluationResponse(
|
| 690 |
+
allowed=result["allowed"],
|
| 691 |
+
requiredLevel=result["requiredLevel"],
|
| 692 |
+
gatesTriggered=gates,
|
| 693 |
+
shouldEscalate=result["shouldEscalate"],
|
| 694 |
+
escalationReason=result["escalationReason"],
|
| 695 |
+
executionLadder=result["executionLadder"]
|
| 696 |
+
)
|
| 697 |
|
| 698 |
@app.get("/api/v1/enterprise/signals")
|
| 699 |
async def get_enterprise_signals():
|
| 700 |
+
"""Lead intelligence endpoint"""
|
| 701 |
+
return {
|
| 702 |
+
"signals": arf.memory.get_enterprise_signals(),
|
| 703 |
+
"session_id": arf.session_id,
|
| 704 |
+
"session_duration": (datetime.utcnow() - arf.start_time).total_seconds()
|
| 705 |
+
}
|
| 706 |
|
|
|
|
| 707 |
@app.get("/health")
|
| 708 |
+
async def health():
|
| 709 |
return {
|
| 710 |
"status": "healthy",
|
| 711 |
"arf_version": "3.3.9",
|
| 712 |
"oss_mode": True,
|
| 713 |
+
"memory_entries": len(arf.memory.incidents),
|
| 714 |
+
"enterprise_signals": len(arf.memory.enterprise_signals)
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
# ============== GRADIO LEAD GEN PAGE ==============
|
| 718 |
+
def create_lead_gen_page():
|
| 719 |
+
"""Simple lead generation page"""
|
| 720 |
+
|
| 721 |
+
with gr.Blocks(title="ARF OSS - Real Bayesian Reliability", theme=gr.themes.Soft()) as demo:
|
| 722 |
+
|
| 723 |
+
gr.HTML("""
|
| 724 |
+
<div style="background: linear-gradient(135deg, #0D47A1, #1565C0); padding: 60px 30px;
|
| 725 |
+
border-radius: 15px; text-align: center; color: white;">
|
| 726 |
+
<h1 style="font-size: 3em; margin-bottom: 20px;">🤖 ARF OSS v3.3.9</h1>
|
| 727 |
+
<h2 style="font-size: 1.8em; font-weight: 300; margin-bottom: 30px;">
|
| 728 |
+
Real Bayesian Risk Assessment • Deterministic Policies • RAG Memory
|
| 729 |
+
</h2>
|
| 730 |
+
<div style="display: inline-block; background: rgba(255,255,255,0.2); padding: 10px 20px;
|
| 731 |
+
border-radius: 50px; margin-bottom: 40px;">
|
| 732 |
+
⚡ Running REAL ARF OSS components - No Simulation
|
| 733 |
+
</div>
|
| 734 |
+
</div>
|
| 735 |
+
""")
|
| 736 |
+
|
| 737 |
+
with gr.Row():
|
| 738 |
+
with gr.Column():
|
| 739 |
+
gr.HTML("""
|
| 740 |
+
<div style="padding: 30px; text-align: center;">
|
| 741 |
+
<h3 style="color: #0D47A1; font-size: 2em;">🚀 From Advisory to Autonomous</h3>
|
| 742 |
+
<p style="font-size: 1.2em; color: #666; margin: 20px 0;">
|
| 743 |
+
This demo uses real ARF OSS components for Bayesian risk assessment.<br>
|
| 744 |
+
Enterprise adds mechanical gates, learning loops, and governed execution.
|
| 745 |
+
</p>
|
| 746 |
+
</div>
|
| 747 |
+
""")
|
| 748 |
+
|
| 749 |
+
with gr.Row():
|
| 750 |
+
features = [
|
| 751 |
+
("🧮 Bayesian Inference", "Real posterior probability calculations"),
|
| 752 |
+
("🛡️ Policy Engine", "Deterministic OSS policies"),
|
| 753 |
+
("💾 RAG Memory", "Similar incident recall"),
|
| 754 |
+
("🔌 MCP Client", "Model Context Protocol integration")
|
| 755 |
+
]
|
| 756 |
+
|
| 757 |
+
for title, desc in features:
|
| 758 |
+
with gr.Column():
|
| 759 |
+
gr.HTML(f"""
|
| 760 |
+
<div style="padding: 20px; background: #f8f9fa; border-radius: 10px; height: 100%;">
|
| 761 |
+
<h4 style="color: #0D47A1;">{title}</h4>
|
| 762 |
+
<p style="color: #666;">{desc}</p>
|
| 763 |
+
</div>
|
| 764 |
+
""")
|
| 765 |
+
|
| 766 |
+
gr.HTML("""
|
| 767 |
+
<div style="margin: 40px 0; padding: 50px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 768 |
+
border-radius: 20px; text-align: center; color: white;">
|
| 769 |
+
<h2 style="font-size: 2.5em; margin-bottom: 20px;">🎯 Ready for Autonomous Operations?</h2>
|
| 770 |
+
<p style="font-size: 1.3em; margin-bottom: 30px;">
|
| 771 |
+
See ARF Enterprise with mechanical gates and execution
|
| 772 |
+
</p>
|
| 773 |
+
|
| 774 |
+
<div style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
|
| 775 |
+
<a href="mailto:petter2025us@outlook.com?subject=ARF%20Enterprise%20Demo"
|
| 776 |
+
style="background: white; color: #667eea; padding: 18px 40px; border-radius: 50px;
|
| 777 |
+
text-decoration: none; font-weight: bold; font-size: 1.2em;">
|
| 778 |
+
📧 petter2025us@outlook.com
|
| 779 |
+
</a>
|
| 780 |
+
<a href="#"
|
| 781 |
+
style="background: #FFD700; color: #333; padding: 18px 40px; border-radius: 50px;
|
| 782 |
+
text-decoration: none; font-weight: bold; font-size: 1.2em;"
|
| 783 |
+
onclick="alert('Calendar booking coming soon. Please email for now!')">
|
| 784 |
+
📅 Schedule Demo
|
| 785 |
+
</a>
|
| 786 |
+
</div>
|
| 787 |
+
|
| 788 |
+
<p style="margin-top: 30px; font-size: 0.95em; opacity: 0.9;">
|
| 789 |
+
⚡ Technical deep-dive • Live autonomous execution • Enterprise pricing
|
| 790 |
+
</p>
|
| 791 |
+
</div>
|
| 792 |
+
""")
|
| 793 |
+
|
| 794 |
+
gr.HTML("""
|
| 795 |
+
<div style="text-align: center; padding: 30px; color: #666;">
|
| 796 |
+
<p>📧 <a href="mailto:petter2025us@outlook.com" style="color: #0D47A1;">petter2025us@outlook.com</a> •
|
| 797 |
+
🐙 <a href="https://github.com/petterjuan/agentic-reliability-framework" style="color: #0D47A1;">GitHub</a></p>
|
| 798 |
+
<p style="font-size: 0.9em;">© 2026 ARF - Real OSS, Enterprise Execution</p>
|
| 799 |
+
</div>
|
| 800 |
+
""")
|
| 801 |
+
|
| 802 |
+
return demo
|
| 803 |
+
|
| 804 |
+
# ============== MAIN ENTRY POINT ==============
|
| 805 |
+
demo = create_lead_gen_page()
|
| 806 |
+
|
| 807 |
+
# Mount FastAPI on Gradio
|
| 808 |
+
app = mount_gradio_app(app, demo, path="/")
|
| 809 |
+
|
| 810 |
+
# For Hugging Face Spaces, this must be the only app file
|
| 811 |
+
# The Space will execute this file and look for 'demo' or 'app'
|
| 812 |
+
|
| 813 |
+
# This is the critical part for Hugging Face Spaces
|
| 814 |
+
if __name__ == "__main__":
|
| 815 |
+
import uvicorn
|
| 816 |
+
port = int(os.environ.get('PORT', 7860))
|
| 817 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|