Skip to content

Commit ff6d719

Browse files
feat: support dynacmic graph via neo4j (#1254)
Co-authored-by: LastWhisper <pkuwkl@gmail.com> Co-authored-by: whuwkl <whuwkl@gmail.com>
1 parent 61827d2 commit ff6d719

File tree

5 files changed

+1332
-8
lines changed

5 files changed

+1332
-8
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ Practical guides and tutorials for implementing specific functionalities in CAME
335335
|:---|:---|
336336
| **[Role-Playing Scraper for Report & Knowledge Graph Generation](https://docs.camel-ai.org/cookbooks/applications/roleplaying_scraper.html)** | Create role-playing agents for data scraping and reporting. |
337337
| **[Create A Hackathon Judge Committee with Workforce](https://docs.camel-ai.org/cookbooks/multi_agent_society/workforce_judge_committee.html)** | Building a team of agents for collaborative judging. |
338+
| **[Dynamic Knowledge Graph Role-Playing: Multi-Agent System with dynamic, temporally-aware knowledge graphs](https://docs.camel-ai.org/cookbooks/applications/dyamic_knowledge_graph.html)** | Builds dynamic, temporally-aware knowledge graphs for financial applications using a multi-agent system. It processes financial reports, news articles, and research papers to help traders analyze data, identify relationships, and uncover market insights. The system also utilizes diverse and optional element node deduplication techniques to ensure data integrity and optimize graph structure for financial decision-making. |
338339
| **[Customer Service Discord Bot with Agentic RAG](https://docs.camel-ai.org/cookbooks/applications/customer_service_Discord_bot_using_SambaNova_with_agentic_RAG.html)** | Learn how to build a robust customer service bot for Discord using Agentic RAG. |
339340
| **[Customer Service Discord Bot with Local Model](https://docs.camel-ai.org/cookbooks/applications/customer_service_Discord_bot_using_local_model_with_agentic_RAG.html)** | Learn how to build a robust customer service bot for Discord using Agentic RAG which supports local deployment. |
340341

camel/agents/knowledge_graph_agent.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ def _parse_graph_elements(self, input_string: str) -> GraphElement:
226226
node_pattern = r"Node\(id='(.*?)', type='(.*?)'\)"
227227
rel_pattern = (
228228
r"Relationship\(subj=Node\(id='(.*?)', type='(.*?)'\), "
229-
r"obj=Node\(id='(.*?)', type='(.*?)'\), type='(.*?)'\)"
229+
r"obj=Node\(id='(.*?)', type='(.*?)'\), "
230+
r"type='(.*?)'(?:, timestamp='(.*?)')?\)"
230231
)
231232

232233
nodes = {}
@@ -243,13 +244,24 @@ def _parse_graph_elements(self, input_string: str) -> GraphElement:
243244

244245
# Extract relationships
245246
for match in re.finditer(rel_pattern, input_string):
246-
subj_id, subj_type, obj_id, obj_type, rel_type = match.groups()
247+
groups = match.groups()
248+
if len(groups) == 6:
249+
subj_id, subj_type, obj_id, obj_type, rel_type, timestamp = (
250+
groups
251+
)
252+
else:
253+
subj_id, subj_type, obj_id, obj_type, rel_type = groups
254+
timestamp = None
247255
properties = {'source': 'agent_created'}
248256
if subj_id in nodes and obj_id in nodes:
249257
subj = nodes[subj_id]
250258
obj = nodes[obj_id]
251259
relationship = Relationship(
252-
subj=subj, obj=obj, type=rel_type, properties=properties
260+
subj=subj,
261+
obj=obj,
262+
type=rel_type,
263+
timestamp=timestamp,
264+
properties=properties,
253265
)
254266
if self._validate_relationship(relationship):
255267
relationships.append(relationship)

camel/storages/graph_storages/graph_element.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
1414
from __future__ import annotations
1515

16-
from typing import List, Union
16+
from typing import List, Optional, Union
1717

1818
from pydantic import BaseModel, ConfigDict, Field
1919

@@ -45,13 +45,15 @@ class Relationship(BaseModel):
4545
subj (Node): The subject/source node of the relationship.
4646
obj (Node): The object/target node of the relationship.
4747
type (str): The type of the relationship.
48+
timestamp (str, optional): The timestamp of the relationship.
4849
properties (dict): Additional properties associated with the
4950
relationship.
5051
"""
5152

5253
subj: Node
5354
obj: Node
5455
type: str = "Relationship"
56+
timestamp: Optional[str] = None
5557
properties: dict = Field(default_factory=dict)
5658

5759

camel/storages/graph_storages/neo4j_graph.py

+78-4
Original file line numberDiff line numberDiff line change
@@ -339,18 +339,24 @@ def refresh_schema(self) -> None:
339339
]
340340
)
341341

342-
def add_triplet(self, subj: str, obj: str, rel: str) -> None:
343-
r"""Adds a relationship (triplet) between two entities in the database.
342+
def add_triplet(
343+
self, subj: str, obj: str, rel: str, timestamp: Optional[str] = None
344+
) -> None:
345+
r"""Adds a relationship (triplet) between two entities
346+
in the database with a timestamp.
344347
345348
Args:
346349
subj (str): The identifier for the subject entity.
347350
obj (str): The identifier for the object entity.
348351
rel (str): The relationship between the subject and object.
352+
timestamp (Optional[str]): The timestamp of the relationship.
353+
Defaults to None.
349354
"""
350355
query = """
351356
MERGE (n1:`%s` {id:$subj})
352357
MERGE (n2:`%s` {id:$obj})
353-
MERGE (n1)-[:`%s`]->(n2)
358+
MERGE (n1)-[r:`%s`]->(n2)
359+
SET r.timestamp = $timestamp
354360
"""
355361

356362
prepared_statement = query % (
@@ -361,7 +367,10 @@ def add_triplet(self, subj: str, obj: str, rel: str) -> None:
361367

362368
# Execute the query within a database session
363369
with self.driver.session(database=self.database) as session:
364-
session.run(prepared_statement, {"subj": subj, "obj": obj})
370+
session.run(
371+
prepared_statement,
372+
{"subj": subj, "obj": obj, "timestamp": timestamp},
373+
)
365374

366375
def _delete_rel(self, subj: str, obj: str, rel: str) -> None:
367376
r"""Deletes a specific relationship between two nodes in the Neo4j
@@ -721,3 +730,68 @@ def common_neighbour_aware_random_walk(
721730
return result[0] if result else {}
722731
except CypherSyntaxError as e:
723732
raise ValueError(f"Generated Cypher Statement is not valid\n{e}")
733+
734+
def get_triplet(
735+
self,
736+
subj: Optional[str] = None,
737+
obj: Optional[str] = None,
738+
rel: Optional[str] = None,
739+
) -> List[Dict[str, Any]]:
740+
r"""
741+
Query triplet information. If subj, obj, or rel is
742+
not specified, returns all matching triplets.
743+
744+
Args:
745+
subj (Optional[str]): The ID of the subject node.
746+
If None, matches any subject node.
747+
obj (Optional[str]): The ID of the object node.
748+
If None, matches any object node.
749+
rel (Optional[str]): The type of relationship.
750+
If None, matches any relationship type.
751+
752+
Returns:
753+
List[Dict[str, Any]]: A list of matching triplets,
754+
each containing subj, obj, rel, and timestamp.
755+
"""
756+
import logging
757+
758+
logging.basicConfig(level=logging.DEBUG)
759+
logger = logging.getLogger(__name__)
760+
761+
# Construct the query
762+
query = """
763+
MATCH (n1:Entity)-[r]->(n2:Entity)
764+
WHERE ($subj IS NULL OR n1.id = $subj)
765+
AND ($obj IS NULL OR n2.id = $obj)
766+
AND ($rel IS NULL OR type(r) = $rel)
767+
RETURN n1.id AS subj, n2.id AS obj,
768+
type(r) AS rel, r.timestamp AS timestamp
769+
"""
770+
771+
# Construct the query parameters
772+
params = {
773+
"subj": subj
774+
if subj is not None
775+
else None, # If subj is None, match any subject node
776+
"obj": obj
777+
if obj is not None
778+
else None, # If obj is None, match any object node
779+
"rel": rel
780+
if rel is not None
781+
else None, # If rel is None, match any relationship type
782+
}
783+
784+
logger.debug(f"Executing query: {query}")
785+
logger.debug(f"Query parameters: {params}")
786+
787+
with self.driver.session(database=self.database) as session:
788+
try:
789+
result = session.run(query, params)
790+
records = [record.data() for record in result]
791+
logger.debug(
792+
f"Query returned {len(records)} records: {records}"
793+
)
794+
return records
795+
except Exception as e:
796+
logger.error(f"Error executing query: {e}")
797+
return []

0 commit comments

Comments
 (0)