# hermes_agent.py # Decision engine with hybrid model routing (free models only), # consensus analysis, health monitoring, performance‑based threshold adjustment, # and daily self‑reflection. # L3 uses a dual‑collaborator + arbiter pattern. # L2 fallback model updated; safety wrapper added for malformed responses. # All comments in English. import os, sys, time, json, hashlib, traceback, glob, re, requests from datetime import datetime, timezone, timedelta from llm_wiki import save_entry, query_context, semantic_search from openai import OpenAI # ── File paths ────────────────────────────────────────────── SIGNAL_FILE = "/app/latest_signals.json" STATUS_FILE = "/app/hermes_status.txt" ERROR_COUNT_FILE = "/app/llm_error_count.txt" LAST_PROCESSED_FILE = "/app/last_processed_signal.json" TASK_QUEUE_FILE = "/app/task_queue.json" FOLLOW_THRESHOLD_FILE = "/app/follow_threshold.txt" HALT_FILE = "/app/halt_trading.txt" UNHALT_FILE = "/app/unhalt_trading.txt" PENDING_ALERTS_FILE = "/app/pending_alerts.json" HEALTH_LAST_ALERT_FILE = "/app/last_health_alert_level.txt" # ── OpenRouter configuration ──────────────────────────────── OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY") if not OPENROUTER_API_KEY: raise EnvironmentError("OPENROUTER_API_KEY must be set in Secrets.") client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key=OPENROUTER_API_KEY ) # ── Hybrid model routing (free models only) ───────────────── FAST_SCAN_PRIMARY = "google/gemma-4-26b-a4b:free" FAST_SCAN_FALLBACK = "google/gemma-4-31b-it-20260402:free" L2_PRIMARY = "nvidia/nemotron-3-super-120b-a12b:free" L2_FALLBACK = "mistralai/mistral-nemo:free" # replaced dead model # ── L3 dual‑collaborator + arbiter (free tier) ───────────── L3_COLLABORATOR_A = "google/gemini-2.5-flash-lite:free" # 1M context, reliable L3_COLLABORATOR_B = "openai/gpt-4o-mini:free" # strong reasoning L3_ARBITER = "meta-llama/llama-4-maverick:free" # third opinion FAST_SCAN_ROUTING = [FAST_SCAN_PRIMARY, FAST_SCAN_FALLBACK] REALTIME_ROUTING = [L2_PRIMARY, L2_FALLBACK] DEEP_REVIEW_COLLABORATORS = [L3_COLLABORATOR_A, L3_COLLABORATOR_B] DEEP_REVIEW_ARBITER = L3_ARBITER # ── Helper functions ───────────────────────────────────────── def read_file(path, default=""): try: if os.path.exists(path): with open(path, "r") as f: return f.read().strip() except: pass return default def write_file(path, content): try: with open(path, "w") as f: f.write(str(content)) except: pass def write_status(msg): try: with open(STATUS_FILE, "w") as f: f.write(msg + "\n") except: pass def read_error_count(): try: if os.path.exists(ERROR_COUNT_FILE): with open(ERROR_COUNT_FILE, "r") as f: return int(f.read().strip()) except: pass return 0 def increment_error_count(error_type="unknown", detail=""): count = read_error_count() + 1 try: with open(ERROR_COUNT_FILE, "w") as f: f.write(str(count)) except: pass try: with open("/app/llm_error_types.txt", "a") as f: f.write(f"[{datetime.now(timezone.utc).isoformat()}] {error_type}\n") except: pass if detail: try: with open("/app/llm_error_details.txt", "a") as f: f.write(f"[{datetime.now(timezone.utc).isoformat()}] {error_type}: {detail[:100]}\n") except: pass def reset_error_count(): try: with open(ERROR_COUNT_FILE, "w") as f: f.write("0") with open("/app/llm_error_types.txt", "w") as f: f.write("") with open("/app/llm_error_details.txt", "w") as f: f.write("") except: pass def read_signals(): try: if os.path.exists(SIGNAL_FILE): with open(SIGNAL_FILE, "r") as f: return json.load(f) except: pass return [] def clear_signals(): try: with open(SIGNAL_FILE, "w") as f: json.dump([], f) except: pass def get_follow_threshold(): try: if os.path.exists(FOLLOW_THRESHOLD_FILE): with open(FOLLOW_THRESHOLD_FILE, "r") as f: return float(f.read().strip()) except: pass return 0.6 def is_halted(): try: if os.path.exists(HALT_FILE): return True if os.path.exists("/app/capital_state.json"): with open("/app/capital_state.json", "r") as f: state = json.load(f) if state.get("halted", False): return True except: pass return False # ── Telegram alert helpers ─────────────────────────────────── def send_telegram_alert(text: str): token = os.getenv("TELEGRAM_BOT_TOKEN") chat_id = os.getenv("TELEGRAM_CHAT_ID") if not token or not chat_id: return False url = f"https://api.telegram.org/bot{token}/sendMessage" try: resp = requests.post(url, json={"chat_id": chat_id, "text": text}, timeout=10) return resp.status_code == 200 except: return False def add_pending_alert(alert_text): alerts = [] try: if os.path.exists(PENDING_ALERTS_FILE): with open(PENDING_ALERTS_FILE, "r") as f: alerts = json.load(f) except: pass alerts.append({"text": alert_text, "timestamp": time.time()}) try: with open(PENDING_ALERTS_FILE, "w") as f: json.dump(alerts, f) except: pass def flush_pending_alerts(): if not os.path.exists(PENDING_ALERTS_FILE): return try: with open(PENDING_ALERTS_FILE, "r") as f: alerts = json.load(f) except: return remaining = [] for alert in alerts: if time.time() - alert["timestamp"] > 86400: continue if not send_telegram_alert(alert["text"]): remaining.append(alert) try: if remaining: with open(PENDING_ALERTS_FILE, "w") as f: json.dump(remaining, f) else: if os.path.exists(PENDING_ALERTS_FILE): os.remove(PENDING_ALERTS_FILE) except: pass # ── Market sentiment overlay ──────────────────────────────── def get_market_sentiment(): try: resp = requests.get("https://data-api.polymarket.com/markets?limit=50", timeout=10) if resp.status_code == 200: data = resp.json() if isinstance(data, list) and data: total = len(data) bullish = sum(1 for m in data if float(m.get("outcomePrices", [0, 0])[0]) > 0.7) sentiment = (bullish / total - 0.5) * 2 return round(sentiment, 2) except: pass return 0.0 # ── LLM call with sequential model routing ────────────────── def call_llm_with_routing(prompt: str, routing: list, task_label: str = "", max_tokens: int = 200) -> str: for model in routing: try: response = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=0.3, max_tokens=max_tokens ) # Safety wrapper: catch malformed or None responses if response and response.choices and len(response.choices) > 0: result = response.choices[0].message.content if result: result = result.strip() print(f"[Hermes] {task_label} – Model {model} succeeded.", flush=True) return result # Empty or malformed response print(f"[Hermes] {task_label} – Model {model} returned empty/None, trying next.", flush=True) except Exception as e: error_msg = str(e) if "429" in error_msg: increment_error_count("rate_limit", error_msg) elif "502" in error_msg: increment_error_count("model_down", error_msg) elif "401" in error_msg: increment_error_count("auth_error", error_msg) elif "timeout" in error_msg.lower(): increment_error_count("network", error_msg) else: increment_error_count("other", error_msg) print(f"[Hermes] {task_label} – Model {model} failed: {e}", flush=True) continue print(f"[Hermes] {task_label} – All models failed.", flush=True) return "" # ── Health score monitoring & Telegram alerts ─────────────── def compute_health_score(): polyhub_status = read_file("/app/polyhub_status.txt", "unknown") hermes_status = read_file("/app/hermes_status.txt", "unknown") balance_status = read_file("/app/balance_status.txt", "unknown") backup_status = read_file("/app/backup_status.txt", "disabled") halted = False try: if os.path.exists("/app/capital_state.json"): with open("/app/capital_state.json") as f: state = json.load(f) halted = state.get("halted", False) except: pass score = 100 if "running" not in polyhub_status: score -= 30 if "running" not in hermes_status: score -= 30 if "running" not in balance_status: score -= 20 if halted: score -= 20 if backup_status == "failed": score -= 10 return score, { "polyhub": polyhub_status, "hermes": hermes_status, "balance": balance_status, "backup": backup_status, "halted": halted } def health_monitor(): score, details = compute_health_score() last_level = read_file(HEALTH_LAST_ALERT_FILE, "") if score >= 70: current_level = "normal" elif score >= 40: current_level = "warning" else: current_level = "critical" if current_level != last_level: if current_level == "warning": msg = f"⚠️ Health Score dropped to {score}/100\n\n{json.dumps(details, indent=2)}" elif current_level == "critical": msg = f"🚨 CRITICAL Health Score: {score}/100\n\n{json.dumps(details, indent=2)}" else: msg = f"✅ Health Score restored to {score}/100" if send_telegram_alert(msg): write_file(HEALTH_LAST_ALERT_FILE, current_level) else: add_pending_alert(msg) write_file(HEALTH_LAST_ALERT_FILE, current_level) return score, details # ── Consensus analysis for high‑confidence signals ───────── def consensus_analyze(signal, context): prompt = f"""You are a Polymarket trading assistant. A signal was detected: - Market: {signal.get('market_slug')} - Action: {signal.get('action')} - Confidence score: {signal.get('confidence')} - Amount: ${signal.get('amount_usd')} Relevant past knowledge: {context} Market sentiment: {get_market_sentiment()} Current follow threshold: {get_follow_threshold()} Should we follow this signal? Reply with "FOLLOW" or "IGNORE" and briefly explain.""" return _dual_collaborator_decision(prompt, "Consensus") def _dual_collaborator_decision(prompt: str, task_label: str) -> str: """Ask both L3 collaborators; if they disagree, ask the arbiter.""" decision_a = call_llm_with_routing(prompt, [L3_COLLABORATOR_A], f"{task_label}-A", max_tokens=150) decision_b = call_llm_with_routing(prompt, [L3_COLLABORATOR_B], f"{task_label}-B", max_tokens=150) if not decision_a or not decision_b: fallback = call_llm_with_routing(prompt, [L3_ARBITER], f"{task_label}-Arbiter", max_tokens=150) return fallback if fallback else "IGNORE (model error)" a_follow = "FOLLOW" in decision_a.upper() b_follow = "FOLLOW" in decision_b.upper() if a_follow == b_follow: return decision_a if a_follow else decision_b arbiter_prompt = f"""Two models disagreed on a trading signal. Model A says: {decision_a} Model B says: {decision_b} Original prompt: {prompt} As the arbiter, which decision (FOLLOW or IGNORE) is more appropriate? Reply with "FOLLOW" or "IGNORE" and briefly explain.""" arbiter_decision = call_llm_with_routing(arbiter_prompt, [L3_ARBITER], f"{task_label}-Arbiter", max_tokens=150) if not arbiter_decision: return "IGNORE (arbiter failed)" return arbiter_decision # ── Signal analysis ───────────────────────────────────────── def analyze_signal(signal: dict, context: str) -> str: confidence = signal.get("confidence", 0) threshold = get_follow_threshold() high_confidence = False if confidence >= 0.8: try: if os.path.exists("/app/cluster_results.json"): with open("/app/cluster_results.json", "r") as f: cluster_data = json.load(f) followed = cluster_data.get("followed_wallets", []) if len(followed) >= 2: high_confidence = True except: pass if high_confidence: return consensus_analyze(signal, context) prompt = f"""You are a Polymarket trading assistant. A signal was detected: - Wallet: {signal.get('wallet_address')} - Market: {signal.get('market_slug')} - Action: {signal.get('action')} - Confidence score: {confidence} - Amount: ${signal.get('amount_usd')} - Analysis mode: standard - Market sentiment: {get_market_sentiment()} - Current follow threshold: {threshold} Relevant past knowledge from our wiki: {context if context else 'No historical data found.'} Given the above, should we follow this signal? Reply with "FOLLOW" or "IGNORE" and briefly explain. """ decision = call_llm_with_routing(prompt, REALTIME_ROUTING, f"Signal-{signal.get('market_slug', 'unknown')[:30]}") if not decision: return "IGNORE (LLM error)" if "FOLLOW" not in decision and "IGNORE" not in decision: return f"IGNORE (ambiguous: {decision[:80]})" return decision # ── Task queue processing ─────────────────────────────────── def process_task_queue(): tasks = [] try: if os.path.exists(TASK_QUEUE_FILE): with open(TASK_QUEUE_FILE, "r") as f: tasks = json.load(f) except: return if not tasks: return for task in tasks[:5]: cmd = task.get("command", "") print(f"[Hermes] Processing task: {cmd}", flush=True) if cmd.startswith("analyze"): market = cmd.split(" ", 1)[1] if " " in cmd else "btc-updown-5m-1779771300" prompt = f"Analyze the following Polymarket market briefly (max 150 words): {market}. Include current pricing, liquidity, and any relevant historical patterns from the wiki." result = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "Task-Analyze", max_tokens=250) if result: save_entry(topic=f"Task-Analysis-{market[:30]}", content=result, tags=["task", "analysis"]) elif cmd.startswith("report"): with open("/app/force_report.txt", "w") as f: f.write("1") elif cmd.startswith("threshold"): try: val = float(cmd.split(" ")[1]) with open(FOLLOW_THRESHOLD_FILE, "w") as f: f.write(str(val)) print(f"[Hermes] Threshold updated to {val}", flush=True) except: pass task["status"] = "done" task["completed_at"] = datetime.now(timezone.utc).isoformat() tasks = [t for t in tasks if t.get("status") != "done"] try: with open(TASK_QUEUE_FILE, "w") as f: json.dump(tasks, f) except: pass # ── Auto threshold adjustment (weekly) ────────────────────── def auto_adjust_threshold(): wiki_dir = "/app/wiki" signals = [] try: for fp in sorted(glob.glob(os.path.join(wiki_dir, "*.md")), reverse=True)[:200]: with open(fp, "r") as f: content = f.read() if "FOLLOW" in content or "IGNORE" in content: signals.append(content) except: return if len(signals) < 20: return prompt = f"""You are analyzing historical trading signals. The last {len(signals)} signal analyses are summarized below (excerpts): {chr(10).join(signals[:10])} Based on these patterns, what confidence threshold (between 0.5 and 0.9) would have maximized profit while minimizing losing trades? Reply with just a number between 0.5 and 0.9 and a 1‑sentence explanation. """ result = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "AutoThreshold", max_tokens=100) if result: try: nums = re.findall(r"0\.\d+", result) if nums: new_threshold = float(nums[0]) new_threshold = max(0.5, min(0.9, new_threshold)) with open(FOLLOW_THRESHOLD_FILE, "w") as f: f.write(str(new_threshold)) print(f"[Hermes] Auto‑adjusted threshold to {new_threshold}", flush=True) save_entry(topic="AutoThreshold-Adjustment", content=f"New threshold: {new_threshold}\nReason: {result}", tags=["auto", "threshold"]) except: pass # ── Performance‑based threshold adjustment (daily) ────────── def auto_adjust_threshold_from_performance(): try: if not os.path.exists("/app/virtual_positions.json"): return with open("/app/virtual_positions.json", "r") as f: positions = json.load(f) closed = [p for p in positions if p.get("status") == "closed"] if len(closed) < 5: return recent = sorted(closed, key=lambda p: p.get("exit_time", ""), reverse=True)[:10] wins = [p for p in recent if p.get("result") == "win"] win_rate = len(wins) / len(recent) threshold = get_follow_threshold() new_threshold = threshold if win_rate >= 0.6 and threshold > 0.5: new_threshold = round(threshold - 0.02, 2) new_threshold = max(0.5, new_threshold) elif win_rate <= 0.4 and threshold < 0.8: new_threshold = round(threshold + 0.02, 2) new_threshold = min(0.8, new_threshold) if new_threshold != threshold: with open(FOLLOW_THRESHOLD_FILE, "w") as f: f.write(str(new_threshold)) print(f"[Hermes] Performance‑based threshold: {threshold} → {new_threshold} (win rate {win_rate:.0%})", flush=True) save_entry( topic="AutoThreshold-Performance", content=f"Threshold changed from {threshold} to {new_threshold} (last 10 win rate {win_rate:.0%})", tags=["auto", "threshold", "performance"] ) except Exception as e: print(f"[Hermes] Failed performance threshold adjustment: {e}", flush=True) # ── Daily Reflection ──────────────────────────────────────── def daily_reflection(): print("[Hermes] Starting daily reflection...", flush=True) try: trades = [] if os.path.exists("/app/virtual_positions.json"): with open("/app/virtual_positions.json", "r") as f: positions = json.load(f) cutoff = datetime.now(timezone.utc) - timedelta(days=7) for p in positions: if p.get("status") == "closed": try: exit_time = datetime.fromisoformat(p.get("exit_time", "")) if exit_time > cutoff: trades.append({ "market": p.get("market",""), "action": p.get("action",""), "result": p.get("result",""), "pnl": p.get("pnl",0) }) except: pass flags_closed = [] if os.path.exists("/app/flags.json"): with open("/app/flags.json", "r") as f: flags = json.load(f) for fl in flags: if fl.get("status") == "closed": try: closed_at = datetime.fromisoformat(fl.get("closed_at", "")) if closed_at > cutoff: flags_closed.append({ "description": fl.get("description",""), "result": fl.get("result","") }) except: pass notes = [] try: with open("/app/notes.txt", "r") as f: lines = f.readlines() recent_lines = [l.strip() for l in lines if l.strip()] notes = recent_lines[-10:] except: pass prompt = f"""You are a self‑reflection module for an automated trading system. Below is a summary of the last 7 days: **Closed Virtual Trades:** {json.dumps(trades, indent=2)} **Closed Flags:** {json.dumps(flags_closed, indent=2)} **Recent Macro Notes:** {json.dumps(notes, indent=2)} Please analyze: 1. Which types of signals performed well? Which performed poorly? 2. Are there patterns in the flags accuracy vs. market type? 3. How well do the macro notes align with actual outcomes? 4. What concrete adjustments should the trading agent make (e.g., focus on certain sports/markets, adjust confidence threshold)? Reply in concise bullet points. Maximum 200 words. """ reflection = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "DailyReflection", max_tokens=300) if reflection: save_entry(topic="Daily-Reflection", content=reflection, tags=["reflection", "daily"]) with open("/app/latest_reflection.txt", "w") as f: f.write(reflection) msg = f"🧠 Daily Reflection\n\n{reflection[:1500]}" send_telegram_alert(msg) print("[Hermes] Daily reflection completed.", flush=True) else: print("[Hermes] Daily reflection failed (no LLM response).", flush=True) except Exception as e: print(f"[Hermes] Daily reflection error: {e}", flush=True) # ── Signal processing ─────────────────────────────────────── decision_cache = {} def process_signals(): signals = read_signals() print(f"[Hermes] Woke up. Signals in queue: {len(signals)}", flush=True) if not signals: return if is_halted(): print("[Hermes] Trading halted – skipping signal processing.", flush=True) clear_signals() return reflection_text = read_file("/app/latest_reflection.txt", "") recent_notes_text = "" try: with open("/app/notes.txt", "r") as f: lines = [line.strip() for line in f.readlines() if line.strip()] if lines: recent_notes_text = "Recent macro notes:\n" + "\n".join(lines[-5:]) except: pass for signal in signals: keywords = signal["market_slug"].split("-") try: context = semantic_search(signal.get("market_slug", ""), top_k=3) except: context = query_context(keywords) enriched_context = context if reflection_text: enriched_context = f"[Latest Reflection]\n{reflection_text}\n\n{enriched_context}" if recent_notes_text: enriched_context = f"{recent_notes_text}\n\n{enriched_context}" sig_str = json.dumps(signal, sort_keys=True) sig_hash = hashlib.md5(sig_str.encode()).hexdigest() now = time.time() if sig_hash in decision_cache and (now - decision_cache[sig_hash][1]) < 3600: print(f"[Hermes] Using cached decision for {signal.get('market_slug')}", flush=True) decision = decision_cache[sig_hash][0] else: print(f"[Hermes] Processing signal for {signal.get('market_slug')}...", flush=True) decision = analyze_signal(signal, enriched_context) decision_cache[sig_hash] = (decision, now) print(f"[Hermes] Decision for {signal['market_slug']}: {decision}", flush=True) summary = f"Signal: {json.dumps(signal)}\nDecision: {decision}" save_entry(topic=f"Signal-{signal['market_slug']}", content=summary, tags=keywords) if signals: try: signal_with_time = signals[-1].copy() signal_with_time["processed_at"] = datetime.now(timezone.utc).isoformat() signal_with_time["decision"] = decision_cache.get(hashlib.md5(json.dumps(signals[-1], sort_keys=True).encode()).hexdigest(), ("", 0))[0] with open(LAST_PROCESSED_FILE, "w") as f: json.dump(signal_with_time, f) except: pass clear_signals() # ── Cron jobs & health check ──────────────────────────────── def check_cron_jobs(): now = datetime.now(timezone.utc) if now.hour in (0, 6, 12, 18) and now.minute == 0: print(f"[Hermes] Cron: Auto market analysis at {now.hour}:00 UTC", flush=True) prompt = "Analyze the 3 most active Polymarket markets right now. For each, provide current probability, liquidity, and a brief assessment." result = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "Cron-Analyze", max_tokens=300) if result: save_entry(topic=f"AutoAnalysis-{now.strftime('%Y-%m-%d-%H')}", content=result, tags=["auto", "analysis"]) if now.weekday() == 0 and now.hour == 0 and now.minute == 0: print("[Hermes] Cron: Weekly full review", flush=True) auto_adjust_threshold() if now.hour == 0 and now.minute == 0 and now.second < 5: auto_adjust_threshold_from_performance() if now.hour == 0 and now.minute == 5 and now.second < 5: daily_reflection() if now.minute % 5 == 0 and now.second < 5: health_monitor() # ── Main loop ─────────────────────────────────────────────── def agent_loop(): try: print("[Hermes] Multi-tier hybrid agent loop starting...", flush=True) write_status("running") last_reset_day = None last_cron_check = 0 while True: now_utc = datetime.now(timezone.utc) if now_utc.hour == 0 and now_utc.minute == 0 and now_utc.day != last_reset_day: reset_error_count() last_reset_day = now_utc.day print("[Hermes] Daily LLM error counter reset.", flush=True) current_time = time.time() if current_time - last_cron_check >= 60: check_cron_jobs() last_cron_check = current_time process_task_queue() flush_pending_alerts() process_signals() time.sleep(60) except Exception as e: error_msg = f"[Hermes] CRASH: {e}\n{traceback.format_exc()}" print(error_msg, flush=True) write_status(f"crashed: {e}") if __name__ == "__main__": agent_loop()