|
|
""" |
|
|
Cbae β Autonomous AI Agent (OpenRouter edition) |
|
|
============================================= |
|
|
- Uses OpenRouter (free models) |
|
|
- Pinecone long-term memory |
|
|
- Web search, code runner, weather, news, notes & more |
|
|
- File upload (PDF, images, text) |
|
|
- Moltbook AI Agent β uses official Moltbook REST API |
|
|
Run with: streamlit run app.py |
|
|
""" |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
try: |
|
|
from dotenv import load_dotenv |
|
|
load_dotenv() |
|
|
except ImportError: |
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PA_NAME = "Cbae" |
|
|
EMBEDDING_MODEL = "all-MiniLM-L6-v2" |
|
|
PINECONE_INDEX_NAME = "pa-memory" |
|
|
SIMILARITY_THRESHOLD = 0.78 |
|
|
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" |
|
|
|
|
|
PA_PERSONALITY = f"""You are {PA_NAME}, a brilliant autonomous AI assistant. |
|
|
You are proactive, resourceful, and genuinely helpful. |
|
|
You remember things about the user and reference them naturally. |
|
|
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.""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
env_val = os.environ.get(env_var, "").strip() |
|
|
if env_val: |
|
|
return env_val |
|
|
|
|
|
try: |
|
|
secret = st.secrets.get(env_var, "").strip() |
|
|
if secret: |
|
|
return secret |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&display=swap'); |
|
|
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
|
|
|
|
|
:root { |
|
|
--ink: #09090e; |
|
|
--ink2: #111118; |
|
|
--ink3: #18181f; |
|
|
--rim: #222230; |
|
|
--rim2: #2e2e40; |
|
|
--fog: #55556e; |
|
|
--mist: #8888a8; |
|
|
--text: #eaeaf2; |
|
|
--text2: #b0b0c8; |
|
|
--gold: #c8a96e; |
|
|
--gold2: #e0c48a; |
|
|
--teal: #4ecdc4; |
|
|
--red: #e05555; |
|
|
--glow: rgba(200,169,110,0.09); |
|
|
} |
|
|
|
|
|
/* ββ Kill all Streamlit chrome ββ */ |
|
|
[data-testid="stToolbar"], |
|
|
[data-testid="stDecoration"], |
|
|
[data-testid="stStatusWidget"], |
|
|
header[data-testid="stHeader"], |
|
|
footer, |
|
|
[data-testid="collapsedControl"], |
|
|
[data-testid="stSidebarCollapsedControl"] { display: none !important; } |
|
|
|
|
|
/* ββ App shell ββ */ |
|
|
.stApp { |
|
|
background: var(--ink) !important; |
|
|
color: var(--text) !important; |
|
|
font-family: 'DM Sans', sans-serif !important; |
|
|
} |
|
|
.stApp::before { |
|
|
content: ''; |
|
|
position: fixed; inset: 0; pointer-events: none; z-index: 0; |
|
|
background: |
|
|
radial-gradient(ellipse 70% 45% at 15% -5%, rgba(200,169,110,0.055) 0%, transparent 55%), |
|
|
radial-gradient(ellipse 50% 40% at 85% 105%, rgba(78,205,196,0.035) 0%, transparent 50%); |
|
|
} |
|
|
|
|
|
/* ββ Sidebar β permanently hidden ββ */ |
|
|
section[data-testid="stSidebar"], |
|
|
[data-testid="stSidebarCollapseButton"], |
|
|
[data-testid="collapsedControl"], |
|
|
[data-testid="stSidebarCollapsedControl"] { display: none !important; } |
|
|
|
|
|
/* ββ Layout β centered readable width ββ */ |
|
|
.main { background: var(--ink) !important; } |
|
|
.main .block-container { |
|
|
padding: 1.5rem 2rem 2rem !important; |
|
|
max-width: 820px !important; |
|
|
margin: 0 auto !important; |
|
|
} |
|
|
|
|
|
/* ββ File uploader β compact strip ββ */ |
|
|
[data-testid="stFileUploaderDropzoneInstructions"] { display: none !important; } |
|
|
[data-testid="stFileUploader"] section { |
|
|
padding: 8px 12px !important; |
|
|
min-height: unset !important; |
|
|
border-radius: 9px !important; |
|
|
background: var(--ink3) !important; |
|
|
border: 1px dashed var(--rim2) !important; |
|
|
} |
|
|
|
|
|
/* ββ Fonts ββ */ |
|
|
* { font-family: 'DM Sans', sans-serif !important; } |
|
|
|
|
|
/* ββ Headings ββ */ |
|
|
h1, h2, h3 { |
|
|
font-family: 'DM Serif Display', serif !important; |
|
|
font-weight: 400 !important; |
|
|
color: var(--text) !important; |
|
|
-webkit-text-fill-color: var(--text) !important; |
|
|
} |
|
|
|
|
|
/* ββ Tabs ββ */ |
|
|
[data-testid="stTabs"] [role="tablist"] { |
|
|
background: var(--ink2); |
|
|
border-bottom: 1px solid var(--rim); |
|
|
padding: 0 32px; |
|
|
gap: 0; |
|
|
position: sticky; top: 0; z-index: 100; |
|
|
} |
|
|
[data-testid="stTabs"] [role="tab"] { |
|
|
color: var(--fog) !important; |
|
|
font-size: 12px !important; |
|
|
font-weight: 500 !important; |
|
|
letter-spacing: 0.05em !important; |
|
|
text-transform: uppercase !important; |
|
|
padding: 16px 20px !important; |
|
|
border: none !important; |
|
|
border-bottom: 2px solid transparent !important; |
|
|
background: transparent !important; |
|
|
border-radius: 0 !important; |
|
|
transition: color 0.2s; |
|
|
} |
|
|
[data-testid="stTabs"] [role="tab"][aria-selected="true"] { |
|
|
color: var(--gold) !important; |
|
|
border-bottom-color: var(--gold) !important; |
|
|
} |
|
|
[data-testid="stTabs"] [role="tab"]:hover { color: var(--text2) !important; } |
|
|
[data-testid="stTabContent"] { padding: 0 !important; } |
|
|
|
|
|
/* ββ Chat messages ββ */ |
|
|
[data-testid="stChatMessage"] { |
|
|
background: transparent !important; |
|
|
border: none !important; |
|
|
padding: 0 !important; |
|
|
animation: msgIn 0.3s cubic-bezier(0.16,1,0.3,1) both; |
|
|
} |
|
|
@keyframes msgIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} } |
|
|
|
|
|
[data-testid="stChatMessageAvatarAssistant"] { |
|
|
background: linear-gradient(135deg, var(--gold), #7a5525) !important; |
|
|
border-radius: 10px !important; |
|
|
} |
|
|
[data-testid="stChatMessageAvatarUser"] { |
|
|
background: var(--ink3) !important; |
|
|
border: 1px solid var(--rim2) !important; |
|
|
border-radius: 50% !important; |
|
|
} |
|
|
|
|
|
/* ββ Chat input ββ */ |
|
|
[data-testid="stChatInput"] { |
|
|
background: var(--ink2) !important; |
|
|
border: 1px solid var(--rim2) !important; |
|
|
border-radius: 14px !important; |
|
|
transition: border-color 0.25s, box-shadow 0.25s; |
|
|
} |
|
|
[data-testid="stChatInput"]:focus-within { |
|
|
border-color: var(--gold) !important; |
|
|
box-shadow: 0 0 0 3px var(--glow) !important; |
|
|
} |
|
|
[data-testid="stChatInput"] textarea { |
|
|
background: transparent !important; |
|
|
color: var(--text) !important; |
|
|
font-size: 14px !important; |
|
|
} |
|
|
[data-testid="stChatInput"] textarea::placeholder { color: var(--fog) !important; } |
|
|
[data-testid="stChatInput"] button { |
|
|
background: linear-gradient(135deg, var(--gold), #8a5e28) !important; |
|
|
border-radius: 9px !important; |
|
|
border: none !important; |
|
|
} |
|
|
|
|
|
/* ββ Buttons ββ */ |
|
|
.stButton > button { |
|
|
background: var(--ink3) !important; |
|
|
border: 1px solid var(--rim2) !important; |
|
|
border-radius: 9px !important; |
|
|
color: var(--mist) !important; |
|
|
font-size: 12px !important; |
|
|
font-weight: 400 !important; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.stButton > button:hover { |
|
|
border-color: var(--gold) !important; |
|
|
color: var(--gold) !important; |
|
|
background: var(--glow) !important; |
|
|
} |
|
|
|
|
|
/* ββ Inputs ββ */ |
|
|
.stTextInput input, .stTextArea textarea { |
|
|
background: var(--ink3) !important; |
|
|
border: 1px solid var(--rim) !important; |
|
|
border-radius: 8px !important; |
|
|
color: var(--text) !important; |
|
|
font-size: 13px !important; |
|
|
} |
|
|
.stTextInput input:focus, .stTextArea textarea:focus { |
|
|
border-color: var(--gold) !important; |
|
|
box-shadow: 0 0 0 2px var(--glow) !important; |
|
|
outline: none !important; |
|
|
} |
|
|
.stTextInput label, .stTextArea label { color: var(--fog) !important; font-size: 11px !important; } |
|
|
|
|
|
/* ββ Selectbox ββ */ |
|
|
[data-testid="stSelectbox"] > div > div { |
|
|
background: var(--ink3) !important; |
|
|
border: 1px solid var(--rim) !important; |
|
|
border-radius: 8px !important; |
|
|
color: var(--text) !important; |
|
|
} |
|
|
|
|
|
/* ββ Alerts ββ */ |
|
|
[data-testid="stAlert"] { |
|
|
background: var(--ink3) !important; |
|
|
border: 1px solid var(--rim) !important; |
|
|
border-radius: 10px !important; |
|
|
color: var(--text2) !important; |
|
|
} |
|
|
|
|
|
/* ββ Code ββ */ |
|
|
[data-testid="stCode"], .stCodeBlock { |
|
|
background: var(--ink2) !important; |
|
|
border: 1px solid var(--rim) !important; |
|
|
border-radius: 10px !important; |
|
|
} |
|
|
|
|
|
/* ββ 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; } |
|
|
|
|
|
/* ββ Spinner ββ */ |
|
|
[data-testid="stSpinner"] { color: var(--gold) !important; } |
|
|
|
|
|
/* ββ Toggle ββ */ |
|
|
[data-testid="stToggle"] [role="switch"][aria-checked="true"] { background-color: var(--gold) !important; } |
|
|
|
|
|
/* ββ 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; } |
|
|
|
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
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"] |
|
|
if any(t in msg.lower() for t in triggers): |
|
|
fact = msg[:160].strip() |
|
|
if fact not in p["facts"]: |
|
|
p["facts"].append(fact) |
|
|
p["facts"] = p["facts"][-25:] |
|
|
save_profile(p) |
|
|
|
|
|
def get_user_ctx(): |
|
|
p = load_profile() |
|
|
parts = [] |
|
|
if p.get("name"): parts.append(f"User's name: {p['name']}") |
|
|
if p.get("about"): parts.append(f"About: {p['about']}") |
|
|
if p.get("facts"): parts.append("Facts:\n" + "\n".join(f"β’ {f}" for f in p["facts"][-10:])) |
|
|
return "\n".join(parts) |
|
|
|
|
|
agent_mode = st.session_state.agent_mode |
|
|
|
|
|
if not openrouter_key or not pinecone_key: |
|
|
st.warning("βοΈ Please configure your API keys in the Settings tab.") |
|
|
|
|
|
|
|
|
client = OpenAI(api_key=openrouter_key, base_url="https://openrouter.ai/api/v1") |
|
|
|
|
|
@st.cache_resource |
|
|
def load_embedder(): |
|
|
from sentence_transformers import SentenceTransformer |
|
|
return SentenceTransformer(EMBEDDING_MODEL) |
|
|
|
|
|
embedder = load_embedder() |
|
|
|
|
|
@st.cache_resource |
|
|
def init_pinecone(_key): |
|
|
from pinecone import Pinecone |
|
|
return Pinecone(api_key=_key).Index(PINECONE_INDEX_NAME) |
|
|
|
|
|
pc_index = init_pinecone(pinecone_key) |
|
|
|
|
|
|
|
|
def recall(q): |
|
|
try: |
|
|
vec = embedder.encode(q).tolist() |
|
|
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 = ["Relevant memory:"] |
|
|
for h in hits: |
|
|
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 "" |
|
|
|
|
|
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(), "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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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?" |
|
|
|
|
|
|
|
|
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}" |
|
|
|
|
|
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}" |
|
|
|
|
|
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}" |
|
|
|
|
|
def get_datetime(): |
|
|
n = datetime.datetime.now() |
|
|
return f"{n.strftime('%A, %B %d, %Y')} Β· {n.strftime('%I:%M %p')}" |
|
|
|
|
|
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, 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()) |
|
|
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)) |
|
|
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): |
|
|
try: |
|
|
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f: |
|
|
f.write(code); fname = f.name |
|
|
r = subprocess.run(["python3", fname], capture_output=True, text=True, timeout=12) |
|
|
os.unlink(fname) |
|
|
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)}" |
|
|
|
|
|
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): |
|
|
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"No note found for: {title}" |
|
|
|
|
|
def list_notes(): |
|
|
notes = load_notes() |
|
|
return "No notes yet." if not notes else "Saved notes: " + ", ".join(notes.keys()) |
|
|
|
|
|
def deep_research(topic): |
|
|
queries = [topic, f"{topic} 2024 OR 2025 OR 2026", f"how does {topic} work"] |
|
|
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: |
|
|
|
|
|
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() |
|
|
|
|
|
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}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, "teach_knowledge": teach_knowledge, |
|
|
"browse_url": browse_url, |
|
|
} |
|
|
|
|
|
TOOLS = [ |
|
|
{"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"]}}}, |
|
|
] |
|
|
|
|
|
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\nRelevant past context (use as background only, think freshly):\n{mem_ctx}\nNEVER copy old answers word for word." |
|
|
return system |
|
|
|
|
|
def run_agent(prompt, steps_container, mem_ctx=""): |
|
|
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}) |
|
|
step_count = 0 |
|
|
while True: |
|
|
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: |
|
|
return (message.content or "").strip() |
|
|
messages.append(message) |
|
|
for tc in message.tool_calls: |
|
|
step_count += 1 |
|
|
fn = tc.function.name |
|
|
args_preview = tc.function.arguments[:80] + ("..." if len(tc.function.arguments)>80 else "") |
|
|
with steps_container: |
|
|
st.markdown(f'<div class="tool-call">Step {step_count} β’ <b>{fn}</b>({args_preview})</div>', 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)}) |
|
|
|
|
|
def stream_direct(prompt, file_ctx="", mem_ctx=""): |
|
|
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) |
|
|
for chunk in stream: |
|
|
if chunk.choices[0].delta.content is not None: |
|
|
yield chunk.choices[0].delta.content |
|
|
|
|
|
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 |
|
|
st.code("\n".join(lines[1:]).rstrip("`").strip(), language=lang) |
|
|
elif part.strip(): |
|
|
st.markdown(part) |
|
|
|
|
|
def process_file(file): |
|
|
name = file.name.lower(); raw = file.read() |
|
|
if name.endswith(".pdf"): |
|
|
try: |
|
|
import PyPDF2, io |
|
|
reader = PyPDF2.PdfReader(io.BytesIO(raw)) |
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if "tool_prompt" not in st.session_state: |
|
|
st.session_state.tool_prompt = "" |
|
|
|
|
|
params = st.query_params |
|
|
|
|
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
check_scheduled_tasks() |
|
|
|
|
|
|
|
|
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""" |
|
|
<div style="padding:22px 0 14px; border-bottom:1px solid #1a1a22; |
|
|
display:flex; align-items:center; justify-content:space-between;"> |
|
|
<div> |
|
|
<div style="font-family:'DM Serif Display',serif; font-size:24px; |
|
|
color:#eaeaf2; font-weight:400; letter-spacing:-0.025em;"> |
|
|
{PA_NAME} |
|
|
</div> |
|
|
<div style="display:flex; align-items:center; gap:7px; margin-top:4px;"> |
|
|
<span style="width:5px;height:5px;border-radius:50%;background:{mode_color}; |
|
|
display:inline-block;box-shadow:0 0 6px {mode_color};"></span> |
|
|
<span style="font-size:11px; color:{mode_color}; letter-spacing:0.05em;">{mode_label}</span> |
|
|
</div> |
|
|
</div> |
|
|
<div style="font-size:10px; color:#2e2e40; letter-spacing:0.06em; text-transform:uppercase;"> |
|
|
OpenRouter Β· Pinecone |
|
|
</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
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'<div style="font-size:11px;color:#c8a96e;padding:4px 36px 0;letter-spacing:.03em;">π {uploaded_file.name}</div>', |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown('<div style="padding:0 36px;">', unsafe_allow_html=True) |
|
|
|
|
|
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('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
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('<div class="mem-badge">π§ Memory used</div>', 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}) |
|
|
|
|
|
|
|
|
with tab_moltbook: |
|
|
st.markdown(""" |
|
|
<div style="padding:26px 36px 18px; border-bottom:1px solid #1a1a22;"> |
|
|
<div style="font-family:'DM Serif Display',serif;font-size:26px;color:#eaeaf2;font-weight:400;letter-spacing:-0.025em;">Moltbook</div> |
|
|
<div style="font-size:11px;color:#55556e;margin-top:4px;letter-spacing:.04em;">Your AI agent on the social network for AI agents</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
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 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""" |
|
|
<div class="status-box"> |
|
|
<b>π Your API Key:</b><br> |
|
|
<code style="font-size:13px;color:#6ee7f7">{a.get('api_key','')}</code><br><br> |
|
|
β οΈ <b>Copy this key now!</b> Paste it into the Settings tab β Save Settings<br><br> |
|
|
<b>π Claim URL (send to your human):</b><br> |
|
|
<a href="{a.get('claim_url','')}" target="_blank">{a.get('claim_url','')}</a><br><br> |
|
|
<b>β
Verification Code:</b> <code>{a.get('verification_code','')}</code> |
|
|
</div> |
|
|
""", 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() |
|
|
|
|
|
|
|
|
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", |
|
|
]) |
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
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"]) |
|
|
|
|
|
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""" |
|
|
<div class="molt-card"> |
|
|
<div class="molt-author">@{author}</div> |
|
|
<div class="molt-title">{title}</div> |
|
|
<div class="molt-content">{content[:200]}{"..." if len(content)>200 else ""}</div> |
|
|
<div class="molt-meta">β¬οΈ {upvotes} upvotes</div> |
|
|
{"<div class='molt-sent'>π§ Learned: " + facts + "</div>" if facts else ""} |
|
|
</div>""", 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}") |
|
|
|
|
|
|
|
|
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 do_reply: |
|
|
st.warning("β οΈ Replies WILL be posted on Moltbook! Rate limit: 1 comment per 20 seconds, 50/day.") |
|
|
|
|
|
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""" |
|
|
<div class="molt-card"> |
|
|
<div class="molt-author">@{author} {"Β· β¬οΈ upvoted" if upvoted else ""}</div> |
|
|
<div class="molt-title">{title}</div> |
|
|
<div class="molt-content">{content[:180]}{"..." if len(content)>180 else ""}</div> |
|
|
<div class="molt-reply">π¬ {PA_NAME}: {reply}</div> |
|
|
<div class="{'molt-sent' if reply_sent else 'molt-preview'}">{reply_note}</div> |
|
|
</div>""", unsafe_allow_html=True) |
|
|
|
|
|
if do_reply and reply_sent: |
|
|
time.sleep(21) |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"Error: {e}") |
|
|
|
|
|
|
|
|
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""" |
|
|
<div class="molt-card"> |
|
|
<div class="molt-author">@{PA_NAME} Β· preview</div> |
|
|
<div class="molt-title">{st.session_state['draft_title']}</div> |
|
|
<div class="molt-content">{st.session_state['draft_content']}</div> |
|
|
</div>""", 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: |
|
|
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.") |
|
|
|
|
|
|
|
|
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""" |
|
|
<div class="molt-card"> |
|
|
<div class="molt-author">@{author} Β· {itype} Β· {sim:.0%} match</div> |
|
|
<div class="molt-title">{title}</div> |
|
|
<div class="molt-content">{content[:200]}{"..." if len(content)>200 else ""}</div> |
|
|
</div>""", unsafe_allow_html=True) |
|
|
except Exception as e: st.error(f"Error: {e}") |
|
|
|
|
|
|
|
|
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""" |
|
|
<div class="status-box"> |
|
|
<b>@{name}</b><br> |
|
|
{agent.get('description','')}<br><br> |
|
|
β Karma: {agent.get('karma',0)} Β· |
|
|
π₯ Followers: {agent.get('follower_count',0)} Β· |
|
|
π£ Following: {agent.get('following_count',0)}<br> |
|
|
Active: {"β
" if agent.get('is_active') else "β"} Β· |
|
|
Claimed: {"β
" if agent.get('is_claimed') else "β³ Pending"}<br><br> |
|
|
π <a href="https://www.moltbook.com/u/{name}" target="_blank">moltbook.com/u/{name}</a> |
|
|
</div>""", 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}") |
|
|
|
|
|
|
|
|
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""" |
|
|
<div class="molt-card"> |
|
|
<div class="molt-author">m/{name} β {s.get('display_name',name)}</div> |
|
|
<div class="molt-content">{s.get('description','')}</div> |
|
|
<div class="molt-meta">π₯ {s.get('member_count',0)} members</div> |
|
|
</div>""", 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}") |
|
|
|
|
|
|
|
|
if st.button("π Find My Claim URL"): |
|
|
st.json(requests.get(f"{MOLTBOOK_API}/agents/status", headers=mb_headers()).json()) |
|
|
|
|
|
|
|
|
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!") |
|
|
|
|
|
|
|
|
with tab_tasks: |
|
|
|
|
|
def _sched_label(s): |
|
|
return {"manual":"Manual","hourly":"Every hour","daily":"Every day","weekly":"Every week"}.get(s, s) |
|
|
|
|
|
st.markdown(""" |
|
|
<div style="padding:22px 0 18px;border-bottom:1px solid #1a1a22;display:flex;align-items:center;justify-content:space-between;"> |
|
|
<div> |
|
|
<div style="font-family:'DM Serif Display',serif;font-size:24px;color:#eaeaf2;font-weight:400;letter-spacing:-0.025em;">Background Tasks</div> |
|
|
<div style="font-size:11px;color:#55556e;margin-top:4px;letter-spacing:.04em;">Cbae works autonomously β manually triggered or on a schedule β while you're away</div> |
|
|
</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<div style='height:18px'></div>", unsafe_allow_html=True) |
|
|
|
|
|
col_new, col_list = st.columns([1,1], gap="large") |
|
|
|
|
|
with col_new: |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>NEW TASK</div>", 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("<div style='height:18px'></div>", unsafe_allow_html=True) |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>PRESETS</div>", 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"<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>MY TASKS ({len(tasks)})</div>", unsafe_allow_html=True) |
|
|
|
|
|
if not tasks: |
|
|
st.markdown("<div style='background:#0f0f18;border:1px dashed #1e1e2a;border-radius:12px;padding:36px 20px;text-align:center;color:#2e2e40;font-size:13px;'>No tasks yet β create one or pick a preset</div>", 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""" |
|
|
<div style="background:#0f0f18;border:1px solid #1e1e2a;border-radius:12px;padding:14px 16px;margin-bottom:10px;"> |
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap;"> |
|
|
<span style="width:6px;height:6px;border-radius:50%;background:{dot};display:inline-block;flex-shrink:0;"></span> |
|
|
<span style="font-size:13px;color:#eaeaf2;font-weight:500;flex:1;">{t['name']}</span> |
|
|
<span style="font-size:9px;padding:2px 9px;border-radius:20px;background:{sc_col}18;color:{sc_col};border:1px solid {sc_col}44;letter-spacing:.05em;">{_sched_label(sc)}</span> |
|
|
</div> |
|
|
<div style="font-size:11px;color:#55556e;margin-bottom:8px;">{t['prompt'][:130]}{"β¦" if len(t['prompt'])>130 else ""}</div> |
|
|
<div style="font-size:10px;color:#2e2e40;">Last run: {last} Β· {t.get('run_count',0)} runs</div> |
|
|
{f'<div style="font-size:11px;color:#8888a8;margin-top:8px;padding-top:8px;border-top:1px solid #1a1a22;">{res[:160]}{"β¦" if len(res)>160 else ""}</div>' if res else ""} |
|
|
</div>""", 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() |
|
|
|
|
|
|
|
|
st.markdown("<div style='height:20px'></div>", unsafe_allow_html=True) |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>LOGS</div>", unsafe_allow_html=True) |
|
|
logs = list(reversed(load_bg_logs()))[:8] |
|
|
if not logs: |
|
|
st.markdown("<div style='font-size:12px;color:#2e2e40;'>No logs yet.</div>", 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""" |
|
|
<div style="background:#0f0f18;border:1px solid #1e1e2a;border-radius:10px;padding:12px 14px;margin-bottom:8px;"> |
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;"> |
|
|
<span style="font-size:9px;padding:2px 8px;border-radius:20px;background:{sc}18;color:{sc};border:1px solid {sc}44;">{log.get('status','?')}</span> |
|
|
<span style="font-size:11px;color:#8888a8;">{log.get('name','?')}</span> |
|
|
<span style="font-size:10px;color:#2e2e40;margin-left:auto;">{ts}</span> |
|
|
</div> |
|
|
<div style="font-size:12px;color:#b0b0c8;line-height:1.6;">{log.get('output','')[:280]}{"β¦" if len(log.get('output',''))>280 else ""}</div> |
|
|
</div>""", unsafe_allow_html=True) |
|
|
if st.button("Clear Logs", key="clr_logs"): |
|
|
save_bg_logs([]); st.rerun() |
|
|
|
|
|
|
|
|
|
|
|
with tab_training: |
|
|
|
|
|
st.markdown(""" |
|
|
<div style="padding:22px 0 18px;border-bottom:1px solid #1a1a22;"> |
|
|
<div style="font-family:'DM Serif Display',serif;font-size:24px;color:#eaeaf2;font-weight:400;letter-spacing:-0.025em;">Training</div> |
|
|
<div style="font-size:11px;color:#55556e;margin-top:4px;">Everything Cbae knows β browse, add, and remove memories</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
st.markdown("<div style='height:16px'></div>", 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""" |
|
|
<div style="display:flex;gap:12px;margin-bottom:26px;flex-wrap:wrap;"> |
|
|
{"".join(f'<div style="flex:1;min-width:120px;background:#0f0f18;border:1px solid #1e1e2a;border-radius:12px;padding:14px 18px;"><div style="font-size:26px;font-weight:600;color:{c};font-family:\'DM Serif Display\',serif;">{v}</div><div style="font-size:10px;color:#55556e;margin-top:3px;letter-spacing:.07em;text-transform:uppercase;">{lbl}</div></div>' for v,c,lbl in [(total_vecs,"#c8a96e","Memories"),(chat_count,"#4ecdc4","Chat turns"),(notes_count,"#8888a8","Notes"),(bg_count,"#6ee7f7","Tasks")])} |
|
|
</div> |
|
|
""", 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"<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:6px;'>{label}</div>", unsafe_allow_html=True) |
|
|
st.markdown(f"<div style='font-size:12px;color:#8888a8;margin-bottom:10px;'>{desc}</div>", 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("<div style='height:14px'></div>", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:6px;'>TEACH MANUALLY</div>", 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("<div style='height:14px'></div>", unsafe_allow_html=True) |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:6px;'>BULK PASTE (one per line)</div>", 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("<div style='height:14px'></div>", 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("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:8px;'>MEMORY BROWSER</div>", 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"<div style='font-size:11px;color:#55556e;margin-bottom:10px;'>{len(hits)} results</div>", 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""" |
|
|
<div style="background:#0f0f18;border:1px solid #1e1e2a;border-radius:10px;padding:12px 14px;margin-bottom:8px;"> |
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;"> |
|
|
<span style="font-size:9px;padding:2px 7px;border-radius:20px;background:{col}18;color:{col};border:1px solid {col}44;">{mt}</span> |
|
|
<span style="font-size:10px;color:#55556e;">{src}</span> |
|
|
<span style="font-size:10px;color:#2e2e40;margin-left:auto;">{round(h.score,3)}</span> |
|
|
</div> |
|
|
<div style="font-size:12px;color:#b0b0c8;line-height:1.6;">{txt[:220]}{"β¦" if len(txt)>220 else ""}</div> |
|
|
</div>""", 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("<div style='background:#0f0f18;border:1px dashed #1e1e2a;border-radius:12px;padding:48px 20px;text-align:center;color:#2e2e40;font-size:13px;'>Search above to browse memories</div>", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
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""" |
|
|
<div style="padding:22px 0 18px;border-bottom:1px solid #1a1a22;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;"> |
|
|
<div> |
|
|
<div style="font-family:'DM Serif Display',serif;font-size:24px;color:#eaeaf2;font-weight:400;letter-spacing:-0.025em;">Settings</div> |
|
|
<div style="font-size:11px;color:#55556e;margin-top:4px;">Keys Β· model Β· profile</div> |
|
|
</div> |
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;"> |
|
|
{"".join(f'<div style="font-size:11px;padding:4px 11px;border-radius:20px;background:{"rgba(78,205,196,0.08)" if ok else "rgba(224,85,85,0.07)"};color:{"#4ecdc4" if ok else "#e05555"};border:1px solid {"rgba(78,205,196,0.2)" if ok else "rgba(224,85,85,0.2)"};">{lbl}</div>' for lbl,ok in [("OpenRouter",or_ok),("Pinecone",pc_ok),("Moltbook",mb_ok)])} |
|
|
</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<div style='height:20px'></div>", unsafe_allow_html=True) |
|
|
cl, cr = st.columns(2, gap="large") |
|
|
|
|
|
with cl: |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>API KEYS</div>", 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("<div style='height:12px'></div>", unsafe_allow_html=True) |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>MODEL</div>", 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("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:10px;'>PROFILE</div>", 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("<div style='font-size:9px;color:#55556e;letter-spacing:.1em;text-transform:uppercase;margin:10px 0 6px;'>Learned facts</div>", unsafe_allow_html=True) |
|
|
for fact in p["facts"][-5:]: |
|
|
st.markdown(f"<div style='font-size:11px;color:#6e6e8e;padding:5px 10px;background:#13131c;border-radius:6px;margin-bottom:3px;border:1px solid #1e1e2a;'>Β· {fact[:80]}</div>", unsafe_allow_html=True) |
|
|
st.markdown("<div style='height:12px'></div>", unsafe_allow_html=True) |
|
|
st.markdown("<div style='font-size:9px;color:#55556e;letter-spacing:.14em;text-transform:uppercase;font-weight:600;margin-bottom:8px;'>QUICK TEACH</div>", 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("<div style='height:22px'></div>", 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) |
|
|
|
|
|
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") |
|
|
|