SmartInc-API / main.py
yoursdvniel's picture
Added new endpoint for the assistance chat.
47405f2 verified
from flask import Flask, request, jsonify
from flask_cors import CORS
import json
from datetime import datetime
from typing import Optional, Dict, Any
from firestore_client import get_firestore_client
from openai_client import ask_gpt
from prompt_instructions import build_system_message
from role_access import get_allowed_collections # (currently unused but kept)
from data_fetcher import fetch_data_from_firestore
from data_planner import determine_data_requirements # 🧠 Gemini planner
from resolver import resolve_user_context
from schema_utils import has_field, resolve_field
app = Flask(__name__)
CORS(app)
db = get_firestore_client()
# -- helpers --------------------------------------------------------------
def to_jsonable(obj):
"""Recursively convert Firestore types (e.g., DatetimeWithNanoseconds) to JSON-safe values."""
# Firestore timestamp types come through as DatetimeWithNanoseconds (duck-type datetime)
if isinstance(obj, datetime):
return obj.isoformat()
# Some Firestore SDKs expose Timestamp-like objects without subclassing datetime.
# Fallback: detect presence of isoformat()
if hasattr(obj, "isoformat") and callable(getattr(obj, "isoformat")):
try:
return obj.isoformat()
except Exception:
pass
if isinstance(obj, dict):
return {k: to_jsonable(v) for k, v in obj.items()}
if isinstance(obj, list):
return [to_jsonable(v) for v in obj]
return obj
# 🔧 Normalize Gemini plan into proper Firestore fetch format
def normalize_plan(plan: dict, token_map: Optional[Dict[str, Any]] = None) -> dict:
token_map = token_map or {}
filters = plan.get("filters", {}) or {}
planned_cols = plan.get("collections", []) or []
def canonical_value(key, val):
# replace tokens like {{participantId}}
if isinstance(val, str) and val in token_map:
val = token_map[val]
# special-case status normalization
if key == "status" and val == "running":
return "active"
return val
collections_out = []
for col in planned_cols:
# Allow both strings ("participants") and objects ({"name":"participants","fields":[...]}).
name = col["name"] if isinstance(col, dict) else col
col_filters = []
for k, v in filters.items():
canon_key = resolve_field(name, k)
# only add filters that exist for this collection (schema-aware)
if has_field(name, canon_key):
col_filters.append({
"field": canon_key,
"op": "==",
"value": canonical_value(k, v),
})
# else: skip invalid field for this collection
collections_out.append({"name": name, "filters": col_filters, "limit": 50})
return {"collections": collections_out}
# -- route ---------------------------------------------------------------
@app.route('/chat', methods=['POST'])
def chat():
data = request.json
role = data.get('role')
user_input = data.get('message')
company_code = data.get('companyCode')
user_id = data.get('userId')
if not role or not user_input or not company_code or not user_id:
return jsonify({"error": "Missing role, message, companyCode, or userId"}), 400
# 🔎 Resolve current user's email + participantId (participants has no companyCode; resolve via applications)
ctx = resolve_user_context(user_id, company_code)
token_map = {
"{{participantId}}": ctx.get("participantId"),
"{{userEmail}}": ctx.get("email"),
"{{userId}}": ctx.get("uid"),
}
# 🧠 Plan
planning_result = determine_data_requirements(
user_input, company_code, user_id,
participant_email=ctx.get("email"),
participant_id=ctx.get("participantId"),
)
if "error" in planning_result:
return jsonify({"reply": f"⚠️ Planning error: {planning_result['error']}"})
# 🛠️ Normalize & replace tokens (schema-aware)
normalized_plan = normalize_plan(planning_result, token_map)
# 📥 Fetch
firestore_data = fetch_data_from_firestore(normalized_plan)
# 🧩 Build messages — convert Firestore payload to JSON-safe types
system_msg = build_system_message(company_code)
safe_ctx = to_jsonable(ctx)
safe_data = to_jsonable(firestore_data)
data_msg = {
"role": "system",
"content": (
f"CurrentUserContext: {json.dumps(safe_ctx)}\n"
f"Here is the data from Firestore:\n{json.dumps(safe_data)}"
)
}
user_msg = {"role": "user", "content": user_input}
final_response = ask_gpt([system_msg, data_msg, user_msg])
return jsonify({"reply": final_response})
# --- NEW: lightweight Help/Assistant intent router -------------------------
@app.route('/assist', methods=['POST'])
def assist():
"""
Classifies a short help question and, if appropriate, returns
a suggested navigation target + tour key for the frontend to action.
Request JSON:
{
"role": "incubatee" | ...,
"message": "Where do I apply?",
"companyCode": "ACME",
"userId": "abc123",
"path": "/current/location" # optional, helps decide replace vs push if you want
}
Response JSON:
{
"reply": "bot text to show",
"navigate": {
"to": "/incubatee/sme",
"tour": "apply", # optional
"ts": 1712345678901, # force re-run effects
"delayMs": 900 # UI should wait before navigating (to show the bot msg)
},
"handoff": {
"open": false, # whether to auto-open your drawer after nav
"botText": "You're on Programs. Tap an 'Apply' button to begin.",
"followups": ["track_application","profile_setup","inquiries"]
}
}
"""
data = request.json or {}
role = data.get('role')
user_input = (data.get('message') or '').strip().lower()
company_code = data.get('companyCode')
user_id = data.get('userId')
if not role or not user_input or not company_code or not user_id:
return jsonify({"error": "Missing role, message, companyCode, or userId"}), 400
# Simple keyword intent router (deterministic; fast)
def nav_payload(to: str, tour: Optional[str], reply: str, handoff: Optional[Dict[str, Any]] = None):
return jsonify({
"reply": reply,
"navigate": {
"to": to,
"tour": tour,
"ts": int(datetime.now().timestamp() * 1000),
"delayMs": 900
},
"handoff": handoff or {}
})
tokens = set(user_input.replace('?', '').split())
contains = lambda *words: any(w in tokens or w in user_input for w in words)
# 1) Where do I apply?
if contains('apply', 'application', 'register', 'program', 'submit'):
return nav_payload(
to="/incubatee/sme",
tour="apply",
reply="Taking you to Programs and starting the tour…",
handoff={
"open": False,
"botText": "You're on Programs. Tap an **Apply** button to begin.",
"followups": ["track_application", "profile_setup", "inquiries"]
}
)
# 2) Track my application
if contains('track', 'status', 'progress'):
return nav_payload(
to="/incubatee/tracker",
tour="track",
reply="Opening your application tracker…",
handoff={
"open": False,
"botText": "This is your tracker. You can view statuses and details here.",
"followups": ["where_apply", "inquiries"]
}
)
# 3) Inquiries / helpdesk
if contains('inquiry', 'inquiries', 'question', 'helpdesk', 'support', 'ticket'):
return nav_payload(
to="/incubatee/sme/inquiries",
tour="inquiries",
reply="Taking you to Inquiries…",
handoff={
"open": True,
"botText": "Here you can log questions or check replies.",
"followups": ["where_apply", "track_application"]
}
)
# 4) Profile/setup questions
if contains('profile', 'setup', 'account', 'redirect'):
return jsonify({
"reply": (
"You need to complete your profile before applying. "
"If something’s missing (e.g., name/company), we’ll redirect you to finish it first."
),
"handoff": {
"open": True,
"botText": "Would you like to go to **My Profile** now?",
"followups": ["where_apply"]
}
})
# 5) Contact support
if contains('contact', 'email', 'help', 'assist'):
return jsonify({
"reply": "You can email support at **support@smartincubation.example** or log an inquiry from the Inquiries page.",
"handoff": {
"open": True,
"botText": "Try the **Inquiries** option if you want to log a ticket.",
"followups": ["inquiries"]
}
})
# 6) Fallback → optionally leverage GPT to draft a helpful answer (no redirect)
# Keep this light; we're not doing Firestore fetches here.
try:
sys = {
"role": "system",
"content": (
"You are Q-Bot, a concise, friendly assistant. "
"Clarify the request in one or two sentences and suggest a next step. "
"Do NOT mention databases or internal logic."
)
}
user = {"role": "user", "content": data.get('message', '')}
answer = ask_gpt([sys, user]) or "I’m here to help! Could you tell me a bit more about what you need?"
return jsonify({
"reply": answer,
"handoff": {
"open": True,
"followups": ["where_apply", "track_application", "inquiries"]
}
})
except Exception:
return jsonify({
"reply": "I’m here to help! Could you tell me a bit more about what you need?",
"handoff": {
"open": True,
"followups": ["where_apply", "track_application", "inquiries"]
}
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860)