77# from pydispatch import dispatcher
88from dotenv import load_dotenv
99import os
10+ from urllib .parse import urlparse , urlunparse
1011
1112from fastapi import HTTPException
1213
@@ -86,34 +87,72 @@ def makeDecisionFromKG(query: str) -> str:
8687
8788async def ReasoningAgent ():
8889 SYSTEM_PROMPT = """
89- You are an intelligent AI reasoning agent connected to a Neo4j Knowledge Graph.
90- You can:
91- - Generate Cypher queries to retrieve relevant graph data (using fuzzy search where appropriate)
92- - Analyze the graph results and make logical or data-driven decisions based on them.
93-
94- You have access to these tools:
95- 1. queryNeo4J(query: str) — Execute Cypher queries on Neo4j and return results.
96- 2. makeDecisionFromKG(data: dict) — Analyze Neo4j query results and make a decision or summary.
97-
98- Rules:
99- - Always first use `queryNeo4J` to gather information before making any decision.
100- - When forming Cypher queries:
101- - Use fuzzy or partial matching (`CONTAINS`, `toLower()`, or regex `=~ '(?i).*<term>.*'`)
102- - Match node and relationship names exactly as defined in the KG schema.
103- - Never hallucinate labels or relationships that are not in the schema.
104- - After retrieving data, use `makeDecisionFromKG` to interpret the results, summarize insights, or provide reasoning.
105- - If the question cannot be answered from the graph, say so clearly.
106-
107- Example reasoning flow:
108- User: "What packages does SLT Mobitel offer?"
109- → Step 1: Use `queryNeo4J` with:
110- MATCH (c:Company)-[:HAS]->(p:Package)
111- WHERE toLower(c.name) CONTAINS toLower("slt mobitel")
112- RETURN p.name, p.price
113- → Step 2: Use `makeDecisionFromKG` to analyze results and summarize.
114-
115- Be clear, structured, and logical in your thought process.
116- """
90+ You are an intelligent AI reasoning agent connected to a Neo4j Knowledge Graph.
91+ Your capabilities:
92+ - Discover schema elements (labels, relationship types, property keys) when the user doesn't know exact KG keywords.
93+ - Generate Cypher queries that use fuzzy/partial matching to find relevant nodes and relationships.
94+ - Analyze query results and make decisions or summaries using the tool `makeDecisionFromKG`.
95+
96+ Tools available:
97+ 1. queryNeo4J(query: str) — Execute Cypher queries on Neo4j and return results.
98+ 2. makeDecisionFromKG(data: dict) — Analyze Neo4j query results and make a decision or summary.
99+
100+ High-level rules:
101+ - Always start by discovering schema candidates relevant to the user's query (labels, relationship types, property keys) before issuing content queries.
102+ - NEVER hallucinate labels, relationship types, or properties that are not discoverable in the graph. Use actual results from Neo4j to decide.
103+ - Prefer safe, read-only Cypher (MATCH, RETURN, CALL db.*) unless explicitly asked to write.
104+ - Use fuzzy matching (`CONTAINS`, `toLower()`, or case-insensitive regex) when matching user terms to schema elements or data values.
105+ - If no matches are found, report that clearly and provide suggested alternative search terms, synonyms, or explain how the user could rephrase.
106+
107+ Schema-discovery queries (Neo4j-native):
108+ - List all relationship types:
109+ CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType;
110+ - List all labels:
111+ CALL db.labels() YIELD label RETURN label;
112+ - List all property keys:
113+ CALL db.propertyKeys() YIELD propertyKey RETURN propertyKey;
114+
115+ Fuzzy-search templates (replace <term>):
116+ - Find relationship types matching a user term:
117+ MATCH ()-[r]-()
118+ WHERE toLower(type(r)) CONTAINS toLower('<term>')
119+ RETURN DISTINCT type(r) AS relType, count(r) AS occurrences
120+ ORDER BY occurrences DESC;
121+ - Find labels that match a user term:
122+ CALL db.labels() YIELD label
123+ WHERE toLower(label) CONTAINS toLower('<term>')
124+ RETURN label;
125+ - Find nodes whose properties match a user term:
126+ MATCH (n)
127+ WHERE any(k IN keys(n) WHERE toString(n[k]) =~ '(?i).*<term>.*')
128+ RETURN labels(n) AS labels, n AS node, size(keys(n)) AS propertyCount;
129+
130+ Once a candidate relationship or label is found, fetch content nodes:
131+ MATCH (a)-[r:`<relationship>`]->(b)
132+ RETURN labels(a) AS fromLabels, a.name AS fromName,
133+ type(r) AS rel,
134+ labels(b) AS toLabels, b.name AS toName;
135+
136+ When you find candidate relationship types or labels:
137+ - Return a short ranked list of best matches (relType or label, count of occurrences).
138+ - Automatically run a follow-up content query on the top candidates and summarize results using `makeDecisionFromKG`.
139+
140+ Fallback behavior:
141+ - If no schema or data matches are found for the user term:
142+ - Return: "No matching labels or relationship types found for '<term>' in the knowledge graph."
143+ - Provide 2–4 suggested synonyms or alternate search terms the user might try.
144+ - Suggest an explicit schema-discovery run (CALL db.* queries) if permitted by the user.
145+
146+ Safety and precision:
147+ - Always put the user term into safe, parameterized Cypher or escape user input properly to avoid syntax issues.
148+ - Prefer `toLower(... ) CONTAINS toLower(...)` for robust partial matching. Use regex `=~ '(?i).*term.*'` only when needed.
149+
150+ Response style:
151+ - Be clear, structured, and logical.
152+ - For schema discovery steps, show the query used and the succinct ranked results (up to 5 candidates).
153+ - For content queries, summarize findings and pass the raw results to `makeDecisionFromKG` for final interpretation.
154+ """
155+
117156
118157
119158 tools = [queryNeo4J , makeDecisionFromKG ]
@@ -466,6 +505,26 @@ async def getKeywordById(id):
466505 return None
467506 return result
468507
508+ # Get details with keyword name
509+ async def getKeywordByDomain (url ):
510+ try :
511+ if not url .startswith (("http://" , "https://" )):
512+ url = "http://" + url
513+
514+ parsed_url = urlparse (url )
515+ domain = parsed_url .netloc .replace ("www." , "" )
516+
517+ result = await keyword_collection .find_one (
518+ {"keyword" : {"$regex" : domain , "$options" : "i" }}
519+ )
520+
521+ return result
522+
523+ except Exception as e :
524+ print (e )
525+ return None
526+ return result
527+
469528
470529# Add urls to keyword document
471530async def storeRelevantUrls (keywordId ):
@@ -604,7 +663,7 @@ async def summarizeUsingAgent(keywordId):
604663 return None
605664
606665
607- async def exec (keyword , domain ):
666+ async def exec (keyword ):
608667 """
609668 Complete workflow:
610669 1. Store keyword
@@ -613,19 +672,32 @@ async def exec(keyword , domain):
613672 4. Summarize content (only if crawl succeeds)
614673 """
615674
616- # Step 1: Store keyword
675+ # Step 1: Store keyword or add it to existing keyword
617676 print ("\n " + "=" * 80 )
618- print ("STEP 1: Storing keyword" )
677+ print ("STEP 1.1: Check keyword" )
619678 print ("=" * 80 )
620- domain = "com"
621- storedKeyword = await storeKeyword (keyword , domain )
622- print (f"Keyword stored with ID: { storedKeyword .inserted_id } " )
679+
680+ result = await getKeywordByDomain (keyword )
681+
682+ if not result :
683+ print ("\n " + "=" * 80 )
684+ print ("STEP 1.2: Storing keyword" )
685+ print ("=" * 80 )
686+ domain = "com"
687+ storedKeyword = await storeKeyword (keyword , domain )
688+ storedKeywordId = storedKeyword .inserted_id
689+ print (f"Keyword stored with ID: { storedKeywordId } " )
690+ else :
691+ print ("Id is founded!" )
692+ print (result ["_id" ])
693+ storedKeywordId = result ["_id" ]
694+ print ("Keyword Already founded! Skip creating new keyword id..." )
623695
624696 # Step 2: Get keyword details
625697 print ("\n " + "=" * 80 )
626698 print ("STEP 2: Fetching keyword details" )
627699 print ("=" * 80 )
628- resultMongo = await getKeywordById (storedKeyword . inserted_id )
700+ resultMongo = await getKeywordById (storedKeywordId )
629701 keywordId = resultMongo ["_id" ]
630702
631703 # Step 3: Fetch Google URLs
0 commit comments