{"repo": "asha-arun/saral-lang", "files": {"core/ai_helper.py": {"baab98328c201fc21ab584f03efced55eaec297a": {"t": 1785674548.9812832, "n": 49185, "h": "85f63319a1a17972", "c": "\"\"\"\nai_helper.py — Saral AI Layer v4.0\n═══════════════════════════════════════════════════════════════════\nWHAT THIS FILE DOES\n Implements the 5 AI features of Saral Lang:\n 1. generate_code — AI writes Saral code from a description\n 2. fix_error — AI explains and fixes broken Saral code\n 3. explain_code — AI explains what a Saral program does\n 4. autocomplete — suggests next lines while coding\n 5. query — answers questions from inside Saral programs\n\nSETUP\n Run once to configure your AI provider:\n python saral.py --setup-ai\n\n Paste any API key — Saral Lang auto-detects the provider.\n\nSUPPORTED AI PROVIDERS\n • xAI Grok — xai-... key (console.x.ai)\n • OpenAI GPT-4o — sk-proj-... key (platform.openai.com)\n • DeepSeek R1 — sk-... key (platform.deepseek.com — free)\n • Anthropic Claude — sk-ant-... key (console.anthropic.com)\n • Google Gemini — AIza... key (aistudio.google.com — free)\n • Groq — gsk_... key (console.groq.com — free)\n • Mistral — alphanumeric (console.mistral.ai)\n • Cohere — alphanumeric (dashboard.cohere.com)\n\nFALLBACK CHAIN (in order):\n Tier 1 — Configured AI provider (set up via --setup-ai)\n Tier 2 — Pattern matching (always works, no key needed)\n═══════════════════════════════════════════════════════════════════\n\"\"\"\n\n# ── Standard library only — no pip installs needed ─────────────\nimport json\nimport os\nimport re\nimport urllib.request\nimport urllib.error\nfrom typing import Optional\n\n# ── ai_config.py handles provider detection, key storage, testing\nfrom ai_config import get_active_ai, PROVIDERS\n\n# ══════════════════════════════════════════════════════════════════\n# TOKEN / TIMEOUT LIMITS — memory safety\n# ══════════════════════════════════════════════════════════════════\n\n# Cap output tokens — Saral code is short; 512 is plenty.\n# Prevents runaway API costs and large memory allocations.\nMAX_OUTPUT_TOKENS = 512\n\n# Timeout per provider type (seconds)\n# Reasoning models (DeepSeek R1) think before answering — give more time\n_TIMEOUT_BY_PROVIDER = {\n \"deepseek\": 45, # R1 does chain-of-thought — can take 20-40s\n \"openai\": 30,\n \"anthropic\": 30,\n \"gemini\": 25,\n \"groq\": 15, # Groq is very fast (hardware accelerated)\n \"xai\": 20, # xAI Grok — fast inference\n \"mistral\": 20,\n \"cohere\": 20,\n}\n_TIMEOUT_DEFAULT = 30\n\n# ══════════════════════════════════════════════════════════════════\n# SARAL LANGUAGE REFERENCE\n# Injected into every AI prompt — teaches the model Saral syntax.\n# Kept minimal (< 1000 tokens) to save API cost and memory.\n# ══════════════════════════════════════════════════════════════════\n\n# ══════════════════════════════════════════════════════════════════\n# FORBIDDEN CONSTRUCTS — injected into every code-generation prompt\n# Lists the Python / non-Saral things the AI must never write.\n# ══════════════════════════════════════════════════════════════════\n\n_SARAL_FORBIDDEN = \"\"\"\nFORBIDDEN — these are Python, NOT Saral. Never use them:\n print(...) → use: show value\n input(\"prompt\") → use: ask \"prompt\" and store in var\n len(list) → use: store length of list in var\n for i in range(n): → use: count from 1 to n as i\n for item in list: → use: for each item in list\n if condition: → use: if condition (no colon)\n elif condition: → use: otherwise if condition\n else: → use: otherwise\n while condition: → use: while condition (no colon)\n def func(param): → use: define func using param\n import module → use: use module\n var = value → use: store value in var\n var += n → use: increase var by n\n var -= n → use: decrease var by n\n list.append(val) → use: add val to list\n list[i] → use: store item i of list in var\n True / False → use: true / false (lower-case is fine too)\n None → use: 0 or \"\" (no None in Saral)\n class / object / self → NOT available in Saral\n try: / except: → use: try / catch / done (no colons)\n f\"string {var}\" → use: \"string \" + var or show var\n\"\"\"\n\n\nSARAL_REFERENCE = \"\"\"\nSARAL LANG — COMPLETE SYNTAX REFERENCE\n\nVARIABLES:\n store 10 in price\n store \"hello\" in name\n increase price by 5\n decrease price by 2\n\nMATH: + - * / ^ % | sqrt, round, floor, ceiling, absolute, random number from A to B\n store sqrt of 144 in result\n store random number from 1 to 100 in lucky\n store sum/average/maximum/minimum of my_list in result\n\nOUTPUT / INPUT:\n show price show \"Hello world\" show blank\n ask \"Your name: \" and store in username\n ask number \"Enter age: \" and store in age\n\nCONDITIONS:\n if price > 100\n show \"expensive\"\n otherwise if price > 50\n show \"medium\"\n otherwise\n show \"cheap\"\n done\n\nLOOPS:\n repeat 5 times for each item in my_list count from 1 to 10 as n\n show \"hello\" show item show n\n done done done\n\n while count < 10 stop (break) skip (continue)\n increase count by 1\n done\n\nLISTS:\n make list called fruits\n add \"apple\" to fruits remove \"apple\" from fruits\n store length of fruits in n store item 1 of fruits in first\n sort fruits sort fruits in reverse reverse fruits\n\nDICTIONARIES:\n make dictionary called person\n set \"name\" of person to \"Asha\"\n store value of \"name\" in person in result\n\nSTRINGS:\n store uppercase/lowercase/trimmed/reversed/length of name in result\n replace \"old\" with \"new\" in text and store in result\n split text by \",\" and store in parts\n join parts with \", \" and store in result\n\nTYPE CONVERSION:\n store \"42\" as number in n store 42 as text in t store 3.14 as decimal in d\n\nFILES:\n read file \"data.txt\" and store in content\n read lines of \"data.txt\" and store in lines\n write content to file \"output.txt\" append content to file \"output.txt\"\n delete file \"old.txt\" if file \"data.txt\" exists\n\nDATE & TIME:\n store today/now/current time/current year/current month/current day in var\n\nFUNCTIONS:\n define greet using name call greet with \"Asha\"\n show \"Hello \" + name store result of greet with \"Asha\" in msg\n end return value return nothing\n\nERROR HANDLING:\n try\n store 10 / 0 in bad\n catch\n show \"something went wrong\"\n show error\n done\n\nAI COMMANDS (inside Saral programs):\n ask ai \"summarize this\" using data and store in result\n ask ai \"translate to Malayalam\" and store in result\n\nLIBRARIES: use math use pandas\nUTILITY: wait 2 seconds clear screen exit program\n\nRULES:\n- Case insensitive: STORE = store = Store\n- Use # for comments\n- Indentation is optional but recommended\n- Single = for equality check inside if statements\n\"\"\"\n\n# ══════════════════════════════════════════════════════════════════\n# PRIVATE HELPERS — HTTP callers\n# ══════════════════════════════════════════════════════════════════\n\ndef _cap_text(text: str, max_chars: int = 3000) -> str:\n \"\"\"\n Memory guard: truncate very long inputs before sending to API.\n 3000 chars ~= 750 tokens — safe, cheap, and fast.\n We use character count (not token count) because token counting\n needs a library. Character estimate is conservative and uses\n zero memory overhead.\n \"\"\"\n if len(text) <= max_chars:\n return text\n return text[:max_chars] + \"\\n[...truncated for memory safety...]\"\n\n\ndef _call_openai_compat(api_key: str, api_url: str, model: str,\n prompt: str, timeout: int) -> str:\n \"\"\"\n Call any OpenAI-compatible endpoint.\n Used for: OpenAI, DeepSeek, Groq, Mistral — they all share\n the same /v1/chat/completions request/response shape.\n \"\"\"\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n \"max_tokens\": MAX_OUTPUT_TOKENS,\n \"temperature\": 0.3,\n \"top_p\": 0.9,\n \"stream\": False,\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n api_url,\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n \"User-Agent\": \"SaralLang/1.0\",\n },\n method = \"POST\",\n )\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n choices = data.get(\"choices\", [])\n if not choices:\n raise RuntimeError(\"Empty choices in response.\")\n content = choices[0].get(\"message\", {}).get(\"content\", \"\").strip()\n if not content:\n raise RuntimeError(\"Empty content in response.\")\n return content\n\n\ndef _call_anthropic(api_key: str, model: str, prompt: str, timeout: int) -> str:\n \"\"\"\n Call Anthropic Claude API.\n Different from OpenAI: uses x-api-key header and /v1/messages shape.\n \"\"\"\n payload = json.dumps({\n \"model\": model,\n \"max_tokens\": MAX_OUTPUT_TOKENS,\n \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.anthropic.com/v1/messages\",\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": api_key,\n \"anthropic-version\": \"2023-06-01\",\n },\n method = \"POST\",\n )\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n content = data.get(\"content\", [])\n if not content:\n raise RuntimeError(\"Empty content in response.\")\n return content[0].get(\"text\", \"\").strip()\n\n\ndef _call_gemini(api_key: str, model: str, prompt: str, timeout: int) -> str:\n \"\"\"\n Call Google Gemini API.\n Different from OpenAI: key goes in the URL, not in the header.\n \"\"\"\n url = (\n f\"https://generativelanguage.googleapis.com/v1beta/models/\"\n f\"{model}:generateContent?key={api_key}\"\n )\n payload = json.dumps({\n \"contents\": [{\"parts\": [{\"text\": prompt}]}],\n \"generationConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": MAX_OUTPUT_TOKENS,\n },\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n url,\n data = payload,\n headers = {\"Content-Type\": \"application/json\"},\n method = \"POST\",\n )\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n candidates = data.get(\"candidates\", [])\n if not candidates:\n raise RuntimeError(\"Empty candidates in response.\")\n return (\n candidates[0]\n .get(\"content\", {})\n .get(\"parts\", [{}])[0]\n .get(\"text\", \"\")\n .strip()\n )\n\n\ndef _call_cohere(api_key: str, model: str, prompt: str, timeout: int) -> str:\n \"\"\"Call Cohere API — uses its own /generate shape.\"\"\"\n payload = json.dumps({\n \"model\": model,\n \"prompt\": prompt,\n \"max_tokens\": MAX_OUTPUT_TOKENS,\n \"temperature\": 0.3,\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.cohere.ai/v1/generate\",\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n method = \"POST\",\n )\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n gens = data.get(\"generations\", [])\n if not gens:\n raise RuntimeError(\"Empty generations in response.\")\n return gens[0].get(\"text\", \"\").strip()\n\n\n# ══════════════════════════════════════════════════════════════════\n# UNIVERSAL AI DISPATCHER\n# ══════════════════════════════════════════════════════════════════\n\ndef _call_ai(prompt: str, silent: bool = False) -> str:\n \"\"\"\n Send a prompt to whatever AI the user has configured.\n\n REASONING PROCESS:\n Step 1 — Ask ai_config.py: who is configured?\n It checks saral_ai.conf first, then env vars.\n Step 2 — Look up the timeout for this provider type.\n Step 3 — Route to the correct HTTP caller based on provider.\n Step 4 — If the call fails, raise RuntimeError so the caller\n can fall back to pattern matching with a helpful message.\n\n WHY ONE DISPATCHER INSTEAD OF TWO HARDCODED TIERS:\n The old approach (try DeepSeek, then try Gemini) meant Asha\n decided for the user. Now the user decides once via --setup-ai,\n and _call_ai just follows that decision.\n Adding a new provider in the future = add one entry in ai_config.py.\n No changes needed in this file.\n \"\"\"\n config = get_active_ai() # reads saral_ai.conf or env vars\n\n if config is None:\n raise RuntimeError(\n \"No AI configured. Run: python saral.py --setup-ai\"\n )\n\n provider_id = config[\"provider\"]\n api_key = config[\"key\"]\n model = config[\"model\"]\n api_url = config.get(\"api_url\", \"\")\n timeout = _TIMEOUT_BY_PROVIDER.get(provider_id, _TIMEOUT_DEFAULT)\n\n try:\n if provider_id in (\"openai\", \"deepseek\", \"groq\", \"mistral\", \"xai\"):\n result = _call_openai_compat(api_key, api_url, model, prompt, timeout)\n elif provider_id == \"anthropic\":\n result = _call_anthropic(api_key, model, prompt, timeout)\n elif provider_id == \"gemini\":\n result = _call_gemini(api_key, model, prompt, timeout)\n elif provider_id == \"cohere\":\n result = _call_cohere(api_key, model, prompt, timeout)\n else:\n raise RuntimeError(f\"Unsupported provider: {provider_id}\")\n\n if not silent:\n print(f\" [AI: {config.get('name', provider_id)} \\u2713]\")\n return result\n\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n raise RuntimeError(f\"HTTP {e.code} from {provider_id}: {body}\") from e\n except urllib.error.URLError as e:\n raise RuntimeError(f\"Network error calling {provider_id}: {e.reason}\") from e\n\n\n# ══════════════════════════════════════════════════════════════════\n# RESPONSE CLEANER\n# ══════════════════════════════════════════════════════════════════\n\ndef _clean(text: str) -> str:\n \"\"\"\n Strip AI formatting noise from responses.\n\n WHY EACH STEP:\n 1. ... — DeepSeek R1 emits chain-of-thought here.\n We want only the final answer, not the reasoning trace.\n 2. ```language ... ``` — Markdown code fences from AI formatting.\n Saral users see raw code; fences would confuse them.\n 3. Leading/trailing whitespace — cosmetic cleanup.\n\n MEMORY NOTE:\n re.sub with DOTALL compiles a regex per call.\n For production scale, pre-compile at module level.\n We don't here because these functions are called rarely\n (user typing speed, not batch loops).\n \"\"\"\n # Remove DeepSeek reasoning traces\n text = re.sub(r\".*?\", \"\", text, flags=re.DOTALL)\n # Remove markdown code fences\n text = re.sub(r\"```[a-z]*\\n?\", \"\", text)\n text = re.sub(r\"```\", \"\", text)\n return text.strip()\n\n\n# ══════════════════════════════════════════════════════════════════\n# DEBUGGER AI HELP — with full pattern fallback\n# Always returns useful output even when no AI key is configured.\n# ══════════════════════════════════════════════════════════════════\n\ndef _pattern_debug_hint(error_type: str, error_msg: str,\n variables: dict) -> str:\n \"\"\"\n Pattern-based debugging hints used when AI is not configured.\n Covers the most common Saral runtime errors with clear, friendly advice.\n Always returns something useful — no silent failures.\n \"\"\"\n hints = []\n em = error_msg.lower()\n\n if \"IndexError\" in error_type or \"index out of range\" in em:\n hints.append(\"Hint: You tried to access an item that does not exist in the list.\")\n hints.append(\" In Saral, list positions start at 1. Make sure your index\")\n hints.append(\" is between 1 and the length of the list.\")\n\n elif \"TypeError\" in error_type and any(\n t in em for t in (\"str\", \"int\", \"float\", \"can only concatenate\")\n ):\n hints.append(\"Hint: You mixed text and numbers in an expression.\")\n hints.append(\" Fix: convert the number to text first —\")\n hints.append(\" store myNumber as text in t\")\n hints.append(\" show myText + t\")\n\n elif \"NameError\" in error_type or \"not defined\" in em:\n m = re.search(r\"name '(\\w+)' is not defined\", error_msg)\n v = m.group(1) if m else \"that variable\"\n hints.append(f\"Hint: '{v}' has not been stored yet.\")\n hints.append(f\" Fix: add store someValue in {v} before using it.\")\n\n elif \"ZeroDivisionError\" in error_type:\n hints.append(\"Hint: Division by zero — the denominator is 0.\")\n hints.append(\" Fix: check the value before dividing —\")\n hints.append(\" if divisor = 0\")\n hints.append(\" show \\\"Cannot divide by zero\\\"\")\n hints.append(\" done\")\n\n elif \"FileNotFoundError\" in error_type:\n m = re.search(r\"'([^']+)'\", error_msg)\n f = m.group(1) if m else \"the file\"\n hints.append(f\"Hint: File '{f}' was not found.\")\n hints.append(\" Fix: check the file name spelling and make sure\")\n hints.append(\" it is in the same folder as your program.\")\n\n elif \"KeyError\" in error_type:\n hints.append(\"Hint: You tried to read a dictionary key that does not exist.\")\n hints.append(\" Fix: make sure you used set \\\"key\\\" of dict to value\")\n hints.append(\" before reading it.\")\n\n elif \"RecursionError\" in error_type:\n hints.append(\"Hint: A function called itself endlessly (infinite recursion).\")\n hints.append(\" Fix: add a stopping condition inside the function.\")\n\n elif error_type or error_msg:\n hints.append(f\"Hint: {error_type} — {error_msg[:120]}\")\n hints.append(\" Use 'vars' to inspect all current values.\")\n hints.append(\" Use 'list' to see where you are in the code.\")\n hints.append(\" Run saral --setup-ai to enable full AI diagnosis.\")\n\n else:\n hints.append(\"Tip: Use 'vars' to see all current variable values.\")\n hints.append(\"Tip: Use 'list' to see your position in the code.\")\n hints.append(\"Tip: Use 'explain' to understand what the current line does.\")\n hints.append(\"Tip: Run saral --setup-ai to enable full AI-powered analysis.\")\n\n if variables:\n snapshot = \", \".join(\n f\"{k}={repr(v)}\" for k, v in list(variables.items())[:5]\n )\n hints.append(f\"Values now: {snapshot}\")\n\n return \"\\n\".join(hints)\n\n\ndef debug_hint(session_context: str, error_type: str = \"\",\n error_msg: str = \"\", variables: dict = None) -> str:\n \"\"\"\n AI-powered debug analysis with automatic pattern fallback.\n\n Called by debugger.py at three moments:\n 1. Automatically when a runtime error occurs\n 2. When the user types 'fix' or 'explain' at the debug> prompt\n 3. After the program completes if there was an error\n\n AI path — sends the full session context to the configured AI provider.\n Fallback — pattern-based hints so the user always gets useful feedback\n even with no API key configured.\n \"\"\"\n prompt = f\"\"\"You are a Saral programming language debugger assistant.\nA developer is debugging a Saral program and needs your help.\nYou have the COMPLETE session history — every step taken, every variable\nvalue, every error that occurred. Use this full context to give a precise\nand helpful diagnosis.\n\n{_cap_text(session_context, max_chars=4000)}\n\nYour task:\n1. Identify what is wrong (or what might go wrong if no error yet).\n2. Explain it in simple plain English — no jargon, assume a beginner.\n3. Point to the exact line number that is the problem.\n4. Show the corrected Saral code for that line if needed.\n5. If no error, explain what the current variable values mean.\n\nBe specific — reference actual variable names and line numbers from\nthe session above. Keep your answer under 10 lines.\n\"\"\"\n try:\n result = _call_ai(prompt, silent=True)\n return _clean(result)\n except RuntimeError:\n return _pattern_debug_hint(error_type, error_msg, variables or {})\n\n\n# ══════════════════════════════════════════════════════════════════\n# PUBLIC API — 5 AI FEATURES\n# ══════════════════════════════════════════════════════════════════\n\n# ── 1. CODE GENERATOR ─────────────────────────────────────────────\n\ndef generate_code(description: str) -> str:\n \"\"\"\n User describes what they want in plain English.\n Returns valid Saral code that implements it.\n\n Pipeline:\n 1. Call AI with a strict Saral-only prompt\n 2. Run the output through _sanitize_saral() to fix common Python slips\n 3. Validate with the Saral compiler\n 4. If errors remain, send them back to AI for one correction pass\n 5. Sanitize and return the best available result\n \"\"\"\n description = _cap_text(description, max_chars=500)\n\n prompt = f\"\"\"You are a Saral programming language code generator.\nYour ONLY job is to write valid Saral code. NEVER write Python.\n\n{SARAL_REFERENCE}\n\n{_SARAL_FORBIDDEN}\n\nTASK: Write Saral code that does exactly this:\n\"{description}\"\n\nMANDATORY RULES — breaking any rule makes the code unusable:\n- Return ONLY raw Saral code. No explanations. No markdown. No backticks.\n- Every single line must use ONLY the keywords shown in the reference above.\n- End-of-line colons (:) are FORBIDDEN — Saral never uses them.\n- Do NOT write print(), input(), len(), def, import, class, x=value, x+=n.\n- Use # for comments — they are welcome and encouraged.\n- Keep it simple. One task per line.\n\nSaral code:\"\"\"\n\n try:\n generated = _call_ai(prompt)\n generated = _clean(generated)\n except RuntimeError:\n return _pattern_generate(description)\n\n # ── Step 1: rule-based sanitizer ──────────────────────────────\n generated = _sanitize_saral(generated)\n\n # ── Step 2: validate with Saral compiler ──────────────────────\n errors = _validate_saral(generated)\n\n # ── Step 3: retry with error feedback if still invalid ────────\n if errors:\n error_summary = \"\\n\".join(f\" - {e}\" for e in errors[:6])\n retry_prompt = f\"\"\"The Saral code you wrote has syntax errors. Rewrite it correctly.\n\n{SARAL_REFERENCE}\n\n{_SARAL_FORBIDDEN}\n\nORIGINAL TASK: \"{description}\"\n\nYOUR BROKEN CODE:\n{generated}\n\nERRORS FOUND BY SARAL COMPILER:\n{error_summary}\n\nFix ALL errors. Return ONLY corrected Saral code. No explanations. No markdown:\"\"\"\n\n try:\n generated = _call_ai(retry_prompt)\n generated = _clean(generated)\n generated = _sanitize_saral(generated)\n except RuntimeError:\n pass # keep sanitized first attempt\n\n return generated\n\n\n# ── 2. ERROR FIXER ────────────────────────────────────────────────\n\ndef fix_error(saral_code: str, error_message: str) -> dict:\n \"\"\"\n Takes broken Saral code and an error message.\n Returns a dict:\n explanation — plain English: what went wrong\n fixed_code — corrected Saral code\n fix_summary — one-line: what was changed\n \"\"\"\n # Protect against huge pastes — cap both inputs\n saral_code = _cap_text(saral_code, max_chars=1000)\n error_message = _cap_text(error_message, max_chars=300)\n\n prompt = f\"\"\"You are a Saral programming language teacher helping a beginner.\n\n{SARAL_REFERENCE}\n\nBROKEN SARAL CODE:\n{saral_code}\n\nERROR MESSAGE:\n{error_message}\n\nYour job:\n1. Explain what went wrong in simple plain English (1-3 sentences, no jargon).\n2. Show the corrected Saral code.\n3. Give a one-line summary of what was fixed.\n\nRespond in this EXACT format (no other text before or after):\nEXPLANATION: \nFIXED_CODE:\n\nFIX_SUMMARY: \"\"\"\n\n try:\n result = _call_ai(prompt)\n result = _clean(result)\n return _parse_fix_response(result, saral_code)\n except RuntimeError:\n return {\n \"explanation\": (\n \"AI is not available right now. \"\n \"Run: python saral.py --setup-ai to configure an AI provider.\"\n ),\n \"fixed_code\": saral_code,\n \"fix_summary\": \"Could not auto-fix — AI unavailable.\"\n }\n\n\ndef _parse_fix_response(text: str, original_code: str) -> dict:\n \"\"\"Extract structured fields from the AI fix response.\"\"\"\n explanation = \"\"\n fixed_code = \"\"\n fix_summary = \"\"\n\n exp_match = re.search(\n r\"EXPLANATION:\\s*(.+?)(?=FIXED_CODE:|FIX_SUMMARY:|$)\", text, re.DOTALL\n )\n code_match = re.search(\n r\"FIXED_CODE:\\s*\\n(.+?)(?=FIX_SUMMARY:|$)\", text, re.DOTALL\n )\n fix_match = re.search(r\"FIX_SUMMARY:\\s*(.+?)$\", text, re.DOTALL)\n\n if exp_match:\n explanation = exp_match.group(1).strip()\n if code_match:\n fixed_code = code_match.group(1).strip()\n if fix_match:\n fix_summary = fix_match.group(1).strip()\n\n return {\n \"explanation\": explanation or \"Something went wrong — check the error message above.\",\n \"fixed_code\": fixed_code or original_code,\n \"fix_summary\": fix_summary or \"Unknown fix.\"\n }\n\n\n# ── 3. CODE EXPLAINER ─────────────────────────────────────────────\n\ndef explain_code(saral_code: str) -> str:\n \"\"\"\n Takes Saral code and explains what it does in plain English.\n Useful when reading someone else's code or AI-generated code.\n \"\"\"\n saral_code = _cap_text(saral_code, max_chars=1000)\n\n prompt = f\"\"\"You are a Saral programming language teacher.\n\n{SARAL_REFERENCE}\n\nSARAL CODE TO EXPLAIN:\n{saral_code}\n\nExplain what this code does in simple plain English.\nRules:\n- Write as if explaining to someone who has never coded before.\n- No technical jargon.\n- Step by step — one step per line.\n- Maximum 10 lines.\n- Do not show any code in your explanation.\n\nExplanation:\"\"\"\n\n try:\n result = _call_ai(prompt)\n return _clean(result)\n except RuntimeError:\n return (\n \"AI is not available right now. \"\n \"Run: python saral.py --setup-ai to configure an AI provider.\"\n )\n\n\n# ── 4. AUTOCOMPLETE ───────────────────────────────────────────────\n\ndef autocomplete(code_so_far: str, current_line: str = \"\") -> list:\n \"\"\"\n Given the code so far and the current partial line,\n returns 1-3 suggested completions.\n\n Fast path: pattern matching (no API call, instant).\n Slow path: AI (only for complex, ambiguous cases).\n \"\"\"\n # Always try fast pattern path first — saves API cost and latency\n pattern_suggestions = _pattern_autocomplete(current_line, code_so_far)\n if pattern_suggestions:\n return pattern_suggestions\n\n # AI path — only use last 15 lines for context (memory efficiency)\n context = \"\\n\".join(code_so_far.strip().splitlines()[-15:])\n context = _cap_text(context, max_chars=600)\n\n prompt = f\"\"\"You are a Saral programming language autocomplete engine.\n\n{SARAL_REFERENCE}\n\nCODE SO FAR:\n{context}\n\nCURRENT PARTIAL LINE: \"{current_line}\"\n\nSuggest the 3 most likely next lines the user wants to write.\nRules:\n- Return ONLY 3 lines, one per line, numbered 1. 2. 3.\n- Each suggestion must be valid Saral syntax.\n- Match the context logically.\n- No explanations.\n\nSuggestions:\"\"\"\n\n try:\n result = _call_ai(prompt, silent=True)\n result = _clean(result)\n suggestions = []\n for line in result.splitlines():\n line = re.sub(r\"^\\d+[\\.\\)]\\s*\", \"\", line).strip()\n if line:\n suggestions.append(line)\n return suggestions[:3]\n except RuntimeError:\n return []\n\n\n# ══════════════════════════════════════════════════════════════════\n# PATTERN LIBRARY — Tier 3 code generation\n# No AI, no internet, always works.\n# Keys are tuples of words to match; values are Saral code templates.\n# ══════════════════════════════════════════════════════════════════\n\n_PATTERN_TEMPLATES: dict = {\n (\"hello\", \"world\"):\n 'show \"Hello, World!\"',\n\n (\"add\", \"two\", \"number\"):\n 'ask number \"Enter first number: \" and store in a\\n'\n 'ask number \"Enter second number: \" and store in b\\n'\n 'store a + b in result\\n'\n 'show result',\n\n (\"greet\", \"name\"):\n 'ask \"Enter your name: \" and store in name\\n'\n 'show \"Hello, \" + name',\n\n (\"average\", \"list\"):\n 'make list called numbers\\n'\n 'add 10 to numbers\\nadd 20 to numbers\\nadd 30 to numbers\\n'\n 'store average of numbers in avg\\n'\n 'show avg',\n\n (\"count\", \"1\", \"10\"):\n 'count from 1 to 10 as n\\n show n\\ndone',\n\n (\"read\", \"file\"):\n 'read file \"myfile.txt\" and store in content\\nshow content',\n\n (\"write\", \"file\"):\n 'store \"Hello\" in message\\nwrite message to file \"output.txt\"',\n\n (\"sort\", \"list\"):\n 'make list called items\\n'\n 'add 3 to items\\nadd 1 to items\\nadd 2 to items\\n'\n 'sort items\\nshow items',\n\n (\"dictionary\", \"store\"):\n 'make dictionary called data\\n'\n 'set \"name\" of data to \"Asha\"\\n'\n 'set \"age\" of data to 30\\n'\n 'store value of \"name\" in data in name\\n'\n 'show name',\n\n (\"random\", \"number\"):\n 'store random number from 1 to 100 in lucky\\nshow lucky',\n\n (\"today\", \"date\"):\n 'store today in date\\nshow date',\n\n (\"repeat\", \"loop\"):\n 'repeat 5 times\\n show \"hello\"\\ndone',\n\n (\"function\", \"define\"):\n 'define greet using name\\n show \"Hello \" + name\\nend\\n\\ncall greet with \"World\"',\n\n (\"calculator\", \"math\"):\n 'ask number \"Enter first number: \" and store in a\\n'\n 'ask \"Enter operation (+, -, *, /): \" and store in op\\n'\n 'ask number \"Enter second number: \" and store in b\\n'\n 'if op = \"+\"\\n store a + b in result\\ndone\\n'\n 'if op = \"-\"\\n store a - b in result\\ndone\\n'\n 'if op = \"*\"\\n store a * b in result\\ndone\\n'\n 'if op = \"/\"\\n store a / b in result\\ndone\\n'\n 'show result',\n\n (\"even\", \"odd\"):\n 'ask number \"Enter a number: \" and store in n\\n'\n 'if n % 2 = 0\\n show \"Even\"\\notherwise\\n show \"Odd\"\\ndone',\n\n (\"password\", \"secret\"):\n 'ask \"Enter password: \" and store in entered\\n'\n 'store \"saral123\" in correct\\n'\n 'if entered = correct\\n show \"Access granted\"\\notherwise\\n show \"Wrong password\"\\ndone',\n}\n\n\ndef _pattern_generate(description: str) -> str:\n \"\"\"\n Match the description words against pattern templates.\n Returns the best-matching Saral code, or a helpful fallback message.\n \"\"\"\n desc_words = set(description.lower().split())\n best_match: Optional[str] = None\n best_score = 0\n\n for keywords, template in _PATTERN_TEMPLATES.items():\n score = sum(1 for k in keywords if k in desc_words)\n if score > best_score:\n best_score = score\n best_match = template\n\n if best_match and best_score >= 1:\n return best_match\n\n return (\n \"# Could not generate code from that description.\\n\"\n \"# Run: python saral.py --setup-ai to configure an AI provider.\\n\"\n 'show \"AI not available. Run --setup-ai to enable code generation.\"'\n )\n\n\ndef _sanitize_saral(code: str) -> str:\n \"\"\"\n Convert common Python constructs in AI output to their Saral equivalents.\n This is a line-by-line rule pass applied BEFORE compiler validation.\n\n Each rule targets one specific Python habit the AI tends to use:\n print(...) → show ...\n elif / else: → otherwise if / otherwise\n for x in y: → for each x in y\n range(n) → count from 1 to n as i\n if/while with trailing colon → remove the colon\n def func(): → define func\n var += / -= → increase / decrease\n list.append() → add to list\n import x → use x\n x = value → store value in x (only for bare assignments)\n \"\"\"\n _SARAL_KEYWORDS = {\n 'store', 'if', 'make', 'add', 'show', 'ask', 'count', 'repeat',\n 'while', 'for', 'define', 'call', 'return', 'increase', 'decrease',\n 'write', 'read', 'append', 'delete', 'sort', 'reverse', 'set',\n 'remove', 'global', 'include', 'use', 'check', 'find', 'replace',\n 'split', 'join', 'wait', 'stop', 'skip', 'exit', 'clear', 'try',\n 'catch', 'raise', 'fetch', 'run', 'otherwise', 'done', 'end',\n }\n\n fixed = []\n for line in code.splitlines():\n stripped = line.strip()\n indent = line[: len(line) - len(line.lstrip())]\n\n # Blank lines and comments pass through unchanged\n if not stripped or stripped.startswith('#'):\n fixed.append(line)\n continue\n\n # ── Inline expression substitutions — applied first so every subsequent\n # rule benefits from them (e.g. print(str(x)) → print(x as text) → show x as text)\n line = re.sub(r'\\blen\\s*\\(\\s*(\\w+)\\s*\\)', r'length of \\1', line)\n line = re.sub(r'\\bstr\\s*\\(\\s*(\\w+)\\s*\\)', r'\\1 as text', line)\n line = re.sub(r'\\bint\\s*\\(\\s*(\\w+)\\s*\\)', r'\\1', line)\n line = re.sub(r'\\bfloat\\s*\\(\\s*(\\w+)\\s*\\)', r'\\1', line)\n stripped = line.strip()\n\n # print(...) → show ...\n m = re.match(r'^print\\s*\\((.+)\\)\\s*$', stripped)\n if m:\n fixed.append(f'{indent}show {m.group(1).strip()}')\n continue\n\n # elif condition: → otherwise if condition (and == → =)\n m = re.match(r'^elif\\s+(.+?):\\s*$', stripped)\n if m:\n cond = re.sub(r'\\s*==\\s*', ' = ', m.group(1).strip())\n fixed.append(f'{indent}otherwise if {cond}')\n continue\n\n # else: → otherwise\n if re.match(r'^else\\s*:\\s*$', stripped):\n fixed.append(f'{indent}otherwise')\n continue\n\n # for item in list: → for each item in list\n m = re.match(r'^for\\s+(\\w+)\\s+in\\s+(?!range)(\\w+)\\s*:\\s*$', stripped)\n if m:\n fixed.append(f'{indent}for each {m.group(1)} in {m.group(2)}')\n continue\n\n # for i in range(n): → count from 1 to n as i\n # for i in range(a, b): → count from a to b as i\n m = re.match(r'^for\\s+(\\w+)\\s+in\\s+range\\s*\\((.+)\\)\\s*:\\s*$', stripped)\n if m:\n var = m.group(1)\n args = [a.strip() for a in m.group(2).split(',')]\n if len(args) == 1:\n fixed.append(f'{indent}count from 1 to {args[0]} as {var}')\n else:\n fixed.append(f'{indent}count from {args[0]} to {args[1]} as {var}')\n continue\n\n # if condition: → if condition (remove colon, replace == with =)\n m = re.match(r'^(if\\s+.+?):\\s*$', stripped)\n if m:\n cond_line = re.sub(r'\\s*==\\s*', ' = ', m.group(1))\n fixed.append(f'{indent}{cond_line}')\n continue\n\n # while condition: → while condition (remove colon, replace == with =)\n m = re.match(r'^(while\\s+.+?):\\s*$', stripped)\n if m:\n cond_line = re.sub(r'\\s*==\\s*', ' = ', m.group(1))\n fixed.append(f'{indent}{cond_line}')\n continue\n\n # try: → try\n if re.match(r'^try\\s*:\\s*$', stripped):\n fixed.append(f'{indent}try')\n continue\n\n # except ...: → catch\n if re.match(r'^except.*:\\s*$', stripped):\n fixed.append(f'{indent}catch')\n continue\n\n # def func(params): → define func using params\n m = re.match(r'^def\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*$', stripped)\n if m:\n fname = m.group(1)\n params = m.group(2).strip()\n if params:\n fixed.append(f'{indent}define {fname} using {params}')\n else:\n fixed.append(f'{indent}define {fname}')\n continue\n\n # var += n → increase var by n\n m = re.match(r'^(\\w+)\\s*\\+=\\s*(.+)$', stripped)\n if m:\n fixed.append(f'{indent}increase {m.group(1)} by {m.group(2).strip()}')\n continue\n\n # var -= n → decrease var by n\n m = re.match(r'^(\\w+)\\s*-=\\s*(.+)$', stripped)\n if m:\n fixed.append(f'{indent}decrease {m.group(1)} by {m.group(2).strip()}')\n continue\n\n # list.append(val) → add val to list\n m = re.match(r'^(\\w+)\\.append\\s*\\((.+)\\)\\s*$', stripped)\n if m:\n fixed.append(f'{indent}add {m.group(2).strip()} to {m.group(1)}')\n continue\n\n # import x → use x\n m = re.match(r'^import\\s+(\\w+)\\s*$', stripped)\n if m:\n fixed.append(f'{indent}use {m.group(1)}')\n continue\n\n # var = int(input(\"prompt\")) / float(input(\"prompt\")) → ask number \"prompt\" and store in var\n m = re.match(r'^([a-zA-Z_]\\w*)\\s*=\\s*(?:int|float)\\s*\\(\\s*input\\s*\\((.+?)\\)\\s*\\)\\s*$', stripped)\n if m:\n fixed.append(f'{indent}ask number {m.group(2).strip()} and store in {m.group(1)}')\n continue\n\n # var = input(\"prompt\") → ask \"prompt\" and store in var\n m = re.match(r'^([a-zA-Z_]\\w*)\\s*=\\s*input\\s*\\((.+)\\)\\s*$', stripped)\n if m:\n fixed.append(f'{indent}ask {m.group(2).strip()} and store in {m.group(1)}')\n continue\n\n # var = len(list) → store length of list in var\n m = re.match(r'^([a-zA-Z_]\\w*)\\s*=\\s*len\\s*\\(\\s*(\\w+)\\s*\\)\\s*$', stripped)\n if m:\n fixed.append(f'{indent}store length of {m.group(2)} in {m.group(1)}')\n continue\n\n # var = [] → make list called var\n m = re.match(r'^([a-zA-Z_]\\w*)\\s*=\\s*\\[\\]\\s*$', stripped)\n if m:\n fixed.append(f'{indent}make list called {m.group(1)}')\n continue\n\n # var = {} → make dictionary called var\n m = re.match(r'^([a-zA-Z_]\\w*)\\s*=\\s*\\{\\}\\s*$', stripped)\n if m:\n fixed.append(f'{indent}make dictionary called {m.group(1)}')\n continue\n\n # var = value (bare Python assignment — only when first word is not a Saral keyword)\n first_word = stripped.split()[0].lower() if stripped.split() else ''\n if first_word not in _SARAL_KEYWORDS:\n m = re.match(r'^([a-zA-Z_]\\w*)\\s*=(?!=)\\s*(.+)$', stripped)\n if m:\n fixed.append(f'{indent}store {m.group(2).strip()} in {m.group(1)}')\n continue\n\n # Replace Python == with Saral = inside if/while/otherwise if conditions\n if re.match(r'^(if|while|otherwise if)\\s+', stripped):\n line = re.sub(r'\\s*==\\s*', ' = ', line)\n\n fixed.append(line)\n\n result = '\\n'.join(fixed)\n\n # ── Block-closer pass ──────────────────────────────────────────\n # If the code has no `done`/`end` at all (i.e. it came from Python\n # with indentation blocks), infer where they belong from dedents.\n if not any(re.match(r'^\\s*(done|end)\\s*$', l) for l in result.splitlines()):\n result = _add_saral_block_closers(result)\n\n return result\n\n\ndef _add_saral_block_closers(code: str) -> str:\n \"\"\"\n Insert `done` / `end` before each dedent in code that has no block closers.\n `define` blocks use `end`; everything else uses `done`.\n `otherwise` / `otherwise if` are continuations — they don't close the if block.\n \"\"\"\n BLOCK_KWS = {'if', 'count', 'for', 'while', 'repeat', 'try', 'catch'}\n DEFINE_KWS = {'define'}\n CONT_KWS = {'otherwise'} # continuations of if — never close\n\n lines = code.splitlines()\n result = []\n stack = [] # list of [indent_col, closer_string]\n\n for line in lines:\n stripped = line.strip()\n if not stripped or stripped.startswith('#'):\n result.append(line)\n continue\n\n this_indent = len(line) - len(line.lstrip())\n first_word = stripped.split()[0].lower()\n\n # Dedented, non-continuation line → close open block(s)\n if stack and this_indent <= stack[-1][0] and first_word not in CONT_KWS:\n while stack and this_indent <= stack[-1][0]:\n col, closer = stack.pop()\n result.append(' ' * col + closer)\n\n result.append(line)\n\n if first_word in BLOCK_KWS | DEFINE_KWS:\n closer = 'end' if first_word in DEFINE_KWS else 'done'\n stack.append([this_indent, closer])\n\n # Close any blocks still open at end-of-file\n while stack:\n col, closer = stack.pop()\n result.append(' ' * col + closer)\n\n return '\\n'.join(result)\n\n\ndef _validate_saral(code: str) -> list:\n \"\"\"\n Compile the Saral code silently and return a list of parse-error strings.\n Returns [] when the code is fully valid.\n\n Uses a lazy import of pipeline.py (to avoid the circular import that would\n occur if imported at module level, since pipeline.py imports ai_helper.py).\n \"\"\"\n import io\n from contextlib import redirect_stdout, redirect_stderr\n try:\n from pipeline import compile_saral # lazy — avoids circular import\n except ImportError:\n return [] # compiler unavailable; assume valid\n\n captured_out = io.StringIO()\n captured_err = io.StringIO()\n try:\n with redirect_stdout(captured_out), redirect_stderr(captured_err):\n _, _, parse_errors = compile_saral(\n code, \"\", show_warnings=False\n )\n return parse_errors or []\n except SystemExit:\n # compile_saral calls sys.exit on fatal errors; treat as error list\n output = captured_out.getvalue().strip()\n return [output] if output else [\"Code contains invalid Saral syntax\"]\n except Exception as exc:\n return [str(exc)]\n\n\ndef validate_generated_code(code: str):\n \"\"\"\n Public function called by saral.py after displaying generated code.\n Returns (is_valid: bool, errors: list[str]).\n \"\"\"\n errors = _validate_saral(code)\n return (len(errors) == 0), errors\n\n\ndef _pattern_autocomplete(current_line: str, context: str) -> list:\n \"\"\"\n Instant rule-based autocomplete — covers the most common Saral patterns.\n No AI, no internet, zero latency.\n \"\"\"\n cl = current_line.strip().lower()\n\n if cl in (\"st\", \"sto\", \"stor\"):\n return [\"store in \", \"store today in date\", \"store current time in t\"]\n if cl.startswith(\"store\") and \"in\" not in cl:\n return [\"store in result\"]\n if cl in (\"sh\", \"sho\"):\n return [\"show \", \"show blank\", \"show error\"]\n if cl == \"if\":\n return [\"if > 0\", 'if = \"\"', \"if > 0\\n \\ndone\"]\n if cl in (\"re\", \"rep\"):\n return [\"repeat 5 times\", \"repeat times\", \"return \"]\n if cl in (\"fo\", \"for\"):\n return [\"for each item in \", \"for each line in lines\"]\n if cl in (\"de\", \"def\"):\n return [\"define using \", \"define \", \"decrease by 1\"]\n if cl in (\"ma\", \"mak\"):\n return [\"make list called \", \"make dictionary called \"]\n if cl == \"ad\":\n return [\"add to \"]\n if cl == \"ca\":\n return [\"call with \", \"call \", \"catch\"]\n if cl == \"wh\":\n return [\"while > 0\", 'write to file \"\"']\n if cl == \"co\":\n return [\"count from 1 to as n\", \"count from 1 to 10 as n\"]\n if cl == \"as\":\n return ['ask \"\" and store in ', 'ask number \"\" and store in ', 'ask ai \"\" and store in ']\n if cl == \"us\":\n return [\"use math\", \"use pandas\", \"use json\"]\n\n return []\n\n\n# ── 5. QUERY (called from inside Saral programs) ──────────────────\n\ndef _pattern_query(prompt: str, data: str = \"\") -> str:\n \"\"\"\n Tier 3 query fallback — pattern-based answers for common questions.\n Always works. No internet, no API key, no memory overhead.\n \"\"\"\n p = prompt.lower().strip()\n\n # Math questions\n math_match = re.search(r\"what is (\\d+)\\s*([\\+\\-\\*\\/])\\s*(\\d+)\", p)\n if math_match:\n a = int(math_match.group(1))\n op = math_match.group(2)\n b = int(math_match.group(3))\n res = {\"+\": a+b, \"-\": a-b, \"*\": a*b, \"/\": (a/b if b else \"undefined\")}.get(op)\n return f\"{a} {op} {b} = {res}\"\n\n # Saral syntax help\n if \"store\" in p and any(w in p for w in (\"do\", \"mean\", \"what\")):\n return (\"The store command saves a value into a variable. \"\n \"Example: store 10 in price — saves the number 10 into price.\")\n if \"show\" in p and any(w in p for w in (\"do\", \"mean\", \"what\")):\n return \"The show command displays output. Example: show price — prints the value of price.\"\n if \"if\" in p and any(w in p for w in (\"do\", \"mean\", \"what\")):\n return \"The if command checks a condition. Example: if price > 100 / show 'expensive' / done\"\n if \"repeat\" in p and any(w in p for w in (\"do\", \"mean\")):\n return \"The repeat command runs a block of code multiple times. Example: repeat 5 times / show 'hello' / done\"\n if \"define\" in p and any(w in p for w in (\"do\", \"mean\")):\n return \"The define command creates a function. Example: define greet / show 'Hello' / end\"\n if \"saral lang\" in p or \"saral language\" in p:\n return (\"Saral Lang is a programming language that reads like plain English. \"\n \"Developed by Asha V S from Kerala, India.\")\n\n # Data analysis\n if data:\n numbers = re.findall(r\"(\\w+)[=:](\\d+)\", data)\n if numbers:\n if any(w in p for w in (\"highest\", \"maximum\", \"most\", \"best\")):\n best = max(numbers, key=lambda x: int(x[1]))\n return f\"The highest value is {best[0]} with {best[1]}.\"\n if any(w in p for w in (\"lowest\", \"minimum\", \"least\")):\n worst = min(numbers, key=lambda x: int(x[1]))\n return f\"The lowest value is {worst[0]} with {worst[1]}.\"\n\n raw_nums = re.findall(r\"\\d+\", data)\n if raw_nums:\n nums = [int(n) for n in raw_nums]\n if any(w in p for w in (\"average\", \"mean\")):\n return f\"The average is {sum(nums)/len(nums):.2f}.\"\n if any(w in p for w in (\"total\", \"sum\")):\n return f\"The total is {sum(nums)}.\"\n\n # General knowledge\n if \"capital\" in p and \"kerala\" in p:\n return \"The capital of Kerala is Thiruvananthapuram.\"\n if \"capital\" in p and \"india\" in p:\n return \"The capital of India is New Delhi.\"\n if \"pi\" in p or \"3.14\" in p:\n return \"Pi (π) is approximately 3.14159265358979.\"\n if \"speed of light\" in p:\n return \"The speed of light is approximately 299,792,458 metres per second.\"\n\n return \"\" # No pattern matched — caller will provide final fallback message\n\n\ndef query(prompt_text: str, data: str = \"\") -> str:\n \"\"\"\n Called from inside Saral programs via:\n ask ai \"...\" using variable and store in result\n\n Returns a plain string result.\n Fallback chain: DeepSeek → Gemini → Pattern → Helpful message.\n \"\"\"\n # Build the full prompt\n if data:\n full_prompt = f\"{prompt_text}\\n\\nContext data:\\n{_cap_text(data, 800)}\"\n else:\n full_prompt = _cap_text(prompt_text, 400)\n\n # Tier 1 + 2: AI\n try:\n result = _call_ai(full_prompt, silent=True)\n return _clean(result)\n except RuntimeError:\n pass\n\n # Tier 3: Pattern matching (always works)\n pattern_result = _pattern_query(prompt_text, data)\n if pattern_result:\n return pattern_result\n\n # Final fallback message\n return (\n \"AI is not available right now. \"\n \"Run: python saral.py --setup-ai to configure an AI provider.\"\n )\n\n\n# ══════════════════════════════════════════════════════════════════\n# AI STATUS CHECK\n# Called by: python saral.py --status\n# ══════════════════════════════════════════════════════════════════\n\ndef check_ai_status() -> dict:\n \"\"\"\n Check which AI is configured and whether it is reachable.\n Returns a status dict consumed by saral.py --status.\n\n REASONING:\n We ask ai_config.py for the active config (file or env var).\n We do NOT make a live test call here — that is done during\n --setup-ai. Status just reports what is saved.\n Live-testing on every --status call would waste API credits\n and slow the command down for no benefit.\n \"\"\"\n from ai_config import get_active_ai\n\n config = get_active_ai()\n\n return {\n \"configured\": config is not None,\n \"provider\": config[\"provider\"] if config else None,\n \"provider_name\": config.get(\"name\", \"Unknown\") if config else None,\n \"model\": config[\"model\"] if config else None,\n \"pattern\": True, # always available\n }\n"}}, "core/ai_config.py": {"baab98328c201fc21ab584f03efced55eaec297a": {"t": 1785674551.162719, "n": 23591, "h": "f3732d4489bfd130", "c": "\"\"\"\nai_config.py — Saral Universal AI Configuration v1.0\n═══════════════════════════════════════════════════════════════════\nPURPOSE\n This module is the \"AI registration desk\" for Saral Lang.\n The user pastes ANY API key from ANY supported AI provider.\n This module:\n 1. Auto-detects which provider the key belongs to\n 2. Tests live connectivity with a tiny real API call\n 3. Saves the key + provider to a local config file\n 4. Provides the key+provider to ai_helper.py at runtime\n\nWHY A SEPARATE FILE\n Keeping configuration separate from ai_helper.py means:\n - One place to add new AI providers in the future\n - ai_helper.py stays clean — it just asks \"who is configured?\"\n - The wizard (--setup-ai) imports only this file, not the full AI stack\n\nSUPPORTED PROVIDERS (auto-detected by key shape + live test)\n ┌─────────────────┬──────────────────────────────────────────────┐\n │ Provider │ Key Pattern │\n ├─────────────────┼──────────────────────────────────────────────┤\n │ xAI Grok │ xai-... (84+ chars) │\n │ OpenAI │ sk-proj-... or sk-... (51+ chars) │\n │ DeepSeek │ sk-... (32 chars, deepseek.com endpoint) │\n │ Anthropic │ sk-ant-... │\n │ Google Gemini │ AIza... (39 chars) │\n │ Groq │ gsk_... │\n │ Mistral │ mixed alphanumeric, mistral-like │\n │ Cohere │ long alphanumeric, no prefix │\n └─────────────────┴──────────────────────────────────────────────┘\n\nCONFIG FILE\n Stored at: /saral_ai.conf\n Format: plain text key=value pairs (no JSON, no binary)\n Never sent anywhere — stays on your machine.\n\nSECURITY NOTE\n The key is stored in plain text on your local disk.\n This is the same as how git credentials, npm tokens, and\n SSH keys are stored. Keep your saral folder private.\n Never share saral_ai.conf with anyone.\n═══════════════════════════════════════════════════════════════════\n\"\"\"\n\nimport json\nimport os\nimport re\nimport urllib.request\nimport urllib.error\nfrom typing import Optional\n\n# ══════════════════════════════════════════════════════════════════\n# CONFIG FILE LOCATION\n# Stored next to saral.py, not in /tmp — survives reboots\n# ══════════════════════════════════════════════════════════════════\n\n_THIS_DIR = os.path.dirname(os.path.abspath(__file__))\nCONFIG_FILE = os.path.join(_THIS_DIR, \"saral_ai.conf\")\n\n# ══════════════════════════════════════════════════════════════════\n# PROVIDER REGISTRY\n# Each entry describes one AI provider:\n# name — display name shown to the user\n# key_pattern — regex that matches this provider's key format\n# api_url — the endpoint we POST to\n# test_fn — function name (string) used to do a live test\n# model — default model to use\n# notes — shown during setup to help the user\n# ══════════════════════════════════════════════════════════════════\n\nPROVIDERS = {\n \"openai\": {\n \"name\": \"OpenAI (ChatGPT / GPT-4o)\",\n \"key_pattern\": r\"^sk-(?!ant-)(?!deepseek)[A-Za-z0-9\\-_]{40,}$\",\n \"api_url\": \"https://api.openai.com/v1/chat/completions\",\n \"model\": \"gpt-4o-mini\", # cheapest GPT-4 class model\n \"notes\": \"Get key at: https://platform.openai.com/api-keys\",\n },\n \"deepseek\": {\n \"name\": \"DeepSeek R1 (Recommended — excellent reasoning)\",\n \"key_pattern\": r\"^sk-[a-f0-9]{32}$\",\n \"api_url\": \"https://api.deepseek.com/v1/chat/completions\",\n \"model\": \"deepseek-reasoner\",\n \"notes\": \"Get free key at: https://platform.deepseek.com\",\n },\n \"anthropic\": {\n \"name\": \"Anthropic Claude\",\n \"key_pattern\": r\"^sk-ant-[A-Za-z0-9\\-_]{80,}$\",\n \"api_url\": \"https://api.anthropic.com/v1/messages\",\n \"model\": \"claude-haiku-4-5-20251001\", # fastest, cheapest Claude\n \"notes\": \"Get key at: https://console.anthropic.com\",\n },\n \"gemini\": {\n \"name\": \"Google Gemini (Free tier available)\",\n \"key_pattern\": r\"^AIza[A-Za-z0-9\\-_]{35,}$\",\n \"api_url\": \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent\",\n \"model\": \"gemini-2.0-flash\",\n \"notes\": \"Get free key at: https://aistudio.google.com\",\n },\n \"groq\": {\n \"name\": \"Groq (Very fast inference)\",\n \"key_pattern\": r\"^gsk_[A-Za-z0-9_\\-]{40,}$\",\n \"api_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n \"model\": \"llama-3.3-70b-versatile\",\n \"notes\": \"Get free key at: https://console.groq.com\",\n },\n \"xai\": {\n \"name\": \"xAI Grok\",\n \"key_pattern\": r\"^xai-[A-Za-z0-9]{80,}$\",\n \"api_url\": \"https://api.x.ai/v1/chat/completions\",\n \"model\": \"grok-3\",\n \"notes\": \"Get key at: https://console.x.ai\",\n },\n \"mistral\": {\n \"name\": \"Mistral AI\",\n \"key_pattern\": r\"^[A-Za-z0-9]{32}$\",\n \"api_url\": \"https://api.mistral.ai/v1/chat/completions\",\n \"model\": \"mistral-small-latest\",\n \"notes\": \"Get key at: https://console.mistral.ai\",\n },\n \"cohere\": {\n \"name\": \"Cohere\",\n \"key_pattern\": r\"^[A-Za-z0-9]{40}$\",\n \"api_url\": \"https://api.cohere.ai/v1/generate\",\n \"model\": \"command\",\n \"notes\": \"Get key at: https://dashboard.cohere.com\",\n },\n}\n\n# Detection order matters — check most specific patterns first\n# (DeepSeek before OpenAI because both start with sk-)\n_DETECTION_ORDER = [\n \"deepseek\", \"anthropic\", \"xai\", \"groq\", \"gemini\",\n \"openai\", \"mistral\", \"cohere\"\n]\n\n# ══════════════════════════════════════════════════════════════════\n# KEY DETECTION\n# ══════════════════════════════════════════════════════════════════\n\ndef detect_provider(api_key: str) -> Optional[str]:\n \"\"\"\n Look at the API key's shape and return the provider name.\n\n REASONING PROCESS:\n Step 1 — Strip whitespace. Users often paste keys with a\n trailing newline or space from their clipboard.\n Step 2 — Try each provider's regex in detection order.\n Order matters: DeepSeek checked before OpenAI\n because both start with 'sk-'.\n Step 3 — Return the first match, or None if nothing matched.\n\n Returns: provider key string (e.g. \"deepseek\") or None.\n \"\"\"\n api_key = api_key.strip()\n\n for provider_id in _DETECTION_ORDER:\n pattern = PROVIDERS[provider_id][\"key_pattern\"]\n if re.match(pattern, api_key):\n return provider_id\n\n return None # Unknown key format\n\n\n# ══════════════════════════════════════════════════════════════════\n# LIVE CONNECTIVITY TESTS\n# One function per provider — each knows the exact API shape.\n# All return (success: bool, message: str)\n# ══════════════════════════════════════════════════════════════════\n\ndef _test_openai_compatible(api_key: str, api_url: str, model: str,\n provider_name: str, timeout: int = 15):\n \"\"\"\n Test any OpenAI-compatible endpoint (OpenAI, DeepSeek, Groq, Mistral).\n They all share the same /v1/chat/completions shape.\n \"\"\"\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": \"Say: OK\"}],\n \"max_tokens\": 5,\n \"stream\": False,\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n api_url,\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n \"User-Agent\": \"SaralLang/1.0\",\n },\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n choices = data.get(\"choices\", [])\n if choices:\n reply = choices[0].get(\"message\", {}).get(\"content\", \"\").strip()\n return True, f\"Connected ✓ Model replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:300]\n return False, f\"HTTP {e.code} from {api_url}\\n Details: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error reaching {api_url}: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef _test_anthropic(api_key: str, model: str, timeout: int = 15):\n \"\"\"Test Anthropic Claude API — uses Messages API shape (different from OpenAI).\"\"\"\n payload = json.dumps({\n \"model\": model,\n \"max_tokens\": 5,\n \"messages\": [{\"role\": \"user\", \"content\": \"Say: OK\"}],\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.anthropic.com/v1/messages\",\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": api_key,\n \"anthropic-version\": \"2023-06-01\",\n },\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n content = data.get(\"content\", [])\n if content:\n reply = content[0].get(\"text\", \"\").strip()\n return True, f\"Connected ✓ Claude replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n return False, f\"HTTP {e.code} error: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef _test_gemini(api_key: str, model: str, timeout: int = 15):\n \"\"\"Test Google Gemini API — uses its own payload shape + key-in-URL pattern.\"\"\"\n url = (\n f\"https://generativelanguage.googleapis.com/v1beta/models/\"\n f\"{model}:generateContent?key={api_key}\"\n )\n payload = json.dumps({\n \"contents\": [{\"parts\": [{\"text\": \"Say: OK\"}]}],\n \"generationConfig\": {\"maxOutputTokens\": 5},\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n url,\n data = payload,\n headers = {\"Content-Type\": \"application/json\"},\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n candidates = data.get(\"candidates\", [])\n if candidates:\n reply = (\n candidates[0]\n .get(\"content\", {})\n .get(\"parts\", [{}])[0]\n .get(\"text\", \"\")\n .strip()\n )\n return True, f\"Connected ✓ Gemini replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n return False, f\"HTTP {e.code} error: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef _test_cohere(api_key: str, timeout: int = 15):\n \"\"\"Test Cohere API — uses its own /generate endpoint shape.\"\"\"\n payload = json.dumps({\n \"model\": \"command\",\n \"prompt\": \"Say: OK\",\n \"max_tokens\": 5,\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.cohere.ai/v1/generate\",\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n generations = data.get(\"generations\", [])\n if generations:\n reply = generations[0].get(\"text\", \"\").strip()\n return True, f\"Connected ✓ Cohere replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n return False, f\"HTTP {e.code} error: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef test_connectivity(api_key: str, provider_id: str) -> tuple:\n \"\"\"\n Run a live test call for the given provider.\n Returns (success: bool, message: str).\n\n REASONING:\n We make a real API call with max_tokens=5.\n This is the smallest possible call that proves:\n - The key is valid (auth works)\n - The network can reach the provider\n - The model is available\n 5 tokens costs fractions of a cent on every platform.\n \"\"\"\n api_key = api_key.strip()\n provider = PROVIDERS.get(provider_id)\n if not provider:\n return False, f\"Unknown provider: {provider_id}\"\n\n model = provider[\"model\"]\n api_url = provider[\"api_url\"]\n\n # Route to the correct test function\n if provider_id in (\"openai\", \"deepseek\", \"groq\", \"mistral\", \"xai\"):\n return _test_openai_compatible(\n api_key, api_url, model, provider[\"name\"]\n )\n elif provider_id == \"anthropic\":\n return _test_anthropic(api_key, model)\n elif provider_id == \"gemini\":\n return _test_gemini(api_key, model)\n elif provider_id == \"cohere\":\n return _test_cohere(api_key)\n else:\n return False, f\"No test function for provider: {provider_id}\"\n\n\n# ══════════════════════════════════════════════════════════════════\n# CONFIG FILE — SAVE / LOAD / CLEAR\n# ══════════════════════════════════════════════════════════════════\n\ndef save_config(api_key: str, provider_id: str) -> None:\n \"\"\"\n Save the API key and provider to saral_ai.conf.\n\n File format (plain text, easy to read/edit/delete):\n provider=deepseek\n key=sk-abc123...\n model=deepseek-reasoner\n\n WHY NOT JSON or .env?\n Plain text with = pairs is human-readable, easy to open\n in any text editor, and requires no parsing library.\n A user can delete it with one keypress if needed.\n \"\"\"\n provider = PROVIDERS[provider_id]\n lines = [\n \"# Saral Lang AI Configuration\",\n \"# Generated by: python saral.py --setup-ai\",\n \"# To remove AI: delete this file or run --setup-ai again\",\n \"# KEEP THIS FILE PRIVATE — do not share it\",\n f\"provider={provider_id}\",\n f\"name={provider['name']}\",\n f\"key={api_key.strip()}\",\n f\"model={provider['model']}\",\n f\"api_url={provider['api_url']}\",\n ]\n with open(CONFIG_FILE, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(lines) + \"\\n\")\n\n\ndef load_config() -> Optional[dict]:\n \"\"\"\n Load the saved AI config from saral_ai.conf.\n Returns a dict with provider, key, model, api_url — or None if not configured.\n\n REASONING:\n We read at call time, not at import time.\n This means if the user sets up AI in one terminal session,\n the next session picks it up automatically without restarting.\n \"\"\"\n if not os.path.exists(CONFIG_FILE):\n return None\n\n config = {}\n try:\n with open(CONFIG_FILE, \"r\", encoding=\"utf-8\") as f:\n for line in f:\n line = line.strip()\n if line.startswith(\"#\") or \"=\" not in line:\n continue\n key, _, value = line.partition(\"=\")\n config[key.strip()] = value.strip()\n except Exception:\n return None\n\n # Validate that we have the minimum required fields\n required = {\"provider\", \"key\", \"model\", \"api_url\"}\n if not required.issubset(config.keys()):\n return None\n\n return config\n\n\ndef clear_config() -> bool:\n \"\"\"Delete the config file. Returns True if deleted, False if not found.\"\"\"\n if os.path.exists(CONFIG_FILE):\n os.remove(CONFIG_FILE)\n return True\n return False\n\n\ndef is_configured() -> bool:\n \"\"\"Quick check — is any AI configured?\"\"\"\n return load_config() is not None\n\n\n# ══════════════════════════════════════════════════════════════════\n# THE SETUP WIZARD\n# Called by: python saral.py --setup-ai\n# Walks the user through pasting a key, detects provider,\n# tests connectivity, and saves on success.\n# ══════════════════════════════════════════════════════════════════\n\ndef run_setup_wizard() -> bool:\n \"\"\"\n Interactive wizard for setting up an AI provider.\n Returns True if setup succeeded, False if user quit or test failed.\n\n DESIGN PHILOSOPHY (Saral's Kerala-audience approach):\n - Speak to the user like a helpful teacher, not a technical manual.\n - Show exactly what is happening at each step.\n - Never fail silently — always explain WHY something failed.\n - The user should feel safe, not judged.\n \"\"\"\n print()\n print(\"╔══════════════════════════════════════════════════════╗\")\n print(\"║ 🤖 Saral AI Setup Wizard ║\")\n print(\"╚══════════════════════════════════════════════════════╝\")\n print()\n print(\" This wizard will connect Saral to an AI of your choice.\")\n print(\" You can use any of these providers:\")\n print()\n\n for i, pid in enumerate(_DETECTION_ORDER, 1):\n p = PROVIDERS[pid]\n print(f\" {i}. {p['name']}\")\n print(f\" {p['notes']}\")\n print()\n\n # Show current config if one exists\n current = load_config()\n if current:\n print(f\" ℹ️ Currently configured: {current.get('name', current['provider'])}\")\n print(\" Setting up a new key will replace it.\")\n print()\n\n print(\"─\" * 56)\n print()\n print(\" Paste your API key below and press Enter.\")\n print(\" (The key will not be shown on screen for security.)\")\n print()\n\n # Read the key using regular input so paste (Ctrl+V) works normally\n print(\" (Your key will be visible as you type — this is fine on your own machine)\")\n api_key = input(\" Your API key: \")\n\n # Remove invisible/control characters (BOM, terminal escape sequences, etc.)\n # that can silently corrupt a pasted key and cause 401 errors\n api_key = ''.join(c for c in api_key if c.isprintable() and ord(c) < 128).strip()\n\n if not api_key:\n print(\"\\n ❌ No key entered. Setup cancelled.\\n\")\n return False\n\n # Show a preview so the user can verify the key was captured correctly\n if len(api_key) >= 12:\n preview = f\"{api_key[:8]}...{api_key[-4:]}\"\n else:\n preview = api_key[:4] + \"...\" if len(api_key) > 4 else \"(very short — may be wrong)\"\n print(f\"\\n 📋 Key received: {preview} ({len(api_key)} characters)\")\n\n if len(api_key) < 10:\n print(\" ⚠️ Key looks too short — please check and try again.\")\n print()\n return False\n print()\n\n print()\n print(\" ⏳ Step 1 of 3 — Identifying your AI provider...\")\n\n # ── Step 1: Detect provider ───────────────────────────────────\n provider_id = detect_provider(api_key)\n\n if provider_id is None:\n print()\n print(\" ⚠️ Could not identify the provider from this key's format.\")\n print()\n print(\" Please choose your provider manually:\")\n print()\n for i, pid in enumerate(_DETECTION_ORDER, 1):\n print(f\" {i}. {PROVIDERS[pid]['name']}\")\n print()\n\n try:\n choice = input(\" Enter number (or press Enter to cancel): \").strip()\n except (KeyboardInterrupt, EOFError):\n print(\"\\n Setup cancelled.\\n\")\n return False\n\n if not choice.isdigit() or not (1 <= int(choice) <= len(_DETECTION_ORDER)):\n print(\"\\n ❌ Invalid choice. Setup cancelled.\\n\")\n return False\n\n provider_id = _DETECTION_ORDER[int(choice) - 1]\n\n provider_name = PROVIDERS[provider_id][\"name\"]\n print(f\" ✅ Detected: {provider_name}\")\n print()\n\n # ── Step 2: Live connectivity test ────────────────────────────\n print(\" ⏳ Step 2 of 3 — Testing live connection...\")\n print(f\" Sending a tiny test message to {provider_name}...\")\n print()\n\n success, message = test_connectivity(api_key, provider_id)\n\n if not success:\n print(f\" ❌ Connection test failed.\")\n print(f\" Reason: {message}\")\n print()\n print(\" Possible causes:\")\n print(\" • The key was copied incorrectly — try again\")\n print(\" • Your internet connection is down\")\n print(\" • The key has expired or was revoked\")\n print(\" • You have no credits left on this provider\")\n print()\n print(\" Key NOT saved. Run --setup-ai again when ready.\")\n print()\n return False\n\n print(f\" ✅ Test passed! {message}\")\n print()\n\n # ── Step 3: Save config ───────────────────────────────────────\n print(\" ⏳ Step 3 of 3 — Saving configuration...\")\n save_config(api_key, provider_id)\n print(f\" ✅ Saved to: {CONFIG_FILE}\")\n print()\n print(\"╔══════════════════════════════════════════════════════╗\")\n print(\"║ 🎉 AI setup complete! ║\")\n print(\"╚══════════════════════════════════════════════════════╝\")\n print()\n print(f\" Your AI: {provider_name}\")\n print(f\" Model: {PROVIDERS[provider_id]['model']}\")\n print()\n print(\" You can now use:\")\n print(\" python saral.py --generate (AI writes Saral code for you)\")\n print(\" python saral.py --explain (AI explains your code)\")\n print(\" python saral.py --status (check AI status anytime)\")\n print()\n print(\" To change AI later, run: python saral.py --setup-ai\")\n print()\n return True\n\n\n# ══════════════════════════════════════════════════════════════════\n# HELPER: get active config for ai_helper.py\n# ══════════════════════════════════════════════════════════════════\n\ndef get_active_ai() -> Optional[dict]:\n \"\"\"\n Called by ai_helper.py to get the configured provider.\n Returns the config dict (with key, model, api_url, provider) or None.\n\n Priority order:\n 1. saral_ai.conf (user set up via --setup-ai)\n 2. Environment variables (power users / CI pipelines)\n SARAL_DEEPSEEK_KEY, SARAL_GEMINI_KEY (backward compat)\n \"\"\"\n # Priority 1: config file\n config = load_config()\n if config:\n return config\n\n # Priority 2: environment variables (backward compatibility)\n deepseek_key = os.environ.get(\"SARAL_DEEPSEEK_KEY\", \"\").strip()\n if deepseek_key:\n p = PROVIDERS[\"deepseek\"]\n return {\n \"provider\": \"deepseek\",\n \"name\": p[\"name\"],\n \"key\": deepseek_key,\n \"model\": p[\"model\"],\n \"api_url\": p[\"api_url\"],\n }\n\n gemini_key = os.environ.get(\"SARAL_GEMINI_KEY\", \"\").strip()\n if gemini_key:\n p = PROVIDERS[\"gemini\"]\n return {\n \"provider\": \"gemini\",\n \"name\": p[\"name\"],\n \"key\": gemini_key,\n \"model\": p[\"model\"],\n \"api_url\": p[\"api_url\"],\n }\n\n return None # No AI configured\n"}, "52e2ff7dc6a9ee88c58143677d7954d99a979bcb": {"t": 1785773473.3858452, "n": 23591, "h": "f3732d4489bfd130", "c": "\"\"\"\nai_config.py — Saral Universal AI Configuration v1.0\n═══════════════════════════════════════════════════════════════════\nPURPOSE\n This module is the \"AI registration desk\" for Saral Lang.\n The user pastes ANY API key from ANY supported AI provider.\n This module:\n 1. Auto-detects which provider the key belongs to\n 2. Tests live connectivity with a tiny real API call\n 3. Saves the key + provider to a local config file\n 4. Provides the key+provider to ai_helper.py at runtime\n\nWHY A SEPARATE FILE\n Keeping configuration separate from ai_helper.py means:\n - One place to add new AI providers in the future\n - ai_helper.py stays clean — it just asks \"who is configured?\"\n - The wizard (--setup-ai) imports only this file, not the full AI stack\n\nSUPPORTED PROVIDERS (auto-detected by key shape + live test)\n ┌─────────────────┬──────────────────────────────────────────────┐\n │ Provider │ Key Pattern │\n ├─────────────────┼──────────────────────────────────────────────┤\n │ xAI Grok │ xai-... (84+ chars) │\n │ OpenAI │ sk-proj-... or sk-... (51+ chars) │\n │ DeepSeek │ sk-... (32 chars, deepseek.com endpoint) │\n │ Anthropic │ sk-ant-... │\n │ Google Gemini │ AIza... (39 chars) │\n │ Groq │ gsk_... │\n │ Mistral │ mixed alphanumeric, mistral-like │\n │ Cohere │ long alphanumeric, no prefix │\n └─────────────────┴──────────────────────────────────────────────┘\n\nCONFIG FILE\n Stored at: /saral_ai.conf\n Format: plain text key=value pairs (no JSON, no binary)\n Never sent anywhere — stays on your machine.\n\nSECURITY NOTE\n The key is stored in plain text on your local disk.\n This is the same as how git credentials, npm tokens, and\n SSH keys are stored. Keep your saral folder private.\n Never share saral_ai.conf with anyone.\n═══════════════════════════════════════════════════════════════════\n\"\"\"\n\nimport json\nimport os\nimport re\nimport urllib.request\nimport urllib.error\nfrom typing import Optional\n\n# ══════════════════════════════════════════════════════════════════\n# CONFIG FILE LOCATION\n# Stored next to saral.py, not in /tmp — survives reboots\n# ══════════════════════════════════════════════════════════════════\n\n_THIS_DIR = os.path.dirname(os.path.abspath(__file__))\nCONFIG_FILE = os.path.join(_THIS_DIR, \"saral_ai.conf\")\n\n# ══════════════════════════════════════════════════════════════════\n# PROVIDER REGISTRY\n# Each entry describes one AI provider:\n# name — display name shown to the user\n# key_pattern — regex that matches this provider's key format\n# api_url — the endpoint we POST to\n# test_fn — function name (string) used to do a live test\n# model — default model to use\n# notes — shown during setup to help the user\n# ══════════════════════════════════════════════════════════════════\n\nPROVIDERS = {\n \"openai\": {\n \"name\": \"OpenAI (ChatGPT / GPT-4o)\",\n \"key_pattern\": r\"^sk-(?!ant-)(?!deepseek)[A-Za-z0-9\\-_]{40,}$\",\n \"api_url\": \"https://api.openai.com/v1/chat/completions\",\n \"model\": \"gpt-4o-mini\", # cheapest GPT-4 class model\n \"notes\": \"Get key at: https://platform.openai.com/api-keys\",\n },\n \"deepseek\": {\n \"name\": \"DeepSeek R1 (Recommended — excellent reasoning)\",\n \"key_pattern\": r\"^sk-[a-f0-9]{32}$\",\n \"api_url\": \"https://api.deepseek.com/v1/chat/completions\",\n \"model\": \"deepseek-reasoner\",\n \"notes\": \"Get free key at: https://platform.deepseek.com\",\n },\n \"anthropic\": {\n \"name\": \"Anthropic Claude\",\n \"key_pattern\": r\"^sk-ant-[A-Za-z0-9\\-_]{80,}$\",\n \"api_url\": \"https://api.anthropic.com/v1/messages\",\n \"model\": \"claude-haiku-4-5-20251001\", # fastest, cheapest Claude\n \"notes\": \"Get key at: https://console.anthropic.com\",\n },\n \"gemini\": {\n \"name\": \"Google Gemini (Free tier available)\",\n \"key_pattern\": r\"^AIza[A-Za-z0-9\\-_]{35,}$\",\n \"api_url\": \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent\",\n \"model\": \"gemini-2.0-flash\",\n \"notes\": \"Get free key at: https://aistudio.google.com\",\n },\n \"groq\": {\n \"name\": \"Groq (Very fast inference)\",\n \"key_pattern\": r\"^gsk_[A-Za-z0-9_\\-]{40,}$\",\n \"api_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n \"model\": \"llama-3.3-70b-versatile\",\n \"notes\": \"Get free key at: https://console.groq.com\",\n },\n \"xai\": {\n \"name\": \"xAI Grok\",\n \"key_pattern\": r\"^xai-[A-Za-z0-9]{80,}$\",\n \"api_url\": \"https://api.x.ai/v1/chat/completions\",\n \"model\": \"grok-3\",\n \"notes\": \"Get key at: https://console.x.ai\",\n },\n \"mistral\": {\n \"name\": \"Mistral AI\",\n \"key_pattern\": r\"^[A-Za-z0-9]{32}$\",\n \"api_url\": \"https://api.mistral.ai/v1/chat/completions\",\n \"model\": \"mistral-small-latest\",\n \"notes\": \"Get key at: https://console.mistral.ai\",\n },\n \"cohere\": {\n \"name\": \"Cohere\",\n \"key_pattern\": r\"^[A-Za-z0-9]{40}$\",\n \"api_url\": \"https://api.cohere.ai/v1/generate\",\n \"model\": \"command\",\n \"notes\": \"Get key at: https://dashboard.cohere.com\",\n },\n}\n\n# Detection order matters — check most specific patterns first\n# (DeepSeek before OpenAI because both start with sk-)\n_DETECTION_ORDER = [\n \"deepseek\", \"anthropic\", \"xai\", \"groq\", \"gemini\",\n \"openai\", \"mistral\", \"cohere\"\n]\n\n# ══════════════════════════════════════════════════════════════════\n# KEY DETECTION\n# ══════════════════════════════════════════════════════════════════\n\ndef detect_provider(api_key: str) -> Optional[str]:\n \"\"\"\n Look at the API key's shape and return the provider name.\n\n REASONING PROCESS:\n Step 1 — Strip whitespace. Users often paste keys with a\n trailing newline or space from their clipboard.\n Step 2 — Try each provider's regex in detection order.\n Order matters: DeepSeek checked before OpenAI\n because both start with 'sk-'.\n Step 3 — Return the first match, or None if nothing matched.\n\n Returns: provider key string (e.g. \"deepseek\") or None.\n \"\"\"\n api_key = api_key.strip()\n\n for provider_id in _DETECTION_ORDER:\n pattern = PROVIDERS[provider_id][\"key_pattern\"]\n if re.match(pattern, api_key):\n return provider_id\n\n return None # Unknown key format\n\n\n# ══════════════════════════════════════════════════════════════════\n# LIVE CONNECTIVITY TESTS\n# One function per provider — each knows the exact API shape.\n# All return (success: bool, message: str)\n# ══════════════════════════════════════════════════════════════════\n\ndef _test_openai_compatible(api_key: str, api_url: str, model: str,\n provider_name: str, timeout: int = 15):\n \"\"\"\n Test any OpenAI-compatible endpoint (OpenAI, DeepSeek, Groq, Mistral).\n They all share the same /v1/chat/completions shape.\n \"\"\"\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": \"Say: OK\"}],\n \"max_tokens\": 5,\n \"stream\": False,\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n api_url,\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n \"User-Agent\": \"SaralLang/1.0\",\n },\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n choices = data.get(\"choices\", [])\n if choices:\n reply = choices[0].get(\"message\", {}).get(\"content\", \"\").strip()\n return True, f\"Connected ✓ Model replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:300]\n return False, f\"HTTP {e.code} from {api_url}\\n Details: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error reaching {api_url}: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef _test_anthropic(api_key: str, model: str, timeout: int = 15):\n \"\"\"Test Anthropic Claude API — uses Messages API shape (different from OpenAI).\"\"\"\n payload = json.dumps({\n \"model\": model,\n \"max_tokens\": 5,\n \"messages\": [{\"role\": \"user\", \"content\": \"Say: OK\"}],\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.anthropic.com/v1/messages\",\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": api_key,\n \"anthropic-version\": \"2023-06-01\",\n },\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n content = data.get(\"content\", [])\n if content:\n reply = content[0].get(\"text\", \"\").strip()\n return True, f\"Connected ✓ Claude replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n return False, f\"HTTP {e.code} error: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef _test_gemini(api_key: str, model: str, timeout: int = 15):\n \"\"\"Test Google Gemini API — uses its own payload shape + key-in-URL pattern.\"\"\"\n url = (\n f\"https://generativelanguage.googleapis.com/v1beta/models/\"\n f\"{model}:generateContent?key={api_key}\"\n )\n payload = json.dumps({\n \"contents\": [{\"parts\": [{\"text\": \"Say: OK\"}]}],\n \"generationConfig\": {\"maxOutputTokens\": 5},\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n url,\n data = payload,\n headers = {\"Content-Type\": \"application/json\"},\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n candidates = data.get(\"candidates\", [])\n if candidates:\n reply = (\n candidates[0]\n .get(\"content\", {})\n .get(\"parts\", [{}])[0]\n .get(\"text\", \"\")\n .strip()\n )\n return True, f\"Connected ✓ Gemini replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n return False, f\"HTTP {e.code} error: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef _test_cohere(api_key: str, timeout: int = 15):\n \"\"\"Test Cohere API — uses its own /generate endpoint shape.\"\"\"\n payload = json.dumps({\n \"model\": \"command\",\n \"prompt\": \"Say: OK\",\n \"max_tokens\": 5,\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.cohere.ai/v1/generate\",\n data = payload,\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n method = \"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n generations = data.get(\"generations\", [])\n if generations:\n reply = generations[0].get(\"text\", \"\").strip()\n return True, f\"Connected ✓ Cohere replied: '{reply}'\"\n return False, \"Connected but received empty response.\"\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n return False, f\"HTTP {e.code} error: {body}\"\n except urllib.error.URLError as e:\n return False, f\"Network error: {e.reason}\"\n except Exception as e:\n return False, f\"Unexpected error: {e}\"\n\n\ndef test_connectivity(api_key: str, provider_id: str) -> tuple:\n \"\"\"\n Run a live test call for the given provider.\n Returns (success: bool, message: str).\n\n REASONING:\n We make a real API call with max_tokens=5.\n This is the smallest possible call that proves:\n - The key is valid (auth works)\n - The network can reach the provider\n - The model is available\n 5 tokens costs fractions of a cent on every platform.\n \"\"\"\n api_key = api_key.strip()\n provider = PROVIDERS.get(provider_id)\n if not provider:\n return False, f\"Unknown provider: {provider_id}\"\n\n model = provider[\"model\"]\n api_url = provider[\"api_url\"]\n\n # Route to the correct test function\n if provider_id in (\"openai\", \"deepseek\", \"groq\", \"mistral\", \"xai\"):\n return _test_openai_compatible(\n api_key, api_url, model, provider[\"name\"]\n )\n elif provider_id == \"anthropic\":\n return _test_anthropic(api_key, model)\n elif provider_id == \"gemini\":\n return _test_gemini(api_key, model)\n elif provider_id == \"cohere\":\n return _test_cohere(api_key)\n else:\n return False, f\"No test function for provider: {provider_id}\"\n\n\n# ══════════════════════════════════════════════════════════════════\n# CONFIG FILE — SAVE / LOAD / CLEAR\n# ══════════════════════════════════════════════════════════════════\n\ndef save_config(api_key: str, provider_id: str) -> None:\n \"\"\"\n Save the API key and provider to saral_ai.conf.\n\n File format (plain text, easy to read/edit/delete):\n provider=deepseek\n key=sk-abc123...\n model=deepseek-reasoner\n\n WHY NOT JSON or .env?\n Plain text with = pairs is human-readable, easy to open\n in any text editor, and requires no parsing library.\n A user can delete it with one keypress if needed.\n \"\"\"\n provider = PROVIDERS[provider_id]\n lines = [\n \"# Saral Lang AI Configuration\",\n \"# Generated by: python saral.py --setup-ai\",\n \"# To remove AI: delete this file or run --setup-ai again\",\n \"# KEEP THIS FILE PRIVATE — do not share it\",\n f\"provider={provider_id}\",\n f\"name={provider['name']}\",\n f\"key={api_key.strip()}\",\n f\"model={provider['model']}\",\n f\"api_url={provider['api_url']}\",\n ]\n with open(CONFIG_FILE, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(lines) + \"\\n\")\n\n\ndef load_config() -> Optional[dict]:\n \"\"\"\n Load the saved AI config from saral_ai.conf.\n Returns a dict with provider, key, model, api_url — or None if not configured.\n\n REASONING:\n We read at call time, not at import time.\n This means if the user sets up AI in one terminal session,\n the next session picks it up automatically without restarting.\n \"\"\"\n if not os.path.exists(CONFIG_FILE):\n return None\n\n config = {}\n try:\n with open(CONFIG_FILE, \"r\", encoding=\"utf-8\") as f:\n for line in f:\n line = line.strip()\n if line.startswith(\"#\") or \"=\" not in line:\n continue\n key, _, value = line.partition(\"=\")\n config[key.strip()] = value.strip()\n except Exception:\n return None\n\n # Validate that we have the minimum required fields\n required = {\"provider\", \"key\", \"model\", \"api_url\"}\n if not required.issubset(config.keys()):\n return None\n\n return config\n\n\ndef clear_config() -> bool:\n \"\"\"Delete the config file. Returns True if deleted, False if not found.\"\"\"\n if os.path.exists(CONFIG_FILE):\n os.remove(CONFIG_FILE)\n return True\n return False\n\n\ndef is_configured() -> bool:\n \"\"\"Quick check — is any AI configured?\"\"\"\n return load_config() is not None\n\n\n# ══════════════════════════════════════════════════════════════════\n# THE SETUP WIZARD\n# Called by: python saral.py --setup-ai\n# Walks the user through pasting a key, detects provider,\n# tests connectivity, and saves on success.\n# ══════════════════════════════════════════════════════════════════\n\ndef run_setup_wizard() -> bool:\n \"\"\"\n Interactive wizard for setting up an AI provider.\n Returns True if setup succeeded, False if user quit or test failed.\n\n DESIGN PHILOSOPHY (Saral's Kerala-audience approach):\n - Speak to the user like a helpful teacher, not a technical manual.\n - Show exactly what is happening at each step.\n - Never fail silently — always explain WHY something failed.\n - The user should feel safe, not judged.\n \"\"\"\n print()\n print(\"╔══════════════════════════════════════════════════════╗\")\n print(\"║ 🤖 Saral AI Setup Wizard ║\")\n print(\"╚══════════════════════════════════════════════════════╝\")\n print()\n print(\" This wizard will connect Saral to an AI of your choice.\")\n print(\" You can use any of these providers:\")\n print()\n\n for i, pid in enumerate(_DETECTION_ORDER, 1):\n p = PROVIDERS[pid]\n print(f\" {i}. {p['name']}\")\n print(f\" {p['notes']}\")\n print()\n\n # Show current config if one exists\n current = load_config()\n if current:\n print(f\" ℹ️ Currently configured: {current.get('name', current['provider'])}\")\n print(\" Setting up a new key will replace it.\")\n print()\n\n print(\"─\" * 56)\n print()\n print(\" Paste your API key below and press Enter.\")\n print(\" (The key will not be shown on screen for security.)\")\n print()\n\n # Read the key using regular input so paste (Ctrl+V) works normally\n print(\" (Your key will be visible as you type — this is fine on your own machine)\")\n api_key = input(\" Your API key: \")\n\n # Remove invisible/control characters (BOM, terminal escape sequences, etc.)\n # that can silently corrupt a pasted key and cause 401 errors\n api_key = ''.join(c for c in api_key if c.isprintable() and ord(c) < 128).strip()\n\n if not api_key:\n print(\"\\n ❌ No key entered. Setup cancelled.\\n\")\n return False\n\n # Show a preview so the user can verify the key was captured correctly\n if len(api_key) >= 12:\n preview = f\"{api_key[:8]}...{api_key[-4:]}\"\n else:\n preview = api_key[:4] + \"...\" if len(api_key) > 4 else \"(very short — may be wrong)\"\n print(f\"\\n 📋 Key received: {preview} ({len(api_key)} characters)\")\n\n if len(api_key) < 10:\n print(\" ⚠️ Key looks too short — please check and try again.\")\n print()\n return False\n print()\n\n print()\n print(\" ⏳ Step 1 of 3 — Identifying your AI provider...\")\n\n # ── Step 1: Detect provider ───────────────────────────────────\n provider_id = detect_provider(api_key)\n\n if provider_id is None:\n print()\n print(\" ⚠️ Could not identify the provider from this key's format.\")\n print()\n print(\" Please choose your provider manually:\")\n print()\n for i, pid in enumerate(_DETECTION_ORDER, 1):\n print(f\" {i}. {PROVIDERS[pid]['name']}\")\n print()\n\n try:\n choice = input(\" Enter number (or press Enter to cancel): \").strip()\n except (KeyboardInterrupt, EOFError):\n print(\"\\n Setup cancelled.\\n\")\n return False\n\n if not choice.isdigit() or not (1 <= int(choice) <= len(_DETECTION_ORDER)):\n print(\"\\n ❌ Invalid choice. Setup cancelled.\\n\")\n return False\n\n provider_id = _DETECTION_ORDER[int(choice) - 1]\n\n provider_name = PROVIDERS[provider_id][\"name\"]\n print(f\" ✅ Detected: {provider_name}\")\n print()\n\n # ── Step 2: Live connectivity test ────────────────────────────\n print(\" ⏳ Step 2 of 3 — Testing live connection...\")\n print(f\" Sending a tiny test message to {provider_name}...\")\n print()\n\n success, message = test_connectivity(api_key, provider_id)\n\n if not success:\n print(f\" ❌ Connection test failed.\")\n print(f\" Reason: {message}\")\n print()\n print(\" Possible causes:\")\n print(\" • The key was copied incorrectly — try again\")\n print(\" • Your internet connection is down\")\n print(\" • The key has expired or was revoked\")\n print(\" • You have no credits left on this provider\")\n print()\n print(\" Key NOT saved. Run --setup-ai again when ready.\")\n print()\n return False\n\n print(f\" ✅ Test passed! {message}\")\n print()\n\n # ── Step 3: Save config ───────────────────────────────────────\n print(\" ⏳ Step 3 of 3 — Saving configuration...\")\n save_config(api_key, provider_id)\n print(f\" ✅ Saved to: {CONFIG_FILE}\")\n print()\n print(\"╔══════════════════════════════════════════════════════╗\")\n print(\"║ 🎉 AI setup complete! ║\")\n print(\"╚══════════════════════════════════════════════════════╝\")\n print()\n print(f\" Your AI: {provider_name}\")\n print(f\" Model: {PROVIDERS[provider_id]['model']}\")\n print()\n print(\" You can now use:\")\n print(\" python saral.py --generate (AI writes Saral code for you)\")\n print(\" python saral.py --explain (AI explains your code)\")\n print(\" python saral.py --status (check AI status anytime)\")\n print()\n print(\" To change AI later, run: python saral.py --setup-ai\")\n print()\n return True\n\n\n# ══════════════════════════════════════════════════════════════════\n# HELPER: get active config for ai_helper.py\n# ══════════════════════════════════════════════════════════════════\n\ndef get_active_ai() -> Optional[dict]:\n \"\"\"\n Called by ai_helper.py to get the configured provider.\n Returns the config dict (with key, model, api_url, provider) or None.\n\n Priority order:\n 1. saral_ai.conf (user set up via --setup-ai)\n 2. Environment variables (power users / CI pipelines)\n SARAL_DEEPSEEK_KEY, SARAL_GEMINI_KEY (backward compat)\n \"\"\"\n # Priority 1: config file\n config = load_config()\n if config:\n return config\n\n # Priority 2: environment variables (backward compatibility)\n deepseek_key = os.environ.get(\"SARAL_DEEPSEEK_KEY\", \"\").strip()\n if deepseek_key:\n p = PROVIDERS[\"deepseek\"]\n return {\n \"provider\": \"deepseek\",\n \"name\": p[\"name\"],\n \"key\": deepseek_key,\n \"model\": p[\"model\"],\n \"api_url\": p[\"api_url\"],\n }\n\n gemini_key = os.environ.get(\"SARAL_GEMINI_KEY\", \"\").strip()\n if gemini_key:\n p = PROVIDERS[\"gemini\"]\n return {\n \"provider\": \"gemini\",\n \"name\": p[\"name\"],\n \"key\": gemini_key,\n \"model\": p[\"model\"],\n \"api_url\": p[\"api_url\"],\n }\n\n return None # No AI configured\n"}, "cf778221e9e023ae530adbd21ccbee560c4a7139": {"t": 1785773474.6118145, "n": 83, "h": "a040cfd973c4234f", "c": "# Saral AI configuration module\n# Handles provider detection and API key management"}}}}