|
import logging |
|
import time |
|
import uvicorn |
|
import requests |
|
import os |
|
|
|
from fastapi import FastAPI, HTTPException, Body |
|
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 |
|
|
|
|
|
import google.generativeai as genai |
|
|
|
|
|
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] |
|
|
|
class SpecificityEvaluationRequest(BaseModel): |
|
title: str |
|
description: str |
|
technical_topic: str |
|
|
|
class SpecificityScore(BaseModel): |
|
predicted_class: str |
|
score: float |
|
|
|
class SpecificityEvaluationResponse(BaseModel): |
|
problematic: str |
|
specificity: SpecificityScore |
|
|
|
class ProblemDescriptionRequest(BaseModel): |
|
description: str |
|
technical_topic: str |
|
|
|
class ProblemDescriptionResponse(BaseModel): |
|
problem_description: str |
|
|
|
|
|
|
|
|
|
|
|
app_graph = None |
|
gemini_client = 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 |
|
|
|
|
|
logger.info("Initializing Gemini client...") |
|
if genai: |
|
try: |
|
|
|
api_key = os.getenv("GEMINI_API_KEY") or getattr(settings, "GEMINI_API_KEY", None) |
|
if not api_key: |
|
raise ValueError("GEMINI_API_KEY not found in environment or settings.") |
|
genai.configure(api_key=api_key) |
|
logger.info("Gemini client configured successfully.") |
|
except Exception as e: |
|
logger.error(f"Failed to configure Gemini client: {e}", exc_info=True) |
|
else: |
|
logger.warning("Gemini library not imported. Endpoints requiring Gemini will not work.") |
|
|
|
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, Specificity, and Description API", |
|
description="API to generate Key Issues, evaluate problematic specificity, and generate problem descriptions.", |
|
version="1.1.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.") |
|
|
|
|
|
@app.post("/evaluate-specificity", response_model=SpecificityEvaluationResponse) |
|
async def evaluation(request: SpecificityEvaluationRequest): |
|
""" |
|
Generates a technical problematic using Gemini based on title, description, |
|
and topic, then evaluates its specificity using an external API (fine-tuned model for specificity). |
|
""" |
|
|
|
if not genai: |
|
logger.error("Gemini client is not available or configured.") |
|
raise HTTPException(status_code=503, detail="Service Unavailable: Gemini client not configured") |
|
|
|
title = request.title |
|
description = request.description |
|
technical_topic = request.technical_topic |
|
|
|
if not all([title, description, technical_topic]): |
|
raise HTTPException(status_code=400, detail="Missing required fields: title, description, or technical_topic.") |
|
|
|
logger.info("Received request for specificity evaluation.") |
|
logger.debug(f"Title: {title}, Topic: {technical_topic}") |
|
|
|
|
|
prompt = f"""I want you to create a technical problematic using a key issue composed of a title and a detailed description. |
|
Here is the title of the key issue to deal with: <title>{title}</title> |
|
|
|
And here is the associated description: <description>{description}</description> |
|
|
|
This key issue is part of the following technical topic: <technical_topic>{technical_topic}</technical_topic> |
|
|
|
The problematic must be in interrogative form. |
|
As the output, I only want you to provide the problematic found, nothing else. |
|
|
|
Here are examples of problematics that you could create, it shows the level of specificity that you should aim for: |
|
|
|
Example 1: 'How can a method for allocating radio resources in a non-GSO satellite communication system, operating in frequency bands shared with geostationary satellite systems and particularly in high frequency bands such as Ka, minimize interference to geostationary systems, without causing reduced geographic coverage due to fixed high separation angle thresholds or incurring cost and sub-optimality from over-dimensioning the non-GSO satellite constellation?' |
|
Example 2: 'How to address the vulnerability of on-aircraft avionics software update integrity checks to system compromises and the logistical challenges of cryptographic key management in digital signature solutions, in order to establish a more secure and logistically efficient method for updating safety-critical avionics equipment?' |
|
Example 3: 'How can SIM cards be protected against collision attacks that aim to retrieve the secret key Ki by analyzing the input and output of the authentication algorithm during the standard GSM authentication process, given that current tamper-proof measures are insufficient to prevent this type of key extraction?' |
|
Example 4: 'How can a Trusted Application in a GlobalPlatform compliant TEE overcome the GP specification limitations that enforce client blocking during task execution, prevent partial task execution, and delete TA execution context between commands, to function as a persistent server with stateful sessions and asynchronous communication capabilities, thereby enabling server functionalities like continuous listening and non-blocking send/receive, currently impossible due to GP's sequential task processing and stateless TA operation?' |
|
|
|
As far as possible, avoid using acronyms in the problematic. |
|
Try to be about the same length as the examples if possible.""" |
|
|
|
try: |
|
logger.info("Calling Gemini API to generate problematic...") |
|
|
|
model_name = "gemini-2.5-flash-preview-04-17" |
|
model = genai.GenerativeModel(model_name) |
|
|
|
response = model.generate_content(prompt) |
|
|
|
problematic_result = response.text.strip() |
|
logger.info("Successfully generated problematic from Gemini.") |
|
logger.debug(f"Generated problematic: {problematic_result[:200]}...") |
|
|
|
except Exception as e: |
|
logger.error(f"Error calling Gemini API: {e}", exc_info=True) |
|
|
|
raise HTTPException(status_code=502, detail=f"Failed to generate problematic using LLM: {e}") |
|
|
|
if not problematic_result: |
|
logger.error("Gemini API returned an empty result.") |
|
raise HTTPException(status_code=502, detail="LLM returned an empty problematic.") |
|
|
|
|
|
API_URL = "https://organizedprogrammers-fastapi-problematic-specificity.hf.space" |
|
endpoint = f"{API_URL}/predict" |
|
data = {"text": problematic_result} |
|
|
|
try: |
|
logger.info(f"Calling specificity prediction API at {endpoint}...") |
|
prediction_response = requests.post(endpoint, json=data, timeout=30) |
|
prediction_response.raise_for_status() |
|
|
|
score_data = prediction_response.json() |
|
logger.info(f"Successfully received specificity score: {score_data}") |
|
|
|
|
|
try: |
|
specificity_score = SpecificityScore(**score_data) |
|
except Exception as pydantic_error: |
|
logger.error(f"Failed to validate specificity score response: {pydantic_error}", exc_info=True) |
|
logger.error(f"Invalid data received from specificity API: {score_data}") |
|
raise HTTPException(status_code=502, detail="Invalid response format from specificity prediction API.") |
|
|
|
except requests.exceptions.RequestException as e: |
|
logger.error(f"Error calling specificity prediction API: {e}", exc_info=True) |
|
raise HTTPException(status_code=502, detail=f"Failed to call specificity prediction API: {e}") |
|
except Exception as e: |
|
logger.error(f"Unexpected error during specificity evaluation: {e}", exc_info=True) |
|
raise HTTPException(status_code=500, detail=f"Internal error during specificity evaluation: {e}") |
|
|
|
|
|
|
|
final_response = SpecificityEvaluationResponse( |
|
problematic=problematic_result, |
|
specificity=specificity_score |
|
) |
|
return final_response |
|
|
|
|
|
@app.post("/create-problem-description", response_model=ProblemDescriptionResponse) |
|
async def gen_problem_description(request: ProblemDescriptionRequest): |
|
""" |
|
Generates a problem description using Gemini based on a detailed description |
|
and technical topic. |
|
""" |
|
description = request.description |
|
technical_topic = request.technical_topic |
|
|
|
if not description or not technical_topic: |
|
raise HTTPException(status_code=400, detail="Missing required fields: description or technical_topic.") |
|
|
|
logger.info("Received request for problem description generation.") |
|
logger.debug(f"Topic: {technical_topic}") |
|
|
|
|
|
prompt = f"""I want you to create a problem description using a key issue explained in a detailed description. |
|
|
|
Here is the description of the key issue: <description>{description}</description> |
|
|
|
This key issue is part of the following technical topic: <technical_topic>{technical_topic}</technical_topic> |
|
|
|
As the output, I only want you to provide the problem description found, nothing else. |
|
|
|
Here are examples of problem descriptions that you could create, it shows the level of specificity that you should aim for: |
|
|
|
Example 1: 'I am working on enhancing security in 4G and 5G telecommunications networks, specifically in the area of network slicing. My goal is to address vulnerabilities in the isolation of slices and improve the integrity of data transmission across different network slices, ensuring they are properly protected against unauthorized access and attacks.' |
|
Example 2: 'I am working on improving user authentication in the context of 3GPP (3rd Generation Partnership Project) networks. Specifically, I need to solve issues related to the security and efficiency of the authentication process in the 5G network architecture, focusing on the use of cryptographic keys and identity management.' |
|
|
|
As far as possible, avoid using acronyms in the problem description. |
|
Try to be about the same length as the examples if possible.""" |
|
|
|
try: |
|
logger.info("Calling Gemini API to generate problem description...") |
|
|
|
model_name = "gemini-2.5-flash-preview-04-17" |
|
model = genai.GenerativeModel(model_name) |
|
|
|
response = model.generate_content(prompt) |
|
|
|
result_text = response.text.strip() |
|
logger.info("Successfully generated problem description from Gemini.") |
|
logger.debug(f"Generated description: {result_text[:200]}...") |
|
|
|
except Exception as e: |
|
logger.error(f"Error calling Gemini API for problem description: {e}", exc_info=True) |
|
|
|
raise HTTPException(status_code=502, detail=f"Failed to generate problem description using LLM: {e}") |
|
|
|
if not result_text: |
|
logger.error("Gemini API returned an empty result for problem description.") |
|
raise HTTPException(status_code=502, detail="LLM returned an empty problem description.") |
|
|
|
|
|
|
|
return ProblemDescriptionResponse(problem_description=result_text) |
|
|
|
|
|
|
|
|
|
|
|
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) |