Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 165 additions & 22 deletions api/memory/graphiti_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# pylint: disable=all
import asyncio
import os
import uuid
from typing import List, Dict, Any, Optional
from datetime import datetime

Expand All @@ -26,6 +27,22 @@
from litellm import completion


def extract_embedding_model_name(full_model_name: str) -> str:
"""
Extract just the model name without provider prefix for Graphiti.

Args:
full_model_name: Model name that may include provider prefix (e.g., "azure/text-embedding-ada-002")

Returns:
Model name without prefix (e.g., "text-embedding-ada-002")
"""
if "/" in full_model_name:
return full_model_name.split("/", 1)[1] # Remove provider prefix
else:
return full_model_name


class MemoryTool:
"""Memory management tool for handling user memories and interactions."""

Expand All @@ -43,10 +60,12 @@ def __init__(self, user_id: str, graph_id: str):


@classmethod
async def create(cls, user_id: str, graph_id: str) -> "MemoryTool":
async def create(cls, user_id: str, graph_id: str, use_direct_entities: bool = True) -> "MemoryTool":
"""Async factory to construct and initialize the tool."""
self = cls(user_id, graph_id)
await self._ensure_database_node(graph_id, user_id)

await self._ensure_entity_nodes_direct(user_id, graph_id)


vector_size = Config.EMBEDDING_MODEL.get_vector_size()
driver = self.graphiti_client.driver
Expand Down Expand Up @@ -128,6 +147,115 @@ async def _ensure_database_node(self, database_name: str, user_id: str) -> Optio
print(f"Error creating database node for {database_name}: {e}")
return None

async def _ensure_entity_nodes_direct(self, user_id: str, database_name: str) -> bool:
"""
Ensure user and database entity nodes exist using direct Cypher queries.
This function creates Entity nodes similar to what Graphiti does but with hardcoded Cypher.
"""
try:
graph_driver = self.graphiti_client.driver

# Check if user entity node already exists
user_node_name = f"User {user_id}"
check_user_query = """
MATCH (n:Entity {name: $name})
RETURN n.uuid AS uuid
LIMIT 1
"""
user_check_result = await graph_driver.execute_query(check_user_query, name=user_node_name)

if not user_check_result[0]: # If no records found, create user node
user_uuid = str(uuid.uuid4())
user_name_embedding = Config.EMBEDDING_MODEL.embed(user_node_name)[0]

user_node_data = {
'uuid': user_uuid,
'name': user_node_name,
'group_id': '\\_',
Comment thread
galshubeli marked this conversation as resolved.
'created_at': datetime.now().isoformat(),
'summary': f'User {user_id} is using QueryWeaver',
'name_embedding': user_name_embedding
}

# Execute Cypher query for user entity node
user_cypher = """
MERGE (n:Entity {uuid: $node.uuid})
SET n = $node
SET n.timestamp = timestamp()
WITH n, $node AS node
SET n.name_embedding = vecf32(node.name_embedding)
RETURN n.uuid AS uuid
"""

await graph_driver.execute_query(user_cypher, node=user_node_data)
print(f"Created user entity node: {user_node_name} with UUID: {user_uuid}")
else:
print(f"User entity node already exists: {user_node_name}")

# Check if database entity node already exists
database_node_name = f"Database {database_name}"
check_database_query = """
MATCH (n:Entity {name: $name})
RETURN n.uuid AS uuid
LIMIT 1
"""
database_check_result = await graph_driver.execute_query(check_database_query, name=database_node_name)

if not database_check_result[0]: # If no records found, create database node
database_uuid = str(uuid.uuid4())
database_name_embedding = Config.EMBEDDING_MODEL.embed(database_node_name)[0]

database_node_data = {
'uuid': database_uuid,
'name': database_node_name,
'group_id': '\\_',
Comment thread
galshubeli marked this conversation as resolved.
'created_at': datetime.now().isoformat(),
'summary': f'Database {database_name} available for querying by user {user_id}',
'name_embedding': database_name_embedding
}

# Execute Cypher query for database entity node
database_cypher = """
MERGE (n:Entity {uuid: $node.uuid})
SET n = $node
SET n.timestamp = timestamp()
WITH n, $node AS node
SET n.name_embedding = vecf32(node.name_embedding)
RETURN n.uuid AS uuid
"""

await graph_driver.execute_query(database_cypher, node=database_node_data)
print(f"Created database entity node: {database_node_name} with UUID: {database_uuid}")
else:
print(f"Database entity node already exists: {database_node_name}")

# Create HAS_DATABASE relationship between user and database entities
try:
relationship_query = """
MATCH (user:Entity {name: $user_name})
MATCH (db:Entity {name: $database_name})
MERGE (user)-[r:HAS_DATABASE]->(db)
RETURN r
"""

await graph_driver.execute_query(
relationship_query,
user_name=user_node_name,
database_name=database_node_name,
timestamp=datetime.now().isoformat()
Comment thread
galshubeli marked this conversation as resolved.
)
print(f"Created HAS_DATABASE relationship between {user_node_name} and {database_node_name}")

except Exception as rel_error:
print(f"Error creating HAS_DATABASE relationship: {rel_error}")
# Don't fail the entire function if relationship creation fails

return True

except Exception as e:
print(f"Error creating entity nodes directly for user {user_id} and database {database_name}: {e}")
return False

async def add_new_memory(self, conversation: Dict[str, Any]) -> bool:
# Use LLM to analyze and summarize the conversation with focus on graph-oriented database facts
analysis = await self.summarize_conversation(conversation)
Expand Down Expand Up @@ -177,26 +305,24 @@ async def save_query_memory(self, query: str, sql_query: str, success: bool, err
"""
try:
database_name = self.graph_id

# Find the database node
database_node_name = f"Database {database_name}"
node_search_config = NODE_HYBRID_SEARCH_RRF.model_copy(deep=True)
node_search_config.limit = 1
graph_driver = self.graphiti_client.driver

database_node_results = await self.graphiti_client.search_(
query=database_node_name,
config=node_search_config,
)
# Find the database node using direct Cypher query
find_database_query = """
MATCH (n:Entity {name: $name})
RETURN n.uuid AS uuid
LIMIT 1
"""

database_result = await graph_driver.execute_query(find_database_query, name=database_node_name)

# Check if database node exists
database_node_exists = False
for node in database_node_results.nodes:
if node.name == database_node_name:
database_node_exists = True
database_node_uuid = node.uuid
break
if not database_node_exists:
if not database_result[0]: # If no records found
print(f"Database entity node {database_node_name} not found")
return False

database_node_uuid = database_result[0][0]['uuid']

# Check if Query node with same user_query and sql_query already exists
relationship_type = "SUCCESS" if success else "FAILED"
Expand Down Expand Up @@ -238,7 +364,7 @@ async def save_query_memory(self, query: str, sql_query: str, success: bool, err
CREATE (db)-[:{relationship_type} {{timestamp: timestamp()}}]->(q)
RETURN q.uuid as query_uuid
"""

# Execute the Cypher query through Graphiti's graph driver
try:
result = await graph_driver.execute_query(cypher_query, embedding=embeddings)
Expand Down Expand Up @@ -598,7 +724,10 @@ def __init__(self):
self.endpoint = os.getenv('AZURE_API_BASE')
self.api_version = os.getenv('AZURE_API_VERSION', '2024-02-01')
self.model_choice = "gpt-4.1" # Use the model name directly
self.embedding_model = "text-embedding-ada-002" # Use model name, not deployment

# Extract just the model name without provider prefix for Graphiti
self.embedding_model = extract_embedding_model_name(Config.EMBEDDING_MODEL_NAME)

self.small_model = os.getenv('AZURE_SMALL_MODEL', 'gpt-4o-mini')

# Use model names directly instead of deployment names
Expand Down Expand Up @@ -652,7 +781,10 @@ def create_graphiti_client(falkor_driver: FalkorDriver) -> Graphiti:
graph_driver=falkor_driver,
llm_client=OpenAIClient(config=azure_llm_config, client=llm_client_azure),
embedder=OpenAIEmbedder(
config=OpenAIEmbedderConfig(embedding_model=config.embedding_deployment),
config=OpenAIEmbedderConfig(
embedding_model=config.embedding_deployment,
embedding_dim=1536
),
client=embedding_client_azure,
),
cross_encoder=OpenAIRerankerClient(
Expand All @@ -662,8 +794,19 @@ def create_graphiti_client(falkor_driver: FalkorDriver) -> Graphiti:
client=llm_client_azure,
),
)
else: # Fallback to default OpenAI config
graphiti_client = Graphiti(graph_driver=falkor_driver)
else: # Fallback to default OpenAI config but use Config's embedding model
# Extract just the model name without provider prefix for Graphiti
embedding_model_name = extract_embedding_model_name(Config.EMBEDDING_MODEL_NAME)

graphiti_client = Graphiti(
graph_driver=falkor_driver,
embedder=OpenAIEmbedder(
config=OpenAIEmbedderConfig(
embedding_model=embedding_model_name,
embedding_dim=1536
)
),
)

return graphiti_client

2 changes: 1 addition & 1 deletion app/templates/components/chat_header.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<img src="/static/icons/queryweaver.svg" alt="Chat Logo" class="logo">
<h1>Natural Language to SQL Generator</h1>
<div class="button-container">
<button class="action-button" id="graph-select-refresh">
<button class="action-button" id="graph-select-refresh" disabled>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-width="2"
d="M20,8 C18.5974037,5.04031171 15.536972,3 12,3 C7.02943725,3 3,7.02943725 3,12 C3,16.9705627 7.02943725,21 12,21 L12,21 C16.9705627,21 21,16.9705627 21,12 M21,3 L21,9 L15,9" />
Expand Down
2 changes: 1 addition & 1 deletion app/templates/components/chat_input.j2
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{# Chat input area with text input and action buttons #}
<div class="chat-input">
<div class="input-container" id="input-container">
<input type="text" id="message-input" placeholder="Describe the SQL query you want..." />
<input type="text" id="message-input" placeholder="Describe the SQL query you want..." disabled />
<button class="input-button" title="Submit" id="submit-button">
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Rounded rectangle background -->
Expand Down
3 changes: 1 addition & 2 deletions app/ts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ function setupEventListeners() {

if (!refreshButton) return;

if (!selected || selected === "Select Database")
return alert("Please select a database to refresh");
if (!selected || selected === "Select Database") return

refreshButton.classList.add("loading");

Expand Down
7 changes: 2 additions & 5 deletions app/ts/modules/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@ export async function sendMessage() {
if (!message) return;

const selectedValue = getSelectedGraph() || '';
if (!selectedValue || selectedValue === "Select Database") {
addMessage('Please select a graph from the dropdown before sending a message.', "followup");
return;
}
if (!selectedValue || selectedValue === "Select Database") return

if (state.currentRequestController) {
state.currentRequestController.abort();
}

Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of a false parameter to addMessage changes the function signature without clear documentation of what this boolean parameter represents. Consider using a named parameter or adding a comment to clarify the purpose of this parameter.

Copilot uses AI. Check for mistakes.
addMessage(message, "user", (window as any).currentUser || null);
addMessage(message, "user", false, (window as any).currentUser || null);
if (DOM.messageInput) DOM.messageInput.value = '';
Comment on lines +20 to 21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure all addMessage call sites use the updated argument order.

This call is correct (3rd arg isQuery=false, 4th user). There are older call sites passing the user object as the 3rd arg, which drops the avatar. Verify and align.

Run to find suspicious 3-arg "user" calls:

If matches exist, update them to addMessage(msg, "user", false, currentUser).


🏁 Script executed:

#!/bin/bash
# 3-arg addMessage with "user" as 2nd arg (likely missing the boolean third arg)
rg -nP --type=ts -C2 '\baddMessage\s*\(\s*[^,]+,\s*"user"\s*,\s*[^,\)]+\s*\)'

Length of output: 407


Update the 3-arg addMessage call on line 257 to include the isQuery boolean.

--- a/app/ts/modules/chat.ts
+++ b/app/ts/modules/chat.ts
@@ 255,262
-   addMessage(`User choice: ${confirmation}`, "user", (window as any).currentUser || null);
+   addMessage(`User choice: ${confirmation}`, "user", false, (window as any).currentUser || null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
addMessage(message, "user", false, (window as any).currentUser || null);
if (DOM.messageInput) DOM.messageInput.value = '';
++ b/app/ts/modules/chat.ts
@@ -255,7 +255,7 @@
// … other logic …
addMessage(`User choice: ${confirmation}`, "user", false, (window as any).currentUser || null);
// … following logic …
🤖 Prompt for AI Agents
In app/ts/modules/chat.ts around lines 20 to 21, the addMessage invocation must
include the isQuery boolean parameter; modify the call to pass the appropriate
isQuery value (insert the boolean as the third argument) so the remaining
arguments are shifted accordingly (e.g., addMessage(message, "user", /*isQuery*/
false, /*other args*/ (window as any).currentUser || null)); ensure the argument
order matches addMessage's signature.


// Show typing indicator
Expand Down
4 changes: 4 additions & 0 deletions app/ts/modules/graph_select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export function addGraphOption(name: string, onSelect: (n: string) => void, onDe
row.appendChild(delBtn);

row.addEventListener('click', () => {
if (DOM.graphSelectRefresh && DOM.submitButton) {
DOM.graphSelectRefresh.disabled = false
DOM.submitButton.disabled = false
};
Comment on lines +46 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Also enable the message input; enable controls independently

Currently only refresh and submit are enabled, and only if both elements exist. Enable each control independently and re-enable the input so users can type immediately.

-        if (DOM.graphSelectRefresh && DOM.submitButton) {
-            DOM.graphSelectRefresh.disabled = false
-            DOM.submitButton.disabled = false
-        };
+        if (DOM.graphSelectRefresh) {
+            DOM.graphSelectRefresh.disabled = false;
+            DOM.graphSelectRefresh.removeAttribute('aria-disabled');
+        }
+        if (DOM.submitButton) {
+            DOM.submitButton.disabled = false;
+        }
+        if (DOM.messageInput) {
+            DOM.messageInput.disabled = false;
+            DOM.messageInput.focus();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (DOM.graphSelectRefresh && DOM.submitButton) {
DOM.graphSelectRefresh.disabled = false
DOM.submitButton.disabled = false
};
// Re-enable the graph-select refresh button if it exists
if (DOM.graphSelectRefresh) {
DOM.graphSelectRefresh.disabled = false;
DOM.graphSelectRefresh.removeAttribute('aria-disabled');
}
// Re-enable the submit button if it exists
if (DOM.submitButton) {
DOM.submitButton.disabled = false;
}
// Re-enable and focus the message input so users can type immediately
if (DOM.messageInput) {
DOM.messageInput.disabled = false;
DOM.messageInput.focus();
}
🤖 Prompt for AI Agents
In app/ts/modules/graph_select.ts around lines 46 to 49, the code only enables
refresh and submit when both elements exist and neglects the message input;
change it to enable each control independently by checking each DOM element
separately (if DOM.graphSelectRefresh then set disabled = false; if
DOM.submitButton then set disabled = false; if DOM.messageInput then set
disabled = false and call focus() so users can type immediately), ensuring each
check is independent rather than combined.

setSelectedGraph(name);
onSelect(name);
optionsContainer.classList.remove('open');
Expand Down