hrodic commited on
Commit
050c845
·
verified ·
1 Parent(s): 8c5c24b

Create markdown_movie_release_sorter.py

Browse files
tools/markdown_movie_release_sorter.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from datetime import datetime, date
3
+ from typing import Any, Dict, List, Optional
4
+ from smolagents.tools import Tool
5
+
6
+ class MarkdownMovieReleaseSorterTool(Tool):
7
+ name = "markdown_movie_release_sorter"
8
+ description = (
9
+ "Extracts, validates, deduplicates, and chronologically sorts movie release dates "
10
+ "from raw markdown text or bullet points. Supports both English and Spanish date "
11
+ "formats, discards past dates relative to today, and outputs a countdown list."
12
+ )
13
+ inputs = {
14
+ "markdown_text": {
15
+ "type": "string",
16
+ "description": (
17
+ "Raw markdown containing movie titles and release dates (bullets, lines, or table rows). "
18
+ "Examples: '- 28 Years Later: June 20, 2026', 'Dune 3 | 2026-12-18 | Sci-fi', "
19
+ "'* Nosferatu - 25 de diciembre de 2026'."
20
+ ),
21
+ }
22
+ }
23
+ output_type = "string"
24
+
25
+ # Multilingual month mapping (English & Spanish)
26
+ MONTH_MAP = {
27
+ # English
28
+ "january": 1, "february": 2, "march": 3, "april": 4, "may": 5, "june": 6,
29
+ "july": 7, "august": 8, "september": 9, "october": 10, "november": 11, "december": 12,
30
+ "jan": 1, "feb": 2, "mar": 3, "apr": 4, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "sept": 9, "oct": 10, "nov": 11, "dec": 12,
31
+ # Spanish
32
+ "enero": 1, "febrero": 2, "marzo": 3, "abril": 4, "mayo": 5, "junio": 6,
33
+ "julio": 7, "agosto": 8, "septiembre": 9, "setiembre": 9, "octubre": 10, "noviembre": 11, "diciembre": 12,
34
+ "ene": 1, "abr": 4, "ago": 8, "dic": 12
35
+ }
36
+
37
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
38
+ super().__init__(*args, **kwargs)
39
+ self.is_initialized = False
40
+
41
+ def _parse_date(self, text: str) -> Optional[date]:
42
+ """Tries various date patterns and returns a datetime.date object if found."""
43
+ # 1. ISO format: YYYY-MM-DD
44
+ m_iso = re.search(r"\b(20\d{2})[-/.](0[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01])\b", text)
45
+ if m_iso:
46
+ y, m, d = int(m_iso.group(1)), int(m_iso.group(2)), int(m_iso.group(3))
47
+ try:
48
+ return date(y, m, d)
49
+ except ValueError:
50
+ pass
51
+
52
+ # 2. European format: DD/MM/YYYY or DD-MM-YYYY
53
+ m_eu = re.search(r"\b(0[1-9]|[12]\d|3[01])[-/.](0[1-9]|1[0-2])[-/.](20\d{2})\b", text)
54
+ if m_eu:
55
+ d, m, y = int(m_eu.group(1)), int(m_eu.group(2)), int(m_eu.group(3))
56
+ try:
57
+ return date(y, m, d)
58
+ except ValueError:
59
+ pass
60
+
61
+ # 3. Text Month Formats: "October 24, 2026", "24 October 2026", or Spanish "24 de octubre de 2026"
62
+ month_names = "|".join(self.MONTH_MAP.keys())
63
+
64
+ # Pattern A: Day Month Year (e.g., "24 October 2026" or "24 de octubre, 2026")
65
+ p_dmy = rf"\b(\d{{1,2}})(?:\s+de)?\s+({month_names})\b(?:\s+de)?,?\s+(20\d{{2}})\b"
66
+ m_dmy = re.search(p_dmy, text, re.IGNORECASE)
67
+ if m_dmy:
68
+ day = int(m_dmy.group(1))
69
+ month = self.MONTH_MAP[m_dmy.group(2).lower()]
70
+ year = int(m_dmy.group(3))
71
+ try:
72
+ return date(year, month, day)
73
+ except ValueError:
74
+ pass
75
+
76
+ # Pattern B: Month Day Year (e.g., "October 24, 2026")
77
+ p_mdy = rf"\b({month_names})\s+(\d{{1,2}}),?\s+(20\d{{2}})\b"
78
+ m_mdy = re.search(p_mdy, text, re.IGNORECASE)
79
+ if m_mdy:
80
+ month = self.MONTH_MAP[m_mdy.group(1).lower()]
81
+ day = int(m_mdy.group(2))
82
+ year = int(m_mdy.group(3))
83
+ try:
84
+ return date(year, month, day)
85
+ except ValueError:
86
+ pass
87
+
88
+ return None
89
+
90
+ def forward(self, markdown_text: str) -> str:
91
+ today = datetime.now().date()
92
+ parsed_movies: Dict[str, Dict[str, Any]] = {}
93
+
94
+ for line in markdown_text.splitlines():
95
+ line = line.strip()
96
+ # Ignore markdown table separators or empty lines
97
+ if not line or set(line) <= {"|", "-", " ", ":"}:
98
+ continue
99
+
100
+ parsed_dt = self._parse_date(line)
101
+ if not parsed_dt:
102
+ continue
103
+
104
+ # Calculate days until release
105
+ days_until = (parsed_dt - today).days
106
+ if days_until < 0:
107
+ continue # Discard past releases
108
+
109
+ # Clean line markers: leading bullets, numbered items, table pipes
110
+ clean_line = re.sub(r"^[\s*\-•#\d.]+|[|]+$", "", line).strip()
111
+ # Normalize title key to prevent duplicates
112
+ title_key = re.sub(r"[^a-zA-Z0-9]", "", clean_line.lower()[:30])
113
+
114
+ if title_key not in parsed_movies:
115
+ parsed_movies[title_key] = {
116
+ "text": clean_line,
117
+ "date": parsed_dt,
118
+ "days_until": days_until,
119
+ }
120
+
121
+ if not parsed_movies:
122
+ return (
123
+ f"No upcoming releases detected after today ({today.strftime('%Y-%m-%d')}). "
124
+ "Ensure dates are formatted with valid upcoming day, month, and year."
125
+ )
126
+
127
+ # Sort chronologically by date ascending
128
+ sorted_list = sorted(parsed_movies.values(), key=lambda x: x["date"])
129
+
130
+ output = [f"### Confirmed Upcoming Releases (Chronological from Today: {today.strftime('%Y-%m-%d')}):\n"]
131
+ for idx, item in enumerate(sorted_list, 1):
132
+ if item["days_until"] == 0:
133
+ countdown = "TODAY"
134
+ elif item["days_until"] == 1:
135
+ countdown = "Tomorrow"
136
+ else:
137
+ countdown = f"in {item['days_until']} days"
138
+
139
+ output.append(f"{idx}. [{item['date'].strftime('%Y-%m-%d')}] {item['text']} — **({countdown})**")
140
+
141
+ return "\n".join(output)