Kunalv commited on
Commit
e074efd
·
1 Parent(s): 42d4f3b

feat: add OpenBMB Core Router (MiniCPM4.1-8B) and wire it to Gradio UI

Browse files
Files changed (2) hide show
  1. agent/brain.py +104 -0
  2. app.py +18 -13
agent/brain.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import types
4
+ import spaces
5
+ import torch
6
+ from transformers import AutoModelForCausalLM, AutoTokenizer
7
+
8
+ # --- HACK TO BYPASS FLASH ATTENTION COMPILATION ---
9
+ # MiniCPM4.1-8B strictly checks if flash_attn is installed, but compiling
10
+ # it takes 20+ minutes on a cold HF Space. We mock the module exactly like
11
+ # we did for the vision model so it passes the internal import check.
12
+ if "flash_attn" not in sys.modules:
13
+ import importlib.machinery
14
+ _mock = types.ModuleType("flash_attn")
15
+ _mock.__spec__ = importlib.machinery.ModuleSpec("flash_attn", None)
16
+ _mock.__version__ = "2.6.0"
17
+ sys.modules["flash_attn"] = _mock
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Global Initialization — Core Boss (OpenBMB MiniCPM4.1-8B)
21
+ # We load the text routing model into CPU memory when the space starts.
22
+ # We do NOT call .cuda() here — ZeroGPU handles that inside @spaces.GPU.
23
+ # ---------------------------------------------------------------------------
24
+ print("Initializing Core Router (OpenBMB MiniCPM4.1-8B)... This may take a moment.")
25
+
26
+ # Using the exact 8B model specified in our architecture
27
+ model_id = "openbmb/MiniCPM4.1-8B"
28
+ hf_token = os.environ.get("HF_TOKEN")
29
+
30
+ try:
31
+ tokenizer = AutoTokenizer.from_pretrained(
32
+ model_id,
33
+ trust_remote_code=True,
34
+ token=hf_token
35
+ )
36
+ model = AutoModelForCausalLM.from_pretrained(
37
+ model_id,
38
+ trust_remote_code=True,
39
+ torch_dtype=torch.bfloat16,
40
+ low_cpu_mem_usage=True,
41
+ attn_implementation="sdpa", # Use built-in PyTorch attention, not flash_attn
42
+ token=hf_token
43
+ )
44
+ except Exception as e:
45
+ print(f"Error loading Core Router: {e}")
46
+ tokenizer = None
47
+ model = None
48
+
49
+ @spaces.GPU
50
+ def _core_router_inference(prompt):
51
+ """
52
+ Executes the LLM logic on the ZeroGPU.
53
+ """
54
+ if model is None or tokenizer is None:
55
+ return "Error: Core Router Model failed to load."
56
+
57
+ model.to("cuda")
58
+ model.eval()
59
+
60
+ # Format the prompt using the chat template if available, else plain text
61
+ messages = [{"role": "user", "content": prompt}]
62
+
63
+ # MiniCPM typically uses a specific chat template or plain tokenization
64
+ try:
65
+ # If the tokenizer supports apply_chat_template
66
+ inputs = tokenizer.apply_chat_template(messages, return_tensors="pt", add_generation_prompt=True).to("cuda")
67
+ except Exception:
68
+ # Fallback to standard tokenization
69
+ inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
70
+ inputs = inputs.input_ids
71
+
72
+ with torch.no_grad():
73
+ outputs = model.generate(
74
+ inputs,
75
+ max_new_tokens=500,
76
+ do_sample=True, # Required — without this, temperature/top_p crash
77
+ temperature=0.7,
78
+ top_p=0.9
79
+ )
80
+
81
+ # Decode the newly generated tokens
82
+ input_length = inputs.shape[1] if hasattr(inputs, "shape") else len(inputs[0])
83
+ response = tokenizer.decode(outputs[0][input_length:], skip_special_tokens=True)
84
+
85
+ return response
86
+
87
+ def process_workflow(user_text, raw_vision_text=None):
88
+ """
89
+ The Main Flow Controller.
90
+ Orchestrates: Vision -> Parse -> Mellum -> Cohere -> VoxCPM2.
91
+ Additional parameters (clean_json, math_summary, translated_summary)
92
+ will be added as we build each specialist module.
93
+ """
94
+
95
+ # Base prompt telling the model what it is
96
+ system_context = "You are the Core Router for the Family Bill Assistant, a helpful AI."
97
+
98
+ # If we have extracted text from the receipt
99
+ if raw_vision_text:
100
+ prompt = f"{system_context}\n\nThe user uploaded a receipt. Here is the raw extracted text:\n{raw_vision_text}\n\nThe user says: {user_text}\n\nPlease respond helpfully."
101
+ else:
102
+ prompt = f"{system_context}\n\nThe user says: {user_text}\n\nPlease respond to their question."
103
+
104
+ return _core_router_inference(prompt)
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import gradio as gr
2
  from ui.layout import create_ui
3
  from tools.vision import process_receipt_image
 
4
 
5
  # Load the custom CSS for the "Off-Brand" Badge
6
  try:
@@ -20,23 +21,27 @@ with demo:
20
  # Initialize the UI layout from the ui folder
21
  image_input, audio_input, submit_btn, chatbot, msg_input = create_ui()
22
 
23
- # Bind the submit button to the vision function
24
- def handle_analyze(image):
25
- if not image:
26
- return [{"role": "assistant", "content": "Please upload an image first!"}]
 
 
 
 
 
 
 
 
27
 
28
- # Trigger the ZeroGPU
29
- result = process_receipt_image(image)
30
-
31
- # Return as a chat message history
32
- return [
33
- {"role": "user", "content": "Here is my bill. Can you extract the text?"},
34
- {"role": "assistant", "content": str(result)}
35
- ]
36
 
37
  submit_btn.click(
38
  fn=handle_analyze,
39
- inputs=[image_input],
40
  outputs=[chatbot]
41
  )
42
 
 
1
  import gradio as gr
2
  from ui.layout import create_ui
3
  from tools.vision import process_receipt_image
4
+ from agent.brain import process_workflow
5
 
6
  # Load the custom CSS for the "Off-Brand" Badge
7
  try:
 
21
  # Initialize the UI layout from the ui folder
22
  image_input, audio_input, submit_btn, chatbot, msg_input = create_ui()
23
 
24
+ # Bind the submit button to the Core Boss workflow
25
+ def handle_analyze(image, user_msg, history):
26
+ if not user_msg:
27
+ user_msg = "Please analyze this bill."
28
+
29
+ # Step 1: If an image is provided, extract raw text
30
+ raw_text = None
31
+ if image:
32
+ raw_text = process_receipt_image(image)
33
+
34
+ # Step 2: Route everything to the Core Boss
35
+ bot_response = process_workflow(user_text=user_msg, raw_vision_text=raw_text)
36
 
37
+ # Step 3: Append to chat history
38
+ history.append({"role": "user", "content": user_msg})
39
+ history.append({"role": "assistant", "content": str(bot_response)})
40
+ return history
 
 
 
 
41
 
42
  submit_btn.click(
43
  fn=handle_analyze,
44
+ inputs=[image_input, msg_input, chatbot],
45
  outputs=[chatbot]
46
  )
47