vachaspathi commited on
Commit
759a82d
·
verified ·
1 Parent(s): 85f38e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +181 -155
app.py CHANGED
@@ -1,5 +1,7 @@
1
- # app.py — MCP POC using OpenRouter for LLM (replaces local HF model)
2
- # Place this file next to config.py. Do NOT store secrets here.
 
 
3
 
4
  from mcp.server.fastmcp import FastMCP
5
  from typing import Optional, List, Tuple, Any, Dict
@@ -11,6 +13,20 @@ import time
11
  import traceback
12
  import inspect
13
  import re
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  # ----------------------------
16
  # Load config
@@ -21,20 +37,16 @@ try:
21
  CLIENT_SECRET,
22
  REFRESH_TOKEN,
23
  API_BASE,
24
- OPENROUTER_API_KEY, # your OpenRouter API key (put this in config.py)
25
- OPENROUTER_MODEL # e.g. "gpt-4o-mini" or any model name routed by OpenRouter
26
  )
27
- except Exception:
28
  raise SystemExit(
29
- "Make sure config.py exists with CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, API_BASE, "
30
- "OPENROUTER_API_KEY and OPENROUTER_MODEL (or set OPENROUTER_MODEL to your preferred model)."
31
  )
32
 
33
- # OpenRouter endpoint (public OpenRouter cloud endpoint)
34
- OPENROUTER_BASE_URL = "https://api.openrouter.ai/v1"
35
-
36
  # ----------------------------
37
- # Initialize FastMCP
38
  # ----------------------------
39
  mcp = FastMCP("ZohoCRMAgent")
40
 
@@ -45,12 +57,7 @@ ANALYTICS_PATH = "mcp_analytics.json"
45
 
46
  def _init_analytics():
47
  if not os.path.exists(ANALYTICS_PATH):
48
- base = {
49
- "tool_calls": {},
50
- "llm_calls": 0,
51
- "last_llm_confidence": None,
52
- "created_at": time.time()
53
- }
54
  with open(ANALYTICS_PATH, "w") as f:
55
  json.dump(base, f, indent=2)
56
 
@@ -84,86 +91,126 @@ def _log_llm_call(confidence: Optional[float] = None):
84
  _init_analytics()
85
 
86
  # ----------------------------
87
- # OpenRouter wrapper
88
  # ----------------------------
89
- def _openrouter_headers():
90
- return {"Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json"}
 
91
 
92
- def openrouter_generate(system_prompt: str, user_prompt: str, history: Optional[List[Tuple[str,str]]] = None, max_tokens: int = 512) -> Dict[str, Any]:
93
  """
94
- Call OpenRouter chat completions endpoint with messages built from system + history + user prompt.
95
- Returns dict: {'text': <str>, 'raw': <resp_json>, 'confidence': Optional[float]}
 
 
 
 
96
  """
97
- messages = []
98
- # system
99
- if system_prompt:
100
- messages.append({"role": "system", "content": system_prompt})
101
- # history (list of (user,assistant))
102
- history = history or []
103
- for pair in history:
104
- try:
105
- u, a = pair[0], pair[1]
106
- if u:
107
- messages.append({"role": "user", "content": u})
108
- if a:
109
- messages.append({"role": "assistant", "content": a})
110
- except Exception:
111
- continue
112
- # current user
113
- messages.append({"role": "user", "content": user_prompt})
114
 
115
- body = {
116
- "model": OPENROUTER_MODEL,
117
- "messages": messages,
118
- "max_tokens": max_tokens,
119
- # "temperature": 0.0, # set if you want deterministic responses
120
- }
 
 
 
121
 
122
- url = f"{OPENROUTER_BASE_URL}/chat/completions"
123
  try:
124
- r = requests.post(url, headers=_openrouter_headers(), json=body, timeout=60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  except Exception as e:
126
- raise RuntimeError(f"OpenRouter request failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
- if r.status_code not in (200, 201):
129
- # surface the error for debugging
130
- raise RuntimeError(f"OpenRouter API error {r.status_code}: {r.text}")
 
 
 
 
 
 
131
 
132
- resp_json = r.json()
133
- # Parse response for text; different routers may vary slightly
134
- text = ""
135
- confidence = None
136
  try:
137
- # typical shape: choices[0].message.content or choices[0].message
138
- choice = resp_json.get("choices", [{}])[0]
139
- message = choice.get("message", {}) or {}
140
- if isinstance(message, dict):
141
- text = message.get("content") or message.get("content", "")
142
- # sometimes content is a dict mapping types -> text
143
- if isinstance(text, dict):
144
- # join possible parts
145
- text = text.get("text") or next(iter(text.values()), "")
146
  else:
147
- text = str(message)
148
- # some providers include scores
149
- confidence = choice.get("finish_reason_score") or choice.get("score") or None
150
- except Exception:
151
- text = json.dumps(resp_json)
152
-
153
- _log_llm_call(confidence)
154
- return {"text": text, "raw": resp_json, "confidence": confidence}
155
 
156
  # ----------------------------
157
- # Zoho token refresh & headers
158
  # ----------------------------
159
  def _get_valid_token_headers() -> dict:
160
  token_url = "https://accounts.zoho.in/oauth/v2/token"
161
- params = {
162
- "refresh_token": REFRESH_TOKEN,
163
- "client_id": CLIENT_ID,
164
- "client_secret": CLIENT_SECRET,
165
- "grant_type": "refresh_token"
166
- }
167
  r = requests.post(token_url, params=params, timeout=20)
168
  if r.status_code == 200:
169
  t = r.json().get("access_token")
@@ -171,9 +218,6 @@ def _get_valid_token_headers() -> dict:
171
  else:
172
  raise RuntimeError(f"Failed to refresh Zoho token: {r.status_code} {r.text}")
173
 
174
- # ----------------------------
175
- # MCP tools: Zoho CRUD & process_document (unchanged)
176
- # ----------------------------
177
  @mcp.tool()
178
  def authenticate_zoho() -> str:
179
  try:
@@ -264,9 +308,9 @@ def create_invoice(data: dict) -> str:
264
  @mcp.tool()
265
  def process_document(file_path: str, target_module: Optional[str] = "Contacts") -> dict:
266
  """
267
- Process an uploaded file path (local path or URL). Per developer instruction,
268
- we accept local paths like '/mnt/data/script_zoho_mcp' and return a file:// URL.
269
- Replace the placeholder OCR block with your real OCR pipeline when ready.
270
  """
271
  try:
272
  if os.path.exists(file_path):
@@ -279,13 +323,7 @@ def process_document(file_path: str, target_module: Optional[str] = "Contacts")
279
  "Confidence": 0.88
280
  }
281
  _log_tool_call("process_document", True)
282
- return {
283
- "status": "success",
284
- "file": os.path.basename(file_path),
285
- "file_url": file_url,
286
- "target_module": target_module,
287
- "extracted_data": extracted
288
- }
289
  else:
290
  _log_tool_call("process_document", False)
291
  return {"status": "error", "error": "file not found", "file_path": file_path}
@@ -294,76 +332,62 @@ def process_document(file_path: str, target_module: Optional[str] = "Contacts")
294
  return {"status": "error", "error": str(e)}
295
 
296
  # ----------------------------
297
- # Simple local command parser
298
  # ----------------------------
299
  def try_parse_and_invoke_command(text: str):
300
  text = text.strip()
301
- # create_record
302
  m = re.match(r"^create_record\s+(\w+)\s+(.+)$", text, re.I)
303
  if m:
304
- module = m.group(1)
305
- body = m.group(2)
306
- try:
307
- record_data = json.loads(body)
308
- except Exception:
309
- return "Invalid JSON for record_data"
310
  return create_record(module, record_data)
311
-
312
- # create_invoice
313
  m = re.match(r"^create_invoice\s+(.+)$", text, re.I)
314
  if m:
315
  body = m.group(1)
316
- try:
317
- invoice_data = json.loads(body)
318
- except Exception:
319
- return "Invalid JSON for invoice_data"
320
  return create_invoice(invoice_data)
321
-
322
- # process_document via local path
323
  m = re.match(r"^(\/mnt\/data\/\S+)$", text)
324
  if m:
325
- path = m.group(1)
326
- return process_document(path)
327
-
328
  return None
329
 
330
  # ----------------------------
331
- # OpenRouter-based chat handler
332
  # ----------------------------
333
- def openrouter_response(message: str, history: Optional[List[Tuple[str,str]]] = None) -> str:
334
  history = history or []
335
- system_prompt = (
336
- "You are Zoho Assistant. Keep responses concise. When appropriate, output a JSON object with keys 'tool' and 'args' "
337
- "so the server can automatically call the corresponding MCP tool. Example:\n"
338
- '{"tool":"create_record","args":{"module_name":"Contacts","record_data":{"Last_Name":"Doe","Email":"[email protected]"}}}\n'
339
- "If not invoking tools, answer conversationally."
340
- )
341
- try:
342
- resp = openrouter_generate(system_prompt, message, history)
343
- text = resp.get("text", "")
344
- # If LLM returned JSON indicating a tool invocation, attempt to parse & run
345
- parsed = None
346
- payload = text.strip()
347
- if payload.startswith("{") or payload.startswith("["):
348
- try:
349
- parsed = json.loads(payload)
350
- except Exception:
351
- parsed = None
352
- if isinstance(parsed, dict) and "tool" in parsed:
353
- tool_name = parsed.get("tool")
354
- args = parsed.get("args", {})
355
- # Try call local tool by name if exists
356
- if tool_name in globals() and callable(globals()[tool_name]):
357
- try:
358
- result = globals()[tool_name](**args) if isinstance(args, dict) else globals()[tool_name](args)
359
- return f"Invoked tool '{tool_name}'. Result:\n{result}"
360
- except Exception as e:
361
- return f"Tool invocation error: {e}"
362
- else:
363
- return f"Requested tool '{tool_name}' not found locally."
364
- return text
365
- except Exception as e:
366
- return f"(OpenRouter error) {e}"
367
 
368
  # ----------------------------
369
  # Gradio chat handler
@@ -372,12 +396,12 @@ def chat_handler(message, history):
372
  history = history or []
373
  trimmed = (message or "").strip()
374
 
375
- # Explicit POC commands
376
  cmd = try_parse_and_invoke_command(trimmed)
377
  if cmd is not None:
378
  return cmd
379
 
380
- # Developer convenience: local path handling (send unchanged)
381
  if trimmed.startswith("/mnt/data/"):
382
  try:
383
  doc = process_document(trimmed)
@@ -385,22 +409,24 @@ def chat_handler(message, history):
385
  except Exception as e:
386
  return f"Error processing document: {e}"
387
 
388
- # Otherwise, call OpenRouter
389
- return openrouter_response(trimmed, history)
 
 
 
 
 
390
 
391
  # ----------------------------
392
  # Gradio UI
393
  # ----------------------------
394
  def chat_interface():
395
- return gr.ChatInterface(
396
- fn=chat_handler,
397
- textbox=gr.Textbox(placeholder="Ask me to create contacts, invoices, upload docs (or paste /mnt/data/... for dev).")
398
- )
399
 
400
  # ----------------------------
401
  # Entrypoint
402
  # ----------------------------
403
  if __name__ == "__main__":
404
- print("[startup] Launching Gradio UI + FastMCP server (OpenRouter mode).")
405
  demo = chat_interface()
406
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ # app.py — MCP server using DeepSeek via Hugging Face transformers (or fallback)
2
+ # - Put this file next to config.py (see example below)
3
+ # - It loads the model in LOCAL_MODEL (e.g., a DeepSeek HF checkpoint) via transformers.pipeline
4
+ # - If the model cannot be loaded (no transformers / OOM / missing weights), it falls back to a small CPU model or rule-based responder
5
 
6
  from mcp.server.fastmcp import FastMCP
7
  from typing import Optional, List, Tuple, Any, Dict
 
13
  import traceback
14
  import inspect
15
  import re
16
+ import logging
17
+
18
+ # Setup simple logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger("mcp_deepseek")
21
+
22
+ # Optional transformers imports — will attempt; we handle missing gracefully
23
+ TRANSFORMERS_AVAILABLE = False
24
+ try:
25
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM, AutoModelForSeq2SeqLM
26
+ TRANSFORMERS_AVAILABLE = True
27
+ except Exception as e:
28
+ logger.warning("transformers not available: %s", e)
29
+ TRANSFORMERS_AVAILABLE = False
30
 
31
  # ----------------------------
32
  # Load config
 
37
  CLIENT_SECRET,
38
  REFRESH_TOKEN,
39
  API_BASE,
40
+ LOCAL_MODEL, # e.g. "deepseek-ai/deepseek-r1-7b" or smaller/distilled variant
41
+ LOCAL_TOKENIZER # optional: tokenizer name if different
42
  )
43
+ except Exception as e:
44
  raise SystemExit(
45
+ "Make sure config.py exists with CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, API_BASE, LOCAL_MODEL (or set LOCAL_MODEL=None)."
 
46
  )
47
 
 
 
 
48
  # ----------------------------
49
+ # FastMCP init
50
  # ----------------------------
51
  mcp = FastMCP("ZohoCRMAgent")
52
 
 
57
 
58
  def _init_analytics():
59
  if not os.path.exists(ANALYTICS_PATH):
60
+ base = {"tool_calls": {}, "llm_calls": 0, "last_llm_confidence": None, "created_at": time.time()}
 
 
 
 
 
61
  with open(ANALYTICS_PATH, "w") as f:
62
  json.dump(base, f, indent=2)
63
 
 
91
  _init_analytics()
92
 
93
  # ----------------------------
94
+ # DeepSeek / HF model loader
95
  # ----------------------------
96
+ LLM_PIPELINE = None
97
+ TOKENIZER = None
98
+ LOADED_MODEL_NAME = None
99
 
100
+ def init_deepseek_model():
101
  """
102
+ Try to load LOCAL_MODEL via transformers.pipeline.
103
+ Expected LOCAL_MODEL examples:
104
+ - "deepseek-ai/deepseek-r1-7b" (may require GPU; big)
105
+ - "deepseek-ai/deepseek-r1-3b" (smaller)
106
+ - "deepseek-ai/deepseek-r1-1.3b" (more likely to load on moderate machines)
107
+ If loading fails, try a fallback small model (distilgpt2 or flan-t5-small if seq2seq).
108
  """
109
+ global LLM_PIPELINE, TOKENIZER, LOADED_MODEL_NAME
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ if not LOCAL_MODEL:
112
+ logger.info("LOCAL_MODEL is None — no local LLM will be used.")
113
+ LLM_PIPELINE = None
114
+ return
115
+
116
+ if not TRANSFORMERS_AVAILABLE:
117
+ logger.warning("transformers not installed; cannot load DeepSeek. Falling back to rule-based.")
118
+ LLM_PIPELINE = None
119
+ return
120
 
 
121
  try:
122
+ tokenizer_name = LOCAL_TOKENIZER or LOCAL_MODEL
123
+ model_name = LOCAL_MODEL
124
+ LOADED_MODEL_NAME = model_name
125
+
126
+ # If model looks like seq2seq (T5/flan) use text2text; else causal
127
+ seq2seq_keywords = ["flan", "t5", "seq2seq"]
128
+ if any(k in model_name.lower() for k in seq2seq_keywords):
129
+ TOKENIZER = AutoTokenizer.from_pretrained(tokenizer_name, use_fast=True)
130
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
131
+ LLM_PIPELINE = pipeline("text2text-generation", model=model, tokenizer=TOKENIZER)
132
+ logger.info("Loaded seq2seq model: %s", model_name)
133
+ else:
134
+ TOKENIZER = AutoTokenizer.from_pretrained(tokenizer_name, use_fast=True)
135
+ model = AutoModelForCausalLM.from_pretrained(model_name)
136
+ LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
137
+ logger.info("Loaded causal model: %s", model_name)
138
+
139
  except Exception as e:
140
+ logger.error("Failed to load requested model '%s': %s", LOCAL_MODEL, e)
141
+ traceback.print_exc()
142
+ # Try a small CPU-friendly fallback
143
+ fallback = None
144
+ try:
145
+ # prefer an instruction-friendly small model if possible
146
+ fallback = "google/flan-t5-small"
147
+ if "flan" in fallback:
148
+ TOKENIZER = AutoTokenizer.from_pretrained(fallback, use_fast=True)
149
+ model = AutoModelForSeq2SeqLM.from_pretrained(fallback)
150
+ LLM_PIPELINE = pipeline("text2text-generation", model=model, tokenizer=TOKENIZER)
151
+ else:
152
+ TOKENIZER = AutoTokenizer.from_pretrained("distilgpt2", use_fast=True)
153
+ model = AutoModelForCausalLM.from_pretrained("distilgpt2")
154
+ LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
155
+ LOADED_MODEL_NAME = fallback
156
+ logger.info("Loaded fallback model: %s", fallback)
157
+ except Exception as e2:
158
+ logger.error("Fallback model also failed: %s", e2)
159
+ traceback.print_exc()
160
+ LLM_PIPELINE = None
161
+ LOADED_MODEL_NAME = None
162
+
163
+ # Initialize model at startup (may take time)
164
+ init_deepseek_model()
165
+
166
+ # ----------------------------
167
+ # Rule-based fallback responder
168
+ # ----------------------------
169
+ def rule_based_response(message: str) -> str:
170
+ msg = (message or "").strip().lower()
171
+ if msg.startswith("create record") or msg.startswith("create contact"):
172
+ return "To create a record, use: create_record MODULE_NAME {\"Field\":\"value\"}"
173
+ if msg.startswith("create_invoice"):
174
+ return "To create invoice: create_invoice {\"customer_id\":\"...\",\"line_items\":[...]} (JSON)"
175
+ if msg.startswith("help") or "what can you do" in msg:
176
+ return "I can run create_record/update_record/delete_record or process local files by pasting their /mnt/data/ path."
177
+ return "(fallback) No local LLM loaded. Use explicit commands like create_record or paste /mnt/data/ path."
178
 
179
+ # ----------------------------
180
+ # LLM wrapper that returns text + confidence (best-effort)
181
+ # ----------------------------
182
+ def deepseek_generate(prompt: str, max_tokens: int = 256) -> Dict[str, Any]:
183
+ """
184
+ Generate using the loaded pipeline. Returns {'text': str, 'confidence': Optional[float], 'raw': resp}
185
+ """
186
+ if LLM_PIPELINE is None:
187
+ return {"text": rule_based_response(prompt), "confidence": None, "raw": None}
188
 
 
 
 
 
189
  try:
190
+ out = LLM_PIPELINE(prompt, max_new_tokens=max_tokens)
191
+ text = ""
192
+ # pipeline returns list: [{'generated_text':...}] or [{'generated_text' or 'text'}]
193
+ if isinstance(out, list) and len(out) > 0:
194
+ first = out[0]
195
+ if isinstance(first, dict):
196
+ text = first.get("generated_text") or first.get("generated_text", "") or first.get("text") or str(first)
197
+ else:
198
+ text = str(first)
199
  else:
200
+ text = str(out)
201
+ _log_llm_call(None)
202
+ return {"text": text, "confidence": None, "raw": out}
203
+ except Exception as e:
204
+ logger.error("LLM generation error: %s", e)
205
+ traceback.print_exc()
206
+ return {"text": rule_based_response(prompt), "confidence": None, "raw": str(e)}
 
207
 
208
  # ----------------------------
209
+ # Zoho token refresh & MCP tools (unchanged)
210
  # ----------------------------
211
  def _get_valid_token_headers() -> dict:
212
  token_url = "https://accounts.zoho.in/oauth/v2/token"
213
+ params = {"refresh_token": REFRESH_TOKEN, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "grant_type": "refresh_token"}
 
 
 
 
 
214
  r = requests.post(token_url, params=params, timeout=20)
215
  if r.status_code == 200:
216
  t = r.json().get("access_token")
 
218
  else:
219
  raise RuntimeError(f"Failed to refresh Zoho token: {r.status_code} {r.text}")
220
 
 
 
 
221
  @mcp.tool()
222
  def authenticate_zoho() -> str:
223
  try:
 
308
  @mcp.tool()
309
  def process_document(file_path: str, target_module: Optional[str] = "Contacts") -> dict:
310
  """
311
+ Accepts local path (e.g. /mnt/data/script_zoho_mcp) or URL.
312
+ Per developer instruction we treat the path as the file URL (file://...).
313
+ Replace placeholder OCR logic with your pipeline.
314
  """
315
  try:
316
  if os.path.exists(file_path):
 
323
  "Confidence": 0.88
324
  }
325
  _log_tool_call("process_document", True)
326
+ return {"status": "success", "file": os.path.basename(file_path), "file_url": file_url, "target_module": target_module, "extracted_data": extracted}
 
 
 
 
 
 
327
  else:
328
  _log_tool_call("process_document", False)
329
  return {"status": "error", "error": "file not found", "file_path": file_path}
 
332
  return {"status": "error", "error": str(e)}
333
 
334
  # ----------------------------
335
+ # Simple command parser (explicit commands in chat)
336
  # ----------------------------
337
  def try_parse_and_invoke_command(text: str):
338
  text = text.strip()
 
339
  m = re.match(r"^create_record\s+(\w+)\s+(.+)$", text, re.I)
340
  if m:
341
+ module = m.group(1); body = m.group(2)
342
+ try: record_data = json.loads(body)
343
+ except Exception: return "Invalid JSON for record_data"
 
 
 
344
  return create_record(module, record_data)
 
 
345
  m = re.match(r"^create_invoice\s+(.+)$", text, re.I)
346
  if m:
347
  body = m.group(1)
348
+ try: invoice_data = json.loads(body)
349
+ except Exception: return "Invalid JSON for invoice_data"
 
 
350
  return create_invoice(invoice_data)
 
 
351
  m = re.match(r"^(\/mnt\/data\/\S+)$", text)
352
  if m:
353
+ path = m.group(1); return process_document(path)
 
 
354
  return None
355
 
356
  # ----------------------------
357
+ # Chat handler that uses DeepSeek generation (or fallback)
358
  # ----------------------------
359
+ def deepseek_response(message: str, history: Optional[List[Tuple[str,str]]] = None) -> str:
360
  history = history or []
361
+ system_prompt = "You are Zoho Assistant. Prefer concise answers. If you want to call a tool, return a JSON object: {\"tool\": \"create_record\", \"args\": {...}}"
362
+ # compact history into text for few-shot context (optional)
363
+ history_text = ""
364
+ for pair in history:
365
+ try:
366
+ u,a = pair[0], pair[1]
367
+ history_text += f"User: {u}\nAssistant: {a}\n"
368
+ except Exception:
369
+ continue
370
+ prompt = f"{system_prompt}\n{history_text}\nUser: {message}\nAssistant:"
371
+ gen = deepseek_generate(prompt, max_tokens=256)
372
+ text = gen.get("text", "")
373
+ # if text looks like JSON with a tool action, try to invoke
374
+ payload = text.strip()
375
+ if payload.startswith("{") or payload.startswith("["):
376
+ try:
377
+ parsed = json.loads(payload)
378
+ if isinstance(parsed, dict) and "tool" in parsed:
379
+ tool_name = parsed.get("tool"); args = parsed.get("args", {})
380
+ if tool_name in globals() and callable(globals()[tool_name]):
381
+ try:
382
+ out = globals()[tool_name](**args) if isinstance(args, dict) else globals()[tool_name](args)
383
+ return f"Invoked tool '{tool_name}'. Result:\n{out}"
384
+ except Exception as e:
385
+ return f"Tool invocation error: {e}"
386
+ else:
387
+ return f"Requested tool '{tool_name}' not found locally."
388
+ except Exception:
389
+ pass
390
+ return text
 
 
391
 
392
  # ----------------------------
393
  # Gradio chat handler
 
396
  history = history or []
397
  trimmed = (message or "").strip()
398
 
399
+ # explicit command parser
400
  cmd = try_parse_and_invoke_command(trimmed)
401
  if cmd is not None:
402
  return cmd
403
 
404
+ # developer dev path handling (send path unchanged)
405
  if trimmed.startswith("/mnt/data/"):
406
  try:
407
  doc = process_document(trimmed)
 
409
  except Exception as e:
410
  return f"Error processing document: {e}"
411
 
412
+ # otherwise, call deepseek_response (LLM or fallback)
413
+ try:
414
+ return deepseek_response(trimmed, history)
415
+ except Exception as e:
416
+ logger.error("deepseek_response error: %s", e)
417
+ traceback.print_exc()
418
+ return rule_based_response(trimmed)
419
 
420
  # ----------------------------
421
  # Gradio UI
422
  # ----------------------------
423
  def chat_interface():
424
+ return gr.ChatInterface(fn=chat_handler, textbox=gr.Textbox(placeholder="Ask me to create contacts, invoices, upload docs (or paste /mnt/data/... for dev)."))
 
 
 
425
 
426
  # ----------------------------
427
  # Entrypoint
428
  # ----------------------------
429
  if __name__ == "__main__":
430
+ logger.info("Starting MCP server (DeepSeek mode). Loaded model: %s", LOADED_MODEL_NAME)
431
  demo = chat_interface()
432
  demo.launch(server_name="0.0.0.0", server_port=7860)