From 036dabbb00d989e01c203a31066a106f608674c1 Mon Sep 17 00:00:00 2001 From: JamesTheGiblet Date: Mon, 29 Dec 2025 16:03:21 +0000 Subject: [PATCH] feat: implement feedback system and integration tests for BuddAI --- README.md | 6 +- __pycache__/buddai_v3.2.cpython-313.pyc | Bin 68839 -> 71279 bytes buddai_v3.2.py | 78 +++++++++--- frontend/index.html | 45 ++++++- tests/test_buddai.py | 43 +++++++ tests/test_integration.py | 162 ++++++++++++++++++++++++ 6 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 tests/test_integration.py diff --git a/README.md b/README.md index 2196e26..5ee3323 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Status: PRODUCTION](https://img.shields.io/badge/Status-PRODUCTION-green.svg)](https://github.com/JamesTheGiblet/BuddAI) [![Version: v3.1](https://img.shields.io/badge/Version-v3.1-blue.svg)](https://github.com/JamesTheGiblet/BuddAI/releases) -[![Tests: 11/11](https://img.shields.io/badge/Tests-11%2F11%20Passing-brightgreen.svg)](https://github.com/JamesTheGiblet/BuddAI/actions) +[![Tests: 24/24](https://img.shields.io/badge/Tests-24%2F24%20Passing-brightgreen.svg)](https://github.com/JamesTheGiblet/BuddAI/actions) --- @@ -976,7 +976,11 @@ void updateLEDPattern() { ### Run the Test Suite ```bash +# Unit Tests python tests/test_buddai.py + +# Integration Tests +python tests/test_integration.py ``` ### Test Coverage (11/11 Passing) diff --git a/__pycache__/buddai_v3.2.cpython-313.pyc b/__pycache__/buddai_v3.2.cpython-313.pyc index 98b7e73a7234d24b66de1284e0cda73a5e4d1d0d..c528e0268577bcd3b1066031e5bfa92df9f316e3 100644 GIT binary patch delta 9714 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 diff --git a/buddai_v3.2.py b/buddai_v3.2.py index e3c2e79..ff1ddbf 100644 --- a/buddai_v3.2.py +++ b/buddai_v3.2.py @@ -283,6 +283,7 @@ class BuddAI: def __init__(self, user_id: str = "default", server_mode: bool = False): self.user_id = user_id + self.last_generated_id = None self.ensure_data_dir() self.init_database() self.session_id = self.create_session() @@ -368,6 +369,15 @@ class BuddAI: except sqlite3.OperationalError: pass + cursor.execute(""" + CREATE TABLE IF NOT EXISTS feedback ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER, + positive BOOLEAN, + timestamp TIMESTAMP + ) + """) + conn.commit() conn.close() @@ -404,15 +414,17 @@ class BuddAI: conn.commit() conn.close() - def save_message(self, role: str, content: str) -> None: + def save_message(self, role: str, content: str) -> int: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( "INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)", (self.session_id, role, content, datetime.now().isoformat()) ) + msg_id = cursor.lastrowid conn.commit() conn.close() + return msg_id def index_local_repositories(self, root_path: str) -> None: """Crawl directories and index .py, .ino, and .cpp files""" @@ -833,6 +845,25 @@ float applyForge(float current, float target, float k) {{ return target + (curre return generated_code + def record_feedback(self, message_id: int, feedback: bool) -> None: + """Learn from user feedback.""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO feedback (message_id, positive, timestamp) + VALUES (?, ?, ?) + """, (message_id, feedback, datetime.now().isoformat())) + conn.commit() + conn.close() + + # Adjust confidence scores + self.update_style_confidence(message_id, feedback) + + def update_style_confidence(self, message_id: int, positive: bool) -> None: + """Adjust confidence of style preferences based on feedback.""" + # Placeholder for V4.0 learning loop + pass + def _route_request(self, user_message: str, force_model: Optional[str], forge_mode: str) -> str: """Route the request to the appropriate model or handler.""" # Determine model based on complexity @@ -865,8 +896,8 @@ float applyForge(float current, float target, float k) {{ return target + (curre if style_context: self.context_messages.append({"role": "system", "content": style_context}) - self.save_message("user", user_message) - self.context_messages.append({"role": "user", "content": user_message, "timestamp": datetime.now().isoformat()}) + user_msg_id = self.save_message("user", user_message) + self.context_messages.append({"id": user_msg_id, "role": "user", "content": user_message, "timestamp": datetime.now().isoformat()}) full_response = "" @@ -899,8 +930,9 @@ float applyForge(float current, float target, float k) {{ return target + (curre full_response += bar yield bar - self.save_message("assistant", full_response) - self.context_messages.append({"role": "assistant", "content": full_response, "timestamp": datetime.now().isoformat()}) + msg_id = self.save_message("assistant", full_response) + self.last_generated_id = msg_id + self.context_messages.append({"id": msg_id, "role": "assistant", "content": full_response, "timestamp": datetime.now().isoformat()}) # --- Main Chat Method --- def chat(self, user_message: str, force_model: Optional[str] = None, forge_mode: str = "2") -> str: @@ -909,17 +941,17 @@ float applyForge(float current, float target, float k) {{ return target + (curre if style_context: self.context_messages.append({"role": "system", "content": style_context}) - - self.save_message("user", user_message) - self.context_messages.append({"role": "user", "content": user_message, "timestamp": datetime.now().isoformat()}) + user_msg_id = self.save_message("user", user_message) + self.context_messages.append({"id": user_msg_id, "role": "user", "content": user_message, "timestamp": datetime.now().isoformat()}) # Direct Schedule Check if "what should i be doing" in user_message.lower() or "my schedule" in user_message.lower() or "schedule check" in user_message.lower(): status = self.get_user_status() response = f"📅 **Schedule Check**\nAccording to your protocol, you should be: **{status}**" print(f"⏰ Schedule check triggered: {status}") - self.save_message("assistant", response) - self.context_messages.append({"role": "assistant", "content": response, "timestamp": datetime.now().isoformat()}) + msg_id = self.save_message("assistant", response) + self.last_generated_id = msg_id + self.context_messages.append({"id": msg_id, "role": "assistant", "content": response, "timestamp": datetime.now().isoformat()}) return response response = self._route_request(user_message, force_model, forge_mode) @@ -933,8 +965,9 @@ float applyForge(float current, float target, float k) {{ return target + (curre bar = "\n\nPROACTIVE: > " + " ".join([f"{i+1}. {s}" for i, s in enumerate(suggestions)]) response += bar - self.save_message("assistant", response) - self.context_messages.append({"role": "assistant", "content": response, "timestamp": datetime.now().isoformat()}) + msg_id = self.save_message("assistant", response) + self.last_generated_id = msg_id + self.context_messages.append({"id": msg_id, "role": "assistant", "content": response, "timestamp": datetime.now().isoformat()}) return response @@ -975,15 +1008,15 @@ float applyForge(float current, float target, float k) {{ return target + (curre conn.close() return [] - cursor.execute("SELECT role, content, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC", (session_id,)) + cursor.execute("SELECT id, role, content, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC", (session_id,)) rows = cursor.fetchall() conn.close() self.session_id = session_id self.context_messages = [] loaded_history = [] - for role, content, ts in rows: - msg = {"role": role, "content": content, "timestamp": ts} + for msg_id, role, content, ts in rows: + msg = {"id": msg_id, "role": role, "content": content, "timestamp": ts} self.context_messages.append(msg) loaded_history.append(msg) return loaded_history @@ -1049,7 +1082,6 @@ float applyForge(float current, float target, float k) {{ return target + (curre # --- Server Implementation --- if SERVER_AVAILABLE: - app = FastAPI(title="BuddAI API", version="3.1") app = FastAPI(title="BuddAI API", version="3.2") # Allow React frontend to communicate @@ -1075,6 +1107,10 @@ if SERVER_AVAILABLE: class SessionDeleteRequest(BaseModel): session_id: str + class FeedbackRequest(BaseModel): + message_id: int + positive: bool + # Multi-user support class BuddAIManager: def __init__(self): @@ -1174,7 +1210,7 @@ if SERVER_AVAILABLE: async def chat_endpoint(request: ChatRequest, user_id: str = Header("default")): server_buddai = buddai_manager.get_instance(user_id) response = server_buddai.chat(request.message, force_model=request.model, forge_mode=request.forge_mode) - return {"response": response} + return {"response": response, "message_id": server_buddai.last_generated_id} @app.websocket("/api/ws/chat") async def websocket_endpoint(websocket: WebSocket): @@ -1192,10 +1228,16 @@ if SERVER_AVAILABLE: for chunk in server_buddai.chat_stream(user_message, model, forge_mode): await websocket.send_json({"type": "token", "content": chunk}) - await websocket.send_json({"type": "end"}) + await websocket.send_json({"type": "end", "message_id": server_buddai.last_generated_id}) except WebSocketDisconnect: pass + @app.post("/api/feedback") + async def feedback_endpoint(req: FeedbackRequest, user_id: str = Header("default")): + server_buddai = buddai_manager.get_instance(user_id) + server_buddai.record_feedback(req.message_id, req.positive) + return {"status": "success"} + @app.get("/api/history") async def history_endpoint(user_id: str = Header("default")): server_buddai = buddai_manager.get_instance(user_id) diff --git a/frontend/index.html b/frontend/index.html index ba512aa..06ab0fc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -205,6 +205,7 @@ const [showSidebar, setShowSidebar] = useState(true); const [editingSession, setEditingSession] = useState(null); const [renameText, setRenameText] = useState(""); + const [feedbackGiven, setFeedbackGiven] = useState({}); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [status, setStatus] = useState("connecting"); @@ -307,6 +308,24 @@ fetchSessions(); }; + const handleFeedback = async (messageId, positive) => { + if (!messageId || feedbackGiven[messageId]) return; + + setFeedbackGiven(prev => ({ ...prev, [messageId]: positive ? 'positive' : 'negative' })); + + try { + await fetch("/api/feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message_id: messageId, positive: positive }) + }); + } catch (e) { + console.error("Feedback submission failed", e); + // Revert UI on failure + setFeedbackGiven(prev => { const n = {...prev}; delete n[messageId]; return n; }); + } + }; + const loadSession = async (sessionId) => { setLoading(true); try { @@ -363,6 +382,14 @@ }); } else if (data.type === 'end') { setLoading(false); + setHistory(prev => { + const newHistory = [...prev]; + const lastMsg = newHistory[newHistory.length - 1]; + if (lastMsg && lastMsg.role === 'assistant') { + lastMsg.id = data.message_id; + } + return newHistory; + }); if (!currentSessionId) fetchSessions(); } }; @@ -375,7 +402,7 @@ body: JSON.stringify({ message: msgText, forge_mode: forgeMode }) }); const data = await res.json(); - setHistory(prev => [...prev, { role: "assistant", content: data.response }]); + setHistory(prev => [...prev, { role: "assistant", content: data.response, id: data.message_id }]); if (!currentSessionId) fetchSessions(); } catch (err) { setHistory(prev => [...prev, { role: "assistant", content: "Error connecting to BuddAI server." }]); @@ -503,6 +530,22 @@ ))} )} + {msg.role === 'assistant' && msg.id && !loading && ( +
+ + +
+ )} ); })} diff --git a/tests/test_buddai.py b/tests/test_buddai.py index 4457311..3b69320 100644 --- a/tests/test_buddai.py +++ b/tests/test_buddai.py @@ -922,6 +922,48 @@ def test_connection_pool(): print_fail(f"Pool overflow handling failed. Size: {pool.pool.qsize()}") return False +# Test 20: Feedback System +def test_feedback_system(): + print_test("Feedback System") + + # Use a named temporary file for DB + fd, test_db_path = tempfile.mkstemp(suffix=".db") + os.close(fd) + test_db = Path(test_db_path) + + try: + with patch('buddai_v3_2.DB_PATH', test_db): + # Suppress prints + with patch('builtins.print'): + buddai = BuddAI(server_mode=False) + + # 1. Create a message to rate + msg_id = buddai.save_message("assistant", "Test response") + + # 2. Record positive feedback + buddai.record_feedback(msg_id, True) + + # 3. Verify in DB + conn = sqlite3.connect(test_db) + cursor = conn.cursor() + cursor.execute("SELECT positive FROM feedback WHERE message_id = ?", (msg_id,)) + row = cursor.fetchone() + conn.close() + + if row and row[0] == 1: # Boolean true is 1 in sqlite + print_pass("Positive feedback recorded successfully") + return True + else: + print_fail(f"Feedback not recorded correctly. Got: {row}") + return False + + finally: + try: + if test_db.exists(): + os.unlink(test_db) + except Exception: + pass + # Main Test Runner def run_all_tests(): print("\n" + "="*60) @@ -948,6 +990,7 @@ def run_all_tests(): ("Upload Security", test_upload_security), ("WebSocket Logic", test_websocket_logic), ("Connection Pooling", test_connection_pool), + ("Feedback System", test_feedback_system), ] results = [] diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..c3d54cb --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +BuddAI v3.2 Integration Test Suite +Tests API endpoints and server integration + +Author: James Gilbert +License: MIT +""" + +import sys +import os +import importlib.util +import tempfile +import unittest +from unittest.mock import patch, MagicMock +from pathlib import Path +import json + +# Dynamic import of buddai_v3.2.py +REPO_ROOT = Path(__file__).parent.parent +MODULE_PATH = REPO_ROOT / "buddai_v3.2.py" +spec = importlib.util.spec_from_file_location("buddai_v3_2", MODULE_PATH) +buddai_module = importlib.util.module_from_spec(spec) +sys.modules["buddai_v3_2"] = buddai_module +spec.loader.exec_module(buddai_module) + +# Check for server dependencies +SERVER_AVAILABLE = getattr(buddai_module, "SERVER_AVAILABLE", False) + +if SERVER_AVAILABLE: + from fastapi.testclient import TestClient + app = buddai_module.app + client = TestClient(app) +else: + print("⚠️ Server dependencies missing. Integration tests skipped.") + +@unittest.skipUnless(SERVER_AVAILABLE, "Server dependencies not installed") +class TestBuddAIIntegration(unittest.TestCase): + + def setUp(self): + # Create a fresh temp DB for each test + self.db_fd, self.db_path = tempfile.mkstemp(suffix=".db") + os.close(self.db_fd) + + # Patch DB_PATH in the module + self.db_patcher = patch("buddai_v3_2.DB_PATH", Path(self.db_path)) + self.mock_db_path = self.db_patcher.start() + + # Reset the manager to ensure fresh BuddAI instances connected to temp DB + if hasattr(buddai_module, 'buddai_manager'): + buddai_module.buddai_manager.instances = {} + + # Suppress prints + self.print_patcher = patch("builtins.print") + self.print_patcher.start() + + def tearDown(self): + self.db_patcher.stop() + self.print_patcher.stop() + try: + os.unlink(self.db_path) + except: + pass + + def test_health_check(self): + """GET / returns 200 and status""" + response = client.get("/") + self.assertEqual(response.status_code, 200) + self.assertIn("BuddAI API Online", response.text) + + def test_chat_flow(self): + """POST /api/chat returns response""" + # Mock the internal chat method to avoid Ollama dependency + with patch.object(buddai_module.BuddAI, 'chat', return_value="Integrated Response") as mock_chat: + response = client.post("/api/chat", json={"message": "Hello API"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"response": "Integrated Response", "message_id": None}) + + # Verify user_id header handling (default) + mock_chat.assert_called_once() + + def test_session_lifecycle_api(self): + """Test full session CRUD via API""" + # 1. Create + resp = client.post("/api/session/new") + self.assertEqual(resp.status_code, 200) + session_id = resp.json()["session_id"] + + # 2. List + resp = client.get("/api/sessions") + self.assertEqual(resp.status_code, 200) + sessions = resp.json()["sessions"] + self.assertTrue(any(s["id"] == session_id for s in sessions)) + + # 3. Rename + new_title = "API Test Session" + resp = client.post("/api/session/rename", json={"session_id": session_id, "title": new_title}) + self.assertEqual(resp.status_code, 200) + + resp = client.get("/api/sessions") + updated_session = next(s for s in resp.json()["sessions"] if s["id"] == session_id) + self.assertEqual(updated_session["title"], new_title) + + # 4. Load + resp = client.post("/api/session/load", json={"session_id": session_id}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()["session_id"], session_id) + + # 5. Delete + resp = client.post("/api/session/delete", json={"session_id": session_id}) + self.assertEqual(resp.status_code, 200) + + resp = client.get("/api/sessions") + self.assertFalse(any(s["id"] == session_id for s in resp.json()["sessions"])) + + def test_multi_user_isolation_api(self): + """Verify data isolation between users via API headers""" + user1_headers = {"user-id": "user1"} + user2_headers = {"user-id": "user2"} + + # User 1 creates session + resp1 = client.post("/api/session/new", headers=user1_headers) + sid1 = resp1.json()["session_id"] + client.post("/api/session/rename", json={"session_id": sid1, "title": "User1 Chat"}, headers=user1_headers) + + # User 2 creates session + resp2 = client.post("/api/session/new", headers=user2_headers) + sid2 = resp2.json()["session_id"] + client.post("/api/session/rename", json={"session_id": sid2, "title": "User2 Chat"}, headers=user2_headers) + + # Verify User 1 sees only their session + list1 = client.get("/api/sessions", headers=user1_headers).json()["sessions"] + ids1 = [s["id"] for s in list1] + self.assertIn(sid1, ids1) + self.assertNotIn(sid2, ids1) + + # Verify User 2 sees only their session + list2 = client.get("/api/sessions", headers=user2_headers).json()["sessions"] + ids2 = [s["id"] for s in list2] + self.assertIn(sid2, ids2) + self.assertNotIn(sid1, ids2) + + def test_upload_api(self): + """Test file upload endpoint""" + with tempfile.TemporaryDirectory() as tmp_data_dir: + with patch("buddai_v3_2.DATA_DIR", Path(tmp_data_dir)): + # Mock indexing to avoid parsing logic + with patch.object(buddai_module.BuddAI, 'index_local_repositories') as mock_index: + + # Create dummy file + files = {'file': ('test.py', b'print("hello")', 'text/x-python')} + + response = client.post("/api/upload", files=files) + self.assertEqual(response.status_code, 200) + self.assertIn("Successfully indexed", response.json()["message"]) + mock_index.assert_called() + +if __name__ == '__main__': + print("\n" + "="*60) + print("🚀 BuddAI v3.2 Integration Tests") + print("="*60) + unittest.main(verbosity=2) \ No newline at end of file