|
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 |
|
|
|
Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5") |
|
Settings.chunk_size = 512 |
|
async_anthropic_client = AsyncAnthropic() |
|
client = MongoClient(os.getenv("MONGODB_URI")) |
|
db = client.get_database() |
|
collection = db.get_collection("travelrecords") |
|
|
|
|
|
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 |
|
|
|
if len(content) <= 5 * 1024 * 1024: |
|
return response.headers.get('Content-Type','image/jpeg'), base64.b64encode(content).decode('utf-8') |
|
|
|
print(f"⚠️ Image >5MB, compressing: {image_url}") |
|
img = Image.open(BytesIO(content)) |
|
buffer = BytesIO() |
|
img.save(buffer, format="JPEG", quality=70, optimize=True) |
|
compressed_data = buffer.getvalue() |
|
|
|
if len(compressed_data) > 5 * 1024 * 1024: |
|
print(f"❌ Compression failed, still too large.") |
|
return None, None |
|
return "image/jpeg", base64.b64encode(compressed_data).decode('utf-8') |
|
except Exception as e: |
|
print(f"❌ Image processing error: {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."} |
|
]}] |
|
) |
|
return response.content[0].text |
|
except Exception as e: |
|
print(f"ERROR calling vision model: {e}") |
|
return "" |
|
|
|
async def describe_all_images(image_urls: list) -> str: |
|
if not image_urls: return "No images provided." |
|
tasks = [get_image_b64_data(url) for url in image_urls] |
|
results = await asyncio.gather(*tasks) |
|
|
|
desc_tasks = [describe_image(img_data, media_type) for media_type, img_data in results if img_data] |
|
descriptions = await asyncio.gather(*desc_tasks) |
|
return "\n".join(descriptions) |
|
|
|
async def create_personalized_plan(user_name: str, new_destination: str, trip_duration_days: int, user_request: str) -> str: |
|
print(f"--- [Corrected Async] Starting Personalized Planner for {user_name} to {new_destination} ---") |
|
try: |
|
user_records = await asyncio.to_thread(list, collection.find({"name": {"$regex": user_name, "$options": "i"}})) |
|
if not user_records: return f"I couldn't find any past travel records for {user_name}." |
|
print(f"Found {len(user_records)} past trips for {user_name}.") |
|
|
|
async def create_doc(record): |
|
image_descriptions = await describe_all_images(record.get('uploadedImages', [])) |
|
text_content = (f"Trip to {record.get('destinationName', 'N/A')}: Highlights: {record.get('highlights', 'N/A')}\nImage Summary: {image_descriptions}") |
|
return Document(text=text_content) |
|
|
|
documents = await asyncio.gather(*[create_doc(r) for r in user_records]) |
|
print(f"Successfully created {len(documents)} documents for RAG.") |
|
|
|
def build_and_retrieve(docs): |
|
print("Building RAG index... You should see a progress bar now.") |
|
index = VectorStoreIndex.from_documents(docs, show_progress=True) |
|
return index.as_retriever(similarity_top_k=3).retrieve(f"Preferences for {new_destination}: {user_request}") |
|
|
|
retrieved_nodes = await asyncio.to_thread(build_and_retrieve, documents) |
|
retrieved_context = "\n\n---\n\n".join([node.get_content() for node in retrieved_nodes]) |
|
print(f"\n--- Retrieved Context for Persona ---\n{retrieved_context}\n-----------------------------------\n") |
|
|
|
|
|
system_prompt = "You are an expert travel agent and persona analyst. Your core function is to synthesize a user's past travel preferences with their current request to generate a truly personalized and actionable travel itinerary." |
|
|
|
final_prompt = f""" |
|
**Mission:** Generate a hyper-personalized travel plan. |
|
|
|
**1. Input Data:** |
|
|
|
* **User Name:** {user_name} |
|
* **Destination:** {new_destination} |
|
* **Trip Duration:** {trip_duration_days} days |
|
* **Specific Request:** "{user_request}" |
|
* **User's Historical Travel Context (for Persona Analysis):** |
|
--- |
|
{retrieved_context} |
|
--- |
|
|
|
**2. Your Task (A mandatory two-step process):** |
|
|
|
* **Step A: Define the User's Travel Persona.** |
|
Based *only* on their historical preferences provided above, build a detailed understanding of this user's core travel style, values, and preferences. |
|
|
|
* **Step B: Craft the Custom Itinerary.** |
|
Using your deep understanding of the user's persona from Step A, create a day-by-day travel plan for their trip to {new_destination}. Every recommendation must align with their inferred preferences. |
|
|
|
**3. Required Output Format (Crucial for user connection):** |
|
|
|
1. **Greeting and Persona Summary:** |
|
Start with a detailed summary of the user's travel persona, beginning with the phrase "Based on your past travel experiences, I've discovered you are a traveler who...". This summary should be rich with insights. For example: "Based on your past travel experiences, I've discovered you are a traveler who seeks out spectacular, awe-inspiring moments and deep cultural immersion. You appreciate both iconic, grand-scale views (like the fireworks in Tokyo and the Valley of the Kings in Luxor) and have a keen sense for authentic cuisine, while actively avoiding overrated experiences (like the cocktails in Helsinki). You balance thrilling adventures (hot air ballooning) with quiet cultural exploration and maintain a savvy, cautious approach to new environments." |
|
|
|
2. **Introduction to the Plan:** |
|
After the persona summary, add a transitional sentence like: "With this understanding of your unique style, I've crafted this tailored itinerary for your Paris adventure:" |
|
|
|
3. **Personalized Itinerary:** |
|
Finally, present the day-by-day itinerary in a clear, easy-to-read format. |
|
""" |
|
|
|
|
|
print("--- Calling Final LLM with direct RAG context... ---") |
|
response_message = await async_anthropic_client.messages.create( |
|
model="claude-3-5-sonnet-20240620", |
|
max_tokens=4096, system=system_prompt, |
|
messages=[{"role":"user", "content":final_prompt}] |
|
) |
|
return response_message.content[0].text |
|
|
|
except Exception as e: |
|
error_message = f"FATAL TOOL ERROR: {type(e).__name__}: {str(e)}" |
|
print("\n\n---" + error_message + "---\n\n") |
|
return error_message |