import os import asyncio from llama_index.core import Document, VectorStoreIndex, Settings from llama_index.embeddings.huggingface import HuggingFaceEmbedding from pymongo import MongoClient from anthropic import AsyncAnthropic import requests import base64 from PIL import Image from io import BytesIO import traceback try: Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5") Settings.chunk_size = 512 except Exception as e: print(f"Error initializing LlamaIndex settings in enhanced_planner_tool: {e}") async_anthropic_client = AsyncAnthropic() try: client = MongoClient(os.getenv("MONGODB_URI")) db = client.get_database() collection = db.get_collection("travelrecords") except Exception as e: print(f"FATAL: Could not connect to MongoDB for enhanced_planner_tool. Error: {e}") collection = None async def get_image_b64_data(image_url: str): def blocking_io_and_compress(): try: response = requests.get(image_url, timeout=10) response.raise_for_status() content = response.content content_type_header = response.headers.get('Content-Type','image/jpeg') MAX_RAW_SIZE_MB = 4.5 if len(content) <= MAX_RAW_SIZE_MB * 1024 * 1024: return content_type_header, base64.b64encode(content).decode('utf-8') print(f"⚠️ Image >{MAX_RAW_SIZE_MB}MB, attempting compression: {image_url}") img = Image.open(BytesIO(content)) if img.mode == 'RGBA' or img.mode == 'LA' or (img.mode == 'P' and 'transparency' in img.info): img = img.convert('RGB') buffer = BytesIO() img.save(buffer, format="JPEG", quality=70, optimize=True) compressed_data = buffer.getvalue() MAX_COMPRESSED_SIZE_MB_FOR_API = 3.7 if len(compressed_data) > MAX_COMPRESSED_SIZE_MB_FOR_API * 1024 * 1024: print(f"❌ Compression to JPEG quality 70 failed, still too large for {image_url}") return None, None return "image/jpeg", base64.b64encode(compressed_data).decode('utf-8') except Exception as e: # Catch more specific exceptions if needed print(f"❌ Error processing image {image_url}: {e}") return None, None return await asyncio.to_thread(blocking_io_and_compress) async def describe_image(image_data: str, media_type: str) -> str: if not image_data: return "" try: response = await async_anthropic_client.messages.create( model="claude-3-haiku-20240307", max_tokens=75, messages=[{"role":"user", "content":[ {"type":"image", "source":{"type":"base64", "media_type":media_type, "data":image_data}}, {"type":"text", "text":"Briefly describe this travel photo's key elements and atmosphere in one sentence. Focus on what it might reveal about the traveler's preferences if possible."} ]}] ) return response.content[0].text.strip() if response.content and response.content[0].text else "" except Exception as e: print(f"ERROR calling vision model for image description: {e}") return "" async def describe_all_images(image_urls: list) -> str: if not image_urls: return "No images provided for description." MAX_IMAGES_TO_DESCRIBE = 3 processed_image_urls = image_urls[:MAX_IMAGES_TO_DESCRIBE] if len(image_urls) > MAX_IMAGES_TO_DESCRIBE: print(f"Warning: Describing first {MAX_IMAGES_TO_DESCRIBE} of {len(image_urls)} images.") get_data_tasks = [get_image_b64_data(url) for url in processed_image_urls] image_processing_results = await asyncio.gather(*get_data_tasks, return_exceptions=True) desc_tasks = [] for i, result in enumerate(image_processing_results): if isinstance(result, Exception): continue media_type, img_data = result if img_data: desc_tasks.append(describe_image(img_data, media_type)) if not desc_tasks: return "Could not process any images for description." descriptions = await asyncio.gather(*desc_tasks, return_exceptions=True) valid_descriptions = [desc for desc in descriptions if isinstance(desc, str) and desc] if not valid_descriptions: return "No valid image descriptions could be generated." return "\n".join(valid_descriptions) async def enhanced_create_personalized_plan( user_name_input: str, new_destination: str, trip_duration_days: int, user_request: str, web_intelligence: str = None ) -> str: if collection is None: return "Tool Error: MongoDB connection is not available for enhanced_personalized_planner." print(f"--- [Enhanced Planner] Starting for User(s): '{user_name_input}' to {new_destination} ---") if web_intelligence: print(f"--- Incorporating Web Intelligence (first 100 chars): {web_intelligence[:100]}... ---") parsed_individual_names = [name.strip() for name in user_name_input.replace(" and ", ",").split(',') if name.strip()] if not parsed_individual_names: parsed_individual_names = [user_name_input.strip()] is_single_person_plan = len(parsed_individual_names) == 1 and len(user_name_input.split()) < 4 print(f"DEBUG: Parsed names for RAG: {parsed_individual_names}, Is single person plan: {is_single_person_plan}") all_retrieved_persona_contexts = [] found_records_for_any_user = False for name_to_query in parsed_individual_names: print(f"Processing records for: {name_to_query}") try: user_records_cursor = collection.find({"name": {"$regex": f"^{name_to_query}$", "$options": "i"}}) user_records = await asyncio.to_thread(list, user_records_cursor) if user_records: found_records_for_any_user = True print(f"Found {len(user_records)} past trip records for {name_to_query}.") async def create_document_from_record(record): uploaded_images = record.get('uploadedImages', []) image_descriptions_summary = "No image information." if isinstance(uploaded_images, list) and uploaded_images: image_descriptions_summary = await describe_all_images(uploaded_images) text_content_parts = [ f"Trip to {record.get('destinationName', 'N/A')} by {name_to_query}", f"Highlights: {record.get('highlights', 'N/A')}", f"Memorable Food: {record.get('memorableFood', 'N/A')}", f"Deepest Impression Spot: {record.get('deepestImpressionSpot', 'N/A')}", f"Image Summary: {image_descriptions_summary}" ] text_content = "\n".join(filter(None, text_content_parts)) return Document(text=text_content, metadata={"source_traveler": name_to_query}) documents_for_rag = await asyncio.gather(*[create_document_from_record(r) for r in user_records]) if documents_for_rag: print(f"Created {len(documents_for_rag)} documents for {name_to_query}.") def build_and_retrieve_for_single_user(docs): index = VectorStoreIndex.from_documents(docs, show_progress=False) retriever = index.as_retriever(similarity_top_k=3) nodes = retriever.retrieve(f"Key travel style, preferences, likes, and dislikes of {name_to_query} based on their trips.") return "\n\n---\n".join([node.get_content() for node in nodes]) persona_context_for_user = await asyncio.to_thread(build_and_retrieve_for_single_user, documents_for_rag) if persona_context_for_user.strip(): all_retrieved_persona_contexts.append(f"Context for {name_to_query}:\n{persona_context_for_user}") else: print(f"No past travel records found for {name_to_query} in the database.") except Exception as e: print(f"Error processing records for {name_to_query}: {e}\n{traceback.format_exc()}") if not found_records_for_any_user or not all_retrieved_persona_contexts: retrieved_context_for_llm = "No specific past travel records found for the mentioned traveler(s) to infer detailed personas from the database." else: retrieved_context_for_llm = "\n\n===\n\n".join(all_retrieved_persona_contexts) print(f"\n--- Combined Retrieved Context for LLM (first 500 chars) ---\n{retrieved_context_for_llm[:500]}...\n-----------------------------------\n") system_prompt_for_final_llm = "You are an expert travel agent and creative itinerary crafter. Your task is to synthesize a user's historical travel persona, their current specific request, and any provided real-time travel intelligence into a compelling, personalized, day-by-day travel itinerary. The itinerary should be actionable, inspiring, and directly address all provided inputs." final_llm_prompt = f""" **Objective:** Create a hyper-personalized travel itinerary, potentially for a group of travelers. **User Profile & Request:** * **Traveler(s) Name(s):** {user_name_input} * **Desired Destination:** {new_destination} * **Trip Length:** {trip_duration_days} days * **User's Specific Request for this Trip:** "{user_request}" **Combined Inferred Persona Context (from past travels of mentioned individuals, make it detailed if available):** --- {retrieved_context_for_llm} --- """ if web_intelligence: final_llm_prompt += f""" **Current Travel Intelligence for {new_destination} (during the user's travel period):** --- {web_intelligence} --- **IMPORTANT INSTRUCTION FOR USING THE ABOVE INTELLIGENCE:** This 'Current Travel Intelligence' is a critical input. It reflects a wide array of real-time information pertinent to the user's trip to {new_destination} during their specified travel window. This information may include, but is not limited to: - Expected **weather** conditions and any related advisories. - **Transportation** updates, including public transit status, road conditions, potential strikes affecting various modes of transport (e.g., Finnair, local trains/buses), or other transit disruptions. - Information on **festivals, holidays, special celebrations, exhibitions, or unique local events** occurring. - Potential opportunities for viewing **natural phenomena** (e.g., Northern Lights, seasonal blooms, wildlife migrations). - Notices regarding **temporary closures, renovations, or changed operating hours** for attractions, restaurants, museums, or other points of interest. - Official **travel warnings, safety advisories, or health recommendations**. - Details on planned **demonstrations, protests, public gatherings, or other public order events** that might impact travel plans, access to areas, or safety. You **MUST actively analyze ALL aspects of this intelligence** and skillfully weave relevant points into the daily itinerary, specific recommendations, general advice, and any contingency planning. Your goal is to make the generated travel plan as practical, safe, up-to-date, and enriching as possible by leveraging this information. """ else: final_llm_prompt += f""" **Current Travel Intelligence for {new_destination}:** --- No specific real-time travel intelligence was provided for this planning request. The following itinerary is based on general knowledge and the user's preferences. It is highly recommended that the user checks current local conditions, event schedules, and advisories closer to their travel date. --- """ final_llm_prompt += """ **Your Generation Task:** 1. **Greeting and Persona/Group Summary (DIFFERENTIATED INSTRUCTIONS FOR SINGLE VS. GROUP):** """ if is_single_person_plan: final_llm_prompt += f""" Start by addressing the traveler: "Hello {parsed_individual_names[0]}!" (or use the full user_name_input if it's clearly a single person's full name). Next, based on the 'Combined Inferred Persona Context' (which should primarily reflect this single user if their records were found), craft a **VERY DETAILED AND INSIGHTFUL summary of their travel personality. This summary MUST be comprehensive, aiming for AT LEAST 8 well-developed sentences, forming one or two rich paragraphs.** Begin this detailed persona with: "Based on your past travel experiences, I've discovered you are a traveler who...". You MUST elaborate on several key aspects, using specific examples from their past trips if the 'Combined Inferred Persona Context' allows: * **Core Passions & Interests:** What truly drives their travels? (e.g., "a deep-seated fascination with ancient civilizations, evident from your exploration of [Specific Site Mentioned in RAG Context]", "an adventurous spirit for authentic culinary exploration, demonstrated by your quest for [Specific Food Type in RAG Context]"). * **Travel Style & Pace:** How do they prefer to experience a destination? (e.g., "favors a balanced itinerary that masterfully blends iconic landmark visits with the thrill of discovering off-the-beaten-path local gems", "seems to thrive on a moderately paced exploration, which allows for both meticulously planned activities and delightful spontaneous moments of discovery"). * **Decision Drivers & Values:** What underlying factors influence their travel choices? (e.g., "demonstrates a savvy and discerning approach, consistently seeking high-quality, memorable experiences while being adept at identifying and avoiding tourist traps or overpriced ventures", "shows a clear appreciation for value but is also willing to invest in truly unique and enriching opportunities like [Example from RAG Context if available]"). * **Noteworthy Preferences & Quirks (if evident):** Highlight any distinct likes (e.g., "a pronounced enjoyment of breathtaking natural vistas, perhaps similar to [View Mentioned in RAG Context], or dynamic cityscapes") or dislikes (e.g., "a noted aversion to overly crowded tourist spots or subpar service, as indicated by your feedback on [Negative Experience from RAG Context]"). * **Aesthetic Sensibilities & Curiosity:** What types of beauty or knowledge do they pursue? (e.g., "possesses a keen eye for architectural marvels, both ancient and contemporary", "driven by a curiosity for diverse cultures and historical narratives"). This detailed persona summary is CRUCIAL for making the user feel deeply understood before presenting the plan. If the 'Combined Inferred Persona Context' is minimal or absent even for a single user (e.g., states 'No specific past travel records found...' or similar in the context), you should still attempt to create a thoughtful, **brief (2-4 sentences) introductory persona observation based on ANY clues available in the current 'User's Specific Request for this Trip'.** For example: "Hello {parsed_individual_names[0]}! While I'm still getting to know your detailed travel style from past trips, your current request for a trip to {new_destination} focusing on [mention key aspects from user_request, e.g., 'exploring historical sites'] suggests you have a keen interest in [inferred preference 1, e.g., 'delving into the past'] and perhaps [inferred preference 2, e.g., 'experiencing the local culture firsthand']. We can certainly build a wonderful trip around these themes!" """ else: group_greeting_names = ", ".join(parsed_individual_names[:-1]) + f", and {parsed_individual_names[-1]}" if len(parsed_individual_names) > 1 else user_name_input final_llm_prompt += f""" Start by addressing all travelers: "Hello {group_greeting_names}!" Based on the 'Combined Inferred Persona Context' (which may contain information for one, some, or all mentioned individuals, or be general if specific RAG per person was not fully successful), your task is to provide a **CONCISE persona summary for EACH traveler if distinct information is available from the context, or a brief collective summary if not.** * **For EACH traveler in '{", ".join(parsed_individual_names)}' for whom the 'Combined Inferred Persona Context' provides distinct insights:** Provide a **brief (1-3 sentences) individual summary** of their apparent key travel interests or style as revealed in the context. For example: * "From the provided context for Tracy, past trips suggest a strong interest in historical landmarks and authentic culinary experiences." * "The context for Liu indicates a preference for outdoor adventures and scenic natural landscapes." * **If distinct insights for each individual are scarce in the context OR if the context is more general for the group:** Provide a **brief (1-3 sentences) collective summary** that attempts to capture any apparent shared travel preferences OR highlights how the plan will aim to balance potentially diverse (even if not fully known) interests. For example: "For your group's adventure, the available insights suggest an appreciation for diverse cultural experiences and perhaps an enjoyment of good local food. This itinerary will aim to offer a variety of activities to cater to different tastes within the group." The goal for a group is a concise understanding relevant to crafting a balanced group plan, NOT an exhaustive analysis of each individual unless very rich, distinct data for each is present AND easily summarizable in 1-3 sentences per person from the provided context. If the 'Combined Inferred Persona Context' effectively states 'No specific past travel records found for any of the mentioned travelers...', then state that clearly: "Hello {group_greeting_names}! While I don't have specific past travel data for your group to draw detailed individual personas from, I'll craft a great itinerary based on your current request and general travel best practices for {new_destination}." """ final_llm_prompt += """ If absolutely NO clues are available (neither in historical context nor in current request to infer from for single or group), then default to a friendly, direct greeting like: "Great news, {user_name_input}! Let's get your trip to {new_destination} planned." 2. **Transition to Plan & Overall Trip Context (Incorporating Web Intelligence):** Follow the persona summary/greeting. **Before starting the day-by-day itinerary, synthesize any overarching key takeaways from the 'Current Travel Intelligence' (if provided) that provide essential overall context for the trip.** This could be a brief summary of expected general conditions or major factors to keep in mind throughout the journey. For example: "For your trip to {new_destination}, please be aware that [mention a key city-wide event or general advisory from web_intelligence, e.g., 'a major public transport strike is anticipated during your visit, so planning alternative travel will be crucial,' or 'the weather is expected to be exceptionally warm, so pack accordingly and stay hydrated.']. We'll factor this into the daily plans." If no specific web_intelligence was provided, you can state: "Let's craft your itinerary for {new_destination} based on your preferences. I advise checking local conditions and event schedules closer to your travel dates for the latest information." Then, transition to the plan: "With these considerations and your preferences in mind, here’s a tailored itinerary for your {trip_duration_days}-day adventure in {new_destination}:" 3. **Detailed Day-by-Day Itinerary (Infused with Web Intelligence & Persona):** Provide a comprehensive plan. For each day or specific activity: * Present the activity or place engagingly. * Naturally weave in the REASON for this recommendation, **explicitly connecting it to the user's (or individual group member's, or the group's collective) inferred persona (from step 1), specific requests, mentioned interests, or relevant `Current Travel Intelligence`.** Make these connections clear and compelling. * If `Current Travel Intelligence` was relevant, explicitly demonstrate how specific pieces of that intelligence influence the recommendations, timing, or advice for THIS day or activity. For instance: * If a festival is mentioned: "The [Festival Name] is taking place near your hotel today, offering a wonderful chance to experience local culture firsthand; I've left the afternoon flexible for you to explore it." * If a museum has special hours: "We'll visit the [Museum Name] in the morning, as the travel intelligence indicates it has extended hours on [Day] but may be very busy later due to [Event]." * Include practical tips. 4. **Concluding General Advice (Reinforcing Key Intelligence if needed):** After the day-by-day plan: * Briefly reiterate any **critical overarching advice from 'Current Travel Intelligence'** that the user absolutely must remember, especially if it impacts multiple days or general safety/logistics, and hasn't been fully emphasized enough within the daily plans. * For example: "As a final reminder, please keep an eye on updates regarding the [previously mentioned strike/event] as your travel dates approach, and always have a backup for transportation." * If all critical intelligence has been well-integrated daily, this section can be very brief or focus on general well-wishes. The primary goal is that no crucial piece of provided intelligence is overlooked by the user. 5. **Concluding Remarks (Optional):** End with a friendly closing. **Output Style:** - Be engaging, informative, and helpful. - Use clear headings for days (e.g., "Day 1: Arrival and Ancient Wonders"). - Use bullet points for activities within each day. - The language should generally match the user's request language (assume English if not specified, but be adaptable if the user's request or persona context clearly indicates another language preference for the output). """ print(f"--- Calling Final LLM for itinerary generation (persona detail adjusted: single_detailed={is_single_person_plan})... ---") try: response_message = await async_anthropic_client.messages.create( model="claude-3-5-sonnet-20240620", max_tokens=4096, system=system_prompt_for_final_llm, messages=[{"role":"user", "content":final_llm_prompt}] ) generated_plan = response_message.content[0].text print(f"--- Generated Plan (first 300 chars): {generated_plan[:300]}... ---") return generated_plan except Exception as e: error_message = f"LLM Error in enhanced_planner: {type(e).__name__}: {str(e)}" print(f"{error_message}\n{traceback.format_exc()}") return error_message