agrogpt / app.py
harivarshannn
Fix LLM prompt to prevent RAW vector document hallucination
c5f92c0
Raw
History Blame Contribute Delete
12.2 kB
import os
import sys
import platform
import argparse
from pathlib import Path
from flask import Flask, render_template, request, jsonify
import threading
import time
from werkzeug.utils import secure_filename
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import uuid
import requests
# Import the new model functionality
from model import check_ollama_connection, generate_with_ollama, analyze_image_for_disease
from weather import get_current_weather, TN_DISTRICTS
from rag_engine import initialize_knowledge_base, query_rag
app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable static file caching
# ----- DATABASE, AUTH & LIMITER SETUP -----
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default_agrogpt_secret_key')
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
app.config['SESSION_COOKIE_SECURE'] = True
database_url = os.environ.get('DATABASE_URL')
if database_url and database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql://", 1)
app.config['SQLALCHEMY_DATABASE_URI'] = database_url or 'sqlite:///agrogpt.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.unauthorized_handler
def unauthorized():
return jsonify({'error': 'Please log in to use this feature.'}), 401
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["1000 per day", "100 per hour"],
storage_uri="memory://"
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
full_name = db.Column(db.String(120), nullable=False)
profession = db.Column(db.String(80), nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
with app.app_context():
db.create_all()
# ------------------------------------------
@app.after_request
def add_no_cache_headers(response):
"""Prevent browser from caching static files during development."""
if request.path.startswith('/static/'):
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
# Configuration for file uploads
if os.environ.get('VERCEL_ENV') or os.environ.get('VERCEL'):
UPLOAD_FOLDER = '/tmp'
else:
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# Create uploads directory if it doesn't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Global status
ollama_connected = False
status_message = "Initializing..."
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def print_header() -> None:
print("AgroGPT Web Interface (Ollama Edition) starting...", flush=True)
print(f"Python: {platform.python_version()} ({sys.executable})", flush=True)
def check_backend_status():
"""Check Ollama status in background"""
global ollama_connected, status_message
status_message = "Connecting to Ollama (via Groq API)..."
print("Connecting to Ollama...", flush=True)
if check_ollama_connection():
ollama_connected = True
status_message = "Connected to Ollama! Web interface is ready."
print(status_message, flush=True)
else:
ollama_connected = False
status_message = "Error: Could not connect to Ollama. Ensure GROQ_API_KEY is set in .env."
print(status_message, flush=True)
# Initialize the vector DB in the background
print("Triggering background RAG knowledge base initialization...", flush=True)
threading.Thread(target=initialize_knowledge_base).start()
@app.route('/')
def index():
"""Serve the main page"""
return app.send_static_file('index.html')
@app.route('/api/status')
def get_status():
"""Get the current status"""
# Re-check occasionally if not connected
if not ollama_connected:
threading.Thread(target=check_backend_status).start()
return jsonify({
'model_loaded': ollama_connected, # Keep key for frontend compatibility
'loading_progress': status_message
})
@app.route('/api/weather', methods=['GET'])
def fetch_weather():
"""Fetch live weather for a district"""
district = request.args.get('district', '').strip()
if not district:
return jsonify({'error': 'District parameter is required.'}), 400
weather_data = get_current_weather(district)
if weather_data.get('success'):
return jsonify(weather_data)
else:
return jsonify(weather_data), 500
# ----- AUTHENTICATION ROUTES -----
@app.route('/api/register', methods=['POST'])
@limiter.limit("5 per minute")
def register():
data = request.get_json()
email = data.get('email')
full_name = data.get('full_name')
profession = data.get('profession')
password = data.get('password')
if not email or not full_name or not profession or not password:
return jsonify({'error': 'All fields are required'}), 400
if User.query.filter_by(email=email).first():
return jsonify({'error': 'Email already exists'}), 400
hashed_password = generate_password_hash(password)
new_user = User(email=email, full_name=full_name, profession=profession, password_hash=hashed_password)
db.session.add(new_user)
db.session.commit()
login_user(new_user)
return jsonify({'success': True, 'email': email, 'full_name': full_name})
@app.route('/api/login', methods=['POST'])
@limiter.limit("10 per minute")
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
user = User.query.filter_by(email=email).first()
if user and check_password_hash(user.password_hash, password):
login_user(user)
return jsonify({'success': True, 'email': user.email, 'full_name': user.full_name})
return jsonify({'error': 'Invalid email or password'}), 401
@app.route('/api/logout', methods=['POST'])
@login_required
def logout():
logout_user()
return jsonify({'success': True})
@app.route('/api/user', methods=['GET'])
def get_current_user():
if current_user.is_authenticated:
return jsonify({'logged_in': True, 'email': current_user.email, 'full_name': current_user.full_name, 'profession': current_user.profession})
return jsonify({'logged_in': False})
# ---------------------------------
@app.route('/api/ask', methods=['POST'])
@limiter.limit("10 per minute")
def ask_question():
"""Handle user questions"""
if not ollama_connected:
return jsonify({'error': 'Ollama is not connected. Please ensure Ollama is running.'}), 503
data = request.get_json()
question = data.get('question', '').strip()
district = data.get('district', '').strip()
if not question:
return jsonify({'error': 'Please provide a question.'}), 400
weather_context = ""
if district and district in TN_DISTRICTS:
weather_data = get_current_weather(district)
if weather_data.get('success'):
weather_context = f"\n\nCurrent Weather in {district}, Tamil Nadu: {weather_data['temperature']}°C, Humidity: {weather_data['humidity']}%, Condition: {weather_data['condition']} {weather_data['emoji']}, Wind Speed: {weather_data['wind_speed']} km/h. Please consider this live weather context in your advice if relevant."
try:
# Construct prompt
weather_section = f"\n\n[Weather Context]: {weather_context.strip()}" if weather_context.strip() else ""
# Fetch RAG context
print("Fetching expert PDF knowledge context...", flush=True)
rag_context = query_rag(question)
rag_section = f"\n\n[Expert Advisory Knowledge Base Context]:\n{rag_context}\n[End of Knowledge Base]\n\n" if rag_context else ""
full_prompt = (
"You are AgroGPT, an expert agriculture assistant with access to detailed agricultural advisory documents. "
f"{rag_section}"
"Answer the following question clearly and concisely in plain text. DO NOT visibly print out or quote the Knowledge Base Context provided above. Synthesize the information naturally into your conversational answer.\n\n"
"Structure your response EXACTLY as follows:\n"
"1. Provide a helpful, robust answer in English. Ensure you include relevant information from the knowledge base if available.\n"
"2. Then write the header 'Malayalam Summary:' followed by the FULL answer translated into native Malayalam script (മലയാളം). Do NOT use English/Latin letters for Malayalam.\n"
"3. Then write the header 'Tamil Summary:' followed by the FULL answer translated into native Tamil script (தமிழ்). Do NOT use English/Latin letters for Tamil.\n"
"Use double line breaks between each section. Do NOT use markdown, asterisks, or bullet points.\n\n"
f"Question: {question}{weather_section}\n\nAnswer:"
)
print(f"Asking Ollama: {question}", flush=True)
response_text = generate_with_ollama(full_prompt)
return jsonify({
'question': question,
'answer': response_text,
'full_response': response_text
})
except Exception as e:
return jsonify({'error': f'Error generating response: {str(e)}'}), 500
@app.route('/api/upload', methods=['POST'])
@login_required
@limiter.limit("10 per minute")
def upload_image():
"""Handle image uploads for disease detection"""
if 'image' not in request.files:
return jsonify({'error': 'No image file provided'}), 400
file = request.files['image']
if file.filename == '':
return jsonify({'error': 'No image file selected'}), 400
if file and allowed_file(file.filename):
# Generate a unique filename to avoid conflicts
filename = secure_filename(file.filename)
unique_filename = f"{uuid.uuid4()}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
try:
# Save the uploaded file
file.save(filepath)
# Analyze the image for disease (uses Ollama for advice)
analysis_result = analyze_image_for_disease(filepath)
# Clean up the uploaded file after analysis
# Optional: Keep it if we want to log queries, but deleting for privacy/space
if os.path.exists(filepath):
os.remove(filepath)
return jsonify({
'success': True,
'analysis': analysis_result,
'filename': filename
})
except Exception as e:
# Clean up file if it exists
if os.path.exists(filepath):
os.remove(filepath)
return jsonify({'error': f'Error processing image: {str(e)}'}), 500
return jsonify({'error': 'Invalid file type. Please upload a PNG, JPG, JPEG, GIF, or BMP image.'}), 400
def main():
"""Main function to start the application"""
print_header()
# Check connection first
check_backend_status()
# Start Flask app
port = int(os.environ.get("PORT", 7860))
print(f"Starting web server...", flush=True)
print(f"Open your browser and go to: http://0.0.0.0:{port}", flush=True)
app.run(host='0.0.0.0', port=port, debug=False)
if __name__ == "__main__":
os.environ.setdefault("PYTHONUNBUFFERED", "1")
main()