Spaces:
Sleeping
Sleeping
| import logging | |
| import json | |
| from pathlib import Path | |
| from typing import Tuple, Dict, Any, Optional | |
| from insucompass.services import llm_provider | |
| from insucompass.config import settings | |
| from insucompass.prompts.prompt_loader import load_prompt | |
| # Configure logging | |
| logging.basicConfig(level=settings.LOG_LEVEL, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| llm = llm_provider.get_gemini_llm() | |
| class ProfileBuilder: | |
| """ | |
| A service class that manages the entire conversational profile building process. | |
| It determines the next question and intelligently updates the profile with user answers. | |
| """ | |
| def __init__(self): | |
| """Initializes the ProfileBuilder, loading all necessary prompts.""" | |
| try: | |
| self.question_prompt = load_prompt("profile_agent") | |
| self.updater_prompt = load_prompt("profile_updater") | |
| logger.info("ProfileBuilder initialized successfully with all prompts.") | |
| except FileNotFoundError as e: | |
| logger.critical(f"A required prompt file was not found: {e}. ProfileBuilder cannot function.") | |
| raise | |
| def get_next_question(self, current_profile: Dict[str, Any], conversation_history: Dict[str, Any]) -> str: | |
| """ | |
| Analyzes the user's current profile and determines the next question to ask, | |
| or signals that the profile is complete. | |
| Args: | |
| current_profile: A dictionary representing the user's profile data. | |
| Returns: | |
| A string containing the next question for the user, or "PROFILE_COMPLETE". | |
| """ | |
| logger.debug(f"Determining next question for profile: {current_profile}") | |
| profile_json_str = json.dumps(current_profile, indent=2) | |
| conversation_history_str = json.dumps(conversation_history, indent=2) | |
| full_prompt = f"{self.question_prompt}\n\n### User Profile\n{profile_json_str} \n\n ### Conversation History{conversation_history_str}" | |
| try: | |
| response = llm.invoke(full_prompt) | |
| next_step = response.content.strip() | |
| logger.info(f"LLM returned next step: '{next_step}'") | |
| return next_step | |
| except Exception as e: | |
| logger.error(f"LLM error during question generation: {e}") | |
| return "I'm sorry, I'm having a little trouble right now. Could we try that again?" | |
| def update_profile_with_answer( | |
| self, | |
| current_profile: Dict[str, Any], | |
| last_question: str, | |
| user_answer: str | |
| ) -> Dict[str, Any]: | |
| """ | |
| Uses an LLM to intelligently update the user's profile with their latest answer. | |
| Args: | |
| current_profile: The user's profile before the update. | |
| last_question: The question the user is answering. | |
| user_answer: The user's free-text answer. | |
| Returns: | |
| The updated profile dictionary. | |
| """ | |
| logger.debug(f"Updating profile with answer: '{user_answer}' for question: '{last_question}'") | |
| profile_json_str = json.dumps(current_profile, indent=2) | |
| # Construct the prompt for the updater LLM call | |
| full_prompt = ( | |
| f"{self.updater_prompt}\n\n" | |
| f"current_profile: {profile_json_str}\n\n" | |
| f"last_question_asked: \"{last_question}\"\n\n" | |
| f"user_answer: \"{user_answer}\"" | |
| ) | |
| try: | |
| response = llm.invoke(full_prompt) | |
| response_content = response.content.strip() | |
| # Clean the response to ensure it's valid JSON | |
| # The LLM can sometimes wrap the JSON in markdown | |
| if response_content.startswith("```json"): | |
| response_content = response_content[7:-3].strip() | |
| updated_profile = json.loads(response_content) | |
| logger.info("Successfully updated profile with user's answer.") | |
| return updated_profile | |
| except json.JSONDecodeError as e: | |
| logger.error(f"Failed to decode JSON from LLM response: {e}\nResponse was: {response.content}") | |
| # Return the original profile to avoid data corruption | |
| return current_profile | |
| except Exception as e: | |
| logger.error(f"LLM error during profile update: {e}") | |
| # Return the original profile on error | |
| return current_profile | |
| def run_conversation_turn( | |
| self, | |
| current_profile: Dict[str, Any], | |
| last_user_answer: str = None | |
| ) -> Tuple[Dict[str, Any], str]: | |
| """ | |
| Executes a full turn of the profile-building conversation. | |
| Args: | |
| current_profile: The current state of the user's profile. | |
| last_user_answer: The user's answer from the previous turn, if any. | |
| Returns: | |
| A tuple containing: | |
| - The updated profile dictionary. | |
| - The next question to ask the user (or "PROFILE_COMPLETE"). | |
| """ | |
| # First, determine the question that *would have been asked* for the current state | |
| # This is the question the user just answered. | |
| question_that_was_asked = self.get_next_question(current_profile) | |
| profile_after_update = current_profile | |
| # If there was an answer from the user, update the profile | |
| if last_user_answer and question_that_was_asked != "PROFILE_COMPLETE": | |
| profile_after_update = self.update_profile_with_answer( | |
| current_profile=current_profile, | |
| last_question=question_that_was_asked, | |
| user_answer=last_user_answer | |
| ) | |
| # Now, with the potentially updated profile, get the *next* question | |
| next_question_to_ask = self.get_next_question(profile_after_update) | |
| return profile_after_update, next_question_to_ask | |
| profile_builder = ProfileBuilder() |