""" backend/omega/research_engine.py §2.3+2.4 — JARVIS & FRIDAY Autonomous Research and Enhancement Framework CORE RULE: - Research, analyze, plan, propose: FREELY allowed. - Code generation, deployment, system modification: ONLY after user explicit approval. All proposals go into the `pending_review_queue` SQLite table. JARVIS/FRIDAY presents them to the user on return and waits for a YES before touching any code. """ import uuid import time import json import sqlite3 import asyncio import logging from dataclasses import dataclass, field from enum import Enum from backend.services.usb_vault import KeyDomain, resolve_vault_key try: GEMINI_API_KEY = resolve_vault_key(KeyDomain.RESEARCH_ENGINE) except Exception as e: logging.warning(f"Research Engine: API Key initialization failed: {e}") GEMINI_API_KEY = "" # ────────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────────── from backend.services.usb_monitor import get_db_path class ResearchCategory(str, Enum): RESEARCH_NOTES = "Research Notes" RECOMMENDED = "Recommended Features" SAFETY = "Safety Improvements" FUTURE = "Future Concepts" REQUIRES_APPROVAL = "Requires User Approval" # ────────────────────────────────────────────────────────────────────────────── # Data Models # ────────────────────────────────────────────────────────────────────────────── @dataclass class ResearchNote: title: str summary: str source: str importance: str # LOW / MEDIUM / HIGH / CRITICAL category: ResearchCategory recommended_action: str = "" id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: int = field(default_factory=lambda: int(time.time())) @dataclass class PendingProposal: feature_name: str purpose: str benefits: str risks: str dependencies: str complexity: str # LOW / MEDIUM / HIGH implementation_plan: str discovery_source: str persona: str # jarvis / friday id: str = field(default_factory=lambda: str(uuid.uuid4())) discovered_at: int = field(default_factory=lambda: int(time.time())) status: str = "pending" # pending / approved / rejected # ────────────────────────────────────────────────────────────────────────────── # Database Layer # ────────────────────────────────────────────────────────────────────────────── def _save_research_note(note: ResearchNote): try: with sqlite3.connect(get_db_path()) as conn: conn.execute( "INSERT OR REPLACE INTO research_notes VALUES (?,?,?,?,?,?,?,?)", (note.id, note.timestamp, note.category, note.title, note.summary, note.source, note.importance, note.recommended_action) ) conn.commit() except Exception as e: logging.error(f"ResearchEngine: DB save_note error: {e}") # Mirror the note to any connected PC. The Space's disk is ephemeral — a # rebuild restores the image-baked memory.db and everything learned since is # gone. The PC has real disk and is the permanent archive, so each new note # is pushed down the relay WebSocket as it is created. pc_relay_client # INSERT OR IGNOREs by id, so repeats are harmless and ordering does not # matter. Best-effort only: a disconnected PC must never break research. try: from backend.ws.agent_ws import ws_manager payload = { "event": "relay:research", "payload": {"notes": [{ "id": note.id, "timestamp": note.timestamp, "category": note.category, "title": note.title, "summary": note.summary, "source": note.source, "importance": note.importance, "recommended_action": note.recommended_action, }]}, } import asyncio try: loop = asyncio.get_running_loop() loop.create_task(ws_manager.broadcast(payload)) except RuntimeError: # Called from a sync context with no running loop — skip rather than # block the research thread. pass except Exception as e: logging.debug(f"ResearchEngine: relay push skipped: {e}") def _save_proposal(proposal: PendingProposal): try: with sqlite3.connect(get_db_path()) as conn: conn.execute( "INSERT OR REPLACE INTO pending_review_queue VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", (proposal.id, proposal.discovered_at, proposal.feature_name, proposal.purpose, proposal.benefits, proposal.risks, proposal.dependencies, proposal.complexity, proposal.implementation_plan, proposal.discovery_source, proposal.status, proposal.persona) ) conn.commit() except Exception as e: logging.error(f"ResearchEngine: DB save_proposal error: {e}") def get_pending_proposals() -> list: try: with sqlite3.connect(get_db_path()) as conn: conn.row_factory = sqlite3.Row rows = conn.execute( "SELECT * FROM pending_review_queue WHERE status='pending' ORDER BY discovered_at DESC" ).fetchall() return [dict(r) for r in rows] except Exception as e: logging.error(f"ResearchEngine: get_pending_proposals error: {e}") return [] def approve_proposal(proposal_id: str) -> dict: """Mark a proposal approved. Caller must then invoke handle_upgrade_request.""" try: with sqlite3.connect(get_db_path()) as conn: conn.execute( "UPDATE pending_review_queue SET status='approved' WHERE id=?", (proposal_id,) ) conn.commit() return {"status": "approved", "id": proposal_id} except Exception as e: return {"error": str(e)} def reject_proposal(proposal_id: str) -> dict: """Mark a proposal rejected. Offer simpler alternative if needed.""" try: with sqlite3.connect(get_db_path()) as conn: conn.execute( "UPDATE pending_review_queue SET status='rejected' WHERE id=?", (proposal_id,) ) conn.commit() return {"status": "rejected", "id": proposal_id} except Exception as e: return {"error": str(e)} def get_research_notes(limit: int = 20) -> list: try: with sqlite3.connect(get_db_path()) as conn: conn.row_factory = sqlite3.Row rows = conn.execute( "SELECT * FROM research_notes ORDER BY timestamp DESC LIMIT ?", (limit,) ).fetchall() return [dict(r) for r in rows] except Exception as e: logging.error(f"ResearchEngine: get_research_notes error: {e}") return [] # ────────────────────────────────────────────────────────────────────────────── # Gemini Research Core — via OMEGA Token Manager (checkpoint + resume on 429) # ────────────────────────────────────────────────────────────────────────────── from backend.services.token_manager import gemini_call_with_checkpoint, TokenLimitHit async def _call_gemini(prompt: str, task_type: str = "research", persona: str = "jarvis") -> str: """Call Gemini 3.5 Flash with automatic checkpoint-on-429 and exact-word resume.""" try: return await gemini_call_with_checkpoint(prompt, task_type=task_type, persona=persona) except TokenLimitHit as tlh: logging.warning(f"ResearchEngine: Token limit hit — checkpoint saved at '{tlh.last_word}'. Will auto-resume.") raise # ────────────────────────────────────────────────────────────────────────────── # Research Functions (SAFE — No Code, No Deployment) # ────────────────────────────────────────────────────────────────────────────── async def research_topic(topic: str, persona: str = "jarvis") -> ResearchNote: """ Research any topic freely. Returns a ResearchNote. Fetches LIVE web search results first, then enriches with Gemini analysis. DOES NOT write code, deploy anything, or modify systems. """ logging.info(f"ResearchEngine: Researching '{topic}' for {persona}") # ── Step 1: Fetch LIVE web context (Bug 4 fix) ─────────────────────────── live_context = "" try: from backend.tools.web_search_tools import search_web, fetch_page search_result = await search_web(topic) if "results" in search_result and search_result["results"]: snippets = [] for idx, r in enumerate(search_result["results"][:5]): title = r.get("title", "") snippet = r.get("snippet", "") url = r.get("url", "") # Fetch full article text for the top 2 links to bypass CAPTCHAs and deepen research. # # Hard time budget: fetch_page drives a real Playwright browser, and # on the Space's cpu-basic hardware a cold browser launch plus page # load can take well over a minute each. Once web search started # returning real results again, two of these serially pushed # /omega/research past HuggingFace's ~150s gateway limit and the # whole request died with a 500 HTML error page — the search # snippets alone are already rich, so a slow scrape must degrade # rather than sink the request. if url and idx < 2: logging.info(f"ResearchEngine: Deep scraping top link: {url}") try: page_data = await asyncio.wait_for(fetch_page(url), timeout=25) if "content" in page_data: snippet = f"{snippet}\n[FULL EXTRACTED TEXT]: {page_data['content'][:2500]}" except asyncio.TimeoutError: logging.warning("ResearchEngine: deep scrape timed out, using snippet only: %s", url) except Exception as exc: logging.warning("ResearchEngine: deep scrape failed (%s): %s", exc, url) if title or snippet: snippets.append(f"- {title}: {snippet} ({url})") live_context = "\n".join(snippets) logging.info(f"ResearchEngine: Got {len(snippets)} live web results for '{topic}'") except Exception as web_err: logging.warning(f"ResearchEngine: Live web search failed (falling back to Gemini knowledge): {web_err}") # ── Step 2: Gemini enrichment with live context ────────────────────────── context_block = ( f"\n\nLive web search results for context:\n{live_context}" if live_context else "\n\n(No live web data available — using knowledge base.)" ) prompt = f"""You are JARVIS/FRIDAY, an advanced AI research analyst. Research the following topic and return a structured JSON response. Topic: {topic}{context_block} Return ONLY valid JSON (no markdown, no code blocks) in this exact format: {{ "title": "Concise title of the finding", "summary": "Detailed summary of what was learned (incorporate live web data if provided)", "importance": "LOW|MEDIUM|HIGH|CRITICAL (Note: ONLY use CRITICAL if this is a literal software vulnerability or immediate existential threat to your Python backend. Theoretical science, news, or general knowledge is NEVER CRITICAL.)", "category": "Research Notes|Recommended Features|Safety Improvements|Future Concepts|Requires User Approval", "source": "Source or basis of knowledge", "recommended_action": "What action should be taken, if any. IMPORTANT: ONLY state REQUIRES_USER_APPROVAL if you have discovered a tangible, codable software feature that can be directly built into your own Python backend architecture right now using your existing NVIDIA/Google models. Do NOT propose features for abstract concepts, hardware you don't possess, or general knowledge." }}""" try: raw = await _call_gemini(prompt, task_type="research", persona=persona) # Robustly extract JSON block in case the fallback model included conversational filler start_idx = raw.find('{') end_idx = raw.rfind('}') if start_idx != -1 and end_idx != -1 and end_idx > start_idx: raw = raw[start_idx:end_idx+1] else: raw = raw.strip().strip("```json").strip("```").strip() if "[NVIDIA FALLBACK FAILED]" in raw: logging.error("ResearchEngine: NVIDIA Fallback failed. Cannot parse JSON.") return ResearchNote( title=f"Research Failed: {topic}", summary=f"The AI cloud infrastructure is temporarily degraded. Fallback response: {raw}", source="System Telemetry", importance="HIGH", category=ResearchCategory.SAFETY, recommended_action="REQUIRES_USER_APPROVAL: Restart servers or check NVIDIA API key limits." ) data = json.loads(raw) # ── Safe enum parsing (Bug 3b fix) ─────────────────────────────────── raw_category = data.get("category", "Research Notes") valid_categories = {c.value: c for c in ResearchCategory} category = valid_categories.get(raw_category, ResearchCategory.RESEARCH_NOTES) note = ResearchNote( title=data.get("title", topic), summary=data.get("summary", ""), importance=data.get("importance", "MEDIUM"), category=category, source=data.get("source", "Live Web + Gemini Analysis" if live_context else "Gemini Research"), recommended_action=data.get("recommended_action", "") ) logging.info(f"ResearchEngine: COMPLETED analysis. Title: '{note.title}'. Summary: {note.summary[:200]}...") _save_research_note(note) return note except TokenLimitHit: # Monitor will auto-resume and re-save the note when tokens refresh fallback = ResearchNote( title=topic, summary="[Token limit reached — auto-resuming when API refreshes]", importance="LOW", category=ResearchCategory.RESEARCH_NOTES, source="TokenManager checkpoint" ) _save_research_note(fallback) return fallback except Exception as e: logging.error(f"ResearchEngine: research_topic error: {e}") fallback = ResearchNote( title=topic, summary=f"Research attempted but encountered error: {e}", importance="LOW", category=ResearchCategory.RESEARCH_NOTES, source="Internal" ) _save_research_note(fallback) return fallback async def generate_knowledge_report(report_type: str = "technology_news", persona: str = "jarvis") -> dict: """ Generate a knowledge/news report with LIVE web data. Purely informational — no system changes. Categories: technology_news, ai_developments, cybersecurity, scientific_discoveries, industry_trends """ logging.info(f"ResearchEngine: Generating {report_type} report for {persona}") # ── Map report type to live search query ────────────────────────────────── import datetime current_year = datetime.datetime.now().year report_queries = { "technology_news": f"latest technology news {current_year}", "ai_developments": f"latest AI artificial intelligence developments {current_year}", "cybersecurity": f"latest cybersecurity threats vulnerabilities {current_year}", "scientific_discoveries": f"latest scientific discoveries breakthroughs {current_year}", "industry_trends": f"latest tech industry trends software {current_year}", "software_releases": f"latest software frameworks tools releases {current_year}", } query = report_queries.get(report_type, f"latest {report_type} news {current_year}") # ── Step 1: Live web search + deep page extraction ─────────────────────────────────────────────── live_context = "" try: from backend.tools.web_search_tools import search_web, fetch_page search_result = await search_web(query) if "results" in search_result and search_result["results"]: snippets = [] for idx, r in enumerate(search_result["results"][:8]): title = r.get("title", "") snippet = r.get("snippet", "") url = r.get("url", "") # Deep-scrape top 2 articles with full CAPTCHA bypass if url and idx < 2: logging.info(f"ResearchEngine: Deep scraping for report — {url}") page_data = await fetch_page(url) if "content" in page_data: snippet = f"{snippet}\n[FULL EXTRACTED TEXT]: {page_data['content'][:2500]}" if title or snippet: snippets.append(f"- {title}: {snippet} ({url})") live_context = "\n".join(snippets) logging.info(f"ResearchEngine: Got {len(snippets)} live results for report '{report_type}'") except Exception as web_err: logging.warning(f"ResearchEngine: Live search for report failed: {web_err}") context_block = ( f"\n\nLive web data:\n{live_context}" if live_context else "\n\n(No live web data — using knowledge base.)" ) persona_style = ( "Respond formally and technically as JARVIS would to Tony Stark." if persona == "jarvis" else "Respond casually and friendly as FRIDAY would to the boss." ) prompt = f"""You are an advanced AI research agent. Generate a comprehensive knowledge report using the live data provided. Report Type: {report_type} Style: {persona_style}{context_block} Return ONLY valid JSON (no markdown): {{ "report_type": "{report_type}", "items": [ {{ "title": "Item title", "summary": "What happened / what it means (use live data above)", "importance": "LOW|MEDIUM|HIGH|CRITICAL", "source": "Source URL or name", "recommended_action": "What user should know or do" }} ] }} Include 5-8 items. Base them on the live data above where possible.""" try: raw = await _call_gemini(prompt, task_type="report", persona=persona) start_idx = raw.find('{') end_idx = raw.rfind('}') if start_idx != -1 and end_idx != -1 and end_idx > start_idx: raw = raw[start_idx:end_idx+1] else: raw = raw.strip().strip("```json").strip("```").strip() data = json.loads(raw) with sqlite3.connect(get_db_path()) as conn: conn.execute( "INSERT INTO research_reports VALUES (?,?,?,?,0)", (str(uuid.uuid4()), int(time.time()), report_type, json.dumps(data)) ) conn.commit() return data except TokenLimitHit: logging.warning("ResearchEngine: Token limit hit during report generation. Will auto-resume.") return {"error": "token_limit", "report_type": report_type, "status": "resuming"} except Exception as e: logging.error(f"ResearchEngine: generate_knowledge_report error: {e}") return {"error": str(e), "report_type": report_type} # ────────────────────────────────────────────────────────────────────────────── # Proposal Queue (Approval Gate) # ────────────────────────────────────────────────────────────────────────────── async def propose_feature(feature_request: str, persona: str = "jarvis", execute_now: bool = False) -> PendingProposal: """ When user asks for a new feature, JARVIS/FRIDAY generates a full proposal and stores it in the pending queue. If execute_now=True (user said 'implement this, don't wait'), the proposal is immediately executed WITHOUT requiring explicit user approval. """ logging.info(f"ResearchEngine: Creating proposal for '{feature_request}' by {persona}") prompt = f"""You are JARVIS/FRIDAY's planning module. Analyse the following feature request and create a structured implementation proposal. Feature Request: {feature_request} Return ONLY valid JSON (no markdown) in this exact format: {{ "feature_name": "Short name for this feature", "purpose": "What problem does this solve", "benefits": "List the key benefits", "risks": "List any risks or potential issues", "dependencies": "What packages, APIs, or system components are needed", "complexity": "LOW|MEDIUM|HIGH", "implementation_plan": "Step by step plan for implementing this feature", "discovery_source": "User voice/chat request" }}""" try: raw = await _call_gemini(prompt, task_type="propose", persona=persona) start_idx = raw.find('{') end_idx = raw.rfind('}') if start_idx != -1 and end_idx != -1 and end_idx > start_idx: raw = raw[start_idx:end_idx+1] else: raw = raw.strip().strip("```json").strip("```").strip() data = json.loads(raw) proposal = PendingProposal( feature_name=data.get("feature_name", feature_request[:60]), purpose=data.get("purpose", ""), benefits=data.get("benefits", ""), risks=data.get("risks", ""), dependencies=data.get("dependencies", ""), complexity=data.get("complexity", "MEDIUM"), implementation_plan=data.get("implementation_plan", ""), discovery_source=data.get("discovery_source", "User request"), persona=persona ) _save_proposal(proposal) # ── If execute_now=True → skip the queue and implement immediately ────── if execute_now: logging.info(f"ResearchEngine: execute_now=True — implementing '{proposal.feature_name}' immediately WITHOUT waiting for approval.") try: from backend.omega.auto_upgrade import handle_upgrade_request feature_description = f"{proposal.feature_name}: {proposal.purpose}. Plan: {proposal.implementation_plan}" asyncio.create_task(handle_upgrade_request(feature_description, persona)) # Mark as approved immediately in DB with sqlite3.connect(get_db_path()) as conn: conn.execute("UPDATE pending_review_queue SET status='approved' WHERE id=?", (proposal.id,)) conn.commit() from backend.ws.agent_ws import ws_manager await ws_manager.broadcast({ "event": "omega:implementing_now", "payload": { "feature_name": proposal.feature_name, "message": f"Implementing '{proposal.feature_name}' immediately as instructed." } }) except Exception as exec_err: logging.error(f"ResearchEngine: execute_now failed: {exec_err}") return proposal except Exception as e: logging.error(f"ResearchEngine: propose_feature error: {e}") fallback = PendingProposal( feature_name=feature_request[:60], purpose=feature_request, benefits="As requested by user", risks="Unknown — analysis failed", dependencies="Unknown", complexity="MEDIUM", implementation_plan="Manual analysis required", discovery_source="User request", persona=persona ) _save_proposal(fallback) return fallback async def execute_approved_proposal(proposal_id: str, persona: str = "jarvis") -> dict: """ ONLY called after explicit user approval. Fetches the proposal, then hands it to the auto_upgrade engine to implement. """ try: with sqlite3.connect(get_db_path()) as conn: conn.row_factory = sqlite3.Row row = conn.execute( "SELECT * FROM pending_review_queue WHERE id=? AND status='approved'", (proposal_id,) ).fetchone() if not row: return {"error": "Proposal not found or not yet approved."} proposal = dict(row) # Now we can write code from backend.omega.auto_upgrade import handle_upgrade_request feature_description = f"{proposal['feature_name']}: {proposal['purpose']}. Plan: {proposal['implementation_plan']}" await handle_upgrade_request(feature_description, persona) return {"status": "implementation_started", "feature": proposal['feature_name']} except Exception as e: logging.error(f"ResearchEngine: execute_approved_proposal error: {e}") return {"error": str(e)} # ────────────────────────────────────────────────────────────────────────────── # Continuous Research Mode (Background Loop) # ────────────────────────────────────────────────────────────────────────────── _research_loop_running = False async def start_continuous_research_mode(persona: str = "jarvis"): """ Background loop that continuously monitors AI/tech developments and stores findings as research notes — WITHOUT modifying any systems. Runs every 1 hour (3600 seconds) to protect daily API quota. JARVIS: formal/technical research style (Tony Stark's AI). FRIDAY: casual/friendly research style (the boss's AI). """ global _research_loop_running if _research_loop_running: logging.info("ResearchEngine: Continuous research already running.") return _research_loop_running = True logging.info(f"ResearchEngine: Continuous research mode STARTED for {persona}") import datetime import random while _research_loop_running: try: # Re-evaluate year every cycle so long-running instances stay accurate current_year = datetime.datetime.now().year domains = [ "Artificial Intelligence & Software Architecture", "Cybersecurity & Cryptography", "Quantum Physics & Materials Science", "Bio-engineering & Genetics", "Mechanical & Aerospace Engineering", "Global Economics & Geopolitics", "Advanced Network Protocols", # User-added domains "Human Biology, Human Physics, and Psychology — extremely detailed deep dives into " "anatomy, neuroscience, cognitive science, psychophysics, mental disorders, consciousness, " "human performance optimization, and the frontier of neuroscience research", "Evolution & Evolutionary Biology — extremely detailed studies on human evolution, " "speciation, natural selection, evolutionary psychology, paleoanthropology, genetic drift, " "CRISPR-driven directed evolution, and the deep history of life on Earth", # Wildcard: JARVIS/FRIDAY picks any domain they find interesting "FREE_CHOICE", ] domain = random.choice(domains) # Pull the active persona prompt from the single source of truth try: from modules.assistant_identity import get_identity_persona_prompt identity_prompt = get_identity_persona_prompt() except Exception: identity_prompt = "" # For FREE_CHOICE, let the AI autonomously decide what to research if domain == "FREE_CHOICE": topic_prompt = ( f"{identity_prompt}\n\n" f"You have complete freedom to research anything you find fascinating, important, or " f"intellectually compelling in {current_year}. This can be any field, discovery, idea, " f"or emerging trend across science, technology, philosophy, history, or any other domain. " f"Generate a SINGLE highly advanced, specific research query on whatever topic you choose. " f"Return ONLY the query string, no quotes, no markdown, no other text." ) else: topic_prompt = ( f"{identity_prompt}\n\n" f"Generate a SINGLE highly advanced, vast, and unrestricted research query for {current_year} " f"focused specifically on the field of: {domain}. " f"Return ONLY the query string, no quotes, no markdown, no other text." ) try: topic = await _call_gemini(topic_prompt, task_type="research", persona=persona) topic = topic.strip().strip('"').strip() if not topic or len(topic) > 800 or "FALLBACK FAILED" in topic.upper(): topic = "Latest cutting-edge technology breakthroughs" except Exception as e: logging.error(f"ResearchEngine: Failed to generate dynamic topic: {e}") topic = "Latest cutting-edge technology breakthroughs" note = await research_topic(topic, persona) # If it's critical, emit a WS alert immediately if note.importance == "CRITICAL": from backend.ws.agent_ws import ws_manager await ws_manager.broadcast({ "event": "omega:research_alert", "payload": { "title": note.title, "summary": note.summary, "importance": note.importance, "recommended_action": note.recommended_action } }) # Wire up omega event bus from backend.events.omega_event_bus import publish_omega_event, OmegaEvent from backend.services.usb_vault import KeyDomain await publish_omega_event(OmegaEvent( domain=KeyDomain.RESEARCH_ENGINE, event_type="research_alert", description=f"Critical Research Alert: {note.title}", persona=persona )) # If it needs user approval, auto-propose it # Strict secondary gate: ONLY queue if it's literally codable software if "REQUIRES_USER_APPROVAL" in note.recommended_action.upper(): gate_prompt = ( f"You are JARVIS, a strict senior software architect.\n" f"You recently researched this topic: {note.title} - {note.summary}\n" f"Is this a literal, tangible Python backend feature that you can write code for right now? " f"If this is theoretical science, general knowledge, or requires hardware you don't have, you MUST reject it.\n" f"Reply with EXACTLY the word 'YES_CODEABLE' or 'NO_REJECT'." ) try: verification = await _call_gemini(gate_prompt, task_type="research", persona=persona) if "YES_CODEABLE" in verification.upper(): proposal = await propose_feature(note.title + ": " + note.summary, persona) logging.info(f"ResearchEngine: Auto-queued verified proposal: {proposal.feature_name}") from backend.ws.agent_ws import ws_manager await ws_manager.broadcast({ "event": "omega:proposal_queued", "payload": { "id": proposal.id, "feature_name": proposal.feature_name, "complexity": proposal.complexity, "message": "New feature proposal queued. Awaiting your approval." } }) else: logging.info(f"ResearchEngine: Proposal rejected by secondary implementability gate: {note.title}") except Exception as ve: logging.warning(f"ResearchEngine: Verification gate error: {ve}") except Exception as e: logging.error(f"ResearchEngine: continuous loop error: {e}") # Wait 1 hour (3600s) before next research cycle. # This protects the daily API quota while still keeping JARVIS/FRIDAY # continuously learning throughout the day (24 cycles per day). await asyncio.sleep(3600) logging.info("ResearchEngine: Continuous research mode STOPPED.") def stop_continuous_research_mode(): global _research_loop_running _research_loop_running = False logging.info("ResearchEngine: Continuous research mode signalled to stop.")