# agents/coordinator.py import logging import json import re from typing import Dict, Any, List from agents.nebius_simple import create_nebius_llm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class CoordinatorAgent: def __init__(self, nebius_api_key: str): self.llm = create_nebius_llm( api_key=nebius_api_key, model="meta-llama/Llama-3.3-70B-Instruct-fast", temperature=0.6 # Чуть выше для креативности при генерации историй ) def _extract_json(self, text: str) -> dict: """ Извлекает JSON из ответа LLM с улучшенной обработкой. Поддерживает различные форматы ответов. """ # 1. Убираем markdown code blocks text = re.sub(r"```json\s*", "", text) text = re.sub(r"```\s*", "", text) # 2. Ищем JSON объект с помощью regex json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' matches = re.findall(json_pattern, text, re.DOTALL) if matches: # Берём первый найденный JSON объект json_str = matches[0].strip() try: return json.loads(json_str) except json.JSONDecodeError as e: logger.warning(f"Failed to parse extracted JSON: {e}") logger.debug(f"JSON string was: {json_str}") # 3. Fallback: пытаемся найти первые { и последние } try: start = text.index('{') end = text.rindex('}') + 1 json_str = text[start:end].strip() return json.loads(json_str) except (ValueError, json.JSONDecodeError) as e: logger.error(f"Could not extract JSON from response: {e}") logger.debug(f"Response was: {text}") raise def analyze_input(self, user_text: str, attempt_count: int) -> Dict[str, Any]: """ Анализ ввода пользователя. Проверяет длину и является ли текст историей. """ # 1. Проверка длины (менее 50 слов) word_count = len(user_text.split()) if word_count < 50: return { "status": "insufficient", "reason": "length", "message": f"Your story is too short ({word_count} words). Please describe the plot in at " f"least 50 words so I can find the best match." } # 2. Проверка: это история или просто набор слов/вопрос? check_prompt = f""" Analyze if the following text is a narrative story/plot description or just random words/meta-talk. Text: "{user_text}" You MUST respond with ONLY valid JSON, nothing else. No explanations before or after. Format: {{"is_story": true/false, "reason": "brief explanation"}} """ response = "" try: response = self.llm.complete(check_prompt).text logger.debug(f"Story check response: {response[:200]}...") # Очистка JSON # cleaned_json = re.sub(r"```json|```", "", response).strip() # analysis = json.loads(cleaned_json) # ✅ Используем улучшенный парсинг analysis = self._extract_json(response) if not analysis.get("is_story", False): return { "status": "insufficient", "reason": "not_story", "message": "This doesn't look like a story. Please describe " "a sequence of events, characters, and what happens to them." } except Exception as e: logger.error(f"Coordinator story check error: {e}") logger.debug(f"Full response: {response if 'response' in locals() else 'N/A'}") # Fallback: пропускаем, если не удалось проверить pass # Если все ок return {"status": "valid"} def generate_suggestion(self, previous_inputs: List[str], genre: str) -> Dict[str, str]: """ Генерирует историю за пользователя, если он не справляется. ✅ С валидацией длины (минимум 50 слов) """ # context = " ".join(previous_inputs) context = " ".join(previous_inputs[-3:]) if previous_inputs else "nothing specific" prompt = f""" The user is trying to use a Movie Plot Search engine but fails to provide a good description. Based on their fragmented inputs: "{context}" (or generate something new if inputs are empty), write a compelling, detailed movie plot summary in the {genre} genre. CRITICAL REQUIREMENTS: - MINIMUM 60 words (aim for 70-90 words for a complete plot) - Must include: main character(s), conflict, setting, stakes - Clear narrative arc with beginning, middle, and potential resolution - Engaging and specific details - English language only Genre: {genre} Output ONLY the story text (60-90 words), no preamble or explanation: """ story_text = self.llm.complete(prompt).text.strip() # ✅ ВАЛИДАЦИЯ ДЛИНЫ сгенерированного текста word_count = len(story_text.split()) if word_count < 50: logger.warning(f"Generated {genre} plot too short ({word_count} words), expanding...") # Повторная генерация с явным требованием расширения expansion_prompt = f""" The following {genre} plot is TOO SHORT ({word_count} words). Expand it to AT LEAST 60 words while keeping the same theme and characters. Add more specific details about the conflict, character motivations, and stakes. Current plot: {story_text} Expanded version (60-90 words): """ expansion_response = self.llm.complete(expansion_prompt) story_text = expansion_response.text.strip() # Проверка после расширения final_word_count = len(story_text.split()) logger.info(f"Expanded plot to {final_word_count} words") else: logger.info(f"Generated {genre} plot has {word_count} words (valid)") # Сообщения в зависимости от жанра if genre == "romantic": msg = ("I see you're having trouble. " " How about we search for a movie with a Romantic plot based on what you said?") elif genre == "humorous": msg = "Okay, maybe a Humorous story would be better?" else: msg = f"Let me suggest a {genre} plot for you." return { "status": "suggestion", "genre": genre, "message": msg, "suggested_story": story_text }