| 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 |
|
|
| |
| from llm_client import LLMToolCaller |
| from init_ollama import init_ollama |
|
|
| |
| 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') |
| ] |
| ) |
|
|
| |
| logging.getLogger('httpx').setLevel(logging.WARNING) |
| logging.getLogger('uvicorn.access').setLevel(logging.WARNING) |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| 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" |
|
|
| |
| 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}") |
| |
| |
| try: |
| |
| 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 |
| |
| |
| 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") |
|
|
| |
| 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 |
| ) |
|
|
| |
| 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 |
|
|
| |
| async def get_mcp_client(): |
| """Get or create MCP client connection""" |
| global mcp_client |
| if mcp_client is not None: |
| return mcp_client |
| |
| try: |
| |
| 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: |
| |
| 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) |
| |
| |
| payload = { |
| "action": action, |
| "client_token": arguments.get("client_token", "test8f4e2b9c7a1d6e3f5a8b2c4d7e9f1a3b6c8e") |
| } |
| |
| |
| 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}") |
| |
| |
| 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() |
|
|
| |
| @app.get("/", response_class=HTMLResponse) |
| async def chat_interface(): |
| """Serve the chat interface""" |
| html_content = """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>PrestaShop Assistant</title> |
| <style> |
| body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } |
| .chat-container { border: 1px solid #ddd; height: 400px; overflow-y: auto; padding: 10px; margin-bottom: 10px; } |
| .message { margin-bottom: 10px; padding: 8px; border-radius: 4px; } |
| .user { background-color: #e3f2fd; margin-left: 20%; } |
| .assistant { background-color: #f3e5f5; margin-right: 20%; } |
| .input-container { display: flex; gap: 10px; } |
| input[type="text"] { flex: 1; padding: 8px; } |
| button { padding: 8px 16px; background-color: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; } |
| </style> |
| </head> |
| <body> |
| <h1>ποΈ PrestaShop Assistant</h1> |
| <p>Ask me about products! Try: "tell me about product 12" or "search for shoes"</p> |
| |
| <div id="chatContainer" class="chat-container"></div> |
| |
| <div class="input-container"> |
| <input type="text" id="messageInput" placeholder="Ask about products..." /> |
| <button onclick="sendMessage()">Send</button> |
| </div> |
| |
| <script> |
| let conversationHistory = []; |
| |
| async function sendMessage() { |
| const input = document.getElementById('messageInput'); |
| const message = input.value.trim(); |
| if (!message) return; |
| |
| // Add user message to chat |
| addMessageToChat('user', message); |
| input.value = ''; |
| |
| try { |
| const response = await fetch('/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| message: message, |
| conversation_history: conversationHistory |
| }) |
| }); |
| |
| const data = await response.json(); |
| addMessageToChat('assistant', data.response); |
| conversationHistory = data.conversation_history; |
| |
| } catch (error) { |
| addMessageToChat('assistant', 'Sorry, I encountered an error: ' + error.message); |
| } |
| } |
| |
| function addMessageToChat(role, content) { |
| const container = document.getElementById('chatContainer'); |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message ${role}`; |
| messageDiv.innerHTML = `<strong>${role === 'user' ? 'You' : 'Assistant'}:</strong> ${content}`; |
| container.appendChild(messageDiv); |
| container.scrollTop = container.scrollHeight; |
| } |
| |
| // Send message on Enter key |
| document.getElementById('messageInput').addEventListener('keypress', function(e) { |
| if (e.key === 'Enter') sendMessage(); |
| }); |
| </script> |
| </body> |
| </html> |
| """ |
| return HTMLResponse(content=html_content) |
|
|
| @app.post("/chat", response_model=ChatOutput) |
| async def chat_endpoint(chat_input: ChatInput) -> ChatOutput: |
| """ |
| Chat endpoint for the web interface - uses LLM for natural tool selection |
| """ |
| global mcp_client, llm_client |
| logger.info(f"π¬ Direct chat request: {chat_input.message[:100]}...") |
| |
| try: |
| |
| if llm_client: |
| tool_name, arguments = await llm_client.parse_message_for_tool_call(chat_input.message) |
| else: |
| |
| tool_name, arguments = None, {} |
| |
| if tool_name and mcp_client: |
| |
| arguments["client_token"] = chat_input.client_token or "test8f4e2b9c7a1d6e3f5a8b2c4d7e9f1a3b6c8e" |
| |
| |
| logger.info(f"π§ Calling MCP tool: {tool_name} with {arguments}") |
| result = await mcp_client.call_tool(tool_name, arguments) |
| |
| |
| if tool_name == "search_products": |
| response = f"I searched for '{arguments['query']}' and found: {result}" |
| elif tool_name == "get_product_details": |
| response = f"Here are the details for product {arguments['product_id']}: {result}" |
| elif tool_name == "get_product_features": |
| response = f"Here are the features for product {arguments['product_id']}: {result}" |
| elif tool_name == "get_product_images": |
| response = f"Here are the images for product {arguments['product_id']}: {result}" |
| else: |
| response = result |
| |
| tool_used = tool_name |
| else: |
| |
| response = f"I understand you said: '{chat_input.message}'. I'm designed to help with PrestaShop products. You can ask me to search for products, get details about specific products (like 'tell me about product 12'), or ask for features and images." |
| tool_used = None |
| |
| |
| updated_history = list(chat_input.conversation_history) |
| updated_history.append(ChatMessage(role="user", content=chat_input.message)) |
| updated_history.append(ChatMessage(role="assistant", content=response)) |
| |
| return ChatOutput( |
| response=response, |
| conversation_history=updated_history, |
| tool_used=tool_used |
| ) |
| |
| except Exception as e: |
| logger.error(f"β Chat endpoint error: {e}") |
| return ChatOutput( |
| response=f"Sorry, I encountered an error: {str(e)}", |
| conversation_history=list(chat_input.conversation_history) |
| ) |
|
|
| class EndpointHandler: |
| """Simple endpoint handler for MCP tool calls""" |
| |
| def __init__(self): |
| """Initialize the handler - lightweight setup only""" |
| pass |
| |
| async def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Main inference handler - calls MCP tools and returns results |
| """ |
| global mcp_client |
| |
| try: |
| |
| payload = InferenceInput(**data) |
| except Exception as e: |
| return { |
| "ok": False, |
| "tool": "unknown", |
| "result": f"I'm sorry, but I couldn't understand your request format. Please make sure you're providing a valid 'tool' name and 'arguments' object. The error was: {str(e)}", |
| "error": f"Invalid input: {e}" |
| } |
| |
| |
| if not mcp_client: |
| try: |
| mcp_client = await get_mcp_client() |
| except Exception as e: |
| return { |
| "ok": False, |
| "tool": payload.tool, |
| "result": f"I apologize, but I'm currently unable to access the PrestaShop MCP tools required for the '{payload.tool}' operation. The MCP server appears to be unavailable at the moment. Please try again later or contact support if this issue persists.", |
| "error": f"MCP server unavailable: {e}" |
| } |
| |
| try: |
| |
| result = await mcp_client.call_tool(payload.tool, payload.arguments) |
| |
| return { |
| "ok": True, |
| "tool": payload.tool, |
| "result": result, |
| "error": None |
| } |
| |
| except Exception as e: |
| error_details = f"Tool '{payload.tool}' failed: {str(e)}" |
| logger.error(f"β MCP tool call error: {error_details}") |
| |
| return { |
| "ok": False, |
| "tool": payload.tool, |
| "result": f"I encountered an error while trying to {payload.tool}: {str(e)}. This might be a temporary issue with the PrestaShop system or the specific operation you requested.", |
| "error": error_details |
| } |
|
|
| |
| handler = EndpointHandler() |
|
|
| |
| @app.post("/api/inference", response_model=InferenceOutput) |
| async def api_inference(data: Dict[str, Any]) -> InferenceOutput: |
| """ |
| API inference endpoint - handles both MCP tool calls and LLM prompt-based requests |
| """ |
| global mcp_client, llm_client |
| logger.info(f"π§ API inference request: {data}") |
| |
| try: |
| |
| if "prompt" in data and "tool" not in data: |
| logger.info("π Processing LLM prompt-based request") |
| |
| |
| if llm_client: |
| tool_name, arguments = await llm_client.parse_message_for_tool_call(data["prompt"]) |
| else: |
| |
| prompt = data["prompt"].lower() |
| if "product" in prompt and any(word in prompt for word in ["details", "about", "information"]): |
| |
| words = data["prompt"].split() |
| product_id = None |
| for word in words: |
| if word.isdigit(): |
| product_id = word |
| break |
| if product_id: |
| tool_name, arguments = "get_product_details", {"product_id": product_id} |
| else: |
| tool_name, arguments = "search_products", {"query": data["prompt"]} |
| else: |
| tool_name, arguments = "search_products", {"query": data["prompt"]} |
| |
| |
| if "client_token" in data: |
| arguments["client_token"] = data["client_token"] |
| else: |
| arguments["client_token"] = "test8f4e2b9c7a1d6e3f5a8b2c4d7e9f1a3b6c8e" |
| |
| |
| payload = { |
| "tool": tool_name, |
| "arguments": arguments |
| } |
| |
| logger.info(f"π Converted prompt to tool call: {payload}") |
| else: |
| |
| payload = data |
| |
| |
| result = await handler(payload) |
| return InferenceOutput(**result) |
| |
| except Exception as e: |
| logger.error(f"β API inference error: {e}") |
| return InferenceOutput( |
| ok=False, |
| tool="unknown", |
| result=f"Error processing request: {str(e)}", |
| error=str(e) |
| ) |
|
|
| |
| @app.get("/health") |
| async def health_check(): |
| """Health check endpoint""" |
| global mcp_client, llm_client |
| return { |
| "status": "healthy", |
| "timestamp": datetime.now().isoformat(), |
| "mcp_client": "connected" if mcp_client else "disconnected", |
| "llm_client": "available" if llm_client else "unavailable", |
| "version": "2.2.0" |
| } |
|
|
| |
| if __name__ == "__main__": |
| port = int(os.getenv("PORT", 7860)) |
| logger.info(f"π Starting server on port {port}") |
| uvicorn.run( |
| app, |
| host="0.0.0.0", |
| port=port, |
| log_level="info" |
| ) |