|
import logging |
|
import time |
|
import uvicorn |
|
from fastapi import FastAPI, HTTPException |
|
from pydantic import BaseModel |
|
from contextlib import asynccontextmanager |
|
from typing import List, Dict, Any |
|
|
|
|
|
|
|
try: |
|
from kig_core.config import settings |
|
from kig_core.schemas import PlannerState, KeyIssue as KigKeyIssue, GraphConfig |
|
from kig_core.planner import build_graph |
|
from kig_core.graph_client import neo4j_client |
|
from langchain_core.messages import HumanMessage |
|
except ImportError as e: |
|
print(f"Error importing kig_core components: {e}") |
|
print("Please ensure kig_core is in your Python path or installed.") |
|
|
|
raise |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
class KeyIssueRequest(BaseModel): |
|
"""Request body containing the user's technical query.""" |
|
query: str |
|
|
|
class KeyIssueResponse(BaseModel): |
|
"""Response body containing the generated key issues.""" |
|
key_issues: List[KigKeyIssue] |
|
|
|
|
|
|
|
|
|
|
|
app_graph = None |
|
|
|
|
|
@asynccontextmanager |
|
async def lifespan(app: FastAPI): |
|
"""Handles startup and shutdown events.""" |
|
global app_graph |
|
logger.info("API starting up...") |
|
|
|
|
|
try: |
|
logger.info("Verifying Neo4j connection...") |
|
neo4j_client._get_driver().verify_connectivity() |
|
logger.info("Neo4j connection verified.") |
|
except Exception as e: |
|
logger.error(f"Neo4j connection verification failed on startup: {e}", exc_info=True) |
|
|
|
|
|
|
|
|
|
logger.info("Building LangGraph application...") |
|
try: |
|
app_graph = build_graph() |
|
logger.info("LangGraph application built successfully.") |
|
except Exception as e: |
|
logger.error(f"Failed to build LangGraph application on startup: {e}", exc_info=True) |
|
|
|
raise RuntimeError("Failed to build LangGraph on startup.") from e |
|
|
|
yield |
|
|
|
|
|
logger.info("API shutting down...") |
|
|
|
|
|
logger.info("Neo4j client closed (likely via atexit).") |
|
logger.info("API shutdown complete.") |
|
|
|
|
|
|
|
app = FastAPI( |
|
title="Key Issue Generator API", |
|
description="API to generate Key Issues based on a technical query using LLMs and Neo4j.", |
|
version="1.0.0", |
|
lifespan=lifespan |
|
) |
|
|
|
|
|
|
|
@app.get("/") |
|
def read_root(): |
|
return {"status": "ok"} |
|
|
|
@app.post("/generate-key-issues", response_model=KeyIssueResponse) |
|
async def generate_issues(request: KeyIssueRequest): |
|
""" |
|
Accepts a technical query and returns a list of generated Key Issues. |
|
""" |
|
global app_graph |
|
if app_graph is None: |
|
logger.error("Graph application is not initialized.") |
|
raise HTTPException(status_code=503, detail="Service Unavailable: Graph not initialized") |
|
|
|
user_query = request.query |
|
if not user_query: |
|
raise HTTPException(status_code=400, detail="Query cannot be empty.") |
|
|
|
logger.info(f"Received request to generate key issues for query: '{user_query[:100]}...'") |
|
start_time = time.time() |
|
|
|
try: |
|
|
|
|
|
initial_state: PlannerState = { |
|
"user_query": user_query, |
|
"messages": [HumanMessage(content=user_query)], |
|
"plan": [], |
|
"current_plan_step_index": -1, |
|
"step_outputs": {}, |
|
"key_issues": [], |
|
"error": None |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config: GraphConfig = {"configurable": {}} |
|
|
|
|
|
logger.info("Invoking LangGraph workflow...") |
|
|
|
final_state = await app_graph.ainvoke(initial_state, config=config) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end_time = time.time() |
|
logger.info(f"Workflow finished in {end_time - start_time:.2f} seconds.") |
|
|
|
|
|
if final_state is None: |
|
logger.error("Workflow execution did not produce a final state.") |
|
raise HTTPException(status_code=500, detail="Workflow execution failed to produce a result.") |
|
|
|
if final_state.get("error"): |
|
error_msg = final_state.get("error", "Unknown error") |
|
logger.error(f"Workflow failed with error: {error_msg}") |
|
|
|
status_code = 500 |
|
if "Neo4j" in error_msg or "connection" in error_msg.lower(): |
|
status_code = 503 |
|
elif "LLM error" in error_msg or "parse" in error_msg.lower(): |
|
status_code = 502 |
|
|
|
raise HTTPException(status_code=status_code, detail=f"Workflow failed: {error_msg}") |
|
|
|
|
|
|
|
generated_issues_data = final_state.get("key_issues", []) |
|
|
|
|
|
try: |
|
|
|
response_data = {"key_issues": generated_issues_data} |
|
logger.info(f"Successfully generated {len(generated_issues_data)} key issues.") |
|
return response_data |
|
except Exception as pydantic_error: |
|
logger.error(f"Failed to validate final key issues against response model: {pydantic_error}", exc_info=True) |
|
logger.error(f"Data that failed validation: {generated_issues_data}") |
|
raise HTTPException(status_code=500, detail="Internal error: Failed to format key issues response.") |
|
|
|
|
|
except HTTPException as http_exc: |
|
|
|
raise http_exc |
|
except ConnectionError as e: |
|
logger.error(f"Connection Error during API request: {e}", exc_info=True) |
|
raise HTTPException(status_code=503, detail=f"Service Unavailable: {e}") |
|
except ValueError as e: |
|
logger.error(f"Value Error during API request: {e}", exc_info=True) |
|
raise HTTPException(status_code=400, detail=f"Bad Request: {e}") |
|
except Exception as e: |
|
logger.error(f"An unexpected error occurred during API request: {e}", exc_info=True) |
|
raise HTTPException(status_code=500, detail=f"Internal Server Error: An unexpected error occurred.") |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
print("Starting API server...") |
|
print("Ensure required environment variables (e.g., NEO4J_URI, NEO4J_PASSWORD, GEMINI_API_KEY) are set or .env file is present.") |
|
|
|
|
|
uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True) |