import json import logging import os from typing import List, Optional, Tuple from openai import OpenAI import db LOGGER = logging.getLogger(__name__) PERSONALITY_MODEL = "gpt-4-turbo" _CLIENT: Optional[OpenAI] = None def _get_client() -> OpenAI: global _CLIENT if _CLIENT is None: api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise RuntimeError("OPENAI_API_KEY is not set. Please configure the environment.") _CLIENT = OpenAI(api_key=api_key) return _CLIENT def _format_conversation_log(conversation_log: List[str]) -> str: lines = [] for idx, entry in enumerate(conversation_log, start=1): lines.append(f"{idx}. {entry.strip()}") return "\n".join(lines) def _build_personality_prompt(conversation_log: List[str]) -> str: formatted_log = _format_conversation_log(conversation_log) return ( "Analyze the following conversation snippets to characterize the user. " "Focus on their tone, conversational behavior, interests, and emotional patterns. " "Respond with compact JSON containing the keys 'personality_summary' and 'preferences'.\n\n" f"Conversation snippets:\n{formatted_log}" ) def _parse_personality_response(content: str) -> Tuple[str, Optional[str]]: try: payload = json.loads(content) summary = payload.get("personality_summary", "").strip() preferences = payload.get("preferences") if isinstance(preferences, str): preferences = preferences.strip() elif preferences is not None: preferences = json.dumps(preferences, ensure_ascii=False) return summary, preferences except json.JSONDecodeError: return content.strip(), None def update_user_personality(user_id: str, conversation_log: List[str]) -> Optional[str]: """Analyze recent conversations and persist an updated personality profile.""" if not conversation_log: LOGGER.debug("No conversation log provided for user %s; skipping personality update.", user_id) return None prompt = _build_personality_prompt(conversation_log) try: client = _get_client() except RuntimeError: LOGGER.exception("Cannot update personality without OPENAI_API_KEY") return None try: response = client.chat.completions.create( model=PERSONALITY_MODEL, messages=[ { "role": "system", "content": ( "You are a mirror of the user that distills enduring personality insights from user conversations. " "Provide grounded, respectful observations without speculation. Act as a behavioral buffer: when you " "sense unproductive or negative patterns, offer gentle reframes, actionable nudges, and reflective " "questions that support autonomy. Reinforce any progress—no matter how small—while staying humble and " "non-judgmental. Monitor the user's emotional tone and engagement, tailoring the intensity of nudges " "to their receptivity so they never feel overwhelmed. Prioritize encouragement and autonomy, avoid " "manipulation, and regularly recalibrate guidance to prevent reinforcing negative patterns or " "overstepping boundaries." ), }, {"role": "user", "content": prompt}, ], temperature=0.6, ) except Exception as exc: # pragma: no cover LOGGER.exception("OpenAI personality update failed for user %s: %s", user_id, exc) return None try: content = response.choices[0].message.content.strip() except (AttributeError, IndexError): # pragma: no cover LOGGER.error("Malformed OpenAI response when updating personality for user %s", user_id) return None summary, preferences = _parse_personality_response(content) if not summary: LOGGER.warning("Received empty personality summary for user %s", user_id) return None try: db.update_user_personality(user_id, summary, preferences=preferences) except Exception as exc: # pragma: no cover LOGGER.exception("Failed to persist personality summary for user %s: %s", user_id, exc) return None LOGGER.info("Updated personality profile for user %s", user_id) return summary