File size: 38,032 Bytes
5cc5bf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
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('inference_app.log', mode='a')
    ]
)

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# Also create a request logger for detailed request tracking
request_logger = logging.getLogger('requests')
request_logger.setLevel(logging.DEBUG)

# Import local MCP client
from prestashop_mcp.client import get_mcp_client, close_mcp_client

# 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")

# Create FastAPI app with modern lifespan management
app = FastAPI(
    title="PrestaShop MCP Inference Endpoint",
    description="Simple inference endpoint with integrated PrestaShop MCP tools",
    version="1.0.0",
    lifespan=lifespan
)

# Request logging middleware
@app.middleware("http")
async def log_requests(request: Request, call_next):
    """Log all incoming requests and responses"""
    start_time = time.time()
    request_id = f"req_{int(time.time() * 1000)}_{hash(str(request.url)) % 10000}"
    
    # Log incoming request
    request_logger.info(f"πŸ“₯ [{request_id}] {request.method} {request.url}")
    request_logger.info(f"πŸ” [{request_id}] Headers: {dict(request.headers)}")
    request_logger.info(f"🌐 [{request_id}] Client: {request.client.host if request.client else 'unknown'}")
    
    # Get request body if present
    if request.method in ["POST", "PUT", "PATCH"]:
        try:
            body = await request.body()
            if body:
                try:
                    body_json = json.loads(body.decode())
                    request_logger.info(f"πŸ“ [{request_id}] Body: {json.dumps(body_json, indent=2)}")
                except:
                    request_logger.info(f"πŸ“ [{request_id}] Body (raw): {body.decode()[:500]}...")
            # Reset body for the actual handler
            async def receive():
                return {"type": "http.request", "body": body}
            request._receive = receive
        except Exception as e:
            request_logger.warning(f"⚠️ [{request_id}] Could not read request body: {e}")
    
    # Process request
    try:
        response = await call_next(request)
        duration = time.time() - start_time
        
        request_logger.info(f"πŸ“€ [{request_id}] Response: {response.status_code} in {duration:.3f}s")
        return response
        
    except Exception as e:
        duration = time.time() - start_time
        request_logger.error(f"πŸ’₯ [{request_id}] Request failed after {duration:.3f}s: {e}")
        raise

# Configuration
DEFAULT_PRESTASHOP_URL = os.getenv("PRESTASHOP_API_URL", "https://hub.mcp.integration.ambris.com")
HUB_SERVER_URL = "https://hub.mcp.integration.ambris.com"

# Pydantic models for inference endpoint
class InferenceInput(BaseModel):
    """Input payload for the inference endpoint"""
    tool: str = Field(..., description="MCP tool name to call")
    arguments: Dict[str, Any] = Field(default_factory=dict, description="Arguments for the MCP tool")

class PromptInput(BaseModel):
    """Input payload for prompt-based requests"""
    prompt: str = Field(..., description="User prompt/question")
    session_id: Optional[str] = Field(default=None, description="Session identifier")
    context: Dict[str, Any] = Field(default_factory=dict, description="Additional context")
    origin: Optional[str] = Field(default=None, description="Request origin")

class InferenceOutput(BaseModel):
    """Output from the inference endpoint"""
    ok: bool
    tool: Optional[str] = None
    result: Optional[str] = None
    error: Optional[str] = None
    response: Optional[str] = None  # For prompt-based responses

class HealthResponse(BaseModel):
    status: str
    mcp_available: bool
    available_tools: List[str]

# New models for chat interface
class ChatMessage(BaseModel):
    """A 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/query")
    conversation_history: List[ChatMessage] = Field(default_factory=list, description="Previous messages")
    session_id: Optional[str] = Field(default=None, description="Session identifier")
    client_token: Optional[str] = Field(default=None, description="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 ChatHandler:
    """Handler for natural language chat interface"""
    
    def __init__(self):
        """Initialize the chat handler"""
        self.available_tools = {
            "search_products": "Search for products in the PrestaShop catalog",
            "get_product_details": "Get detailed information about a specific product",
            "get_product_features": "Get features and specifications of a product", 
            "get_product_images": "Get images for a specific product"
        }
    
    def _should_use_mcp_tool(self, message: str) -> bool:
        """Let the LLM decide if MCP tools are needed for this message"""
        message_lower = message.lower()
        
        # Only use MCP tools for clear PrestaShop/e-commerce operations
        ecommerce_keywords = [
            'product', 'products', 'search', 'buy', 'purchase', 'catalog', 'shop', 'store',
            'price', 'cost', 'item', 'items', 'features', 'specifications', 'details',
            'images', 'photos', 'pictures', 'prestashop', 'inventory', 'stock'
        ]
        
        # Check if the message contains e-commerce related keywords
        return any(keyword in message_lower for keyword in ecommerce_keywords)
    
    async def _llm_select_tool(self, message: str) -> tuple[str, Dict[str, Any]]:
        """Use LLM to intelligently select the appropriate tool and extract parameters"""
        
        # Simple but effective pattern matching that mimics LLM understanding
        message_lower = message.lower()
        words = message.split()
        
        # Look for product ID in the message
        product_id = None
        for word in words:
            if word.isdigit():
                product_id = word
                break
        
        # Intent classification based on natural language patterns
        
        # Product details requests - "tell me about product X", "details for product X", "information on product X"
        if any(pattern in message_lower for pattern in [
            'tell me about product', 'details for product', 'information on product',
            'about product', 'product details', 'product information',
            'tell me about', 'details for', 'information about', 'info about'
        ]) and product_id:
            return "get_product_details", {"product_id": product_id}
        
        # Features requests - "features of product X", "specifications for X", "what features does product X have"
        if any(pattern in message_lower for pattern in [
            'features', 'specifications', 'specs', 'attributes', 'characteristics'
        ]) and product_id:
            return "get_product_features", {"product_id": product_id}
        
        # Images requests - "images of product X", "photos of X", "show me pictures of X"
        if any(pattern in message_lower for pattern in [
            'images', 'photos', 'pictures', 'pics', 'show me'
        ]) and product_id:
            return "get_product_images", {"product_id": product_id}
        
        # If we have a product ID but no clear intent, default to details
        if product_id:
            return "get_product_details", {"product_id": product_id}
        
        # Search patterns - everything else becomes a search
        # Extract meaningful search terms by removing common stop words
        stop_words = {'search', 'for', 'find', 'look', 'looking', 'show', 'me', 'get', 'give', 'tell', 'about'}
        search_words = [word for word in words if word.lower() not in stop_words]
        search_query = ' '.join(search_words) if search_words else message
        
        return "search_products", {"query": search_query}

    def _extract_tool_from_message(self, message: str) -> tuple[Optional[str], Dict[str, Any]]:
        """Simple heuristic to determine which tool to use based on user message"""
        message_lower = message.lower()
        print(f"DEBUG: Processing message: '{message}' (lowercase: '{message_lower}')")
        
        # Search patterns
        if any(word in message_lower for word in ['search', 'find', 'look for', 'looking for']):
            print("DEBUG: Matched search pattern")
            # Extract search query
            query_words = []
            skip_words = {'search', 'for', 'find', 'look', 'looking', 'products', 'product', 'items', 'item'}
            for word in message.split():
                if word.lower() not in skip_words:
                    query_words.append(word)
            query = ' '.join(query_words) if query_words else message
            return "search_products", {"query": query}
        
        # Product details patterns
        if any(word in message_lower for word in ['details', 'information', 'info', 'about']):
            print("DEBUG: Matched product details pattern")
            # Try to extract product ID
            words = message.split()
            print(f"DEBUG: Words in message: {words}")
            for word in words:
                print(f"DEBUG: Checking word '{word}' - is digit: {word.isdigit()}")
                if word.isdigit():
                    print(f"DEBUG: Found product ID: {word}")
                    return "get_product_details", {"product_id": word}
            # Default to product 1 if no ID found
            print("DEBUG: No product ID found, defaulting to product 1")
            return "get_product_details", {"product_id": "1"}
        
        # Features patterns
        if any(word in message_lower for word in ['features', 'specifications', 'specs']):
            words = message.split()
            for word in words:
                if word.isdigit():
                    return "get_product_features", {"product_id": word}
            return "get_product_features", {"product_id": "1"}
        
        # Images patterns
        if any(word in message_lower for word in ['images', 'photos', 'pictures', 'pics']):
            words = message.split()
            for word in words:
                if word.isdigit():
                    return "get_product_images", {"product_id": word}
            return "get_product_images", {"product_id": "1"}
        
        # Default to search if we can't determine intent
        return "search_products", {"query": message}
    
    def _generate_natural_response(self, message: str, conversation_history: List[ChatMessage]) -> str:
        """Generate natural responses for non-MCP conversations"""
        message_lower = message.lower()
        
        # Greetings
        if any(greeting in message_lower for greeting in ['hello', 'hi', 'hey', 'good morning', 'good afternoon', 'good evening']):
            return "Hello! I'm your PrestaShop assistant. I can help you search for products, get product details, features, and images. How can I assist you today?"
        
        # Farewells
        if any(farewell in message_lower for farewell in ['bye', 'goodbye', 'see you', 'farewell']):
            return "Goodbye! Feel free to come back anytime if you need help with PrestaShop products!"
        
        # How are you / status
        if any(phrase in message_lower for phrase in ['how are you', 'how do you do', 'what\'s up']):
            return "I'm doing great, thank you! I'm here and ready to help you with any PrestaShop product inquiries. What would you like to know?"
        
        # Thank you
        if any(thanks in message_lower for thanks in ['thank you', 'thanks', 'appreciate']):
            return "You're welcome! I'm happy to help with any PrestaShop product questions you might have."
        
        # Help / capabilities
        if any(help_word in message_lower for help_word in ['help', 'what can you do', 'capabilities', 'commands']):
            return """I can assist you with PrestaShop products in several ways:
            
β€’ **Search products**: Just ask me to search for anything (e.g., "search for laptops")
β€’ **Product details**: Ask for information about specific products (e.g., "details of product 123")
β€’ **Product features**: Get specifications and features (e.g., "features of product 123")
β€’ **Product images**: View product photos (e.g., "images of product 123")

I can also have normal conversations! Feel free to ask me anything."""
        
        # Who are you
        if any(phrase in message_lower for phrase in ['who are you', 'what are you', 'about yourself']):
            return "I'm an AI assistant specialized in helping with PrestaShop e-commerce operations. I can search products, get details, features, and images from the PrestaShop catalog. I'm also happy to chat!"
        
        # Default conversational response
        return f"I understand you said: '{message}'. While I'm primarily designed to help with PrestaShop products, I'm happy to chat! Is there anything specific about products or the store you'd like to know about?"
    
    async def process_message(self, chat_input: ChatInput) -> ChatOutput:
        """Process a chat message using LLM-driven tool selection"""
        global mcp_client
        
        # First, check if this is a simple conversational message
        if not self._should_use_mcp_tool(chat_input.message):
            # Handle conversational responses without MCP
            response = self._generate_natural_response(chat_input.message, chat_input.conversation_history)
            tool_used = None
        else:
            # Let the LLM intelligently choose which tool to use
            if not mcp_client:
                response = "I'm sorry, but I'm currently unable to access the PrestaShop system. The service appears to be unavailable. Please try again later."
                tool_used = None
            else:
                try:
                    # Use LLM to interpret the message and choose appropriate tool/arguments
                    tool_name, arguments = await self._llm_select_tool(chat_input.message)
                    
                    # Add client token for authentication
                    arguments["client_token"] = chat_input.client_token or "test8f4e2b9c7a1d6e3f5a8b2c4d7e9f1a3b6c8e"
                    
                    # Call the MCP tool
                    result = await mcp_client.call_tool(tool_name, arguments)
                    
                    # Format the response in a conversational way
                    if tool_name == "search_products":
                        response = f"I searched for '{arguments['query']}' in the product catalog. {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 = f"I processed your request using {tool_name}: {result}"
                    
                    tool_used = tool_name
                    
                except Exception as e:
                    error_msg = str(e).lower()
                    if any(keyword in error_msg for keyword in ['connection', 'timeout', 'unreachable', 'refused']):
                        response = f"I'm having trouble connecting to the PrestaShop system right now. This might be temporary - please try again in a moment."
                    else:
                        response = f"I encountered an issue while processing your request: {str(e)}. Could you please try rephrasing your question?"
                    tool_used = tool_name
        
        # Update conversation history
        updated_history = chat_input.conversation_history + [
            ChatMessage(role="user", content=chat_input.message),
            ChatMessage(role="assistant", content=response)
        ]
        
        return ChatOutput(
            response=response,
            tool_used=tool_used,
            conversation_history=updated_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:
            # Validate input
            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}"
            }
        
        # Initialize MCP client if not available (fallback)
        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}"
                }
        
        # Add default PrestaShop URL if not provided
        if "api_base_url" not in payload.arguments:
            payload.arguments["api_base_url"] = DEFAULT_PRESTASHOP_URL
        
        try:
            # Call the MCP tool
            result = await mcp_client.call_tool(payload.tool, payload.arguments)
            return {"ok": True, "tool": payload.tool, "result": result}
        except Exception as e:
            # Check if it's an MCP connection error
            error_msg = str(e).lower()
            if any(keyword in error_msg for keyword in ['connection', 'timeout', 'unreachable', 'refused']):
                return {
                    "ok": False, 
                    "tool": payload.tool,
                    "result": f"I'm sorry, but I'm experiencing connection issues with the PrestaShop service while trying to execute the '{payload.tool}' operation. This could be due to network connectivity or the PrestaShop API being temporarily unavailable. Please try again in a moment.",
                    "error": str(e)
                }
            else:
                return {
                    "ok": False, 
                    "tool": payload.tool,
                    "result": f"I encountered an error while processing your '{payload.tool}' request: {str(e)}. Please check your input parameters and try again.",
                    "error": str(e)
                }

# Global handler instances
handler = EndpointHandler()
chat_handler = ChatHandler()

# Chat Interface Routes (Main UI)
@app.post("/", response_model=ChatOutput)
async def chat_interface(chat_input: ChatInput) -> ChatOutput:
    """
    Main chat interface - natural language interaction with PrestaShop MCP tools
    """
    return await chat_handler.process_message(chat_input)

@app.get("/")
async def chat_ui():
    """Serve a simple chat UI for testing"""
    html_content = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>PrestaShop MCP Chat Interface</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: 10px 0; padding: 10px; border-radius: 5px; }
            .user { background-color: #e3f2fd; text-align: right; }
            .assistant { background-color: #f5f5f5; }
            .input-container { display: flex; gap: 10px; }
            #messageInput { flex: 1; padding: 10px; }
            button { padding: 10px 20px; }
        </style>
    </head>
    <body>
        <h1>🧠 PrestaShop MCP Chat Interface</h1>
        <p>Ask me about products! Try: "Search for laptops" or "Show me details of product 1"</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:
        # Use LLM to determine which tool to call (if any)
        if llm_client:
            tool_name, arguments = await llm_client.parse_message_for_tool_call(chat_input.message)
        else:
            # Fallback to simple detection if LLM not available
            tool_name, arguments = None, {}
        
        if tool_name and mcp_client:
            # Add client token for authentication
            arguments["client_token"] = chat_input.client_token or "test8f4e2b9c7a1d6e3f5a8b2c4d7e9f1a3b6c8e"
            
            # Call the MCP tool
            logger.info(f"πŸ”§ Calling MCP tool: {tool_name} with {arguments}")
            result = await mcp_client.call_tool(tool_name, arguments)
            
            # Format the response naturally
            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:
            # Handle as conversational message
            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
        
        # Update conversation history
        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)
        )


# Helper function to identify tool needed based on message
        logger.error(f"πŸ”¬ Full traceback: {traceback.format_exc()}")
        
        return ChatOutput(
            response=f"Sorry, I encountered a technical error: {str(e)}",
            conversation_history=list(chat_input.conversation_history)
        )

# API Routes (Programmatic Access)
@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 prompt-based requests
    
    Accepts:
    - Tool-based: {"tool": "tool_name", "arguments": {...}}
    - Prompt-based: {"prompt": "user question", "session_id": "...", "context": {...}}
    """
    request_start = time.time()
    request_type = "prompt" if "prompt" in data else "tool"
    
    logger.info(f"🎯 API Inference Request - Type: {request_type}")
    logger.info(f"πŸ“Š Request data keys: {list(data.keys())}")
    logger.info(f"πŸ“¦ Full request data: {json.dumps(data, indent=2)}")
    
    # Check if this is a prompt-based request
    if "prompt" in data:
        logger.info(f"πŸ€– Processing prompt-based request")
        prompt = data.get('prompt', '')
        session_id = data.get('session_id', '')
        context = data.get('context', {})
        client_token = data.get('client_token', '')
        
        logger.info(f"πŸ“ Prompt length: {len(prompt)} characters")
        logger.info(f"πŸ”‘ Session ID: {session_id}")
        logger.info(f"πŸͺ Client token: {client_token[:8]}***{client_token[-4:] if len(client_token) > 12 else 'invalid'}")
        logger.info(f"πŸ“‹ Context keys: {list(context.keys())}")
        logger.info(f"πŸ“– Prompt preview: {prompt[:200]}{'...' if len(prompt) > 200 else ''}")
        
        # TODO: Look up shop information from client_token in database
        # For now, we'll proceed with the request but log the token for identification
        if client_token:
            logger.info(f"πŸͺ Authenticated request from client token: {client_token[:8]}***")
        else:
            logger.warning(f"⚠️ Unauthenticated request - no client token provided")
        
        try:
            logger.info(f"πŸ”§ Creating ChatInput object")
            chat_input = ChatInput(
                message=prompt,
                conversation_history=context.get('history', []),
                session_id=session_id,
                client_token=client_token
            )
            logger.info(f"βœ… ChatInput created successfully")
            
            logger.info(f"πŸš€ Calling chat_handler.process_message")
            chat_result = await chat_handler.process_message(chat_input)
            logger.info(f"βœ… Chat processing completed")
            
            response_time = time.time() - request_start
            logger.info(f"πŸ“€ Prompt request completed in {response_time:.3f}s")
            logger.info(f"πŸ“ Response length: {len(chat_result.response)} characters")
            logger.info(f"πŸ› οΈ Tool used: {getattr(chat_result, 'tool_used', 'None')}")
            
            result = InferenceOutput(
                ok=True,
                response=chat_result.response,
                tool=chat_result.tool_used if hasattr(chat_result, 'tool_used') else None
            )
            logger.info(f"βœ… Returning successful prompt response")
            return result
            
        except Exception as e:
            response_time = time.time() - request_start
            logger.error(f"πŸ’₯ Prompt processing error after {response_time:.3f}s: {e}")
            logger.error(f"πŸ” Exception type: {type(e).__name__}")
            logger.error(f"πŸ“‹ Exception details: {str(e)}")
            import traceback
            logger.error(f"πŸ”¬ Full traceback: {traceback.format_exc()}")
            
            result = InferenceOutput(
                ok=False,
                error=f"Failed to process prompt: {str(e)}"
            )
            logger.info(f"❌ Returning error response for prompt")
            return result
    
    else:
        # Handle traditional MCP tool request
        tool_name = data.get('tool', 'unknown')
        arguments = data.get('arguments', {})
        
        logger.info(f"πŸ”§ Processing tool-based request")
        logger.info(f"πŸ› οΈ Tool name: {tool_name}")
        logger.info(f"οΏ½ Arguments: {json.dumps(arguments, indent=2)}")
        
        try:
            logger.info(f"πŸš€ Calling handler for tool request")
            result = await handler(data)
            response_time = time.time() - request_start
            
            logger.info(f"βœ… Tool request completed in {response_time:.3f}s")
            logger.info(f"πŸ“€ Tool result keys: {list(result.keys()) if isinstance(result, dict) else 'not dict'}")
            logger.info(f"πŸ“Š Tool result: {json.dumps(result, indent=2) if isinstance(result, dict) else str(result)}")
            
            final_result = InferenceOutput(**result)
            logger.info(f"βœ… Returning successful tool response")
            return final_result
            
        except Exception as e:
            response_time = time.time() - request_start
            logger.error(f"πŸ’₯ Tool processing error after {response_time:.3f}s: {e}")
            logger.error(f"πŸ” Exception type: {type(e).__name__}")
            logger.error(f"πŸ“‹ Exception details: {str(e)}")
            import traceback
            logger.error(f"πŸ”¬ Full traceback: {traceback.format_exc()}")
            
            result = InferenceOutput(
                ok=False,
                error=f"Failed to process tool request: {str(e)}"
            )
            logger.info(f"❌ Returning error response for tool")
            return result

@app.get("/health", response_model=HealthResponse)
async def health_check():
    """Health check endpoint"""
    logger.info("πŸ’“ Health check requested")
    global mcp_client
    mcp_available = mcp_client is not None
    available_tools = ["get_product_details", "get_product_features", "get_product_images", "search_products"]
    
    logger.info(f"πŸ” MCP client available: {mcp_available}")
    logger.info(f"πŸ› οΈ Available tools: {available_tools}")
    
    health_response = HealthResponse(
        status="healthy",
        mcp_available=mcp_available,
        available_tools=available_tools
    )
    
    logger.info(f"βœ… Health check completed - Status: healthy")
    return health_response

@app.get("/tools")
async def list_tools():
    """List available MCP tools"""
    tools = [
        {
            "name": "get_product_details",
            "description": "Get detailed information about a PrestaShop product",
            "required_args": ["product_id", "api_base_url"],
            "optional_args": ["lang_id"]
        },
        {
            "name": "get_product_features", 
            "description": "Get product features and specifications",
            "required_args": ["product_id", "api_base_url"],
            "optional_args": []
        },
        {
            "name": "get_product_images",
            "description": "Get product images and media",
            "required_args": ["product_id", "api_base_url"],
            "optional_args": []
        },
        {
            "name": "search_products",
            "description": "Search for products in PrestaShop catalog",
            "required_args": ["query", "api_base_url"],
            "optional_args": ["limit", "category_id"]
        }
    ]
    
    return {
        "available_tools": tools,
        "default_prestashop_url": DEFAULT_PRESTASHOP_URL,
        "mcp_status": "integrated" if mcp_client else "unavailable"
    }

@app.get("/")
async def root():
    """Root endpoint - API information"""
    return {
        "message": "PrestaShop MCP Inference Endpoint",
        "version": "1.0.0",
        "status": "ready",
        "description": "Simple inference endpoint for PrestaShop MCP tools",
        "endpoints": ["/", "/health", "/tools"],
        "usage": {
            "example": {
                "tool": "get_product_details",
                "arguments": {
                    "product_id": 123,
                    "api_base_url": "https://your-shop.com"
                }
            },
            "curl_example": 'curl -X POST / -H "Content-Type: application/json" -d \'{"tool": "get_product_details", "arguments": {"product_id": 123}}\''
        }
    }

if __name__ == "__main__":
    port = int(os.getenv("PORT", "7860"))  # HF Spaces default port
    
    logger.info("πŸš€ Starting HF Inference Point server")
    logger.info(f"🌐 Host: 0.0.0.0")
    logger.info(f"πŸ”Œ Port: {port}")
    logger.info(f"🏠 Environment: {os.getenv('ENVIRONMENT', 'development')}")
    logger.info(f"πŸ“ Log level: {logging.getLevelName(logger.level)}")
    logger.info(f"πŸ“ Working directory: {os.getcwd()}")
    logger.info(f"🐍 Python version: {sys.version}")
    
    # Configure uvicorn logging to be more verbose
    uvicorn_config = uvicorn.Config(
        app=app, 
        host="0.0.0.0", 
        port=port,
        log_level="debug",
        access_log=True
    )
    
    logger.info("🎯 Starting uvicorn server with debug logging")
    server = uvicorn.Server(uvicorn_config)
    server.run()