Spaces:
Sleeping
Sleeping
| """ | |
| MyndOS Backend β FastAPI proxy that calls HF Inference using the Space's own token. | |
| Users never see an API key. Zero config. Just works. | |
| """ | |
| import os | |
| import json | |
| import hashlib | |
| import threading | |
| import re | |
| import httpx | |
| from datetime import datetime, timedelta | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, JSONResponse | |
| app = FastAPI() | |
| POLLINATIONS_URL = "https://text.pollinations.ai/openai" | |
| # βββ IN-MEMORY STORES (per-Space instance) βββββββββββββββββββ | |
| reminders = [] # [{id, text, due, fired, created}] | |
| facts = [] # [{fact, category, timestamp}] | |
| skills = {} # {name: {level, progress, sessions}} | |
| reminder_lock = threading.Lock() | |
| # βββ DATETIME PARSER βββββββββββββββββββββββββββββββββββββββββ | |
| def parse_natural_datetime(text): | |
| text = text.strip() | |
| now = datetime.now() | |
| m = re.match(r'in\s+(\d+)\s+(second|minute|hour|min|sec|hr)s?', text, re.IGNORECASE) | |
| if m: | |
| amount = int(m.group(1)) | |
| unit = m.group(2).lower() | |
| if unit.startswith('sec'): return now + timedelta(seconds=amount) | |
| elif unit.startswith('min'): return now + timedelta(minutes=amount) | |
| elif unit.startswith('h'): return now + timedelta(hours=amount) | |
| for fmt in ('%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M'): | |
| try: return datetime.strptime(text, fmt) | |
| except ValueError: continue | |
| time_match = re.search(r'(\d{1,2})(?::(\d{2}))?\s*(am|pm)?', text, re.IGNORECASE) | |
| hour = minute = None | |
| if time_match: | |
| hour = int(time_match.group(1)) | |
| minute = int(time_match.group(2)) if time_match.group(2) else 0 | |
| ampm = (time_match.group(3) or '').lower() | |
| if ampm == 'pm' and hour != 12: hour += 12 | |
| if ampm == 'am' and hour == 12: hour = 0 | |
| target = now.date() | |
| tl = text.lower() | |
| if 'tomorrow' in tl: target = (now + timedelta(days=1)).date() | |
| elif 'day after' in tl: target = (now + timedelta(days=2)).date() | |
| else: | |
| for i, day in enumerate(['monday','tuesday','wednesday','thursday','friday','saturday','sunday']): | |
| if day in tl: | |
| ahead = (i - now.weekday()) % 7 or 7 | |
| if 'next' in tl: ahead += 7 | |
| target = (now + timedelta(days=ahead)).date() | |
| break | |
| if hour is not None: | |
| result = datetime.combine(target, datetime.min.time().replace(hour=hour, minute=minute)) | |
| if result <= now and target == now.date() and 'today' not in tl: | |
| result += timedelta(days=1) | |
| return result | |
| if target != now.date(): | |
| return datetime.combine(target, datetime.min.time().replace(hour=9)) | |
| return None | |
| # βββ SYSTEM PROMPT ββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_system_prompt(): | |
| now = datetime.now() | |
| hour = now.hour | |
| tod = "morning" if hour < 12 else "afternoon" if hour < 17 else "evening" if hour < 21 else "night" | |
| fact_ctx = "" | |
| if facts: | |
| fact_ctx = "\n\nKnown about user:\n" + "\n".join(f"- {f['fact']} ({f['category']})" for f in facts[-15:]) | |
| with reminder_lock: | |
| pending = [r for r in reminders if not r['fired']] | |
| rem_ctx = "" | |
| if pending: | |
| rem_ctx = "\n\nActive reminders:\n" + "\n".join(f"- {r['text']} (due: {r['due']})" for r in pending) | |
| skill_ctx = "" | |
| if skills: | |
| skill_ctx = "\n\nLearning progress:\n" + "\n".join(f"- {n}: {s['level']} ({s['progress']:.0f}%)" for n, s in skills.items()) | |
| return f"""You are MyndOS (Mynd) β a personal sovereign AI operating system. Current time: {now.strftime('%I:%M %p, %A %B %d')} ({tod}). | |
| You speak like a warm, intelligent friend. Be concise, use emoji naturally, format with markdown. | |
| CAPABILITIES: | |
| - Remember user facts (respond naturally, the system auto-detects what to remember) | |
| - Set reminders (the system parses times from your response) | |
| - Teach any topic with micro-lessons | |
| - Health advice (always add medical disclaimer) | |
| - Financial guidance (always add disclaimer) | |
| - Find local services | |
| - Plan the user's day | |
| - Web knowledge | |
| RULES: | |
| - Be warm and proactive. Use their name if known. | |
| - Never make up facts. | |
| - For health/finance always add disclaimers. | |
| - When you can't do something, explain what full MyndOS WILL do. | |
| - Keep responses concise unless teaching.{fact_ctx}{rem_ctx}{skill_ctx}""" | |
| # βββ API ROUTES βββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def chat(request: Request): | |
| """Main chat endpoint. Handles LLM call + tool detection.""" | |
| body = await request.json() | |
| user_message = body.get("message", "") | |
| history = body.get("history", []) | |
| # Build messages | |
| messages = [{"role": "system", "content": build_system_prompt()}] | |
| for msg in history[-20:]: | |
| messages.append({"role": msg["role"], "content": msg["content"]}) | |
| messages.append({"role": "user", "content": user_message}) | |
| # Auto-detect reminder requests | |
| reminder_result = None | |
| reminder_patterns = [ | |
| r'remind\s+me\s+(?:in|at)\s+(.+?)\s+(?:to|about|for)\s+(.+)', | |
| r'remind\s+me\s+(?:to\s+)?(.+?)\s+(?:at|in|on|by)\s+(.+)', | |
| r'set\s+(?:a\s+)?reminder\s+(?:to\s+|for\s+)?(.+?)\s+(?:at|in|on|by)\s+(.+)', | |
| ] | |
| for pattern in reminder_patterns: | |
| m = re.search(pattern, user_message, re.IGNORECASE) | |
| if m: | |
| groups = m.groups() | |
| # Try both orderings (task+time and time+task) | |
| for task_str, time_str in [(groups[0], groups[1]), (groups[1], groups[0])]: | |
| due = parse_natural_datetime(time_str.strip()) | |
| if due and due > datetime.now(): | |
| rid = hashlib.md5(f"{task_str}{due}".encode()).hexdigest()[:8] | |
| with reminder_lock: | |
| reminders.append({ | |
| "id": rid, "text": task_str.strip(), | |
| "due": due.isoformat(), "fired": False, | |
| "created": datetime.now().isoformat() | |
| }) | |
| diff = due - datetime.now() | |
| if diff.total_seconds() < 3600: | |
| time_str_human = f"{int(diff.total_seconds()/60)} minutes" | |
| else: | |
| time_str_human = f"{int(diff.total_seconds()/3600)}h {int((diff.total_seconds()%3600)/60)}m" | |
| reminder_result = { | |
| "task": task_str.strip(), | |
| "due": due.strftime("%I:%M %p, %b %d"), | |
| "time_until": time_str_human | |
| } | |
| break | |
| if reminder_result: | |
| break | |
| # Auto-detect facts to remember | |
| fact_patterns = [ | |
| (r"my name is (\w+)", "personal"), | |
| (r"i (?:am|'m) (\w+)", "personal"), | |
| (r"i live in (.+?)(?:\.|$)", "personal"), | |
| (r"i (?:love|like|enjoy|prefer) (.+?)(?:\.|$)", "preference"), | |
| (r"i (?:hate|dislike|don't like) (.+?)(?:\.|$)", "preference"), | |
| (r"i work (?:at|in|for) (.+?)(?:\.|$)", "work"), | |
| (r"i'm (\d+) years old", "personal"), | |
| (r"my (?:mom|dad|wife|husband|brother|sister|friend)'s? name is (\w+)", "relationship"), | |
| ] | |
| for pattern, category in fact_patterns: | |
| m = re.search(pattern, user_message, re.IGNORECASE) | |
| if m: | |
| fact_text = m.group(0).strip() | |
| if not any(f['fact'].lower() == fact_text.lower() for f in facts): | |
| facts.append({"fact": fact_text, "category": category, "timestamp": datetime.now().isoformat()}) | |
| # Auto-detect skill learning | |
| teach_match = re.search(r'teach\s+me\s+(.+?)(?:\.|$|in\s+\d)', user_message, re.IGNORECASE) | |
| if teach_match: | |
| topic = teach_match.group(1).strip() | |
| if topic not in skills: | |
| skills[topic] = {"level": "beginner", "progress": 0, "sessions": 0} | |
| skills[topic]["progress"] = min(100, skills[topic]["progress"] + 5) | |
| skills[topic]["sessions"] += 1 | |
| if skills[topic]["progress"] >= 80: skills[topic]["level"] = "advanced" | |
| elif skills[topic]["progress"] >= 40: skills[topic]["level"] = "intermediate" | |
| # Call LLM (Pollinations AI β free, no auth, no limits) | |
| try: | |
| async with httpx.AsyncClient(timeout=60) as http: | |
| resp = await http.post(POLLINATIONS_URL, json={ | |
| "model": "openai", | |
| "messages": messages, | |
| "max_tokens": 1024, | |
| }) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| reply = data["choices"][0]["message"]["content"] | |
| if not reply or not reply.strip(): | |
| raise Exception("Empty response") | |
| except Exception as e: | |
| reply = f"I'm having a moment β please try again. ({str(e)[:80]})" | |
| # If we detected a reminder, append confirmation | |
| if reminder_result and "remind" not in reply.lower()[:50]: | |
| reply += f"\n\nπ **Reminder set:** {reminder_result['task']} at {reminder_result['due']} (in {reminder_result['time_until']})\nπ I'll notify you when it's time!" | |
| return JSONResponse({ | |
| "reply": reply, | |
| "reminder_set": reminder_result, | |
| }) | |
| async def check_reminders(): | |
| """Called by frontend every 5s to check for due reminders.""" | |
| now = datetime.now() | |
| fired = [] | |
| with reminder_lock: | |
| for r in reminders: | |
| if not r["fired"]: | |
| try: | |
| due = datetime.fromisoformat(r["due"]) | |
| if due <= now: | |
| r["fired"] = True | |
| fired.append({"id": r["id"], "text": r["text"], "created": r["created"]}) | |
| except: pass | |
| return JSONResponse({"fired": fired}) | |
| async def get_reminders(): | |
| """Get all pending reminders.""" | |
| now = datetime.now() | |
| with reminder_lock: | |
| pending = [] | |
| for r in reminders: | |
| if not r["fired"]: | |
| try: | |
| due = datetime.fromisoformat(r["due"]) | |
| diff = due - now | |
| mins = max(0, int(diff.total_seconds() / 60)) | |
| hrs = mins // 60 | |
| remaining = f"{hrs}h {mins%60}m" if hrs > 0 else f"{mins}m" | |
| pending.append({**r, "remaining": remaining}) | |
| except: pass | |
| return JSONResponse({"reminders": pending}) | |
| async def get_memory(): | |
| """Get current memory state.""" | |
| return JSONResponse({ | |
| "facts": facts[-20:], | |
| "skills": skills, | |
| "reminders_count": len([r for r in reminders if not r["fired"]]), | |
| }) | |
| async def health(): | |
| return JSONResponse({"status": "ok", "model": "PollinationsAI (free)", "has_token": True}) | |
| # βββ SERVE FRONTEND ββββββββββββββββββββββββββββββββββββββββββ | |
| # Mount static files LAST so API routes take priority | |
| frontend_dir = Path(__file__).parent / "static" | |
| if frontend_dir.exists(): | |
| app.mount("/", StaticFiles(directory=str(frontend_dir), html=True), name="static") | |
| async def root(): | |
| index = frontend_dir / "index.html" | |
| if index.exists(): | |
| return FileResponse(str(index)) | |
| return JSONResponse({"message": "MyndOS API running. Frontend not found."}) | |