-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnotifier.py
More file actions
341 lines (309 loc) · 14.5 KB
/
Copy pathnotifier.py
File metadata and controls
341 lines (309 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""
Azurro Telegram notifier: send alerts and optional Sentry (Gemini) for traffic-light colours.
"""
from __future__ import annotations
import json
import os
from dotenv import load_dotenv
_script_dir = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(_script_dir, ".env"))
TELEGRAM_BOT_TOKEN = (os.getenv("TELEGRAM_BOT_TOKEN") or os.getenv("AZURO_TELEGRAM_BOT_TOKEN") or "").strip()
TELEGRAM_CHAT_ID = (os.getenv("TELEGRAM_CHAT_ID") or os.getenv("AZURO_TELEGRAM_CHAT_ID") or "").strip()
GEMINI_API_KEY = (os.getenv("GEMINI_API_KEY") or "").strip()
AZURRO_AUTO_ADAPT = (os.getenv("AZURRO_AUTO_ADAPT") or "false").strip().lower() == "true"
def _escape_html(text: str) -> str:
if not text:
return ""
return str(text).replace("&", "&").replace("<", "<").replace(">", ">")
def _bold_html(text: str) -> str:
return "<b>" + _escape_html(str(text)) + "</b>"
def send_simple_message(text: str) -> None:
"""Send one HTML message to TELEGRAM_CHAT_ID."""
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
return
try:
import telebot
bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN)
bot.send_message(TELEGRAM_CHAT_ID, text, parse_mode="HTML")
except Exception:
pass
def send_azurro_alert(
window_name: str,
entries: list[dict],
unit_dollars: float = 0.0,
snap_id: int | None = None,
colours: list[str] | None = None,
) -> tuple[int | None, int | None]:
"""
Send Azurro snap alert to Telegram. entries have name, home, away, score, target_line, odds, league.
colours[i] = RED|YELLOW|GREEN for entries[i]. Returns (message_id, chat_id) for primary recipient.
"""
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
return (None, None)
try:
import telebot
from telebot import types
except ImportError:
return (None, None)
bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN)
chat_id = int(TELEGRAM_CHAT_ID)
lines = [_bold_html(window_name)]
if snap_id is not None:
lines.append(_escape_html(f"Snap #{snap_id}"))
lines.append("🔵 AZURRO SNAP")
lines.append("")
for i, e in enumerate(entries):
home = e.get("home") or "?"
away = e.get("away") or "?"
score = e.get("score", "? - ?")
target = e.get("target_line") or "?"
odds_val = e.get("odds")
league = e.get("league") or ""
colour = (colours or [])[i] if colours and i < len(colours) else "GREEN"
line1 = "⚽ " + _bold_html(home) + " vs " + _bold_html(away) + " (" + _escape_html(score) + ")"
if league:
line1 += "\n " + _escape_html(league)
line2 = "🎯 " + _escape_html(target)
if odds_val is not None and odds_val > 0:
line2 += " | Odds: " + _escape_html(f"{odds_val:.2f}")
line2 += " | " + _escape_html(f"[{colour}]")
lines.append(line1)
lines.append(line2)
shots = e.get("total_shots", "")
corners = e.get("total_corners", "")
fouls = e.get("fouls", "")
if shots != "" or corners != "" or fouls != "":
lines.append("📊 Shots: " + _escape_html(str(shots)) + " | Corners: " + _escape_html(str(corners)) + " | Fouls: " + _escape_html(str(fouls)))
preds = e.get("predictions_text")
if preds:
lines.append("📈 Predictions: " + _escape_html(str(preds)))
lines.append("")
if unit_dollars > 0:
lines.append("Unit: " + _escape_html(f"${unit_dollars:.2f}"))
text = "\n".join(lines).strip()
try:
msg = bot.send_message(chat_id, text, parse_mode="HTML")
return (msg.message_id, chat_id)
except Exception:
return (None, None)
def ask_gemini_sentry(
entries: list[dict],
events_by_fixture: dict[int, list[str]] | None = None,
) -> tuple[list[str], str]:
"""
Ask Gemini for one colour per entry (RED/YELLOW/GREEN) and one short narrative.
events_by_fixture optional (Azurro may have no fixture_id); use empty dict.
"""
if not GEMINI_API_KEY or not entries:
return (["GREEN"] * len(entries) if entries else [], "Sentry unavailable.")
# For now we only support single-entry snaps; extend later if needed.
e = entries[0]
events_by_fixture = events_by_fixture or {}
try:
import google.generativeai as genai
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel("gemini-2.5-flash")
from memory_retrieval import get_relevant_memories_for_entry
window_name = e.get("window_name") or e.get("window") or ""
memory_bundle = get_relevant_memories_for_entry(e, window_name=window_name or None)
episodes = memory_bundle.get("episodes") or []
lessons = memory_bundle.get("lessons") or []
# Build context strings for history / lessons
history_lines: list[str] = []
for ep in episodes[:8]:
feats = ep.get("features") or {}
res = feats.get("result") or ""
history_lines.append(
f"- [{ep.get('kind')}] {ep.get('league') or ''} {ep.get('window_name') or ''} "
f"score={feats.get('score')} line={feats.get('target_line')} "
f"shots={feats.get('shots')} corners={feats.get('corners')} fouls={feats.get('fouls')} "
f"colour={feats.get('colour')} placed={feats.get('placed')} result={res}"
)
lesson_lines: list[str] = []
for l in lessons[:6]:
lesson_lines.append(f"- {l.get('text')}")
name = e.get("name") or "?"
score = e.get("score", "? - ?")
target = e.get("target_line") or "?"
league = e.get("league") or ""
shots = e.get("total_shots", "?")
corners = e.get("total_corners", "?")
fouls = e.get("fouls", "?")
preds = e.get("predictions_text") or ""
fid = e.get("fixture_id")
evts = events_by_fixture.get(fid, []) if fid is not None else []
evt_str = "; ".join(evts) if evts else "None"
base_match_desc = (
f"Match: {name} ({league}) | Window: {window_name or 'N/A'} | "
f"Score: {score} | Line: {target} | Shots={shots}, Corners={corners}, Fouls={fouls} | "
f"Predictions: {preds or 'None'} | Events: {evt_str}"
)
# === Analyst pass ===
analyst_prompt = (
"You are Azurro's Sentry, an investor-minded analyst deciding whether an UNDER bet "
"on this live football match is GREEN (good), YELLOW (caution), or RED (avoid).\n\n"
"Mindset:\n"
"- Your goal is to grow the bankroll steadily over many bets while avoiding ruin.\n"
"- It is acceptable to miss some winners; it is not acceptable to blow up the bankroll.\n"
"- First-half scans often use lines like Under X+1.5 only for the first half; treat these as separate from full-time Unders.\n\n"
"Colour rules:\n"
"- GREEN only when pressure is clearly low AND history/lessons support this spot.\n"
"- YELLOW when there is some edge but meaningful risk or conflicting signals.\n"
"- RED when stats are ugly, pace is high, or history/lessons warn strongly against it.\n\n"
f"Current candidate:\n{base_match_desc}\n\n"
"Relevant past episodes (bets/skips):\n"
+ ("\n".join(history_lines) if history_lines else "(none)")
+ "\n\nKey lessons and notes:\n"
+ ("\n".join(lesson_lines) if lesson_lines else "(none)")
+ "\n\n"
"Respond with exactly two lines:\n"
"Line 1: one word - GREEN, YELLOW, or RED.\n"
"Line 2: one short sentence explaining your choice (max 200 chars).\n"
)
analyst_resp = model.generate_content(analyst_prompt)
analyst_text = (analyst_resp.text or "").strip() if analyst_resp else ""
a_parts = analyst_text.splitlines()
analyst_colour = "YELLOW"
analyst_reason = "No comment."
if a_parts:
first = a_parts[0].strip().upper()
if first in ("GREEN", "YELLOW", "RED"):
analyst_colour = first
else:
toks = first.split()
for t in toks:
if t.upper() in ("GREEN", "YELLOW", "RED"):
analyst_colour = t.upper()
break
if len(a_parts) > 1 and a_parts[1].strip():
analyst_reason = a_parts[1].strip()[:200]
elif a_parts and not analyst_reason:
analyst_reason = a_parts[0].strip()[:200]
# === Skeptic pass (risk annotator) ===
skeptic_prompt = (
"You are Azurro's Skeptic. Your job is NOT to pick a new colour, but to identify "
"hidden risks the Analyst might be underweighting.\n\n"
f"Analyst colour: {analyst_colour}\n"
f"Analyst reasoning: {analyst_reason}\n\n"
f"Current candidate:\n{base_match_desc}\n\n"
"Relevant past episodes:\n"
+ ("\n".join(history_lines) if history_lines else "(none)")
+ "\n\nKey lessons:\n"
+ ("\n".join(lesson_lines) if lesson_lines else "(none)")
+ "\n\n"
"Reply as strict JSON on a single line with this schema:\n"
'{\"risk_flag\": \"NONE\" | \"MEDIUM\" | \"CRITICAL\", '
'\"notes\": \"short explanation (max 200 chars)\"}.\n'
)
skeptic_resp = model.generate_content(skeptic_prompt)
skeptic_text = (skeptic_resp.text or "").strip() if skeptic_resp else ""
risk_flag = "NONE"
risk_notes = ""
if skeptic_text:
try:
parsed = json.loads(skeptic_text)
rf = str(parsed.get("risk_flag") or "").upper()
if rf in ("NONE", "MEDIUM", "CRITICAL"):
risk_flag = rf
risk_notes = str(parsed.get("notes") or "").strip()[:200]
except Exception:
pass
# === Optional downgrading logic ===
final_colour = analyst_colour
if risk_flag == "MEDIUM":
if analyst_colour == "GREEN":
final_colour = "YELLOW"
elif risk_flag == "CRITICAL":
final_colour = "RED"
# Attach for downstream logging/memory if caller wants
e["analyst_colour"] = analyst_colour
e["analyst_reason"] = analyst_reason
e["risk_flag"] = risk_flag
e["risk_notes"] = risk_notes
e["final_colour"] = final_colour
combined_narrative_parts = [analyst_reason]
if risk_flag != "NONE":
combined_narrative_parts.append(f"Risk={risk_flag}: {risk_notes or 'additional risks flagged.'}")
combined_narrative = " ".join(p for p in combined_narrative_parts if p)
return ([final_colour], combined_narrative or "No comment.")
except Exception:
return (["YELLOW"] * len(entries) if entries else [], "Sentry unavailable.")
def ask_gemini_two_odds_sentry(
slip: dict,
) -> tuple[str, str]:
"""
Dedicated Sentry for brother's two-odds-a-day slips.
Input `slip` is a lightweight dict that may contain:
- description: free-text description of the slip
- stake: stake size
- total_odds: combined odds
- legs: optional list of { fixture_hint, league_hint, market, line, odds }
Returns (colour, narrative).
"""
if not GEMINI_API_KEY:
return ("YELLOW", "Two-odds Sentry unavailable.")
desc = str(slip.get("description") or "").strip()
stake = slip.get("stake")
total_odds = slip.get("total_odds")
legs = slip.get("legs") or []
lines: list[str] = []
if desc:
lines.append(f"Slip description: {desc}")
if stake is not None:
lines.append(f"Stake: {stake}")
if total_odds is not None:
lines.append(f"Total odds: {total_odds}")
if legs:
lines.append("Legs:")
for i, leg in enumerate(legs, start=1):
lh = leg.get("league_hint") or ""
lines.append(
f"- Leg {i}: {leg.get('fixture_hint') or '?'} ({lh}) "
f"market={leg.get('market') or '?'} line={leg.get('line')} "
f"odds={leg.get('odds')}"
)
slip_summary = "\n".join(lines) if lines else "No structured slip details provided."
try:
import google.generativeai as genai
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel("gemini-2.5-flash")
prompt = (
"You are Azurro's Two-Odds Sentry. Your job is to evaluate a daily ~2.0 odds slip from the "
"perspective of a calm, long-term investor who wants steady bankroll growth with regular withdrawals, "
"not lottery-style swings.\n\n"
"Mindset:\n"
"- Maximize gains over many days while keeping downside tightly controlled.\n"
"- Protect the 2-odds bankroll first; it is acceptable to skip marginal slips.\n"
"- Remember that the user often withdraws a portion of gains after good runs.\n\n"
"Colour meanings:\n"
"- GREEN: slip is sensibly constructed with reasonable edge and no obvious structural flaws.\n"
"- YELLOW: slip may be playable but has noticeable concentration, fragile legs, or unclear edge.\n"
"- RED: slip is structurally poor (e.g. highly correlated fragile legs, unrealistic expectations, or patterns that have lost historically).\n\n"
"Consider: correlation between legs, realism of total odds, how many things must go right, and whether this fits a safe 2-odds bankroll plan.\n\n"
f"Current slip:\n{slip_summary}\n\n"
"Respond with exactly two lines:\n"
"Line 1: one word - GREEN, YELLOW, or RED.\n"
"Line 2: one short sentence explaining your choice (max 200 chars).\n"
)
resp = model.generate_content(prompt)
text = (resp.text or "").strip() if resp else ""
parts = text.splitlines()
colour = "YELLOW"
reason = "No comment."
if parts:
first = parts[0].strip().upper()
if first in ("GREEN", "YELLOW", "RED"):
colour = first
else:
toks = first.split()
for t in toks:
if t.upper() in ("GREEN", "YELLOW", "RED"):
colour = t.upper()
break
if len(parts) > 1 and parts[1].strip():
reason = parts[1].strip()[:200]
elif parts and not reason:
reason = parts[0].strip()[:200]
return (colour, reason)
except Exception:
return ("YELLOW", "Two-odds Sentry unavailable.")