Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,7 +14,7 @@ import base64
|
|
| 14 |
import asyncio
|
| 15 |
import gc
|
| 16 |
|
| 17 |
-
# ---
|
| 18 |
try:
|
| 19 |
from ocr_engine import extract_text_from_file
|
| 20 |
from prompts import get_ocr_extraction_prompt, get_agent_prompt
|
|
@@ -31,7 +31,7 @@ logger = logging.getLogger("mcp_server")
|
|
| 31 |
# Attempt to import transformers
|
| 32 |
TRANSFORMERS_AVAILABLE = False
|
| 33 |
try:
|
| 34 |
-
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
| 35 |
TRANSFORMERS_AVAILABLE = True
|
| 36 |
except Exception as e:
|
| 37 |
logger.warning("transformers not available: %s", e)
|
|
@@ -63,14 +63,10 @@ _init_analytics()
|
|
| 63 |
# Regex JSON Extractor
|
| 64 |
# ----------------------------
|
| 65 |
def extract_json_safely(text: str) -> Optional[Any]:
|
| 66 |
-
"""
|
| 67 |
-
Extracts JSON from text even if the model adds conversational filler.
|
| 68 |
-
"""
|
| 69 |
try:
|
| 70 |
return json.loads(text)
|
| 71 |
except:
|
| 72 |
pass
|
| 73 |
-
|
| 74 |
try:
|
| 75 |
match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
|
| 76 |
if match:
|
|
@@ -81,7 +77,7 @@ def extract_json_safely(text: str) -> Optional[Any]:
|
|
| 81 |
return None
|
| 82 |
|
| 83 |
# ----------------------------
|
| 84 |
-
# Local LLM loader
|
| 85 |
# ----------------------------
|
| 86 |
LLM_PIPELINE = None
|
| 87 |
TOKENIZER = None
|
|
@@ -89,8 +85,14 @@ LOADED_MODEL_NAME = None
|
|
| 89 |
|
| 90 |
def init_local_model():
|
| 91 |
global LLM_PIPELINE, TOKENIZER, LOADED_MODEL_NAME
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
if not LOCAL_MODEL or not TRANSFORMERS_AVAILABLE:
|
| 93 |
return
|
|
|
|
| 94 |
try:
|
| 95 |
logger.info(f"Loading model: {LOCAL_MODEL}...")
|
| 96 |
TOKENIZER = AutoTokenizer.from_pretrained(LOCAL_MODEL, trust_remote_code=True)
|
|
@@ -98,23 +100,28 @@ def init_local_model():
|
|
| 98 |
|
| 99 |
LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
|
| 100 |
LOADED_MODEL_NAME = LOCAL_MODEL
|
| 101 |
-
logger.info("Model loaded.")
|
| 102 |
except Exception as e:
|
| 103 |
logger.error(f"Model load failed: {e}")
|
| 104 |
|
| 105 |
-
init_local_model()
|
|
|
|
| 106 |
|
| 107 |
-
# --- FIX APPLIED HERE ---
|
| 108 |
def local_llm_generate(prompt: str, max_tokens: int = 512) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
if LLM_PIPELINE is None:
|
| 110 |
return {"text": "LLM not loaded.", "raw": None}
|
|
|
|
| 111 |
try:
|
| 112 |
-
#
|
| 113 |
out = LLM_PIPELINE(
|
| 114 |
prompt,
|
| 115 |
max_new_tokens=max_tokens,
|
| 116 |
return_full_text=False,
|
| 117 |
-
use_cache=False
|
| 118 |
)
|
| 119 |
text = out[0]["generated_text"] if out else ""
|
| 120 |
return {"text": text, "raw": out}
|
|
@@ -144,15 +151,19 @@ def _get_valid_token_headers() -> dict:
|
|
| 144 |
r = requests.post(token_url, params=params, timeout=20)
|
| 145 |
if r.status_code == 200:
|
| 146 |
return {"Authorization": f"Zoho-oauthtoken {r.json().get('access_token')}"}
|
| 147 |
-
|
|
|
|
|
|
|
| 148 |
|
| 149 |
@mcp.tool()
|
| 150 |
def authenticate_zoho() -> str:
|
| 151 |
-
|
|
|
|
| 152 |
|
| 153 |
@mcp.tool()
|
| 154 |
def create_record(module_name: str, record_data: dict) -> str:
|
| 155 |
headers = _get_valid_token_headers()
|
|
|
|
| 156 |
url = f"{API_BASE}/{module_name}"
|
| 157 |
r = requests.post(url, headers=headers, json={"data": [record_data]}, timeout=20)
|
| 158 |
if r.status_code in (200, 201): return json.dumps(r.json(), ensure_ascii=False)
|
|
@@ -161,18 +172,21 @@ def create_record(module_name: str, record_data: dict) -> str:
|
|
| 161 |
@mcp.tool()
|
| 162 |
def get_records(module_name: str, page: int = 1, per_page: int = 200) -> list:
|
| 163 |
headers = _get_valid_token_headers()
|
|
|
|
| 164 |
r = requests.get(f"{API_BASE}/{module_name}", headers=headers, params={"page": page, "per_page": per_page})
|
| 165 |
return r.json().get("data", []) if r.status_code == 200 else []
|
| 166 |
|
| 167 |
@mcp.tool()
|
| 168 |
def update_record(module_name: str, record_id: str, data: dict) -> str:
|
| 169 |
headers = _get_valid_token_headers()
|
|
|
|
| 170 |
r = requests.put(f"{API_BASE}/{module_name}/{record_id}", headers=headers, json={"data": [data]})
|
| 171 |
return json.dumps(r.json()) if r.status_code == 200 else r.text
|
| 172 |
|
| 173 |
@mcp.tool()
|
| 174 |
def delete_record(module_name: str, record_id: str) -> str:
|
| 175 |
headers = _get_valid_token_headers()
|
|
|
|
| 176 |
r = requests.delete(f"{API_BASE}/{module_name}/{record_id}", headers=headers)
|
| 177 |
return json.dumps(r.json()) if r.status_code == 200 else r.text
|
| 178 |
|
|
@@ -183,6 +197,7 @@ def _ensure_invoice_config():
|
|
| 183 |
def create_invoice(data: dict) -> str:
|
| 184 |
_ensure_invoice_config()
|
| 185 |
headers = _get_valid_token_headers()
|
|
|
|
| 186 |
params = {"organization_id": ORGANIZATION_ID}
|
| 187 |
r = requests.post(f"{INVOICE_API_BASE}/invoices", headers=headers, params=params, json=data)
|
| 188 |
if r.status_code in (200, 201): return json.dumps(r.json(), ensure_ascii=False)
|
|
@@ -191,6 +206,7 @@ def create_invoice(data: dict) -> str:
|
|
| 191 |
def upload_invoice_attachment(invoice_id: str, file_path: str) -> str:
|
| 192 |
if not os.path.exists(file_path): return "File not found"
|
| 193 |
headers = _get_valid_token_headers()
|
|
|
|
| 194 |
headers.pop("Content-Type", None)
|
| 195 |
url = f"{INVOICE_API_BASE}/invoices/{invoice_id}/attachments"
|
| 196 |
with open(file_path, "rb") as f:
|
|
@@ -200,9 +216,6 @@ def upload_invoice_attachment(invoice_id: str, file_path: str) -> str:
|
|
| 200 |
|
| 201 |
@mcp.tool()
|
| 202 |
def process_document(file_path: str, target_module: Optional[str] = "Contacts") -> dict:
|
| 203 |
-
"""
|
| 204 |
-
Extracts data from file using OCR + LLM.
|
| 205 |
-
"""
|
| 206 |
try:
|
| 207 |
if not os.path.exists(file_path):
|
| 208 |
return {"status": "error", "error": "file not found"}
|
|
@@ -215,7 +228,7 @@ def process_document(file_path: str, target_module: Optional[str] = "Contacts")
|
|
| 215 |
# 2. Use Prompt Template
|
| 216 |
prompt = get_ocr_extraction_prompt(raw_text)
|
| 217 |
|
| 218 |
-
# 3. Generate
|
| 219 |
llm_out = local_llm_generate(prompt, max_tokens=300)
|
| 220 |
extracted_text = llm_out.get("text", "")
|
| 221 |
|
|
@@ -334,7 +347,7 @@ def deepseek_response(message: str, file_path: Optional[str] = None, history: li
|
|
| 334 |
history_text = "\n".join([f"User: {h[0]}\nBot: {h[1]}" for h in history])
|
| 335 |
prompt = get_agent_prompt(history_text, ocr_context, message)
|
| 336 |
|
| 337 |
-
# 3. Generate
|
| 338 |
gen = local_llm_generate(prompt, max_tokens=256)
|
| 339 |
response_text = gen["text"]
|
| 340 |
|
|
@@ -384,6 +397,9 @@ def cleanup_event_loop():
|
|
| 384 |
if __name__ == "__main__":
|
| 385 |
cleanup_event_loop()
|
| 386 |
|
|
|
|
|
|
|
|
|
|
| 387 |
demo = gr.ChatInterface(
|
| 388 |
fn=chat_handler,
|
| 389 |
multimodal=True,
|
|
|
|
| 14 |
import asyncio
|
| 15 |
import gc
|
| 16 |
|
| 17 |
+
# --- Import OCR Engine & Prompts ---
|
| 18 |
try:
|
| 19 |
from ocr_engine import extract_text_from_file
|
| 20 |
from prompts import get_ocr_extraction_prompt, get_agent_prompt
|
|
|
|
| 31 |
# Attempt to import transformers
|
| 32 |
TRANSFORMERS_AVAILABLE = False
|
| 33 |
try:
|
| 34 |
+
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
| 35 |
TRANSFORMERS_AVAILABLE = True
|
| 36 |
except Exception as e:
|
| 37 |
logger.warning("transformers not available: %s", e)
|
|
|
|
| 63 |
# Regex JSON Extractor
|
| 64 |
# ----------------------------
|
| 65 |
def extract_json_safely(text: str) -> Optional[Any]:
|
|
|
|
|
|
|
|
|
|
| 66 |
try:
|
| 67 |
return json.loads(text)
|
| 68 |
except:
|
| 69 |
pass
|
|
|
|
| 70 |
try:
|
| 71 |
match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
|
| 72 |
if match:
|
|
|
|
| 77 |
return None
|
| 78 |
|
| 79 |
# ----------------------------
|
| 80 |
+
# Local LLM loader (Lazy Loading Fix)
|
| 81 |
# ----------------------------
|
| 82 |
LLM_PIPELINE = None
|
| 83 |
TOKENIZER = None
|
|
|
|
| 85 |
|
| 86 |
def init_local_model():
|
| 87 |
global LLM_PIPELINE, TOKENIZER, LOADED_MODEL_NAME
|
| 88 |
+
|
| 89 |
+
# FIX 1: Check if already loaded to prevent double-memory usage
|
| 90 |
+
if LLM_PIPELINE is not None:
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
if not LOCAL_MODEL or not TRANSFORMERS_AVAILABLE:
|
| 94 |
return
|
| 95 |
+
|
| 96 |
try:
|
| 97 |
logger.info(f"Loading model: {LOCAL_MODEL}...")
|
| 98 |
TOKENIZER = AutoTokenizer.from_pretrained(LOCAL_MODEL, trust_remote_code=True)
|
|
|
|
| 100 |
|
| 101 |
LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
|
| 102 |
LOADED_MODEL_NAME = LOCAL_MODEL
|
| 103 |
+
logger.info("Model loaded successfully.")
|
| 104 |
except Exception as e:
|
| 105 |
logger.error(f"Model load failed: {e}")
|
| 106 |
|
| 107 |
+
# FIX 2: Removed global call to init_local_model() here.
|
| 108 |
+
# It will now be called only in __main__ or lazily.
|
| 109 |
|
|
|
|
| 110 |
def local_llm_generate(prompt: str, max_tokens: int = 512) -> Dict[str, Any]:
|
| 111 |
+
# FIX 3: Lazy load if accessed before main (safety net)
|
| 112 |
+
if LLM_PIPELINE is None:
|
| 113 |
+
init_local_model()
|
| 114 |
+
|
| 115 |
if LLM_PIPELINE is None:
|
| 116 |
return {"text": "LLM not loaded.", "raw": None}
|
| 117 |
+
|
| 118 |
try:
|
| 119 |
+
# Includes Cache Fix from previous step
|
| 120 |
out = LLM_PIPELINE(
|
| 121 |
prompt,
|
| 122 |
max_new_tokens=max_tokens,
|
| 123 |
return_full_text=False,
|
| 124 |
+
use_cache=False
|
| 125 |
)
|
| 126 |
text = out[0]["generated_text"] if out else ""
|
| 127 |
return {"text": text, "raw": out}
|
|
|
|
| 151 |
r = requests.post(token_url, params=params, timeout=20)
|
| 152 |
if r.status_code == 200:
|
| 153 |
return {"Authorization": f"Zoho-oauthtoken {r.json().get('access_token')}"}
|
| 154 |
+
# Don't crash entire app on token fail, just return None so tool can report it
|
| 155 |
+
logger.error(f"Token Refresh Failed: {r.text}")
|
| 156 |
+
return {}
|
| 157 |
|
| 158 |
@mcp.tool()
|
| 159 |
def authenticate_zoho() -> str:
|
| 160 |
+
h = _get_valid_token_headers()
|
| 161 |
+
return "Zoho token refreshed." if h else "Failed to refresh token."
|
| 162 |
|
| 163 |
@mcp.tool()
|
| 164 |
def create_record(module_name: str, record_data: dict) -> str:
|
| 165 |
headers = _get_valid_token_headers()
|
| 166 |
+
if not headers: return "Auth Error"
|
| 167 |
url = f"{API_BASE}/{module_name}"
|
| 168 |
r = requests.post(url, headers=headers, json={"data": [record_data]}, timeout=20)
|
| 169 |
if r.status_code in (200, 201): return json.dumps(r.json(), ensure_ascii=False)
|
|
|
|
| 172 |
@mcp.tool()
|
| 173 |
def get_records(module_name: str, page: int = 1, per_page: int = 200) -> list:
|
| 174 |
headers = _get_valid_token_headers()
|
| 175 |
+
if not headers: return []
|
| 176 |
r = requests.get(f"{API_BASE}/{module_name}", headers=headers, params={"page": page, "per_page": per_page})
|
| 177 |
return r.json().get("data", []) if r.status_code == 200 else []
|
| 178 |
|
| 179 |
@mcp.tool()
|
| 180 |
def update_record(module_name: str, record_id: str, data: dict) -> str:
|
| 181 |
headers = _get_valid_token_headers()
|
| 182 |
+
if not headers: return "Auth Error"
|
| 183 |
r = requests.put(f"{API_BASE}/{module_name}/{record_id}", headers=headers, json={"data": [data]})
|
| 184 |
return json.dumps(r.json()) if r.status_code == 200 else r.text
|
| 185 |
|
| 186 |
@mcp.tool()
|
| 187 |
def delete_record(module_name: str, record_id: str) -> str:
|
| 188 |
headers = _get_valid_token_headers()
|
| 189 |
+
if not headers: return "Auth Error"
|
| 190 |
r = requests.delete(f"{API_BASE}/{module_name}/{record_id}", headers=headers)
|
| 191 |
return json.dumps(r.json()) if r.status_code == 200 else r.text
|
| 192 |
|
|
|
|
| 197 |
def create_invoice(data: dict) -> str:
|
| 198 |
_ensure_invoice_config()
|
| 199 |
headers = _get_valid_token_headers()
|
| 200 |
+
if not headers: return "Auth Error"
|
| 201 |
params = {"organization_id": ORGANIZATION_ID}
|
| 202 |
r = requests.post(f"{INVOICE_API_BASE}/invoices", headers=headers, params=params, json=data)
|
| 203 |
if r.status_code in (200, 201): return json.dumps(r.json(), ensure_ascii=False)
|
|
|
|
| 206 |
def upload_invoice_attachment(invoice_id: str, file_path: str) -> str:
|
| 207 |
if not os.path.exists(file_path): return "File not found"
|
| 208 |
headers = _get_valid_token_headers()
|
| 209 |
+
if not headers: return "Auth Error"
|
| 210 |
headers.pop("Content-Type", None)
|
| 211 |
url = f"{INVOICE_API_BASE}/invoices/{invoice_id}/attachments"
|
| 212 |
with open(file_path, "rb") as f:
|
|
|
|
| 216 |
|
| 217 |
@mcp.tool()
|
| 218 |
def process_document(file_path: str, target_module: Optional[str] = "Contacts") -> dict:
|
|
|
|
|
|
|
|
|
|
| 219 |
try:
|
| 220 |
if not os.path.exists(file_path):
|
| 221 |
return {"status": "error", "error": "file not found"}
|
|
|
|
| 228 |
# 2. Use Prompt Template
|
| 229 |
prompt = get_ocr_extraction_prompt(raw_text)
|
| 230 |
|
| 231 |
+
# 3. Generate
|
| 232 |
llm_out = local_llm_generate(prompt, max_tokens=300)
|
| 233 |
extracted_text = llm_out.get("text", "")
|
| 234 |
|
|
|
|
| 347 |
history_text = "\n".join([f"User: {h[0]}\nBot: {h[1]}" for h in history])
|
| 348 |
prompt = get_agent_prompt(history_text, ocr_context, message)
|
| 349 |
|
| 350 |
+
# 3. Generate
|
| 351 |
gen = local_llm_generate(prompt, max_tokens=256)
|
| 352 |
response_text = gen["text"]
|
| 353 |
|
|
|
|
| 397 |
if __name__ == "__main__":
|
| 398 |
cleanup_event_loop()
|
| 399 |
|
| 400 |
+
# FIX: Explicitly load model once here to prevent concurrent load attempts by Gradio
|
| 401 |
+
init_local_model()
|
| 402 |
+
|
| 403 |
demo = gr.ChatInterface(
|
| 404 |
fn=chat_handler,
|
| 405 |
multimodal=True,
|