vachaspathi commited on
Commit
d1fb6bf
·
verified ·
1 Parent(s): e494622

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -129
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py — MCP POC using local Hugging Face model (flan-t5 or other) or rule-based fallback.
2
  # Place this file next to config.py. Do NOT store secrets here.
3
 
4
  from mcp.server.fastmcp import FastMCP
@@ -12,14 +12,6 @@ import traceback
12
  import inspect
13
  import re
14
 
15
- # Optional transformers imports — load only if available
16
- TRANSFORMERS_AVAILABLE = False
17
- try:
18
- from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM, AutoModelForCausalLM
19
- TRANSFORMERS_AVAILABLE = True
20
- except Exception:
21
- TRANSFORMERS_AVAILABLE = False
22
-
23
  # ----------------------------
24
  # Load config
25
  # ----------------------------
@@ -29,13 +21,18 @@ try:
29
  CLIENT_SECRET,
30
  REFRESH_TOKEN,
31
  API_BASE,
32
- LOCAL_MODEL, # e.g. "google/flan-t5-base" or None
 
33
  )
34
  except Exception:
35
  raise SystemExit(
36
- "Make sure config.py exists with CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, API_BASE, LOCAL_MODEL (or leave LOCAL_MODEL=None)."
 
37
  )
38
 
 
 
 
39
  # ----------------------------
40
  # Initialize FastMCP
41
  # ----------------------------
@@ -87,71 +84,79 @@ def _log_llm_call(confidence: Optional[float] = None):
87
  _init_analytics()
88
 
89
  # ----------------------------
90
- # Local LLM pipeline initialization
91
  # ----------------------------
92
- LLM_PIPELINE = None
93
- TOKENIZER = None
94
 
95
- def init_local_model():
96
  """
97
- Initialize local HF model pipeline depending on LOCAL_MODEL.
98
- Supports seq2seq (flan/t5) and causal models.
99
- If transformers is unavailable or LOCAL_MODEL is None, leaves LLM_PIPELINE as None.
100
  """
101
- global LLM_PIPELINE, TOKENIZER
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- if not LOCAL_MODEL:
104
- print("LOCAL_MODEL is None — using rule-based fallback.")
105
- LLM_PIPELINE = None
106
- return
 
107
 
108
- if not TRANSFORMERS_AVAILABLE:
109
- print("transformers not installed using rule-based fallback.")
110
- LLM_PIPELINE = None
111
- return
112
 
 
 
 
 
113
  try:
114
- tokenizer_name = LOCAL_TOKENIZER or LOCAL_MODEL
115
-
116
- # Detect seq2seq family (T5/Flan)
117
- if any(x in LOCAL_MODEL.lower() for x in ["flan", "t5", "seq2seq"]):
118
- TOKENIZER = AutoTokenizer.from_pretrained(tokenizer_name, use_fast=True)
119
- model = AutoModelForSeq2SeqLM.from_pretrained(LOCAL_MODEL)
120
- LLM_PIPELINE = pipeline("text2text-generation", model=model, tokenizer=TOKENIZER)
121
- print(f"Loaded seq2seq model pipeline for {LOCAL_MODEL}")
 
122
  else:
123
- # causal model path
124
- TOKENIZER = AutoTokenizer.from_pretrained(tokenizer_name, use_fast=True)
125
- model = AutoModelForCausalLM.from_pretrained(LOCAL_MODEL)
126
- LLM_PIPELINE = pipeline("text-generation", model=model, tokenizer=TOKENIZER)
127
- print(f"Loaded causal model pipeline for {LOCAL_MODEL}")
128
-
129
- except Exception as e:
130
- print("Failed to load local model:", e)
131
- traceback.print_exc()
132
- LLM_PIPELINE = None
133
 
134
- # Try to init model at startup (may be slow)
135
- init_local_model()
136
 
137
  # ----------------------------
138
- # Rule-based fallback responder
139
- # ----------------------------
140
- def rule_based_response(message: str) -> str:
141
- msg = (message or "").strip().lower()
142
- if msg.startswith("create record") or msg.startswith("create contact"):
143
- return "To create a record, use the command: create_record MODULE_NAME {\"Field\": \"value\"}"
144
- if msg.startswith("create_invoice"):
145
- return "To create invoice: create_invoice {\"customer_id\": \"...\", \"line_items\": [...]} (JSON)"
146
- if msg.startswith("help") or msg.startswith("what can you do"):
147
- return "I can create/update/delete records in Zoho (create_record/update_record/delete_record) or process local files by pasting their path (/mnt/data/...)."
148
- return "(fallback) No local LLM loaded. Use explicit commands like `create_record` or paste a /mnt/data/ path."
149
-
150
- # ----------------------------
151
- # Zoho token refresh & headers helper
152
  # ----------------------------
153
  def _get_valid_token_headers() -> dict:
154
- # Note: region-specific account host may need .com or .eu — ensure API_BASE matches services used.
155
  token_url = "https://accounts.zoho.in/oauth/v2/token"
156
  params = {
157
  "refresh_token": REFRESH_TOKEN,
@@ -167,7 +172,7 @@ def _get_valid_token_headers() -> dict:
167
  raise RuntimeError(f"Failed to refresh Zoho token: {r.status_code} {r.text}")
168
 
169
  # ----------------------------
170
- # MCP tools: Zoho CRM & Books (CRUD + document processing)
171
  # ----------------------------
172
  @mcp.tool()
173
  def authenticate_zoho() -> str:
@@ -243,10 +248,6 @@ def delete_record(module_name: str, record_id: str) -> str:
243
 
244
  @mcp.tool()
245
  def create_invoice(data: dict) -> str:
246
- """
247
- Creates an invoice in Zoho Books.
248
- NOTE: Ensure API_BASE points to the Books base (e.g. https://books.zoho.in/api/v3) when calling invoices.
249
- """
250
  try:
251
  headers = _get_valid_token_headers()
252
  url = f"{API_BASE}/invoices"
@@ -270,7 +271,6 @@ def process_document(file_path: str, target_module: Optional[str] = "Contacts")
270
  try:
271
  if os.path.exists(file_path):
272
  file_url = f"file://{file_path}"
273
- # Placeholder extraction — replace with OCR + parsing logic
274
  extracted = {
275
  "Name": "ACME Corp (simulated)",
276
  "Email": "[email protected]",
@@ -294,15 +294,9 @@ 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 to call tools explicitly from chat (POC)
298
  # ----------------------------
299
  def try_parse_and_invoke_command(text: str):
300
- """
301
- Very small parser for explicit chat commands:
302
- create_record MODULE {json}
303
- create_invoice {json}
304
- process_document /mnt/data/...
305
- """
306
  text = text.strip()
307
  # create_record
308
  m = re.match(r"^create_record\s+(\w+)\s+(.+)$", text, re.I)
@@ -334,45 +328,56 @@ def try_parse_and_invoke_command(text: str):
334
  return None
335
 
336
  # ----------------------------
337
- # Local LLM / fallback generator
338
  # ----------------------------
339
- def local_llm_generate(prompt: str) -> str:
340
- if LLM_PIPELINE is not None:
341
- try:
342
- # For seq2seq (text2text) the pipeline returns 'generated_text'
343
- out = LLM_PIPELINE(prompt, max_new_tokens=256)
344
- if isinstance(out, list) and len(out) > 0:
345
- # text2text pipelines often provide 'generated_text'
346
- if isinstance(out[0], dict):
347
- return out[0].get("generated_text") or out[0].get("text") or str(out[0])
348
- return str(out[0])
349
- return str(out)
350
- except Exception as e:
351
- print("LLM pipeline error:", e)
352
- traceback.print_exc()
353
- return rule_based_response(prompt)
354
- else:
355
- return rule_based_response(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  # ----------------------------
358
- # Gradio chat handler (accepts message, history)
359
  # ----------------------------
360
  def chat_handler(message, history):
361
- """
362
- Gradio ChatInterface calls this with (message, history).
363
- If the message is a local file path (starting with /mnt/data/), we pass it unchanged
364
- to process_document() — this satisfies the developer instruction to send the path as the URL.
365
- Otherwise, try explicit commands, then local LLM fallback.
366
- """
367
  history = history or []
368
  trimmed = (message or "").strip()
369
 
370
- # 1) explicit commands
371
  cmd = try_parse_and_invoke_command(trimmed)
372
  if cmd is not None:
373
  return cmd
374
 
375
- # 2) developer convenience: local path handling
376
  if trimmed.startswith("/mnt/data/"):
377
  try:
378
  doc = process_document(trimmed)
@@ -380,31 +385,8 @@ def chat_handler(message, history):
380
  except Exception as e:
381
  return f"Error processing document: {e}"
382
 
383
- # 3) otherwise call local LLM (if available) or fallback
384
- # build a compact prompt including a short system instruction and history
385
- history_text = ""
386
- for pair in history:
387
- try:
388
- user_turn, assistant_turn = pair[0], pair[1]
389
- except Exception:
390
- if isinstance(pair, dict):
391
- user_turn = pair.get("user", "")
392
- assistant_turn = pair.get("assistant", "")
393
- else:
394
- user_turn, assistant_turn = "", ""
395
- if user_turn:
396
- history_text += f"User: {user_turn}\n"
397
- if assistant_turn:
398
- history_text += f"Assistant: {assistant_turn}\n"
399
-
400
- system = "You are a Zoho assistant that can call local MCP tools when asked. Keep replies short and actionable."
401
- prompt = f"{system}\n{history_text}\nUser: {trimmed}\nAssistant:"
402
- try:
403
- resp = local_llm_generate(prompt)
404
- _log_llm_call(None)
405
- return resp
406
- except Exception as e:
407
- return f"LLM error: {e}"
408
 
409
  # ----------------------------
410
  # Gradio UI
@@ -412,13 +394,13 @@ def chat_handler(message, history):
412
  def chat_interface():
413
  return gr.ChatInterface(
414
  fn=chat_handler,
415
- textbox=gr.Textbox(placeholder="Ask me to create contacts, invoices, or paste /mnt/data/... for dev.")
416
  )
417
 
418
  # ----------------------------
419
  # Entrypoint
420
  # ----------------------------
421
  if __name__ == "__main__":
422
- print("[startup] Launching Gradio UI + FastMCP server (local LLM mode).")
423
  demo = chat_interface()
424
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
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
 
12
  import inspect
13
  import re
14
 
 
 
 
 
 
 
 
 
15
  # ----------------------------
16
  # Load config
17
  # ----------------------------
 
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
  # ----------------------------
 
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,
 
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:
 
248
 
249
  @mcp.tool()
250
  def create_invoice(data: dict) -> str:
 
 
 
 
251
  try:
252
  headers = _get_valid_token_headers()
253
  url = f"{API_BASE}/invoices"
 
271
  try:
272
  if os.path.exists(file_path):
273
  file_url = f"file://{file_path}"
 
274
  extracted = {
275
  "Name": "ACME Corp (simulated)",
276
  "Email": "[email protected]",
 
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)
 
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
370
  # ----------------------------
371
  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
  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
 
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)