import gradio as gr import requests import time import json import re from urllib.parse import quote API_URL = "http://154.201.126.27:3000" client = requests.Session() session_ok = False tools_ok = False initialized = False pending_requests = [] pending_auth_token = "" DEMO_MODE = True DEMO_SYSTEM_PROMPT = """You are a helpful AI assistant for a banking and task management API. You can check balances, view transactions, transfer money, and manage tasks. Be friendly, concise, and always confirm before taking actions.""" DEMO_ENV_VARS = """{ "BASE_URL": "https://" }""" PROVIDERS = { "nebius-free": {"name": "๐Ÿ†“ Nebius Free (10 requests/12 hour)", "requires_key": False}, "openai": {"name": "๐Ÿ’ฐ OpenAI (Your API Key)", "requires_key": True}, "anthropic": {"name": "๐Ÿ’ฐ Anthropic (Your API Key)", "requires_key": True}, "gemini": {"name": "๐Ÿ’ฐ Google Gemini (Your API Key)", "requires_key": True} } MODEL_MAP = { "nebius-free": ["moonshotai/Kimi-K2-Instruct"], "openai": ["gpt-4.1-mini", "gpt-4.1", "o1"], "anthropic": ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"], "gemini": ["gemini-2.5-flash-lite", "gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0"], } # ============================================ # GLASSMORPHISM CSS # ============================================ CUSTOM_CSS = """ /* ========================================================= DARK GLASSMORPHISM THEME FOR FLUIDTOOLS ========================================================= */ :root { --glass-primary: rgba(102, 126, 234, 0.55); --glass-primary-dark: rgba(118, 75, 162, 0.65); --glass-accent: rgba(139, 92, 246, 0.65); --glass-bg: rgba(18, 18, 26, 0.45); --glass-hover: rgba(28, 28, 38, 0.57); --glass-border: rgba(195, 170, 255, 0.38); --text-light: rgba(235, 235, 255, 0.92); --text-dim: rgba(190, 190, 220, 0.65); --shadow-sm: 0 4px 6px rgba(0, 0, 0, 0.4); --shadow-md: 0 10px 18px rgba(0, 0, 0, 0.45); --shadow-lg: 0 18px 36px rgba(0, 0, 0, 0.55); } /* ========================================================= GLOBAL BACKGROUND (dark glass, animated) ========================================================= */ .gradio-container { background: linear-gradient(135deg, #131421 0%, #19172c 25%, #201a3a 50%, #161324 75%, #131421 100%) !important; background-size: 350% 350% !important; animation: gradientShift 18s ease infinite; min-height: 100vh; font-family: "Inter", "SF Pro Display", "Segoe UI", sans-serif !important; } @keyframes gradientShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } /* ========================================================= UNIVERSAL TEXT VISIBILITY FOR DARK MODE ========================================================= */ *, label, span, p, h1, h2, h3, div, .markdown, .prose { color: var(--text-light) !important; } /* ========================================================= GLASS CARDS ========================================================= */ .glass-card { background: var(--glass-bg) !important; border: 1px solid var(--glass-border) !important; border-radius: 20px !important; box-shadow: var(--shadow-md) !important; padding: 24px !important; backdrop-filter: blur(18px) !important; -webkit-backdrop-filter: blur(18px) !important; transition: 0.25s ease; } .glass-card:hover { background: var(--glass-hover) !important; box-shadow: var(--shadow-lg) !important; transform: translateY(-1px); } #status_column > * { max-width: 100%; } /* The real fix: REMOVE overflow clipping on the Gradio column */ #status_column.gr-column { overflow: visible !important; } /* Sticky fix โ€” transfer scroll to viewport and enable sticky panel */ .gradio-container, .gr-block, .gr-blocks, .gr-row, .gr-column { overflow: visible !important; } #status_column { position: sticky !important; top: 20px !important; z-index: 999 !important; height: max-content !important; } /* ========================================================= BUTTONS ========================================================= */ button { border-radius: 12px !important; padding: 12px 24px !important; font-weight: 600 !important; transition: 0.25s ease; } button.primary { background: linear-gradient(135deg, var(--glass-primary) 0%, var(--glass-primary-dark) 100%) !important; border: 1px solid var(--glass-border) !important; color: white !important; box-shadow: var(--shadow-sm) !important; } button.primary:hover { transform: translateY(-1px) scale(1.02); box-shadow: var(--shadow-md) !important; } button.secondary { background: var(--glass-bg) !important; border: 1px solid var(--glass-border) !important; color: var(--text-light) !important; } button.secondary:hover { background: var(--glass-hover) !important; } button.approve-btn { background: rgba(0, 180, 85, 0.68) !important; border: 1px solid rgba(0, 255, 140, 0.45) !important; color: white !important; } button.approve-btn:hover { background: rgba(0, 205, 95, 0.85) !important; } button.reject-btn { background: rgba(200, 38, 65, 0.7) !important; border: 1px solid rgba(255, 90, 110, 0.5) !important; color: white !important; } button.reject-btn:hover { background: rgba(225, 45, 75, 0.86) !important; } /* ========================================================= INPUTS โ€ข TEXTBOX โ€ข DROPDOWN ========================================================= */ input, textarea, select { background: rgba(25, 25, 36, 0.55) !important; border: 1px solid var(--glass-border) !important; border-radius: 12px !important; padding: 12px !important; color: var(--text-light) !important; backdrop-filter: blur(12px) !important; } /* FORCE TRUE DROPDOWN INSTEAD OF INPUT */ div[data-testid="dropdown"] input { display: none !important; } div[data-testid="dropdown"] select { appearance: auto !important; cursor: pointer !important; } /* ========================================================= FILE UPLOAD + LABEL BLACK BUG FIX ========================================================= */ [data-testid*="file-upload"] { background: rgba(25, 25, 36, 0.55) !important; border: 2px dashed rgba(145, 115, 255, 0.55) !important; border-radius: 18px !important; backdrop-filter: blur(14px) !important; } [data-testid*="file-upload"] .label { background: rgba(20, 20, 30, 0.75) !important; border-radius: 10px !important; padding: 6px 12px !important; font-weight: 600; } /* ========================================================= SEPARATE SYSTEM PROMPT & ENV JSON VISUALLY ========================================================= */ .gradio-container textarea:last-of-type { margin-top: 12px !important; } /* ========================================================= CHAT AREA VISIBILITY ========================================================= */ .gr-chatbot, .chatbot { background: rgba(25, 25, 36, 0.45) !important; border-radius: 20px !important; backdrop-filter: blur(16px) !important; } .gr-chatbot .message { color: var(--text-light) !important; } /* ========================================================= ACCORDION ========================================================= */ .accordion { background: var(--glass-bg) !important; border: 1px solid var(--glass-border) !important; border-radius: 18px !important; backdrop-filter: blur(14px) !important; } .accordion button span { color: var(--text-light) !important; font-weight: 600; } /* ========================================================= SCROLLBAR ========================================================= */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-thumb { background: rgba(140, 110, 255, 0.55); border-radius: 10px; } /* ========================================================= RESPONSIVE ========================================================= */ @media (max-width: 768px) { .glass-card { padding: 18px !important; } button { padding: 10px 20px !important; } } """ # JavaScript for localStorage API key management STORAGE_JS = """ """ # ============================================ # GLASSMORPHISM JAVASCRIPT ENHANCEMENTS # ============================================ GLASSMORPHISM_JS = """ """ # ----------------------------- STATUS UI ----------------------------- def format_status(message, status_type="info"): icons = {"success": "โœ…", "error": "โŒ", "warning": "โš ๏ธ", "info": "โ„น๏ธ"} return f"""
{icons.get(status_type,'โ„น๏ธ')} STATUS

{message}
""" # ----------------------------- BACKEND CALLS ----------------------------- def start_session(): global session_ok try: r = client.get(f"{API_URL}/session") if r.status_code == 200: session_ok = True return format_status("Session started successfully!", "success") return format_status(f"Failed to start session: {r.text}", "error") except Exception as e: return format_status(f"Error: {str(e)}", "error") def upload_tools(file): global tools_ok if not session_ok: return format_status("Start session first.", "warning") try: r = client.post(f"{API_URL}/tools", files={"api": open(file, "rb")}) if r.status_code == 200: tools_ok = True return format_status("Tools generated from Postman collection!", "success") return format_status(f"Upload failed: {r.text}", "error") except Exception as e: return format_status(f"Error: {str(e)}", "error") def initialize(system_prompt, env_vars, provider_display, api_key, model_name): global initialized provider = next((id for id, p in PROVIDERS.items() if p["name"] == provider_display), None) try: parsed = json.loads(env_vars) if env_vars.strip() else {} except Exception as e: return format_status(f"Invalid JSON: {str(e)}", "error") if PROVIDERS[provider]["requires_key"] and not api_key.strip(): return format_status("API key required for this provider.", "error") body = { "systemIntructions": system_prompt, "envVariables": parsed, "provider": provider, "model": model_name, "apiKey": api_key if api_key else None, } try: r = client.post(f"{API_URL}/initialize", json=body) if r.status_code == 200: initialized = True return format_status("Agent initialized! Start chatting below.", "success") return format_status(f"Initialization failed: {r.text}", "error") except Exception as e: return format_status(f"Error: {str(e)}", "error") # ----------------------------- CHAT LOGIC ----------------------------- def call_query(message, token): headers = {"Authorization": f"Bearer {token}"} if token else {} try: r = client.get(API_URL, params={"query": message}, headers=headers) body = r.json() return {"message": body.get("message", ""), "pending": body.get("data", [])} except Exception as e: return {"message": f"โŒ Error: {str(e)}", "pending": []} def chat_send(message, history, auth_token): global pending_requests, pending_auth_token if not initialized: history.append((None, "โš ๏ธ **Please initialize the agent first!**\n\nGo to **STEP 1** above and complete the setup.")) return history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) if not message.strip(): return history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) # FIXED: Add user message as tuple history.append((message, None)) # Show loading history[-1] = (message, "๐Ÿ”„ *Thinking and calling APIs...*") yield history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) result = call_query(message, auth_token) # remove loading message safely history[-1] = (message, None) # Check for rate limit error if "Rate limit exceeded" in result.get("message", ""): history[-1] = (message, f"โŒ {result['message']}\n\n๐Ÿ’ก **Tip:** Switch to a paid provider with your own API key for unlimited requests!") yield history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) return if result["pending"]: pending_requests = result["pending"] pending_auth_token = auth_token approvals_msg = "### โš ๏ธ APPROVAL REQUIRED\n\nThe following action(s) need your permission:\n\n" for i, p in enumerate(result["pending"], 1): approvals_msg += f"**{i}. Tool:** `{p['name']}`\n" if 'args' in p: approvals_msg += f"**Arguments:**\n```json\n{json.dumps(p['args'], indent=2)}\n```\n\n" approvals_msg += "**๐Ÿ‘‡ Use the buttons below to approve or reject:**" # FIXED: approval message history[-1] = (message, approvals_msg) yield history, "", gr.update(visible=True), gr.update(visible=True), gr.update(visible=True) else: history[-1] = (message, result["message"]) yield history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) def send_approval(approved, history): global pending_requests, pending_auth_token if not pending_requests: history.append((None, "No pending approvals")) return "Done", history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) data = [{"toolCallId": p["id"], "approved": approved} for p in pending_requests] r = client.post(f"{API_URL}/approval", json=data, headers={"Authorization": f"Bearer {pending_auth_token}"} if pending_auth_token else {}) pending_requests = [] history.append((None, r.json().get("message", "Done"))) return "Done", history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) def reset_chat(auth_token): global pending_requests, pending_auth_token # Attempt to reset session on backend try: headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {} client.delete(f"{API_URL}/session", headers=headers) except Exception as e: # Silently handle errors as reset should not fail the UI pass # Reset only pending requests and chat UI, keep initialization pending_requests = [] pending_auth_token = "" return [], gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) def toggle_provider_fields(provider_display): provider_id = next(k for k,v in PROVIDERS.items() if v["name"] == provider_display) return ( gr.update(visible=PROVIDERS[provider_id]["requires_key"]), gr.update(visible=True, choices=MODEL_MAP[provider_id], value=MODEL_MAP[provider_id][0]) ) # ----------------------------- UI ----------------------------- with gr.Blocks(title="FluidTools", css=CUSTOM_CSS, head=STORAGE_JS + GLASSMORPHISM_JS) as demo: # Glassmorphic Hero Header gr.HTML("""
๐Ÿค–

FluidTools

Turn any Postman API collection into an AI-powered chatbot

โœจ Multi-Provider | ๐Ÿ›ก๏ธ Human-in-the-Loop | ๐Ÿš€ Zero Code
""") gr.Markdown(""" ### ๐ŸŽฏ What This Demo Does: 1. **Upload** your Postman collection (JSON export from Postman) 2. **Initialize** the AI agent with your API tools 3. **Chat** with your APIs using natural language - no coding required! """, elem_classes=["glass-card"]) # --- SECTION LAYOUT --- with gr.Row(): # ---- LEFT: STEPS AREA (70%) ---- with gr.Column(scale=7): with gr.Accordion("๐Ÿš€ STEP 1 โ€” Initialize System", open=True): start_btn = gr.Button("๐Ÿงช Start Session") file_in = gr.File(file_types=[".json"]) provider_selector = gr.Dropdown( choices=[p["name"] for p in PROVIDERS.values()], value="๐Ÿ†“ Nebius Free (10 requests/day)", label="Provider", ) model_dropdown = gr.Dropdown( label="Model", choices=[], visible=False ) api_key_box = gr.Textbox(label="API Key (required for paid providers)", type="password", visible=False) sys_prompt = gr.Textbox(label="System Prompt", value=DEMO_SYSTEM_PROMPT if DEMO_MODE else "") env_vars = gr.Textbox(label="Environment Variables JSON", value=DEMO_ENV_VARS if DEMO_MODE else "") init_btn = gr.Button("๐Ÿš€ Initialize Agent", interactive=False) with gr.Accordion("๐Ÿ’ฌ STEP 2 โ€” Chat with API", open=False): auth_box = gr.Textbox(label="Auth Token", type="password") chat = gr.Chatbot(height=450) msg = gr.Textbox() send = gr.Button("๐Ÿ“ค Send") reset = gr.Button("๐Ÿ”„ Reset Conversation") approval_md = gr.Markdown(visible=False) approve_btn = gr.Button("Approve All", visible=False, elem_classes=["approve-btn"]) reject_btn = gr.Button("Reject All", visible=False, elem_classes=["reject-btn"]) approval_result = gr.Textbox(visible=False) # ---- RIGHT: STATUS AREA (30%) ---- with gr.Column(scale=3, elem_id="status_column"): status_box = gr.HTML(format_status("Awaiting Session Start")) gr.HTML("""

๐Ÿ“š Learn More

๐ŸŒŸ Sponsors

FluidTools ecosystem โ€ข Multi-provider LLM support (OpenAI, Anthropic, Gemini, Nebius) โ€ข Human-in-the-loop safety โ€ข Semantic tool selection
""") # INTERACTIONS def toggle_api(provider_display): provider_id = next(k for k,v in PROVIDERS.items() if v["name"] == provider_display) return gr.update(visible=PROVIDERS[provider_id]["requires_key"]) provider_selector.change(toggle_api, provider_selector, api_key_box) provider_selector.change( toggle_provider_fields, provider_selector, [api_key_box, model_dropdown] ) def refresh_buttons(): return gr.update(interactive=session_ok), gr.update(interactive=(session_ok and tools_ok)) start_btn.click(start_session, None, status_box).then(refresh_buttons, None, [file_in, init_btn]) file_in.upload(upload_tools, file_in, status_box).then(refresh_buttons, None, [file_in, init_btn]) init_btn.click(initialize, [sys_prompt, env_vars, provider_selector, api_key_box, model_dropdown], status_box) send.click(chat_send, [msg, chat, auth_box], [chat, msg, approval_md, approve_btn, reject_btn]) msg.submit(chat_send, [msg, chat, auth_box], [chat, msg, approval_md, approve_btn, reject_btn]) approve_btn.click(send_approval, [gr.State(True), chat], [approval_result, chat, approval_md, approve_btn, reject_btn]) reject_btn.click(send_approval, [gr.State(False), chat], [approval_result, chat, approval_md, approve_btn, reject_btn]) reset.click(reset_chat, auth_box, [chat, approval_md, approve_btn, reject_btn]) if __name__ == "__main__": demo.launch(server_port=7860, share=False)