1 # Main application entry point for NovaChat 2 3 from flask import Flask, render_template, session, jsonify, request 4 from flask_session import Session 5 import logging 6 import asyncio 7 from datetime import datetime 8 from ollama import AsyncClient, ChatResponse 9 from typing import Any 10 from werkzeug.exceptions import HTTPException 11 12 13 14 from modules.llm.ollama_client import list_models, pull_model 15 from modules.chat.chat_handler import process_message 16 from modules.authentication import configure_auth 17 from modules.search.news import fetch_news 18 from modules.search.arxiv import fetch_arxiv 19 from modules.search.web import fetch_ddg, fetch_torrents 20 21 # Configure logging 22 logging.basicConfig(level=logging.DEBUG) 23 logger = logging.getLogger(__name__) 24 logger.setLevel(logging.DEBUG) 25 26 # Create file handler which logs even debug messages 27 fh = logging.FileHandler('app.log') 28 fh.setLevel(logging.DEBUG) 29 30 # Create formatter and add it to the handlers 31 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 32 fh.setFormatter(formatter) 33 34 # Add the handlers to the logger 35 logger.addHandler(fh) 36 37 app = Flask(__name__, template_folder='frontend/templates') 38 app.config['SECRET_KEY'] = 'your-secret-key' 39 app.config['SESSION_TYPE'] = 'filesystem' 40 Session(app) 41 42 # Register error handlers 43 @app.errorhandler(Exception) 44 def handle_exception(e): 45 # Log the error 46 logger.error(f"Unhandled Exception: {str(e)}", exc_info=True) 47 48 # Handle HTTP exceptions 49 if isinstance(e, HTTPException): 50 response = { 51 "error": e.name, 52 "message": e.description, 53 "status": e.code, 54 "timestamp": datetime.now().isoformat() 55 } 56 return jsonify(response), e.code 57 58 # Handle other exceptions 59 response = { 60 "error": "Internal Server Error", 61 "message": "An unexpected error occurred", 62 "status": 500, 63 "timestamp": datetime.now().isoformat() 64 } 65 return jsonify(response), 500 66 67 @app.route('/') 68 def index(): 69 if 'chat_history' not in session: 70 session['chat_history'] = [] 71 return render_template('index.html', chat_history=session['chat_history']) 72 73 @app.route('/api/models') 74 def get_models(): 75 # Get configured required models 76 required_models = config['ollama'].get('required_models', []) 77 78 # Get available models from Ollama 79 available_models = list_models() 80 81 # Check and pull missing models 82 for model in required_models: 83 if model not in available_models: 84 try: 85 pull_model(model) 86 available_models.append(model) 87 app.logger.info(f'Auto-pulled required model: {model}') 88 except Exception as e: 89 app.logger.error(f'Failed to auto-pull {model}: {str(e)}') 90 91 # Set default model in session if not set 92 if 'selected_model' not in session and required_models: 93 session['selected_model'] = required_models[0] 94 95 return jsonify({ 96 'available': available_models, 97 'required': required_models, 98 'selected': session.get('selected_model') 99 }) 100 101 @app.route('/api/pull-model', methods=['POST']) 102 def add_model(): 103 model_name = request.json.get('model') 104 if not model_name: 105 return jsonify({'error': 'Model name required'}), 400 106 107 try: 108 pull_model(model_name) 109 return jsonify({'message': f'Model {model_name} pulled successfully'}), 200 110 except Exception as e: 111 app.logger.error(f'Model pull failed: {str(e)}') 112 return jsonify({ 113 'error': 'Model deployment failed', 114 'details': str(e), 115 'resolution': 'Check model name validity and network connection' 116 }), 500 117 118 119 120 121 import yaml 122 123 with open('config.yaml') as f: 124 config = yaml.safe_load(f) 125 126 # Initialize authentication 127 configure_auth(app) 128 129 # ——— System prompts ——— 130 # ——— Compatibility patch for asyncio.to_thread ——— 131 if not hasattr(asyncio, 'to_thread'): 132 def _to_thread(func, *args, **kwargs): 133 loop = asyncio.get_event_loop() 134 return loop.run_in_executor(None, lambda: func(*args, **kwargs)) 135 asyncio.to_thread = _to_thread 136 137 # ——— System prompts and session management ——— 138 from modules.system_prompts import system_prompts 139 140 MODE = "COGITO" 141 142 # ——— Tool functions and descriptors ——— 143 # ——— Chat endpoints ——— 144 @app.route('/chat', methods=['POST']) 145 async def chat() -> Any: 146 if not request.is_json: 147 return jsonify(error="Content-Type must be application/json"), 415 148 149 try: 150 request_data = request.get_json() 151 except Exception as e: 152 app.logger.error(f"Invalid JSON: {str(e)}") 153 return jsonify(error="Invalid JSON format"), 400 154 155 global MODE 156 user_message = request_data.get('message', '').strip() 157 app.logger.info("User ▶ %s", user_message) 158 if not user_message: 159 return jsonify(error="No message provided"), 400 160 161 # Mode toggles 162 if user_message == "Enable flirty mode": 163 MODE = "FLIRTY" 164 system_prompts.init_chat_history("FLIRTY") 165 elif user_message == "Enable companion mode": 166 MODE = "COMPANION" 167 system_prompts.init_chat_history("COMPANION") 168 elif user_message == "Enable investigative mode": 169 MODE = "INVESTIGATE" 170 system_prompts.init_chat_history("INVESTIGATE") 171 elif user_message == "Back to normal.": 172 MODE = "COGITO" 173 system_prompts.init_chat_history("COGITO") 174 else: 175 if 'chat_history' not in session: 176 init_chat_history(MODE) 177 session['chat_history'].append({"role": "user", "content": user_message}) 178 179 chat_history = session['chat_history'] 180 model_name = "hf.co/openerotica/..." if MODE == "FLIRTY" else "huihui_ai/qwen2.5-1m-abliterated" 181 client = AsyncClient() 182 183 response: ChatResponse = await client.chat( 184 model=model_name, 185 messages=chat_history, 186 tools=[fetch_news_tool, fetch_arxiv_tool, fetch_ddg_tool, fetch_torrents_tool], 187 options={"temperature": 0.4} 188 ) 189 190 assistant_text = response.message.content 191 192 # Process tool calls 193 if response.message.tool_calls: 194 for call in response.message.tool_calls: 195 fname = call.function.name 196 args = call.function.arguments or {} 197 if fname in available_functions: 198 app.logger.info("Invoking %s(%s)", fname, args) 199 result = await asyncio.to_thread(available_functions[fname], **args) 200 session['chat_history'].append({"role": "tool", "name": fname, "content": "\n\n".join(result)}) 201 202 # Rerun with tool outputs 203 follow_up: ChatResponse = await client.chat( 204 model=model_name, 205 messages=session['chat_history'], 206 options={"temperature": 0.4} 207 ) 208 assistant_text = follow_up.message.content 209 210 session['chat_history'].append({"role": "assistant", "content": assistant_text}) 211 session.modified = True 212 app.logger.info("Nova ▶ %s", assistant_text) 213 return jsonify(chat_history=session['chat_history']) 214 215 from flask import session 216 217 SYSTEM_PROMPT = "You are a helpful AI assistant." 218 219 def init_chat_history(mode="COGITO"): 220 """Initialize chat history with a system message (hidden from UI).""" 221 session['chat_history'] = [ 222 {"role": "system", "content": SYSTEM_PROMPT} 223 ] 224 session['chat_history'].append({"role": "system", "content": f"Mode set to {mode}."}) 225 226 @app.route('/clear', methods=['POST']) 227 def clear_history() -> Any: 228 init_chat_history(MODE) 229 return jsonify(chat_history=session['chat_history']) 230 231 from modules.tools import fetch_news_tool, fetch_arxiv_tool, fetch_ddg_tool, fetch_torrents_tool, available_functions 232 233 234 if __name__ == '__main__': 235 logging.basicConfig(level=logging.INFO) 236 logger = logging.getLogger(__name__) 237 # print("Starting Nova server on http://localhost:9125") 238 app.run(host=config['server']['host'], port=config['server']['port'], 239 debug=False, use_reloader=False)