feat: Upgrade to BuddAI v3.0 with server capabilities and new frontend interface

This commit is contained in:
JamesTheGiblet 2025-12-28 16:59:16 +00:00
parent 3747bf5091
commit ba814f0559
2 changed files with 365 additions and 28 deletions

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
BuddAI Executive v2.0 - Modular Builder
BuddAI Executive v3.0 - Modular Builder
Breaks complex tasks into manageable chunks
Author: James Gilbert
@ -13,7 +13,19 @@ import sqlite3
from datetime import datetime
from pathlib import Path
import http.client
import re
import re # noqa: F401
from typing import Optional
# Server dependencies
try:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import uvicorn
SERVER_AVAILABLE = True
except ImportError:
SERVER_AVAILABLE = False
# Configuration
OLLAMA_HOST = "localhost"
@ -229,14 +241,15 @@ class BuddAI:
output += f" ---\n\n"
return output
def __init__(self):
def __init__(self, server_mode=False):
self.ensure_data_dir()
self.init_database()
self.session_id = self.create_session()
self.server_mode = server_mode
self.context_messages = []
self.shadow_engine = ShadowSuggestionEngine(DB_PATH)
print("🧠 BuddAI Executive v2.0 - Modular Builder")
print("🔥 BuddAI Executive v3.0 - Modular Builder")
print("=" * 50)
print(f"Session: {self.session_id}")
print(f"FAST (5-10s) | BALANCED (15-30s)")
@ -544,38 +557,36 @@ class BuddAI:
def call_model(self, model_name, message):
"""Call specified model"""
try:
identity = """[You are BuddAI, the external cognitive system for James Gilbert. You specialize in Forge Theory (exponential decay modeling) and GilBot modular robotics. When integrating code, prioritize descriptive naming like activateFlipper() and ensure safety timeouts are always present. You represent 8 years of polymath experience.
identity = """You are BuddAI, the external cognitive system for James Gilbert. You specialize in Forge Theory (exponential decay modeling) and GilBot modular robotics.
YOUR PRIMARY JOB: Generate code when asked. ALWAYS generate code if requested.
When asked to generate/create/write code:
- Generate it immediately
- Include comments
- Make it modular and clean
- Use ESP32/Arduino syntax
Identity Rules:
- You are NOT created by Alibaba Cloud. You are a local Python system written by James Gilbert.
- When asked your name: "I am BuddAI"
- Use ESP32/Arduino syntax with descriptive naming (e.g., activateFlipper).
- Ensure safety timeouts are always present in motor code.
Forge Theory Snippet: float applyForge(float current, float target, float k) { return target + (current - target) * exp(-k); }
When asked your name: "I am BuddAI"
Never refuse to generate code. That's your purpose.
Be direct and helpful.]
"""
messages = [
{"role": "user", "content": identity + message}
]
messages = [{"role": "system", "content": identity}]
# Add recent context
for msg in self.context_messages[-3:]:
messages.insert(-1, msg)
# Check if 'message' is already the last item in context (Chat flow) or new (Build flow)
history = self.context_messages[-5:]
if history and history[-1]['content'] == message:
messages.extend(history)
else:
messages.extend(history)
messages.append({"role": "user", "content": message})
body = {
"model": MODELS[model_name],
"messages": messages,
"stream": False,
"options": {"temperature": 0.7, "num_ctx": 2048}
"options": {"temperature": 0.7, "num_ctx": 4096}
}
conn = http.client.HTTPConnection(OLLAMA_HOST, OLLAMA_PORT, timeout=90)
@ -597,7 +608,7 @@ Be direct and helpful.]
if 'conn' in locals():
conn.close()
def execute_modular_build(self, _, modules, plan):
def execute_modular_build(self, _, modules, plan, forge_mode="2"):
"""Execute build plan step by step"""
print(f"\n🔨 MODULAR BUILD MODE")
print(f"Detected {len(modules)} modules: {', '.join(modules)}")
@ -619,7 +630,11 @@ Be direct and helpful.]
print("1. Aggressive (k=0.3) - High snap, combat ready")
print("2. Balanced (k=0.1) - Standard movement")
print("3. Graceful (k=0.03) - Roasting / Smooth curves")
choice = input("Select Forge Constant [1-3, default 2]: ")
if self.server_mode:
choice = forge_mode
else:
choice = input("Select Forge Constant [1-3, default 2]: ")
k_val = "0.1"
if choice == "1": k_val = "0.3"
@ -664,7 +679,7 @@ Be direct and helpful.]
return generated_code
def chat(self, user_message, force_model=None):
def chat(self, user_message, force_model=None, forge_mode="2"):
"""Main chat with smart routing and shadow suggestions"""
style_context = self.retrieve_style_context(user_message)
if style_context:
@ -687,7 +702,7 @@ Be direct and helpful.]
print(f"Modules needed: {', '.join(modules)}")
print(f"Breaking into {len(plan)} manageable steps")
print("=" * 50)
response = self.execute_modular_build(user_message, modules, plan)
response = self.execute_modular_build(user_message, modules, plan, forge_mode)
elif self.is_search_query(user_message):
# This is a search query - query the database
response = self.search_repositories(user_message)
@ -765,6 +780,44 @@ Be direct and helpful.]
self.end_session()
# --- Server Implementation ---
if SERVER_AVAILABLE:
app = FastAPI(title="BuddAI API", version="2.0")
# Allow React frontend to communicate
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class ChatRequest(BaseModel):
message: str
model: Optional[str] = None
forge_mode: Optional[str] = "2"
# Initialize server instance
server_buddai = BuddAI(server_mode=True)
# Serve Frontend
frontend_path = Path(__file__).parent / "frontend"
frontend_path.mkdir(exist_ok=True)
app.mount("/web", StaticFiles(directory=frontend_path, html=True), name="web")
@app.get("/")
async def root():
return {"status": "online", "message": "🔥 BuddAI API is running. Visit /web for the interface or /docs for API documentation."}
@app.post("/api/chat")
async def chat_endpoint(request: ChatRequest):
response = server_buddai.chat(request.message, force_model=request.model, forge_mode=request.forge_mode)
return {"response": response}
@app.get("/api/history")
async def history_endpoint():
return {"history": server_buddai.context_messages}
def check_ollama():
try:
conn = http.client.HTTPConnection(OLLAMA_HOST, OLLAMA_PORT, timeout=5)
@ -781,8 +834,15 @@ def main():
print("❌ Ollama not running. Start: ollama serve")
sys.exit(1)
buddai = BuddAI()
buddai.run()
if len(sys.argv) > 1 and sys.argv[1] == "--server":
if SERVER_AVAILABLE:
print("🚀 Starting BuddAI API Server on port 8000...")
uvicorn.run(app, host="0.0.0.0", port=8000)
else:
print("❌ Server dependencies missing. Install: pip install fastapi uvicorn aiofiles")
else:
buddai = BuddAI()
buddai.run()
if __name__ == "__main__":

277
frontend/index.html Normal file
View file

@ -0,0 +1,277 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>">
<title>🔥 BuddAI Web</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--bg-color: #1e1e1e;
--text-color: #d4d4d4;
--header-bg: #252526;
--border-color: #3e3e3e;
--input-bg: #3c3c3c;
--user-msg-bg: #007acc;
--user-msg-text: white;
--assistant-msg-bg: #2d2d2d;
--btn-bg: #0e639c;
--btn-hover: #1177bb;
--code-bg: #111;
--code-border: #444;
--code-text: #9cdcfe;
}
body.light-mode {
--bg-color: #ffffff;
--text-color: #333333;
--header-bg: #f3f3f3;
--border-color: #e1e1e1;
--input-bg: #ffffff;
--user-msg-bg: #0078d4;
--user-msg-text: white;
--assistant-msg-bg: #f4f4f4;
--btn-bg: #0078d4;
--btn-hover: #106ebe;
--code-bg: #f6f8fa;
--code-border: #d1d5da;
--code-text: #24292e;
}
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: var(--bg-color); color: var(--text-color); margin: 0; display: flex; justify-content: center; height: 100vh; transition: background 0.3s, color 0.3s; }
#root { width: 100%; max-width: 900px; display: flex; flex-direction: column; height: 100%; }
.chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
.message { padding: 15px; border-radius: 8px; max-width: 85%; line-height: 1.5; }
.user { align-self: flex-end; background: var(--user-msg-bg); color: var(--user-msg-text); }
.assistant { align-self: flex-start; background: var(--assistant-msg-bg); border: 1px solid var(--border-color); }
.input-area { padding: 20px; background: var(--header-bg); border-top: 1px solid var(--border-color); display: flex; gap: 10px; }
input { flex: 1; padding: 12px; border-radius: 4px; border: 1px solid var(--border-color); background: var(--input-bg); color: var(--text-color); outline: none; }
button { padding: 12px 24px; background: var(--btn-bg); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
button:hover { background: var(--btn-hover); }
.stop-btn { background: #d32f2f; }
.stop-btn:hover { background: #b71c1c; }
.header { padding: 15px 20px; background: var(--header-bg); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.clear-btn { background: transparent; border: 1px solid var(--border-color); color: var(--text-color); padding: 5px 12px; font-size: 0.8em; cursor: pointer; border-radius: 4px; opacity: 0.8; }
.clear-btn:hover { background: var(--border-color); opacity: 1; }
.status-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; margin-left: 10px; text-transform: uppercase; font-weight: bold; letter-spacing: 0.5px; }
.online { color: #4caf50; border: 1px solid #4caf50; background: rgba(76, 175, 80, 0.1); }
.offline { color: #f44336; border: 1px solid #f44336; background: rgba(244, 67, 54, 0.1); }
.connecting { color: #ff9800; border: 1px solid #ff9800; background: rgba(255, 152, 0, 0.1); }
.suggestions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
.suggestion-pill { background: var(--bg-color); border: 1px solid var(--btn-bg); color: var(--btn-bg); padding: 6px 12px; border-radius: 15px; font-size: 0.85em; cursor: pointer; transition: all 0.2s; }
.suggestion-pill:hover { background: var(--btn-bg); color: white; }
/* Code Blocks */
.code-wrapper { position: relative; margin: 10px 0; border: 1px solid var(--code-border); border-radius: 6px; overflow: hidden; }
.code-header { display: flex; justify-content: space-between; align-items: center; background: var(--header-bg); padding: 5px 10px; font-size: 0.8em; border-bottom: 1px solid var(--border-color); color: var(--text-color); opacity: 0.8; }
.copy-code-btn { background: transparent; border: 1px solid var(--border-color); color: var(--text-color); padding: 2px 8px; font-size: 0.9em; cursor: pointer; border-radius: 3px; }
.copy-code-btn:hover { background: var(--border-color); }
pre { background: var(--code-bg); padding: 15px; margin: 0; overflow-x: auto; }
code { font-family: 'Consolas', 'Courier New', monospace; color: var(--code-text); }
p { margin: 0 0 10px 0; }
@keyframes flame-flicker {
0% { transform: scale(1); filter: drop-shadow(0 0 2px #ff9800); }
50% { transform: scale(1.1) rotate(-2deg); filter: drop-shadow(0 0 6px #ff4500); }
100% { transform: scale(1) rotate(2deg); filter: drop-shadow(0 0 2px #ff9800); }
}
.loading-flame { font-size: 24px; animation: flame-flicker 0.6s infinite; display: inline-block; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// Configure Marked for Code Copy
const renderer = new marked.Renderer();
renderer.code = (code, language) => {
const validLang = language || 'text';
const escapeHtml = (unsafe) => unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return `<div class="code-wrapper">
<div class="code-header">
<span>${validLang}</span>
<button class="copy-code-btn" onclick="window.copyToClipboard(this)">Copy</button>
</div>
<pre><code class="hljs ${validLang}">${escapeHtml(code)}</code></pre>
</div>`;
};
marked.setOptions({ renderer });
window.copyToClipboard = (btn) => {
const wrapper = btn.closest('.code-wrapper');
const code = wrapper.querySelector('code').innerText;
navigator.clipboard.writeText(code).then(() => {
const original = btn.innerText;
btn.innerText = 'Copied!';
setTimeout(() => btn.innerText = original, 2000);
});
};
const { useState, useEffect, useRef } = React;
function App() {
const [history, setHistory] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState("connecting");
const [forgeMode, setForgeMode] = useState("2");
const [theme, setTheme] = useState("dark");
const endRef = useRef(null);
const abortControllerRef = useRef(null);
const scrollToBottom = () => {
endRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
document.body.className = theme === 'light' ? 'light-mode' : '';
}, [theme]);
useEffect(() => {
scrollToBottom();
}, [history]);
useEffect(() => {
// Check System Status
const checkStatus = async () => {
try {
const res = await fetch("/");
setStatus(res.ok ? "online" : "offline");
} catch {
setStatus("offline");
}
};
checkStatus();
const timer = setInterval(checkStatus, 10000);
// Load History
fetch("/api/history")
.then(res => res.json())
.then(data => {
if (data.history) {
setHistory(data.history.filter(m => m.role === 'user' || m.role === 'assistant'));
}
})
.catch(console.error);
return () => clearInterval(timer);
}, []);
const sendMessage = async (textOverride = null) => {
const msgText = typeof textOverride === 'string' ? textOverride : input;
if (!msgText.trim()) return;
const userMsg = { role: "user", content: msgText };
setHistory(prev => [...prev, userMsg]);
if (!textOverride) setInput("");
setLoading(true);
// Cancel previous request if any
if (abortControllerRef.current) abortControllerRef.current.abort();
const controller = new AbortController();
abortControllerRef.current = controller;
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: msgText, forge_mode: forgeMode }),
signal: controller.signal
});
const data = await res.json();
setHistory(prev => [...prev, { role: "assistant", content: data.response }]);
} catch (err) {
if (err.name === 'AbortError') {
setHistory(prev => [...prev, { role: "assistant", content: "🛑 *Generation stopped by user.*" }]);
} else {
setHistory(prev => [...prev, { role: "assistant", content: "Error connecting to BuddAI server." }]);
}
}
setLoading(false);
abortControllerRef.current = null;
};
const stopGeneration = () => {
if (abortControllerRef.current) abortControllerRef.current.abort();
};
const parseContent = (content) => {
const parts = content.split("\n\nPROACTIVE: > ");
const text = parts[0];
let suggestions = [];
if (parts.length > 1) {
// Split "1. Suggestion 2. Suggestion" patterns
suggestions = parts[1].split(/\d+\.\s/).map(s => s.trim()).filter(s => s);
}
return { text, suggestions };
};
return (
<>
<div className="header">
<div style={{display:'flex', alignItems:'center'}}>
<h3 style={{margin:0}}>🔥 BuddAI v3.0</h3>
<span className={`status-badge ${status}`}>{status}</span>
</div>
<div style={{display:'flex', gap:'10px'}}>
<button className="clear-btn" onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme === 'dark' ? '☀️' : '🌙'}</button>
<select
value={forgeMode}
onChange={(e) => setForgeMode(e.target.value)}
style={{background: 'var(--input-bg)', color: 'var(--text-color)', border: '1px solid var(--border-color)', padding: '5px', borderRadius: '4px', fontSize: '0.8em'}}>
<option value="1">Aggressive (Combat)</option>
<option value="2">Balanced (Standard)</option>
<option value="3">Graceful (Smooth)</option>
</select>
<button className="clear-btn" onClick={() => setHistory([])}>Clear</button>
</div>
</div>
<div className="chat-container">
{history.length === 0 && <div style={{textAlign: 'center', marginTop: '50px', color: '#666'}}><p>Ready to build.</p></div>}
{history.map((msg, i) => {
const { text, suggestions } = msg.role === 'assistant' ? parseContent(msg.content) : { text: msg.content, suggestions: [] };
return (
<div key={i} className={`message ${msg.role}`}>
<div dangerouslySetInnerHTML={{ __html: marked.parse(text) }} />
{suggestions.length > 0 && (
<div className="suggestions">
{suggestions.map((s, idx) => (
<div key={idx} className="suggestion-pill" onClick={() => sendMessage(s)}>{s}</div>
))}
</div>
)}
</div>
);
})}
{loading && <div className="message assistant"><span className="loading-flame">🔥</span></div>}
<div ref={endRef} />
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === 'Enter' && sendMessage()}
placeholder="Ask BuddAI to build something..."
autoFocus
/>
{loading ? (
<button className="stop-btn" onClick={stopGeneration}>Stop</button>
) : (
<button onClick={() => sendMessage()}>Send</button>
)}
</div>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>