vachaspathi commited on
Commit
54efa47
·
verified ·
1 Parent(s): 168e3cd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -308
app.py CHANGED
@@ -7,10 +7,8 @@ import os
7
  import gradio as gr
8
  import json
9
  import time
10
- import traceback
11
  import re
12
  import logging
13
- import base64
14
  import asyncio
15
  import gc
16
 
@@ -19,390 +17,230 @@ try:
19
  from ocr_engine import extract_text_from_file
20
  from prompts import get_ocr_extraction_prompt, get_agent_prompt
21
  except ImportError:
22
- # Fallback
23
- def extract_text_from_file(path): return "OCR Engine not loaded."
24
  def get_ocr_extraction_prompt(txt): return txt
25
  def get_agent_prompt(h, c, u): return u
26
 
27
- # Setup logging
28
  logging.basicConfig(level=logging.INFO)
29
  logger = logging.getLogger("mcp_server")
30
 
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)
38
- TRANSFORMERS_AVAILABLE = False
39
-
40
- # ----------------------------
41
- # Load config
42
- # ----------------------------
43
  try:
44
  from config import (
45
  CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, API_BASE,
46
  INVOICE_API_BASE, ORGANIZATION_ID, LOCAL_MODEL
47
  )
48
- except Exception as e:
49
- raise SystemExit("Config missing. Check config.py.")
50
 
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
  try:
67
  return json.loads(text)
68
  except:
69
- pass
70
- try:
71
  match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
72
- if match:
73
- json_str = match.group(0)
74
- return json.loads(json_str)
75
- except:
76
- pass
77
- return None
78
 
79
- # ----------------------------
80
- # Local LLM loader (Lazy Loading Fix)
81
- # ----------------------------
82
- LLM_PIPELINE = None
83
- TOKENIZER = None
84
- LOADED_MODEL_NAME = 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)
99
- model = AutoModelForCausalLM.from_pretrained(LOCAL_MODEL, trust_remote_code=True, device_map="auto")
 
 
 
 
 
 
 
 
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}
128
  except Exception as e:
129
- logger.error(f"Generation Error: {e}")
130
  return {"text": f"Error: {e}", "raw": None}
131
 
132
- # ----------------------------
133
- # Helper: normalize local file_path args
134
- # ----------------------------
135
- def _normalize_local_path_args(args: Any) -> Any:
136
- if not isinstance(args, dict): return args
137
- fp = args.get("file_path") or args.get("path")
138
- if isinstance(fp, str) and fp.startswith("/mnt/data/") and os.path.exists(fp):
139
- args["file_url"] = f"file://{fp}"
140
- return args
141
-
142
- # ----------------------------
143
- # Zoho Auth & Tools
144
- # ----------------------------
145
  def _get_valid_token_headers() -> dict:
146
- token_url = "https://accounts.zoho.in/oauth/v2/token"
147
- params = {
148
  "refresh_token": REFRESH_TOKEN, "client_id": CLIENT_ID,
149
  "client_secret": CLIENT_SECRET, "grant_type": "refresh_token"
150
- }
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)
170
- return f"Error: {r.text}"
171
-
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
-
193
- def _ensure_invoice_config():
194
- if not INVOICE_API_BASE or not ORGANIZATION_ID: raise RuntimeError("Invoice Config Missing")
195
 
196
  @mcp.tool()
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)
204
- return f"Error creating invoice: {r.text}"
205
-
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:
213
- files = {"attachment": (os.path.basename(file_path), f)}
214
- r = requests.post(url, headers=headers, params={"organization_id": ORGANIZATION_ID}, files=files)
215
  return json.dumps(r.json()) if r.status_code in (200, 201) else r.text
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"}
222
-
223
- # 1. Perform OCR
224
- raw_text = extract_text_from_file(file_path)
225
- if not raw_text or len(raw_text) < 5:
226
- return {"status": "error", "error": "OCR failed to extract text."}
227
-
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
-
235
- # 4. Extract JSON
236
- extracted_data = extract_json_safely(extracted_text)
237
-
238
- if not extracted_data:
239
- extracted_data = {"raw_llm_text": extracted_text}
240
-
241
- return {
242
- "status": "success",
243
- "file": os.path.basename(file_path),
244
- "extracted_data": extracted_data
245
- }
246
-
247
- except Exception as e:
248
- return {"status": "error", "error": str(e)}
249
-
250
- # ----------------------------
251
- # Helpers: map LLM args -> Zoho payloads
252
- # ----------------------------
253
- def _extract_created_id_from_zoho_response(resp_json) -> Optional[str]:
254
- try:
255
- if isinstance(resp_json, str): resp_json = json.loads(resp_json)
256
- data = resp_json.get("data") or resp_json.get("result")
257
- if data and isinstance(data, list):
258
- d = data[0].get("details") or data[0]
259
- return str(d.get("id") or d.get("ID") or d.get("Id"))
260
- if "invoice" in resp_json: return str(resp_json["invoice"].get("invoice_id"))
261
- except: pass
262
- return None
263
-
264
- def _map_contact_args_to_zoho_payload(args: dict) -> dict:
265
- p = {}
266
- if "contact" in args: p["Last_Name"] = args["contact"]
267
- if "email" in args: p["Email"] = args["email"]
268
- for k,v in args.items():
269
- if k not in ["contact", "email", "items"]: p[k] = v
270
- return p
271
-
272
- def _build_invoice_payload_for_zoho(contact_id: str, invoice_items: List[dict], currency: str = None, vat_pct: float = 0.0) -> dict:
273
- line_items = []
274
- for it in invoice_items:
275
- qty = int(it.get("quantity", 1))
276
- rate = float(str(it.get("rate", 0)).replace("$",""))
277
- line_items.append({"name": it.get("name","Item"), "rate": rate, "quantity": qty})
278
- payload = {"customer_id": contact_id, "line_items": line_items}
279
- if currency: payload["currency_code"] = currency
280
- return payload
281
 
282
- # ----------------------------
283
- # Parse & Execute
284
- # ----------------------------
285
- def parse_and_execute_model_tool_output(model_text: str, history: Optional[List] = None) -> str:
286
  payload = extract_json_safely(model_text)
 
287
 
288
- if not payload:
289
- return "(Parse) Model output was not valid JSON tool instruction."
290
-
291
- instructions = [payload] if isinstance(payload, dict) else payload
292
  results = []
293
- contact_id = None
 
 
294
 
295
- for instr in instructions:
296
- if not isinstance(instr, dict): continue
297
- tool = instr.get("tool")
298
- args = instr.get("args", {})
299
- args = _normalize_local_path_args(args)
300
 
301
  if tool == "create_record":
302
- res = create_record(args.get("module", "Contacts"), _map_contact_args_to_zoho_payload(args))
303
- results.append(f"create_record -> {res}")
304
- contact_id = _extract_created_id_from_zoho_response(res)
305
-
 
 
 
 
 
306
  elif tool == "create_invoice":
307
- if not contact_id: contact_id = args.get("customer_id")
308
- if contact_id:
309
- inv_payload = _build_invoice_payload_for_zoho(contact_id, args.get("line_items", []))
310
- res = create_invoice(inv_payload)
311
- results.append(f"create_invoice -> {res}")
312
- else:
313
- results.append("Skipped invoice: missing contact_id")
314
-
315
- elif tool == "process_document":
316
- res = process_document(args.get("file_path"))
317
- results.append(f"process -> {res}")
 
 
 
 
 
 
 
318
 
319
- return "\n".join(results) if results else "No tools executed."
 
 
320
 
321
- # ----------------------------
322
- # Command Parser (Debug)
323
- # ----------------------------
324
- def try_parse_and_invoke_command(text: str):
325
- if text.startswith("/mnt/data/"): return str(process_document(text))
326
- return None
327
 
328
- # ----------------------------
329
- # Chat Logic
330
- # ----------------------------
331
- def deepseek_response(message: str, file_path: Optional[str] = None, history: list = []) -> str:
332
-
333
- # 1. Handle File (OCR)
334
- ocr_context = ""
335
  if file_path:
336
- logger.info(f"Processing file: {file_path}")
337
- doc_result = process_document(file_path)
338
- if doc_result.get("status") == "success":
339
- data = doc_result["extracted_data"]
340
- ocr_context = json.dumps(data, ensure_ascii=False)
341
- if not message:
342
- message = "I uploaded a file. Create the contact and invoice."
343
  else:
344
- return f"Error processing file: {doc_result.get('error')}"
345
 
346
- # 2. Build Prompt
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
 
354
- # 4. Check for JSON Tool Call
355
- tool_json = extract_json_safely(response_text)
356
 
357
- if tool_json and isinstance(tool_json, (dict, list)):
358
- try:
359
- return parse_and_execute_model_tool_output(json.dumps(tool_json), history)
360
- except Exception as e:
361
- return f"(Execute) Error: {e}"
362
-
363
- return response_text
364
 
365
- # ----------------------------
366
- # Chat Handler
367
- # ----------------------------
368
- def chat_handler(message, history):
369
- user_text = ""
370
- uploaded_file_path = None
371
 
372
- if isinstance(message, dict):
373
- user_text = message.get("text", "")
374
- files = message.get("files", [])
375
- if files: uploaded_file_path = files[0]
376
- else:
377
- user_text = str(message)
378
-
379
- if not uploaded_file_path:
380
- cmd = try_parse_and_invoke_command(user_text)
381
- if cmd: return cmd
382
-
383
- return deepseek_response(user_text, uploaded_file_path, history)
384
-
385
- # ----------------------------
386
- # Cleanup
387
- # ----------------------------
388
- def cleanup_event_loop():
389
- gc.collect()
390
- try:
391
- loop = asyncio.get_event_loop()
392
- if loop.is_closed():
393
- asyncio.set_event_loop(asyncio.new_event_loop())
394
- except RuntimeError:
395
- asyncio.set_event_loop(asyncio.new_event_loop())
396
 
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,
406
- textbox=gr.MultimodalTextbox(interactive=True, file_count="single", placeholder="Upload Invoice or ask to create records...")
407
- )
408
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
7
  import gradio as gr
8
  import json
9
  import time
 
10
  import re
11
  import logging
 
12
  import asyncio
13
  import gc
14
 
 
17
  from ocr_engine import extract_text_from_file
18
  from prompts import get_ocr_extraction_prompt, get_agent_prompt
19
  except ImportError:
20
+ def extract_text_from_file(path): return ""
 
21
  def get_ocr_extraction_prompt(txt): return txt
22
  def get_agent_prompt(h, c, u): return u
23
 
 
24
  logging.basicConfig(level=logging.INFO)
25
  logger = logging.getLogger("mcp_server")
26
 
27
+ # --- Load Config ---
 
 
 
 
 
 
 
 
 
 
 
28
  try:
29
  from config import (
30
  CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, API_BASE,
31
  INVOICE_API_BASE, ORGANIZATION_ID, LOCAL_MODEL
32
  )
33
+ except Exception:
34
+ raise SystemExit("Config missing.")
35
 
36
  mcp = FastMCP("ZohoCRMAgent")
37
 
38
+ # --- Globals ---
39
+ LLM_PIPELINE = None
40
+ TOKENIZER = None
 
 
 
 
 
41
 
42
+ # --- Helpers ---
 
 
43
  def extract_json_safely(text: str) -> Optional[Any]:
44
  try:
45
  return json.loads(text)
46
  except:
 
 
47
  match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
48
+ return json.loads(match.group(0)) if match else None
 
 
 
 
 
49
 
50
+ def _normalize_local_path_args(args: Any) -> Any:
51
+ if not isinstance(args, dict): return args
52
+ fp = args.get("file_path") or args.get("path")
53
+ if isinstance(fp, str) and fp.startswith("/mnt/data/") and os.path.exists(fp):
54
+ args["file_url"] = f"file://{fp}"
55
+ return args
56
 
57
+ # --- Model Loading (Lazy & Light) ---
58
  def init_local_model():
59
+ global LLM_PIPELINE, TOKENIZER
60
+ if LLM_PIPELINE is not None: return
 
 
 
 
 
 
61
 
62
  try:
63
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
64
+
65
+ logger.info(f"Loading lighter model: {LOCAL_MODEL}...")
66
+ TOKENIZER = AutoTokenizer.from_pretrained(LOCAL_MODEL)
67
+
68
+ # Load model (Standard load is fine for Qwen on CPU)
69
+ model = AutoModelForCausalLM.from_pretrained(
70
+ LOCAL_MODEL,
71
+ device_map="auto",
72
+ torch_dtype="auto"
73
+ )
74
 
75
  LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
76
+ logger.info("Model loaded.")
 
77
  except Exception as e:
78
+ logger.error(f"Model load error: {e}")
 
 
 
79
 
80
  def local_llm_generate(prompt: str, max_tokens: int = 512) -> Dict[str, Any]:
 
81
  if LLM_PIPELINE is None:
82
  init_local_model()
 
 
 
83
 
84
+ if LLM_PIPELINE is None:
85
+ return {"text": "Model not loaded.", "raw": None}
86
+
87
  try:
88
+ # Standard generation (Qwen is robust, no cache hacks needed)
89
  out = LLM_PIPELINE(
90
  prompt,
91
  max_new_tokens=max_tokens,
92
+ return_full_text=False,
93
+ do_sample=False, # Deterministic for tools
94
+ temperature=0.0
95
  )
96
  text = out[0]["generated_text"] if out else ""
97
  return {"text": text, "raw": out}
98
  except Exception as e:
 
99
  return {"text": f"Error: {e}", "raw": None}
100
 
101
+ # --- Tools (Zoho) ---
 
 
 
 
 
 
 
 
 
 
 
 
102
  def _get_valid_token_headers() -> dict:
103
+ r = requests.post("https://accounts.zoho.in/oauth/v2/token", params={
 
104
  "refresh_token": REFRESH_TOKEN, "client_id": CLIENT_ID,
105
  "client_secret": CLIENT_SECRET, "grant_type": "refresh_token"
106
+ }, timeout=10)
 
107
  if r.status_code == 200:
108
  return {"Authorization": f"Zoho-oauthtoken {r.json().get('access_token')}"}
 
 
109
  return {}
110
 
 
 
 
 
 
111
  @mcp.tool()
112
  def create_record(module_name: str, record_data: dict) -> str:
113
+ h = _get_valid_token_headers()
114
+ if not h: return "Auth Failed"
115
+ r = requests.post(f"{API_BASE}/{module_name}", headers=h, json={"data": [record_data]})
116
+ if r.status_code in (200, 201):
117
+ # Extract ID for downstream use
118
+ try:
119
+ d = r.json().get("data", [{}])[0].get("details", {})
120
+ return json.dumps({"status": "success", "id": d.get("id"), "response": r.json()})
121
+ except:
122
+ return json.dumps(r.json())
123
+ return r.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  @mcp.tool()
126
  def create_invoice(data: dict) -> str:
127
+ h = _get_valid_token_headers()
128
+ if not h: return "Auth Failed"
129
+ r = requests.post(f"{INVOICE_API_BASE}/invoices", headers=h,
130
+ params={"organization_id": ORGANIZATION_ID}, json=data)
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  return json.dumps(r.json()) if r.status_code in (200, 201) else r.text
132
 
133
  @mcp.tool()
134
  def process_document(file_path: str, target_module: Optional[str] = "Contacts") -> dict:
135
+ if not os.path.exists(file_path): return {"error": "File not found"}
136
+
137
+ # 1. OCR
138
+ raw_text = extract_text_from_file(file_path)
139
+ if not raw_text: return {"error": "OCR empty"}
140
+
141
+ # 2. LLM Extraction
142
+ prompt = get_ocr_extraction_prompt(raw_text)
143
+ res = local_llm_generate(prompt, max_tokens=300)
144
+ data = extract_json_safely(res["text"])
145
+
146
+ return {
147
+ "status": "success",
148
+ "file": os.path.basename(file_path),
149
+ "extracted_data": data or {"raw": res["text"]}
150
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ # --- Executor ---
153
+ def parse_and_execute(model_text: str, history: list) -> str:
 
 
154
  payload = extract_json_safely(model_text)
155
+ if not payload: return "No valid tool call found."
156
 
157
+ # Normalize
158
+ cmds = [payload] if isinstance(payload, dict) else payload
 
 
159
  results = []
160
+
161
+ # Context State
162
+ last_contact_id = None
163
 
164
+ for cmd in cmds:
165
+ if not isinstance(cmd, dict): continue
166
+ tool = cmd.get("tool")
167
+ args = _normalize_local_path_args(cmd.get("args", {}))
 
168
 
169
  if tool == "create_record":
170
+ res = create_record(args.get("module", "Contacts"), args)
171
+ results.append(f"Record: {res}")
172
+ # Try capture ID
173
+ try:
174
+ rj = json.loads(res)
175
+ if isinstance(rj, dict) and "id" in rj:
176
+ last_contact_id = rj["id"]
177
+ except: pass
178
+
179
  elif tool == "create_invoice":
180
+ # Auto-fill contact_id if we just created one
181
+ if not args.get("customer_id") and last_contact_id:
182
+ args["customer_id"] = last_contact_id
183
+
184
+ # Map Items
185
+ items = []
186
+ for it in args.get("line_items", []):
187
+ items.append({
188
+ "name": it.get("name", "Item"),
189
+ "rate": float(str(it.get("rate", 0)).replace("$", "")),
190
+ "quantity": int(it.get("quantity", 1))
191
+ })
192
+
193
+ payload = {"customer_id": args.get("customer_id"), "line_items": items}
194
+ if args.get("currency"): payload["currency_code"] = args["currency"]
195
+
196
+ res = create_invoice(payload)
197
+ results.append(f"Invoice: {res}")
198
 
199
+ elif tool == "process_document":
200
+ res = process_document(args.get("file_path"))
201
+ results.append(f"Processed: {res}")
202
 
203
+ return "\n".join(results)
 
 
 
 
 
204
 
205
+ # --- Chat Core ---
206
+ def chat_logic(message: str, file_path: str, history: list) -> str:
207
+ # 1. Ingest File
208
+ file_context = ""
 
 
 
209
  if file_path:
210
+ doc = process_document(file_path)
211
+ if doc.get("status") == "success":
212
+ file_context = json.dumps(doc["extracted_data"])
213
+ if not message: message = "Create records from this file."
 
 
 
214
  else:
215
+ return f"OCR Failed: {doc}"
216
 
217
+ # 2. Decision
218
+ hist_txt = "\n".join([f"U: {h[0]}\nA: {h[1]}" for h in history])
219
+ prompt = get_agent_prompt(hist_txt, file_context, message)
220
 
221
+ # 3. Gen & Execute
222
+ gen = local_llm_generate(prompt, max_tokens=200)
223
+ tool_data = extract_json_safely(gen["text"])
224
 
225
+ if tool_data:
226
+ return parse_and_execute(gen["text"], history)
227
 
228
+ return gen["text"]
 
 
 
 
 
 
229
 
230
+ # --- UI ---
231
+ def chat_handler(msg, hist):
232
+ txt = msg.get("text", "")
233
+ files = msg.get("files", [])
234
+ path = files[0] if files else None
 
235
 
236
+ # Direct path bypass for debugging
237
+ if not path and txt.startswith("/mnt/data"):
238
+ return str(process_document(txt))
239
+
240
+ return chat_logic(txt, path, hist)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  if __name__ == "__main__":
243
+ gc.collect()
244
+ # Lazy init will happen on first request, saving startup memory
245
+ demo = gr.ChatInterface(fn=chat_handler, multimodal=True)
 
 
 
 
 
 
 
246
  demo.launch(server_name="0.0.0.0", server_port=7860)