import os import asyncio import logging import sys import time import json import httpx from datetime import datetime from typing import Any, Dict, List, Optional, Union from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field import uvicorn from contextlib import asynccontextmanager # Import our LLM client from llm_client import LLMToolCaller from init_ollama import init_ollama # Configure comprehensive logging logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler('/app/inference_app.log') ] ) # Set external library log levels logging.getLogger('httpx').setLevel(logging.WARNING) logging.getLogger('uvicorn.access').setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Configuration DEFAULT_PRESTASHOP_URL = "https://hub.mcp.integration.ambris.com" HUB_SERVER_URL = "https://hub.mcp.integration.ambris.com/index.php?fc=module&module=ambmcpclient&controller=api" # Global MCP client and LLM client instances mcp_client = None llm_client = None @asynccontextmanager async def lifespan(app: FastAPI): """Manage application lifespan - better for HF Spaces""" global mcp_client, llm_client logger.info("🚀 Starting HF Inference Point application") logger.info(f"📦 Environment: {os.getenv('ENVIRONMENT', 'unknown')}") logger.info(f"🔗 Default PrestaShop URL: {DEFAULT_PRESTASHOP_URL}") # Startup try: # Initialize Ollama first logger.info("🤖 Initializing Ollama LLM...") if init_ollama(): llm_client = LLMToolCaller() logger.info("✅ LLM client initialized successfully") else: logger.warning("⚠️ LLM initialization failed, using fallback") llm_client = None logger.info("🔧 Attempting to initialize MCP client...") start_time = time.time() mcp_client = await get_mcp_client() init_time = time.time() - start_time logger.info(f"✅ MCP client initialized successfully in {init_time:.2f}s") logger.info(f"🛠️ MCP client type: {type(mcp_client)}") except Exception as e: logger.error(f"❌ Failed to initialize MCP client: {e}") logger.error(f"📋 Exception details: {type(e).__name__}: {str(e)}") logger.error(f"🔍 MCP client will be None, falling back to graceful degradation") mcp_client = None logger.info("🎯 Application startup complete, ready to serve requests") yield # App runs here # Shutdown logger.info("🛑 Application shutdown initiated") try: await close_mcp_client() logger.info("✅ MCP client closed successfully") except Exception as e: logger.error(f"⚠️ Error closing MCP client: {e}") logger.info("👋 Application shutdown complete") # Initialize FastAPI with lifespan app = FastAPI( title="PrestaShop MCP Inference Point", description="HuggingFace-compatible inference endpoint for PrestaShop MCP tools with LLM-driven tool selection", version="2.2.0", lifespan=lifespan ) # Data Models class ChatMessage(BaseModel): """Individual chat message""" role: str = Field(..., description="Role: user or assistant") content: str = Field(..., description="Message content") class ChatInput(BaseModel): """Input for chat interface""" message: str = Field(..., description="User message") conversation_history: List[ChatMessage] = Field(default=[], description="Previous conversation") client_token: Optional[str] = Field(default=None, description="Optional client authentication token") class ChatOutput(BaseModel): """Output from chat interface""" response: str = Field(..., description="Assistant response") tool_used: Optional[str] = None conversation_history: List[ChatMessage] = Field(..., description="Updated conversation history") class InferenceInput(BaseModel): """Input model for direct MCP tool calls""" tool: str = Field(..., description="Name of the MCP tool to call") arguments: Dict[str, Any] = Field(..., description="Arguments for the tool") class InferenceOutput(BaseModel): """Output model for direct MCP tool calls""" ok: bool tool: str result: str error: Optional[str] = None # MCP Client Management async def get_mcp_client(): """Get or create MCP client connection""" global mcp_client if mcp_client is not None: return mcp_client try: # Use httpx client for HTTP-based MCP communication mcp_client = HTTPMCPClient(base_url=HUB_SERVER_URL) logger.info("✅ HTTP MCP client created successfully") return mcp_client except Exception as e: logger.error(f"❌ Failed to create MCP client: {e}") raise async def close_mcp_client(): """Close MCP client connection""" global mcp_client if mcp_client is not None: try: if hasattr(mcp_client, 'close'): await mcp_client.close() mcp_client = None logger.info("✅ MCP client closed") except Exception as e: logger.error(f"⚠️ Error closing MCP client: {e}") class HTTPMCPClient: """Simple HTTP client for MCP communication via hub""" def __init__(self, base_url: str): self.base_url = base_url self.client = httpx.AsyncClient(timeout=30.0) async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> str: """Call an MCP tool via HTTP""" try: # Map tool name to action action_map = { "search_products": "search_products", "get_product_details": "get_product", "get_product_features": "get_product_features", "get_product_images": "get_product_images" } action = action_map.get(tool_name, tool_name) # Prepare payload for hub payload = { "action": action, "client_token": arguments.get("client_token", "test8f4e2b9c7a1d6e3f5a8b2c4d7e9f1a3b6c8e") } # Add specific parameters based on tool if tool_name == "search_products": payload["query"] = arguments.get("query", "") elif tool_name in ["get_product_details", "get_product_features", "get_product_images"]: payload["product_id"] = arguments.get("product_id", 1) logger.info(f"🌐 Calling hub with: {payload}") # Make HTTP request to hub response = await self.client.post( self.base_url, json=payload, headers={"Content-Type": "application/json"} ) if response.status_code == 200: try: result = response.json() logger.info(f"✅ Hub response: {result}") return json.dumps(result, indent=2) except: return response.text else: logger.error(f"❌ Hub error {response.status_code}: {response.text}") return f"Error: {response.status_code} - {response.text}" except Exception as e: logger.error(f"❌ HTTP MCP call failed: {e}") return f"Connection error: {str(e)}" async def close(self): """Close the HTTP client""" await self.client.aclose() # Web Interface @app.get("/", response_class=HTMLResponse) async def chat_interface(): """Serve the chat interface""" html_content = """
Ask me about products! Try: "tell me about product 12" or "search for shoes"