from flask import Flask, request, jsonify
from flask_cors import CORS
import os, json, uuid, requests
from supabase import create_client, Client
import pycafe
app = Flask(__name__)
CORS(app)
SUPABASE_URL = pycafe.get_secret("SUPABASE_URL")
SUPABASE_KEY = pycafe.get_secret("SUPABASE_KEY")
if not SUPABASE_URL or not SUPABASE_KEY:
raise ValueError(
"SUPABASE_URL and SUPABASE_KEY environment variables must be set. "
"Please check your environment setup. "
)
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
ROLE_MAPPING = {
"java": {
"full_name": "java developer",
"json_file": "java polished.json"
},
"android": {
"full_name": "android developer",
"json_file": "android.json"
}
}
OPENROUTER_KEY = pycafe.get_secret("OPENROUTER_KEY")
if not OPENROUTER_KEY:
raise ValueError(
"OPENROUTER_KEY environment variable must be set. "
"Please get one from https://openrouter.ai/ and set it."
)
def load_json(file_name):
"""Loads a JSON file from the local filesystem."""
try:
with open(file_name, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
raise FileNotFoundError(f"JSON file not found: {file_name}. "
"Please ensure it's in the correct directory.")
except json.JSONDecodeError:
raise ValueError(f"Error decoding JSON from file: {file_name}. "
"Please check if the file content is valid JSON.")
def extract_role(query):
"""Extracts a role keyword from the user query."""
role_map_keywords = {
"java": ["java", "jdk", "java developer"],
"android": ["android", "kotlin", "android studio", "android developer"]
}
query = query.lower()
for role_key, keywords in role_map_keywords.items():
if any(k in query for k in keywords):
return role_key
return None
async def save_session_to_supabase(session_id: str, session_data: dict):
"""Saves or updates session data in Supabase."""
try:
response = supabase.table("sessions").upsert(
{"session_id": session_id, "session_data": session_data},
on_conflict="session_id"
).execute()
response.data
except Exception as e:
print(f"Error saving session '{session_id}' to Supabase: {e}")
async def load_session_from_supabase(session_id: str) -> dict:
"""Loads session data from Supabase."""
try:
response = supabase.table("sessions").select("session_data").eq("session_id", session_id).execute()
if response.data and len(response.data) > 0:
return response.data[0]["session_data"]
return {"history": []}
except Exception as e:
print(f"Error loading session '{session_id}' from Supabase: {e}")
return {"history": []}
def generate_response(user_query: str, json_data: dict, role_title: str) -> str:
"""Generates an AI response using OpenRouter."""
prompt = f"""
You are a helpful assistant for {role_title}(softwarestackhubassisstant).
Respond in **Markdown** with:
- Headings
- Bullet points
- Code blocks
- Better formatting
- Use emojis where appropriate
---
### User Query:
{user_query}
---
### Tool Dataset:
{json.dumps(json_data, indent=2)}
---
Now write the answer:
"""
headers = {
"Authorization": f"Bearer {OPENROUTER_KEY}",
"HTTP-Referer": "https://dsfapp.com/",
"X-Title": "StackHub AI"
}
data = {
"model": "deepseek/deepseek-r1-0528:free",
"messages": [{"role": "user", "content": prompt}]
}
try:
r = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=data)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
except requests.exceptions.RequestException as e:
print(f"OpenRouter API request failed: {e}")
return f"AI response failed (API request error): {str(e)}"
except KeyError:
print(f"Unexpected API response format from OpenRouter: {r.json()}")
return "AI response failed: Unexpected API response format."
except Exception as e:
print(f"An unexpected error occurred during AI response generation: {e}")
return f"AI response failed: {str(e)}"
@app.route("/ask", methods=["POST"])
async def ask():
"""Handles user queries, manages sessions, and generates AI responses."""
data = request.get_json()
raw_query = data.get("query", "").strip()
try:
session_id, query = raw_query.split(" ", 1)
session_id = session_id.strip()
query = query.strip()
except ValueError:
return jsonify({"error": "Query format: '<session_id> job:...' or '<session_id> user query: ...'"}), 400
session_data = await load_session_from_supabase(session_id)
if "history" not in session_data:
session_data["history"] = []
if query.lower() == "system: delete":
try:
response = supabase.table("sessions").delete().eq("session_id", session_id).execute()
if response.data:
return jsonify({"status": f"Session '{session_id}' deleted."})
return jsonify({"status": f"No session found with ID '{session_id}'."})
except Exception as e:
return jsonify({"error": f"Failed to delete session from Supabase: {e}"}), 500
if query.lower().startswith("job:"):
extracted_role_key = extract_role(query)
role_details = ROLE_MAPPING.get(extracted_role_key)
if role_details:
full_role_name = role_details["full_name"]
json_file_name = role_details["json_file"]
welcome_message = (
f"👋 Welcome! You are now connected with a professional AI assistant for "
f"**{full_role_name.title()}**."
)
session_data["history"].append({"query": query, "response": welcome_message, "role": full_role_name})
session_data["json_used"] = json_file_name
await save_session_to_supabase(session_id, session_data)
return jsonify({"session_id": session_id, "answer": welcome_message})
else:
sorry_message = "Sorry, this profession is not yet supported."
session_data["history"].append({"query": query, "response": sorry_message})
await save_session_to_supabase(session_id, session_data)
return jsonify({"session_id": session_id, "answer": sorry_message})
json_file = session_data.get("json_used")
if not json_file:
assigned_role_from_history = None
for entry in reversed(session_data["history"]):
if "role" in entry:
assigned_role_from_history = entry["role"]
break
if assigned_role_from_history:
for key, details in ROLE_MAPPING.items():
if details["full_name"] == assigned_role_from_history:
json_file = details["json_file"]
break
if not json_file:
return jsonify({"error": "No valid profession assigned. Please start with 'job:...' to select a role."}), 400
if not os.path.exists(json_file):
return jsonify({"error": f"Assigned JSON file '{json_file}' not found on the server. "
"Please check your ROLE_MAPPING configuration and file paths."}), 500
current_role_for_prompt = "General User"
for entry in reversed(session_data["history"]):
if "role" in entry:
current_role_for_prompt = entry["role"].title()
break
try:
json_data_for_ai = load_json(json_file)
response = generate_response(query, json_data_for_ai, current_role_for_prompt)
session_data["history"].append({"query": query, "response": response})
await save_session_to_supabase(session_id, session_data)
return jsonify({"session_id": session_id, "answer": response})
except Exception as e:
print(f"Error during AI response generation or session saving: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/history/<session_id>", methods=["GET"])
async def history(session_id: str):
"""Retrieves and returns the full history for a given session ID."""
session_data = await load_session_from_supabase(session_id)
if not session_data.get("history"):
return jsonify({"error": f"No session found with ID '{session_id}' or session is empty."}), 404
role_in_session_title_cased = None
for entry in reversed(session_data["history"]):
if "role" in entry:
role_in_session_title_cased = entry["role"].title()
break
return jsonify({
"session_id": session_id,
"role": role_in_session_title_cased,
"json_used": session_data.get("json_used"),
"history": session_data["history"]
})
@app.route("/reset", methods=["POST"])
async def reset():
"""Clears all sessions from the Supabase database."""
try:
response = supabase.table("sessions").delete().neq("session_id", "NULL").execute()
return jsonify({"status": f"All {len(response.data)} sessions cleared from Supabase."})
except Exception as e:
print(f"Error clearing all sessions from Supabase: {e}")
return jsonify({"error": f"Failed to clear all sessions: {e}"}), 500
if __name__ == "__main__":
app.run(debug=True, port=5000)