Spaces:
Sleeping
Sleeping
resolve dynamic cache
Browse files
app.py
CHANGED
|
@@ -51,31 +51,26 @@ except Exception as e:
|
|
| 51 |
mcp = FastMCP("ZohoCRMAgent")
|
| 52 |
|
| 53 |
# ----------------------------
|
| 54 |
-
# Analytics
|
| 55 |
# ----------------------------
|
| 56 |
ANALYTICS_PATH = "mcp_analytics.json"
|
| 57 |
def _init_analytics():
|
| 58 |
if not os.path.exists(ANALYTICS_PATH):
|
| 59 |
with open(ANALYTICS_PATH, "w") as f: json.dump({}, f)
|
| 60 |
-
def _log_tool_call(t, s): pass
|
| 61 |
-
def _log_llm_call(c): pass
|
| 62 |
_init_analytics()
|
| 63 |
|
| 64 |
# ----------------------------
|
| 65 |
-
#
|
| 66 |
# ----------------------------
|
| 67 |
def extract_json_safely(text: str) -> Optional[Any]:
|
| 68 |
"""
|
| 69 |
Extracts JSON from text even if the model adds conversational filler.
|
| 70 |
-
Fixes the '(Parse) Model output was not valid JSON' error.
|
| 71 |
"""
|
| 72 |
try:
|
| 73 |
-
# 1. Try direct parse
|
| 74 |
return json.loads(text)
|
| 75 |
except:
|
| 76 |
pass
|
| 77 |
|
| 78 |
-
# 2. Regex search for { ... } or [ ... ]
|
| 79 |
try:
|
| 80 |
match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
|
| 81 |
if match:
|
|
@@ -99,10 +94,8 @@ def init_local_model():
|
|
| 99 |
try:
|
| 100 |
logger.info(f"Loading model: {LOCAL_MODEL}...")
|
| 101 |
TOKENIZER = AutoTokenizer.from_pretrained(LOCAL_MODEL, trust_remote_code=True)
|
| 102 |
-
# Use CPU if needed, or remove device_map="auto" if causing issues
|
| 103 |
model = AutoModelForCausalLM.from_pretrained(LOCAL_MODEL, trust_remote_code=True, device_map="auto")
|
| 104 |
|
| 105 |
-
# FIX: Lower max_new_tokens to prevent 400s generation loops
|
| 106 |
LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
|
| 107 |
LOADED_MODEL_NAME = LOCAL_MODEL
|
| 108 |
logger.info("Model loaded.")
|
|
@@ -111,19 +104,26 @@ def init_local_model():
|
|
| 111 |
|
| 112 |
init_local_model()
|
| 113 |
|
|
|
|
| 114 |
def local_llm_generate(prompt: str, max_tokens: int = 512) -> Dict[str, Any]:
|
| 115 |
if LLM_PIPELINE is None:
|
| 116 |
return {"text": "LLM not loaded.", "raw": None}
|
| 117 |
try:
|
| 118 |
-
# FIX:
|
| 119 |
-
out = LLM_PIPELINE(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
text = out[0]["generated_text"] if out else ""
|
| 121 |
return {"text": text, "raw": out}
|
| 122 |
except Exception as e:
|
|
|
|
| 123 |
return {"text": f"Error: {e}", "raw": None}
|
| 124 |
|
| 125 |
# ----------------------------
|
| 126 |
-
# Helper: normalize local file_path args
|
| 127 |
# ----------------------------
|
| 128 |
def _normalize_local_path_args(args: Any) -> Any:
|
| 129 |
if not isinstance(args, dict): return args
|
|
@@ -133,7 +133,7 @@ def _normalize_local_path_args(args: Any) -> Any:
|
|
| 133 |
return args
|
| 134 |
|
| 135 |
# ----------------------------
|
| 136 |
-
# Zoho Auth & Tools
|
| 137 |
# ----------------------------
|
| 138 |
def _get_valid_token_headers() -> dict:
|
| 139 |
token_url = "https://accounts.zoho.in/oauth/v2/token"
|
|
@@ -212,18 +212,17 @@ def process_document(file_path: str, target_module: Optional[str] = "Contacts")
|
|
| 212 |
if not raw_text or len(raw_text) < 5:
|
| 213 |
return {"status": "error", "error": "OCR failed to extract text."}
|
| 214 |
|
| 215 |
-
# 2. Use Prompt Template
|
| 216 |
-
# FIX: Use prompts.py template + reduce max_tokens for speed
|
| 217 |
prompt = get_ocr_extraction_prompt(raw_text)
|
| 218 |
|
| 219 |
-
|
|
|
|
| 220 |
extracted_text = llm_out.get("text", "")
|
| 221 |
|
| 222 |
-
#
|
| 223 |
extracted_data = extract_json_safely(extracted_text)
|
| 224 |
|
| 225 |
if not extracted_data:
|
| 226 |
-
# Fallback for debugging
|
| 227 |
extracted_data = {"raw_llm_text": extracted_text}
|
| 228 |
|
| 229 |
return {
|
|
@@ -236,10 +235,9 @@ def process_document(file_path: str, target_module: Optional[str] = "Contacts")
|
|
| 236 |
return {"status": "error", "error": str(e)}
|
| 237 |
|
| 238 |
# ----------------------------
|
| 239 |
-
# Helpers: map LLM args -> Zoho payloads
|
| 240 |
# ----------------------------
|
| 241 |
def _extract_created_id_from_zoho_response(resp_json) -> Optional[str]:
|
| 242 |
-
# (Same implementation as before)
|
| 243 |
try:
|
| 244 |
if isinstance(resp_json, str): resp_json = json.loads(resp_json)
|
| 245 |
data = resp_json.get("data") or resp_json.get("result")
|
|
@@ -251,17 +249,14 @@ def _extract_created_id_from_zoho_response(resp_json) -> Optional[str]:
|
|
| 251 |
return None
|
| 252 |
|
| 253 |
def _map_contact_args_to_zoho_payload(args: dict) -> dict:
|
| 254 |
-
# (Same implementation as before - abbreviated for strict structure compliance)
|
| 255 |
p = {}
|
| 256 |
if "contact" in args: p["Last_Name"] = args["contact"]
|
| 257 |
if "email" in args: p["Email"] = args["email"]
|
| 258 |
-
# ... map other fields ...
|
| 259 |
for k,v in args.items():
|
| 260 |
if k not in ["contact", "email", "items"]: p[k] = v
|
| 261 |
return p
|
| 262 |
|
| 263 |
def _build_invoice_payload_for_zoho(contact_id: str, invoice_items: List[dict], currency: str = None, vat_pct: float = 0.0) -> dict:
|
| 264 |
-
# (Same implementation as before)
|
| 265 |
line_items = []
|
| 266 |
for it in invoice_items:
|
| 267 |
qty = int(it.get("quantity", 1))
|
|
@@ -272,16 +267,14 @@ def _build_invoice_payload_for_zoho(contact_id: str, invoice_items: List[dict],
|
|
| 272 |
return payload
|
| 273 |
|
| 274 |
# ----------------------------
|
| 275 |
-
# Parse & Execute
|
| 276 |
# ----------------------------
|
| 277 |
def parse_and_execute_model_tool_output(model_text: str, history: Optional[List] = None) -> str:
|
| 278 |
-
# FIX: Use Safe Extraction first
|
| 279 |
payload = extract_json_safely(model_text)
|
| 280 |
|
| 281 |
if not payload:
|
| 282 |
return "(Parse) Model output was not valid JSON tool instruction."
|
| 283 |
|
| 284 |
-
# Normalize to list
|
| 285 |
instructions = [payload] if isinstance(payload, dict) else payload
|
| 286 |
results = []
|
| 287 |
contact_id = None
|
|
@@ -293,13 +286,11 @@ def parse_and_execute_model_tool_output(model_text: str, history: Optional[List]
|
|
| 293 |
args = _normalize_local_path_args(args)
|
| 294 |
|
| 295 |
if tool == "create_record":
|
| 296 |
-
# ... (logic same as before)
|
| 297 |
res = create_record(args.get("module", "Contacts"), _map_contact_args_to_zoho_payload(args))
|
| 298 |
results.append(f"create_record -> {res}")
|
| 299 |
contact_id = _extract_created_id_from_zoho_response(res)
|
| 300 |
|
| 301 |
elif tool == "create_invoice":
|
| 302 |
-
# ... (logic same as before)
|
| 303 |
if not contact_id: contact_id = args.get("customer_id")
|
| 304 |
if contact_id:
|
| 305 |
inv_payload = _build_invoice_payload_for_zoho(contact_id, args.get("line_items", []))
|
|
@@ -318,7 +309,6 @@ def parse_and_execute_model_tool_output(model_text: str, history: Optional[List]
|
|
| 318 |
# Command Parser (Debug)
|
| 319 |
# ----------------------------
|
| 320 |
def try_parse_and_invoke_command(text: str):
|
| 321 |
-
# (Same implementation)
|
| 322 |
if text.startswith("/mnt/data/"): return str(process_document(text))
|
| 323 |
return None
|
| 324 |
|
|
@@ -340,24 +330,19 @@ def deepseek_response(message: str, file_path: Optional[str] = None, history: li
|
|
| 340 |
else:
|
| 341 |
return f"Error processing file: {doc_result.get('error')}"
|
| 342 |
|
| 343 |
-
# 2. Build Prompt
|
| 344 |
-
# Flatten history for the prompt
|
| 345 |
history_text = "\n".join([f"User: {h[0]}\nBot: {h[1]}" for h in history])
|
| 346 |
prompt = get_agent_prompt(history_text, ocr_context, message)
|
| 347 |
|
| 348 |
-
# 3. Generate
|
| 349 |
gen = local_llm_generate(prompt, max_tokens=256)
|
| 350 |
response_text = gen["text"]
|
| 351 |
|
| 352 |
-
# 4. Check for JSON Tool Call
|
| 353 |
tool_json = extract_json_safely(response_text)
|
| 354 |
|
| 355 |
if tool_json and isinstance(tool_json, (dict, list)):
|
| 356 |
try:
|
| 357 |
-
# We must pass the RAW text or the JSON object?
|
| 358 |
-
# Your existing function `parse_and_execute...` expects a string or valid json structure.
|
| 359 |
-
# Let's pass the JSON stringified to be safe, or modify the caller.
|
| 360 |
-
# The safest way given your strict structure requirement is:
|
| 361 |
return parse_and_execute_model_tool_output(json.dumps(tool_json), history)
|
| 362 |
except Exception as e:
|
| 363 |
return f"(Execute) Error: {e}"
|
|
@@ -378,7 +363,6 @@ def chat_handler(message, history):
|
|
| 378 |
else:
|
| 379 |
user_text = str(message)
|
| 380 |
|
| 381 |
-
# Debug command bypass
|
| 382 |
if not uploaded_file_path:
|
| 383 |
cmd = try_parse_and_invoke_command(user_text)
|
| 384 |
if cmd: return cmd
|
|
@@ -386,7 +370,7 @@ def chat_handler(message, history):
|
|
| 386 |
return deepseek_response(user_text, uploaded_file_path, history)
|
| 387 |
|
| 388 |
# ----------------------------
|
| 389 |
-
#
|
| 390 |
# ----------------------------
|
| 391 |
def cleanup_event_loop():
|
| 392 |
gc.collect()
|
|
|
|
| 51 |
mcp = FastMCP("ZohoCRMAgent")
|
| 52 |
|
| 53 |
# ----------------------------
|
| 54 |
+
# Analytics
|
| 55 |
# ----------------------------
|
| 56 |
ANALYTICS_PATH = "mcp_analytics.json"
|
| 57 |
def _init_analytics():
|
| 58 |
if not os.path.exists(ANALYTICS_PATH):
|
| 59 |
with open(ANALYTICS_PATH, "w") as f: json.dump({}, f)
|
|
|
|
|
|
|
| 60 |
_init_analytics()
|
| 61 |
|
| 62 |
# ----------------------------
|
| 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:
|
|
|
|
| 94 |
try:
|
| 95 |
logger.info(f"Loading model: {LOCAL_MODEL}...")
|
| 96 |
TOKENIZER = AutoTokenizer.from_pretrained(LOCAL_MODEL, trust_remote_code=True)
|
|
|
|
| 97 |
model = AutoModelForCausalLM.from_pretrained(LOCAL_MODEL, trust_remote_code=True, device_map="auto")
|
| 98 |
|
|
|
|
| 99 |
LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
|
| 100 |
LOADED_MODEL_NAME = LOCAL_MODEL
|
| 101 |
logger.info("Model loaded.")
|
|
|
|
| 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 |
+
# FIX: Added `use_cache=False` to resolve 'DynamicCache' object has no attribute 'seen_tokens'
|
| 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}
|
| 121 |
except Exception as e:
|
| 122 |
+
logger.error(f"Generation Error: {e}")
|
| 123 |
return {"text": f"Error: {e}", "raw": None}
|
| 124 |
|
| 125 |
# ----------------------------
|
| 126 |
+
# Helper: normalize local file_path args
|
| 127 |
# ----------------------------
|
| 128 |
def _normalize_local_path_args(args: Any) -> Any:
|
| 129 |
if not isinstance(args, dict): return args
|
|
|
|
| 133 |
return args
|
| 134 |
|
| 135 |
# ----------------------------
|
| 136 |
+
# Zoho Auth & Tools
|
| 137 |
# ----------------------------
|
| 138 |
def _get_valid_token_headers() -> dict:
|
| 139 |
token_url = "https://accounts.zoho.in/oauth/v2/token"
|
|
|
|
| 212 |
if not raw_text or len(raw_text) < 5:
|
| 213 |
return {"status": "error", "error": "OCR failed to extract text."}
|
| 214 |
|
| 215 |
+
# 2. Use Prompt Template
|
|
|
|
| 216 |
prompt = get_ocr_extraction_prompt(raw_text)
|
| 217 |
|
| 218 |
+
# 3. Generate (with cache fix applied in local_llm_generate)
|
| 219 |
+
llm_out = local_llm_generate(prompt, max_tokens=300)
|
| 220 |
extracted_text = llm_out.get("text", "")
|
| 221 |
|
| 222 |
+
# 4. Extract JSON
|
| 223 |
extracted_data = extract_json_safely(extracted_text)
|
| 224 |
|
| 225 |
if not extracted_data:
|
|
|
|
| 226 |
extracted_data = {"raw_llm_text": extracted_text}
|
| 227 |
|
| 228 |
return {
|
|
|
|
| 235 |
return {"status": "error", "error": str(e)}
|
| 236 |
|
| 237 |
# ----------------------------
|
| 238 |
+
# Helpers: map LLM args -> Zoho payloads
|
| 239 |
# ----------------------------
|
| 240 |
def _extract_created_id_from_zoho_response(resp_json) -> Optional[str]:
|
|
|
|
| 241 |
try:
|
| 242 |
if isinstance(resp_json, str): resp_json = json.loads(resp_json)
|
| 243 |
data = resp_json.get("data") or resp_json.get("result")
|
|
|
|
| 249 |
return None
|
| 250 |
|
| 251 |
def _map_contact_args_to_zoho_payload(args: dict) -> dict:
|
|
|
|
| 252 |
p = {}
|
| 253 |
if "contact" in args: p["Last_Name"] = args["contact"]
|
| 254 |
if "email" in args: p["Email"] = args["email"]
|
|
|
|
| 255 |
for k,v in args.items():
|
| 256 |
if k not in ["contact", "email", "items"]: p[k] = v
|
| 257 |
return p
|
| 258 |
|
| 259 |
def _build_invoice_payload_for_zoho(contact_id: str, invoice_items: List[dict], currency: str = None, vat_pct: float = 0.0) -> dict:
|
|
|
|
| 260 |
line_items = []
|
| 261 |
for it in invoice_items:
|
| 262 |
qty = int(it.get("quantity", 1))
|
|
|
|
| 267 |
return payload
|
| 268 |
|
| 269 |
# ----------------------------
|
| 270 |
+
# Parse & Execute
|
| 271 |
# ----------------------------
|
| 272 |
def parse_and_execute_model_tool_output(model_text: str, history: Optional[List] = None) -> str:
|
|
|
|
| 273 |
payload = extract_json_safely(model_text)
|
| 274 |
|
| 275 |
if not payload:
|
| 276 |
return "(Parse) Model output was not valid JSON tool instruction."
|
| 277 |
|
|
|
|
| 278 |
instructions = [payload] if isinstance(payload, dict) else payload
|
| 279 |
results = []
|
| 280 |
contact_id = None
|
|
|
|
| 286 |
args = _normalize_local_path_args(args)
|
| 287 |
|
| 288 |
if tool == "create_record":
|
|
|
|
| 289 |
res = create_record(args.get("module", "Contacts"), _map_contact_args_to_zoho_payload(args))
|
| 290 |
results.append(f"create_record -> {res}")
|
| 291 |
contact_id = _extract_created_id_from_zoho_response(res)
|
| 292 |
|
| 293 |
elif tool == "create_invoice":
|
|
|
|
| 294 |
if not contact_id: contact_id = args.get("customer_id")
|
| 295 |
if contact_id:
|
| 296 |
inv_payload = _build_invoice_payload_for_zoho(contact_id, args.get("line_items", []))
|
|
|
|
| 309 |
# Command Parser (Debug)
|
| 310 |
# ----------------------------
|
| 311 |
def try_parse_and_invoke_command(text: str):
|
|
|
|
| 312 |
if text.startswith("/mnt/data/"): return str(process_document(text))
|
| 313 |
return None
|
| 314 |
|
|
|
|
| 330 |
else:
|
| 331 |
return f"Error processing file: {doc_result.get('error')}"
|
| 332 |
|
| 333 |
+
# 2. Build Prompt
|
|
|
|
| 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 (Cache Fix applies here too)
|
| 338 |
gen = local_llm_generate(prompt, max_tokens=256)
|
| 339 |
response_text = gen["text"]
|
| 340 |
|
| 341 |
+
# 4. Check for JSON Tool Call
|
| 342 |
tool_json = extract_json_safely(response_text)
|
| 343 |
|
| 344 |
if tool_json and isinstance(tool_json, (dict, list)):
|
| 345 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
return parse_and_execute_model_tool_output(json.dumps(tool_json), history)
|
| 347 |
except Exception as e:
|
| 348 |
return f"(Execute) Error: {e}"
|
|
|
|
| 363 |
else:
|
| 364 |
user_text = str(message)
|
| 365 |
|
|
|
|
| 366 |
if not uploaded_file_path:
|
| 367 |
cmd = try_parse_and_invoke_command(user_text)
|
| 368 |
if cmd: return cmd
|
|
|
|
| 370 |
return deepseek_response(user_text, uploaded_file_path, history)
|
| 371 |
|
| 372 |
# ----------------------------
|
| 373 |
+
# Cleanup
|
| 374 |
# ----------------------------
|
| 375 |
def cleanup_event_loop():
|
| 376 |
gc.collect()
|