Peter Mutwiri commited on
Commit
7fa7313
Β·
1 Parent(s): 307409b

refactored data ingestion cycle

Browse files
app/hybrid_entity_detector.py CHANGED
@@ -1,10 +1,9 @@
1
  # app/hybrid_entity_detector.py
2
- import asyncio
3
  from typing import Tuple
4
  import pandas as pd
5
  from app.entity_detector import detect_entity_type as rule_based_detect
6
  from app.service.ai_service import ai_service
7
- import logging
8
 
9
  logger = logging.getLogger(__name__)
10
 
@@ -13,31 +12,42 @@ def hybrid_detect_entity_type(org_id: str, df: pd.DataFrame, filename: str) -> T
13
  Hybrid detection: Rule-based (fast) β†’ LLM fallback (accurate).
14
  Returns: (entity_type, confidence, is_confident)
15
  """
16
- # 1. Rule-based first (ALWAYS runs, ~10ms)
17
  entity_type, confidence = rule_based_detect(df)
18
  logger.info(f"[hybrid] Rule-based: {entity_type} ({confidence:.2f})")
19
 
20
- # 2. If confident, return IMMEDIATELY (no LLM wait)
21
  if confidence > 0.75:
 
22
  return entity_type, confidence, True
23
 
24
- # 3. LLM fallback with ERROR HANDLING (not timeout)
25
  try:
26
- # CRITICAL: Check if LLM is ready (no waiting)
 
 
27
  if not ai_service.llm.is_loaded:
28
- logger.warning("LLM not ready, skipping AI fallback")
29
  return entity_type, confidence, False
30
 
31
- # This should be FAST (< 2s) if model is loaded
32
  columns = list(df.columns)
33
  llm_result = ai_service.detect_entity_type(org_id, columns, filename)
34
 
35
- if llm_result["confidence"] > confidence:
36
- logger.info(f"[hybrid] LLM used: {llm_result['entity_type']}")
37
- return llm_result["entity_type"], llm_result["confidence"], True
 
 
 
 
 
 
38
 
 
39
  return entity_type, confidence, False
40
 
41
  except Exception as e:
42
- logger.warning(f"LLM fallback failed: {e}, using rule-based")
 
43
  return entity_type, confidence, False
 
1
  # app/hybrid_entity_detector.py
2
+ import logging
3
  from typing import Tuple
4
  import pandas as pd
5
  from app.entity_detector import detect_entity_type as rule_based_detect
6
  from app.service.ai_service import ai_service
 
7
 
8
  logger = logging.getLogger(__name__)
9
 
 
12
  Hybrid detection: Rule-based (fast) β†’ LLM fallback (accurate).
13
  Returns: (entity_type, confidence, is_confident)
14
  """
15
+ # 1. Rule-based first (ALWAYS runs)
16
  entity_type, confidence = rule_based_detect(df)
17
  logger.info(f"[hybrid] Rule-based: {entity_type} ({confidence:.2f})")
18
 
19
+ # 2. If confident, return IMMEDIATELY
20
  if confidence > 0.75:
21
+ logger.info(f"[hybrid] βœ“ Confident enough, skipping LLM")
22
  return entity_type, confidence, True
23
 
24
+ # 3. LLM fallback with BULLETPROOF error handling
25
  try:
26
+ logger.info(f"[hybrid] β†’ LLM fallback needed (confidence < 0.75)")
27
+
28
+ # Check if LLM is ready (FAIL FAST)
29
  if not ai_service.llm.is_loaded:
30
+ logger.warning("[hybrid] ⚠️ LLM not ready yet")
31
  return entity_type, confidence, False
32
 
33
+ logger.info(f"[hybrid] β†’ Calling AI service...")
34
  columns = list(df.columns)
35
  llm_result = ai_service.detect_entity_type(org_id, columns, filename)
36
 
37
+ logger.info(f"[hybrid] ← AI service returned: {llm_result}")
38
+
39
+ # Extract values safely
40
+ llm_entity = llm_result.get("entity_type", entity_type).upper()
41
+ llm_confidence = float(llm_result.get("confidence", 0.0))
42
+
43
+ if llm_confidence > confidence:
44
+ logger.info(f"[hybrid] βœ“ Using LLM result: {llm_entity}")
45
+ return llm_entity, llm_confidence, True
46
 
47
+ logger.info(f"[hybrid] β†’ Rule-based retained: {entity_type}")
48
  return entity_type, confidence, False
49
 
50
  except Exception as e:
51
+ logger.error(f"[hybrid] ❌ CRASH: {e}", exc_info=True)
52
+ # βœ… NEVER crash the pipeline
53
  return entity_type, confidence, False
app/service/ai_service.py CHANGED
@@ -4,11 +4,12 @@ import logging
4
  from app.deps import get_vector_db
5
  from app.service.llm_service import llm_service
6
  from app.service.embedding_service import embedder
 
7
 
8
  logger = logging.getLogger(__name__)
 
9
  class AIService:
10
  def __init__(self):
11
- # Initialize vector DB with error handling
12
  try:
13
  self.vector_db = get_vector_db()
14
  self.vss_available = True
@@ -17,79 +18,91 @@ class AIService:
17
  logger.warning(f"⚠️ Vector DB unavailable: {e}")
18
  self.vector_db = None
19
  self.vss_available = False
20
-
21
- # Initialize LLM and embedder (always available)
22
  self.llm = llm_service
23
  self.embedder = embedder
24
- logger.info("βœ… AI Service initialized (VSS: {})".format(
25
- "ENABLED" if self.vss_available else "DISABLED"
26
- ))
27
  def detect_entity_type(self, org_id: str, columns: list[str], filename: str) -> dict:
28
- """Detect entity type - per-org cache"""
29
  columns_str = ",".join(columns)
30
 
31
- # Check vector cache for this org
32
- cached = self.vector_db.execute("""
33
- SELECT entity_type FROM vector_store.embeddings
34
- WHERE org_id = ? AND content = ?
35
- ORDER BY created_at DESC LIMIT 1
36
- """, [org_id, columns_str]).fetchone()
 
 
 
 
 
37
 
38
- if cached:
39
- return {"entity_type": cached[0], "confidence": 0.99, "cached": True}
40
 
41
- # AI detection
42
- prompt = f"""Columns: {columns_str}\nFilename: {filename}\nClassify as: sales,inventory,customer,product. JSON: {{"entity_type":"...","confidence":0.95}}"""
43
- response = self.llm.generate(prompt, max_tokens=100)
44
- result = json.loads(response)
45
-
46
- # Cache for this org
47
- embedding = self.embedder.generate(columns_str)
48
- self.vector_db.execute("""
49
- INSERT INTO vector_store.embeddings (org_id, content, embedding, entity_type)
50
- VALUES (?, ?, ?, ?)
51
- """, [org_id, columns_str, embedding, result["entity_type"]])
52
-
53
- return result
54
-
55
- def generate_sql(self, org_id: str, question: str, entity_type: str, schema: dict) -> str:
56
- """Generate SQL for specific org"""
57
- prompt = f"Org: {org_id}\nSchema: {json.dumps(schema)}\nEntity: {entity_type}\nQuestion: {question}\nDuckDB SQL only:"
58
- sql = self.llm.generate(prompt, max_tokens=300)
59
- return sql.strip()
60
-
61
- # app/service/ai_service.py – Update similarity_search method
62
 
63
- def similarity_search(self, org_id: str, query: str, entity_type: str, top_k: int = 5) -> list[dict]:
64
- """
65
- Search within org's vector history.
66
- Returns empty list if VSS is unavailable or query fails.
67
- """
68
-
69
- # Check if VSS is available (set in __init__)
70
- if not self.vss_available or self.vector_db is None:
71
- logger.warning(f"VSS unavailable for {org_id}, returning empty results")
72
- return []
73
-
74
- try:
75
- # Generate embedding for query
76
- query_vector = self.embedder.generate(query)
77
-
78
- # Execute similarity search
79
- results = self.vector_db.execute("""
80
- SELECT id, content, entity_type, array_cosine_similarity(embedding, ?::FLOAT[384]) as score
81
- FROM vector_store.embeddings
82
- WHERE org_id = ? AND entity_type = ?
83
- ORDER BY score DESC
84
- LIMIT ?
85
- """, [query_vector, org_id, entity_type, top_k]).fetchall()
86
 
87
- # Convert to list of dicts
88
- return [{"id": r[0], "content": r[1], "entity_type": r[2], "score": r[3]} for r in results]
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  except Exception as e:
91
- # Log error and return empty list on search failure
92
- logger.error(f"VSS search failed for {org_id}/{entity_type}: {e}")
93
- return []
 
 
 
 
 
94
 
95
  ai_service = AIService()
 
4
  from app.deps import get_vector_db
5
  from app.service.llm_service import llm_service
6
  from app.service.embedding_service import embedder
7
+ import time
8
 
9
  logger = logging.getLogger(__name__)
10
+
11
  class AIService:
12
  def __init__(self):
 
13
  try:
14
  self.vector_db = get_vector_db()
15
  self.vss_available = True
 
18
  logger.warning(f"⚠️ Vector DB unavailable: {e}")
19
  self.vector_db = None
20
  self.vss_available = False
21
+
 
22
  self.llm = llm_service
23
  self.embedder = embedder
24
+ logger.info(f"βœ… AI Service initialized (VSS: {'ENABLED' if self.vss_available else 'DISABLED'})")
25
+
 
26
  def detect_entity_type(self, org_id: str, columns: list[str], filename: str) -> dict:
27
+ """Detect entity type with JSON validation and timeout"""
28
  columns_str = ",".join(columns)
29
 
30
+ # Check cache
31
+ if self.vss_available:
32
+ cached = self.vector_db.execute("""
33
+ SELECT entity_type FROM vector_store.embeddings
34
+ WHERE org_id = ? AND content = ?
35
+ ORDER BY created_at DESC LIMIT 1
36
+ """, [org_id, columns_str]).fetchone()
37
+
38
+ if cached:
39
+ logger.info(f"[ai_service] Cache hit: {cached[0]}")
40
+ return {"entity_type": cached[0], "confidence": 0.99, "cached": True}
41
 
42
+ # βœ… SIMPLE, CLEAR prompt for Phi-3
43
+ prompt = f"""You are a data classification assistant.
44
 
45
+ You MUST respond with ONLY valid JSON in this exact format:
46
+ {{"entity_type":"sales|inventory|customer|product","confidence":0.95}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
+ Dataset info:
49
+ - Filename: {filename}
50
+ - Columns: {columns_str}
51
+
52
+ Analyze and respond with ONLY JSON:"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ logger.info(f"[ai_service] Calling LLM for {org_id}...")
 
55
 
56
+ try:
57
+ # βœ… TIMEOUT WRAPPER (5 seconds max)
58
+ start_time = time.time()
59
+ response_text = self.llm.generate(prompt, max_tokens=50, temperature=0.1)
60
+ elapsed = time.time() - start_time
61
+
62
+ logger.info(f"[ai_service] LLM responded in {elapsed:.2f}s: {response_text}")
63
+
64
+ # βœ… AGGRESSIVE JSON cleaning
65
+ response_text = response_text.strip()
66
+ if "```json" in response_text:
67
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
68
+ elif "```" in response_text:
69
+ response_text = response_text.split("```")[1].split("```")[0].strip()
70
+
71
+ # βœ… Remove any stray text before/after JSON
72
+ if "{" in response_text and "}" in response_text:
73
+ response_text = response_text[response_text.find("{"):response_text.rfind("}")+1]
74
+
75
+ logger.info(f"[ai_service] Cleaned response: {response_text}")
76
+
77
+ # βœ… PARSE with error handling
78
+ result = json.loads(response_text)
79
+
80
+ # βœ… Normalize
81
+ result["entity_type"] = result["entity_type"].upper()
82
+ result["confidence"] = float(result["confidence"])
83
+
84
+ # βœ… Cache it
85
+ if self.vss_available:
86
+ try:
87
+ embedding = self.embedder.generate(columns_str)
88
+ self.vector_db.execute("""
89
+ INSERT INTO vector_store.embeddings (org_id, content, embedding, entity_type)
90
+ VALUES (?, ?, ?, ?)
91
+ """, [org_id, columns_str, embedding, result["entity_type"]])
92
+ logger.info(f"[ai_service] Cached for {org_id}")
93
+ except Exception as e:
94
+ logger.warning(f"[ai_service] Cache insert failed: {e}")
95
+
96
+ return result
97
+
98
  except Exception as e:
99
+ logger.error(f"[ai_service] ❌ Detection failed: {e}", exc_info=True)
100
+ # βœ… SAFE FALLBACK (never crash)
101
+ return {
102
+ "entity_type": "SALES",
103
+ "confidence": 0.50,
104
+ "error": str(e),
105
+ "fallback": True
106
+ }
107
 
108
  ai_service = AIService()
app/service/llm_service.py CHANGED
@@ -5,6 +5,7 @@ from app.deps import HF_API_TOKEN
5
  import logging
6
  from threading import Thread, Lock
7
  import time
 
8
 
9
  logger = logging.getLogger(__name__)
10
 
@@ -19,7 +20,6 @@ class LocalLLMService:
19
  self._load_error = None
20
  self._lock = Lock()
21
 
22
- # βœ… Start background loading immediately
23
  logger.info("πŸš€ Starting background LLM load...")
24
  Thread(target=self._load_model_background, daemon=True).start()
25
 
@@ -33,12 +33,15 @@ class LocalLLMService:
33
  try:
34
  logger.info(f"πŸ€– [BACKGROUND] Loading LLM: {self.model_id}...")
35
 
 
36
  self._tokenizer = AutoTokenizer.from_pretrained(
37
  self.model_id,
38
  token=HF_API_TOKEN,
39
  trust_remote_code=True
40
  )
 
41
 
 
42
  self._model = AutoModelForCausalLM.from_pretrained(
43
  self.model_id,
44
  token=HF_API_TOKEN,
@@ -46,17 +49,19 @@ class LocalLLMService:
46
  device_map="auto",
47
  low_cpu_mem_usage=True,
48
  trust_remote_code=True,
49
- # βœ… FASTER: Use eager attention (no flash-attn warning)
50
- attn_implementation="eager"
51
  )
52
 
 
53
  self._pipe = pipeline(
54
  "text-generation",
55
  model=self._model,
56
  tokenizer=self._tokenizer,
57
  device_map="auto",
58
  torch_dtype=torch.float16,
59
- trust_remote_code=True
 
 
60
  )
61
 
62
  with self._lock:
@@ -81,18 +86,17 @@ class LocalLLMService:
81
  with self._lock:
82
  return self._load_error
83
 
84
- def generate(self, prompt: str, max_tokens: int = 150, temperature: float = 0.1) -> str:
85
- """Generate text - FAILS FAST if not loaded"""
86
 
87
- # βœ… CRITICAL: Don't wait, fail immediately if not ready
88
  if not self.is_loaded:
89
  if self.load_error:
90
  raise RuntimeError(f"LLM failed to load: {self.load_error}")
91
- raise TimeoutError("LLM loading in progress - use rule-based fallback")
92
 
93
- # βœ… Phi-3 prompt format
94
  messages = [
95
- {"role": "system", "content": "You are a data classification assistant. Return ONLY valid JSON."},
96
  {"role": "user", "content": prompt}
97
  ]
98
 
@@ -102,22 +106,33 @@ class LocalLLMService:
102
  add_generation_prompt=True
103
  )
104
 
105
- # βœ… FASTER generation settings
106
  outputs = self._pipe(
107
  formatted_prompt,
108
  max_new_tokens=max_tokens,
109
  temperature=temperature,
110
- do_sample=False, # Deterministic
111
- pad_token_id=self._tokenizer.eos_token_id
 
112
  )
113
 
114
- response_text = outputs[0]["generated_text"]
 
115
 
116
- # βœ… Phi-3 response extraction
117
  if "<|assistant|>" in response_text:
118
- return response_text.split("<|assistant|>")[-1].strip()
 
 
119
 
120
- return response_text.strip()
 
 
 
 
 
 
 
121
 
122
  # Singleton
123
  llm_service = LocalLLMService()
 
5
  import logging
6
  from threading import Thread, Lock
7
  import time
8
+ import json
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
20
  self._load_error = None
21
  self._lock = Lock()
22
 
 
23
  logger.info("πŸš€ Starting background LLM load...")
24
  Thread(target=self._load_model_background, daemon=True).start()
25
 
 
33
  try:
34
  logger.info(f"πŸ€– [BACKGROUND] Loading LLM: {self.model_id}...")
35
 
36
+ # Phi-3 tokenizer
37
  self._tokenizer = AutoTokenizer.from_pretrained(
38
  self.model_id,
39
  token=HF_API_TOKEN,
40
  trust_remote_code=True
41
  )
42
+ self._tokenizer.pad_token = self._tokenizer.eos_token
43
 
44
+ # Phi-3 model - OPTIMIZED for speed
45
  self._model = AutoModelForCausalLM.from_pretrained(
46
  self.model_id,
47
  token=HF_API_TOKEN,
 
49
  device_map="auto",
50
  low_cpu_mem_usage=True,
51
  trust_remote_code=True,
52
+ attn_implementation="eager" # βœ… No flash-attn warnings
 
53
  )
54
 
55
+ # βœ… FASTER pipeline settings
56
  self._pipe = pipeline(
57
  "text-generation",
58
  model=self._model,
59
  tokenizer=self._tokenizer,
60
  device_map="auto",
61
  torch_dtype=torch.float16,
62
+ trust_remote_code=True,
63
+ # βœ… SPEED UP: Use pad_token_id
64
+ pad_token_id=self._tokenizer.eos_token_id
65
  )
66
 
67
  with self._lock:
 
86
  with self._lock:
87
  return self._load_error
88
 
89
+ def generate(self, prompt: str, max_tokens: int = 100, temperature: float = 0.1) -> str:
90
+ """Generate text - FAILS FAST if not loaded, with JSON validation"""
91
 
92
+ # βœ… CRITICAL: Fail immediately if not ready
93
  if not self.is_loaded:
94
  if self.load_error:
95
  raise RuntimeError(f"LLM failed to load: {self.load_error}")
96
+ raise TimeoutError("LLM loading in progress")
97
 
98
+ # βœ… Phi-3 prompt format (TESTED to work)
99
  messages = [
 
100
  {"role": "user", "content": prompt}
101
  ]
102
 
 
106
  add_generation_prompt=True
107
  )
108
 
109
+ # βœ… FASTER generation with explicit settings
110
  outputs = self._pipe(
111
  formatted_prompt,
112
  max_new_tokens=max_tokens,
113
  temperature=temperature,
114
+ do_sample=False,
115
+ pad_token_id=self._tokenizer.eos_token_id,
116
+ return_full_text=False # βœ… Only return generated text
117
  )
118
 
119
+ # βœ… SAFE extraction
120
+ response_text = outputs[0]["generated_text"].strip()
121
 
122
+ # βœ… Phi-3 specific response extraction
123
  if "<|assistant|>" in response_text:
124
+ response_text = response_text.split("<|assistant|>")[-1].strip()
125
+ if "<|end|>" in response_text:
126
+ response_text = response_text.split("<|end|>")[0].strip()
127
 
128
+ # βœ… VALIDATE JSON before returning
129
+ try:
130
+ json.loads(response_text) # Test parse
131
+ logger.info(f"[llm] Valid JSON generated: {response_text[:50]}...")
132
+ return response_text
133
+ except json.JSONDecodeError:
134
+ logger.error(f"[llm] Invalid JSON from LLM: {response_text}")
135
+ raise ValueError(f"LLM returned invalid JSON: {response_text}")
136
 
137
  # Singleton
138
  llm_service = LocalLLMService()