cinematch-ai / agents /coordinator.py
dbadeev's picture
Upload 25 files
16ce932 verified
# 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
}