Isaac commited on
Commit
0525c17
Β·
verified Β·
1 Parent(s): 7b5cb27

Upload bios_controller.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. bios_controller.py +345 -124
bios_controller.py CHANGED
@@ -3,7 +3,7 @@
3
  β•‘ β•‘
4
  β•‘ BIOS β€” Business Idea Operating System β•‘
5
  β•‘ Model Controller Β· bios_controller.py β•‘
6
- β•‘ Version: 1.0.0 Β· Kernel: BIOS-kernel-v1 β•‘
7
  β•‘ β•‘
8
  β•‘ "We don't just analyse businesses. We illuminate them." β•‘
9
  β•‘ β•‘
@@ -25,47 +25,60 @@ import os
25
  import re
26
  import time
27
  import uuid
 
28
  from dataclasses import dataclass, field, asdict
29
  from datetime import datetime, timezone
30
  from enum import Enum
31
  from typing import Any, Optional
32
 
33
- import psycopg # psycopg v3 (pip install psycopg[binary])
34
- from psycopg.rows import dict_row
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  # ── Optional: HuggingFace Inference (pip install huggingface_hub) ──────────
37
  try:
38
- from huggingface_hub import InferenceClient
39
  HF_AVAILABLE = True
40
- except ImportError:
41
  HF_AVAILABLE = False
42
 
43
  # ── Optional: Groq client for llama-3.3-70b (pip install groq) ─────────────
44
  try:
45
- from groq import Groq
46
  GROQ_AVAILABLE = True
47
- except ImportError:
48
  GROQ_AVAILABLE = False
49
 
50
  # ── Optional: OpenAI (pip install openai) ────────────────────────────
51
  try:
52
- import openai
53
  OPENAI_AVAILABLE = True
54
- except ImportError:
55
  OPENAI_AVAILABLE = False
56
 
57
  # ── Optional: Gemini (pip install google-generativeai) ───────────────
58
  try:
59
- import google.generativeai as genai
60
  GEMINI_AVAILABLE = True
61
- except ImportError:
62
  GEMINI_AVAILABLE = False
63
 
64
  # ── Optional: Anthropic (pip install anthropic) ────────────────────────────
65
  try:
66
- import anthropic
67
  ANTHROPIC_AVAILABLE = True
68
- except ImportError:
69
  ANTHROPIC_AVAILABLE = False
70
 
71
 
@@ -81,6 +94,129 @@ logging.basicConfig(
81
  log = logging.getLogger("bios.controller")
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  # ═══════════════════════════════════════════════════════════════════════════════
85
  # ENUMS & CONSTANTS
86
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -102,10 +238,14 @@ class ModelVariant(str, Enum):
102
  BIOS_INSIGHT = "bios_insight" # Fine-tuned BIOS-Insight-v1
103
 
104
 
105
- # Model identifiers
 
 
 
 
106
  MODEL_IDS = {
107
  ModelVariant.BASE: "meta-llama/llama-3.3-70b-versatile",
108
- ModelVariant.BIOS_INSIGHT: "BIOS-kernel/BIOS-Insight-v1", # future HF repo
109
  }
110
 
111
  GROQ_MODEL_IDS = {
@@ -274,25 +414,36 @@ class DiagnosisReport:
274
 
275
  model_used: str = ""
276
  generation_time_ms: int = 0
 
277
 
278
  def to_dict(self) -> dict:
279
  return {
280
- "session_id": self.session_id,
281
- "business_name": self.business_name,
282
  "industry": self.industry,
283
  "location": self.location,
284
- "generated_at": self.generated_at,
285
- "health_score": self.health_score,
286
- "health_label": self.health_label,
287
- "health_dimensions": self.health_dimensions.to_dict(),
288
- "top_3_weaknesses": [w.to_dict() for w in self.top_3_weaknesses],
289
- "growth_opportunities": [o.to_dict() for o in self.growth_opportunities],
290
- "priority_action_items": [a.to_dict() for a in self.priority_action_items],
291
- "ai_narrative": self.ai_narrative,
292
- "benchmarking": self.benchmarking,
293
- "next_module": self.next_module,
294
- "model_used": self.model_used,
295
- "generation_time_ms": self.generation_time_ms,
 
 
 
 
 
 
 
 
 
 
296
  }
297
 
298
  def to_json(self, indent: int = 2) -> str:
@@ -331,6 +482,8 @@ class ModelRouter:
331
  # Clients initialised lazily
332
  self._groq_client: Any = None
333
  self._hf_client: Any = None
 
 
334
  self._anthropic_client: Any = None
335
 
336
  log.info(
@@ -354,10 +507,10 @@ class ModelRouter:
354
  if self._hf_client is None:
355
  if not HF_AVAILABLE:
356
  raise RuntimeError("huggingface_hub not installed. Run: pip install huggingface_hub")
357
- api_key = os.getenv("HF_API_KEY")
358
  if not api_key:
359
- raise RuntimeError("HF_API_KEY environment variable not set")
360
- self._hf_client = InferenceClient(token=api_key)
361
  return self._hf_client
362
 
363
  def _get_openai(self):
@@ -395,7 +548,8 @@ class ModelRouter:
395
 
396
  def _resolve_route(self) -> tuple[ModelBackend, ModelVariant]:
397
  """Determine which backend + variant to actually use."""
398
- if self.bios_insight_ready and HF_AVAILABLE and os.getenv("HF_API_KEY"):
 
399
  return ModelBackend.HF_INFERENCE, ModelVariant.BIOS_INSIGHT
400
  if OPENAI_AVAILABLE and os.getenv("OPENAI_API_KEY"):
401
  return ModelBackend.OPENAI, ModelVariant.BASE
@@ -459,13 +613,25 @@ class ModelRouter:
459
  {"role": "system", "content": system},
460
  {"role": "user", "content": user},
461
  ]
462
- response = client.chat_completion(
463
- messages=messages,
464
- model=model,
465
- max_tokens=self.max_tokens,
466
- temperature=self.temperature,
467
- )
468
- return response.choices[0].message.content, f"hf/{model}"
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
  def _infer_openai(self, system: str, user: str) -> tuple[str, str]:
471
  client = self._get_openai()
@@ -847,7 +1013,8 @@ You always respond in valid JSON with this exact structure:
847
 
848
  Rules:
849
  - Be specific with numbers from the data provided
850
- - Use the language specified in preferred_language
 
851
  - Never be generic β€” reference the actual business, industry, and goals
852
  - Tone: elite advisory, not chatbot small talk"""
853
 
@@ -909,31 +1076,16 @@ Generate the executive narrative JSON now."""
909
  class NeonDBWriter:
910
  """
911
  Persists BIOS diagnosis reports to NeonDB (PostgreSQL) via psycopg v3.
912
-
913
- Required table (run schema_auth.sql first):
914
- CREATE TABLE IF NOT EXISTS diagnoses (
915
- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
916
- session_id VARCHAR(255) UNIQUE NOT NULL,
917
- business_name VARCHAR(255),
918
- industry VARCHAR(100),
919
- location VARCHAR(100),
920
- health_score INTEGER,
921
- health_label VARCHAR(50),
922
- health_dimensions JSONB,
923
- top_3_weaknesses JSONB,
924
- growth_opportunities JSONB,
925
- priority_action_items JSONB,
926
- ai_narrative TEXT,
927
- benchmarking JSONB,
928
- model_used VARCHAR(255),
929
- generation_time_ms INTEGER,
930
- status VARCHAR(50) DEFAULT 'COMPLETED',
931
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
932
- );
933
  """
934
 
935
- def __init__(self, database_url: Optional[str] = None):
 
 
 
 
936
  self.database_url = database_url or os.getenv("DATABASE_URL")
 
937
  if not self.database_url:
938
  raise ValueError(
939
  "DATABASE_URL not set. Export it or pass database_url= to NeonDBWriter."
@@ -944,84 +1096,149 @@ class NeonDBWriter:
944
  ).replace(
945
  "postgres+asyncpg://", "postgresql://"
946
  )
 
947
 
948
- def save_report(self, report: DiagnosisReport) -> str:
949
  """
950
- Upsert a DiagnosisReport into the diagnoses table.
951
- Returns the session_id of the saved record.
952
  """
953
  d = report.to_dict()
 
 
 
954
 
955
- sql = """
 
956
  INSERT INTO diagnoses (
957
- session_id, business_name, industry, location,
958
- health_score, health_label, health_dimensions,
959
- top_3_weaknesses, growth_opportunities, priority_action_items,
960
- ai_narrative, benchmarking, model_used, generation_time_ms,
961
- status, created_at
 
 
962
  ) VALUES (
963
- %(session_id)s, %(business_name)s, %(industry)s, %(location)s,
964
- %(health_score)s, %(health_label)s, %(health_dimensions)s,
965
- %(top_3_weaknesses)s, %(growth_opportunities)s, %(priority_action_items)s,
966
- %(ai_narrative)s, %(benchmarking)s, %(model_used)s, %(generation_time_ms)s,
967
- 'COMPLETED', NOW()
 
 
968
  )
969
- ON CONFLICT (session_id) DO UPDATE SET
970
- health_score = EXCLUDED.health_score,
971
- health_label = EXCLUDED.health_label,
972
- health_dimensions = EXCLUDED.health_dimensions,
973
- top_3_weaknesses = EXCLUDED.top_3_weaknesses,
974
- growth_opportunities = EXCLUDED.growth_opportunities,
975
- priority_action_items = EXCLUDED.priority_action_items,
976
- ai_narrative = EXCLUDED.ai_narrative,
977
- benchmarking = EXCLUDED.benchmarking,
978
- model_used = EXCLUDED.model_used,
979
- generation_time_ms = EXCLUDED.generation_time_ms,
980
- status = 'COMPLETED'
981
  """
982
 
983
- params = {
984
- "session_id": d["session_id"],
985
- "business_name": d["business_name"],
986
- "industry": d["industry"],
987
- "location": d["location"],
988
- "health_score": d["health_score"],
989
- "health_label": d["health_label"],
990
- "health_dimensions": json.dumps(d["health_dimensions"]),
991
- "top_3_weaknesses": json.dumps(d["top_3_weaknesses"]),
992
- "growth_opportunities": json.dumps(d["growth_opportunities"]),
993
- "priority_action_items": json.dumps(d["priority_action_items"]),
994
- "ai_narrative": d["ai_narrative"],
995
- "benchmarking": json.dumps(d["benchmarking"]),
996
- "model_used": d["model_used"],
997
- "generation_time_ms": d["generation_time_ms"],
998
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999
 
1000
  with psycopg.connect(self.database_url) as conn:
1001
  with conn.cursor() as cur:
1002
- cur.execute(sql, params)
 
1003
  conn.commit()
1004
 
1005
- log.info(f"βœ… Report saved to NeonDB | session_id={report.session_id} | score={report.health_score}")
1006
- return report.session_id
1007
 
1008
  def fetch_report(self, session_id: str) -> Optional[dict]:
1009
- """Fetch a previously saved report by session_id."""
1010
- sql = "SELECT * FROM diagnoses WHERE session_id = %s"
1011
- with psycopg.connect(self.database_url, row_factory=dict_row) as conn:
 
 
 
 
 
1012
  with conn.cursor() as cur:
1013
  cur.execute(sql, (session_id,))
1014
  row = cur.fetchone()
1015
- return dict(row) if row else None
 
 
 
 
 
 
 
 
 
 
 
1016
 
1017
  def list_reports(self, limit: int = 20) -> list[dict]:
1018
- """Return the most recent diagnoses."""
1019
  sql = "SELECT session_id, business_name, industry, health_score, health_label, status, created_at FROM diagnoses ORDER BY created_at DESC LIMIT %s"
1020
- with psycopg.connect(self.database_url, row_factory=dict_row) as conn:
1021
  with conn.cursor() as cur:
1022
  cur.execute(sql, (limit,))
1023
  rows = cur.fetchall()
1024
- return [dict(r) for r in rows]
 
 
 
 
 
 
 
1025
 
1026
 
1027
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -1055,7 +1272,9 @@ class BIOSController:
1055
  max_tokens: int = 2048,
1056
  database_url: Optional[str] = None,
1057
  save_to_db: bool = True,
 
1058
  ):
 
1059
  self.router = ModelRouter(
1060
  backend=backend,
1061
  bios_insight_ready=bios_insight_ready,
@@ -1080,17 +1299,18 @@ class BIOSController:
1080
  @property
1081
  def db(self) -> NeonDBWriter:
1082
  if self._db is None:
1083
- self._db = NeonDBWriter(self._db_url)
1084
  return self._db
1085
 
1086
  # ── Main pipeline ─────────────────────────────────────────────────────────
1087
 
1088
- def run_diagnosis(self, inputs: BusinessInputs) -> DiagnosisReport:
1089
  """
1090
  Full Module 1 pipeline.
1091
 
1092
  Args:
1093
  inputs: Completed BusinessInputs with all 24 question answers.
 
1094
 
1095
  Returns:
1096
  DiagnosisReport with health_score, top_3_weaknesses,
@@ -1145,6 +1365,7 @@ class BIOSController:
1145
  benchmarking = benchmarking,
1146
  model_used = model_used,
1147
  generation_time_ms = t_ms,
 
1148
  )
1149
 
1150
  log.info(f"βœ” Diagnosis complete | score={score} | {t_ms}ms")
@@ -1152,7 +1373,7 @@ class BIOSController:
1152
  # ── Step 8: Save to NeonDB ────────────────────────────────────────────
1153
  if self.save_to_db:
1154
  try:
1155
- self.db.save_report(report)
1156
  except Exception as e:
1157
  log.warning(f"DB save failed (non-fatal): {e}")
1158
 
@@ -1243,10 +1464,10 @@ def _demo_inputs() -> BusinessInputs:
1243
 
1244
 
1245
  if __name__ == "__main__":
1246
- print("\n" + "═" * 60)
1247
  print(" BIOS β€” Business Idea Operating System")
1248
  print(" BIOS-kernel-v1 Β· Module 1: Business Diagnosis")
1249
- print("═" * 60 + "\n")
1250
 
1251
  # ── Instantiate controller ────────────────────────────────────────────────
1252
  # Set save_to_db=True and export DATABASE_URL to persist to NeonDB.
@@ -1260,14 +1481,14 @@ if __name__ == "__main__":
1260
  report = controller.run_diagnosis(inputs)
1261
 
1262
  # ── Print structured JSON output ──────────────────────────────────────────
1263
- print("\n" + "─" * 60)
1264
  print(" BIOS DIAGNOSIS REPORT")
1265
- print("─" * 60)
1266
  print(report.to_json())
1267
 
1268
- print("\n" + "═" * 60)
1269
  print(f" Health Score : {report.health_score}/100 ({report.health_label})")
1270
  print(f" Session ID : {report.session_id}")
1271
  print(f" Model Used : {report.model_used}")
1272
  print(f" Generated in : {report.generation_time_ms}ms")
1273
- print("═" * 60 + "\n")
 
3
  β•‘ β•‘
4
  β•‘ BIOS β€” Business Idea Operating System β•‘
5
  β•‘ Model Controller Β· bios_controller.py β•‘
6
+ β•‘ Version: 1.0.0 Β· Kernel: BIOS-Insight-v1 β•‘
7
  β•‘ β•‘
8
  β•‘ "We don't just analyse businesses. We illuminate them." β•‘
9
  β•‘ β•‘
 
25
  import re
26
  import time
27
  import uuid
28
+ import base64
29
  from dataclasses import dataclass, field, asdict
30
  from datetime import datetime, timezone
31
  from enum import Enum
32
  from typing import Any, Optional
33
 
34
+ try:
35
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM # pyright: ignore[reportMissingImports]
36
+ CRYPTOGRAPHY_AVAILABLE = True
37
+ except Exception:
38
+ CRYPTOGRAPHY_AVAILABLE = False
39
+
40
+ try:
41
+ import psycopg # psycopg v3 (pip install psycopg[binary]) # pyright: ignore[reportMissingImports]
42
+ from psycopg.rows import dict_row as psycopg_dict_row # pyright: ignore[reportMissingImports]
43
+ PSYCOPG_AVAILABLE = True
44
+ except Exception:
45
+ psycopg = None
46
+ psycopg_dict_row = None
47
+ PSYCOPG_AVAILABLE = False
48
 
49
  # ── Optional: HuggingFace Inference (pip install huggingface_hub) ──────────
50
  try:
51
+ from huggingface_hub import InferenceClient # pyright: ignore[reportMissingImports]
52
  HF_AVAILABLE = True
53
+ except Exception:
54
  HF_AVAILABLE = False
55
 
56
  # ── Optional: Groq client for llama-3.3-70b (pip install groq) ─────────────
57
  try:
58
+ from groq import Groq # pyright: ignore[reportMissingImports]
59
  GROQ_AVAILABLE = True
60
+ except Exception:
61
  GROQ_AVAILABLE = False
62
 
63
  # ── Optional: OpenAI (pip install openai) ────────────────────────────
64
  try:
65
+ import openai # pyright: ignore[reportMissingImports]
66
  OPENAI_AVAILABLE = True
67
+ except Exception:
68
  OPENAI_AVAILABLE = False
69
 
70
  # ── Optional: Gemini (pip install google-generativeai) ───────────────
71
  try:
72
+ import google.generativeai as genai # pyright: ignore[reportMissingImports]
73
  GEMINI_AVAILABLE = True
74
+ except Exception:
75
  GEMINI_AVAILABLE = False
76
 
77
  # ── Optional: Anthropic (pip install anthropic) ────────────────────────────
78
  try:
79
+ import anthropic # pyright: ignore[reportMissingImports]
80
  ANTHROPIC_AVAILABLE = True
81
+ except Exception:
82
  ANTHROPIC_AVAILABLE = False
83
 
84
 
 
94
  log = logging.getLogger("bios.controller")
95
 
96
 
97
+ # ═══════════════════════════════════════════════════════════════════════════════
98
+ # SECURITY MANAGER (AES-256-GCM)
99
+ # ═══════════════════════════════════════════════════════════════════════════════
100
+
101
+ class SecurityManager:
102
+ """
103
+ Handles high-security AES-256-GCM encryption for sensitive business data.
104
+ Ensures 'The Fortress' level protection for SME information.
105
+ """
106
+
107
+ def __init__(self, key: Optional[str] = None):
108
+ if not CRYPTOGRAPHY_AVAILABLE:
109
+ log.warning("⚠️ cryptography package not found. Data will remain in plaintext.")
110
+ self._aes = None
111
+ return
112
+
113
+ raw_key = key or os.getenv("BIOS_ENCRYPTION_KEY")
114
+ if not raw_key:
115
+ log.warning("⚠️ BIOS_ENCRYPTION_KEY not set. Security disabled.")
116
+ self._aes = None
117
+ return
118
+
119
+ try:
120
+ # Ensure key is 32 bytes for AES-256
121
+ key_bytes = raw_key.encode("utf-8")
122
+ if len(key_bytes) > 32:
123
+ key_bytes = key_bytes[:32]
124
+ elif len(key_bytes) < 32:
125
+ key_bytes = key_bytes.ljust(32, b"0")
126
+
127
+ self._aes = AESGCM(key_bytes)
128
+ log.info("πŸ”’ BIOS Security Manager initialized (AES-256-GCM)")
129
+ except Exception as e:
130
+ log.error(f"❌ Failed to initialize Security Manager: {e}")
131
+ self._aes = None
132
+
133
+ def encrypt(self, plaintext: str) -> str:
134
+ """Encrypt string data to base64-encoded ciphertext."""
135
+ if not self._aes or not plaintext:
136
+ return plaintext
137
+
138
+ try:
139
+ nonce = os.urandom(12)
140
+ ct = self._aes.encrypt(nonce, plaintext.encode("utf-8"), None)
141
+ # Combine nonce + ciphertext and encode
142
+ return base64.b64encode(nonce + ct).decode("utf-8")
143
+ except Exception as e:
144
+ log.error(f"Encryption failed: {e}")
145
+ return plaintext
146
+
147
+ def decrypt(self, ciphertext: str) -> str:
148
+ """Decrypt base64 ciphertext back to plaintext."""
149
+ if not self._aes or not ciphertext:
150
+ return ciphertext
151
+
152
+ try:
153
+ data = base64.b64decode(ciphertext)
154
+ nonce, ct = data[:12], data[12:]
155
+ pt = self._aes.decrypt(nonce, ct, None)
156
+ return pt.decode("utf-8")
157
+ except Exception as e:
158
+ # If decryption fails (e.g. data wasn't encrypted), return as-is
159
+ return ciphertext
160
+
161
+
162
+ # ═══════════════════════════════════════════════════════════════════════════════
163
+ # USER & CREDIT MANAGER (The Fortress)
164
+ # ═══════════════════════════════════════════════════════════════════════════════
165
+
166
+ class UserManager:
167
+ """
168
+ Handles user accounts, credits, and refills in NeonDB.
169
+ Initial: 1000 credits. Daily Refill: 500 credits.
170
+ """
171
+
172
+ def __init__(self, database_url: str):
173
+ self.db_url = database_url
174
+
175
+ def ensure_user_exists(self, email: str, business_name: str) -> str:
176
+ """Create user if not exists, return user_id."""
177
+ sql = """
178
+ INSERT INTO users (email, business_name, credits, last_refill)
179
+ VALUES (%s, %s, 1000, NOW())
180
+ ON CONFLICT (email) DO UPDATE SET business_name = EXCLUDED.business_name
181
+ RETURNING id, credits
182
+ """
183
+ with psycopg.connect(self.db_url) as conn:
184
+ with conn.cursor(row_factory=psycopg_dict_row) as cur:
185
+ cur.execute(sql, (email, business_name))
186
+ row = cur.fetchone()
187
+ conn.commit()
188
+ return str(row["id"])
189
+
190
+ def check_and_refill_credits(self, user_id: str) -> int:
191
+ """Check if daily refill is due (24h), apply it, return current balance."""
192
+ sql_check = "SELECT credits, last_refill FROM users WHERE id = %s"
193
+ sql_refill = "UPDATE users SET credits = credits + 500, last_refill = NOW() WHERE id = %s RETURNING credits"
194
+
195
+ with psycopg.connect(self.db_url) as conn:
196
+ with conn.cursor(row_factory=psycopg_dict_row) as cur:
197
+ cur.execute(sql_check, (user_id,))
198
+ user = cur.fetchone()
199
+ if not user: return 0
200
+
201
+ last_refill = user["last_refill"]
202
+ if datetime.now(timezone.utc) - last_refill > timedelta(days=1):
203
+ cur.execute(sql_refill, (user_id,))
204
+ user = cur.fetchone()
205
+ conn.commit()
206
+
207
+ return user["credits"]
208
+
209
+ def deduct_credits(self, user_id: str, amount: int) -> bool:
210
+ """Deduct credits if balance is sufficient."""
211
+ sql = "UPDATE users SET credits = credits - %s WHERE id = %s AND credits >= %s RETURNING credits"
212
+ with psycopg.connect(self.db_url) as conn:
213
+ with conn.cursor() as cur:
214
+ cur.execute(sql, (amount, user_id, amount))
215
+ row = cur.fetchone()
216
+ conn.commit()
217
+ return row is not None
218
+
219
+
220
  # ═══════════════════════════════════════════════════════════════════════════════
221
  # ENUMS & CONSTANTS
222
  # ═══════════════════════════════════════════════════════════════════════════════
 
238
  BIOS_INSIGHT = "bios_insight" # Fine-tuned BIOS-Insight-v1
239
 
240
 
241
+ # Model identifiers (BIOS-Insight fine-tune β€” override with HF_BIOS_MODEL_ID in .env)
242
+ def _bios_insight_model_id() -> str:
243
+ return os.getenv("HF_BIOS_MODEL_ID", "isaaclk907/BIOS-Insight-v1")
244
+
245
+
246
  MODEL_IDS = {
247
  ModelVariant.BASE: "meta-llama/llama-3.3-70b-versatile",
248
+ ModelVariant.BIOS_INSIGHT: _bios_insight_model_id(),
249
  }
250
 
251
  GROQ_MODEL_IDS = {
 
414
 
415
  model_used: str = ""
416
  generation_time_ms: int = 0
417
+ inputs: Optional[BusinessInputs] = None
418
 
419
  def to_dict(self) -> dict:
420
  return {
421
+ "sessionId": self.session_id,
422
+ "businessName": self.business_name,
423
  "industry": self.industry,
424
  "location": self.location,
425
+ "generatedAt": self.generated_at,
426
+ "healthScore": {
427
+ "total": self.health_score,
428
+ "label": self.health_label,
429
+ "dimensions": [
430
+ { "dimension": "revenue_strength", "label": "Revenue", "score": self.health_dimensions.revenue_strength },
431
+ { "dimension": "customer_retention", "label": "Customer", "score": self.health_dimensions.customer_retention },
432
+ { "dimension": "market_position", "label": "Market", "score": self.health_dimensions.market_position },
433
+ { "dimension": "technology_adoption", "label": "Technology", "score": self.health_dimensions.technology_adoption },
434
+ { "dimension": "growth_trajectory", "label": "Growth", "score": self.health_dimensions.growth_trajectory }
435
+ ]
436
+ },
437
+ "weaknesses": [w.to_dict() for w in self.top_3_weaknesses],
438
+ "opportunities": [o.to_dict() for o in self.growth_opportunities],
439
+ "actionItems": [a.to_dict() for a in self.priority_action_items],
440
+ "narrative": self.ai_narrative,
441
+ "benchmarking": self.benchmarking,
442
+ "nextModule": self.next_module,
443
+ "modelUsed": self.model_used,
444
+ "generationTimeMs":self.generation_time_ms,
445
+ "inputs": asdict(self.inputs) if self.inputs else None,
446
+ "pipeline": "bios_controller"
447
  }
448
 
449
  def to_json(self, indent: int = 2) -> str:
 
482
  # Clients initialised lazily
483
  self._groq_client: Any = None
484
  self._hf_client: Any = None
485
+ self._openai_client: Any = None
486
+ self._gemini_client: Any = None
487
  self._anthropic_client: Any = None
488
 
489
  log.info(
 
507
  if self._hf_client is None:
508
  if not HF_AVAILABLE:
509
  raise RuntimeError("huggingface_hub not installed. Run: pip install huggingface_hub")
510
+ api_key = os.getenv("HF_API_KEY") or os.getenv("HUGGINGFACE_API_KEY")
511
  if not api_key:
512
+ raise RuntimeError("Set HF_API_KEY or HUGGINGFACE_API_KEY for Hugging Face inference")
513
+ self._hf_client = InferenceClient(token=api_key.strip())
514
  return self._hf_client
515
 
516
  def _get_openai(self):
 
548
 
549
  def _resolve_route(self) -> tuple[ModelBackend, ModelVariant]:
550
  """Determine which backend + variant to actually use."""
551
+ _hf_token = os.getenv("HF_API_KEY") or os.getenv("HUGGINGFACE_API_KEY")
552
+ if self.bios_insight_ready and HF_AVAILABLE and _hf_token:
553
  return ModelBackend.HF_INFERENCE, ModelVariant.BIOS_INSIGHT
554
  if OPENAI_AVAILABLE and os.getenv("OPENAI_API_KEY"):
555
  return ModelBackend.OPENAI, ModelVariant.BASE
 
613
  {"role": "system", "content": system},
614
  {"role": "user", "content": user},
615
  ]
616
+ try:
617
+ response = client.chat_completion(
618
+ messages=messages,
619
+ model=model,
620
+ max_tokens=self.max_tokens,
621
+ temperature=self.temperature,
622
+ )
623
+ return response.choices[0].message.content, f"hf/{model}"
624
+ except Exception as e:
625
+ # Many uploaded causal LMs only expose text-generation, not chat templates
626
+ log.warning("HF chat_completion failed (%s); trying text_generation", e)
627
+ prompt = f"{system}\n\n{user}"
628
+ text = client.text_generation(
629
+ prompt,
630
+ model=model,
631
+ max_new_tokens=min(self.max_tokens, 2048),
632
+ temperature=self.temperature,
633
+ )
634
+ return text, f"hf/{model}"
635
 
636
  def _infer_openai(self, system: str, user: str) -> tuple[str, str]:
637
  client = self._get_openai()
 
1013
 
1014
  Rules:
1015
  - Be specific with numbers from the data provided
1016
+ - Use the language specified in preferred_language.
1017
+ - If preferred_language is "Both", provide the response in both English and Burmese (e.g., paragraph by paragraph or section by section).
1018
  - Never be generic β€” reference the actual business, industry, and goals
1019
  - Tone: elite advisory, not chatbot small talk"""
1020
 
 
1076
  class NeonDBWriter:
1077
  """
1078
  Persists BIOS diagnosis reports to NeonDB (PostgreSQL) via psycopg v3.
1079
+ Matches the Prisma schema in schema.prisma.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1080
  """
1081
 
1082
+ def __init__(self, database_url: Optional[str] = None, security: Optional[SecurityManager] = None):
1083
+ if not PSYCOPG_AVAILABLE:
1084
+ raise ValueError(
1085
+ "psycopg is not available. Install it with: pip install psycopg[binary]"
1086
+ )
1087
  self.database_url = database_url or os.getenv("DATABASE_URL")
1088
+ self.security = security or SecurityManager()
1089
  if not self.database_url:
1090
  raise ValueError(
1091
  "DATABASE_URL not set. Export it or pass database_url= to NeonDBWriter."
 
1096
  ).replace(
1097
  "postgres+asyncpg://", "postgresql://"
1098
  )
1099
+ self.user_manager = UserManager(self.database_url)
1100
 
1101
+ def save_report(self, report: DiagnosisReport, user_id: Optional[str] = None) -> str:
1102
  """
1103
+ Save a DiagnosisReport into the diagnoses and results tables.
1104
+ Returns the session_id/diagnosis_id.
1105
  """
1106
  d = report.to_dict()
1107
+ inp = report.inputs
1108
+ diagnosis_id = str(uuid.uuid4())
1109
+ session_id = d["sessionId"]
1110
 
1111
+ # 1. Save to diagnoses table
1112
+ sql_diagnosis = """
1113
  INSERT INTO diagnoses (
1114
+ id, user_id, session_id, status,
1115
+ industry, location, years_in_business, monthly_revenue, team_size,
1116
+ ideal_customer, acquisition_channels, clv, repeat_purchase_rate,
1117
+ competitors, unique_selling_point, sales_channels,
1118
+ operational_challenge, pain_point, tech_stack, marketing_channels,
1119
+ marketing_budget, revenue_goals, budget_constraint,
1120
+ tech_readiness, preferred_language, created_at, updated_at
1121
  ) VALUES (
1122
+ %s, %s, %s, %s,
1123
+ %s, %s, %s, %s, %s,
1124
+ %s, %s, %s, %s,
1125
+ %s, %s, %s,
1126
+ %s, %s, %s, %s,
1127
+ %s, %s, %s,
1128
+ %s, %s, NOW(), NOW()
1129
  )
 
 
 
 
 
 
 
 
 
 
 
 
1130
  """
1131
 
1132
+ # Encrypt sensitive fields
1133
+ enc_revenue = self.security.encrypt(str(inp.monthly_revenue))
1134
+ enc_clv = self.security.encrypt(str(inp.avg_customer_lifetime_value))
1135
+ enc_pain_point = self.security.encrypt(inp.biggest_pain_point)
1136
+ enc_mkt_budget = self.security.encrypt(str(inp.monthly_marketing_budget))
1137
+ enc_rev_goals = self.security.encrypt(json.dumps({
1138
+ "3m": inp.goal_3_month,
1139
+ "6m": inp.goal_6_month,
1140
+ "12m": inp.goal_12_month
1141
+ }))
1142
+
1143
+ # Normalize enum values for PostgreSQL
1144
+ norm_industry = inp.industry.replace(" ", "_").replace("&", "").replace("-", "_")
1145
+ if norm_industry == "F_B": norm_industry = "FB" # Special case
1146
+ if norm_industry == "Technology_Startup": norm_industry = "Startup"
1147
+
1148
+ norm_location = inp.location
1149
+ if norm_location not in ["Yangon", "Mandalay", "Naypyidaw"]: norm_location = "Other"
1150
+
1151
+ norm_challenge = inp.operational_challenge.replace(" ", "_")
1152
+ if norm_challenge not in ["Inventory_management", "Customer_service", "Marketing", "Payment_processing"]: norm_challenge = "Other"
1153
+
1154
+ norm_budget = "Moderate_200_500K" # default
1155
+ if "50K" in inp.budget_constraint and "<" in inp.budget_constraint: norm_budget = "Very_tight_lt_50K"
1156
+ elif "50-200K" in inp.budget_constraint: norm_budget = "Tight_50_200K"
1157
+ elif "200-500K" in inp.budget_constraint: norm_budget = "Moderate_200_500K"
1158
+ elif "500K+" in inp.budget_constraint: norm_budget = "Comfortable_500K_plus"
1159
+
1160
+ norm_readiness = inp.tech_readiness.replace(" ", "_")
1161
+ if norm_readiness not in ["Not_ready", "Somewhat_ready", "Very_ready", "Extremely_ready"]: norm_readiness = "Somewhat_ready"
1162
+
1163
+ params_diagnosis = (
1164
+ diagnosis_id, user_id, session_id, 'completed',
1165
+ norm_industry, norm_location, inp.years_in_business, enc_revenue, inp.team_size,
1166
+ inp.target_customer, json.dumps(inp.acquisition_channels), enc_clv, inp.retention_rate,
1167
+ inp.main_competitors, inp.unique_selling_proposition, json.dumps(inp.sales_channels),
1168
+ norm_challenge, enc_pain_point, json.dumps(inp.current_technology), json.dumps(inp.marketing_channels),
1169
+ enc_mkt_budget, enc_rev_goals, norm_budget,
1170
+ norm_readiness, inp.preferred_language
1171
+ )
1172
+
1173
+ # 2. Save to results table
1174
+ sql_result = """
1175
+ INSERT INTO results (
1176
+ id, diagnosis_id, overall_health_score, health_label,
1177
+ dimension_scores, weaknesses, opportunities, action_items,
1178
+ ai_narrative, model_version, meta_data, created_at, updated_at
1179
+ ) VALUES (
1180
+ %s, %s, %s, %s,
1181
+ %s, %s, %s, %s,
1182
+ %s, %s, %s, NOW(), NOW()
1183
+ )
1184
+ """
1185
+
1186
+ params_result = (
1187
+ str(uuid.uuid4()), diagnosis_id, d["healthScore"]["total"], d["healthScore"]["label"],
1188
+ json.dumps(d["healthScore"]["dimensions"]), json.dumps(d["weaknesses"]),
1189
+ json.dumps(d["opportunities"]), json.dumps(d["actionItems"]),
1190
+ self.security.encrypt(d["narrative"]), d["modelUsed"], json.dumps(d["benchmarking"])
1191
+ )
1192
 
1193
  with psycopg.connect(self.database_url) as conn:
1194
  with conn.cursor() as cur:
1195
+ cur.execute(sql_diagnosis, params_diagnosis)
1196
+ cur.execute(sql_result, params_result)
1197
  conn.commit()
1198
 
1199
+ log.info(f"βœ… Report saved to NeonDB | session_id={session_id} | score={d['health_score']}")
1200
+ return session_id
1201
 
1202
  def fetch_report(self, session_id: str) -> Optional[dict]:
1203
+ """Fetch a previously saved report by session_id and decrypt sensitive fields."""
1204
+ sql = """
1205
+ SELECT d.*, r.*
1206
+ FROM diagnoses d
1207
+ JOIN results r ON d.id = r.diagnosis_id
1208
+ WHERE d.session_id = %s
1209
+ """
1210
+ with psycopg.connect(self.database_url, row_factory=psycopg_dict_row) as conn:
1211
  with conn.cursor() as cur:
1212
  cur.execute(sql, (session_id,))
1213
  row = cur.fetchone()
1214
+
1215
+ if row:
1216
+ data = dict(row)
1217
+ # Decrypt sensitive fields
1218
+ data["monthly_revenue"] = self.security.decrypt(data["monthly_revenue"])
1219
+ data["clv"] = self.security.decrypt(data["clv"])
1220
+ data["pain_point"] = self.security.decrypt(data["pain_point"])
1221
+ data["marketing_budget"] = self.security.decrypt(data["marketing_budget"])
1222
+ data["revenue_goals"] = json.loads(self.security.decrypt(data["revenue_goals"]))
1223
+ data["ai_narrative"] = self.security.decrypt(data["ai_narrative"])
1224
+ return data
1225
+ return None
1226
 
1227
  def list_reports(self, limit: int = 20) -> list[dict]:
1228
+ """Return the most recent diagnoses with decrypted business names."""
1229
  sql = "SELECT session_id, business_name, industry, health_score, health_label, status, created_at FROM diagnoses ORDER BY created_at DESC LIMIT %s"
1230
+ with psycopg.connect(self.database_url, row_factory=psycopg_dict_row) as conn:
1231
  with conn.cursor() as cur:
1232
  cur.execute(sql, (limit,))
1233
  rows = cur.fetchall()
1234
+
1235
+ results = []
1236
+ for r in rows:
1237
+ data = dict(r)
1238
+ if "business_name" in data:
1239
+ data["business_name"] = self.security.decrypt(data["business_name"])
1240
+ results.append(data)
1241
+ return results
1242
 
1243
 
1244
  # ═══════════════════════════════════════════════════════════════════════════════
 
1272
  max_tokens: int = 2048,
1273
  database_url: Optional[str] = None,
1274
  save_to_db: bool = True,
1275
+ encryption_key: Optional[str] = None,
1276
  ):
1277
+ self.security = SecurityManager(encryption_key)
1278
  self.router = ModelRouter(
1279
  backend=backend,
1280
  bios_insight_ready=bios_insight_ready,
 
1299
  @property
1300
  def db(self) -> NeonDBWriter:
1301
  if self._db is None:
1302
+ self._db = NeonDBWriter(self._db_url, security=self.security)
1303
  return self._db
1304
 
1305
  # ── Main pipeline ─────────────────────────────────────────────────────────
1306
 
1307
+ def run_diagnosis(self, inputs: BusinessInputs, user_id: Optional[str] = None) -> DiagnosisReport:
1308
  """
1309
  Full Module 1 pipeline.
1310
 
1311
  Args:
1312
  inputs: Completed BusinessInputs with all 24 question answers.
1313
+ user_id: Optional UUID of the authenticated user.
1314
 
1315
  Returns:
1316
  DiagnosisReport with health_score, top_3_weaknesses,
 
1365
  benchmarking = benchmarking,
1366
  model_used = model_used,
1367
  generation_time_ms = t_ms,
1368
+ inputs = inputs,
1369
  )
1370
 
1371
  log.info(f"βœ” Diagnosis complete | score={score} | {t_ms}ms")
 
1373
  # ── Step 8: Save to NeonDB ────────────────────────────────────────────
1374
  if self.save_to_db:
1375
  try:
1376
+ self.db.save_report(report, user_id=user_id)
1377
  except Exception as e:
1378
  log.warning(f"DB save failed (non-fatal): {e}")
1379
 
 
1464
 
1465
 
1466
  if __name__ == "__main__":
1467
+ print("\n" + "=" * 60)
1468
  print(" BIOS β€” Business Idea Operating System")
1469
  print(" BIOS-kernel-v1 Β· Module 1: Business Diagnosis")
1470
+ print("=" * 60 + "\n")
1471
 
1472
  # ── Instantiate controller ────────────────────────────────────────────────
1473
  # Set save_to_db=True and export DATABASE_URL to persist to NeonDB.
 
1481
  report = controller.run_diagnosis(inputs)
1482
 
1483
  # ── Print structured JSON output ──────────────────────────────────────────
1484
+ print("\n" + "-" * 60)
1485
  print(" BIOS DIAGNOSIS REPORT")
1486
+ print("-" * 60)
1487
  print(report.to_json())
1488
 
1489
+ print("\n" + "=" * 60)
1490
  print(f" Health Score : {report.health_score}/100 ({report.health_label})")
1491
  print(f" Session ID : {report.session_id}")
1492
  print(f" Model Used : {report.model_used}")
1493
  print(f" Generated in : {report.generation_time_ms}ms")
1494
+ print("=" * 60 + "\n")