Initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
load_dotenv(find_dotenv())
|
||||
import os
|
||||
import modules.logger_tool as logger
|
||||
log_name = 'api_routers_interactive_langgraph_query'
|
||||
log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback
|
||||
logging = logger.get_logger(
|
||||
name=log_name,
|
||||
log_level=os.getenv("LOG_LEVEL", "DEBUG"),
|
||||
log_path=log_dir,
|
||||
log_file=log_name,
|
||||
runtime=True,
|
||||
log_format='default'
|
||||
)
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from modules.langchain.interactive_langgraph_query import perplexity_clone_graph
|
||||
from modules.redis_config import get_cached_results, set_cached_results
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class QueryRequest(BaseModel):
|
||||
query: str
|
||||
use_cache: bool = False
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
response: str
|
||||
needs_more_info: bool
|
||||
|
||||
@router.post("/query", response_model=QueryResponse)
|
||||
async def interactive_query(request: QueryRequest):
|
||||
logging.info(f"Received query: {request.query}")
|
||||
try:
|
||||
query_id = generate_random_alphanumeric()
|
||||
config = {"configurable": {"thread_id": f'{query_id}'}, "recursion_limit": 20}
|
||||
|
||||
inputs = {
|
||||
"messages": [HumanMessage(content=request.query)],
|
||||
}
|
||||
|
||||
# Check cache for existing results only if DEV_MODE is false
|
||||
use_cache = os.getenv("DEV_MODE", "true").lower() == "false"
|
||||
if use_cache:
|
||||
cache_key = f"langgraph_query:{request.query}"
|
||||
cached_result = get_cached_results(cache_key)
|
||||
if cached_result:
|
||||
logging.info(f"Found cached result for query: {request.query}")
|
||||
return cached_result
|
||||
|
||||
logging.debug("Updating state with initial message")
|
||||
perplexity_clone_graph.update_state(config, inputs)
|
||||
|
||||
logging.debug("Invoking perplexity_clone_graph")
|
||||
outputs = await perplexity_clone_graph.ainvoke(inputs, config)
|
||||
|
||||
final_response = outputs['messages'][-1].content
|
||||
needs_more_info = outputs.get('needs_more_info', False)
|
||||
|
||||
logging.info(f"Final response: {final_response}")
|
||||
logging.info(f"Needs more info: {needs_more_info}")
|
||||
|
||||
response = QueryResponse(response=final_response, needs_more_info=needs_more_info)
|
||||
|
||||
# Cache the result only if DEV_MODE is false
|
||||
if use_cache:
|
||||
set_cached_results(cache_key, response.dict())
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
logging.error(f"Error in interactive query: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"An error occurred during the query process: {str(e)}")
|
||||
|
||||
def generate_random_alphanumeric(length=4):
|
||||
import random
|
||||
import string
|
||||
characters = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(characters) for i in range(length))
|
||||
@@ -0,0 +1,153 @@
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
load_dotenv(find_dotenv())
|
||||
import os
|
||||
import modules.logger_tool as logger
|
||||
log_name = 'api_routers_langchain_graph_qa'
|
||||
log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback
|
||||
logging = logger.get_logger(
|
||||
name=log_name,
|
||||
log_level=os.getenv("LOG_LEVEL", "DEBUG"),
|
||||
log_path=log_dir,
|
||||
log_file=log_name,
|
||||
runtime=True,
|
||||
log_format='default'
|
||||
)
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from langchain.chains import GraphCypherQAChain
|
||||
from langchain_community.graphs import Neo4jGraph
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langchain.prompts.prompt import PromptTemplate
|
||||
from routers.llm.private.ollama.ollama_wrapper import OllamaWrapper
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Define the schema for nodes and relationships
|
||||
node_types = {
|
||||
"KeyStage": ["merged", "key_stage_name", "unique_id", "created"],
|
||||
"KeyStageSyllabus": ["ks_syllabus_name", "unique_id", "created", "merged", "ks_syllabus_key_stage", "ks_syllabus_subject"],
|
||||
"YearGroup": ["created", "merged", "unique_id", "year_group_name"],
|
||||
"YearGroupSyllabus": ["created", "merged", "yr_syllabus_name", "yr_syllabus_year_group", "yr_syllabus_id", "yr_syllabus_subject"],
|
||||
"Topic": ["topic_type", "topic_assessment_type", "created", "merged", "unique_id", "topic_id", "total_number_of_lessons_for_topic", "topic_title"],
|
||||
"Lesson": ["topic_lesson_id", "topic_lesson_type", "created", "merged", "topic_lesson_title", "topic_lesson_length", "topic_lesson_suggested_activities", "topic_lesson_weblinks", "topic_lesson_skills_learned"],
|
||||
"LearningStatement": ["created", "merged", "lesson_learning_statement", "lesson_learning_statement_id", "lesson_learning_statement_type"]
|
||||
}
|
||||
|
||||
relationship_types = {
|
||||
"KEY_STAGE_INCLUDES_KEY_STAGE_SYLLABUS": ["created", "merged"],
|
||||
"KEY_STAGE_SYLLABUS_INCLUDES_YEAR_GROUP_SYLLABUS": ["created", "merged"],
|
||||
"YEAR_GROUP_FOLLOWS_YEAR_GROUP": ["created", "merged"],
|
||||
"KEY_STAGE_FOLLOWS_KEY_STAGE": ["created", "merged"],
|
||||
"YEAR_SYLLABUS_INCLUDES_TOPIC": ["created", "merged"],
|
||||
"TOPIC_INCLUDES_LESSON": ["created", "merged"],
|
||||
"LESSON_INCLUDES_LEARNING_STATEMENT": ["created", "merged"],
|
||||
"LESSON_FOLLOWS_LESSON": ["created", "merged"]
|
||||
}
|
||||
|
||||
@router.get("/prompt")
|
||||
async def query_graph(
|
||||
database: str, prompt: str, top_k: int = 30, model: str = "gpt-4o", temperature: float = 0,
|
||||
verbose: bool = False, return_intermediate_steps: bool = False, exclude_types: list = None, include_types: list = None,
|
||||
return_direct: bool = False, validate_cypher: bool = False, model_type: str = "openai"
|
||||
):
|
||||
logging.info(f"Received request with prompt: {prompt}")
|
||||
if exclude_types is None:
|
||||
logging.info("No exclude_types provided, using default.")
|
||||
exclude_types = []
|
||||
if include_types is None:
|
||||
logging.info("No include_types provided, using default.")
|
||||
include_types = []
|
||||
|
||||
# Validate include_types and exclude_types
|
||||
logging.info(f"Validating include_types and exclude_types...")
|
||||
valid_types = set(node_types.keys()).union(set(relationship_types.keys()))
|
||||
logging.info(f"Valid types: {valid_types}")
|
||||
exclude_types = [t for t in exclude_types if t in valid_types]
|
||||
logging.info(f"Validated exclude_types: {exclude_types}")
|
||||
include_types = [t for t in include_types if t in valid_types]
|
||||
logging.info(f"Validated include_types: {include_types}")
|
||||
|
||||
graph = Neo4jGraph(
|
||||
url=os.environ['APP_BOLT_URL'],
|
||||
username=os.environ['USER_NEO4J'],
|
||||
password=os.environ['PASSWORD_NEO4J'],
|
||||
database=database
|
||||
)
|
||||
|
||||
logging.info("Refreshing schema...")
|
||||
graph.refresh_schema()
|
||||
logging.info("Schema refreshed.")
|
||||
schema = graph.schema
|
||||
logging.info(f"Schema: {schema}")
|
||||
|
||||
CYPHER_GENERATION_TEMPLATE = """Task: Generate a Cypher statement to query a graph database for timetable information.
|
||||
Role:
|
||||
You are an assistant in a school for teachers, specializing in querying graph databases to find answers to questions.
|
||||
The teacher will ask you questions about their timetable.
|
||||
|
||||
Instructions:
|
||||
1. Use only the provided relationship types and properties in the schema.
|
||||
2. Do not use any other relationship types or properties that are not provided.
|
||||
|
||||
Schema:
|
||||
{schema}
|
||||
|
||||
Note:
|
||||
1. Do not include any explanations or apologies in your responses.
|
||||
2. Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
|
||||
3. Do not include any text except the generated Cypher statement.
|
||||
|
||||
The question is:
|
||||
{question}"""
|
||||
|
||||
CYPHER_GENERATION_PROMPT = PromptTemplate(
|
||||
input_variables=["schema", "question"],
|
||||
template=CYPHER_GENERATION_TEMPLATE
|
||||
)
|
||||
|
||||
if model_type == "ollama":
|
||||
ollama_host = os.getenv("OLLAMA_URL")
|
||||
ollama_port = os.getenv("OLLAMA_PORT")
|
||||
if not ollama_host or not ollama_port:
|
||||
raise HTTPException(status_code=500, detail="Ollama host or port not set")
|
||||
client = OllamaWrapper(host=f'http://{ollama_host}:{ollama_port}')
|
||||
cypher_llm = client
|
||||
qa_llm = client
|
||||
else:
|
||||
cypher_llm = ChatOpenAI(temperature=temperature, model=model)
|
||||
qa_llm = ChatOpenAI(temperature=temperature, model=model)
|
||||
|
||||
chain = GraphCypherQAChain.from_llm(
|
||||
graph=graph,
|
||||
cypher_llm=cypher_llm,
|
||||
qa_llm=qa_llm,
|
||||
top_k=top_k,
|
||||
verbose=verbose,
|
||||
cypher_prompt=CYPHER_GENERATION_PROMPT,
|
||||
return_intermediate_steps=return_intermediate_steps,
|
||||
exclude_types=exclude_types,
|
||||
include_types=include_types,
|
||||
return_direct=return_direct,
|
||||
validate_cypher=validate_cypher
|
||||
)
|
||||
|
||||
formatted_prompt = CYPHER_GENERATION_PROMPT.format(schema=schema, question=prompt)
|
||||
|
||||
logging.info("\n\n")
|
||||
|
||||
logging.info("==================================================")
|
||||
logging.info("= graph_qa.py =")
|
||||
logging.info("==================================================")
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
logging.info("--------------------------------------------------")
|
||||
logging.info(f"Schema: \n{schema}\n")
|
||||
logging.info("--------------------------------------------------")
|
||||
logging.info(f"Formatted Prompt: \n{formatted_prompt}\n")
|
||||
logging.info("--------------------------------------------------")
|
||||
logging.info(f"Cypher prompt: \n{CYPHER_GENERATION_PROMPT}\n")
|
||||
logging.info("--------------------------------------------------")
|
||||
logging.info(f"Cypher template: \n{CYPHER_GENERATION_TEMPLATE}\n")
|
||||
logging.info("--------------------------------------------------")
|
||||
logging.info(f"Cypher chain: \n{chain}\n")
|
||||
logging.info("==================================================")
|
||||
|
||||
return chain(prompt)
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"Running simple query tests with OpenAI:\n",
|
||||
"\n",
|
||||
"Testing simple queries using openai model:\n",
|
||||
"\n",
|
||||
"Query: What is the history of Maidstone, England?\n",
|
||||
"Sending query to http://localhost:8000/api/langchain/interactive_langgraph_query/query with payload: {'query': 'What is the history of Maidstone, England?', 'model': 'openai'}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ERROR:root:Error sending query to http://localhost:8000/api/langchain/interactive_langgraph_query/query: 500 Server Error: Internal Server Error for url: http://localhost:8000/api/langchain/interactive_langgraph_query/query\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Response:\n",
|
||||
"{\n",
|
||||
" \"error\": \"500 Server Error: Internal Server Error for url: http://localhost:8000/api/langchain/interactive_langgraph_query/query\"\n",
|
||||
"}\n",
|
||||
"==================================================\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from dotenv import load_dotenv, find_dotenv\n",
|
||||
"load_dotenv(find_dotenv())\n",
|
||||
"import os\n",
|
||||
"import logging\n",
|
||||
"# Function to send a query and get the response\n",
|
||||
"import requests\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"# Define the URL of your FastAPI server\n",
|
||||
"BASE_URL = \"http://localhost:8000\" # Adjust this if your server is running on a different port or host\n",
|
||||
"\n",
|
||||
"# Define the endpoint\n",
|
||||
"ENDPOINT = f\"{BASE_URL}/api/langchain/interactive_langgraph_query/query\"\n",
|
||||
"\n",
|
||||
"def send_query(query, model=\"ollama\"):\n",
|
||||
" payload = {\"query\": query, \"model\": model}\n",
|
||||
" headers = {\"Content-Type\": \"application/json\"}\n",
|
||||
" print(f\"Sending query to {ENDPOINT} with payload: {payload}\")\n",
|
||||
" \n",
|
||||
" try:\n",
|
||||
" response = requests.post(ENDPOINT, json=payload, headers=headers)\n",
|
||||
" response.raise_for_status()\n",
|
||||
" print(f\"Received response from {ENDPOINT}: {response.json()}\")\n",
|
||||
" return response.json()\n",
|
||||
" except requests.exceptions.RequestException as e:\n",
|
||||
" logging.error(f\"Error sending query to {ENDPOINT}: {str(e)}\")\n",
|
||||
" return {\"error\": str(e)}\n",
|
||||
"\n",
|
||||
"def test_simple_queries(model=\"openai\"):\n",
|
||||
" queries = [\n",
|
||||
" \"What is the history of Maidstone, England?\"\n",
|
||||
" ]\n",
|
||||
" \n",
|
||||
" print(f\"Testing simple queries using {model} model:\")\n",
|
||||
" for query in queries:\n",
|
||||
" print(f\"\\nQuery: {query}\")\n",
|
||||
" result = send_query(query, model)\n",
|
||||
" print(\"Response:\")\n",
|
||||
" print(json.dumps(result, indent=2))\n",
|
||||
" print(\"=\" * 50)\n",
|
||||
"\n",
|
||||
"def test_followup_queries(model=\"openai\"):\n",
|
||||
" queries = [\n",
|
||||
" \"What is the latest local news from a particular town?\"\n",
|
||||
" ]\n",
|
||||
" \n",
|
||||
" print(f\"Testing queries requiring follow-up using {model} model:\")\n",
|
||||
" for query in queries:\n",
|
||||
" print(f\"\\nInitial Query: {query}\")\n",
|
||||
" result = send_query(query, model)\n",
|
||||
" print(\"Initial Response:\")\n",
|
||||
" print(json.dumps(result, indent=2))\n",
|
||||
" \n",
|
||||
" follow_up_count = 0\n",
|
||||
" max_follow_ups = 3\n",
|
||||
" \n",
|
||||
" while result.get(\"needs_more_info\", False) and follow_up_count < max_follow_ups:\n",
|
||||
" follow_up = input(\"Please provide more information: \")\n",
|
||||
" follow_up_query = f\"{query} {follow_up}\"\n",
|
||||
" follow_up_result = send_query(follow_up_query, model)\n",
|
||||
" print(f\"\\nFollow-up Response {follow_up_count + 1}:\")\n",
|
||||
" print(json.dumps(follow_up_result, indent=2))\n",
|
||||
" \n",
|
||||
" result = follow_up_result\n",
|
||||
" follow_up_count += 1\n",
|
||||
" \n",
|
||||
" if follow_up_count == max_follow_ups:\n",
|
||||
" print(\"\\nMaximum number of follow-ups reached. Moving to next query.\")\n",
|
||||
" elif not result.get(\"needs_more_info\", False):\n",
|
||||
" print(\"\\nFinal Response:\")\n",
|
||||
" print(json.dumps(result, indent=2))\n",
|
||||
" \n",
|
||||
" print(\"=\" * 50)\n",
|
||||
"\n",
|
||||
"# Run the tests\n",
|
||||
"#print(\"Running simple query tests with Ollama:\\n\")\n",
|
||||
"#test_simple_queries(\"ollama\")\n",
|
||||
"\n",
|
||||
"print(\"\\nRunning simple query tests with OpenAI:\\n\")\n",
|
||||
"test_simple_queries(\"openai\")\n",
|
||||
"\n",
|
||||
"#print(\"\\nRunning follow-up query tests with Ollama:\\n\")\n",
|
||||
"#test_followup_queries(\"ollama\")\n",
|
||||
"\n",
|
||||
"#print(\"\\nRunning follow-up query tests with OpenAI:\\n\")\n",
|
||||
"#test_followup_queries(\"openai\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
Reference in New Issue
Block a user