Spaces:
Sleeping
Sleeping
File size: 8,021 Bytes
dc79584 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# app.py — MCP server (single-file)
from mcp.server.fastmcp import FastMCP
from typing import Optional, List, Tuple, Any, Dict
import requests
import os
import gradio as gr
import json
import re
import logging
import gc
# --- Import OCR Engine & Prompts ---
try:
from ocr_engine import extract_text_from_file
from prompts import get_ocr_extraction_prompt, get_agent_prompt
except ImportError:
def extract_text_from_file(path): return ""
def get_ocr_extraction_prompt(txt): return txt
def get_agent_prompt(h, u): return u
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp_server")
# --- Load Config ---
try:
from config import (
CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, API_BASE,
INVOICE_API_BASE, ORGANIZATION_ID, LOCAL_MODEL
)
except Exception:
raise SystemExit("Config missing.")
mcp = FastMCP("ZohoCRMAgent")
# --- Globals ---
LLM_PIPELINE = None
TOKENIZER = None
# --- Helpers ---
def extract_json_safely(text: str) -> Optional[Any]:
try:
return json.loads(text)
except:
match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
return json.loads(match.group(0)) if match else None
def _normalize_local_path_args(args: Any) -> Any:
if not isinstance(args, dict): return args
fp = args.get("file_path") or args.get("path")
if isinstance(fp, str) and fp.startswith("/mnt/data/") and os.path.exists(fp):
args["file_url"] = f"file://{fp}"
return args
# --- Model Loading ---
def init_local_model():
global LLM_PIPELINE, TOKENIZER
if LLM_PIPELINE is not None: return
try:
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
logger.info(f"Loading lighter model: {LOCAL_MODEL}...")
TOKENIZER = AutoTokenizer.from_pretrained(LOCAL_MODEL)
model = AutoModelForCausalLM.from_pretrained(
LOCAL_MODEL,
device_map="auto",
torch_dtype="auto"
)
LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
logger.info("Model loaded.")
except Exception as e:
logger.error(f"Model load error: {e}")
def local_llm_generate(prompt: str, max_tokens: int = 512) -> Dict[str, Any]:
if LLM_PIPELINE is None:
init_local_model()
if LLM_PIPELINE is None:
return {"text": "Model not loaded.", "raw": None}
try:
out = LLM_PIPELINE(
prompt,
max_new_tokens=max_tokens,
return_full_text=False,
do_sample=False
)
text = out[0]["generated_text"] if out else ""
return {"text": text, "raw": out}
except Exception as e:
return {"text": f"Error: {e}", "raw": None}
# --- Tools (Zoho) ---
def _get_valid_token_headers() -> dict:
r = requests.post("https://accounts.zoho.in/oauth/v2/token", params={
"refresh_token": REFRESH_TOKEN, "client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET, "grant_type": "refresh_token"
}, timeout=10)
if r.status_code == 200:
return {"Authorization": f"Zoho-oauthtoken {r.json().get('access_token')}"}
return {}
@mcp.tool()
def create_record(module_name: str, record_data: dict) -> str:
h = _get_valid_token_headers()
if not h: return "Auth Failed"
r = requests.post(f"{API_BASE}/{module_name}", headers=h, json={"data": [record_data]})
if r.status_code in (200, 201):
try:
d = r.json().get("data", [{}])[0].get("details", {})
return json.dumps({"status": "success", "id": d.get("id"), "zoho_response": r.json()})
except:
return json.dumps(r.json())
return r.text
@mcp.tool()
def create_invoice(data: dict) -> str:
h = _get_valid_token_headers()
if not h: return "Auth Failed"
r = requests.post(f"{INVOICE_API_BASE}/invoices", headers=h,
params={"organization_id": ORGANIZATION_ID}, json=data)
return json.dumps(r.json()) if r.status_code in (200, 201) else r.text
@mcp.tool()
def process_document(file_path: str, target_module: Optional[str] = "Contacts") -> dict:
if not os.path.exists(file_path):
return {"error": f"File not found at path: {file_path}"}
# 1. OCR
raw_text = extract_text_from_file(file_path)
if not raw_text: return {"error": "OCR empty"}
# 2. LLM Extraction
prompt = get_ocr_extraction_prompt(raw_text)
res = local_llm_generate(prompt, max_tokens=300)
data = extract_json_safely(res["text"])
return {
"status": "success",
"file": os.path.basename(file_path),
"extracted_data": data or {"raw": res["text"]}
}
# --- Executor ---
def parse_and_execute(model_text: str, history: list) -> str:
payload = extract_json_safely(model_text)
if not payload: return "No valid tool call found."
cmds = [payload] if isinstance(payload, dict) else payload
results = []
last_contact_id = None
for cmd in cmds:
if not isinstance(cmd, dict): continue
tool = cmd.get("tool")
args = _normalize_local_path_args(cmd.get("args", {}))
if tool == "create_record":
res = create_record(args.get("module_name", "Contacts"), args.get("record_data", {}))
results.append(f"Record: {res}")
try:
rj = json.loads(res)
if isinstance(rj, dict) and "id" in rj:
last_contact_id = rj["id"]
except: pass
elif tool == "create_invoice":
# Auto-fill contact_id if we just created one
if not args.get("customer_id") and last_contact_id:
args["customer_id"] = last_contact_id
# Map Items from strict structure
invoice_payload = args # Assuming LLM passes correct structure, or map here
if last_contact_id and "customer_id" not in invoice_payload:
invoice_payload["customer_id"] = last_contact_id
res = create_invoice(invoice_payload)
results.append(f"Invoice: {res}")
return "\n".join(results)
# --- Chat Core ---
def chat_logic(message: str, file_path: str, history: list) -> str:
# PHASE 1: File Upload -> Extraction Only (No Zoho Auth yet)
if file_path:
logger.info(f"Processing file: {file_path}")
doc = process_document(file_path)
if doc.get("status") == "success":
extracted_json = json.dumps(doc["extracted_data"], indent=2)
# We return this text. It gets added to history.
# The User must then say "Yes, push it" to trigger Phase 2.
return (
f"I extracted the following data from **{doc['file']}**:\n\n"
f"```json\n{extracted_json}\n```\n\n"
"Please review it. If it looks correct, type **'Create Invoice'** or **'Push to Zoho'**."
)
else:
return f"OCR Failed: {doc.get('error')}"
# PHASE 2: Text Interaction (Check History for JSON + Intent)
hist_txt = "\n".join([f"U: {h[0]}\nA: {h[1]}" for h in history])
# The Prompt now checks history for JSON and waits for explicit "save/push" keywords
prompt = get_agent_prompt(hist_txt, message)
gen = local_llm_generate(prompt, max_tokens=256)
logger.info(f"LLM Decision: {gen['text']}")
tool_data = extract_json_safely(gen["text"])
if tool_data:
# User confirmed -> Execute Tool (Triggers Zoho Auth)
return parse_and_execute(gen["text"], history)
# Just chat/clarification
return gen["text"]
# --- UI ---
def chat_handler(msg, hist):
txt = msg.get("text", "")
files = msg.get("files", [])
path = files[0] if files else None
return chat_logic(txt, path, hist)
if __name__ == "__main__":
gc.collect()
demo = gr.ChatInterface(fn=chat_handler, multimodal=True)
demo.launch(server_name="0.0.0.0", server_port=7860) |