petter2025 commited on
Commit
5a6ca7f
·
verified ·
1 Parent(s): 4240faa

Update hf_demo.py

Browse files
Files changed (1) hide show
  1. hf_demo.py +674 -50
hf_demo.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- ARF OSS v3.3.9 - Enterprise Lead Generation Engine (API Only)
3
- Compatible with Pydantic V2
4
  """
5
 
6
  import os
@@ -12,19 +12,19 @@ import logging
12
  import sqlite3
13
  import requests
14
  import fcntl
15
- from datetime import datetime
16
- from typing import Dict, List, Optional, Any, Tuple
17
  from contextlib import contextmanager
 
18
  from enum import Enum
 
19
 
20
- # FastAPI and Pydantic
21
  from fastapi import FastAPI, HTTPException, Depends, status
22
  from fastapi.middleware.cors import CORSMiddleware
23
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
24
  from pydantic import BaseModel, Field, field_validator
25
  from pydantic_settings import BaseSettings, SettingsConfigDict
26
 
27
- # ============== INFRASTRUCTURE GOVERNANCE MODULE IMPORTS ==============
28
  from infrastructure import (
29
  AzureInfrastructureSimulator,
30
  RegionAllowedPolicy,
@@ -36,7 +36,6 @@ from infrastructure import (
36
  Environment,
37
  RecommendedAction,
38
  )
39
- import yaml
40
 
41
  # ============== SINGLE INSTANCE LOCK (per port) ==============
42
  PORT = int(os.environ.get('PORT', 7860))
@@ -51,37 +50,24 @@ except (IOError, OSError):
51
 
52
  # ============== CONFIGURATION (Pydantic V2) ==============
53
  class Settings(BaseSettings):
54
- """Centralized configuration using Pydantic Settings V2"""
55
-
56
- # Hugging Face settings (aliased to match expected env vars)
57
  hf_space_id: str = Field(default='local', alias='SPACE_ID')
58
  hf_token: str = Field(default='', alias='HF_TOKEN')
59
-
60
- # Persistence - HF persistent storage
61
  data_dir: str = Field(
62
  default='/data' if os.path.exists('/data') else './data',
63
  alias='DATA_DIR'
64
  )
65
-
66
- # Lead generation (kept for reference, but UI removed)
67
  lead_email: str = "petter2025us@outlook.com"
68
  calendly_url: str = "https://calendly.com/petter2025us/arf-demo"
69
-
70
- # Webhook for lead alerts (set in HF secrets)
71
  slack_webhook: str = Field(default='', alias='SLACK_WEBHOOK')
72
  sendgrid_api_key: str = Field(default='', alias='SENDGRID_API_KEY')
73
-
74
- # Security
75
  api_key: str = Field(
76
  default_factory=lambda: str(uuid.uuid4()),
77
  alias='ARF_API_KEY'
78
  )
79
-
80
- # ARF defaults
81
  default_confidence_threshold: float = 0.9
82
  default_max_risk: str = "MEDIUM"
83
-
84
- # Pydantic V2 configuration
85
  model_config = SettingsConfigDict(
86
  populate_by_name=True,
87
  extra='ignore',
@@ -106,7 +92,7 @@ logging.basicConfig(
106
  )
107
  logger = logging.getLogger('arf.oss')
108
 
109
- # ============== ENUMS & TYPES (from original ARF) ==============
110
  class RiskLevel(str, Enum):
111
  LOW = "LOW"
112
  MEDIUM = "MEDIUM"
@@ -126,11 +112,471 @@ class LeadSignal(str, Enum):
126
  CONFIDENCE_LOW = "confidence_low"
127
  REPEATED_FAILURE = "repeated_failure"
128
 
129
- # ============== ORIGINAL ARF COMPONENTS (unchanged) ==============
130
- # ... (BayesianRiskEngine, PolicyEngine, RAGMemory classes as in your original file) ...
131
- # To keep this message concise, I'm omitting the long original classes.
132
- # They remain exactly as you had them. The full file would include them here.
133
- # For brevity, I'll place a placeholder.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  # ============== AUTHENTICATION ==============
136
  security = HTTPBearer()
@@ -143,8 +589,53 @@ async def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(sec
143
  )
144
  return credentials.credentials
145
 
146
- # ============== PYDANTIC MODELS (existing) ==============
147
- # ... (ActionRequest, ConfigUpdateRequest, GateResult, EvaluationResponse, LeadSignalResponse) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  # ============== NEW INFRASTRUCTURE MODELS ==============
150
  class InfrastructureIntentRequest(BaseModel):
@@ -154,9 +645,7 @@ class InfrastructureIntentRequest(BaseModel):
154
  size: Optional[str] = None
155
  environment: str = "PROD"
156
  requester: str
157
- # For deploy intent
158
  config_content: Optional[Dict[str, Any]] = None
159
- # For grant intent
160
  permission: Optional[str] = None
161
  target: Optional[str] = None
162
 
@@ -189,11 +678,13 @@ app.add_middleware(
189
  )
190
 
191
  # Initialize original ARF components
192
- # ... (risk_engine, policy_engine, memory as in original) ...
 
 
193
 
194
- # ============== INFRASTRUCTURE SIMULATOR INSTANCE (cached) ==============
195
- # For simplicity, we use a default policy. In production, you might load from config.
196
- _default_policy = RegionAllowedPolicy(regions={"eastus", "westeurope"}) & CostThresholdPolicy(500.0)
197
  infra_simulator = AzureInfrastructureSimulator(
198
  policy=_default_policy,
199
  pricing_file="pricing.yml" if os.path.exists("pricing.yml") else None
@@ -203,7 +694,12 @@ infra_simulator = AzureInfrastructureSimulator(
203
 
204
  @app.get("/")
205
  async def root():
206
- return {"service": "ARF OSS API", "version": "3.3.9", "status": "operational", "docs": "/docs"}
 
 
 
 
 
207
 
208
  @app.get("/health")
209
  async def health_check():
@@ -211,20 +707,153 @@ async def health_check():
211
  "status": "healthy",
212
  "version": "3.3.9",
213
  "edition": "OSS",
214
- "memory_entries": 0, # simplified
215
  "timestamp": datetime.utcnow().isoformat()
216
  }
217
 
218
- # ... existing ARF endpoints (get_config, update_config, evaluate_action, etc.) ...
 
 
 
 
 
 
 
 
219
 
220
- # ============== NEW INFRASTRUCTURE EVALUATION ENDPOINT ==============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  @app.post("/api/v1/infrastructure/evaluate", dependencies=[Depends(verify_api_key)], response_model=InfrastructureEvaluationResponse)
222
  async def evaluate_infrastructure_intent(request: InfrastructureIntentRequest):
223
- """
224
- Evaluate an infrastructure change intent against policies, cost, and risk.
225
- """
226
  try:
227
- # Map request to appropriate intent type
228
  if request.intent_type == "provision":
229
  if not all([request.resource_type, request.region, request.size]):
230
  raise HTTPException(400, "Missing fields for provision intent")
@@ -238,7 +867,7 @@ async def evaluate_infrastructure_intent(request: InfrastructureIntentRequest):
238
  elif request.intent_type == "deploy":
239
  intent = DeployConfigurationIntent(
240
  service_name=request.resource_type or "unknown",
241
- change_scope="canary", # default; could be made configurable
242
  deployment_target=Environment(request.environment.lower()),
243
  configuration=request.config_content or {},
244
  requester=request.requester
@@ -253,10 +882,8 @@ async def evaluate_infrastructure_intent(request: InfrastructureIntentRequest):
253
  else:
254
  raise HTTPException(400, f"Unknown intent type: {request.intent_type}")
255
 
256
- # Evaluate using the simulator
257
  healing_intent = infra_simulator.evaluate(intent)
258
 
259
- # Transform to response model
260
  return InfrastructureEvaluationResponse(
261
  recommended_action=healing_intent.recommended_action.value,
262
  justification=healing_intent.justification,
@@ -275,9 +902,7 @@ async def evaluate_infrastructure_intent(request: InfrastructureIntentRequest):
275
  # ============== MAIN ENTRY POINT ==============
276
  if __name__ == "__main__":
277
  import uvicorn
278
-
279
  port = int(os.environ.get('PORT', 7860))
280
-
281
  logger.info("="*60)
282
  logger.info("🚀 ARF OSS v3.3.9 (API Only) Starting")
283
  logger.info(f"📊 Data directory: {settings.data_dir}")
@@ -285,7 +910,6 @@ if __name__ == "__main__":
285
  logger.info(f"🔑 API Key: {settings.api_key[:8]}... (set in HF secrets)")
286
  logger.info(f"🌐 Serving API at: http://0.0.0.0:{port}")
287
  logger.info("="*60)
288
-
289
  uvicorn.run(
290
  "hf_demo:app",
291
  host="0.0.0.0",
 
1
  """
2
+ ARF OSS v3.3.9 - Enterprise Reliability Engine (Backend API only)
3
+ With integrated Infrastructure Governance Module
4
  """
5
 
6
  import os
 
12
  import sqlite3
13
  import requests
14
  import fcntl
 
 
15
  from contextlib import contextmanager
16
+ from datetime import datetime
17
  from enum import Enum
18
+ from typing import Dict, List, Optional, Any, Tuple
19
 
20
+ import yaml
21
  from fastapi import FastAPI, HTTPException, Depends, status
22
  from fastapi.middleware.cors import CORSMiddleware
23
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
24
  from pydantic import BaseModel, Field, field_validator
25
  from pydantic_settings import BaseSettings, SettingsConfigDict
26
 
27
+ # ============== INFRASTRUCTURE MODULE IMPORTS ==============
28
  from infrastructure import (
29
  AzureInfrastructureSimulator,
30
  RegionAllowedPolicy,
 
36
  Environment,
37
  RecommendedAction,
38
  )
 
39
 
40
  # ============== SINGLE INSTANCE LOCK (per port) ==============
41
  PORT = int(os.environ.get('PORT', 7860))
 
50
 
51
  # ============== CONFIGURATION (Pydantic V2) ==============
52
  class Settings(BaseSettings):
53
+ """Application settings loaded from environment variables."""
 
 
54
  hf_space_id: str = Field(default='local', alias='SPACE_ID')
55
  hf_token: str = Field(default='', alias='HF_TOKEN')
 
 
56
  data_dir: str = Field(
57
  default='/data' if os.path.exists('/data') else './data',
58
  alias='DATA_DIR'
59
  )
 
 
60
  lead_email: str = "petter2025us@outlook.com"
61
  calendly_url: str = "https://calendly.com/petter2025us/arf-demo"
 
 
62
  slack_webhook: str = Field(default='', alias='SLACK_WEBHOOK')
63
  sendgrid_api_key: str = Field(default='', alias='SENDGRID_API_KEY')
 
 
64
  api_key: str = Field(
65
  default_factory=lambda: str(uuid.uuid4()),
66
  alias='ARF_API_KEY'
67
  )
 
 
68
  default_confidence_threshold: float = 0.9
69
  default_max_risk: str = "MEDIUM"
70
+
 
71
  model_config = SettingsConfigDict(
72
  populate_by_name=True,
73
  extra='ignore',
 
92
  )
93
  logger = logging.getLogger('arf.oss')
94
 
95
+ # ============== ENUMS (original ARF) ==============
96
  class RiskLevel(str, Enum):
97
  LOW = "LOW"
98
  MEDIUM = "MEDIUM"
 
112
  CONFIDENCE_LOW = "confidence_low"
113
  REPEATED_FAILURE = "repeated_failure"
114
 
115
+ # ============== ORIGINAL ARF COMPONENTS ==============
116
+ class BayesianRiskEngine:
117
+ """True Bayesian inference with conjugate priors."""
118
+ def __init__(self):
119
+ self.prior_alpha = 2.0
120
+ self.prior_beta = 5.0
121
+ self.action_priors = {
122
+ 'database': {'alpha': 1.5, 'beta': 8.0},
123
+ 'network': {'alpha': 3.0, 'beta': 4.0},
124
+ 'compute': {'alpha': 4.0, 'beta': 3.0},
125
+ 'security': {'alpha': 2.0, 'beta': 6.0},
126
+ 'default': {'alpha': 2.0, 'beta': 5.0}
127
+ }
128
+ self.evidence_db = f"{settings.data_dir}/evidence.db"
129
+ self._init_db()
130
+
131
+ def _init_db(self):
132
+ try:
133
+ with self._get_db() as conn:
134
+ conn.execute('''
135
+ CREATE TABLE IF NOT EXISTS evidence (
136
+ id TEXT PRIMARY KEY,
137
+ action_type TEXT,
138
+ action_hash TEXT,
139
+ success INTEGER,
140
+ total INTEGER,
141
+ timestamp TEXT,
142
+ metadata TEXT
143
+ )
144
+ ''')
145
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_action_hash ON evidence(action_hash)')
146
+ except sqlite3.Error as e:
147
+ logger.error(f"Failed to initialize evidence database: {e}")
148
+ raise RuntimeError("Could not initialize evidence storage") from e
149
+
150
+ @contextmanager
151
+ def _get_db(self):
152
+ conn = None
153
+ try:
154
+ conn = sqlite3.connect(self.evidence_db)
155
+ yield conn
156
+ except sqlite3.Error as e:
157
+ logger.error(f"Database error: {e}")
158
+ raise
159
+ finally:
160
+ if conn:
161
+ conn.close()
162
+
163
+ def classify_action(self, action_text: str) -> str:
164
+ action_lower = action_text.lower()
165
+ if any(word in action_lower for word in ['database', 'db', 'sql', 'table', 'drop', 'delete']):
166
+ return 'database'
167
+ elif any(word in action_lower for word in ['network', 'firewall', 'load balancer']):
168
+ return 'network'
169
+ elif any(word in action_lower for word in ['pod', 'container', 'deploy', 'scale']):
170
+ return 'compute'
171
+ elif any(word in action_lower for word in ['security', 'cert', 'key', 'access']):
172
+ return 'security'
173
+ else:
174
+ return 'default'
175
+
176
+ def get_prior(self, action_type: str) -> Tuple[float, float]:
177
+ prior = self.action_priors.get(action_type, self.action_priors['default'])
178
+ return prior['alpha'], prior['beta']
179
+
180
+ def get_evidence(self, action_hash: str) -> Tuple[int, int]:
181
+ try:
182
+ with self._get_db() as conn:
183
+ cursor = conn.execute(
184
+ 'SELECT SUM(success), SUM(total) FROM evidence WHERE action_hash = ?',
185
+ (action_hash[:50],)
186
+ )
187
+ row = cursor.fetchone()
188
+ return (row[0] or 0, row[1] or 0) if row else (0, 0)
189
+ except sqlite3.Error as e:
190
+ logger.error(f"Failed to retrieve evidence: {e}")
191
+ return (0, 0)
192
+
193
+ def calculate_posterior(self, action_text: str, context: Dict[str, Any]) -> Dict[str, Any]:
194
+ action_type = self.classify_action(action_text)
195
+ alpha0, beta0 = self.get_prior(action_type)
196
+ action_hash = hashlib.sha256(action_text.encode()).hexdigest()
197
+ successes, trials = self.get_evidence(action_hash)
198
+ alpha_n = alpha0 + successes
199
+ beta_n = beta0 + (trials - successes)
200
+ posterior_mean = alpha_n / (alpha_n + beta_n)
201
+ context_multiplier = self._context_likelihood(context)
202
+ risk_score = posterior_mean * context_multiplier
203
+ risk_score = min(0.99, max(0.01, risk_score))
204
+
205
+ variance = (alpha_n * beta_n) / ((alpha_n + beta_n)**2 * (alpha_n + beta_n + 1))
206
+ std_dev = variance ** 0.5
207
+ ci_lower = max(0.01, posterior_mean - 1.96 * std_dev)
208
+ ci_upper = min(0.99, posterior_mean + 1.96 * std_dev)
209
+
210
+ if risk_score > 0.8:
211
+ risk_level = RiskLevel.CRITICAL
212
+ elif risk_score > 0.6:
213
+ risk_level = RiskLevel.HIGH
214
+ elif risk_score > 0.4:
215
+ risk_level = RiskLevel.MEDIUM
216
+ else:
217
+ risk_level = RiskLevel.LOW
218
+
219
+ return {
220
+ "score": risk_score,
221
+ "level": risk_level,
222
+ "credible_interval": [ci_lower, ci_upper],
223
+ "posterior_parameters": {"alpha": alpha_n, "beta": beta_n},
224
+ "prior_used": {"alpha": alpha0, "beta": beta0, "type": action_type},
225
+ "evidence_used": {"successes": successes, "trials": trials},
226
+ "context_multiplier": context_multiplier,
227
+ "calculation": f"""
228
+ Posterior = Beta(α={alpha_n:.1f}, β={beta_n:.1f})
229
+ Mean = {alpha_n:.1f} / ({alpha_n:.1f} + {beta_n:.1f}) = {posterior_mean:.3f}
230
+ × Context multiplier {context_multiplier:.2f} = {risk_score:.3f}
231
+ """
232
+ }
233
+
234
+ def _context_likelihood(self, context: Dict) -> float:
235
+ multiplier = 1.0
236
+ if context.get('environment') == 'production':
237
+ multiplier *= 1.5
238
+ elif context.get('environment') == 'staging':
239
+ multiplier *= 0.8
240
+ hour = datetime.now().hour
241
+ if hour < 6 or hour > 22:
242
+ multiplier *= 1.3
243
+ if context.get('user_role') == 'junior':
244
+ multiplier *= 1.4
245
+ elif context.get('user_role') == 'senior':
246
+ multiplier *= 0.9
247
+ if not context.get('backup_available', True):
248
+ multiplier *= 1.6
249
+ return multiplier
250
+
251
+ def record_outcome(self, action_text: str, success: bool):
252
+ action_hash = hashlib.sha256(action_text.encode()).hexdigest()
253
+ action_type = self.classify_action(action_text)
254
+ try:
255
+ with self._get_db() as conn:
256
+ conn.execute('''
257
+ INSERT INTO evidence (id, action_type, action_hash, success, total, timestamp)
258
+ VALUES (?, ?, ?, ?, ?, ?)
259
+ ''', (
260
+ str(uuid.uuid4()),
261
+ action_type,
262
+ action_hash[:50],
263
+ 1 if success else 0,
264
+ 1,
265
+ datetime.utcnow().isoformat()
266
+ ))
267
+ conn.commit()
268
+ logger.info(f"Recorded outcome for {action_type}: success={success}")
269
+ except sqlite3.Error as e:
270
+ logger.error(f"Failed to record outcome: {e}")
271
+
272
+ class PolicyEngine:
273
+ """Deterministic OSS policies – advisory only."""
274
+ def __init__(self):
275
+ self.config = {
276
+ "confidence_threshold": settings.default_confidence_threshold,
277
+ "max_autonomous_risk": settings.default_max_risk,
278
+ "risk_thresholds": {
279
+ RiskLevel.LOW: 0.7,
280
+ RiskLevel.MEDIUM: 0.5,
281
+ RiskLevel.HIGH: 0.3,
282
+ RiskLevel.CRITICAL: 0.1
283
+ },
284
+ "destructive_patterns": [
285
+ r'\bdrop\s+database\b',
286
+ r'\bdelete\s+from\b',
287
+ r'\btruncate\b',
288
+ r'\balter\s+table\b',
289
+ r'\bdrop\s+table\b',
290
+ r'\bshutdown\b',
291
+ r'\bterminate\b',
292
+ r'\brm\s+-rf\b'
293
+ ],
294
+ "require_human": [RiskLevel.CRITICAL, RiskLevel.HIGH],
295
+ "require_rollback": True
296
+ }
297
+
298
+ def evaluate(self, action: str, risk: Dict[str, Any], confidence: float) -> Dict[str, Any]:
299
+ import re
300
+ gates = []
301
+
302
+ confidence_passed = confidence >= self.config["confidence_threshold"]
303
+ gates.append({
304
+ "gate": "confidence_threshold",
305
+ "passed": confidence_passed,
306
+ "threshold": self.config["confidence_threshold"],
307
+ "actual": confidence,
308
+ "reason": f"Confidence {confidence:.2f} {'≥' if confidence_passed else '<'} threshold {self.config['confidence_threshold']}",
309
+ "type": "numerical"
310
+ })
311
+
312
+ risk_levels = list(RiskLevel)
313
+ max_idx = risk_levels.index(RiskLevel(self.config["max_autonomous_risk"]))
314
+ action_idx = risk_levels.index(risk["level"])
315
+ risk_passed = action_idx <= max_idx
316
+ gates.append({
317
+ "gate": "risk_assessment",
318
+ "passed": risk_passed,
319
+ "max_allowed": self.config["max_autonomous_risk"],
320
+ "actual": risk["level"].value,
321
+ "reason": f"Risk level {risk['level'].value} {'≤' if risk_passed else '>'} max autonomous {self.config['max_autonomous_risk']}",
322
+ "type": "categorical",
323
+ "metadata": {"risk_score": risk["score"], "credible_interval": risk["credible_interval"]}
324
+ })
325
+
326
+ is_destructive = any(re.search(pattern, action.lower()) for pattern in self.config["destructive_patterns"])
327
+ gates.append({
328
+ "gate": "destructive_check",
329
+ "passed": not is_destructive,
330
+ "is_destructive": is_destructive,
331
+ "reason": "Non-destructive operation" if not is_destructive else "Destructive operation detected",
332
+ "type": "boolean",
333
+ "metadata": {"requires_rollback": is_destructive}
334
+ })
335
+
336
+ requires_human = risk["level"] in self.config["require_human"]
337
+ gates.append({
338
+ "gate": "human_review",
339
+ "passed": not requires_human,
340
+ "requires_human": requires_human,
341
+ "reason": "Human review not required" if not requires_human else f"Human review required for {risk['level'].value} risk",
342
+ "type": "boolean"
343
+ })
344
+
345
+ gates.append({
346
+ "gate": "license_check",
347
+ "passed": True,
348
+ "edition": "OSS",
349
+ "reason": "OSS edition - advisory only",
350
+ "type": "license"
351
+ })
352
+
353
+ all_passed = all(g["passed"] for g in gates)
354
+
355
+ if not all_passed:
356
+ required_level = ExecutionLevel.OPERATOR_REVIEW
357
+ elif risk["level"] == RiskLevel.LOW:
358
+ required_level = ExecutionLevel.AUTONOMOUS_LOW
359
+ elif risk["level"] == RiskLevel.MEDIUM:
360
+ required_level = ExecutionLevel.AUTONOMOUS_HIGH
361
+ else:
362
+ required_level = ExecutionLevel.SUPERVISED
363
+
364
+ return {
365
+ "allowed": all_passed,
366
+ "required_level": required_level.value,
367
+ "gates": gates,
368
+ "advisory_only": True,
369
+ "oss_disclaimer": "OSS edition provides advisory only. Enterprise adds execution."
370
+ }
371
+
372
+ def update_config(self, key: str, value: Any):
373
+ if key in self.config:
374
+ self.config[key] = value
375
+ logger.info(f"Policy updated: {key} = {value}")
376
+ return True
377
+ return False
378
+
379
+ class RAGMemory:
380
+ """Persistent RAG memory with SQLite and simple embeddings."""
381
+ def __init__(self):
382
+ self.db_path = f"{settings.data_dir}/memory.db"
383
+ self._init_db()
384
+ self.embedding_cache = {}
385
+
386
+ def _init_db(self):
387
+ try:
388
+ with self._get_db() as conn:
389
+ conn.execute('''
390
+ CREATE TABLE IF NOT EXISTS incidents (
391
+ id TEXT PRIMARY KEY,
392
+ action TEXT,
393
+ action_hash TEXT,
394
+ risk_score REAL,
395
+ risk_level TEXT,
396
+ confidence REAL,
397
+ allowed BOOLEAN,
398
+ gates TEXT,
399
+ timestamp TEXT,
400
+ embedding TEXT
401
+ )
402
+ ''')
403
+ conn.execute('''
404
+ CREATE TABLE IF NOT EXISTS signals (
405
+ id TEXT PRIMARY KEY,
406
+ signal_type TEXT,
407
+ action TEXT,
408
+ risk_score REAL,
409
+ metadata TEXT,
410
+ timestamp TEXT,
411
+ contacted BOOLEAN DEFAULT 0
412
+ )
413
+ ''')
414
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_action_hash ON incidents(action_hash)')
415
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_signal_type ON signals(signal_type)')
416
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_signal_contacted ON signals(contacted)')
417
+ except sqlite3.Error as e:
418
+ logger.error(f"Failed to initialize memory database: {e}")
419
+ raise RuntimeError("Could not initialize memory storage") from e
420
+
421
+ @contextmanager
422
+ def _get_db(self):
423
+ conn = None
424
+ try:
425
+ conn = sqlite3.connect(self.db_path)
426
+ conn.row_factory = sqlite3.Row
427
+ yield conn
428
+ except sqlite3.Error as e:
429
+ logger.error(f"Database error in memory: {e}")
430
+ raise
431
+ finally:
432
+ if conn:
433
+ conn.close()
434
+
435
+ def _simple_embedding(self, text: str) -> List[float]:
436
+ if text in self.embedding_cache:
437
+ return self.embedding_cache[text]
438
+ words = text.lower().split()
439
+ trigrams = set()
440
+ for word in words:
441
+ for i in range(len(word) - 2):
442
+ trigrams.add(word[i:i+3])
443
+ vector = [hash(t) % 1000 / 1000.0 for t in sorted(trigrams)[:100]]
444
+ while len(vector) < 100:
445
+ vector.append(0.0)
446
+ vector = vector[:100]
447
+ self.embedding_cache[text] = vector
448
+ return vector
449
+
450
+ def store_incident(self, action: str, risk_score: float, risk_level: RiskLevel,
451
+ confidence: float, allowed: bool, gates: List[Dict]):
452
+ action_hash = hashlib.sha256(action.encode()).hexdigest()[:50]
453
+ embedding = json.dumps(self._simple_embedding(action))
454
+ try:
455
+ with self._get_db() as conn:
456
+ conn.execute('''
457
+ INSERT INTO incidents
458
+ (id, action, action_hash, risk_score, risk_level, confidence, allowed, gates, timestamp, embedding)
459
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
460
+ ''', (
461
+ str(uuid.uuid4()),
462
+ action[:500],
463
+ action_hash,
464
+ risk_score,
465
+ risk_level.value,
466
+ confidence,
467
+ 1 if allowed else 0,
468
+ json.dumps(gates),
469
+ datetime.utcnow().isoformat(),
470
+ embedding
471
+ ))
472
+ conn.commit()
473
+ except sqlite3.Error as e:
474
+ logger.error(f"Failed to store incident: {e}")
475
+
476
+ def find_similar(self, action: str, limit: int = 5) -> List[Dict]:
477
+ query_embedding = self._simple_embedding(action)
478
+ try:
479
+ with self._get_db() as conn:
480
+ cursor = conn.execute('SELECT * FROM incidents ORDER BY timestamp DESC LIMIT 100')
481
+ incidents = []
482
+ for row in cursor.fetchall():
483
+ stored_embedding = json.loads(row['embedding'])
484
+ dot = sum(q * s for q, s in zip(query_embedding, stored_embedding))
485
+ norm_q = sum(q*q for q in query_embedding) ** 0.5
486
+ norm_s = sum(s*s for s in stored_embedding) ** 0.5
487
+ similarity = dot / (norm_q * norm_s) if (norm_q > 0 and norm_s > 0) else 0
488
+ incidents.append({
489
+ 'id': row['id'],
490
+ 'action': row['action'],
491
+ 'risk_score': row['risk_score'],
492
+ 'risk_level': row['risk_level'],
493
+ 'confidence': row['confidence'],
494
+ 'allowed': bool(row['allowed']),
495
+ 'timestamp': row['timestamp'],
496
+ 'similarity': similarity
497
+ })
498
+ incidents.sort(key=lambda x: x['similarity'], reverse=True)
499
+ return incidents[:limit]
500
+ except sqlite3.Error as e:
501
+ logger.error(f"Failed to find similar incidents: {e}")
502
+ return []
503
+
504
+ def track_enterprise_signal(self, signal_type: LeadSignal, action: str,
505
+ risk_score: float, metadata: Dict = None):
506
+ signal = {
507
+ 'id': str(uuid.uuid4()),
508
+ 'signal_type': signal_type.value,
509
+ 'action': action[:200],
510
+ 'risk_score': risk_score,
511
+ 'metadata': json.dumps(metadata or {}),
512
+ 'timestamp': datetime.utcnow().isoformat(),
513
+ 'contacted': 0
514
+ }
515
+ try:
516
+ with self._get_db() as conn:
517
+ conn.execute('''
518
+ INSERT INTO signals
519
+ (id, signal_type, action, risk_score, metadata, timestamp, contacted)
520
+ VALUES (?, ?, ?, ?, ?, ?, ?)
521
+ ''', (
522
+ signal['id'],
523
+ signal['signal_type'],
524
+ signal['action'],
525
+ signal['risk_score'],
526
+ signal['metadata'],
527
+ signal['timestamp'],
528
+ signal['contacted']
529
+ ))
530
+ conn.commit()
531
+ except sqlite3.Error as e:
532
+ logger.error(f"Failed to track signal: {e}")
533
+ return None
534
+
535
+ logger.info(f"🔔 Enterprise signal: {signal_type.value} - {action[:50]}...")
536
+ if signal_type in [LeadSignal.HIGH_RISK_BLOCKED, LeadSignal.NOVEL_ACTION]:
537
+ self._notify_sales_team(signal)
538
+ return signal
539
+
540
+ def _notify_sales_team(self, signal: Dict):
541
+ if settings.slack_webhook:
542
+ try:
543
+ requests.post(settings.slack_webhook, json={
544
+ "text": f"🚨 *Enterprise Lead Signal*\n"
545
+ f"Type: {signal['signal_type']}\n"
546
+ f"Action: {signal['action']}\n"
547
+ f"Risk Score: {signal['risk_score']:.2f}\n"
548
+ f"Time: {signal['timestamp']}\n"
549
+ f"Contact: {settings.lead_email}"
550
+ }, timeout=5)
551
+ except requests.RequestException as e:
552
+ logger.error(f"Slack notification failed: {e}")
553
+
554
+ def get_uncontacted_signals(self) -> List[Dict]:
555
+ try:
556
+ with self._get_db() as conn:
557
+ cursor = conn.execute('SELECT * FROM signals WHERE contacted = 0 ORDER BY timestamp DESC')
558
+ signals = []
559
+ for row in cursor.fetchall():
560
+ signals.append({
561
+ 'id': row['id'],
562
+ 'signal_type': row['signal_type'],
563
+ 'action': row['action'],
564
+ 'risk_score': row['risk_score'],
565
+ 'metadata': json.loads(row['metadata']),
566
+ 'timestamp': row['timestamp']
567
+ })
568
+ return signals
569
+ except sqlite3.Error as e:
570
+ logger.error(f"Failed to get uncontacted signals: {e}")
571
+ return []
572
+
573
+ def mark_contacted(self, signal_id: str):
574
+ try:
575
+ with self._get_db() as conn:
576
+ conn.execute('UPDATE signals SET contacted = 1 WHERE id = ?', (signal_id,))
577
+ conn.commit()
578
+ except sqlite3.Error as e:
579
+ logger.error(f"Failed to mark signal as contacted: {e}")
580
 
581
  # ============== AUTHENTICATION ==============
582
  security = HTTPBearer()
 
589
  )
590
  return credentials.credentials
591
 
592
+ # ============== PYDANTIC SCHEMAS (original) ==============
593
+ class ActionRequest(BaseModel):
594
+ proposedAction: str = Field(..., min_length=1, max_length=1000)
595
+ confidenceScore: float = Field(..., ge=0.0, le=1.0)
596
+ riskLevel: RiskLevel
597
+ description: Optional[str] = None
598
+ requiresHuman: bool = False
599
+ rollbackFeasible: bool = True
600
+ user_role: str = "devops"
601
+ session_id: Optional[str] = None
602
+
603
+ @field_validator('proposedAction')
604
+ @classmethod
605
+ def validate_action(cls, v: str) -> str:
606
+ if len(v.strip()) == 0:
607
+ raise ValueError('Action cannot be empty')
608
+ return v
609
+
610
+ class ConfigUpdateRequest(BaseModel):
611
+ confidenceThreshold: Optional[float] = Field(None, ge=0.5, le=1.0)
612
+ maxAutonomousRisk: Optional[RiskLevel] = None
613
+
614
+ class GateResult(BaseModel):
615
+ gate: str
616
+ reason: str
617
+ passed: bool
618
+ threshold: Optional[float] = None
619
+ actual: Optional[float] = None
620
+ type: str = "boolean"
621
+ metadata: Optional[Dict] = None
622
+
623
+ class EvaluationResponse(BaseModel):
624
+ allowed: bool
625
+ requiredLevel: str
626
+ gatesTriggered: List[GateResult]
627
+ shouldEscalate: bool
628
+ escalationReason: Optional[str] = None
629
+ executionLadder: Optional[Dict] = None
630
+ oss_disclaimer: str = "OSS edition provides advisory only. Enterprise adds mechanical gates and execution."
631
+
632
+ class LeadSignalResponse(BaseModel):
633
+ id: str
634
+ signal_type: str
635
+ action: str
636
+ risk_score: float
637
+ timestamp: str
638
+ metadata: Dict
639
 
640
  # ============== NEW INFRASTRUCTURE MODELS ==============
641
  class InfrastructureIntentRequest(BaseModel):
 
645
  size: Optional[str] = None
646
  environment: str = "PROD"
647
  requester: str
 
648
  config_content: Optional[Dict[str, Any]] = None
 
649
  permission: Optional[str] = None
650
  target: Optional[str] = None
651
 
 
678
  )
679
 
680
  # Initialize original ARF components
681
+ risk_engine = BayesianRiskEngine()
682
+ policy_engine = PolicyEngine()
683
+ memory = RAGMemory()
684
 
685
+ # ============== INFRASTRUCTURE SIMULATOR INSTANCE ==============
686
+ # Corrected: RegionAllowedPolicy expects 'allowed_regions', not 'regions'
687
+ _default_policy = RegionAllowedPolicy(allowed_regions={"eastus", "westeurope"}) & CostThresholdPolicy(500.0)
688
  infra_simulator = AzureInfrastructureSimulator(
689
  policy=_default_policy,
690
  pricing_file="pricing.yml" if os.path.exists("pricing.yml") else None
 
694
 
695
  @app.get("/")
696
  async def root():
697
+ return {
698
+ "service": "ARF OSS API",
699
+ "version": "3.3.9",
700
+ "status": "operational",
701
+ "docs": "/docs"
702
+ }
703
 
704
  @app.get("/health")
705
  async def health_check():
 
707
  "status": "healthy",
708
  "version": "3.3.9",
709
  "edition": "OSS",
710
+ "memory_entries": len(memory.get_uncontacted_signals()),
711
  "timestamp": datetime.utcnow().isoformat()
712
  }
713
 
714
+ @app.get("/api/v1/config", dependencies=[Depends(verify_api_key)])
715
+ async def get_config():
716
+ return {
717
+ "confidenceThreshold": policy_engine.config["confidence_threshold"],
718
+ "maxAutonomousRisk": policy_engine.config["max_autonomous_risk"],
719
+ "riskScoreThresholds": policy_engine.config["risk_thresholds"],
720
+ "version": "3.3.9",
721
+ "edition": "OSS"
722
+ }
723
 
724
+ @app.post("/api/v1/config", dependencies=[Depends(verify_api_key)])
725
+ async def update_config(config: ConfigUpdateRequest):
726
+ if config.confidenceThreshold:
727
+ policy_engine.update_config("confidence_threshold", config.confidenceThreshold)
728
+ if config.maxAutonomousRisk:
729
+ policy_engine.update_config("max_autonomous_risk", config.maxAutonomousRisk.value)
730
+ return await get_config()
731
+
732
+ @app.post("/api/v1/evaluate", dependencies=[Depends(verify_api_key)], response_model=EvaluationResponse)
733
+ async def evaluate_action(request: ActionRequest):
734
+ try:
735
+ context = {
736
+ "environment": "production",
737
+ "user_role": request.user_role,
738
+ "backup_available": request.rollbackFeasible,
739
+ "requires_human": request.requiresHuman
740
+ }
741
+ risk = risk_engine.calculate_posterior(
742
+ action_text=request.proposedAction,
743
+ context=context
744
+ )
745
+ policy = policy_engine.evaluate(
746
+ action=request.proposedAction,
747
+ risk=risk,
748
+ confidence=request.confidenceScore
749
+ )
750
+ similar = memory.find_similar(request.proposedAction, limit=3)
751
+
752
+ if not policy["allowed"] and risk["score"] > 0.7:
753
+ memory.track_enterprise_signal(
754
+ signal_type=LeadSignal.HIGH_RISK_BLOCKED,
755
+ action=request.proposedAction,
756
+ risk_score=risk["score"],
757
+ metadata={
758
+ "confidence": request.confidenceScore,
759
+ "risk_level": risk["level"].value,
760
+ "failed_gates": [g["gate"] for g in policy["gates"] if not g["passed"]]
761
+ }
762
+ )
763
+ if len(similar) < 2 and risk["score"] > 0.6:
764
+ memory.track_enterprise_signal(
765
+ signal_type=LeadSignal.NOVEL_ACTION,
766
+ action=request.proposedAction,
767
+ risk_score=risk["score"],
768
+ metadata={"similar_count": len(similar)}
769
+ )
770
+ memory.store_incident(
771
+ action=request.proposedAction,
772
+ risk_score=risk["score"],
773
+ risk_level=risk["level"],
774
+ confidence=request.confidenceScore,
775
+ allowed=policy["allowed"],
776
+ gates=policy["gates"]
777
+ )
778
+ gates = []
779
+ for g in policy["gates"]:
780
+ gates.append(GateResult(
781
+ gate=g["gate"],
782
+ reason=g["reason"],
783
+ passed=g["passed"],
784
+ threshold=g.get("threshold"),
785
+ actual=g.get("actual"),
786
+ type=g.get("type", "boolean"),
787
+ metadata=g.get("metadata")
788
+ ))
789
+ execution_ladder = {
790
+ "levels": [
791
+ {"name": "AUTONOMOUS_LOW", "required": gates[0].passed and gates[1].passed},
792
+ {"name": "AUTONOMOUS_HIGH", "required": all(g.passed for g in gates[:3])},
793
+ {"name": "SUPERVISED", "required": all(g.passed for g in gates[:4])},
794
+ {"name": "OPERATOR_REVIEW", "required": True}
795
+ ],
796
+ "current": policy["required_level"]
797
+ }
798
+ return EvaluationResponse(
799
+ allowed=policy["allowed"],
800
+ requiredLevel=policy["required_level"],
801
+ gatesTriggered=gates,
802
+ shouldEscalate=not policy["allowed"],
803
+ escalationReason=None if policy["allowed"] else "Failed mechanical gates",
804
+ executionLadder=execution_ladder
805
+ )
806
+ except Exception as e:
807
+ logger.error(f"Evaluation failed: {e}", exc_info=True)
808
+ raise HTTPException(status_code=500, detail="Internal server error during evaluation")
809
+
810
+ @app.get("/api/v1/enterprise/signals", dependencies=[Depends(verify_api_key)])
811
+ async def get_enterprise_signals(contacted: bool = False):
812
+ try:
813
+ if contacted:
814
+ signals = memory.get_uncontacted_signals()
815
+ else:
816
+ with memory._get_db() as conn:
817
+ cursor = conn.execute('''
818
+ SELECT * FROM signals
819
+ WHERE datetime(timestamp) > datetime('now', '-30 days')
820
+ ORDER BY timestamp DESC
821
+ ''')
822
+ signals = []
823
+ for row in cursor.fetchall():
824
+ signals.append({
825
+ 'id': row['id'],
826
+ 'signal_type': row['signal_type'],
827
+ 'action': row['action'],
828
+ 'risk_score': row['risk_score'],
829
+ 'metadata': json.loads(row['metadata']),
830
+ 'timestamp': row['timestamp'],
831
+ 'contacted': bool(row['contacted'])
832
+ })
833
+ return {"signals": signals, "count": len(signals)}
834
+ except Exception as e:
835
+ logger.error(f"Failed to retrieve signals: {e}")
836
+ raise HTTPException(status_code=500, detail="Could not retrieve signals")
837
+
838
+ @app.post("/api/v1/enterprise/signals/{signal_id}/contact", dependencies=[Depends(verify_api_key)])
839
+ async def mark_signal_contacted(signal_id: str):
840
+ memory.mark_contacted(signal_id)
841
+ return {"status": "success", "message": "Signal marked as contacted"}
842
+
843
+ @app.get("/api/v1/memory/similar", dependencies=[Depends(verify_api_key)])
844
+ async def get_similar_actions(action: str, limit: int = 5):
845
+ similar = memory.find_similar(action, limit=limit)
846
+ return {"similar": similar, "count": len(similar)}
847
+
848
+ @app.post("/api/v1/feedback", dependencies=[Depends(verify_api_key)])
849
+ async def record_outcome(action: str, success: bool):
850
+ risk_engine.record_outcome(action, success)
851
+ return {"status": "success", "message": "Outcome recorded"}
852
+
853
+ # ============== NEW INFRASTRUCTURE ENDPOINT ==============
854
  @app.post("/api/v1/infrastructure/evaluate", dependencies=[Depends(verify_api_key)], response_model=InfrastructureEvaluationResponse)
855
  async def evaluate_infrastructure_intent(request: InfrastructureIntentRequest):
 
 
 
856
  try:
 
857
  if request.intent_type == "provision":
858
  if not all([request.resource_type, request.region, request.size]):
859
  raise HTTPException(400, "Missing fields for provision intent")
 
867
  elif request.intent_type == "deploy":
868
  intent = DeployConfigurationIntent(
869
  service_name=request.resource_type or "unknown",
870
+ change_scope="canary",
871
  deployment_target=Environment(request.environment.lower()),
872
  configuration=request.config_content or {},
873
  requester=request.requester
 
882
  else:
883
  raise HTTPException(400, f"Unknown intent type: {request.intent_type}")
884
 
 
885
  healing_intent = infra_simulator.evaluate(intent)
886
 
 
887
  return InfrastructureEvaluationResponse(
888
  recommended_action=healing_intent.recommended_action.value,
889
  justification=healing_intent.justification,
 
902
  # ============== MAIN ENTRY POINT ==============
903
  if __name__ == "__main__":
904
  import uvicorn
 
905
  port = int(os.environ.get('PORT', 7860))
 
906
  logger.info("="*60)
907
  logger.info("🚀 ARF OSS v3.3.9 (API Only) Starting")
908
  logger.info(f"📊 Data directory: {settings.data_dir}")
 
910
  logger.info(f"🔑 API Key: {settings.api_key[:8]}... (set in HF secrets)")
911
  logger.info(f"🌐 Serving API at: http://0.0.0.0:{port}")
912
  logger.info("="*60)
 
913
  uvicorn.run(
914
  "hf_demo:app",
915
  host="0.0.0.0",