Spaces:
Running
Running
| # NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework | |
| # All Rights Reserved 2008-2025 | |
| # Licensed under the BSD 3-Clause License | |
| # https://opensource.org/licenses/BSD-3-Clause | |
| # | |
| # Copyright (c) 2008-2025, Neongecko.com Inc. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions are met: | |
| # 1. Redistributions of source code must retain the above copyright notice, | |
| # this list of conditions and the following disclaimer. | |
| # 2. Redistributions in binary form must reproduce the above copyright notice, | |
| # this list of conditions and the following disclaimer in the documentation | |
| # and/or other materials provided with the distribution. | |
| # 3. Neither the name of the copyright holder nor the names of its contributors | |
| # may be used to endorse or promote products derived from this software | |
| # without specific prior written permission. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
| # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| # POSSIBILITY OF SUCH DAMAGE. | |
| import logging | |
| from datetime import datetime | |
| from typing import Any, Dict, Optional | |
| from fastapi import APIRouter, Depends | |
| from pydantic import BaseModel | |
| from app.api.routes.user_profile import _is_field_filled, enrich_profile_from_user | |
| from app.core.auth import get_current_active_user | |
| from app.core.bootstrap import create_llm_client | |
| from app.core.database import get_database | |
| from app.core.onboarding_agent import OnboardingAgent, PROFILE_FIELDS | |
| from app.models.user import User | |
| LOG = logging.getLogger(__name__) | |
| router = APIRouter() | |
| ONBOARDING_COLLECTION = "onboarding_conversations" | |
| class OnboardingMessage(BaseModel): | |
| user_input: str | |
| def _progress(profile: Dict[str, Any]) -> int: | |
| filled = sum(1 for k, *_ in PROFILE_FIELDS if _is_field_filled(profile.get(k))) | |
| return int(filled / len(PROFILE_FIELDS) * 100) | |
| def _next_missing_question(profile: Dict[str, Any]) -> Optional[str]: | |
| """Return the human-friendly question for the first unfilled field.""" | |
| for key, question, _desc in PROFILE_FIELDS: | |
| if not _is_field_filled(profile.get(key)): | |
| return question | |
| return None | |
| async def onboarding_start( | |
| current_user: User = Depends(get_current_active_user), | |
| ) -> Dict[str, Any]: | |
| """Return conversation history (if any) and current progress. | |
| If the user has an in-progress conversation it is returned so the | |
| frontend can restore the chat. Otherwise a fresh contextual welcome | |
| message is generated based on which fields are still missing. | |
| """ | |
| db = get_database() | |
| doc = await db.user_profiles.find_one({"user_id": current_user.id}) | |
| profile = enrich_profile_from_user(doc, current_user) | |
| progress = _progress(profile) | |
| if progress >= 100: | |
| await db[ONBOARDING_COLLECTION].delete_many({"user_id": current_user.id}) | |
| return { | |
| "messages": [{"role": "agent", | |
| "text": "Your profile is already complete! Feel free to update anything by chatting here."}], | |
| "progress": 100, | |
| "complete": True, | |
| } | |
| conv = await db[ONBOARDING_COLLECTION].find_one({"user_id": current_user.id}) | |
| if conv and conv.get("messages"): | |
| return { | |
| "messages": conv["messages"], | |
| "progress": progress, | |
| "complete": False, | |
| } | |
| next_q = _next_missing_question(profile) | |
| if progress == 0: | |
| greeting = ( | |
| f"Hey {current_user.firstName}! I'd like to learn a bit about your security background so " | |
| "your advisors can tailor depth and examples. " | |
| f"Let's start — {next_q.lower() if next_q else 'tell me about your role and goals!'}" | |
| ) | |
| else: | |
| greeting = ( | |
| f"Welcome back, {current_user.firstName}! You're {progress}% done. " | |
| f"Let's pick up where we left off — {next_q.lower() if next_q else 'what else can you tell me?'}" | |
| ) | |
| messages = [{"role": "agent", "text": greeting}] | |
| await db[ONBOARDING_COLLECTION].update_one( | |
| {"user_id": current_user.id}, | |
| {"$set": {"messages": messages, "updated_at": datetime.utcnow()}, | |
| "$setOnInsert": {"user_id": current_user.id}}, | |
| upsert=True, | |
| ) | |
| return {"messages": messages, "progress": progress, "complete": False} | |
| async def onboarding_chat( | |
| msg: OnboardingMessage, | |
| current_user: User = Depends(get_current_active_user), | |
| ) -> Dict[str, Any]: | |
| db = get_database() | |
| doc = await db.user_profiles.find_one({"user_id": current_user.id}) | |
| profile = enrich_profile_from_user(doc, current_user) | |
| agent = OnboardingAgent(create_llm_client()) | |
| result = await agent.chat(msg.user_input, current_user.id, profile) | |
| user_msg = {"role": "user", "text": msg.user_input} | |
| agent_msg = {"role": "agent", "text": result["reply"]} | |
| await db[ONBOARDING_COLLECTION].update_one( | |
| {"user_id": current_user.id}, | |
| {"$push": {"messages": {"$each": [user_msg, agent_msg]}}, | |
| "$set": {"updated_at": datetime.utcnow()}, | |
| "$setOnInsert": {"user_id": current_user.id}}, | |
| upsert=True, | |
| ) | |
| if result.get("complete"): | |
| await db[ONBOARDING_COLLECTION].delete_many({"user_id": current_user.id}) | |
| return result | |