Format: text/x-python
⬇ Download File (8.1 KB)
   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)