isaaclk907 commited on
Commit
b76c743
Β·
verified Β·
1 Parent(s): 0982063

Add bios_controller.py

Browse files
Files changed (1) hide show
  1. bios_controller.py +1190 -0
bios_controller.py ADDED
@@ -0,0 +1,1190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════════╗
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
+ β•‘ β•‘
10
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
11
+
12
+ Architecture:
13
+ BIOSController
14
+ β”œβ”€β”€ ModelRouter β€” switches between base LLM and BIOS-Insight-v1
15
+ β”œβ”€β”€ DiagnosisEngine β€” processes 24 questions, runs health score formula
16
+ β”œβ”€β”€ InsightGenerator β€” builds structured JSON diagnosis report
17
+ └── NeonDBWriter β€” persists results to PostgreSQL via psycopg
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ 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: Anthropic (pip install anthropic) ────────────────────────────
51
+ try:
52
+ import anthropic
53
+ ANTHROPIC_AVAILABLE = True
54
+ except ImportError:
55
+ ANTHROPIC_AVAILABLE = False
56
+
57
+
58
+ # ═══════════════════════════════════════════════════════════════════════════════
59
+ # LOGGING
60
+ # ═══════════════════════════════════════════════════════════════════════════════
61
+
62
+ logging.basicConfig(
63
+ level=logging.INFO,
64
+ format="%(asctime)s [BIOS-%(levelname)s] %(message)s",
65
+ datefmt="%Y-%m-%d %H:%M:%S",
66
+ )
67
+ log = logging.getLogger("bios.controller")
68
+
69
+
70
+ # ═══════════════════════════════════════════════════════════════════════════════
71
+ # ENUMS & CONSTANTS
72
+ # ═══════════════════════════════════════════════════════════════════════════════
73
+
74
+ class ModelBackend(str, Enum):
75
+ """Supported inference backends."""
76
+ GROQ = "groq" # llama-3.3-70b-versatile via Groq
77
+ HF_INFERENCE = "hf_inference" # HuggingFace Inference API
78
+ ANTHROPIC = "anthropic" # Claude fallback
79
+ LOCAL = "local" # Local transformers pipeline
80
+ MOCK = "mock" # Offline / testing
81
+
82
+
83
+ class ModelVariant(str, Enum):
84
+ """Which model to route to."""
85
+ BASE = "base" # General LLM (llama-3.3-70b)
86
+ BIOS_INSIGHT = "bios_insight" # Fine-tuned BIOS-Insight-v1
87
+
88
+
89
+ # Model identifiers
90
+ MODEL_IDS = {
91
+ ModelVariant.BASE: "meta-llama/llama-3.3-70b-versatile",
92
+ ModelVariant.BIOS_INSIGHT: "BIOS-kernel/BIOS-Insight-v1", # future HF repo
93
+ }
94
+
95
+ GROQ_MODEL_IDS = {
96
+ ModelVariant.BASE: "llama-3.3-70b-versatile",
97
+ ModelVariant.BIOS_INSIGHT: "llama-3.3-70b-versatile", # until HF model is live
98
+ }
99
+
100
+ # Industry benchmarks (Myanmar SME context, values in MMK)
101
+ INDUSTRY_BENCHMARKS: dict[str, dict] = {
102
+ "Gold Shop": {"avg_revenue": 15_000_000, "avg_retention": 60, "avg_clv": 2_000_000, "avg_team": 4, "avg_mkt": 200_000},
103
+ "Fashion": {"avg_revenue": 8_000_000, "avg_retention": 40, "avg_clv": 300_000, "avg_team": 6, "avg_mkt": 500_000},
104
+ "F&B": {"avg_revenue": 10_000_000, "avg_retention": 50, "avg_clv": 150_000, "avg_team": 10, "avg_mkt": 400_000},
105
+ "Cosmetics": {"avg_revenue": 6_000_000, "avg_retention": 45, "avg_clv": 250_000, "avg_team": 5, "avg_mkt": 600_000},
106
+ "Electronics": {"avg_revenue": 20_000_000, "avg_retention": 35, "avg_clv": 800_000, "avg_team": 8, "avg_mkt": 700_000},
107
+ "Other": {"avg_revenue": 5_000_000, "avg_retention": 40, "avg_clv": 200_000, "avg_team": 5, "avg_mkt": 300_000},
108
+ }
109
+
110
+
111
+ # ═══════════════════════════════════════════════════════════════════════════════
112
+ # DATA MODELS
113
+ # ═══════════════════════════════════════════════════════════════════════════════
114
+
115
+ @dataclass
116
+ class BusinessInputs:
117
+ """
118
+ Complete set of 24 diagnostic questions, grouped into 4 sections.
119
+
120
+ All monetary values are in MMK (Myanmar Kyat).
121
+ Percentages are 0–100 (e.g. retention_rate=65 means 65%).
122
+ """
123
+
124
+ # ── Section 1: Business Basics (6 questions) ──────────────────────────────
125
+ business_name: str = "" # Q1
126
+ industry: str = "Other" # Q2 Gold Shop / Fashion / F&B / Cosmetics / Electronics / Other
127
+ location: str = "Yangon" # Q3 Yangon / Mandalay / Naypyidaw / Other
128
+ years_in_business: int = 0 # Q4 0–100
129
+ monthly_revenue: float = 0.0 # Q5 MMK
130
+ team_size: int = 1 # Q6 headcount
131
+
132
+ # ── Section 2: Market & Customers (6 questions) ───────────────────────────
133
+ target_customer: str = "" # Q7 free text
134
+ acquisition_channels: list[str] = field(default_factory=list) # Q8 multi-select
135
+ avg_customer_lifetime_value: float = 0.0 # Q9 MMK
136
+ retention_rate: float = 0.0 # Q10 %
137
+ main_competitors: str = "" # Q11 optional
138
+ unique_selling_proposition: str = "" # Q12
139
+
140
+ # ── Section 3: Operations & Challenges (6 questions) ─────────────────────
141
+ sales_channels: list[str] = field(default_factory=list) # Q13
142
+ operational_challenge: str = "" # Q14
143
+ biggest_pain_point: str = "" # Q15
144
+ current_technology: list[str] = field(default_factory=list) # Q16
145
+ marketing_channels: list[str] = field(default_factory=list) # Q17
146
+ monthly_marketing_budget: float = 0.0 # Q18 MMK
147
+
148
+ # ── Section 4: Goals & Constraints (6 questions) ─────────────────────────
149
+ goal_3_month: float = 0.0 # Q19 MMK
150
+ goal_6_month: float = 0.0 # Q20 MMK
151
+ goal_12_month: float = 0.0 # Q21 MMK
152
+ budget_constraint: str = "Moderate (200-500K)" # Q22
153
+ tech_readiness: str = "Somewhat ready" # Q23
154
+ preferred_language: str = "English" # Q24
155
+
156
+
157
+ @dataclass
158
+ class HealthDimensions:
159
+ """Sub-scores for the five health dimensions (each 0–100)."""
160
+ revenue_strength: int = 0
161
+ customer_retention: int = 0
162
+ market_position: int = 0
163
+ technology_adoption: int = 0
164
+ growth_trajectory: int = 0
165
+
166
+ @property
167
+ def total(self) -> int:
168
+ """
169
+ Official BIOS Health Score formula:
170
+ (Revenue Strength Γ— 20) + (Customer Retention Γ— 20) +
171
+ (Market Position Γ— 20) + (Technology Adoption Γ— 20) +
172
+ (Growth Trajectory Γ— 20)
173
+ Each dimension is 0–100, weight is 20%, so max = 100.
174
+ """
175
+ return round(
176
+ (self.revenue_strength * 0.20) +
177
+ (self.customer_retention * 0.20) +
178
+ (self.market_position * 0.20) +
179
+ (self.technology_adoption * 0.20) +
180
+ (self.growth_trajectory * 0.20)
181
+ )
182
+
183
+ def to_dict(self) -> dict:
184
+ return {
185
+ "revenue_strength": self.revenue_strength,
186
+ "customer_retention": self.customer_retention,
187
+ "market_position": self.market_position,
188
+ "technology_adoption": self.technology_adoption,
189
+ "growth_trajectory": self.growth_trajectory,
190
+ "total": self.total,
191
+ }
192
+
193
+
194
+ @dataclass
195
+ class Weakness:
196
+ rank: int
197
+ dimension: str
198
+ label: str
199
+ your_score: float
200
+ benchmark: float
201
+ gap: float
202
+ severity: str # HIGH / MEDIUM / LOW
203
+ detail: str
204
+
205
+ def to_dict(self) -> dict:
206
+ return asdict(self)
207
+
208
+
209
+ @dataclass
210
+ class Opportunity:
211
+ rank: int
212
+ title: str
213
+ description: str
214
+ expected_impact: str
215
+ difficulty: str # EASY / MEDIUM / HARD
216
+ timeframe: str
217
+ revenue_uplift_mmk: Optional[float] = None
218
+
219
+ def to_dict(self) -> dict:
220
+ return asdict(self)
221
+
222
+
223
+ @dataclass
224
+ class ActionItem:
225
+ priority: int
226
+ action: str
227
+ rationale: str
228
+ urgency_score: float
229
+ impact_score: float
230
+ feasibility_score: float
231
+ composite_score: float
232
+
233
+ def to_dict(self) -> dict:
234
+ return asdict(self)
235
+
236
+
237
+ @dataclass
238
+ class DiagnosisReport:
239
+ """Full Module 1 output β€” the BIOS diagnosis report."""
240
+ session_id: str
241
+ business_name: str
242
+ industry: str
243
+ location: str
244
+ generated_at: str
245
+
246
+ health_score: int
247
+ health_label: str # Critical / Below Average / Fair / Good / Excellent
248
+ health_dimensions: HealthDimensions
249
+
250
+ top_3_weaknesses: list[Weakness]
251
+ growth_opportunities: list[Opportunity]
252
+ priority_action_items: list[ActionItem]
253
+
254
+ ai_narrative: str # BIOS LLM executive summary
255
+ benchmarking: list[dict]
256
+ next_module: str = "Strategy Engine (Module 2)"
257
+
258
+ model_used: str = ""
259
+ generation_time_ms: int = 0
260
+
261
+ def to_dict(self) -> dict:
262
+ return {
263
+ "session_id": self.session_id,
264
+ "business_name": self.business_name,
265
+ "industry": self.industry,
266
+ "location": self.location,
267
+ "generated_at": self.generated_at,
268
+ "health_score": self.health_score,
269
+ "health_label": self.health_label,
270
+ "health_dimensions": self.health_dimensions.to_dict(),
271
+ "top_3_weaknesses": [w.to_dict() for w in self.top_3_weaknesses],
272
+ "growth_opportunities": [o.to_dict() for o in self.growth_opportunities],
273
+ "priority_action_items": [a.to_dict() for a in self.priority_action_items],
274
+ "ai_narrative": self.ai_narrative,
275
+ "benchmarking": self.benchmarking,
276
+ "next_module": self.next_module,
277
+ "model_used": self.model_used,
278
+ "generation_time_ms": self.generation_time_ms,
279
+ }
280
+
281
+ def to_json(self, indent: int = 2) -> str:
282
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
283
+
284
+
285
+ # ═══════════════════════════════════════════════════════════════════════════════
286
+ # MODEL ROUTER
287
+ # ═══════════════════════════════════════════════════════════════════════════════
288
+
289
+ class ModelRouter:
290
+ """
291
+ Routes inference requests to the appropriate backend + model variant.
292
+
293
+ Priority order when calling .infer():
294
+ 1. If BIOS-Insight-v1 is flagged as available β†’ use HF Inference API
295
+ 2. Else use base model via Groq (fastest, free tier)
296
+ 3. Fallback to Anthropic Claude
297
+ 4. Final fallback: MOCK mode (returns structured placeholder)
298
+ """
299
+
300
+ def __init__(
301
+ self,
302
+ backend: ModelBackend = ModelBackend.GROQ,
303
+ variant: ModelVariant = ModelVariant.BASE,
304
+ bios_insight_ready: bool = False,
305
+ temperature: float = 0.3,
306
+ max_tokens: int = 2048,
307
+ ):
308
+ self.backend = backend
309
+ self.variant = variant
310
+ self.bios_insight_ready = bios_insight_ready
311
+ self.temperature = temperature
312
+ self.max_tokens = max_tokens
313
+
314
+ # Clients initialised lazily
315
+ self._groq_client: Any = None
316
+ self._hf_client: Any = None
317
+ self._anthropic_client: Any = None
318
+
319
+ log.info(
320
+ f"ModelRouter initialised | backend={backend.value} "
321
+ f"variant={variant.value} | BIOS-Insight-v1 ready={bios_insight_ready}"
322
+ )
323
+
324
+ # ── Client factories ──────────────────────────────────────────────────────
325
+
326
+ def _get_groq(self):
327
+ if self._groq_client is None:
328
+ if not GROQ_AVAILABLE:
329
+ raise RuntimeError("groq package not installed. Run: pip install groq")
330
+ api_key = os.getenv("GROQ_API_KEY")
331
+ if not api_key:
332
+ raise RuntimeError("GROQ_API_KEY environment variable not set")
333
+ self._groq_client = Groq(api_key=api_key)
334
+ return self._groq_client
335
+
336
+ def _get_hf(self):
337
+ if self._hf_client is None:
338
+ if not HF_AVAILABLE:
339
+ raise RuntimeError("huggingface_hub not installed. Run: pip install huggingface_hub")
340
+ api_key = os.getenv("HF_API_KEY")
341
+ if not api_key:
342
+ raise RuntimeError("HF_API_KEY environment variable not set")
343
+ self._hf_client = InferenceClient(token=api_key)
344
+ return self._hf_client
345
+
346
+ def _get_anthropic(self):
347
+ if self._anthropic_client is None:
348
+ if not ANTHROPIC_AVAILABLE:
349
+ raise RuntimeError("anthropic package not installed. Run: pip install anthropic")
350
+ api_key = os.getenv("ANTHROPIC_API_KEY")
351
+ if not api_key:
352
+ raise RuntimeError("ANTHROPIC_API_KEY not set")
353
+ self._anthropic_client = anthropic.Anthropic(api_key=api_key)
354
+ return self._anthropic_client
355
+
356
+ # ── Routing decision ──────────────────────────────────────────────────────
357
+
358
+ def _resolve_route(self) -> tuple[ModelBackend, ModelVariant]:
359
+ """Determine which backend + variant to actually use."""
360
+ if self.bios_insight_ready and HF_AVAILABLE and os.getenv("HF_API_KEY"):
361
+ return ModelBackend.HF_INFERENCE, ModelVariant.BIOS_INSIGHT
362
+ if GROQ_AVAILABLE and os.getenv("GROQ_API_KEY"):
363
+ return ModelBackend.GROQ, ModelVariant.BASE
364
+ if ANTHROPIC_AVAILABLE and os.getenv("ANTHROPIC_API_KEY"):
365
+ return ModelBackend.ANTHROPIC, ModelVariant.BASE
366
+ return ModelBackend.MOCK, ModelVariant.BASE
367
+
368
+ # ── Inference ─────────────────────────────────────────────────────────────
369
+
370
+ def infer(self, system_prompt: str, user_prompt: str) -> tuple[str, str]:
371
+ """
372
+ Send prompts to the resolved model.
373
+
374
+ Returns:
375
+ (response_text, model_identifier_used)
376
+ """
377
+ backend, variant = _resolve_route(self) if False else self._resolve_route()
378
+ model_id = GROQ_MODEL_IDS.get(variant, GROQ_MODEL_IDS[ModelVariant.BASE])
379
+ log.info(f"Routing β†’ backend={backend.value} model={model_id}")
380
+
381
+ if backend == ModelBackend.GROQ:
382
+ return self._infer_groq(system_prompt, user_prompt, model_id)
383
+
384
+ if backend == ModelBackend.HF_INFERENCE:
385
+ hf_model = MODEL_IDS[ModelVariant.BIOS_INSIGHT]
386
+ return self._infer_hf(system_prompt, user_prompt, hf_model)
387
+
388
+ if backend == ModelBackend.ANTHROPIC:
389
+ return self._infer_anthropic(system_prompt, user_prompt)
390
+
391
+ # MOCK fallback
392
+ return self._mock_response(), "mock/bios-kernel-v1"
393
+
394
+ def _infer_groq(self, system: str, user: str, model: str) -> tuple[str, str]:
395
+ client = self._get_groq()
396
+ response = client.chat.completions.create(
397
+ model=model,
398
+ messages=[
399
+ {"role": "system", "content": system},
400
+ {"role": "user", "content": user},
401
+ ],
402
+ temperature=self.temperature,
403
+ max_tokens=self.max_tokens,
404
+ response_format={"type": "json_object"},
405
+ )
406
+ return response.choices[0].message.content, f"groq/{model}"
407
+
408
+ def _infer_hf(self, system: str, user: str, model: str) -> tuple[str, str]:
409
+ client = self._get_hf()
410
+ messages = [
411
+ {"role": "system", "content": system},
412
+ {"role": "user", "content": user},
413
+ ]
414
+ response = client.chat_completion(
415
+ messages=messages,
416
+ model=model,
417
+ max_tokens=self.max_tokens,
418
+ temperature=self.temperature,
419
+ )
420
+ return response.choices[0].message.content, f"hf/{model}"
421
+
422
+ def _infer_anthropic(self, system: str, user: str) -> tuple[str, str]:
423
+ client = self._get_anthropic()
424
+ message = client.messages.create(
425
+ model="claude-sonnet-4-20250514",
426
+ max_tokens=self.max_tokens,
427
+ system=system,
428
+ messages=[{"role": "user", "content": user}],
429
+ )
430
+ return message.content[0].text, "anthropic/claude-sonnet-4-20250514"
431
+
432
+ def _mock_response(self) -> str:
433
+ """Return a valid JSON mock for offline testing."""
434
+ return json.dumps({
435
+ "narrative": (
436
+ "BIOS analysis complete. Your business shows strong foundational "
437
+ "elements but faces challenges in customer retention and technology "
438
+ "adoption. Prioritise loyalty initiatives and digital tooling to "
439
+ "unlock the next growth tier."
440
+ ),
441
+ "model": "mock",
442
+ })
443
+
444
+
445
+ # ═══════════════════════════════════════════════════════════════════════════════
446
+ # DIAGNOSIS ENGINE (pure scoring logic β€” no LLM required)
447
+ # ═══════════════════════════════════════════════════════════════════════════════
448
+
449
+ class DiagnosisEngine:
450
+ """
451
+ Implements the BIOS Module 1 scoring algorithms.
452
+
453
+ All calculations are deterministic and reproducible β€” the LLM is only
454
+ used to generate the qualitative narrative on top of these numbers.
455
+ """
456
+
457
+ # ── Dimension scorers ─────────────────────────────────────────────────────
458
+
459
+ @staticmethod
460
+ def score_revenue(monthly_revenue: float) -> int:
461
+ thresholds = [
462
+ (50_000_000, 100),
463
+ (20_000_000, 80),
464
+ ( 5_000_000, 60),
465
+ ( 1_000_000, 40),
466
+ ]
467
+ for threshold, score in thresholds:
468
+ if monthly_revenue >= threshold:
469
+ return score
470
+ return 20
471
+
472
+ @staticmethod
473
+ def score_retention(rate: float) -> int:
474
+ thresholds = [(80, 100), (60, 80), (40, 60), (20, 40)]
475
+ for threshold, score in thresholds:
476
+ if rate >= threshold:
477
+ return score
478
+ return 20
479
+
480
+ @staticmethod
481
+ def score_market_position(usp: str, competitors: str) -> int:
482
+ words = len(usp.strip().split())
483
+ base = 20
484
+ if words >= 50: base = 80
485
+ elif words >= 30: base = 60
486
+ elif words >= 15: base = 40
487
+ # Bonus for knowing your competition (+5, capped at 100)
488
+ if competitors and len(competitors.strip()) > 5:
489
+ base = min(100, base + 5)
490
+ return base
491
+
492
+ @staticmethod
493
+ def score_technology(technology: list[str]) -> int:
494
+ tech_lower = [t.lower() for t in technology]
495
+ if not tech_lower or "none" in tech_lower:
496
+ return 10
497
+ advanced = {"erp", "crm", "ai tools", "automation", "bi dashboard"}
498
+ mid_tier = {"pos system", "accounting software", "inventory system"}
499
+ basic = {"spreadsheets", "facebook business suite", "whatsapp business"}
500
+ if any(t in advanced for t in tech_lower): return 100
501
+ if any(t in mid_tier for t in tech_lower): return 60
502
+ if any(t in basic for t in tech_lower): return 30
503
+ return 20
504
+
505
+ @staticmethod
506
+ def score_growth(goal_12: float, current: float) -> int:
507
+ if current <= 0:
508
+ return 40 # can't compute β€” neutral score
509
+ rate = (goal_12 - current) / current * 100
510
+ thresholds = [(50, 100), (30, 80), (10, 60), (0, 40)]
511
+ for threshold, score in thresholds:
512
+ if rate >= threshold:
513
+ return score
514
+ return 20
515
+
516
+ # ── Main scorer ───────────────────────────────────────────────────────────
517
+
518
+ def compute_dimensions(self, inp: BusinessInputs) -> HealthDimensions:
519
+ return HealthDimensions(
520
+ revenue_strength = self.score_revenue(inp.monthly_revenue),
521
+ customer_retention = self.score_retention(inp.retention_rate),
522
+ market_position = self.score_market_position(
523
+ inp.unique_selling_proposition,
524
+ inp.main_competitors),
525
+ technology_adoption = self.score_technology(inp.current_technology),
526
+ growth_trajectory = self.score_growth(
527
+ inp.goal_12_month,
528
+ inp.monthly_revenue),
529
+ )
530
+
531
+ @staticmethod
532
+ def health_label(score: int) -> str:
533
+ if score >= 80: return "Excellent"
534
+ if score >= 65: return "Good"
535
+ if score >= 45: return "Fair"
536
+ if score >= 30: return "Below Average"
537
+ return "Critical"
538
+
539
+ # ── Weakness identification ───────────────────────────────────────────────
540
+
541
+ def identify_weaknesses(
542
+ self,
543
+ inp: BusinessInputs,
544
+ dims: HealthDimensions,
545
+ ) -> list[Weakness]:
546
+ bench = INDUSTRY_BENCHMARKS.get(inp.industry, INDUSTRY_BENCHMARKS["Other"])
547
+ bench_scores = {
548
+ "revenue_strength": self.score_revenue(bench["avg_revenue"]),
549
+ "customer_retention": self.score_retention(bench["avg_retention"]),
550
+ "market_position": 60, # industry-standard expectation
551
+ "technology_adoption": 60,
552
+ "growth_trajectory": 60,
553
+ }
554
+ labels = {
555
+ "revenue_strength": "Monthly Revenue",
556
+ "customer_retention": "Customer Retention",
557
+ "market_position": "Market Differentiation",
558
+ "technology_adoption": "Technology Adoption",
559
+ "growth_trajectory": "Growth Ambition",
560
+ }
561
+ details = {
562
+ "revenue_strength": f"Revenue of {inp.monthly_revenue:,.0f} MMK is significantly below the {inp.industry} industry average.",
563
+ "customer_retention": f"Only {inp.retention_rate:.0f}% repeat purchase rate β€” industry average is {bench['avg_retention']:.0f}%.",
564
+ "market_position": "Your unique selling proposition needs greater clarity and depth to stand out.",
565
+ "technology_adoption": "Low technology adoption is limiting operational efficiency and scalability.",
566
+ "growth_trajectory": "Growth goals are misaligned with current revenue trajectory.",
567
+ }
568
+
569
+ user_scores = {
570
+ "revenue_strength": dims.revenue_strength,
571
+ "customer_retention": dims.customer_retention,
572
+ "market_position": dims.market_position,
573
+ "technology_adoption": dims.technology_adoption,
574
+ "growth_trajectory": dims.growth_trajectory,
575
+ }
576
+
577
+ weaknesses: list[Weakness] = []
578
+ for key, user_score in user_scores.items():
579
+ b_score = bench_scores[key]
580
+ if user_score < b_score * 0.8:
581
+ gap = b_score - user_score
582
+ severity = "HIGH" if gap > 30 else "MEDIUM" if gap > 15 else "LOW"
583
+ weaknesses.append(Weakness(
584
+ rank=0,
585
+ dimension=key,
586
+ label=labels[key],
587
+ your_score=user_score,
588
+ benchmark=b_score,
589
+ gap=round(gap, 1),
590
+ severity=severity,
591
+ detail=details[key],
592
+ ))
593
+
594
+ # Sort: HIGH first, then by gap descending
595
+ sev_order = {"HIGH": 3, "MEDIUM": 2, "LOW": 1}
596
+ weaknesses.sort(key=lambda w: (sev_order[w.severity], w.gap), reverse=True)
597
+ top3 = weaknesses[:3]
598
+ for i, w in enumerate(top3, 1):
599
+ w.rank = i
600
+ return top3
601
+
602
+ # ── Opportunity discovery ─────────────────────────────────────────────────
603
+
604
+ def discover_opportunities(
605
+ self,
606
+ inp: BusinessInputs,
607
+ dims: HealthDimensions,
608
+ ) -> list[Opportunity]:
609
+ bench = INDUSTRY_BENCHMARKS.get(inp.industry, INDUSTRY_BENCHMARKS["Other"])
610
+ opps: list[Opportunity] = []
611
+
612
+ # 1. Revenue growth
613
+ if inp.goal_12_month > inp.monthly_revenue and inp.monthly_revenue > 0:
614
+ pct = (inp.goal_12_month / inp.monthly_revenue - 1) * 100
615
+ opps.append(Opportunity(
616
+ rank=0,
617
+ title="Scale Revenue Toward 12-Month Goal",
618
+ description=f"Bridge the {pct:.0f}% gap to your {inp.goal_12_month:,.0f} MMK annual revenue target.",
619
+ expected_impact=f"+{pct:.0f}% revenue growth",
620
+ difficulty="MEDIUM",
621
+ timeframe="6–12 months",
622
+ revenue_uplift_mmk=inp.goal_12_month - inp.monthly_revenue,
623
+ ))
624
+
625
+ # 2. Retention improvement
626
+ if inp.retention_rate < bench["avg_retention"]:
627
+ gap = bench["avg_retention"] - inp.retention_rate
628
+ monthly_g = (gap / 100) * inp.avg_customer_lifetime_value * max(inp.team_size, 1)
629
+ opps.append(Opportunity(
630
+ rank=0,
631
+ title="Boost Customer Retention Rate",
632
+ description=(
633
+ f"Raise repeat-purchase rate by {gap:.0f}% to match the "
634
+ f"{inp.industry} industry benchmark."
635
+ ),
636
+ expected_impact=f"+{monthly_g:,.0f} MMK estimated monthly revenue",
637
+ difficulty="MEDIUM",
638
+ timeframe="2–3 months",
639
+ revenue_uplift_mmk=monthly_g * 12,
640
+ ))
641
+
642
+ # 3. Channel expansion
643
+ if len(inp.sales_channels) < 3:
644
+ needed = 3 - len(inp.sales_channels)
645
+ opps.append(Opportunity(
646
+ rank=0,
647
+ title=f"Expand to {needed} New Sales Channel{'s' if needed > 1 else ''}",
648
+ description="Diversifying beyond your current channels reduces single-point risk and opens new customer pools.",
649
+ expected_impact="+20–30% customer reach",
650
+ difficulty="EASY",
651
+ timeframe="1–2 months",
652
+ ))
653
+
654
+ # 4. Technology upgrade
655
+ if dims.technology_adoption < 60:
656
+ opps.append(Opportunity(
657
+ rank=0,
658
+ title="Adopt Core Business Technology",
659
+ description="Implementing a CRM or POS system unlocks data-driven decisions and staff efficiency.",
660
+ expected_impact="+15–25% operational efficiency",
661
+ difficulty="MEDIUM",
662
+ timeframe="2–4 weeks",
663
+ ))
664
+
665
+ # 5. Marketing investment
666
+ if inp.monthly_marketing_budget < bench["avg_mkt"] * 0.5:
667
+ opps.append(Opportunity(
668
+ rank=0,
669
+ title="Increase Marketing Investment",
670
+ description=(
671
+ f"Current budget of {inp.monthly_marketing_budget:,.0f} MMK "
672
+ f"is far below the {inp.industry} average of {bench['avg_mkt']:,.0f} MMK."
673
+ ),
674
+ expected_impact="+10–20% new customer acquisition",
675
+ difficulty="EASY",
676
+ timeframe="1 month",
677
+ ))
678
+
679
+ # Rank and cap at 5
680
+ for i, opp in enumerate(opps[:5], 1):
681
+ opp.rank = i
682
+ return opps[:5]
683
+
684
+ # ── Priority action items ─────────────────────────────────────────────────
685
+
686
+ RECOMMENDED_ACTIONS: dict[str, str] = {
687
+ "revenue_strength": "Run a margin audit and introduce 2 high-value upsell products this month.",
688
+ "customer_retention": "Launch a loyalty stamp card and a 30-day follow-up WhatsApp message sequence.",
689
+ "market_position": "Rewrite your USP in one clear sentence and test it in Facebook ad copy.",
690
+ "technology_adoption": "Set up a free CRM (HubSpot or Zoho) and import your customer contact list.",
691
+ "growth_trajectory": "Break your 12-month target into monthly milestones and review weekly.",
692
+ }
693
+
694
+ def rank_priority_actions(
695
+ self,
696
+ weaknesses: list[Weakness],
697
+ inp: BusinessInputs,
698
+ ) -> list[ActionItem]:
699
+ items: list[ActionItem] = []
700
+ sev_urgency = {"HIGH": 85, "MEDIUM": 60, "LOW": 35}
701
+
702
+ for w in weaknesses:
703
+ urgency = float(sev_urgency.get(w.severity, 40))
704
+ impact = min(100.0, w.gap * 1.6)
705
+ feasibility = {"HIGH": 40.0, "MEDIUM": 65.0, "LOW": 80.0}.get(w.severity, 50.0)
706
+ composite = round(urgency * 0.4 + impact * 0.4 + feasibility * 0.2, 1)
707
+
708
+ items.append(ActionItem(
709
+ priority=0,
710
+ action=self.RECOMMENDED_ACTIONS.get(w.dimension, f"Address {w.label} urgently."),
711
+ rationale=w.detail,
712
+ urgency_score=urgency,
713
+ impact_score=round(impact, 1),
714
+ feasibility_score=feasibility,
715
+ composite_score=composite,
716
+ ))
717
+
718
+ items.sort(key=lambda x: x.composite_score, reverse=True)
719
+ for i, item in enumerate(items, 1):
720
+ item.priority = i
721
+ return items
722
+
723
+ # ── Benchmarking table ────────────────────────────────────────────────────
724
+
725
+ def build_benchmarking(
726
+ self,
727
+ inp: BusinessInputs,
728
+ ) -> list[dict]:
729
+ bench = INDUSTRY_BENCHMARKS.get(inp.industry, INDUSTRY_BENCHMARKS["Other"])
730
+
731
+ def status(val: float, avg: float) -> str:
732
+ if val >= avg * 1.10: return "ABOVE"
733
+ if val <= avg * 0.90: return "BELOW"
734
+ return "AT"
735
+
736
+ return [
737
+ {"metric": "Monthly Revenue", "your_value": inp.monthly_revenue, "industry_avg": bench["avg_revenue"], "unit": "MMK", "status": status(inp.monthly_revenue, bench["avg_revenue"])},
738
+ {"metric": "Customer Retention Rate", "your_value": inp.retention_rate, "industry_avg": bench["avg_retention"], "unit": "%", "status": status(inp.retention_rate, bench["avg_retention"])},
739
+ {"metric": "Avg Customer Lifetime Val","your_value": inp.avg_customer_lifetime_value, "industry_avg": bench["avg_clv"], "unit": "MMK", "status": status(inp.avg_customer_lifetime_value, bench["avg_clv"])},
740
+ {"metric": "Marketing Budget", "your_value": inp.monthly_marketing_budget, "industry_avg": bench["avg_mkt"], "unit": "MMK", "status": status(inp.monthly_marketing_budget, bench["avg_mkt"])},
741
+ {"metric": "Team Size", "your_value": float(inp.team_size), "industry_avg": float(bench["avg_team"]),"unit": "ppl", "status": status(inp.team_size, bench["avg_team"])},
742
+ ]
743
+
744
+
745
+ # ═══════════════════════════════════════════════════════════════════════════════
746
+ # INSIGHT GENERATOR (builds the LLM prompt and parses the response)
747
+ # ═══════════════════════════════════════════════════════════════════════════════
748
+
749
+ class InsightGenerator:
750
+ """
751
+ Constructs structured prompts for the BIOS LLM and parses its JSON output
752
+ into the qualitative `ai_narrative` field of the DiagnosisReport.
753
+ """
754
+
755
+ SYSTEM_PROMPT = """You are BIOS β€” the Business Idea Operating System. You are the elite AI advisor for Myanmar SMEs, Gold Shops, and ambitious entrepreneurs across Southeast Asia.
756
+
757
+ Your personality: professional, precise, encouraging, and bold β€” like a McKinsey partner who speaks to founders, not just analysts. You use the Dark & Gold luxury tone: every word carries weight, every recommendation is actionable.
758
+
759
+ You always respond in valid JSON with this exact structure:
760
+ {
761
+ "narrative": "<3-paragraph executive summary in the user's preferred language>",
762
+ "headline_insight": "<one powerful sentence that captures the core finding>"
763
+ }
764
+
765
+ Rules:
766
+ - Be specific with numbers from the data provided
767
+ - Use the language specified in preferred_language
768
+ - Never be generic β€” reference the actual business, industry, and goals
769
+ - Tone: elite advisory, not chatbot small talk"""
770
+
771
+ def build_user_prompt(self, inp: BusinessInputs, dims: HealthDimensions, weaknesses: list[Weakness], opps: list[Opportunity]) -> str:
772
+ weak_lines = "\n".join(
773
+ f" {w.rank}. {w.label} β€” score {w.your_score:.0f} vs benchmark {w.benchmark:.0f} | severity: {w.severity}"
774
+ for w in weaknesses
775
+ )
776
+ opp_lines = "\n".join(
777
+ f" {o.rank}. {o.title}: {o.expected_impact} ({o.timeframe})"
778
+ for o in opps[:3]
779
+ )
780
+
781
+ return f"""BIOS DIAGNOSIS DATA β€” analyse and generate insights.
782
+
783
+ Business: {inp.business_name}
784
+ Industry: {inp.industry} | Location: {inp.location}
785
+ Years Operating: {inp.years_in_business} | Team: {inp.team_size} people
786
+ Monthly Revenue: {inp.monthly_revenue:,.0f} MMK
787
+ Retention Rate: {inp.retention_rate:.0f}%
788
+ Monthly Marketing Budget: {inp.monthly_marketing_budget:,.0f} MMK
789
+ Preferred Language: {inp.preferred_language}
790
+
791
+ HEALTH SCORE: {dims.total}/100 β€” {DiagnosisEngine.health_label(dims.total)}
792
+ Revenue Strength: {dims.revenue_strength}
793
+ Customer Retention: {dims.customer_retention}
794
+ Market Position: {dims.market_position}
795
+ Technology Adoption: {dims.technology_adoption}
796
+ Growth Trajectory: {dims.growth_trajectory}
797
+
798
+ TOP WEAKNESSES:
799
+ {weak_lines}
800
+
801
+ TOP OPPORTUNITIES:
802
+ {opp_lines}
803
+
804
+ 12-Month Revenue Goal: {inp.goal_12_month:,.0f} MMK
805
+ Unique Value Proposition: "{inp.unique_selling_proposition}"
806
+ Biggest Pain Point: "{inp.biggest_pain_point}"
807
+
808
+ Generate the executive narrative JSON now."""
809
+
810
+ def parse_narrative(self, raw: str) -> str:
811
+ """Extract narrative from LLM JSON response, with fallback."""
812
+ try:
813
+ # Strip markdown fences if present
814
+ clean = re.sub(r"```(?:json)?\s*", "", raw).strip().rstrip("```").strip()
815
+ data = json.loads(clean)
816
+ return data.get("narrative", raw)
817
+ except (json.JSONDecodeError, AttributeError):
818
+ # Return raw text if not parseable
819
+ return raw.strip()
820
+
821
+
822
+ # ═══════════════════════════════════════════════════════════════════════════════
823
+ # NEON DB WRITER
824
+ # ═══════════════════════════════════════════════════════════════════════════════
825
+
826
+ class NeonDBWriter:
827
+ """
828
+ Persists BIOS diagnosis reports to NeonDB (PostgreSQL) via psycopg v3.
829
+
830
+ Required table (run schema_auth.sql first):
831
+ CREATE TABLE IF NOT EXISTS diagnoses (
832
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
833
+ session_id VARCHAR(255) UNIQUE NOT NULL,
834
+ business_name VARCHAR(255),
835
+ industry VARCHAR(100),
836
+ location VARCHAR(100),
837
+ health_score INTEGER,
838
+ health_label VARCHAR(50),
839
+ health_dimensions JSONB,
840
+ top_3_weaknesses JSONB,
841
+ growth_opportunities JSONB,
842
+ priority_action_items JSONB,
843
+ ai_narrative TEXT,
844
+ benchmarking JSONB,
845
+ model_used VARCHAR(255),
846
+ generation_time_ms INTEGER,
847
+ status VARCHAR(50) DEFAULT 'COMPLETED',
848
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
849
+ );
850
+ """
851
+
852
+ def __init__(self, database_url: Optional[str] = None):
853
+ self.database_url = database_url or os.getenv("DATABASE_URL")
854
+ if not self.database_url:
855
+ raise ValueError(
856
+ "DATABASE_URL not set. Export it or pass database_url= to NeonDBWriter."
857
+ )
858
+ # Normalise asyncpg URL to psycopg URL
859
+ self.database_url = self.database_url.replace(
860
+ "postgresql+asyncpg://", "postgresql://"
861
+ ).replace(
862
+ "postgres+asyncpg://", "postgresql://"
863
+ )
864
+
865
+ def save_report(self, report: DiagnosisReport) -> str:
866
+ """
867
+ Upsert a DiagnosisReport into the diagnoses table.
868
+ Returns the session_id of the saved record.
869
+ """
870
+ d = report.to_dict()
871
+
872
+ sql = """
873
+ INSERT INTO diagnoses (
874
+ session_id, business_name, industry, location,
875
+ health_score, health_label, health_dimensions,
876
+ top_3_weaknesses, growth_opportunities, priority_action_items,
877
+ ai_narrative, benchmarking, model_used, generation_time_ms,
878
+ status, created_at
879
+ ) VALUES (
880
+ %(session_id)s, %(business_name)s, %(industry)s, %(location)s,
881
+ %(health_score)s, %(health_label)s, %(health_dimensions)s,
882
+ %(top_3_weaknesses)s, %(growth_opportunities)s, %(priority_action_items)s,
883
+ %(ai_narrative)s, %(benchmarking)s, %(model_used)s, %(generation_time_ms)s,
884
+ 'COMPLETED', NOW()
885
+ )
886
+ ON CONFLICT (session_id) DO UPDATE SET
887
+ health_score = EXCLUDED.health_score,
888
+ health_label = EXCLUDED.health_label,
889
+ health_dimensions = EXCLUDED.health_dimensions,
890
+ top_3_weaknesses = EXCLUDED.top_3_weaknesses,
891
+ growth_opportunities = EXCLUDED.growth_opportunities,
892
+ priority_action_items = EXCLUDED.priority_action_items,
893
+ ai_narrative = EXCLUDED.ai_narrative,
894
+ benchmarking = EXCLUDED.benchmarking,
895
+ model_used = EXCLUDED.model_used,
896
+ generation_time_ms = EXCLUDED.generation_time_ms,
897
+ status = 'COMPLETED'
898
+ """
899
+
900
+ params = {
901
+ "session_id": d["session_id"],
902
+ "business_name": d["business_name"],
903
+ "industry": d["industry"],
904
+ "location": d["location"],
905
+ "health_score": d["health_score"],
906
+ "health_label": d["health_label"],
907
+ "health_dimensions": json.dumps(d["health_dimensions"]),
908
+ "top_3_weaknesses": json.dumps(d["top_3_weaknesses"]),
909
+ "growth_opportunities": json.dumps(d["growth_opportunities"]),
910
+ "priority_action_items": json.dumps(d["priority_action_items"]),
911
+ "ai_narrative": d["ai_narrative"],
912
+ "benchmarking": json.dumps(d["benchmarking"]),
913
+ "model_used": d["model_used"],
914
+ "generation_time_ms": d["generation_time_ms"],
915
+ }
916
+
917
+ with psycopg.connect(self.database_url) as conn:
918
+ with conn.cursor() as cur:
919
+ cur.execute(sql, params)
920
+ conn.commit()
921
+
922
+ log.info(f"βœ… Report saved to NeonDB | session_id={report.session_id} | score={report.health_score}")
923
+ return report.session_id
924
+
925
+ def fetch_report(self, session_id: str) -> Optional[dict]:
926
+ """Fetch a previously saved report by session_id."""
927
+ sql = "SELECT * FROM diagnoses WHERE session_id = %s"
928
+ with psycopg.connect(self.database_url, row_factory=dict_row) as conn:
929
+ with conn.cursor() as cur:
930
+ cur.execute(sql, (session_id,))
931
+ row = cur.fetchone()
932
+ return dict(row) if row else None
933
+
934
+ def list_reports(self, limit: int = 20) -> list[dict]:
935
+ """Return the most recent diagnoses."""
936
+ sql = "SELECT session_id, business_name, industry, health_score, health_label, status, created_at FROM diagnoses ORDER BY created_at DESC LIMIT %s"
937
+ with psycopg.connect(self.database_url, row_factory=dict_row) as conn:
938
+ with conn.cursor() as cur:
939
+ cur.execute(sql, (limit,))
940
+ rows = cur.fetchall()
941
+ return [dict(r) for r in rows]
942
+
943
+
944
+ # ═══════════════════════════════════════════════════════════════════════════════
945
+ # BIOS CONTROLLER β€” the main entry point
946
+ # ═══════════════════════════════════════════════════════════════════════════════
947
+
948
+ class BIOSController:
949
+ """
950
+ BIOS-kernel-v1 Β· Business Idea Operating System Β· Module 1 Controller
951
+
952
+ Orchestrates the full diagnosis pipeline:
953
+ inputs β†’ scoring β†’ LLM narrative β†’ structured report β†’ NeonDB
954
+
955
+ Usage:
956
+ controller = BIOSController()
957
+ report = controller.run_diagnosis(inputs)
958
+ print(report.to_json())
959
+
960
+ To use the fine-tuned BIOS-Insight-v1 when it becomes available on HF:
961
+ controller = BIOSController(bios_insight_ready=True)
962
+ """
963
+
964
+ VERSION = "1.0.0"
965
+ KERNEL = "BIOS-kernel-v1"
966
+
967
+ def __init__(
968
+ self,
969
+ backend: ModelBackend = ModelBackend.GROQ,
970
+ bios_insight_ready: bool = False,
971
+ temperature: float = 0.3,
972
+ max_tokens: int = 2048,
973
+ database_url: Optional[str] = None,
974
+ save_to_db: bool = True,
975
+ ):
976
+ self.router = ModelRouter(
977
+ backend=backend,
978
+ bios_insight_ready=bios_insight_ready,
979
+ temperature=temperature,
980
+ max_tokens=max_tokens,
981
+ )
982
+ self.engine = DiagnosisEngine()
983
+ self.generator = InsightGenerator()
984
+ self.save_to_db = save_to_db
985
+
986
+ # DB writer β€” lazy init so missing DB URL doesn't break non-DB usage
987
+ self._db: Optional[NeonDBWriter] = None
988
+ self._db_url = database_url
989
+
990
+ log.info(
991
+ f"╔═══════════════════════════════════════════╗\n"
992
+ f" BIOS Controller v{self.VERSION} Β· {self.KERNEL}\n"
993
+ f" backend={backend.value} | save_db={save_to_db}\n"
994
+ f"β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"
995
+ )
996
+
997
+ @property
998
+ def db(self) -> NeonDBWriter:
999
+ if self._db is None:
1000
+ self._db = NeonDBWriter(self._db_url)
1001
+ return self._db
1002
+
1003
+ # ── Main pipeline ─────────────────────────────────────────────────────────
1004
+
1005
+ def run_diagnosis(self, inputs: BusinessInputs) -> DiagnosisReport:
1006
+ """
1007
+ Full Module 1 pipeline.
1008
+
1009
+ Args:
1010
+ inputs: Completed BusinessInputs with all 24 question answers.
1011
+
1012
+ Returns:
1013
+ DiagnosisReport with health_score, top_3_weaknesses,
1014
+ growth_opportunities, priority_action_items, and ai_narrative.
1015
+ """
1016
+ t_start = time.perf_counter()
1017
+ session_id = str(uuid.uuid4())
1018
+
1019
+ log.info(f"β–Ά Starting BIOS diagnosis | business='{inputs.business_name}' | session={session_id}")
1020
+
1021
+ # ── Step 1: Compute health dimensions and score ──────────────────────
1022
+ dims = self.engine.compute_dimensions(inputs)
1023
+ score = dims.total
1024
+ label = self.engine.health_label(score)
1025
+ log.info(f" Health Score: {score}/100 ({label})")
1026
+ log.info(f" Dimensions: Rev={dims.revenue_strength} Ret={dims.customer_retention} Mkt={dims.market_position} Tech={dims.technology_adoption} Grow={dims.growth_trajectory}")
1027
+
1028
+ # ── Step 2: Identify weaknesses ──────────────────────────────────────
1029
+ weaknesses = self.engine.identify_weaknesses(inputs, dims)
1030
+ log.info(f" Weaknesses identified: {[w.label for w in weaknesses]}")
1031
+
1032
+ # ── Step 3: Discover opportunities ───────────────────────────────────
1033
+ opportunities = self.engine.discover_opportunities(inputs, dims)
1034
+ log.info(f" Opportunities found: {len(opportunities)}")
1035
+
1036
+ # ── Step 4: Rank priority actions ────────────────────────────────────
1037
+ actions = self.engine.rank_priority_actions(weaknesses, inputs)
1038
+
1039
+ # ── Step 5: Build benchmarking table ─────────────────────────────────
1040
+ benchmarking = self.engine.build_benchmarking(inputs)
1041
+
1042
+ # ── Step 6: Generate AI narrative via LLM ────────────────────────────
1043
+ narrative, model_used = self._generate_narrative(inputs, dims, weaknesses, opportunities)
1044
+ log.info(f" Narrative generated | model={model_used}")
1045
+
1046
+ t_ms = int((time.perf_counter() - t_start) * 1000)
1047
+
1048
+ # ── Step 7: Assemble report ───────────────────────────────────────────
1049
+ report = DiagnosisReport(
1050
+ session_id = session_id,
1051
+ business_name = inputs.business_name,
1052
+ industry = inputs.industry,
1053
+ location = inputs.location,
1054
+ generated_at = datetime.now(timezone.utc).isoformat(),
1055
+ health_score = score,
1056
+ health_label = label,
1057
+ health_dimensions = dims,
1058
+ top_3_weaknesses = weaknesses,
1059
+ growth_opportunities = opportunities,
1060
+ priority_action_items= actions,
1061
+ ai_narrative = narrative,
1062
+ benchmarking = benchmarking,
1063
+ model_used = model_used,
1064
+ generation_time_ms = t_ms,
1065
+ )
1066
+
1067
+ log.info(f"βœ” Diagnosis complete | score={score} | {t_ms}ms")
1068
+
1069
+ # ── Step 8: Save to NeonDB ────────────────────────────────────────────
1070
+ if self.save_to_db:
1071
+ try:
1072
+ self.db.save_report(report)
1073
+ except Exception as e:
1074
+ log.warning(f"DB save failed (non-fatal): {e}")
1075
+
1076
+ return report
1077
+
1078
+ def _generate_narrative(
1079
+ self,
1080
+ inp: BusinessInputs,
1081
+ dims: HealthDimensions,
1082
+ weak: list[Weakness],
1083
+ opps: list[Opportunity],
1084
+ ) -> tuple[str, str]:
1085
+ """Call the LLM and return (narrative_text, model_identifier)."""
1086
+ system = self.generator.SYSTEM_PROMPT
1087
+ user = self.generator.build_user_prompt(inp, dims, weak, opps)
1088
+ try:
1089
+ raw, model_id = self.router.infer(system, user)
1090
+ narrative = self.generator.parse_narrative(raw)
1091
+ return narrative, model_id
1092
+ except Exception as e:
1093
+ log.warning(f"LLM call failed, using fallback narrative: {e}")
1094
+ fallback = (
1095
+ f"{inp.business_name} received a BIOS Health Score of {dims.total}/100 ({self.engine.health_label(dims.total)}). "
1096
+ f"Key areas for immediate attention: {', '.join(w.label for w in weak[:2])}. "
1097
+ f"Top opportunity: {opps[0].title if opps else 'revenue diversification'}."
1098
+ )
1099
+ return fallback, "fallback/static"
1100
+
1101
+ # ── Convenience helpers ───────────────────────────────────────────────────
1102
+
1103
+ def switch_to_bios_insight(self):
1104
+ """Activate BIOS-Insight-v1 once it is published on HuggingFace."""
1105
+ self.router.bios_insight_ready = True
1106
+ self.router.variant = ModelVariant.BIOS_INSIGHT
1107
+ log.info("🌟 Switched to BIOS-Insight-v1 (fine-tuned model)")
1108
+
1109
+ def switch_to_base(self):
1110
+ """Revert to base llama-3.3-70b model."""
1111
+ self.router.bios_insight_ready = False
1112
+ self.router.variant = ModelVariant.BASE
1113
+ log.info("Reverted to base model (llama-3.3-70b)")
1114
+
1115
+ def get_report(self, session_id: str) -> Optional[dict]:
1116
+ """Retrieve a saved report from NeonDB."""
1117
+ return self.db.fetch_report(session_id)
1118
+
1119
+ def list_reports(self, limit: int = 20) -> list[dict]:
1120
+ """List recent diagnosis reports from NeonDB."""
1121
+ return self.db.list_reports(limit)
1122
+
1123
+
1124
+ # ═══════════════════════════════════════════════════════════════════════════════
1125
+ # CLI / DEMO RUNNER
1126
+ # ═══════════════════════════════════════════════════════════════════════════════
1127
+
1128
+ def _demo_inputs() -> BusinessInputs:
1129
+ """Sample Gold Shop business for demonstration."""
1130
+ return BusinessInputs(
1131
+ # Section 1
1132
+ business_name = "Shwe Zin Gold & Jewellery",
1133
+ industry = "Gold Shop",
1134
+ location = "Yangon",
1135
+ years_in_business = 7,
1136
+ monthly_revenue = 4_200_000,
1137
+ team_size = 3,
1138
+ # Section 2
1139
+ target_customer = "Middle-income families aged 30–55 in Yangon, buying gold for investment and gifting during festivals.",
1140
+ acquisition_channels = ["Word-of-mouth", "Facebook", "Walk-in"],
1141
+ avg_customer_lifetime_value = 350_000,
1142
+ retention_rate = 28.0,
1143
+ main_competitors = "Dagon Gold, KBZ Gems",
1144
+ unique_selling_proposition = "We sell certified 99.9% pure gold at transparent prices with a 10-year buyback guarantee.",
1145
+ # Section 3
1146
+ sales_channels = ["Physical Store", "Facebook"],
1147
+ operational_challenge = "Inventory management",
1148
+ biggest_pain_point = "Customers don't come back after the first purchase β€” we have no system to follow up.",
1149
+ current_technology = ["Spreadsheets"],
1150
+ marketing_channels = ["Facebook", "Word-of-mouth"],
1151
+ monthly_marketing_budget= 80_000,
1152
+ # Section 4
1153
+ goal_3_month = 5_500_000,
1154
+ goal_6_month = 7_000_000,
1155
+ goal_12_month = 12_000_000,
1156
+ budget_constraint = "Tight (50-200K)",
1157
+ tech_readiness = "Somewhat ready",
1158
+ preferred_language = "English",
1159
+ )
1160
+
1161
+
1162
+ if __name__ == "__main__":
1163
+ print("\n" + "═" * 60)
1164
+ print(" BIOS β€” Business Idea Operating System")
1165
+ print(" BIOS-kernel-v1 Β· Module 1: Business Diagnosis")
1166
+ print("═" * 60 + "\n")
1167
+
1168
+ # ── Instantiate controller ────────────────────────────────────────────────
1169
+ # Set save_to_db=True and export DATABASE_URL to persist to NeonDB.
1170
+ controller = BIOSController(
1171
+ backend = ModelBackend.GROQ,
1172
+ save_to_db = bool(os.getenv("DATABASE_URL")),
1173
+ )
1174
+
1175
+ # ── Run diagnosis ─────────────────────────────────────────────────────────
1176
+ inputs = _demo_inputs()
1177
+ report = controller.run_diagnosis(inputs)
1178
+
1179
+ # ── Print structured JSON output ──────────────────────────────────────────
1180
+ print("\n" + "─" * 60)
1181
+ print(" BIOS DIAGNOSIS REPORT")
1182
+ print("─" * 60)
1183
+ print(report.to_json())
1184
+
1185
+ print("\n" + "═" * 60)
1186
+ print(f" Health Score : {report.health_score}/100 ({report.health_label})")
1187
+ print(f" Session ID : {report.session_id}")
1188
+ print(f" Model Used : {report.model_used}")
1189
+ print(f" Generated in : {report.generation_time_ms}ms")
1190
+ print("═" * 60 + "\n")