import uuid import json import time from time import perf_counter from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import StreamingResponse from pydantic import BaseModel import uvicorn from markdown_pdf import MarkdownPdf, Section import os from langchain_huggingface import HuggingFaceEndpointEmbeddings from tools import load_env_variables from langchain_google_genai import ChatGoogleGenerativeAI from vector_store import RDSVectorStore from retriever import Retriever from orchestrator_2 import create_orchestrator # Load environment variables at module load time load_env_variables(".env") # Global variables to hold initialized components embed_model = None retriever = None rds_wrapper = None llm = None langgraph_app = None @asynccontextmanager async def lifespan(app: FastAPI): """ Lifespan context manager to initialize components once at server startup and clean up on shutdown. """ global embed_model, retriever, rds_wrapper, llm, langgraph_app # --------- STARTUP --------- print("\n========== SERVER STARTUP ==========") # Initialize Embedding Model try: print("> Initializing the Embedding model (HuggingFace Inference API)...") embed_model = HuggingFaceEndpointEmbeddings( model="BAAI/bge-small-en-v1.5", huggingfacehub_api_token=os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN") ) print("✓ Embedding model initialized") except Exception as e: print(f"✗ Error Loading the Embedding model: {e}") raise # Initialize Vector Store try: print(">Initializing the vector_store...") rds_wrapper = RDSVectorStore(collection_name='research_paper') rds_wrapper.initialize_store(embedding_manager=embed_model) print("✓ Vector store initialized") except Exception as e: print(f"✗ Vector store initialization failed: {e}") import traceback traceback.print_exc() raise # Initialize Retriever try: print(">Initializing the retriever...") retriever = Retriever( rds_wrapper=rds_wrapper, all_documents=[], k=5, top_n=5, collection_name='research_paper' ) retriever.initialize_retriever() print("✓ Retriever initialized") except Exception as e: print(f"✗ Retriever initialization failed: {e}") import traceback traceback.print_exc() raise # Initialize LLM try: print(">Initializing the LLM...") llm = ChatGoogleGenerativeAI( model="gemini-3.1-flash-lite", temperature=0 ) print("✓ LLM initialized") except Exception as e: print(f"✗ LLM initialization failed: {e}") raise # Create the orchestrator with pre-initialized components try: print(">Creating orchestrator...") langgraph_app = create_orchestrator(embed_model, retriever, rds_wrapper, llm) print("✓ Orchestrator created") except Exception as e: print(f"✗ Orchestrator creation failed: {e}") raise print("========== SERVER READY ==========\n") yield # Server runs here # --------- SHUTDOWN --------- print("\n========== SERVER SHUTDOWN ==========") # Add cleanup logic here if needed print("========== GOODBYE ==========\n") app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Create a dedicated folder for reports base_dir = Path.cwd() report_folder = base_dir / "reports" report_folder.mkdir(parents=True, exist_ok=True) # Mount the folder so the frontend can access the PDFs via URL app.mount("/reports", StaticFiles(directory="reports"), name="reports") # -------------- REQUEST MODELS --------------- # class ResearchRequest(BaseModel): query: str # -------------- API ENDPOINTS --------------- # @app.post("/api/research") async def generate_research(request: ResearchRequest): init_time = perf_counter() print(f"\n{'='*60}") print(f"Received query from frontend: {request.query}") print(f"{'='*60}\n") async def event_stream(): try: start_time = time.time() initial_state = { "original_query": request.query, "search_queries": {}, "query_type": "", "gathered_chunks": [], "failed_urls": [], "missing_information": [], "draft_report": "", "is_complete": False, "formatted_references": [], "iteration_count": 0, "max_iterations": 3, } # 1. STREAM REAL-TIME LOGS final_state = initial_state for event in langgraph_app.stream(initial_state): for node_name, state_update in event.items(): # Broadcast the real log to the frontend instantly log_message = f"[{node_name.upper()}] Execution completed." yield json.dumps({"type": "log", "message": log_message}) + "\n" # Keep tracking the state so we have the final output at the end final_state.update(state_update) end_time = time.time() execution_time = round(end_time - start_time, 2) # 2. FINALIZE THE REPORT final_report = final_state.get("draft_report", "") query_type = final_state.get("query_type", "deep_research") # Handle different report formats if isinstance(final_report, list): # Handle LangChain dictionary list format: [{'text': '...'}] if len(final_report) > 0 and isinstance(final_report[0], dict) and "text" in final_report[0]: final_report = final_report[0]["text"] else: # Fallback: join the list items into a single string final_report = "\n".join(str(item) for item in final_report) elif not isinstance(final_report, str): # Catch-all for any other weird data types final_report = str(final_report) # 3. ROUTE RESPONSE BY QUERY TYPE if query_type in ["chit_chat", "general_knowledge", "out_of_scope"]: yield json.dumps({ "type": "result", "response_type": "text", "content": final_report, "time": execution_time }) + "\n" else: filename = f"AARA_Report_{uuid.uuid4().hex[:6]}.pdf" file_path = report_folder / filename pdf = MarkdownPdf(toc_level=2) pdf.add_section(Section(final_report)) pdf.save(str(file_path)) yield json.dumps({ "type": "result", "response_type": "pdf", "pdf_url": f"/reports/{filename}", "time": execution_time }) + "\n" except Exception as e: print(f"Error in research pipeline: {e}") import traceback traceback.print_exc() yield json.dumps({ "type": "error", "message": str(e) }) + "\n" # Return the stream with the specific NDJSON media type total_time_taken = perf_counter() - init_time print(f"\nAction completed in {total_time_taken}") return StreamingResponse(event_stream(), media_type="application/x-ndjson") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)