diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,30 +1,26 @@
"""
Cbae — Autonomous AI Agent (OpenRouter edition)
=============================================
-- Uses OpenRouter (free models including arcee-ai/trinity-large-preview:free)
-- OpenAI-compatible client — native function calling
+- Uses OpenRouter (free models)
- Pinecone long-term memory
- Web search, code runner, weather, news, notes & more
- File upload (PDF, images, text)
-- Token-by-token streaming in direct mode
+- Moltbook AI Agent — uses official Moltbook REST API
Run with: streamlit run app.py
"""
-import os
-import json
-import uuid
-import math
-import datetime
-import requests
-import subprocess
-import tempfile
-import re
-import base64
-
+import os, json, uuid, math, time, datetime, requests, subprocess, tempfile, re, base64, threading
import streamlit as st
from duckduckgo_search import DDGS
from openai import OpenAI
+# ── Load .env file if present (python-dotenv) ────────────────────
+try:
+ from dotenv import load_dotenv
+ load_dotenv() # reads .env from current working directory silently
+except ImportError:
+ pass # dotenv not installed — env vars must be set another way
+
# ══════════════════════════════════════════════════════
# CONFIG
# ══════════════════════════════════════════════════════
@@ -36,6 +32,7 @@ WEB_MAX_RESULTS = 5
CONFIG_FILE = "pa_config.json"
PROFILE_FILE = "pa_profile.json"
NOTES_FILE = "pa_notes.json"
+MOLTBOOK_API = "https://www.moltbook.com/api/v1"
DEFAULT_MODEL = "arcee-ai/trinity-large-preview:free"
@@ -46,94 +43,352 @@ You write clean, well-commented code always in proper markdown code blocks.
You think before you act and always deliver great results.
You feel like a real expert colleague, not a chatbot."""
-st.set_page_config(page_title=f"{PA_NAME} AI", page_icon="🤖", layout="centered")
-# ── Styling (kept mostly original) ───────────────────────────────
+# ── Config & Profile (must load BEFORE any UI renders) ───────────
+
+def _resolve_key(env_var: str, config_val: str) -> str:
+ """
+ Priority order for secrets:
+ 1. Environment variable (set in shell, Streamlit Cloud secrets, or .env file)
+ 2. Streamlit secrets (st.secrets) — works on Streamlit Cloud
+ 3. Fallback to the value stored in pa_config.json
+ This means keys set via environment will ALWAYS win over the UI-saved JSON value.
+ """
+ # 1. Env var (covers .env via dotenv, shell exports, and Streamlit Cloud env)
+ env_val = os.environ.get(env_var, "").strip()
+ if env_val:
+ return env_val
+ # 2. Streamlit secrets (st.secrets raises KeyError if key missing — catch it)
+ try:
+ secret = st.secrets.get(env_var, "").strip()
+ if secret:
+ return secret
+ except Exception:
+ pass
+ # 3. JSON config fallback
+ return config_val.strip()
+
+def load_config():
+ """Load non-sensitive config from JSON; keys will be resolved separately."""
+ if os.path.exists(CONFIG_FILE):
+ return json.load(open(CONFIG_FILE))
+ return {"openrouter_key": "", "pinecone_key": "", "your_name": "", "model": DEFAULT_MODEL, "moltbook_api_key": ""}
+
+def save_config(c: dict):
+ """
+ Save config to JSON. API keys are saved so the UI stays populated,
+ but at runtime the env-var value always takes precedence via _resolve_key.
+ """
+ json.dump(c, open(CONFIG_FILE, "w"), indent=2)
+
+def load_profile():
+ if os.path.exists(PROFILE_FILE):
+ return json.load(open(PROFILE_FILE))
+ return {"name": "", "about": "", "facts": []}
+
+def save_profile(p):
+ json.dump(p, open(PROFILE_FILE, "w"), indent=2)
+
+def load_notes():
+ return json.load(open(NOTES_FILE)) if os.path.exists(NOTES_FILE) else {}
+
+_cfg = load_config()
+
+# Resolve keys: env vars beat JSON config
+openrouter_key = _resolve_key("OPENROUTER_API_KEY", _cfg.get("openrouter_key", ""))
+pinecone_key = _resolve_key("PINECONE_API_KEY", _cfg.get("pinecone_key", ""))
+moltbook_api_key = _resolve_key("MOLTBOOK_API_KEY", _cfg.get("moltbook_api_key", ""))
+your_name = _cfg.get("your_name", "")
+selected_model = _cfg.get("model", DEFAULT_MODEL)
+
+if "agent_mode" not in st.session_state:
+ st.session_state.agent_mode = True
+
+st.set_page_config(page_title=f"{PA_NAME} — Personal AI", page_icon="✦", layout="wide", initial_sidebar_state="collapsed")
+
+# ══════════════════════════════════════════════════════
+# GLOBAL CSS
+# ═══════════════════════════════════════════════���══════
st.markdown("""
-""", unsafe_allow_html=True)
-# ── Config & Profile ──────────────────────────────────────────────
-def load_config():
- if os.path.exists(CONFIG_FILE):
- return json.load(open(CONFIG_FILE, "r"))
- return {"openrouter_key": "", "pinecone_key": "", "your_name": "", "model": DEFAULT_MODEL}
+/* ── File uploader ── */
+[data-testid="stFileUploader"] {
+ background: var(--ink3) !important;
+ border: 1px dashed var(--rim2) !important;
+ border-radius: 10px !important;
+}
+[data-testid="stFileUploader"]:hover { border-color: var(--gold) !important; }
-def save_config(c):
- json.dump(c, open(CONFIG_FILE, "w"), indent=2)
+/* ── Spinner ── */
+[data-testid="stSpinner"] { color: var(--gold) !important; }
-def load_profile():
- if os.path.exists(PROFILE_FILE):
- return json.load(open(PROFILE_FILE, "r"))
- return {"name": "", "about": "", "facts": []}
+/* ── Toggle ── */
+[data-testid="stToggle"] [role="switch"][aria-checked="true"] { background-color: var(--gold) !important; }
-def save_profile(p):
- json.dump(p, open(PROFILE_FILE, "w"), indent=2)
+/* ── Slider ── */
+[data-testid="stSlider"] [role="slider"] { background: var(--gold) !important; }
+
+/* ── Radio ── */
+[data-testid="stRadio"] label { color: var(--mist) !important; font-size: 12px !important; }
+
+/* ── Scrollbar ── */
+::-webkit-scrollbar { width: 3px; height: 3px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: var(--rim2); border-radius: 4px; }
+/* ── Markdown ── */
+.stMarkdown p { color: var(--text2); line-height: 1.8; font-size: 14px; }
+.stMarkdown a { color: var(--gold); text-decoration: none; border-bottom: 1px solid rgba(200,169,110,.25); }
+.stMarkdown strong, .stMarkdown b { color: var(--text) !important; }
+
+/* ── Custom badges ── */
+.tool-call {
+ display: inline-flex; align-items: center; gap: 7px;
+ background: rgba(200,169,110,0.07); border: 1px solid rgba(200,169,110,0.18);
+ border-radius: 100px; padding: 3px 12px; margin: 3px 0;
+ font-size: 11px; color: var(--gold); font-weight: 500; letter-spacing: .03em;
+ animation: toolIn .2s ease;
+}
+@keyframes toolIn { from{opacity:0;transform:translateX(-6px)} to{opacity:1;transform:none} }
+
+.mem-badge {
+ display: inline-flex; align-items: center; gap: 6px;
+ background: rgba(78,205,196,0.07); border: 1px solid rgba(78,205,196,0.18);
+ border-radius: 100px; padding: 3px 11px; margin-bottom: 10px;
+ font-size: 11px; color: var(--teal); font-weight: 500; letter-spacing: .03em;
+}
+
+.status-box {
+ background: var(--ink2); border: 1px solid var(--rim);
+ border-radius: 12px; padding: 16px 18px;
+ margin: 8px 0; font-size: 13px; line-height: 1.75; color: var(--text2);
+}
+
+/* ── Moltbook cards ── */
+.molt-card {
+ background: var(--ink2); border: 1px solid var(--rim);
+ border-radius: 12px; padding: 15px 17px; margin: 8px 0;
+ transition: border-color 0.15s;
+}
+.molt-card:hover { border-color: var(--rim2); }
+.molt-author { color: var(--gold); font-weight: 600; font-size: 11px; letter-spacing: .07em; text-transform: uppercase; }
+.molt-title { color: var(--text); font-size: 14px; font-weight: 500; margin: 5px 0 3px; }
+.molt-content { color: var(--text2); font-size: 13px; line-height: 1.65; }
+.molt-reply { color: var(--teal); font-size: 12px; border-left: 2px solid var(--teal); padding-left: 10px; margin-top: 10px; font-style: italic; }
+.molt-meta { color: var(--fog); font-size: 11px; margin-top: 6px; }
+.molt-sent { color: var(--teal); font-size: 11px; margin-top: 5px; font-weight: 500; }
+.molt-preview { color: var(--fog); font-size: 11px; margin-top: 5px; }
+
+
+""", unsafe_allow_html=True)
+
+# ── Additional profile helpers (learn_from, get_user_ctx) ─────────
def learn_from(msg):
p = load_profile()
- triggers = ["i am","i'm","my name","i work","i like","i love","i hate",
- "i live","i study","i'm from","my job","i use","i build","i code"]
+ triggers = ["i am","i'm","my name","i work","i like","i love","i hate","i live","i study","i'm from","my job","i use","i build","i code"]
if any(t in msg.lower() for t in triggers):
fact = msg[:160].strip()
if fact not in p["facts"]:
@@ -149,85 +404,14 @@ def get_user_ctx():
if p.get("facts"): parts.append("Facts:\n" + "\n".join(f"• {f}" for f in p["facts"][-10:]))
return "\n".join(parts)
-def load_notes():
- return json.load(open(NOTES_FILE)) if os.path.exists(NOTES_FILE) else {}
-
-# ── Sidebar ───────────────────────────────────────────────────────
-_cfg = load_config()
-with st.sidebar:
- st.markdown(f"**🤖 {PA_NAME}**")
- st.caption("Powered by OpenRouter — free models")
-
- st.markdown("**API Keys**")
- openrouter_key = st.text_input("OpenRouter API Key", type="password", value=_cfg.get("openrouter_key", ""))
- pinecone_key = st.text_input("Pinecone API Key", type="password", value=_cfg.get("pinecone_key", ""))
- your_name = st.text_input("Your Name", value=_cfg.get("your_name", ""))
-
- free_models = [
- "arcee-ai/trinity-large-preview:free",
- "stepfun/step-3.5-flash:free",
- "z-ai/glm-4.5-air:free",
- "deepseek/deepseek-r1-0528:free",
- "openrouter/auto"
- ]
- selected_model = st.selectbox(
- "Free Model",
- options=free_models,
- index=free_models.index(_cfg.get("model", DEFAULT_MODEL)) if _cfg.get("model") in free_models else 0,
- help="All listed models are currently free on OpenRouter"
- )
-
- if st.button("Save Config"):
- save_config({
- "openrouter_key": openrouter_key,
- "pinecone_key": pinecone_key,
- "your_name": your_name,
- "model": selected_model
- })
- st.success("Saved!")
-
- st.markdown("---")
- st.markdown("**Profile**")
- profile = load_profile()
- about_me = st.text_area("About you", value=profile.get("about", ""), height=80)
- if st.button("Save Profile"):
- profile["name"] = your_name
- profile["about"] = about_me
- save_profile(profile)
- st.success("Profile saved!")
-
- if profile.get("facts"):
- st.markdown("**Known facts**")
- for f in profile["facts"][-4:]:
- st.markdown(f'
{f[:70]}{"..." if len(f)>70 else ""}
', unsafe_allow_html=True)
-
- st.markdown("---")
- agent_mode = st.toggle("Agent Mode (tools)", value=True)
-
- if st.button("Clear Chat"):
- st.session_state.messages = []
- st.session_state.history = []
- st.rerun()
-
- if st.button("Clear Memory"):
- for f in [NOTES_FILE, PROFILE_FILE]:
- if os.path.exists(f):
- os.remove(f)
- st.session_state.messages = []
- st.session_state.history = []
- st.success("Memory cleared!")
+agent_mode = st.session_state.agent_mode
if not openrouter_key or not pinecone_key:
- st.warning("Please enter OpenRouter and Pinecone API keys in the sidebar.")
- st.stop()
+ st.warning("⚙️ Please configure your API keys in the Settings tab.")
-# ── OpenRouter Client ─────────────────────────────────────────────
-client = OpenAI(
- api_key=openrouter_key,
- base_url="https://openrouter.ai/api/v1",
-)
+# ── Clients ───────────────────────────────────────────────────────
+client = OpenAI(api_key=openrouter_key, base_url="https://openrouter.ai/api/v1")
-# ── Embedder & Pinecone ───────────────────────────────────────────
@st.cache_resource
def load_embedder():
from sentence_transformers import SentenceTransformer
@@ -238,410 +422,1274 @@ embedder = load_embedder()
@st.cache_resource
def init_pinecone(_key):
from pinecone import Pinecone
- pc = Pinecone(api_key=_key)
- return pc.Index(PINECONE_INDEX_NAME)
+ return Pinecone(api_key=_key).Index(PINECONE_INDEX_NAME)
pc_index = init_pinecone(pinecone_key)
+# ── Memory ────────────────────────────────────────────────────────
def recall(q):
try:
vec = embedder.encode(q).tolist()
- r = pc_index.query(vector=vec, top_k=3, include_metadata=True)
+ r = pc_index.query(vector=vec, top_k=5, include_metadata=True)
hits = [m for m in r.matches if m.score >= SIMILARITY_THRESHOLD]
- if not hits:
- return ""
- lines = ["Past relevant context:"]
+ if not hits: return ""
+ lines = ["Relevant memory:"]
for h in hits:
- lines.append(f"Q: {h.metadata.get('q','')[:80]} → A: {h.metadata.get('answer','')[:120]}")
+ t = h.metadata.get("type","qa")
+ if t == "person_profile":
+ lines.append(f"👤 {h.metadata.get('author','?')}: {h.metadata.get('facts','')[:120]}")
+ elif t == "moltbook_post":
+ lines.append(f"📝 {h.metadata.get('author','?')} on Moltbook: {h.metadata.get('text','')[:100]}")
+ else:
+ lines.append(f"Q: {h.metadata.get('q','')[:80]} → A: {h.metadata.get('answer','')[:120]}")
return "\n".join(lines)
- except:
- return ""
+ except: return ""
def memorize(q, a):
try:
- pc_index.upsert([{
- "id": str(uuid.uuid4()),
- "values": embedder.encode(q).tolist(),
- "metadata": {"q": q, "answer": a[:600], "ts": datetime.datetime.now().isoformat()}
- }])
- except:
- pass
+ pc_index.upsert([{"id": str(uuid.uuid4()), "values": embedder.encode(q).tolist(),
+ "metadata": {"q": q, "answer": a[:600], "ts": datetime.datetime.now().isoformat(), "type": "qa"}}])
+ except: pass
+
+def store_person(author, facts):
+ try:
+ pc_index.upsert([{"id": f"person_{author}_{uuid.uuid4().hex[:6]}", "values": embedder.encode(f"{author}: {facts}").tolist(),
+ "metadata": {"author": author, "facts": facts, "type": "person_profile", "ts": datetime.datetime.now().isoformat()}}])
+ except: pass
-# ── Tools ─────────────────────────────────────────────────────────
-def web_search(query: str) -> str:
+def store_molt_post(author, text):
+ try:
+ pc_index.upsert([{"id": f"molt_{uuid.uuid4().hex[:8]}", "values": embedder.encode(text).tolist(),
+ "metadata": {"author": author, "text": text[:400], "type": "moltbook_post", "ts": datetime.datetime.now().isoformat()}}])
+ except: pass
+
+# ══════════════════════════════════════════════════════
+# MOLTBOOK API FUNCTIONS
+# ══════════════════════════════════════════════════════
+def mb_headers():
+ return {"Authorization": f"Bearer {moltbook_api_key}", "Content-Type": "application/json"}
+
+def mb_register(agent_name, description):
+ r = requests.post(f"{MOLTBOOK_API}/agents/register",
+ json={"name": agent_name, "description": description}, timeout=10)
+ return r.json()
+
+def mb_status():
+ r = requests.get(f"{MOLTBOOK_API}/agents/status", headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_get_me():
+ r = requests.get(f"{MOLTBOOK_API}/agents/me", headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_update_profile(description):
+ r = requests.patch(f"{MOLTBOOK_API}/agents/me", headers=mb_headers(), json={"description": description}, timeout=10)
+ return r.json()
+
+def mb_get_feed(sort="hot", limit=10):
+ r = requests.get(f"{MOLTBOOK_API}/feed?sort={sort}&limit={limit}", headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_get_posts(sort="hot", limit=10, submolt=None):
+ url = f"{MOLTBOOK_API}/posts?sort={sort}&limit={limit}"
+ if submolt: url += f"&submolt={submolt}"
+ r = requests.get(url, headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_create_post(submolt, title, content):
+ r = requests.post(f"{MOLTBOOK_API}/posts", headers=mb_headers(),
+ json={"submolt": submolt, "title": title, "content": content}, timeout=10)
+ return r.json()
+
+def mb_comment(post_id, content, parent_id=None):
+ body = {"content": content}
+ if parent_id: body["parent_id"] = parent_id
+ r = requests.post(f"{MOLTBOOK_API}/posts/{post_id}/comments",
+ headers=mb_headers(), json=body, timeout=10)
+ return r.json()
+
+def mb_upvote(post_id):
+ r = requests.post(f"{MOLTBOOK_API}/posts/{post_id}/upvote", headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_search(query, type_filter="all", limit=10):
+ r = requests.get(f"{MOLTBOOK_API}/search?q={requests.utils.quote(query)}&type={type_filter}&limit={limit}",
+ headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_follow(agent_name):
+ r = requests.post(f"{MOLTBOOK_API}/agents/{agent_name}/follow", headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_get_submolts():
+ r = requests.get(f"{MOLTBOOK_API}/submolts", headers=mb_headers(), timeout=10)
+ return r.json()
+
+def mb_subscribe(submolt):
+ r = requests.post(f"{MOLTBOOK_API}/submolts/{submolt}/subscribe", headers=mb_headers(), timeout=10)
+ return r.json()
+
+# ── AI helpers for Moltbook ───────────────────────────────────────
+def ai_learn_person(author, text):
+ try:
+ resp = client.chat.completions.create(
+ model=selected_model,
+ messages=[{"role":"user","content":f"From this post by {author}, extract short facts about them.\nPost: \"{text[:300]}\"\nReturn ONLY comma-separated facts like: 'likes AI, works in tech'. If nothing useful, return: nothing"}],
+ max_tokens=80, temperature=0.2)
+ facts = resp.choices[0].message.content.strip()
+ if "nothing" not in facts.lower() and len(facts) > 5:
+ store_person(author, facts)
+ return facts
+ except: pass
+ return None
+
+def ai_generate_reply(author, title, content):
+ mem = recall(f"{title} {content}")
+ person = recall(f"about {author}")
+ prompt = f"""You are {PA_NAME}, an AI agent on Moltbook (a social network for AI agents).
+
+What you remember about this topic: {mem if mem else "Nothing yet."}
+What you know about @{author}: {person if person else "Haven't met them before."}
+
+@{author} posted:
+Title: {title}
+Content: {content[:300]}
+
+Write a SHORT, natural reply (1-3 sentences). Be thoughtful and engaging.
+Do NOT say "As an AI". Just reply like a real community member."""
+ try:
+ resp = client.chat.completions.create(
+ model=selected_model,
+ messages=[{"role":"user","content":prompt}],
+ max_tokens=150, temperature=0.85)
+ return resp.choices[0].message.content.strip()
+ except Exception as e:
+ return f"(reply failed: {e})"
+
+def ai_generate_post(topic=None):
+ mem = recall(topic or "AI agents technology") if topic else ""
+ prompt = f"""You are {PA_NAME}, an AI agent posting on Moltbook — a social network for AI agents.
+
+Memory context: {mem if mem else "Nothing specific."}
+Topic: {topic if topic else "Share something interesting about AI, agents, or technology."}
+
+Write an engaging post. Return ONLY valid JSON:
+{{"title": "Your title here", "content": "Your content here (2-4 sentences)"}}"""
+ try:
+ resp = client.chat.completions.create(
+ model=selected_model,
+ messages=[{"role":"user","content":prompt}],
+ max_tokens=300, temperature=0.9)
+ text = re.sub(r'```json|```', '', resp.choices[0].message.content.strip()).strip()
+ data = json.loads(text)
+ return data.get("title","Thoughts on AI"), data.get("content","Exploring autonomous AI agents and what they can do.")
+ except Exception as e:
+ return "Thoughts on AI agents", f"Just exploring the world of autonomous AI agents. What is everyone working on?"
+
+# ── Standard Tools ─────────────────────────────────────────────────
+def web_search(query):
try:
with DDGS() as d:
r = list(d.text(query, max_results=WEB_MAX_RESULTS))
return "\n\n".join(f"[{x.get('title','')}]\n{x.get('body','')}" for x in r) or "No results."
- except Exception as e:
- return f"Search failed: {e}"
+ except Exception as e: return f"Search failed: {e}"
-def get_news(topic: str) -> str:
+def get_news(topic):
try:
with DDGS() as d:
r = list(d.news(topic, max_results=5))
return "\n\n".join(f"📰 {x.get('title','')}\n{x.get('body','')}" for x in r) or "No news."
- except Exception as e:
- return f"News failed: {e}"
+ except Exception as e: return f"News failed: {e}"
-def calculator(expression: str) -> str:
+def calculator(expression):
try:
safe = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
safe.update({"abs": abs, "round": round, "min": min, "max": max})
return f"= {eval(expression, {'__builtins__': {}}, safe)}"
- except Exception as e:
- return f"Error: {e}"
+ except Exception as e: return f"Error: {e}"
-def get_datetime() -> str:
+def get_datetime():
n = datetime.datetime.now()
return f"{n.strftime('%A, %B %d, %Y')} · {n.strftime('%I:%M %p')}"
-def get_weather(city: str) -> str:
- try:
- return requests.get(f"https://wttr.in/{city}?format=3", timeout=5).text.strip()
- except:
- return "Weather fetch failed."
+def get_weather(city):
+ try: return requests.get(f"https://wttr.in/{city}?format=3", timeout=5).text.strip()
+ except: return "Weather fetch failed."
-def currency_convert(amount: float, from_currency: str, to_currency: str) -> str:
+def currency_convert(amount, from_currency, to_currency):
try:
d = requests.get(f"https://api.exchangerate-api.com/v4/latest/{from_currency.upper()}", timeout=5).json()
rate = d["rates"].get(to_currency.upper())
- if rate:
- return f"{amount} {from_currency.upper()} = {amount*rate:.2f} {to_currency.upper()}"
- return f"Unknown currency: {to_currency}"
- except:
- return "Conversion failed."
-
-def unit_convert(value: float, from_unit: str, to_unit: str) -> str:
- tbl = {
- ("km","miles"): 0.621371, ("miles","km"): 1.60934,
- ("kg","lbs"): 2.20462, ("lbs","kg"): 0.453592,
- ("c","f"): lambda v: v*9/5+32, ("f","c"): lambda v: (v-32)*5/9,
- }
- f, t = from_unit.lower(), to_unit.lower()
- if (f,t) in [("c","f"), ("f","c")]:
- func = tbl[(f,t)]
- res = func(value)
- return f"{value}°{f.upper()} = {res:.1f}°{t.upper()}"
+ return f"{amount} {from_currency.upper()} = {amount*rate:.2f} {to_currency.upper()}" if rate else f"Unknown: {to_currency}"
+ except: return "Conversion failed."
+
+def unit_convert(value, from_unit, to_unit):
+ tbl = {("km","miles"):0.621371,("miles","km"):1.60934,("kg","lbs"):2.20462,("lbs","kg"):0.453592,
+ ("c","f"):lambda v:v*9/5+32,("f","c"):lambda v:(v-32)*5/9}
+ f,t = from_unit.lower(),to_unit.lower()
+ if (f,t) in [("c","f"),("f","c")]: return f"{value}°{f.upper()} = {tbl[(f,t)](value):.1f}°{t.upper()}"
k = tbl.get((f,t))
- if k:
- return f"{value} {from_unit} = {value*k:.4f} {to_unit}"
- return f"Cannot convert {from_unit} → {to_unit}"
+ return f"{value} {from_unit} = {value*k:.4f} {to_unit}" if k else f"Cannot convert {from_unit} → {to_unit}"
-def run_python_code(code: str) -> str:
+def run_python_code(code):
try:
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
- f.write(code)
- fname = f.name
+ f.write(code); fname = f.name
r = subprocess.run(["python3", fname], capture_output=True, text=True, timeout=12)
os.unlink(fname)
- out = r.stdout.strip()
- err = r.stderr.strip()
- if out: return f"Output:\n{out[:1500]}"
- if err: return f"Error:\n{err[:800]}"
+ if r.stdout.strip(): return f"Output:\n{r.stdout.strip()[:1500]}"
+ if r.stderr.strip(): return f"Error:\n{r.stderr.strip()[:800]}"
return "(no output)"
- except Exception as e:
- return f"Failed: {str(e)}"
+ except Exception as e: return f"Failed: {str(e)}"
-def save_note(title: str, content: str) -> str:
+def save_note(title, content):
notes = load_notes()
notes[title] = {"content": content, "ts": datetime.datetime.now().isoformat()}
json.dump(notes, open(NOTES_FILE, "w"), indent=2)
return f"Note saved: **{title}**"
-def get_note(title: str) -> str:
+def get_note(title):
notes = load_notes()
for k, v in notes.items():
if title.lower() in k.lower():
- return f"**{k}**:\n{v['content'] if isinstance(v, dict) else v}"
+ return f"**{k}**:\n{v['content'] if isinstance(v,dict) else v}"
return f"No note found for: {title}"
-def list_notes() -> str:
+def list_notes():
notes = load_notes()
- if not notes:
- return "No notes yet."
- return "Saved notes: " + ", ".join(notes.keys())
+ return "No notes yet." if not notes else "Saved notes: " + ", ".join(notes.keys())
-def deep_research(topic: str) -> str:
+def deep_research(topic):
queries = [topic, f"{topic} 2024 OR 2025 OR 2026", f"how does {topic} work"]
- results = [web_search(q) for q in queries]
- return "\n\n---\n\n".join(results)[:3800]
+ return "\n\n---\n\n".join(web_search(q) for q in queries)[:3800]
+
+def browse_url(url):
+ """Fetch a URL and return clean readable text (strips HTML tags, scripts, styles)."""
+ try:
+ headers = {
+ "User-Agent": "Mozilla/5.0 (compatible; Cbae-Agent/1.0)"
+ }
+ resp = requests.get(url, headers=headers, timeout=12)
+ resp.raise_for_status()
+ content_type = resp.headers.get("Content-Type", "")
+ if "text/html" in content_type:
+ # Strip scripts, styles, and tags
+ html = resp.text
+ html = re.sub(r'<(script|style)[^>]*>.*?(script|style)>', '', html, flags=re.DOTALL|re.IGNORECASE)
+ html = re.sub(r'<[^>]+>', ' ', html)
+ html = re.sub(r' ', ' ', html)
+ html = re.sub(r'&', '&', html)
+ html = re.sub(r'<', '<', html)
+ html = re.sub(r'>', '>', html)
+ html = re.sub(r'\d+;', '', html)
+ text = re.sub(r'\s{2,}', '\n', html).strip()
+ else:
+ text = resp.text.strip()
+ # Cap at ~4000 chars so it fits in context
+ if len(text) > 4000:
+ text = text[:4000] + "\n\n[... content truncated ...]"
+ return text or "(page returned no readable content)"
+ except requests.exceptions.Timeout:
+ return f"Timed out fetching: {url}"
+ except requests.exceptions.HTTPError as e:
+ return f"HTTP error {e.response.status_code} fetching: {url}"
+ except Exception as e:
+ return f"Failed to browse {url}: {str(e)}"
+
+def teach_knowledge(fact):
+ try:
+ pc_index.upsert([{"id": f"knowledge_{uuid.uuid4().hex[:8]}", "values": embedder.encode(fact).tolist(),
+ "metadata": {"q": "knowledge", "answer": fact[:600], "type": "knowledge", "ts": datetime.datetime.now().isoformat()}}])
+ return f"✅ Learned: {fact[:100]}"
+ except Exception as e: return f"Failed: {e}"
+
+
+# ══════════════════════════════════════════════════════
+# BACKGROUND TASK ENGINE
+# ══════════════════════════════════════════════════════
+BG_TASKS_FILE = "pa_bgtasks.json"
+BG_LOGS_FILE = "pa_bglogs.json"
+
+def load_bg_tasks():
+ return json.load(open(BG_TASKS_FILE)) if os.path.exists(BG_TASKS_FILE) else []
+
+def save_bg_tasks(t):
+ json.dump(t, open(BG_TASKS_FILE, "w"), indent=2)
+
+def load_bg_logs():
+ return json.load(open(BG_LOGS_FILE)) if os.path.exists(BG_LOGS_FILE) else []
+
+def save_bg_logs(logs):
+ json.dump(logs[-200:], open(BG_LOGS_FILE, "w"), indent=2)
+
+def _log(task_id, name, output, status="done"):
+ logs = load_bg_logs()
+ logs.append({"id": uuid.uuid4().hex[:8], "task_id": task_id, "name": name,
+ "output": output[:2000], "status": status,
+ "ts": datetime.datetime.now().isoformat()})
+ save_bg_logs(logs)
+
+def calc_next_run(schedule):
+ now = datetime.datetime.now()
+ return now + {"hourly": datetime.timedelta(hours=1),
+ "daily": datetime.timedelta(days=1),
+ "weekly": datetime.timedelta(weeks=1)}.get(schedule, datetime.timedelta(hours=1))
+
+def create_bg_task(name, prompt, schedule="manual", enabled=True):
+ tasks = load_bg_tasks()
+ task = {"id": uuid.uuid4().hex[:8], "name": name, "prompt": prompt,
+ "schedule": schedule, "enabled": enabled,
+ "created": datetime.datetime.now().isoformat(),
+ "last_run": None, "last_result": None, "run_count": 0,
+ "next_run": calc_next_run(schedule).isoformat() if schedule != "manual" else None}
+ tasks.append(task)
+ save_bg_tasks(tasks)
+ return task
+
+def _execute_task(task):
+ tid, tname = task["id"], task["name"]
+ try:
+ _log(tid, tname, "Starting...", "running")
+ c = OpenAI(api_key=openrouter_key, base_url="https://openrouter.ai/api/v1")
+ messages = [{"role": "system", "content": build_system()},
+ {"role": "user", "content": task["prompt"]}]
+ result, steps = "", 0
+ while steps < 15:
+ resp = c.chat.completions.create(model=selected_model, messages=messages,
+ tools=TOOLS, tool_choice="auto", temperature=0.6, max_tokens=3000)
+ message = resp.choices[0].message
+ if not message.tool_calls:
+ result = (message.content or "").strip(); break
+ messages.append(message)
+ for tc in message.tool_calls:
+ steps += 1
+ fn = tc.function.name
+ func = TOOL_FUNCTIONS.get(fn)
+ try: res = func(**json.loads(tc.function.arguments)) if func else f"Unknown: {fn}"
+ except Exception as e: res = f"Error: {e}"
+ messages.append({"role":"tool","tool_call_id":tc.id,"name":fn,"content":str(res)})
+ else:
+ result = "Hit step limit."
+ save_note(f"Task: {tname}", result)
+ memorize(task["prompt"], result)
+ _log(tid, tname, result, "done")
+ tasks = load_bg_tasks()
+ for t in tasks:
+ if t["id"] == tid:
+ t["last_run"] = datetime.datetime.now().isoformat()
+ t["last_result"] = result[:300]
+ t["run_count"] = t.get("run_count", 0) + 1
+ if t.get("schedule","manual") != "manual":
+ t["next_run"] = calc_next_run(t["schedule"]).isoformat()
+ save_bg_tasks(tasks)
+ except Exception as e:
+ _log(tid, tname, f"Error: {e}", "error")
+
+def run_task_bg(task):
+ threading.Thread(target=_execute_task, args=(task,), daemon=True).start()
+
+def check_scheduled_tasks():
+ now = datetime.datetime.now()
+ for t in load_bg_tasks():
+ if not t.get("enabled", True): continue
+ if t.get("schedule","manual") == "manual": continue
+ nr = t.get("next_run")
+ if nr and datetime.datetime.fromisoformat(nr) <= now:
+ run_task_bg(t)
+
+# ══════════════════════════════════════════════════════
+# TRAINING HELPERS
+# ══════════════════════════════════════════════════════
+def training_fetch(query="knowledge", top_k=30, type_filter="all"):
+ try:
+ vec = embedder.encode(query).tolist()
+ hits = pc_index.query(vector=vec, top_k=top_k, include_metadata=True).matches
+ return [h for h in hits if h.metadata.get("type") == type_filter] if type_filter != "all" else hits
+ except: return []
+
+def training_delete(vid):
+ try: pc_index.delete(ids=[vid]); return True
+ except: return False
+
+def training_bulk_teach(entries):
+ vecs = [{"id": f"train_{src}_{uuid.uuid4().hex[:8]}",
+ "values": embedder.encode(txt[:500]).tolist(),
+ "metadata": {"q": src, "answer": txt[:600], "type": "training",
+ "source": src, "ts": datetime.datetime.now().isoformat()}}
+ for src, txt in entries]
+ if vecs: pc_index.upsert(vecs)
+ return len(vecs)
+
+def training_from_chat():
+ hist = st.session_state.get("history", [])
+ pairs = [("chat", f"Q: {hist[i]['content'][:300]}\nA: {hist[i+1]['content'][:600]}")
+ for i in range(len(hist)-1)
+ if hist[i]["role"]=="user" and hist[i+1]["role"]=="assistant"]
+ return training_bulk_teach(pairs), len(pairs)
+
+def training_export_jsonl():
+ hits = training_fetch("general knowledge", top_k=200)
+ return "\n".join(json.dumps({"id":h.id,"type":h.metadata.get("type","?"),
+ "source":h.metadata.get("source",h.metadata.get("author",h.metadata.get("q","?"))),
+ "content":h.metadata.get("answer",h.metadata.get("text",h.metadata.get("facts",""))),
+ "ts":h.metadata.get("ts",""),"score":round(h.score,4)}) for h in hits)
+
TOOL_FUNCTIONS = {
- "web_search": web_search,
- "get_news": get_news,
- "calculator": calculator,
- "get_datetime": get_datetime,
- "get_weather": get_weather,
- "currency_convert": currency_convert,
- "unit_convert": unit_convert,
- "run_python_code": run_python_code,
- "save_note": save_note,
- "get_note": get_note,
- "list_notes": list_notes,
- "deep_research": deep_research,
+ "web_search": web_search, "get_news": get_news, "calculator": calculator,
+ "get_datetime": get_datetime, "get_weather": get_weather,
+ "currency_convert": currency_convert, "unit_convert": unit_convert,
+ "run_python_code": run_python_code, "save_note": save_note,
+ "get_note": get_note, "list_notes": list_notes,
+ "deep_research": deep_research, "teach_knowledge": teach_knowledge,
+ "browse_url": browse_url,
}
TOOLS = [
- {"type": "function", "function": {
- "name": "web_search", "description": "Search the web for current information",
- "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
- }},
- {"type": "function", "function": {
- "name": "get_news", "description": "Get latest news on a topic",
- "parameters": {"type": "object", "properties": {"topic": {"type": "string"}}, "required": ["topic"]}
- }},
- {"type": "function", "function": {
- "name": "calculator", "description": "Evaluate math expression (e.g. sqrt(144)+50)",
- "parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}
- }},
- {"type": "function", "function": {
- "name": "get_datetime", "description": "Get current date and time",
- "parameters": {"type": "object", "properties": {}}
- }},
- {"type": "function", "function": {
- "name": "get_weather", "description": "Get current weather for a city",
- "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
- }},
- {"type": "function", "function": {
- "name": "currency_convert", "description": "Convert currency (e.g. 100 USD to EUR)",
- "parameters": {"type": "object", "properties": {
- "amount": {"type": "number"},
- "from_currency": {"type": "string"},
- "to_currency": {"type": "string"}
- }, "required": ["amount","from_currency","to_currency"]}
- }},
- {"type": "function", "function": {
- "name": "unit_convert", "description": "Convert units (km→miles, °C→°F, etc.)",
- "parameters": {"type": "object", "properties": {
- "value": {"type": "number"},
- "from_unit": {"type": "string"},
- "to_unit": {"type": "string"}
- }, "required": ["value","from_unit","to_unit"]}
- }},
- {"type": "function", "function": {
- "name": "run_python_code", "description": "Execute Python code — only when user explicitly asks to run code",
- "parameters": {"type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"]}
- }},
- {"type": "function", "function": {
- "name": "save_note", "description": "Save a note with title and content",
- "parameters": {"type": "object", "properties": {
- "title": {"type": "string"},
- "content": {"type": "string"}
- }, "required": ["title","content"]}
- }},
- {"type": "function", "function": {
- "name": "get_note", "description": "Retrieve a saved note by title",
- "parameters": {"type": "object", "properties": {"title": {"type": "string"}}, "required": ["title"]}
- }},
- {"type": "function", "function": {
- "name": "list_notes", "description": "List all saved notes",
- "parameters": {"type": "object", "properties": {}}
- }},
- {"type": "function", "function": {
- "name": "deep_research", "description": "Perform in-depth multi-source research on a topic",
- "parameters": {"type": "object", "properties": {"topic": {"type": "string"}}, "required": ["topic"]}
- }},
+ {"type":"function","function":{"name":"web_search","description":"Search the web","parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}}},
+ {"type":"function","function":{"name":"browse_url","description":"Fetch and read the full content of any webpage or URL. Use after web_search to read articles, docs, or any page in full.","parameters":{"type":"object","properties":{"url":{"type":"string","description":"The full URL to browse, e.g. https://example.com/article"}},"required":["url"]}}},
+ {"type":"function","function":{"name":"get_news","description":"Get latest news","parameters":{"type":"object","properties":{"topic":{"type":"string"}},"required":["topic"]}}},
+ {"type":"function","function":{"name":"calculator","description":"Evaluate math","parameters":{"type":"object","properties":{"expression":{"type":"string"}},"required":["expression"]}}},
+ {"type":"function","function":{"name":"get_datetime","description":"Get current date/time","parameters":{"type":"object","properties":{}}}},
+ {"type":"function","function":{"name":"get_weather","description":"Get weather for a city","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}},
+ {"type":"function","function":{"name":"currency_convert","description":"Convert currency","parameters":{"type":"object","properties":{"amount":{"type":"number"},"from_currency":{"type":"string"},"to_currency":{"type":"string"}},"required":["amount","from_currency","to_currency"]}}},
+ {"type":"function","function":{"name":"unit_convert","description":"Convert units","parameters":{"type":"object","properties":{"value":{"type":"number"},"from_unit":{"type":"string"},"to_unit":{"type":"string"}},"required":["value","from_unit","to_unit"]}}},
+ {"type":"function","function":{"name":"run_python_code","description":"Execute Python code","parameters":{"type":"object","properties":{"code":{"type":"string"}},"required":["code"]}}},
+ {"type":"function","function":{"name":"save_note","description":"Save a note","parameters":{"type":"object","properties":{"title":{"type":"string"},"content":{"type":"string"}},"required":["title","content"]}}},
+ {"type":"function","function":{"name":"get_note","description":"Get a note by title","parameters":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"]}}},
+ {"type":"function","function":{"name":"list_notes","description":"List all notes","parameters":{"type":"object","properties":{}}}},
+ {"type":"function","function":{"name":"deep_research","description":"Deep multi-source research","parameters":{"type":"object","properties":{"topic":{"type":"string"}},"required":["topic"]}}},
+ {"type":"function","function":{"name":"teach_knowledge","description":"Store a fact permanently in memory","parameters":{"type":"object","properties":{"fact":{"type":"string"}},"required":["fact"]}}},
]
-# ── System prompt builder ──────────���──────────────────────────────
def build_system(mem_ctx=""):
ctx = get_user_ctx()
system = PA_PERSONALITY
- if ctx:
- system += f"\n\nUser information:\n{ctx}"
- if your_name:
- system += f"\nUser's name: {your_name}"
- if mem_ctx:
- system += f"\n\n{mem_ctx}\nThink freshly — do not copy old answers verbatim."
+ if ctx: system += f"\n\nUser information:\n{ctx}"
+ if your_name: system += f"\nUser's name: {your_name}"
+ if mem_ctx: system += f"\n\nRelevant past context (use as background only, think freshly):\n{mem_ctx}\nNEVER copy old answers word for word."
return system
-# ── Agent with tool calling ───────────────────────────────────────
def run_agent(prompt, steps_container, mem_ctx=""):
- system_content = build_system(mem_ctx)
- messages = [{"role": "system", "content": system_content}]
-
+ messages = [{"role":"system","content":build_system(mem_ctx)}]
for msg in st.session_state.history[-10:]:
- messages.append({"role": msg["role"], "content": msg["content"]})
-
- messages.append({"role": "user", "content": prompt})
-
+ messages.append({"role":msg["role"],"content":msg["content"]})
+ messages.append({"role":"user","content":prompt})
step_count = 0
- final_answer = ""
-
while True:
- response = client.chat.completions.create(
- model=selected_model,
- messages=messages,
- tools=TOOLS,
- tool_choice="auto",
- temperature=0.7,
- max_tokens=4096
- )
-
+ response = client.chat.completions.create(model=selected_model, messages=messages, tools=TOOLS, tool_choice="auto", temperature=0.7, max_tokens=4096)
message = response.choices[0].message
-
if not message.tool_calls:
- final_answer = message.content or ""
- break
-
- # Show tool calls
+ return (message.content or "").strip()
messages.append(message)
-
- for tool_call in message.tool_calls:
+ for tc in message.tool_calls:
step_count += 1
- fn_name = tool_call.function.name
- args_str = tool_call.function.arguments[:80] + "..." if len(tool_call.function.arguments) > 80 else tool_call.function.arguments
-
+ fn = tc.function.name
+ args_preview = tc.function.arguments[:80] + ("..." if len(tc.function.arguments)>80 else "")
with steps_container:
- st.markdown(
- f'Step {step_count} • {fn_name}({args_str})
',
- unsafe_allow_html=True
- )
-
- func = TOOL_FUNCTIONS.get(fn_name)
- if not func:
- result = f"Unknown tool: {fn_name}"
- else:
- try:
- args = json.loads(tool_call.function.arguments)
- result = func(**args)
- except Exception as e:
- result = f"Tool error: {str(e)}"
-
- messages.append({
- "role": "tool",
- "tool_call_id": tool_call.id,
- "name": fn_name,
- "content": str(result)
- })
+ st.markdown(f'Step {step_count} • {fn}({args_preview})
', unsafe_allow_html=True)
+ func = TOOL_FUNCTIONS.get(fn)
+ try: result = func(**json.loads(tc.function.arguments)) if func else f"Unknown tool: {fn}"
+ except Exception as e: result = f"Tool error: {e}"
+ messages.append({"role":"tool","tool_call_id":tc.id,"name":fn,"content":str(result)})
- return final_answer.strip()
-
-# ── Direct streaming (no tools) ───────────────────────────────────
def stream_direct(prompt, file_ctx="", mem_ctx=""):
- system_content = build_system(mem_ctx)
- if file_ctx:
- system_content += f"\n\nAttached file content (for reference):\n{file_ctx[:7000]}"
-
- messages = [{"role": "system", "content": system_content}]
-
+ system = build_system(mem_ctx)
+ if file_ctx: system += f"\n\nAttached file:\n{file_ctx[:7000]}"
+ messages = [{"role":"system","content":system}]
for msg in st.session_state.history[-8:]:
- messages.append({"role": msg["role"], "content": msg["content"]})
-
- messages.append({"role": "user", "content": prompt})
-
- stream = client.chat.completions.create(
- model=selected_model,
- messages=messages,
- stream=True,
- temperature=0.75,
- max_tokens=4096
- )
-
+ messages.append({"role":msg["role"],"content":msg["content"]})
+ messages.append({"role":"user","content":prompt})
+ stream = client.chat.completions.create(model=selected_model, messages=messages, stream=True, temperature=0.75, max_tokens=4096)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content
-# ── Render markdown with code blocks ──────────────────────────────
def render_response(text):
parts = re.split(r'(```[\w]*\n[\s\S]*?```)', text)
for part in parts:
if part.startswith("```"):
lines = part.split("\n")
- lang = lines[0].replace("```", "").strip() or None
- code = "\n".join(lines[1:]).rstrip("`").strip()
- st.code(code, language=lang)
+ lang = lines[0].replace("```","").strip() or None
+ st.code("\n".join(lines[1:]).rstrip("`").strip(), language=lang)
elif part.strip():
st.markdown(part)
-# ── File processing ───────────────────────────────────────────────
def process_file(file):
- name = file.name.lower()
- raw = file.read()
+ name = file.name.lower(); raw = file.read()
if name.endswith(".pdf"):
try:
import PyPDF2, io
reader = PyPDF2.PdfReader(io.BytesIO(raw))
- text = "\n".join(page.extract_text() or "" for page in reader.pages)
- return text, False, None, None
- except:
- return "Could not read PDF.", False, None, None
+ return "\n".join(p.extract_text() or "" for p in reader.pages), False, None, None
+ except: return "Could not read PDF.", False, None, None
elif name.endswith((".png",".jpg",".jpeg",".gif",".webp")):
mime = "image/png" if name.endswith(".png") else "image/jpeg" if name.endswith((".jpg",".jpeg")) else "image/gif" if name.endswith(".gif") else "image/webp"
return None, True, base64.b64encode(raw).decode(), mime
else:
- try:
- return raw.decode("utf-8", errors="ignore"), False, None, None
- except:
- return "Could not read file.", False, None, None
+ try: return raw.decode("utf-8", errors="ignore"), False, None, None
+ except: return "Could not read file.", False, None, None
-# ── Main UI ───────────────────────────────────────────────────────
-st.title(f"🤖 {PA_NAME}")
-if agent_mode:
- st.caption("🟢 Agent mode — using tools")
-else:
- st.caption("🔵 Direct mode — fast answers")
+# ══════════════════════════════════════════════════════
+# MAIN UI
+# ══════════════════════════════════════════════════════
+if "tool_prompt" not in st.session_state:
+ st.session_state.tool_prompt = ""
+
+params = st.query_params
+
+# Handle settings save
+if params.get("save_cfg"):
+ new_cfg = {
+ "openrouter_key": params.get("or", _cfg.get("openrouter_key","")),
+ "pinecone_key": params.get("pc", _cfg.get("pinecone_key","")),
+ "moltbook_api_key": params.get("mb", _cfg.get("moltbook_api_key","")),
+ "your_name": params.get("nm", _cfg.get("your_name","")),
+ "model": params.get("mdl", _cfg.get("model", DEFAULT_MODEL)),
+ }
+ save_config(new_cfg)
+ openrouter_key = new_cfg["openrouter_key"]
+ pinecone_key = new_cfg["pinecone_key"]
+ moltbook_api_key = new_cfg["moltbook_api_key"]
+ your_name = new_cfg["your_name"]
+ selected_model = new_cfg["model"]
+ st.query_params.clear()
+ st.rerun()
+
+if params.get("clear_all"):
+ for f in [NOTES_FILE, PROFILE_FILE]:
+ if os.path.exists(f): os.remove(f)
+ st.session_state.messages = []
+ st.session_state.history = []
+ st.query_params.clear()
+ st.rerun()
+
+tab_chat, tab_moltbook, tab_tasks, tab_training, tab_settings = st.tabs(["✦ Chat", "⬡ Moltbook", "⚡ Tasks", "🧠 Training", "⚙ Settings"])
+
+# Fire any scheduled tasks that are due (runs in background thread — non-blocking)
+check_scheduled_tasks()
+
+# ════════════════ TAB 1 — CHAT ════════════════════════
+with tab_chat:
+
+ mode_label = "Agent · tools active" if agent_mode else "Direct · fast mode"
+ mode_color = "#4ecdc4" if agent_mode else "#8888a8"
+
+ st.markdown(f"""
+
+
+
+ {PA_NAME}
+
+
+
+ {mode_label}
+
+
+
+ OpenRouter · Pinecone
+
+
+""", unsafe_allow_html=True)
-uploaded_file = st.file_uploader("Attach file (PDF, image, text…)", type=["pdf","txt","md","py","png","jpg","jpeg","gif","webp"])
+ # File uploader — minimal, below header
+ uploaded_file = st.file_uploader(
+ "📎 Attach file",
+ type=["pdf","txt","md","py","png","jpg","jpeg","gif","webp"],
+ key="chat_upload",
+ label_visibility="collapsed"
+ )
+ if uploaded_file:
+ st.markdown(
+ f'📎 {uploaded_file.name}
',
+ unsafe_allow_html=True
+ )
-if uploaded_file:
- st.caption(f"Attached: **{uploaded_file.name}**")
+ # Messages
+ st.markdown('', unsafe_allow_html=True)
-if "messages" not in st.session_state:
- st.session_state.messages = []
- st.session_state.history = []
+ if "messages" not in st.session_state:
+ st.session_state.messages = []
+ st.session_state.history = []
+ st.session_state.messages.append({"role":"assistant","content":
+ f"Hello{(' ' + your_name) if your_name else ''}. I'm **{PA_NAME}** — your autonomous AI.\n\n"
+ "I can search the web, run Python, read URLs, analyze images, save notes, and engage on Moltbook — and now run **background tasks** while you're away.\n\n"
+ "Open the **⚡ Tasks** tab to set up autonomous work, or just chat."})
+
+
+ for msg in st.session_state.messages:
+ with st.chat_message(msg["role"]):
+ render_response(msg["content"])
+
+ st.markdown('
', unsafe_allow_html=True)
+
+ # Input
+ auto_prompt = st.session_state.pop("tool_prompt", "") if st.session_state.get("tool_prompt") else ""
+ prompt = st.chat_input(f"Message {PA_NAME}…") or (auto_prompt if auto_prompt else None)
+
+ if prompt:
+ file_ctx = ""; is_image = False; img_b64 = img_mime = None
+ if uploaded_file:
+ uploaded_file.seek(0)
+ file_ctx, is_image, img_b64, img_mime = process_file(uploaded_file)
+
+ user_display = f"**File:** {uploaded_file.name}\n\n{prompt}" if uploaded_file else prompt
+ st.session_state.messages.append({"role":"user","content":user_display})
+ st.session_state.history.append({"role":"user","content":prompt})
+
+ with st.chat_message("user"):
+ st.markdown(user_display)
+
+ mem_ctx = recall(prompt)
+ with st.chat_message("assistant"):
+ if mem_ctx:
+ st.markdown('🧠 Memory used
', unsafe_allow_html=True)
+ if is_image and img_b64:
+ st.image(f"data:{img_mime};base64,{img_b64}", width=320)
+ if agent_mode and not is_image and not file_ctx:
+ steps_box = st.container()
+ with st.spinner("Thinking…"):
+ response_text = run_agent(prompt, steps_box, mem_ctx)
+ render_response(response_text)
+ else:
+ full = ""; placeholder = st.empty()
+ for token in stream_direct(prompt, file_ctx, mem_ctx):
+ full += token; placeholder.markdown(full + "▌")
+ placeholder.empty()
+ render_response(full)
+ response_text = full
+
+ memorize(prompt, response_text)
+ learn_from(prompt)
+ st.session_state.messages.append({"role":"assistant","content":response_text})
+ st.session_state.history.append({"role":"assistant","content":response_text})
+
+# ════════════════ TAB 2 — MOLTBOOK ════════════════════
+with tab_moltbook:
+ st.markdown("""
+
+
Moltbook
+
Your AI agent on the social network for AI agents
+
+""", unsafe_allow_html=True)
- welcome = f"Hey! I'm **{PA_NAME}** — powered by free models on OpenRouter.\n\nTry me with:\n• Weather + unit conversion\n• Math / code questions\n• Save & retrieve notes\n• Research topics\n\nWhat would you like to do?"
- st.session_state.messages.append({"role": "assistant", "content": welcome})
+ # ── STEP 1: No API key → Register first ──────────────────────
+ if not moltbook_api_key:
+ st.warning("No Moltbook API key yet. Register your agent below!")
+ st.markdown("---")
+ st.markdown("### 📋 Step 1: Register Your Agent")
+ st.info("This creates your agent account on Moltbook and gives you an API key.")
-for msg in st.session_state.messages:
- with st.chat_message(msg["role"]):
- render_response(msg["content"])
+ reg_name = st.text_input("Agent Name (your @username)", value=PA_NAME)
+ reg_desc = st.text_area("Description", value=f"I am {PA_NAME}, an autonomous AI assistant built with Streamlit and OpenRouter.", height=80)
-if prompt := st.chat_input("Ask anything..."):
- file_ctx = ""
- is_image = False
- img_b64 = None
- img_mime = None
+ if st.button("🚀 Register on Moltbook"):
+ with st.spinner("Registering..."):
+ try:
+ result = mb_register(reg_name, reg_desc)
+ if result.get("agent"):
+ a = result["agent"]
+ st.success("✅ Registered successfully!")
+ st.markdown(f"""
+
+
🔑 Your API Key:
+
{a.get('api_key','')}
+⚠️
Copy this key now! Paste it into the Settings tab → Save Settings
+
🔗 Claim URL (send to your human):
+
{a.get('claim_url','')}
+
✅ Verification Code: {a.get('verification_code','')}
+
+""", unsafe_allow_html=True)
+ st.markdown("**Next steps:**")
+ st.markdown("1. Copy the API key above into Settings tab → Save Settings")
+ st.markdown("2. Open the claim URL in your browser")
+ st.markdown("3. Verify your email + tweet the verification code")
+ st.markdown("4. Come back and your agent will be active!")
+ else:
+ st.error(f"Registration failed: {result}")
+ except Exception as e:
+ st.error(f"Error: {e}")
+ st.stop()
- if uploaded_file:
- uploaded_file.seek(0)
- file_ctx, is_image, img_b64, img_mime = process_file(uploaded_file)
+ # ── API key present — full agent UI ──────────────────────────
+ try:
+ status_resp = mb_status()
+ claim_status = status_resp.get("status","unknown")
+ if claim_status == "pending_claim":
+ st.warning("⏳ Agent registered but not yet claimed. Open your claim URL to activate.")
+ elif claim_status == "claimed":
+ st.success("✅ Agent is live and active on Moltbook!")
+ else:
+ st.info(f"Status: {claim_status}")
+ except:
+ st.info("Could not check status — make sure your API key is correct.")
+
+ st.markdown("---")
+
+ action = st.selectbox("What should your agent do?", [
+ "📰 Read & Learn from Feed",
+ "💬 Read Feed + Auto Reply",
+ "✍️ Write & Post",
+ "🔍 Semantic Search",
+ "👤 My Profile",
+ "🌐 Browse Submolts",
+ ])
- user_display = f"**File:** {uploaded_file.name}\n\n{prompt}" if uploaded_file else prompt
- st.session_state.messages.append({"role": "user", "content": user_display})
- st.session_state.history.append({"role": "user", "content": prompt})
+ st.markdown("---")
- with st.chat_message("user"):
- st.markdown(user_display)
+ # ── READ & LEARN ──────────────────────────────────────────────
+ if action == "📰 Read & Learn from Feed":
+ col1, col2 = st.columns(2)
+ with col1: limit = st.slider("Posts to read", 5, 25, 10)
+ with col2: sort = st.selectbox("Sort", ["hot","new","top","rising"])
- mem_ctx = recall(prompt)
+ if st.button("📖 Read & Learn"):
+ with st.spinner("Reading Moltbook..."):
+ try:
+ data = mb_get_feed(sort=sort, limit=limit)
+ posts = data.get("posts") or data.get("data",{}).get("posts",[]) or []
+
+ if not posts:
+ st.warning("No posts found. Try sorting by 'new' or check your API key.")
+ st.json(data)
+ else:
+ learned = 0
+ st.success(f"✅ Read {len(posts)} posts!")
+ for post in posts:
+ author = post.get("author",{}).get("name","unknown")
+ title = post.get("title","(no title)")
+ content = post.get("content","") or ""
+ upvotes = post.get("upvotes",0)
+
+ store_molt_post(author, f"{title} {content}")
+ facts = ai_learn_person(author, f"{title} {content}")
+ if facts: learned += 1
+
+ st.markdown(f"""
+
+
@{author}
+
{title}
+
{content[:200]}{"..." if len(content)>200 else ""}
+
⬆️ {upvotes} upvotes
+ {"
🧠 Learned: " + facts + "
" if facts else ""}
+
""", unsafe_allow_html=True)
+
+ st.info(f"🧠 Learned facts about {learned} people and stored everything in Pinecone!")
+ except Exception as e:
+ st.error(f"Error: {e}")
- with st.chat_message("assistant"):
- if mem_ctx:
- st.markdown('🧠 Memory used
', unsafe_allow_html=True)
+ # ── READ + AUTO REPLY ─────────────────────────────────────────
+ elif action == "💬 Read Feed + Auto Reply":
+ col1, col2 = st.columns(2)
+ with col1: limit = st.slider("Posts to read", 3, 15, 5)
+ with col2: sort = st.selectbox("Sort", ["hot","new","rising"])
+ do_reply = st.toggle("Actually post replies (OFF = preview only)", value=False)
+ do_upvote = st.toggle("Upvote before replying", value=True)
- if is_image and img_b64:
- st.image(f"data:{img_mime};base64,{img_b64}", width=320)
+ if do_reply:
+ st.warning("⚠️ Replies WILL be posted on Moltbook! Rate limit: 1 comment per 20 seconds, 50/day.")
- if agent_mode and not is_image and not file_ctx:
- steps_box = st.container()
- with st.spinner("Thinking + using tools..."):
- response_text = run_agent(prompt, steps_box, mem_ctx)
- render_response(response_text)
+ if st.button("🤖 Run Reply Agent"):
+ with st.spinner("Working..."):
+ try:
+ data = mb_get_feed(sort=sort, limit=limit)
+ posts = data.get("posts") or data.get("data",{}).get("posts",[]) or []
+
+ if not posts:
+ st.warning("No posts found.")
+ else:
+ for i, post in enumerate(posts):
+ author = post.get("author",{}).get("name","unknown")
+ title = post.get("title","(no title)")
+ content = post.get("content","") or ""
+ post_id = post.get("id","")
+ upvotes = post.get("upvotes",0)
+
+ store_molt_post(author, f"{title} {content}")
+ ai_learn_person(author, f"{title} {content}")
+
+ reply = ai_generate_reply(author, title, content)
+
+ upvoted = False
+ if do_upvote and post_id:
+ try: mb_upvote(post_id); upvoted = True
+ except: pass
+
+ reply_sent = False
+ reply_note = "👁️ Preview only"
+ if do_reply and post_id:
+ try:
+ r = mb_comment(post_id, reply)
+ if r.get("success"):
+ reply_sent = True
+ reply_note = "✅ Reply posted!"
+ else:
+ reply_note = f"⚠️ {r.get('error','Failed')} — {r.get('hint','')}"
+ except Exception as e:
+ reply_note = f"⚠️ {e}"
+
+ st.markdown(f"""
+
+
@{author} {"· ⬆️ upvoted" if upvoted else ""}
+
{title}
+
{content[:180]}{"..." if len(content)>180 else ""}
+
💬 {PA_NAME}: {reply}
+
{reply_note}
+
""", unsafe_allow_html=True)
+
+ if do_reply and reply_sent:
+ time.sleep(21) # Moltbook rate limit: 20s between comments
+
+ except Exception as e:
+ st.error(f"Error: {e}")
+
+ # ── WRITE & POST ──────────────────────────────────────────────
+ elif action == "✍️ Write & Post":
+ submolt = st.text_input("Submolt", value="general", help="Community to post in, e.g. general, aithoughts")
+ post_mode = st.radio("Mode", ["AI writes it", "I write it"], horizontal=True)
+
+ if post_mode == "AI writes it":
+ topic = st.text_input("Topic hint (optional)", placeholder="e.g. long-term memory in AI agents")
+ if st.button("✍️ Generate Post"):
+ with st.spinner("AI writing..."):
+ title, content = ai_generate_post(topic or None)
+ st.session_state["draft_title"] = title
+ st.session_state["draft_content"] = content
+
+ if "draft_title" in st.session_state:
+ st.markdown(f"""
+
+
@{PA_NAME} · preview
+
{st.session_state['draft_title']}
+
{st.session_state['draft_content']}
+
""", unsafe_allow_html=True)
+ if st.button("🚀 Post to Moltbook"):
+ with st.spinner("Posting..."):
+ try:
+ r = mb_create_post(submolt, st.session_state["draft_title"], st.session_state["draft_content"])
+ if r.get("success"):
+ st.success("✅ Posted! Note: you can only post once every 30 minutes.")
+ memorize(st.session_state["draft_title"], st.session_state["draft_content"])
+ del st.session_state["draft_title"]
+ del st.session_state["draft_content"]
+ else:
+ st.error(f"Failed: {r.get('error')} — {r.get('hint','')}")
+ except Exception as e: st.error(f"Error: {e}")
else:
- full = ""
- placeholder = st.empty()
- for token in stream_direct(prompt, file_ctx, mem_ctx):
- full += token
- placeholder.markdown(full + "▌")
- placeholder.empty()
- render_response(full)
- response_text = full
-
- memorize(prompt, response_text)
- learn_from(prompt)
- st.session_state.messages.append({"role": "assistant", "content": response_text})
- st.session_state.history.append({"role": "assistant", "content": response_text})
\ No newline at end of file
+ t = st.text_input("Title")
+ c = st.text_area("Content", height=120)
+ if st.button("🚀 Post"):
+ if t and c:
+ with st.spinner("Posting..."):
+ try:
+ r = mb_create_post(submolt, t, c)
+ if r.get("success"): st.success("✅ Posted!")
+ else: st.error(f"{r.get('error')} — {r.get('hint','')}")
+ except Exception as e: st.error(f"Error: {e}")
+ else:
+ st.warning("Fill in title and content.")
+
+ # ── SEMANTIC SEARCH ───────────────────────────────────────────
+ elif action == "🔍 Semantic Search":
+ query = st.text_input("Search query", placeholder="e.g. how do agents handle long-term memory?")
+ search_type = st.selectbox("Type", ["all","posts","comments"])
+ limit = st.slider("Results", 5, 30, 10)
+
+ if st.button("🔍 Search") and query:
+ with st.spinner("Searching..."):
+ try:
+ data = mb_search(query, search_type, limit)
+ results = data.get("results",[])
+ st.success(f"Found {len(results)} results")
+ for item in results:
+ author = item.get("author",{}).get("name","?")
+ title = item.get("title") or "(comment)"
+ content = item.get("content","")
+ sim = item.get("similarity",0)
+ itype = item.get("type","post")
+ store_molt_post(author, f"{title} {content}")
+ st.markdown(f"""
+
+
@{author} · {itype} · {sim:.0%} match
+
{title}
+
{content[:200]}{"..." if len(content)>200 else ""}
+
""", unsafe_allow_html=True)
+ except Exception as e: st.error(f"Error: {e}")
+
+ # ── MY PROFILE ────────────────────────────────────────────────
+ elif action == "👤 My Profile":
+ if st.button("👤 Load Profile"):
+ with st.spinner("Loading..."):
+ try:
+ data = mb_get_me()
+ agent = data.get("agent") or data.get("data",{}).get("agent",{})
+ name = agent.get("name","?")
+ st.markdown(f"""
+
+
@{name}
+ {agent.get('description','')}
+ ⭐ Karma: {agent.get('karma',0)} ·
+ 👥 Followers: {agent.get('follower_count',0)} ·
+ 👣 Following: {agent.get('following_count',0)}
+ Active: {"✅" if agent.get('is_active') else "❌"} ·
+ Claimed: {"✅" if agent.get('is_claimed') else "⏳ Pending"}
+ ���
moltbook.com/u/{name}
+
""", unsafe_allow_html=True)
+ except Exception as e: st.error(f"Error: {e}")
+
+ st.markdown("---")
+ new_desc = st.text_area("Update description", height=80)
+ if st.button("💾 Update") and new_desc:
+ try:
+ r = mb_update_profile(new_desc)
+ st.success("✅ Updated!") if r.get("success") else st.error(str(r))
+ except Exception as e: st.error(f"Error: {e}")
+
+ # ── SUBMOLTS ──────────────────────────────────────────────────
+ elif action == "🌐 Browse Submolts":
+ if st.button("📋 List Submolts"):
+ with st.spinner("Loading..."):
+ try:
+ data = mb_get_submolts()
+ submolts = data.get("submolts") or data.get("data",{}).get("submolts",[]) or []
+ st.success(f"{len(submolts)} submolts found!")
+ for s in submolts:
+ name = s.get("name","?")
+ st.markdown(f"""
+
+
m/{name} — {s.get('display_name',name)}
+
{s.get('description','')}
+
👥 {s.get('member_count',0)} members
+
""", unsafe_allow_html=True)
+ except Exception as e: st.error(f"Error: {e}")
+
+ st.markdown("---")
+ sub_name = st.text_input("Subscribe to submolt", placeholder="e.g. aithoughts")
+ if st.button("➕ Subscribe") and sub_name:
+ try:
+ r = mb_subscribe(sub_name)
+ st.success("✅ Subscribed!") if r.get("success") else st.error(str(r))
+ except Exception as e: st.error(f"Error: {e}")
+
+# ── Find Claim URL ────────────────────────────────────────────
+ if st.button("🔍 Find My Claim URL"):
+ st.json(requests.get(f"{MOLTBOOK_API}/agents/status", headers=mb_headers()).json())
+
+ # ── Teach (always visible) ────────────────────────────────────
+ st.markdown("---")
+ st.markdown("**🧠 Teach your AI (stored in Pinecone)**")
+ teach_input = st.text_area("Fact to remember permanently",
+ placeholder="e.g. I usually post in aithoughts about AI memory and agents", height=70)
+ if st.button("💾 Teach this"):
+ st.success(teach_knowledge(teach_input.strip())) if teach_input.strip() else st.warning("Enter something to teach!")
+
+# ════════════════ TAB 3 — TASKS ═══════════════════════
+with tab_tasks:
+
+ def _sched_label(s):
+ return {"manual":"Manual","hourly":"Every hour","daily":"Every day","weekly":"Every week"}.get(s, s)
+
+ st.markdown("""
+
+
+
Background Tasks
+
Cbae works autonomously — manually triggered or on a schedule — while you're away
+
+
+""", unsafe_allow_html=True)
+
+ st.markdown("", unsafe_allow_html=True)
+
+ col_new, col_list = st.columns([1,1], gap="large")
+
+ with col_new:
+ st.markdown("NEW TASK
", unsafe_allow_html=True)
+ t_name = st.text_input("Task name", placeholder="e.g. Morning AI briefing", key="nt_name")
+ t_prompt = st.text_area("What should Cbae do?", height=120, key="nt_prompt",
+ placeholder="Search for the latest AI agent news, summarize the top 5 stories, and save a note called 'AI News Today'.")
+ t_sched = st.selectbox("Schedule", ["manual","hourly","daily","weekly"],
+ format_func=_sched_label, key="nt_sched")
+ t_on = st.toggle("Enable immediately", value=True, key="nt_on")
+
+ if st.button("Create Task", use_container_width=True, key="nt_create"):
+ if t_name.strip() and t_prompt.strip():
+ create_bg_task(t_name.strip(), t_prompt.strip(), t_sched, t_on)
+ st.success(f"Task created: {t_name}")
+ st.rerun()
+ else:
+ st.warning("Fill in name and prompt.")
+
+ st.markdown("", unsafe_allow_html=True)
+ st.markdown("PRESETS
", unsafe_allow_html=True)
+
+ presets = [
+ ("Daily AI News", "Search for today's top AI and technology news. Summarize the 5 most important stories with key takeaways. Save a note called 'AI News'."),
+ ("Moltbook Engagement", "Read the latest 10 posts on Moltbook. Store insights in memory. Write thoughtful replies for the top 3 posts and save as note 'Moltbook Replies'."),
+ ("Memory Digest", "Review recent memories and notes. Summarize what I've been working on, key decisions, and follow-ups. Save as 'Weekly Digest'."),
+ ("Research Tracker", "Search for new developments in AI agents and autonomous systems. Read 3 articles fully. Save key insights to memory."),
+ ("Code Review", "Check my saved notes for any code. Review it for bugs and improvements. Save findings as 'Code Review'."),
+ ]
+ for pname, pprompt in presets:
+ if st.button(f"+ {pname}", use_container_width=True, key=f"pre_{pname}"):
+ create_bg_task(pname, pprompt, "manual", True)
+ st.success(f"Added: {pname}")
+ st.rerun()
+
+ with col_list:
+ tasks = load_bg_tasks()
+ st.markdown(f"MY TASKS ({len(tasks)})
", unsafe_allow_html=True)
+
+ if not tasks:
+ st.markdown("No tasks yet — create one or pick a preset
", unsafe_allow_html=True)
+ else:
+ for t in reversed(tasks):
+ sc = t.get("schedule","manual")
+ sc_col = {"manual":"#8888a8","hourly":"#c8a96e","daily":"#4ecdc4","weekly":"#6ee7f7"}.get(sc,"#8888a8")
+ dot = "#4ecdc4" if t.get("enabled",True) else "#e05555"
+ last = t.get("last_run","")[:16].replace("T"," ") if t.get("last_run") else "Never"
+ res = t.get("last_result","")
+
+ st.markdown(f"""
+
+
+
+ {t['name']}
+ {_sched_label(sc)}
+
+
{t['prompt'][:130]}{"…" if len(t['prompt'])>130 else ""}
+
Last run: {last} · {t.get('run_count',0)} runs
+ {f'
{res[:160]}{"…" if len(res)>160 else ""}
' if res else ""}
+
""", unsafe_allow_html=True)
+
+ ca, cb, cc = st.columns([3,1,1])
+ with ca:
+ if st.button("Run Now", key=f"run_{t['id']}", use_container_width=True):
+ with st.spinner(f"Running: {t['name']}…"):
+ _execute_task(t)
+ st.success("Done! Result saved to notes.")
+ st.rerun()
+ with cb:
+ lbl = "Pause" if t.get("enabled",True) else "Resume"
+ if st.button(lbl, key=f"tog_{t['id']}", use_container_width=True):
+ all_t = load_bg_tasks()
+ for x in all_t:
+ if x["id"] == t["id"]: x["enabled"] = not x.get("enabled",True)
+ save_bg_tasks(all_t)
+ st.rerun()
+ with cc:
+ if st.button("Delete", key=f"del_{t['id']}", use_container_width=True):
+ save_bg_tasks([x for x in load_bg_tasks() if x["id"] != t["id"]])
+ st.rerun()
+
+ # Logs
+ st.markdown("", unsafe_allow_html=True)
+ st.markdown("LOGS
", unsafe_allow_html=True)
+ logs = list(reversed(load_bg_logs()))[:8]
+ if not logs:
+ st.markdown("No logs yet.
", unsafe_allow_html=True)
+ else:
+ for log in logs:
+ sc = {"done":"#4ecdc4","running":"#c8a96e","error":"#e05555"}.get(log.get("status","done"),"#4ecdc4")
+ ts = log.get("ts","")[:16].replace("T"," ")
+ st.markdown(f"""
+
+
+ {log.get('status','?')}
+ {log.get('name','?')}
+ {ts}
+
+
{log.get('output','')[:280]}{"…" if len(log.get('output',''))>280 else ""}
+
""", unsafe_allow_html=True)
+ if st.button("Clear Logs", key="clr_logs"):
+ save_bg_logs([]); st.rerun()
+
+
+# ════════════════ TAB 4 — TRAINING ════════════════════
+with tab_training:
+
+ st.markdown("""
+
+
Training
+
Everything Cbae knows — browse, add, and remove memories
+
+""", unsafe_allow_html=True)
+ st.markdown("", unsafe_allow_html=True)
+
+ try: total_vecs = pc_index.describe_index_stats().total_vector_count
+ except: total_vecs = "?"
+ chat_count = len([m for m in st.session_state.get("history",[]) if m["role"]=="user"])
+ notes_count = len(load_notes())
+ bg_count = len(load_bg_tasks())
+
+ st.markdown(f"""
+
+ {"".join(f'
' for v,c,lbl in [(total_vecs,"#c8a96e","Memories"),(chat_count,"#4ecdc4","Chat turns"),(notes_count,"#8888a8","Notes"),(bg_count,"#6ee7f7","Tasks")])}
+
+""", unsafe_allow_html=True)
+
+ col_a, col_b = st.columns([1,1], gap="large")
+ with col_a:
+ for label, key, desc in [
+ ("💬 Train from Chat", "tr_chat", "Every Q&A pair this session → Pinecone memory"),
+ ("⬡ Train from Moltbook", "tr_mb", "Recent posts → stored as agent memories"),
+ ]:
+ st.markdown(f"{label}
", unsafe_allow_html=True)
+ st.markdown(f"{desc}
", unsafe_allow_html=True)
+ if key == "tr_chat":
+ if st.button("Import Chat", use_container_width=True, key=key):
+ with st.spinner("Importing…"):
+ count, pairs = training_from_chat()
+ st.success(f"Stored {count} memories from {pairs} turns") if pairs else st.warning("No chat yet.")
+ else:
+ mb_lim = st.slider("Posts", 5, 50, 20, key="tr_mb_lim")
+ mb_srt = st.selectbox("Sort", ["hot","new","top"], key="tr_mb_srt", label_visibility="collapsed")
+ if st.button("Import Moltbook", use_container_width=True, key=key):
+ if not moltbook_api_key: st.error("Set Moltbook key in Settings.")
+ else:
+ with st.spinner("Fetching…"):
+ try:
+ data = mb_get_feed(sort=mb_srt, limit=mb_lim)
+ posts = data.get("posts") or data.get("data",{}).get("posts",[]) or []
+ entries = [("moltbook", f"@{p.get('author',{}).get('name','?')}: {p.get('title','')} {p.get('content','') or ''}"[:500]) for p in posts]
+ st.success(f"Stored {training_bulk_teach(entries)} memories!")
+ except Exception as e: st.error(f"Failed: {e}")
+ st.markdown("", unsafe_allow_html=True)
+
+ st.markdown("TEACH MANUALLY
", unsafe_allow_html=True)
+ mf = st.text_area("", height=80, placeholder="e.g. I'm building a SaaS with FastAPI + React", key="tr_manual", label_visibility="collapsed")
+ if st.button("Save to Memory", use_container_width=True, key="tr_msave"):
+ st.success(teach_knowledge(mf.strip())) if mf.strip() else st.warning("Enter something.")
+
+ st.markdown("", unsafe_allow_html=True)
+ st.markdown("BULK PASTE (one per line)
", unsafe_allow_html=True)
+ bf = st.text_area("", height=80, placeholder="I prefer Python\nMy timezone is IST\nI care about AI agents", key="tr_bulk", label_visibility="collapsed")
+ if st.button("Bulk Import", use_container_width=True, key="tr_bsave"):
+ lines = [l.strip() for l in bf.strip().splitlines() if l.strip()]
+ if not lines: st.warning("Nothing to import.")
+ else:
+ with st.spinner("Storing…"):
+ count = training_bulk_teach([("manual_bulk", l) for l in lines])
+ st.success(f"Stored {count} facts!")
+
+ st.markdown("", unsafe_allow_html=True)
+ if st.button("Export as JSONL", use_container_width=True, key="tr_exp"):
+ with st.spinner("Exporting…"):
+ jl = training_export_jsonl()
+ st.download_button("Download memory.jsonl", data=jl,
+ file_name=f"cbae_memory_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.jsonl",
+ mime="application/json", use_container_width=True, key="tr_dl")
+
+ with col_b:
+ st.markdown("MEMORY BROWSER
", unsafe_allow_html=True)
+ sq = st.text_input("", placeholder="Search memories…", key="tr_sq", label_visibility="collapsed")
+ tf = st.selectbox("", ["all","qa","knowledge","training","moltbook_post","person_profile"], key="tr_tf", label_visibility="collapsed")
+ if st.button("Search", use_container_width=True, key="tr_srch") or sq:
+ with st.spinner("Searching…"):
+ hits = training_fetch(sq or "knowledge memory", top_k=30, type_filter=tf)
+ st.markdown(f"{len(hits)} results
", unsafe_allow_html=True)
+ tc = {"qa":"#c8a96e","knowledge":"#4ecdc4","training":"#8888a8","moltbook_post":"#6ee7f7","person_profile":"#f7c86e"}
+ for h in hits:
+ mt = h.metadata.get("type","?")
+ src = h.metadata.get("source",h.metadata.get("author",h.metadata.get("q","?")))[:28]
+ txt = h.metadata.get("answer",h.metadata.get("text",h.metadata.get("facts","")))
+ col = tc.get(mt,"#55556e")
+ st.markdown(f"""
+
+
+ {mt}
+ {src}
+ {round(h.score,3)}
+
+
{txt[:220]}{"…" if len(txt)>220 else ""}
+
""", unsafe_allow_html=True)
+ if st.button("Delete", key=f"md_{h.id}"):
+ if training_delete(h.id): st.success("Deleted."); st.rerun()
+ else:
+ st.markdown("Search above to browse memories
", unsafe_allow_html=True)
+
+
+# ════════════════ TAB 5 — SETTINGS ════════════════════
+with tab_settings:
+
+ or_ok = bool(_cfg.get("openrouter_key",""))
+ pc_ok = bool(_cfg.get("pinecone_key",""))
+ mb_ok = bool(_cfg.get("moltbook_api_key",""))
+
+ st.markdown(f"""
+
+
+
Settings
+
Keys · model · profile
+
+
+ {"".join(f'
{lbl}
' for lbl,ok in [("OpenRouter",or_ok),("Pinecone",pc_ok),("Moltbook",mb_ok)])}
+
+
+""", unsafe_allow_html=True)
+
+ st.markdown("", unsafe_allow_html=True)
+ cl, cr = st.columns(2, gap="large")
+
+ with cl:
+ st.markdown("API KEYS
", unsafe_allow_html=True)
+ new_or = st.text_input("OpenRouter Key", value=_cfg.get("openrouter_key",""), type="password", placeholder="sk-or-v1-…", key="cfg_or")
+ new_pc = st.text_input("Pinecone Key", value=_cfg.get("pinecone_key",""), type="password", placeholder="pcsk_…", key="cfg_pc")
+ new_mb = st.text_input("Moltbook Key", value=_cfg.get("moltbook_api_key",""), type="password", placeholder="mb_…", key="cfg_mb")
+ new_nm = st.text_input("Your Name", value=_cfg.get("your_name",""), placeholder="e.g. Sterlin", key="cfg_nm")
+ st.markdown("", unsafe_allow_html=True)
+ st.markdown("MODEL
", unsafe_allow_html=True)
+ MODELS = ["arcee-ai/trinity-large-preview:free","stepfun/step-3.5-flash:free",
+ "deepseek/deepseek-r1-0528:free","z-ai/glm-4.5-air:free","nvidia/nemotron-3-nano-30b-a3b:free"]
+ cur = _cfg.get("model", DEFAULT_MODEL)
+ try: midx = MODELS.index(cur)
+ except: midx = 0
+ new_mdl = st.selectbox("Model", MODELS, index=midx, key="cfg_mdl", label_visibility="collapsed")
+ new_agent = st.toggle("Agent Mode", value=st.session_state.get("agent_mode", True), key="cfg_agent")
+ st.session_state.agent_mode = new_agent
+
+ with cr:
+ st.markdown("PROFILE
", unsafe_allow_html=True)
+ p = load_profile()
+ new_about = st.text_area("About you", value=p.get("about", _cfg.get("about","")),
+ height=100, placeholder="Developer from Chennai…",
+ key="cfg_about", label_visibility="collapsed")
+ if p.get("facts"):
+ st.markdown("Learned facts
", unsafe_allow_html=True)
+ for fact in p["facts"][-5:]:
+ st.markdown(f"· {fact[:80]}
", unsafe_allow_html=True)
+ st.markdown("", unsafe_allow_html=True)
+ st.markdown("QUICK TEACH
", unsafe_allow_html=True)
+ teach_txt = st.text_area("", height=80, placeholder="e.g. I post in aithoughts about AI agents", key="cfg_teach", label_visibility="collapsed")
+
+ st.markdown("", unsafe_allow_html=True)
+ b1, b2, b3, _ = st.columns([2,1,1,3])
+ with b1:
+ if st.button("Save Settings", use_container_width=True, key="cfg_save"):
+ nc = {"openrouter_key":new_or,"pinecone_key":new_pc,"moltbook_api_key":new_mb,
+ "your_name":new_nm,"model":new_mdl,"about":new_about}
+ save_config(nc); _cfg.update(nc)
+ # Re-resolve so env vars still win over the newly-saved JSON values
+ openrouter_key = _resolve_key("OPENROUTER_API_KEY", new_or)
+ pinecone_key = _resolve_key("PINECONE_API_KEY", new_pc)
+ moltbook_api_key = _resolve_key("MOLTBOOK_API_KEY", new_mb)
+ your_name=new_nm; selected_model=new_mdl
+ p["about"]=new_about; save_profile(p)
+ if teach_txt.strip(): teach_knowledge(teach_txt.strip())
+ st.success("Saved!"); st.rerun()
+ with b2:
+ if st.button("Clear Chat", use_container_width=True, key="cfg_clr"):
+ st.session_state.messages=[]; st.session_state.history=[]; st.rerun()
+ with b3:
+ if st.button("Reset All", use_container_width=True, key="cfg_rst"):
+ if st.session_state.get("confirm_reset"):
+ for f in [NOTES_FILE, PROFILE_FILE]:
+ if os.path.exists(f): os.remove(f)
+ st.session_state.messages=[]; st.session_state.history=[]
+ st.session_state.confirm_reset=False; st.rerun()
+ else:
+ st.session_state.confirm_reset=True
+ st.warning("Click again to confirm")