petter2025 commited on
Commit
7cfde2b
Β·
verified Β·
1 Parent(s): a0bb7d7

Update hf_demo.py

Browse files
Files changed (1) hide show
  1. hf_demo.py +311 -325
hf_demo.py CHANGED
@@ -7,6 +7,8 @@ import os
7
  # πŸ”₯ CRITICAL: Force Gradio to use port 7860 for Hugging Face Spaces
8
  os.environ['GRADIO_SERVER_PORT'] = '7860'
9
  os.environ['GRADIO_SERVER_NAME'] = '0.0.0.0'
 
 
10
 
11
  import json
12
  import uuid
@@ -23,38 +25,50 @@ from dataclasses import dataclass, asdict
23
  from enum import Enum
24
 
25
  import gradio as gr
26
- from fastapi import FastAPI, HTTPException, Depends
 
 
 
27
  from fastapi.middleware.cors import CORSMiddleware
28
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
29
- from pydantic import BaseModel, Field, field_validator # Changed from validator
 
30
  from gradio import mount_gradio_app
31
 
32
- # ============== CONFIGURATION ==============
33
- class Settings:
34
- """Centralized configuration - easy to modify"""
35
 
36
  # Hugging Face settings
37
- HF_SPACE_ID = os.environ.get('SPACE_ID', 'local')
38
- HF_TOKEN = os.environ.get('HF_TOKEN', '')
39
 
40
  # Persistence - HF persistent storage
41
- DATA_DIR = '/data' if os.path.exists('/data') else './data'
42
- os.makedirs(DATA_DIR, exist_ok=True)
43
 
44
  # Lead generation
45
- LEAD_EMAIL = "petter2025us@outlook.com"
46
- CALENDLY_URL = "https://calendly.com/petter2025us/arf-demo"
47
 
48
  # Webhook for lead alerts (set in HF secrets)
49
- SLACK_WEBHOOK = os.environ.get('SLACK_WEBHOOK', '')
50
- SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY', '')
51
 
52
  # Security
53
- API_KEY = os.environ.get('ARF_API_KEY', str(uuid.uuid4()))
54
 
55
  # ARF defaults
56
- DEFAULT_CONFIDENCE_THRESHOLD = 0.9
57
- DEFAULT_MAX_RISK = "MEDIUM"
 
 
 
 
 
 
 
 
 
58
 
59
  settings = Settings()
60
 
@@ -98,54 +112,58 @@ class BayesianRiskEngine:
98
 
99
  def __init__(self):
100
  # Beta-Binomial conjugate prior
101
- # Prior represents belief about risk before seeing evidence
102
- self.prior_alpha = 2.0 # Pseudocounts for "safe" outcomes
103
- self.prior_beta = 5.0 # Pseudocounts for "risky" outcomes
104
 
105
- # Action type priors (learned from industry data)
106
  self.action_priors = {
107
- 'database': {'alpha': 1.5, 'beta': 8.0}, # DB ops are risky
108
- 'network': {'alpha': 3.0, 'beta': 4.0}, # Network ops medium risk
109
- 'compute': {'alpha': 4.0, 'beta': 3.0}, # Compute ops safer
110
- 'security': {'alpha': 2.0, 'beta': 6.0}, # Security ops risky
111
  'default': {'alpha': 2.0, 'beta': 5.0}
112
  }
113
 
114
- # Load historical evidence from persistent storage
115
  self.evidence_db = f"{settings.DATA_DIR}/evidence.db"
116
  self._init_db()
117
 
118
  def _init_db(self):
119
  """Initialize SQLite DB for evidence storage"""
120
- with self._get_db() as conn:
121
- conn.execute('''
122
- CREATE TABLE IF NOT EXISTS evidence (
123
- id TEXT PRIMARY KEY,
124
- action_type TEXT,
125
- action_hash TEXT,
126
- success INTEGER,
127
- total INTEGER,
128
- timestamp TEXT,
129
- metadata TEXT
130
- )
131
- ''')
132
- conn.execute('''
133
- CREATE INDEX IF NOT EXISTS idx_action_hash
134
- ON evidence(action_hash)
135
- ''')
 
 
 
 
136
 
137
  @contextmanager
138
  def _get_db(self):
139
- conn = sqlite3.connect(self.evidence_db)
140
  try:
 
141
  yield conn
 
 
 
142
  finally:
143
- conn.close()
 
144
 
145
  def classify_action(self, action_text: str) -> str:
146
- """Classify action type for appropriate prior"""
147
  action_lower = action_text.lower()
148
-
149
  if any(word in action_lower for word in ['database', 'db', 'sql', 'table', 'drop', 'delete']):
150
  return 'database'
151
  elif any(word in action_lower for word in ['network', 'firewall', 'load balancer']):
@@ -158,27 +176,26 @@ class BayesianRiskEngine:
158
  return 'default'
159
 
160
  def get_prior(self, action_type: str) -> Tuple[float, float]:
161
- """Get prior parameters for action type"""
162
  prior = self.action_priors.get(action_type, self.action_priors['default'])
163
  return prior['alpha'], prior['beta']
164
 
165
  def get_evidence(self, action_hash: str) -> Tuple[int, int]:
166
- """Get historical evidence for similar actions"""
167
- with self._get_db() as conn:
168
- cursor = conn.execute(
169
- 'SELECT SUM(success), SUM(total) FROM evidence WHERE action_hash = ?',
170
- (action_hash[:50],)
171
- )
172
- row = cursor.fetchone()
173
- return (row[0] or 0, row[1] or 0) if row else (0, 0)
 
 
 
174
 
175
  def calculate_posterior(self,
176
  action_text: str,
177
  context: Dict[str, Any]) -> Dict[str, Any]:
178
- """
179
- True Bayesian posterior calculation
180
- P(risk | action, context) ∝ P(action, context | risk) * P(risk)
181
- """
182
  # 1. Classify action for appropriate prior
183
  action_type = self.classify_action(action_text)
184
  alpha0, beta0 = self.get_prior(action_type)
@@ -201,8 +218,7 @@ class BayesianRiskEngine:
201
  risk_score = posterior_mean * context_multiplier
202
  risk_score = min(0.99, max(0.01, risk_score))
203
 
204
- # 7. 95% credible interval (Beta distribution quantiles)
205
- # Using approximation for computational efficiency
206
  variance = (alpha_n * beta_n) / ((alpha_n + beta_n)**2 * (alpha_n + beta_n + 1))
207
  std_dev = variance ** 0.5
208
  ci_lower = max(0.01, posterior_mean - 1.96 * std_dev)
@@ -234,60 +250,46 @@ class BayesianRiskEngine:
234
  }
235
 
236
  def _context_likelihood(self, context: Dict) -> float:
237
- """Calculate likelihood multiplier from context"""
238
  multiplier = 1.0
239
-
240
- # Environment
241
  if context.get('environment') == 'production':
242
  multiplier *= 1.5
243
  elif context.get('environment') == 'staging':
244
  multiplier *= 0.8
245
-
246
- # Time
247
  hour = datetime.now().hour
248
- if hour < 6 or hour > 22: # Off-hours
249
  multiplier *= 1.3
250
-
251
- # User seniority
252
  if context.get('user_role') == 'junior':
253
  multiplier *= 1.4
254
  elif context.get('user_role') == 'senior':
255
  multiplier *= 0.9
256
-
257
- # Backup status
258
  if not context.get('backup_available', True):
259
  multiplier *= 1.6
260
-
261
  return multiplier
262
 
263
  def record_outcome(self, action_text: str, success: bool):
264
- """Record actual outcome for future Bayesian updates"""
265
  action_hash = hashlib.sha256(action_text.encode()).hexdigest()
266
  action_type = self.classify_action(action_text)
267
-
268
- with self._get_db() as conn:
269
- conn.execute('''
270
- INSERT INTO evidence (id, action_type, action_hash, success, total, timestamp)
271
- VALUES (?, ?, ?, ?, ?, ?)
272
- ''', (
273
- str(uuid.uuid4()),
274
- action_type,
275
- action_hash[:50],
276
- 1 if success else 0,
277
- 1,
278
- datetime.utcnow().isoformat()
279
- ))
280
- conn.commit()
281
-
282
- logger.info(f"Recorded outcome for {action_type}: success={success}")
 
283
 
284
  # ============== POLICY ENGINE ==============
285
  class PolicyEngine:
286
- """
287
- Deterministic OSS policies - advisory only
288
- Matches ARF OSS healing_policies.py
289
- """
290
-
291
  def __init__(self):
292
  self.config = {
293
  "confidence_threshold": settings.DEFAULT_CONFIDENCE_THRESHOLD,
@@ -316,10 +318,7 @@ class PolicyEngine:
316
  action: str,
317
  risk: Dict[str, Any],
318
  confidence: float) -> Dict[str, Any]:
319
- """
320
- Evaluate action against policies
321
- Returns gate results and final decision
322
- """
323
  gates = []
324
 
325
  # Gate 1: Confidence threshold
@@ -391,7 +390,6 @@ class PolicyEngine:
391
  # Overall decision
392
  all_passed = all(g["passed"] for g in gates)
393
 
394
- # Determine required level
395
  if not all_passed:
396
  required_level = ExecutionLevel.OPERATOR_REVIEW
397
  elif risk["level"] == RiskLevel.LOW:
@@ -410,7 +408,6 @@ class PolicyEngine:
410
  }
411
 
412
  def update_config(self, key: str, value: Any):
413
- """Live policy updates"""
414
  if key in self.config:
415
  self.config[key] = value
416
  logger.info(f"Policy updated: {key} = {value}")
@@ -419,83 +416,76 @@ class PolicyEngine:
419
 
420
  # ============== RAG MEMORY WITH PERSISTENCE ==============
421
  class RAGMemory:
422
- """
423
- Persistent RAG memory using SQLite + vector embeddings
424
- Survives HF Space restarts
425
- """
426
-
427
  def __init__(self):
428
  self.db_path = f"{settings.DATA_DIR}/memory.db"
429
  self._init_db()
430
  self.embedding_cache = {}
431
 
432
  def _init_db(self):
433
- """Initialize memory tables"""
434
- with self._get_db() as conn:
435
- # Incidents table
436
- conn.execute('''
437
- CREATE TABLE IF NOT EXISTS incidents (
438
- id TEXT PRIMARY KEY,
439
- action TEXT,
440
- action_hash TEXT,
441
- risk_score REAL,
442
- risk_level TEXT,
443
- confidence REAL,
444
- allowed BOOLEAN,
445
- gates TEXT,
446
- timestamp TEXT,
447
- embedding TEXT
448
- )
449
- ''')
450
-
451
- # Enterprise signals table
452
- conn.execute('''
453
- CREATE TABLE IF NOT EXISTS signals (
454
- id TEXT PRIMARY KEY,
455
- signal_type TEXT,
456
- action TEXT,
457
- risk_score REAL,
458
- metadata TEXT,
459
- timestamp TEXT,
460
- contacted BOOLEAN DEFAULT 0
461
- )
462
- ''')
463
-
464
- # Create indexes
465
- conn.execute('CREATE INDEX IF NOT EXISTS idx_action_hash ON incidents(action_hash)')
466
- conn.execute('CREATE INDEX IF NOT EXISTS idx_signal_type ON signals(signal_type)')
467
- conn.execute('CREATE INDEX IF NOT EXISTS idx_signal_contacted ON signals(contacted)')
468
 
469
  @contextmanager
470
  def _get_db(self):
471
- conn = sqlite3.connect(self.db_path)
472
- conn.row_factory = sqlite3.Row
473
  try:
 
 
474
  yield conn
 
 
 
475
  finally:
476
- conn.close()
 
477
 
478
  def _simple_embedding(self, text: str) -> List[float]:
479
- """Simple bag-of-words embedding for demo"""
480
- # Cache embeddings
481
  if text in self.embedding_cache:
482
  return self.embedding_cache[text]
483
 
484
- # Simple character trigram embedding
485
  words = text.lower().split()
486
  trigrams = set()
487
  for word in words:
488
  for i in range(len(word) - 2):
489
  trigrams.add(word[i:i+3])
490
 
491
- # Convert to fixed-size vector (simplified)
492
- # In production, use sentence-transformers
493
  vector = [hash(t) % 1000 / 1000.0 for t in sorted(trigrams)[:100]]
494
- # Pad to fixed length
495
  while len(vector) < 100:
496
  vector.append(0.0)
497
  vector = vector[:100]
498
-
499
  self.embedding_cache[text] = vector
500
  return vector
501
 
@@ -506,77 +496,67 @@ class RAGMemory:
506
  confidence: float,
507
  allowed: bool,
508
  gates: List[Dict]):
509
- """Store incident in persistent memory"""
510
  action_hash = hashlib.sha256(action.encode()).hexdigest()[:50]
511
  embedding = json.dumps(self._simple_embedding(action))
512
-
513
- with self._get_db() as conn:
514
- conn.execute('''
515
- INSERT INTO incidents
516
- (id, action, action_hash, risk_score, risk_level, confidence, allowed, gates, timestamp, embedding)
517
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
518
- ''', (
519
- str(uuid.uuid4()),
520
- action[:500],
521
- action_hash,
522
- risk_score,
523
- risk_level.value,
524
- confidence,
525
- 1 if allowed else 0,
526
- json.dumps(gates),
527
- datetime.utcnow().isoformat(),
528
- embedding
529
- ))
530
- conn.commit()
 
 
531
 
532
  def find_similar(self, action: str, limit: int = 5) -> List[Dict]:
533
- """Find similar incidents using cosine similarity"""
534
  query_embedding = self._simple_embedding(action)
535
-
536
- with self._get_db() as conn:
537
- # Get all recent incidents
538
- cursor = conn.execute('''
539
- SELECT * FROM incidents
540
- ORDER BY timestamp DESC
541
- LIMIT 100
542
- ''')
543
-
544
- incidents = []
545
- for row in cursor.fetchall():
546
- stored_embedding = json.loads(row['embedding'])
547
-
548
- # Cosine similarity
549
- dot = sum(q * s for q, s in zip(query_embedding, stored_embedding))
550
- norm_q = sum(q*q for q in query_embedding) ** 0.5
551
- norm_s = sum(s*s for s in stored_embedding) ** 0.5
552
-
553
- if norm_q > 0 and norm_s > 0:
554
- similarity = dot / (norm_q * norm_s)
555
- else:
556
- similarity = 0
557
-
558
- incidents.append({
559
- 'id': row['id'],
560
- 'action': row['action'],
561
- 'risk_score': row['risk_score'],
562
- 'risk_level': row['risk_level'],
563
- 'confidence': row['confidence'],
564
- 'allowed': bool(row['allowed']),
565
- 'timestamp': row['timestamp'],
566
- 'similarity': similarity
567
- })
568
-
569
- # Sort by similarity and return top k
570
- incidents.sort(key=lambda x: x['similarity'], reverse=True)
571
- return incidents[:limit]
572
 
573
  def track_enterprise_signal(self,
574
  signal_type: LeadSignal,
575
  action: str,
576
  risk_score: float,
577
  metadata: Dict = None):
578
- """Track enterprise interest signals with persistence"""
579
-
580
  signal = {
581
  'id': str(uuid.uuid4()),
582
  'signal_type': signal_type.value,
@@ -586,35 +566,32 @@ class RAGMemory:
586
  'timestamp': datetime.utcnow().isoformat(),
587
  'contacted': 0
588
  }
589
-
590
- with self._get_db() as conn:
591
- conn.execute('''
592
- INSERT INTO signals
593
- (id, signal_type, action, risk_score, metadata, timestamp, contacted)
594
- VALUES (?, ?, ?, ?, ?, ?, ?)
595
- ''', (
596
- signal['id'],
597
- signal['signal_type'],
598
- signal['action'],
599
- signal['risk_score'],
600
- signal['metadata'],
601
- signal['timestamp'],
602
- signal['contacted']
603
- ))
604
- conn.commit()
 
 
 
605
 
606
  logger.info(f"πŸ”” Enterprise signal: {signal_type.value} - {action[:50]}...")
607
-
608
- # Trigger immediate notification for high-value signals
609
  if signal_type in [LeadSignal.HIGH_RISK_BLOCKED, LeadSignal.NOVEL_ACTION]:
610
  self._notify_sales_team(signal)
611
-
612
  return signal
613
 
614
  def _notify_sales_team(self, signal: Dict):
615
- """Real-time notification to sales team"""
616
-
617
- # Slack webhook
618
  if settings.SLACK_WEBHOOK:
619
  try:
620
  requests.post(settings.SLACK_WEBHOOK, json={
@@ -624,49 +601,52 @@ class RAGMemory:
624
  f"Risk Score: {signal['risk_score']:.2f}\n"
625
  f"Time: {signal['timestamp']}\n"
626
  f"Contact: {settings.LEAD_EMAIL}"
627
- })
628
- except:
629
- pass
630
-
631
- # Email via SendGrid (if configured)
632
- if settings.SENDGRID_API_KEY:
633
- # Send email logic here
634
- pass
635
 
636
  def get_uncontacted_signals(self) -> List[Dict]:
637
- """Get signals that haven't been followed up"""
638
- with self._get_db() as conn:
639
- cursor = conn.execute('''
640
- SELECT * FROM signals
641
- WHERE contacted = 0
642
- ORDER BY timestamp DESC
643
- ''')
644
-
645
- signals = []
646
- for row in cursor.fetchall():
647
- signals.append({
648
- 'id': row['id'],
649
- 'signal_type': row['signal_type'],
650
- 'action': row['action'],
651
- 'risk_score': row['risk_score'],
652
- 'metadata': json.loads(row['metadata']),
653
- 'timestamp': row['timestamp']
654
- })
655
- return signals
 
 
656
 
657
  def mark_contacted(self, signal_id: str):
658
- """Mark signal as contacted"""
659
- with self._get_db() as conn:
660
- conn.execute('UPDATE signals SET contacted = 1 WHERE id = ?', (signal_id,))
661
- conn.commit()
 
 
662
 
663
  # ============== AUTHENTICATION ==============
664
  security = HTTPBearer()
665
 
666
- def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
667
- """Simple API key authentication for enterprise endpoints"""
668
  if credentials.credentials != settings.API_KEY:
669
- raise HTTPException(status_code=403, detail="Invalid API key")
 
 
 
670
  return credentials.credentials
671
 
672
  # ============== PYDANTIC MODELS ==============
@@ -680,7 +660,6 @@ class ActionRequest(BaseModel):
680
  user_role: str = "devops"
681
  session_id: Optional[str] = None
682
 
683
- # FIXED: Using Pydantic V2 field_validator instead of deprecated validator
684
  @field_validator('proposedAction')
685
  @classmethod
686
  def validate_action(cls, v: str) -> str:
@@ -742,10 +721,22 @@ risk_engine = BayesianRiskEngine()
742
  policy_engine = PolicyEngine()
743
  memory = RAGMemory()
744
 
745
- # ============== API ENDPOINTS ==============
746
- @app.get("/api/v1/config")
 
 
 
 
 
 
 
 
 
 
 
 
747
  async def get_config():
748
- """Get current ARF configuration"""
749
  return {
750
  "confidenceThreshold": policy_engine.config["confidence_threshold"],
751
  "maxAutonomousRisk": policy_engine.config["max_autonomous_risk"],
@@ -754,20 +745,19 @@ async def get_config():
754
  "edition": "OSS"
755
  }
756
 
757
- @app.post("/api/v1/config")
758
  async def update_config(config: ConfigUpdateRequest):
759
- """Update ARF configuration (live)"""
760
  if config.confidenceThreshold:
761
  policy_engine.update_config("confidence_threshold", config.confidenceThreshold)
762
  if config.maxAutonomousRisk:
763
  policy_engine.update_config("max_autonomous_risk", config.maxAutonomousRisk.value)
764
  return await get_config()
765
 
766
- @app.post("/api/v1/evaluate", response_model=EvaluationResponse)
767
  async def evaluate_action(request: ActionRequest):
768
  """
769
- Real ARF OSS evaluation pipeline
770
- Used by Replit UI frontend
771
  """
772
  try:
773
  # Build context
@@ -860,75 +850,66 @@ async def evaluate_action(request: ActionRequest):
860
 
861
  except Exception as e:
862
  logger.error(f"Evaluation failed: {e}", exc_info=True)
863
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
864
 
865
  @app.get("/api/v1/enterprise/signals", dependencies=[Depends(verify_api_key)])
866
  async def get_enterprise_signals(contacted: bool = False):
867
  """
868
  Get enterprise lead signals (protected endpoint)
869
- Requires API key from HF secrets
870
  """
871
- if contacted:
872
- signals = memory.get_uncontacted_signals()
873
- else:
874
- # Get all signals from last 30 days
875
- with memory._get_db() as conn:
876
- cursor = conn.execute('''
877
- SELECT * FROM signals
878
- WHERE datetime(timestamp) > datetime('now', '-30 days')
879
- ORDER BY timestamp DESC
880
- ''')
881
- signals = []
882
- for row in cursor.fetchall():
883
- signals.append({
884
- 'id': row['id'],
885
- 'signal_type': row['signal_type'],
886
- 'action': row['action'],
887
- 'risk_score': row['risk_score'],
888
- 'metadata': json.loads(row['metadata']),
889
- 'timestamp': row['timestamp'],
890
- 'contacted': bool(row['contacted'])
891
- })
892
-
893
- return {"signals": signals, "count": len(signals)}
 
 
894
 
895
- @app.post("/api/v1/enterprise/signals/{signal_id}/contact")
896
  async def mark_signal_contacted(signal_id: str):
897
- """Mark a lead signal as contacted"""
898
  memory.mark_contacted(signal_id)
899
  return {"status": "success", "message": "Signal marked as contacted"}
900
 
901
- @app.get("/api/v1/memory/similar")
902
  async def get_similar_actions(action: str, limit: int = 5):
903
- """Find similar historical actions"""
904
  similar = memory.find_similar(action, limit=limit)
905
  return {"similar": similar, "count": len(similar)}
906
 
907
- @app.post("/api/v1/feedback")
908
  async def record_outcome(action: str, success: bool):
909
  """
910
- Record actual outcome for Bayesian updating
911
- This is how ARF learns
912
  """
913
  risk_engine.record_outcome(action, success)
914
  return {"status": "success", "message": "Outcome recorded"}
915
 
916
- @app.get("/health")
917
- async def health_check():
918
- """Health check endpoint"""
919
- return {
920
- "status": "healthy",
921
- "version": "3.3.9",
922
- "edition": "OSS",
923
- "memory_entries": len(memory.get_uncontacted_signals()),
924
- "timestamp": datetime.utcnow().isoformat()
925
- }
926
-
927
  # ============== GRADIO LEAD GENERATION UI ==============
928
  def create_lead_gen_ui():
929
- """Professional lead generation interface"""
930
-
931
- # FIXED: Moved theme and css to launch() method
932
  with gr.Blocks(title="ARF OSS - Enterprise Reliability Intelligence") as ui:
933
 
934
  # Header
@@ -991,7 +972,7 @@ def create_lead_gen_ui():
991
  </div>
992
  """)
993
 
994
- # Live Demo Stats - FIXED: Removed 'every' parameter for Gradio 4.x
995
  demo_stats = gr.JSON(
996
  label="πŸ“Š Live Demo Statistics",
997
  value={
@@ -1055,9 +1036,14 @@ app = mount_gradio_app(app, gradio_ui, path="/")
1055
  if __name__ == "__main__":
1056
  import uvicorn
1057
 
1058
- # βœ… Use PORT environment variable (defaults to 7860 for HF Spaces)
1059
  port = int(os.environ.get('PORT', 7860))
1060
 
 
 
 
 
 
 
1061
  logger.info("="*60)
1062
  logger.info("πŸš€ ARF OSS v3.3.9 Starting")
1063
  logger.info(f"πŸ“Š Data directory: {settings.DATA_DIR}")
@@ -1066,10 +1052,10 @@ if __name__ == "__main__":
1066
  logger.info(f"🌐 Serving at: http://0.0.0.0:{port}")
1067
  logger.info("="*60)
1068
 
1069
- # βœ… Run on the correct port
1070
  uvicorn.run(
1071
- app,
1072
  host="0.0.0.0",
1073
  port=port,
1074
- log_level="info"
 
1075
  )
 
7
  # πŸ”₯ CRITICAL: Force Gradio to use port 7860 for Hugging Face Spaces
8
  os.environ['GRADIO_SERVER_PORT'] = '7860'
9
  os.environ['GRADIO_SERVER_NAME'] = '0.0.0.0'
10
+ # πŸ”₯ Prevent Gradio from auto-launching its own server
11
+ os.environ['GRADIO_ANALYTICS_ENABLED'] = 'False'
12
 
13
  import json
14
  import uuid
 
25
  from enum import Enum
26
 
27
  import gradio as gr
28
+ # πŸ”₯ Close any existing Gradio instances immediately after import
29
+ gr.close_all()
30
+
31
+ from fastapi import FastAPI, HTTPException, Depends, status
32
  from fastapi.middleware.cors import CORSMiddleware
33
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
34
+ from pydantic import BaseModel, Field, field_validator
35
+ from pydantic_settings import BaseSettings # <-- NEW: Pydantic settings
36
  from gradio import mount_gradio_app
37
 
38
+ # ============== CONFIGURATION (Pydantic) ==============
39
+ class Settings(BaseSettings):
40
+ """Centralized configuration using Pydantic Settings"""
41
 
42
  # Hugging Face settings
43
+ HF_SPACE_ID: str = Field(default='local', env='SPACE_ID')
44
+ HF_TOKEN: str = Field(default='', env='HF_TOKEN')
45
 
46
  # Persistence - HF persistent storage
47
+ DATA_DIR: str = Field(default='/data' if os.path.exists('/data') else './data')
 
48
 
49
  # Lead generation
50
+ LEAD_EMAIL: str = "petter2025us@outlook.com"
51
+ CALENDLY_URL: str = "https://calendly.com/petter2025us/arf-demo"
52
 
53
  # Webhook for lead alerts (set in HF secrets)
54
+ SLACK_WEBHOOK: str = Field(default='', env='SLACK_WEBHOOK')
55
+ SENDGRID_API_KEY: str = Field(default='', env='SENDGRID_API_KEY')
56
 
57
  # Security
58
+ API_KEY: str = Field(default_factory=lambda: str(uuid.uuid4()), env='ARF_API_KEY')
59
 
60
  # ARF defaults
61
+ DEFAULT_CONFIDENCE_THRESHOLD: float = 0.9
62
+ DEFAULT_MAX_RISK: str = "MEDIUM"
63
+
64
+ class Config:
65
+ env_file = '.env' # optionally load from .env file
66
+ extra = 'ignore' # ignore extra env vars
67
+
68
+ def __init__(self, **kwargs):
69
+ super().__init__(**kwargs)
70
+ # Ensure data directory exists
71
+ os.makedirs(self.DATA_DIR, exist_ok=True)
72
 
73
  settings = Settings()
74
 
 
112
 
113
  def __init__(self):
114
  # Beta-Binomial conjugate prior
115
+ self.prior_alpha = 2.0
116
+ self.prior_beta = 5.0
 
117
 
 
118
  self.action_priors = {
119
+ 'database': {'alpha': 1.5, 'beta': 8.0},
120
+ 'network': {'alpha': 3.0, 'beta': 4.0},
121
+ 'compute': {'alpha': 4.0, 'beta': 3.0},
122
+ 'security': {'alpha': 2.0, 'beta': 6.0},
123
  'default': {'alpha': 2.0, 'beta': 5.0}
124
  }
125
 
 
126
  self.evidence_db = f"{settings.DATA_DIR}/evidence.db"
127
  self._init_db()
128
 
129
  def _init_db(self):
130
  """Initialize SQLite DB for evidence storage"""
131
+ try:
132
+ with self._get_db() as conn:
133
+ conn.execute('''
134
+ CREATE TABLE IF NOT EXISTS evidence (
135
+ id TEXT PRIMARY KEY,
136
+ action_type TEXT,
137
+ action_hash TEXT,
138
+ success INTEGER,
139
+ total INTEGER,
140
+ timestamp TEXT,
141
+ metadata TEXT
142
+ )
143
+ ''')
144
+ conn.execute('''
145
+ CREATE INDEX IF NOT EXISTS idx_action_hash
146
+ ON evidence(action_hash)
147
+ ''')
148
+ except sqlite3.Error as e:
149
+ logger.error(f"Failed to initialize evidence database: {e}")
150
+ raise RuntimeError("Could not initialize evidence storage") from e
151
 
152
  @contextmanager
153
  def _get_db(self):
154
+ conn = None
155
  try:
156
+ conn = sqlite3.connect(self.evidence_db)
157
  yield conn
158
+ except sqlite3.Error as e:
159
+ logger.error(f"Database error: {e}")
160
+ raise
161
  finally:
162
+ if conn:
163
+ conn.close()
164
 
165
  def classify_action(self, action_text: str) -> str:
 
166
  action_lower = action_text.lower()
 
167
  if any(word in action_lower for word in ['database', 'db', 'sql', 'table', 'drop', 'delete']):
168
  return 'database'
169
  elif any(word in action_lower for word in ['network', 'firewall', 'load balancer']):
 
176
  return 'default'
177
 
178
  def get_prior(self, action_type: str) -> Tuple[float, float]:
 
179
  prior = self.action_priors.get(action_type, self.action_priors['default'])
180
  return prior['alpha'], prior['beta']
181
 
182
  def get_evidence(self, action_hash: str) -> Tuple[int, int]:
183
+ try:
184
+ with self._get_db() as conn:
185
+ cursor = conn.execute(
186
+ 'SELECT SUM(success), SUM(total) FROM evidence WHERE action_hash = ?',
187
+ (action_hash[:50],)
188
+ )
189
+ row = cursor.fetchone()
190
+ return (row[0] or 0, row[1] or 0) if row else (0, 0)
191
+ except sqlite3.Error as e:
192
+ logger.error(f"Failed to retrieve evidence: {e}")
193
+ return (0, 0) # fallback to no evidence
194
 
195
  def calculate_posterior(self,
196
  action_text: str,
197
  context: Dict[str, Any]) -> Dict[str, Any]:
198
+ # ... (same as before, no changes needed) ...
 
 
 
199
  # 1. Classify action for appropriate prior
200
  action_type = self.classify_action(action_text)
201
  alpha0, beta0 = self.get_prior(action_type)
 
218
  risk_score = posterior_mean * context_multiplier
219
  risk_score = min(0.99, max(0.01, risk_score))
220
 
221
+ # 7. 95% credible interval (approximation)
 
222
  variance = (alpha_n * beta_n) / ((alpha_n + beta_n)**2 * (alpha_n + beta_n + 1))
223
  std_dev = variance ** 0.5
224
  ci_lower = max(0.01, posterior_mean - 1.96 * std_dev)
 
250
  }
251
 
252
  def _context_likelihood(self, context: Dict) -> float:
 
253
  multiplier = 1.0
 
 
254
  if context.get('environment') == 'production':
255
  multiplier *= 1.5
256
  elif context.get('environment') == 'staging':
257
  multiplier *= 0.8
 
 
258
  hour = datetime.now().hour
259
+ if hour < 6 or hour > 22:
260
  multiplier *= 1.3
 
 
261
  if context.get('user_role') == 'junior':
262
  multiplier *= 1.4
263
  elif context.get('user_role') == 'senior':
264
  multiplier *= 0.9
 
 
265
  if not context.get('backup_available', True):
266
  multiplier *= 1.6
 
267
  return multiplier
268
 
269
  def record_outcome(self, action_text: str, success: bool):
 
270
  action_hash = hashlib.sha256(action_text.encode()).hexdigest()
271
  action_type = self.classify_action(action_text)
272
+ try:
273
+ with self._get_db() as conn:
274
+ conn.execute('''
275
+ INSERT INTO evidence (id, action_type, action_hash, success, total, timestamp)
276
+ VALUES (?, ?, ?, ?, ?, ?)
277
+ ''', (
278
+ str(uuid.uuid4()),
279
+ action_type,
280
+ action_hash[:50],
281
+ 1 if success else 0,
282
+ 1,
283
+ datetime.utcnow().isoformat()
284
+ ))
285
+ conn.commit()
286
+ logger.info(f"Recorded outcome for {action_type}: success={success}")
287
+ except sqlite3.Error as e:
288
+ logger.error(f"Failed to record outcome: {e}")
289
 
290
  # ============== POLICY ENGINE ==============
291
  class PolicyEngine:
292
+ # ... (unchanged) ...
 
 
 
 
293
  def __init__(self):
294
  self.config = {
295
  "confidence_threshold": settings.DEFAULT_CONFIDENCE_THRESHOLD,
 
318
  action: str,
319
  risk: Dict[str, Any],
320
  confidence: float) -> Dict[str, Any]:
321
+ # ... unchanged ...
 
 
 
322
  gates = []
323
 
324
  # Gate 1: Confidence threshold
 
390
  # Overall decision
391
  all_passed = all(g["passed"] for g in gates)
392
 
 
393
  if not all_passed:
394
  required_level = ExecutionLevel.OPERATOR_REVIEW
395
  elif risk["level"] == RiskLevel.LOW:
 
408
  }
409
 
410
  def update_config(self, key: str, value: Any):
 
411
  if key in self.config:
412
  self.config[key] = value
413
  logger.info(f"Policy updated: {key} = {value}")
 
416
 
417
  # ============== RAG MEMORY WITH PERSISTENCE ==============
418
  class RAGMemory:
419
+ # ... (unchanged except error handling) ...
 
 
 
 
420
  def __init__(self):
421
  self.db_path = f"{settings.DATA_DIR}/memory.db"
422
  self._init_db()
423
  self.embedding_cache = {}
424
 
425
  def _init_db(self):
426
+ try:
427
+ with self._get_db() as conn:
428
+ conn.execute('''
429
+ CREATE TABLE IF NOT EXISTS incidents (
430
+ id TEXT PRIMARY KEY,
431
+ action TEXT,
432
+ action_hash TEXT,
433
+ risk_score REAL,
434
+ risk_level TEXT,
435
+ confidence REAL,
436
+ allowed BOOLEAN,
437
+ gates TEXT,
438
+ timestamp TEXT,
439
+ embedding TEXT
440
+ )
441
+ ''')
442
+ conn.execute('''
443
+ CREATE TABLE IF NOT EXISTS signals (
444
+ id TEXT PRIMARY KEY,
445
+ signal_type TEXT,
446
+ action TEXT,
447
+ risk_score REAL,
448
+ metadata TEXT,
449
+ timestamp TEXT,
450
+ contacted BOOLEAN DEFAULT 0
451
+ )
452
+ ''')
453
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_action_hash ON incidents(action_hash)')
454
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_signal_type ON signals(signal_type)')
455
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_signal_contacted ON signals(contacted)')
456
+ except sqlite3.Error as e:
457
+ logger.error(f"Failed to initialize memory database: {e}")
458
+ raise RuntimeError("Could not initialize memory storage") from e
 
 
459
 
460
  @contextmanager
461
  def _get_db(self):
462
+ conn = None
 
463
  try:
464
+ conn = sqlite3.connect(self.db_path)
465
+ conn.row_factory = sqlite3.Row
466
  yield conn
467
+ except sqlite3.Error as e:
468
+ logger.error(f"Database error in memory: {e}")
469
+ raise
470
  finally:
471
+ if conn:
472
+ conn.close()
473
 
474
  def _simple_embedding(self, text: str) -> List[float]:
475
+ # ... unchanged ...
 
476
  if text in self.embedding_cache:
477
  return self.embedding_cache[text]
478
 
 
479
  words = text.lower().split()
480
  trigrams = set()
481
  for word in words:
482
  for i in range(len(word) - 2):
483
  trigrams.add(word[i:i+3])
484
 
 
 
485
  vector = [hash(t) % 1000 / 1000.0 for t in sorted(trigrams)[:100]]
 
486
  while len(vector) < 100:
487
  vector.append(0.0)
488
  vector = vector[:100]
 
489
  self.embedding_cache[text] = vector
490
  return vector
491
 
 
496
  confidence: float,
497
  allowed: bool,
498
  gates: List[Dict]):
 
499
  action_hash = hashlib.sha256(action.encode()).hexdigest()[:50]
500
  embedding = json.dumps(self._simple_embedding(action))
501
+ try:
502
+ with self._get_db() as conn:
503
+ conn.execute('''
504
+ INSERT INTO incidents
505
+ (id, action, action_hash, risk_score, risk_level, confidence, allowed, gates, timestamp, embedding)
506
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
507
+ ''', (
508
+ str(uuid.uuid4()),
509
+ action[:500],
510
+ action_hash,
511
+ risk_score,
512
+ risk_level.value,
513
+ confidence,
514
+ 1 if allowed else 0,
515
+ json.dumps(gates),
516
+ datetime.utcnow().isoformat(),
517
+ embedding
518
+ ))
519
+ conn.commit()
520
+ except sqlite3.Error as e:
521
+ logger.error(f"Failed to store incident: {e}")
522
 
523
  def find_similar(self, action: str, limit: int = 5) -> List[Dict]:
 
524
  query_embedding = self._simple_embedding(action)
525
+ try:
526
+ with self._get_db() as conn:
527
+ cursor = conn.execute('''
528
+ SELECT * FROM incidents
529
+ ORDER BY timestamp DESC
530
+ LIMIT 100
531
+ ''')
532
+ incidents = []
533
+ for row in cursor.fetchall():
534
+ stored_embedding = json.loads(row['embedding'])
535
+ dot = sum(q * s for q, s in zip(query_embedding, stored_embedding))
536
+ norm_q = sum(q*q for q in query_embedding) ** 0.5
537
+ norm_s = sum(s*s for s in stored_embedding) ** 0.5
538
+ similarity = dot / (norm_q * norm_s) if (norm_q > 0 and norm_s > 0) else 0
539
+ incidents.append({
540
+ 'id': row['id'],
541
+ 'action': row['action'],
542
+ 'risk_score': row['risk_score'],
543
+ 'risk_level': row['risk_level'],
544
+ 'confidence': row['confidence'],
545
+ 'allowed': bool(row['allowed']),
546
+ 'timestamp': row['timestamp'],
547
+ 'similarity': similarity
548
+ })
549
+ incidents.sort(key=lambda x: x['similarity'], reverse=True)
550
+ return incidents[:limit]
551
+ except sqlite3.Error as e:
552
+ logger.error(f"Failed to find similar incidents: {e}")
553
+ return []
 
 
 
 
 
 
 
 
554
 
555
  def track_enterprise_signal(self,
556
  signal_type: LeadSignal,
557
  action: str,
558
  risk_score: float,
559
  metadata: Dict = None):
 
 
560
  signal = {
561
  'id': str(uuid.uuid4()),
562
  'signal_type': signal_type.value,
 
566
  'timestamp': datetime.utcnow().isoformat(),
567
  'contacted': 0
568
  }
569
+ try:
570
+ with self._get_db() as conn:
571
+ conn.execute('''
572
+ INSERT INTO signals
573
+ (id, signal_type, action, risk_score, metadata, timestamp, contacted)
574
+ VALUES (?, ?, ?, ?, ?, ?, ?)
575
+ ''', (
576
+ signal['id'],
577
+ signal['signal_type'],
578
+ signal['action'],
579
+ signal['risk_score'],
580
+ signal['metadata'],
581
+ signal['timestamp'],
582
+ signal['contacted']
583
+ ))
584
+ conn.commit()
585
+ except sqlite3.Error as e:
586
+ logger.error(f"Failed to track signal: {e}")
587
+ return None
588
 
589
  logger.info(f"πŸ”” Enterprise signal: {signal_type.value} - {action[:50]}...")
 
 
590
  if signal_type in [LeadSignal.HIGH_RISK_BLOCKED, LeadSignal.NOVEL_ACTION]:
591
  self._notify_sales_team(signal)
 
592
  return signal
593
 
594
  def _notify_sales_team(self, signal: Dict):
 
 
 
595
  if settings.SLACK_WEBHOOK:
596
  try:
597
  requests.post(settings.SLACK_WEBHOOK, json={
 
601
  f"Risk Score: {signal['risk_score']:.2f}\n"
602
  f"Time: {signal['timestamp']}\n"
603
  f"Contact: {settings.LEAD_EMAIL}"
604
+ }, timeout=5)
605
+ except requests.RequestException as e:
606
+ logger.error(f"Slack notification failed: {e}")
607
+ # Email via SendGrid (if configured) could be added similarly
 
 
 
 
608
 
609
  def get_uncontacted_signals(self) -> List[Dict]:
610
+ try:
611
+ with self._get_db() as conn:
612
+ cursor = conn.execute('''
613
+ SELECT * FROM signals
614
+ WHERE contacted = 0
615
+ ORDER BY timestamp DESC
616
+ ''')
617
+ signals = []
618
+ for row in cursor.fetchall():
619
+ signals.append({
620
+ 'id': row['id'],
621
+ 'signal_type': row['signal_type'],
622
+ 'action': row['action'],
623
+ 'risk_score': row['risk_score'],
624
+ 'metadata': json.loads(row['metadata']),
625
+ 'timestamp': row['timestamp']
626
+ })
627
+ return signals
628
+ except sqlite3.Error as e:
629
+ logger.error(f"Failed to get uncontacted signals: {e}")
630
+ return []
631
 
632
  def mark_contacted(self, signal_id: str):
633
+ try:
634
+ with self._get_db() as conn:
635
+ conn.execute('UPDATE signals SET contacted = 1 WHERE id = ?', (signal_id,))
636
+ conn.commit()
637
+ except sqlite3.Error as e:
638
+ logger.error(f"Failed to mark signal as contacted: {e}")
639
 
640
  # ============== AUTHENTICATION ==============
641
  security = HTTPBearer()
642
 
643
+ async def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
644
+ """Verify API key for protected endpoints"""
645
  if credentials.credentials != settings.API_KEY:
646
+ raise HTTPException(
647
+ status_code=status.HTTP_403_FORBIDDEN,
648
+ detail="Invalid API key"
649
+ )
650
  return credentials.credentials
651
 
652
  # ============== PYDANTIC MODELS ==============
 
660
  user_role: str = "devops"
661
  session_id: Optional[str] = None
662
 
 
663
  @field_validator('proposedAction')
664
  @classmethod
665
  def validate_action(cls, v: str) -> str:
 
721
  policy_engine = PolicyEngine()
722
  memory = RAGMemory()
723
 
724
+ # ============== API ENDPOINTS (with authentication) ==============
725
+
726
+ @app.get("/health")
727
+ async def health_check():
728
+ """Public health check endpoint (no auth required)"""
729
+ return {
730
+ "status": "healthy",
731
+ "version": "3.3.9",
732
+ "edition": "OSS",
733
+ "memory_entries": len(memory.get_uncontacted_signals()),
734
+ "timestamp": datetime.utcnow().isoformat()
735
+ }
736
+
737
+ @app.get("/api/v1/config", dependencies=[Depends(verify_api_key)])
738
  async def get_config():
739
+ """Get current ARF configuration (protected)"""
740
  return {
741
  "confidenceThreshold": policy_engine.config["confidence_threshold"],
742
  "maxAutonomousRisk": policy_engine.config["max_autonomous_risk"],
 
745
  "edition": "OSS"
746
  }
747
 
748
+ @app.post("/api/v1/config", dependencies=[Depends(verify_api_key)])
749
  async def update_config(config: ConfigUpdateRequest):
750
+ """Update ARF configuration (protected)"""
751
  if config.confidenceThreshold:
752
  policy_engine.update_config("confidence_threshold", config.confidenceThreshold)
753
  if config.maxAutonomousRisk:
754
  policy_engine.update_config("max_autonomous_risk", config.maxAutonomousRisk.value)
755
  return await get_config()
756
 
757
+ @app.post("/api/v1/evaluate", dependencies=[Depends(verify_api_key)], response_model=EvaluationResponse)
758
  async def evaluate_action(request: ActionRequest):
759
  """
760
+ Real ARF OSS evaluation pipeline (protected)
 
761
  """
762
  try:
763
  # Build context
 
850
 
851
  except Exception as e:
852
  logger.error(f"Evaluation failed: {e}", exc_info=True)
853
+ raise HTTPException(
854
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
855
+ detail="Internal server error during evaluation"
856
+ )
857
 
858
  @app.get("/api/v1/enterprise/signals", dependencies=[Depends(verify_api_key)])
859
  async def get_enterprise_signals(contacted: bool = False):
860
  """
861
  Get enterprise lead signals (protected endpoint)
 
862
  """
863
+ try:
864
+ if contacted:
865
+ signals = memory.get_uncontacted_signals()
866
+ else:
867
+ with memory._get_db() as conn:
868
+ cursor = conn.execute('''
869
+ SELECT * FROM signals
870
+ WHERE datetime(timestamp) > datetime('now', '-30 days')
871
+ ORDER BY timestamp DESC
872
+ ''')
873
+ signals = []
874
+ for row in cursor.fetchall():
875
+ signals.append({
876
+ 'id': row['id'],
877
+ 'signal_type': row['signal_type'],
878
+ 'action': row['action'],
879
+ 'risk_score': row['risk_score'],
880
+ 'metadata': json.loads(row['metadata']),
881
+ 'timestamp': row['timestamp'],
882
+ 'contacted': bool(row['contacted'])
883
+ })
884
+ return {"signals": signals, "count": len(signals)}
885
+ except Exception as e:
886
+ logger.error(f"Failed to retrieve signals: {e}")
887
+ raise HTTPException(status_code=500, detail="Could not retrieve signals")
888
 
889
+ @app.post("/api/v1/enterprise/signals/{signal_id}/contact", dependencies=[Depends(verify_api_key)])
890
  async def mark_signal_contacted(signal_id: str):
891
+ """Mark a lead signal as contacted (protected)"""
892
  memory.mark_contacted(signal_id)
893
  return {"status": "success", "message": "Signal marked as contacted"}
894
 
895
+ @app.get("/api/v1/memory/similar", dependencies=[Depends(verify_api_key)])
896
  async def get_similar_actions(action: str, limit: int = 5):
897
+ """Find similar historical actions (protected)"""
898
  similar = memory.find_similar(action, limit=limit)
899
  return {"similar": similar, "count": len(similar)}
900
 
901
+ @app.post("/api/v1/feedback", dependencies=[Depends(verify_api_key)])
902
  async def record_outcome(action: str, success: bool):
903
  """
904
+ Record actual outcome for Bayesian updating (protected)
 
905
  """
906
  risk_engine.record_outcome(action, success)
907
  return {"status": "success", "message": "Outcome recorded"}
908
 
 
 
 
 
 
 
 
 
 
 
 
909
  # ============== GRADIO LEAD GENERATION UI ==============
910
  def create_lead_gen_ui():
911
+ """Professional lead generation interface (no auth needed for UI)"""
912
+ # ... (unchanged) ...
 
913
  with gr.Blocks(title="ARF OSS - Enterprise Reliability Intelligence") as ui:
914
 
915
  # Header
 
972
  </div>
973
  """)
974
 
975
+ # Live Demo Stats
976
  demo_stats = gr.JSON(
977
  label="πŸ“Š Live Demo Statistics",
978
  value={
 
1036
  if __name__ == "__main__":
1037
  import uvicorn
1038
 
 
1039
  port = int(os.environ.get('PORT', 7860))
1040
 
1041
+ # πŸ”₯ Ensure any lingering Gradio servers are closed before starting
1042
+ try:
1043
+ gr.close_all()
1044
+ except:
1045
+ pass
1046
+
1047
  logger.info("="*60)
1048
  logger.info("πŸš€ ARF OSS v3.3.9 Starting")
1049
  logger.info(f"πŸ“Š Data directory: {settings.DATA_DIR}")
 
1052
  logger.info(f"🌐 Serving at: http://0.0.0.0:{port}")
1053
  logger.info("="*60)
1054
 
 
1055
  uvicorn.run(
1056
+ "hf_demo:app",
1057
  host="0.0.0.0",
1058
  port=port,
1059
+ log_level="info",
1060
+ reload=False
1061
  )