""" Flare – Prompt Builder (Refactored with Multi-language Support) ============================================================== """ from typing import List, Dict, Any from datetime import datetime, timedelta from config.config_provider import ConfigProvider from utils.logger import log_info, log_error, log_warning, log_debug from config.locale_manager import LocaleManager # Date helper for locale-aware date expressions def _get_date_context(locale_code: str = "tr") -> Dict[str, str]: """Generate date context for date expressions""" now = datetime.now() # Get locale data locale_data = LocaleManager.get_locale(locale_code) weekdays = locale_data.get("weekdays", ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]) today_weekday = weekdays[now.weekday()] # Calculate various dates dates = { "today": now.strftime("%Y-%m-%d"), "tomorrow": (now + timedelta(days=1)).strftime("%Y-%m-%d"), "day_after_tomorrow": (now + timedelta(days=2)).strftime("%Y-%m-%d"), "this_weekend_saturday": (now + timedelta(days=(5-now.weekday())%7)).strftime("%Y-%m-%d"), "this_weekend_sunday": (now + timedelta(days=(6-now.weekday())%7)).strftime("%Y-%m-%d"), "next_week_same_day": (now + timedelta(days=7)).strftime("%Y-%m-%d"), "two_weeks_later": (now + timedelta(days=14)).strftime("%Y-%m-%d"), "today_weekday": today_weekday, "today_day": now.day, "today_month": now.month, "today_year": now.year, "locale_code": locale_code } return dates def _build_locale_aware_date_prompt(param, date_ctx: Dict, locale_data: Dict, locale_code: str) -> str: """Build locale-aware date extraction prompt""" # Get locale-specific date patterns date_patterns = locale_data.get("date_patterns", {}) months = locale_data.get("months", []) prompt_parts = [f"• {param.name}: {param.extraction_prompt}"] prompt_parts.append(f" IMPORTANT DATE RULES for {locale_data.get('name', 'this language')}:") # Add locale-specific patterns if locale_code == "tr": prompt_parts.extend([ f" - 'bugün' = {date_ctx['today']}", f" - 'yarın' = {date_ctx['tomorrow']}", f" - 'öbür gün' = {date_ctx['day_after_tomorrow']}", f" - 'bu hafta sonu' = {date_ctx['this_weekend_saturday']} or {date_ctx['this_weekend_sunday']}", f" - 'bu cumartesi' = {date_ctx['this_weekend_saturday']}", f" - 'bu pazar' = {date_ctx['this_weekend_sunday']}", f" - 'haftaya' or 'gelecek hafta' = add 7 days to current date", f" - 'haftaya bugün' = {date_ctx['next_week_same_day']}", f" - 'iki hafta sonra' = {date_ctx['two_weeks_later']}", f" - '15 gün sonra' = add 15 days to today", f" - '10 Temmuz' = {date_ctx['today_year']}-07-10", f" - Turkish months: Ocak=01, Şubat=02, Mart=03, Nisan=04, Mayıs=05, Haziran=06, " f"Temmuz=07, Ağustos=08, Eylül=09, Ekim=10, Kasım=11, Aralık=12" ]) elif locale_code == "en": prompt_parts.extend([ f" - 'today' = {date_ctx['today']}", f" - 'tomorrow' = {date_ctx['tomorrow']}", f" - 'day after tomorrow' = {date_ctx['day_after_tomorrow']}", f" - 'this weekend' = {date_ctx['this_weekend_saturday']} or {date_ctx['this_weekend_sunday']}", f" - 'this Saturday' = {date_ctx['this_weekend_saturday']}", f" - 'this Sunday' = {date_ctx['this_weekend_sunday']}", f" - 'next week' = add 7 days to current date", f" - 'next {date_ctx['today_weekday']}' = {date_ctx['next_week_same_day']}", f" - 'in two weeks' = {date_ctx['two_weeks_later']}", f" - 'July 10' or '10th of July' = {date_ctx['today_year']}-07-10", f" - English months: January=01, February=02, March=03, April=04, May=05, June=06, " f"July=07, August=08, September=09, October=10, November=11, December=12" ]) # Add month mappings if available if months: month_mapping = [f"{month}={i+1:02d}" for i, month in enumerate(months)] prompt_parts.append(f" - Month names: {', '.join(month_mapping)}") return "\n".join(prompt_parts) # ───────────────────────────────────────────────────────────────────────────── # INTENT PROMPT # ───────────────────────────────────────────────────────────────────────────── def build_intent_prompt(version: Any, # VersionConfig conversation: List[Dict[str, str]], project_locale: str) -> str: """Build intent detection prompt with enhanced detection prompts and examples""" # Get config cfg = ConfigProvider.get() # Get internal prompt from LLM provider settings internal_prompt = "" if cfg.global_config.llm_provider and cfg.global_config.llm_provider.settings: internal_prompt = cfg.global_config.llm_provider.settings.get("internal_prompt", "") # Extract intent names and captions intent_names = [it.name for it in version.intents] intent_captions = [str(it.caption) if it.caption else it.name for it in version.intents] # Get project language name locale_data = LocaleManager.get_locale(project_locale) project_language = locale_data.get("name", "Turkish") current_language_name = project_language # Replace placeholders in internal prompt if internal_prompt: # Intent names - quoted and comma-separated intent_names_str = ', '.join([f'"{name}"' for name in intent_names]) internal_prompt = internal_prompt.replace("", intent_names_str) # Intent captions - quoted and comma-separated intent_captions_str = ', '.join([f'"{caption}"' for caption in intent_captions]) internal_prompt = internal_prompt.replace("", intent_captions_str) # Project language internal_prompt = internal_prompt.replace("", project_language) internal_prompt = internal_prompt.replace("{{current_language_name}}", current_language_name) internal_prompt = internal_prompt.replace("{{project_language}}", project_language) # === ENHANCED INTENT INDEX WITH DETECTION PROMPTS AND EXAMPLES === lines = ["### INTENT DETECTION RULES ###"] for it in version.intents: lines.append(f"\n{it.name}:") # Add detection prompt if it.detection_prompt: lines.append(f" Detection Rule: {it.detection_prompt}") # Add examples to enhance detection examples = it.get_examples_for_locale(project_locale) if examples: # Combine detection prompt with examples examples_str = " | ".join([f'"{ex}"' for ex in examples[:5]]) # Max 5 examples lines.append(f" Examples that match this intent: {examples_str}") # If we have both detection prompt and examples, make it clear if it.detection_prompt and examples: lines.append(f" → This intent should be detected when user says something similar to the examples above AND matches the detection rule.") intent_index = "\n".join(lines) # === HISTORY === history_block = "\n".join( f"{m['role'].upper()}: {m['content']}" for m in conversation[-10:] ) # Combine prompts combined_prompt = internal_prompt + "\n\n" + version.general_prompt if internal_prompt else version.general_prompt # Get last user message user_input = conversation[-1]['content'] if conversation else "" prompt = ( f"{combined_prompt}\n\n" f"{intent_index}\n\n" f"Conversation so far:\n{history_block}\n\n" f"USER: {user_input.strip()}" ) log_info("✅ Intent prompt built with enhanced detection prompts and examples") return prompt # ───────────────────────────────────────────────────────────────────────────── # PARAMETER PROMPT # ───────────────────────────────────────────────────────────────────────────── _FMT = """#PARAMETERS:{"extracted":[{"name":"","value":""},...],"missing":["",...]}""" def build_parameter_prompt( version: Any, # VersionConfig intent_config: Any, # IntentConfig chat_history: List[Dict[str, str]], collected_params: Dict[str, Any], missing_params: List[str], params_to_ask: List[str], max_params: int, project_locale: str, unanswered_params: List[str] = None ) -> str: """Build parameter collection prompt with approval support""" # Check if we're asking for approval parameter is_approval_question = len(params_to_ask) == 1 and params_to_ask[0] == "is_approved" if is_approval_question: # For approval, use special format without LLM collection prompt # This will be handled in chat_handler with custom approval_question return "" # Return empty, chat_handler will use custom question # Normal parameter collection cfg = ConfigProvider.get() collection_config = cfg.global_config.llm_provider.settings.get("parameter_collection_config", {}) collection_prompt = collection_config.get("collection_prompt", "") if not collection_prompt: # Fallback prompt collection_prompt = "Ask for the missing parameters naturally." # Build conversation history string history_str = "\n".join([ f"{msg['role'].upper()}: {msg['content']}" for msg in chat_history[-5:] # Last 5 messages ]) # Build collected params string collected_str = "\n".join([ f"- {k}: {v}" for k, v in collected_params.items() ]) # Build missing params string with captions missing_str = [] for param_name in missing_params: param = next((p for p in intent_config.parameters if p.name == param_name), None) if param: caption = param.get_caption_for_locale(project_locale) if hasattr(param, 'get_caption_for_locale') else param_name missing_str.append(f"- {param_name} ({caption})") else: missing_str.append(f"- {param_name}") missing_str = "\n".join(missing_str) # Build unanswered params string unanswered_str = "\n".join([f"- {p}" for p in (unanswered_params or [])]) # Replace placeholders prompt = collection_prompt prompt = prompt.replace("{{conversation_history}}", history_str) prompt = prompt.replace("{{intent_name}}", intent_config.name) prompt = prompt.replace("{{intent_caption}}", str(intent_config.caption)) prompt = prompt.replace("{{collected_params}}", collected_str) prompt = prompt.replace("{{missing_params}}", missing_str) prompt = prompt.replace("{{unanswered_params}}", unanswered_str) prompt = prompt.replace("{{max_params}}", str(max_params)) prompt = prompt.replace("{{project_language}}", project_locale) prompt = prompt.replace("{{current_language_name}}", project_locale) # For compatibility return prompt # ───────────────────────────────────────────────────────────────────────────── # SMART PARAMETER COLLECTION PROMPT # ───────────────────────────────────────────────────────────────────────────── def build_smart_parameter_question_prompt(intent_cfg, missing_params: List[str], conversation: List[Dict[str, str]], collection_config: Dict[str, Any], project_language: str) -> str: """Build prompt for smart parameter collection""" # Get the collection prompt template template = collection_config.get("collection_prompt", "Ask for the missing parameters.") # Replace placeholders prompt = template.replace("{{max_params}}", str(collection_config.get("max_params_per_question", 2))) prompt = prompt.replace("{{project_language}}", project_language) # Add parameter information param_info = [] for param_name in missing_params[:collection_config.get("max_params_per_question", 2)]: param = next((p for p in intent_cfg.parameters if p.name == param_name), None) if param: # Get caption for default locale (first supported locale) caption = param.get_caption_for_locale("tr") # Default to Turkish param_info.append(f"- {param_name}: {caption}") # Add context parts = [ prompt, "", "Missing parameters:", "\n".join(param_info), "", "Recent conversation:" ] # Add recent conversation for msg in conversation[-5:]: parts.append(f"{msg['role'].upper()}: {msg['content']}") return "\n".join(parts) # ───────────────────────────────────────────────────────────────────────────── # PARAMETER EXTRACTION FROM QUESTION # ───────────────────────────────────────────────────────────────────────────── def extract_params_from_question(question: str, param_names: List[str]) -> Dict[str, str]: """Extract parameters that might be embedded in the question itself""" extracted = {} # Simple pattern matching for common cases # This is a basic implementation - can be enhanced with NLP # Example: "İstanbul'dan Ankara'ya ne zaman gitmek istiyorsunuz?" # Could extract origin=İstanbul, destination=Ankara # For now, return empty dict # This function can be enhanced based on specific use cases return extracted # ───────────────────────────────────────────────────────────────────────────── # API RESPONSE PROMPT # ───────────────────────────────────────────────────────────────────────────── def build_api_response_prompt(api_config, api_response: Dict) -> str: """Build prompt for API response with mappings""" response_prompt = api_config.response_prompt if not response_prompt: return "İşlem başarıyla tamamlandı." # Apply response mappings if available mapped_data = {} if hasattr(api_config, 'response_mappings'): for mapping in api_config.response_mappings: field_path = mapping.get('field_path', '') display_name = mapping.get('display_name', field_path) # Extract value from response value = _extract_value_from_path(api_response, field_path) if value is not None: mapped_data[display_name] = value # Replace placeholders in response prompt for key, value in mapped_data.items(): response_prompt = response_prompt.replace(f"{{{key}}}", str(value)) # Also try direct field replacement for key, value in api_response.items(): response_prompt = response_prompt.replace(f"{{{key}}}", str(value)) return response_prompt def _extract_value_from_path(data: Dict, path: str) -> Any: """Extract value from nested dict using dot notation""" try: parts = path.split('.') value = data for part in parts: if isinstance(value, dict): value = value.get(part) elif isinstance(value, list) and part.isdigit(): value = value[int(part)] else: return None return value except: return None