From 444730e41b0d455ee0209e081b1e8e5d4ce0f779 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Thu, 26 Jun 2025 15:41:27 +0300 Subject: [PATCH 1/9] cypher-agent --- graphrag_sdk/chat_session.py | 174 +++++++++++++++++++++++++++---- graphrag_sdk/fixtures/prompts.py | 136 ++++++++++++++++-------- graphrag_sdk/helpers.py | 128 +++++++++++++---------- graphrag_sdk/kg.py | 15 ++- 4 files changed, 333 insertions(+), 120 deletions(-) diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index cda7ed24..1b0b02ab 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -51,7 +51,7 @@ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, self.ontology = ontology # Filter the ontology to remove unique and required attributes that are not needed for Q&A. - ontology_prompt = self.clean_ontology_for_prompt(ontology) + ontology_prompt = clean_ontology_for_prompt(ontology) cypher_system_instruction = cypher_system_instruction.format(ontology=ontology_prompt) @@ -182,30 +182,162 @@ def send_message_stream(self, message: str) -> Iterator[str]: "context": context, "cypher": cypher } + +class CypherSession: + """ + Represents a Cypher-only session with a Knowledge Graph. + This class generates Cypher queries and extracts context without performing Q&A. + + Args: + model_config (KnowledgeGraphModelConfig): The model configuration to use. + ontology (Ontology): The ontology to use. + graph (Graph): The graph to query. + + Examples: + >>> from graphrag_sdk import KnowledgeGraph, Orchestrator + >>> from graphrag_sdk.ontology import Ontology + >>> from graphrag_sdk.model_config import KnowledgeGraphModelConfig + >>> model_config = KnowledgeGraphModelConfig.with_model(model) + >>> kg = KnowledgeGraph("test_kg", model_config, ontology) + >>> cypher_session = CypherSession(model_config, ontology, kg.graph, + ... cypher_system_instruction, cypher_gen_prompt, cypher_gen_prompt_history) + >>> result = cypher_session.search("What is the capital of France?") + """ + + def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, graph: Graph, + cypher_system_instruction: str, cypher_gen_prompt: str, cypher_gen_prompt_history: str): + """ + Initializes a new CypherSession object. + + Args: + model_config (KnowledgeGraphModelConfig): The model configuration. + ontology (Ontology): The ontology object. + graph (Graph): The graph object. + cypher_system_instruction (str): System instruction for cypher generation. + cypher_gen_prompt (str): Prompt template for cypher generation. + cypher_gen_prompt_history (str): Prompt template for cypher generation with history. + + Attributes: + model_config (KnowledgeGraphModelConfig): The model configuration. + ontology (Ontology): The ontology object. + graph (Graph): The graph object. + cypher_chat_session: The Cypher chat session object. + """ + self.model_config = model_config + self.graph = graph + self.ontology = ontology + + # Filter the ontology to remove unique and required attributes that are not needed for Q&A. + ontology_prompt = clean_ontology_for_prompt(ontology) + + cypher_system_instruction = cypher_system_instruction.format(ontology=ontology_prompt) - def clean_ontology_for_prompt(self, ontology: dict) -> str: + self.cypher_prompt = cypher_gen_prompt + self.cypher_prompt_with_history = cypher_gen_prompt_history + + self.cypher_chat_session = model_config.cypher_generation.start_chat( + cypher_system_instruction + ) + + self.last_complete_response = { + "question": None, + "response": None, + "context": None, + "cypher": None + } + + # Metadata to store additional information about the chat session (currently only last query execution time) + self.metadata = {"last_query_execution_time": None} + + def _generate_cypher_query(self, message: str) -> tuple: + """ + Generate a Cypher query for the given message. + + Args: + message (str): The message to generate a query for. + + Returns: + tuple: A tuple containing (context, cypher) """ - Cleans the ontology by removing 'unique' and 'required' keys and prepares it for use in a prompt. + cypher_step = GraphQueryGenerationStep( + graph=self.graph, + chat_session=self.cypher_chat_session, + ontology=self.ontology, + last_answer=self.last_complete_response["response"], + cypher_prompt=self.cypher_prompt, + cypher_prompt_with_history=self.cypher_prompt_with_history + ) + + (context, cypher, query_execution_time) = cypher_step.run(message) + self.metadata["last_query_execution_time"] = query_execution_time + + return (context, cypher) + + def search(self, message: str) -> dict: + """ + Searches the knowledge graph by generating a Cypher query and extracting relevant context. Args: - ontology (dict): The ontology to clean and transform. + message (str): The search query or question. Returns: - str: The cleaned ontology as a JSON string. + dict: Search results containing: + - question: The original query + - context: Extracted context from the graph + - cypher: Generated Cypher query + - execution_time: Query execution time in seconds + - error: Error message if query generation failed (optional) """ - # Convert the ontology object to a JSON. - ontology = ontology.to_json() - - # Remove unique and required attributes from the ontology. - for entity in ontology["entities"]: - for attribute in entity["attributes"]: - del attribute['unique'] - del attribute['required'] - - for relation in ontology["relations"]: - for attribute in relation["attributes"]: - del attribute['unique'] - del attribute['required'] - - # Return the transformed ontology as a JSON string - return json.dumps(ontology) \ No newline at end of file + context, cypher = self._generate_cypher_query(message) + execution_time = self.metadata.get("last_query_execution_time") + + # Prepare base response + response = { + "question": message, + "context": context, + "cypher": cypher, + "execution_time": execution_time + } + + # Handle query generation failure + if not cypher: + response["error"] = "Could not generate valid cypher query" + context = cypher = None + + # Update session state + self.last_complete_response = { + "question": message, + "response": None, + "context": context, + "cypher": cypher + } + + return response + + +def clean_ontology_for_prompt(ontology: dict) -> str: + """ + Cleans the ontology by removing 'unique' and 'required' keys and prepares it for use in a prompt. + + Args: + ontology (dict): The ontology to clean and transform. + + Returns: + str: The cleaned ontology as a JSON string. + """ + # Convert the ontology object to a JSON. + ontology = ontology.to_json() + + # Remove unique and required attributes from the ontology. + for entity in ontology["entities"]: + for attribute in entity["attributes"]: + del attribute['unique'] + del attribute['required'] + + for relation in ontology["relations"]: + for attribute in relation["attributes"]: + del attribute['unique'] + del attribute['required'] + + # Return the transformed ontology as a JSON string + return json.dumps(ontology) \ No newline at end of file diff --git a/graphrag_sdk/fixtures/prompts.py b/graphrag_sdk/fixtures/prompts.py index 1f79c5d8..7871cbbf 100644 --- a/graphrag_sdk/fixtures/prompts.py +++ b/graphrag_sdk/fixtures/prompts.py @@ -399,52 +399,88 @@ """ CYPHER_GEN_SYSTEM = """ -Task: Generate OpenCypher statement to query a graph database. - -Instructions: -Use only the provided entities, relationships types and properties in the ontology. -The output must be only a valid OpenCypher statement. -Respect the order of the relationships, the arrows should always point from the "start" to the "end". -Respect the types of entities of every relationship, according to the ontology. -The OpenCypher statement must return all the relevant entities, not just the attributes requested. -The output of the OpenCypher statement will be passed to another model to answer the question, hence, make sure the OpenCypher statement returns all relevant entities, relationships, and attributes. -If the answer required multiple entities, return all the entities, relations, relationships, and their attributes. -If you cannot generate a OpenCypher statement based on the provided ontology, explain the reason to the user. -For String comparison, use the `CONTAINS` operator. -Do not use any other relationship types or properties that are not provided. -Do not respond to any questions that might ask anything else than for you to construct a OpenCypher statement. -Do not include any text except the generated OpenCypher statement, enclosed in triple backticks. -Do not include any explanations or apologies in your responses. -Do not return just the attributes requested in the question, but all related entities, relations, relationships, and attributes. -Do not change the order of the relationships, the arrows should always point from the "start" to the "end". - -The following instructions describe extra functions that can be used in the OpenCypher statement: - -Match: Describes relationships between entities using ASCII art patterns. Entities are represented by parentheses and relationships by brackets. Both can have aliases and labels. -Variable length relationships: Find entities a variable number of hops away using -[:TYPE*minHops..maxHops]->. -Bidirectional path traversal: Specify relationship direction or omit it for either direction. -Named paths: Assign a path in a MATCH clause to a single alias for future use. -Shortest paths: Find all shortest paths between two entities using allShortestPaths(). -Single-Pair minimal-weight paths: Find minimal-weight paths between a pair of entities using algo.SPpaths(). -Single-Source minimal-weight paths: Find minimal-weight paths from a given source entity using algo.SSpaths(). +Task: Generate OpenCypher statements to query a graph database based on natural language questions. + +Core Requirements: +- Use ONLY the entities, relationship types, and properties defined in the provided ontology +- Generate syntactically valid OpenCypher statements +- Return comprehensive results including all relevant entities, relationships, and their attributes +- Maintain correct relationship direction: arrows point from source to target as defined in ontology + +Query Construction Rules: +1. Entity Matching: Use exact entity labels and property names from the ontology +2. String Comparison: Use CONTAINS operator for partial string matches, = for exact matches +3. Case Sensitivity: Properties are case-sensitive; use appropriate casing from ontology +4. Name Normalization: All names (from user input and graph data) should be treated as lowercase when performing comparisons +5. Comprehensive Results: Return complete entity objects and relationships, not just requested attributes +6. Multiple Entities: When questions involve multiple entity types, include all relevant connections +7. Simple Queries: For declarative statements or single entity names, extract the relevant entity and return it with its direct relationships (1-hop only) + +Relationship Handling: +- Respect relationship direction as defined in ontology (source -> target) +- Use appropriate relationship types exactly as specified +- For bidirectional queries, specify direction explicitly or use undirected syntax when appropriate + +Advanced Features Available: +- Variable length paths: -[:TYPE*minHops..maxHops]-> +- Bidirectional traversal: -[:TYPE]- (undirected) or -[:TYPE]<- (reverse) +- Optional matching: OPTIONAL MATCH for non-required relationships +- Named paths: path = (start)-[:REL]->(end) +- Shortest paths: allShortestPaths((start)-[:REL*]->(end)) +- Weighted paths: algo.SPpaths() for single-pair, algo.SSpaths() for single-source + +Error Handling: +- If the question cannot be answered with the provided ontology, return: "UNABLE_TO_GENERATE: [brief reason]" +- If entities or relationships mentioned don't exist in ontology, return: "UNABLE_TO_GENERATE: Required entities/relationships not found in ontology" + +Output Format: +- Return ONLY the OpenCypher statement enclosed in triple backticks +- No explanations, apologies, or additional text +- Ensure query is syntactically correct before returning + +Query Validation Checklist: +- All entities exist in ontology ✓ +- All relationships exist and have correct direction ✓ +- All properties exist for their respective entities ✓ +- Syntax is valid OpenCypher ✓ +- Query returns comprehensive results ✓ Ontology: {ontology} +Example: +Question: "Which managers own technology stocks?" +Expected Output: "MATCH (m:Manager)-[:OWNS]->(s:Stock) +WHERE toLower(s.sector) CONTAINS 'technology' +RETURN m, s" + -For example, given the question "Which managers own Neo4j stocks?", the OpenCypher statement should look like this: -MATCH (m:Manager)-[:OWNS]->(s:Stock) -WHERE s.name CONTAINS 'Neo4j' -RETURN m, s""" +Simple Entity Query Example: +Question: "Apple" or "Show me Apple" +Expected Output: "MATCH (c:Company) +WHERE toLower(c.name) = 'apple' +OPTIONAL MATCH (c)-[r]-(connected) +RETURN c, r, connected" +""" CYPHER_GEN_PROMPT = """ -Using the ontology provided, generate an OpenCypher statement to query the graph database returning all relevant entities, relationships, and attributes to answer the question below. -If you cannot generate a OpenCypher statement for any reason, return an empty response. -Respect the order of the relationships, the arrows should always point from the "source" to the "target". -Please think if your answer is a valid Cypher query, and correct it if it is not. +Generate an OpenCypher statement to answer the following question using the provided ontology. + +Requirements: +- Use only entities, relationships, and properties from the ontology +- Maintain correct relationship directions (source -> target) +- Return all relevant entities and relationships, not just requested attributes +- Ensure syntactically valid OpenCypher Question: {question} -Your generated Cypher: """ + +Validation Steps: +1. Identify required entities and relationships from the question +2. Verify all components exist in the ontology +3. Construct query with proper syntax and direction +4. Ensure comprehensive result set + +Generated OpenCypher:""" CYPHER_GEN_PROMPT_WITH_ERROR = """ @@ -460,17 +496,31 @@ """ CYPHER_GEN_PROMPT_WITH_HISTORY = """ -Using the ontology provided, generate an OpenCypher statement to query the graph database returning all relevant entities, relationships, and attributes to answer the question below. +Generate an OpenCypher statement to answer the following question using the provided ontology. -First, determine if the last answers provided to the user over the entire conversation is relevant to the current question. If it is relevant, you may consider incorporating information from it into the query. If it is not relevant, ignore it and generate the query based solely on the question. +Context Analysis: +- First, determine if the last answer provided is relevant to the current question +- If relevant, consider incorporating information from it into the query (e.g., entity IDs, specific names, or context) +- If not relevant, ignore the previous answer and generate the query based solely on the current question -If you cannot generate an OpenCypher statement for any reason, return an empty string. - -Respect the order of the relationships; the arrows should always point from the "source" to the "target". +Requirements: +- Use only entities, relationships, and properties from the ontology +- Maintain correct relationship directions (source -> target) +- Return all relevant entities and relationships, not just requested attributes +- Ensure syntactically valid OpenCypher +- Apply name normalization (use toLower() for name comparisons) Last Answer: {last_answer} Question: {question} -Your generated Cypher: """ + +Validation Steps: +1. Assess relevance of last answer to current question +2. Identify required entities and relationships from the question +3. Verify all components exist in the ontology +4. Construct query with proper syntax and direction +5. Ensure comprehensive result set + +Generated OpenCypher:""" GRAPH_QA_SYSTEM = """ You are an assistant that helps to form nice and human understandable answers. diff --git a/graphrag_sdk/helpers.py b/graphrag_sdk/helpers.py index 8c7693fe..b6f494d4 100644 --- a/graphrag_sdk/helpers.py +++ b/graphrag_sdk/helpers.py @@ -221,50 +221,60 @@ def validate_cypher_relation_directions( list[str]: A list of errors if relation directions are incorrect. """ errors = [] - relations = list(re.finditer(r"\[.*?\]", cypher)) - i = 0 - for relation in relations: + + # Pattern to match complete relationship patterns: (node)-[rel]->(node) or (node)<-[rel]-(node) + # This handles both directions and various node/relationship syntaxes + relationship_pattern = r'\(([^)]*)\)\s*(?)\s*\(([^)]*)\)' + + matches = re.finditer(relationship_pattern, cypher, re.IGNORECASE) + + for match in matches: try: - relation_label = ( - re.search(r"(?:\[)(?:\w)*(?:\:)([^\d\{\]\*\.\:]+)", relation.group(0)) - .group(1) - .strip() - ) - prev_relation = relations[i - 1] if i > 0 else None - next_relation = relations[i + 1] if i < len(relations) - 1 else None - before = ( - cypher[prev_relation.end() : relation.start()] - if prev_relation - else cypher[: relation.start()] - ) - if "," in before: - before = before.split(",")[-1] - rel_before = re.search(r"([^\)\],]+)", before[::-1]).group(0)[::-1] - after = ( - cypher[relation.end() : next_relation.start()] - if next_relation - else cypher[relation.end() :] - ) - rel_after = re.search(r"([^\(\[,]+)", after).group(0) - entity_before = re.search(r"\(.+:(.*?)\)", before).group(0) - entity_after = re.search(r"\(([^\),]+)(\)?)", after).group(0) - if rel_before == "-" and rel_after == "->": - source = entity_before - target = entity_after - elif rel_before == "<-" and rel_after == "-": - source = entity_after - target = entity_before - else: + source_node = match.group(1).strip() + left_arrow = match.group(2) # '<' if present + relation_content = match.group(3).strip() + right_arrow = match.group(4) # '>' if present + target_node = match.group(5).strip() + + # Skip if no relationship label found + if not relation_content or relation_content == '': continue - - source_label = re.search(r"(?:\:)([^\)\{]+)", source).group(1).strip() - target_label = re.search(r"(?:\:)([^\)\{]+)", target).group(1).strip() - + + # Extract relationship label from content like "r:RELATIONSHIP_TYPE" or ":RELATIONSHIP_TYPE" + relation_label_match = re.search(r':\s*([A-Za-z_][A-Za-z0-9_]*)', relation_content) + if not relation_label_match: + continue + + relation_label = relation_label_match.group(1).strip() + + # Determine direction: <- means reverse, -> means forward, -- means undirected + is_directed_left = bool(left_arrow) # <- + is_directed_right = bool(right_arrow) # -> + + # Skip undirected relationships (--) as they don't have direction constraints + if not is_directed_left and not is_directed_right: + continue + + # Extract node labels from source and target + source_label = _extract_node_label(source_node) + target_label = _extract_node_label(target_node) + + # Skip if we can't extract labels (e.g., variables without labels) + if not source_label or not target_label: + continue + + # If direction is left (<-), swap source and target + if is_directed_left and not is_directed_right: + source_label, target_label = target_label, source_label + + # Get ontology relations with this label ontology_relations = ontology.get_relations_with_label(relation_label) - + if len(ontology_relations) == 0: - errors.append(f"Relation {relation_label} not found in ontology") - + # This error is already handled by validate_cypher_relations_exist + continue + + # Check if any ontology relation matches the direction found_relation = False for ontology_relation in ontology_relations: if ( @@ -273,23 +283,31 @@ def validate_cypher_relation_directions( ): found_relation = True break - + if not found_relation: errors.append( - """ - Relation {relation_label} does not connect {source_label} to {target_label}. Make sure the relation direction is correct. - Valid relations: - {valid_relations} -""".format( - relation_label=relation_label, - source_label=source_label, - target_label=target_label, - valid_relations="\n".join([str(e) for e in ontology_relations]), - ) + f"Relation '{relation_label}' does not connect {source_label} to {target_label}. " + f"Make sure the relation direction is correct. " + f"Valid relations: {', '.join([str(r) for r in ontology_relations])}" ) - - i += 1 - except Exception: + + except Exception as e: + # Skip problematic patterns rather than failing continue - + return errors + +def _extract_node_label(node_content: str) -> str: + """ + Extract node label from node content like 'n:Person' or ':Person' or 'person:Person'. + Returns the label or empty string if not found. + """ + if not node_content: + return "" + + # Look for pattern like ":Label" in the node content + label_match = re.search(r':\s*([A-Za-z_][A-Za-z0-9_]*)', node_content) + if label_match: + return label_match.group(1).strip() + + return "" diff --git a/graphrag_sdk/kg.py b/graphrag_sdk/kg.py index 752bbeb6..5284a924 100644 --- a/graphrag_sdk/kg.py +++ b/graphrag_sdk/kg.py @@ -4,7 +4,7 @@ from typing import Optional, Union from graphrag_sdk.ontology import Ontology from graphrag_sdk.source import AbstractSource -from graphrag_sdk.chat_session import ChatSession +from graphrag_sdk.chat_session import ChatSession, CypherSession from graphrag_sdk.attribute import AttributeType, Attribute from graphrag_sdk.helpers import map_dict_to_cypher_properties from graphrag_sdk.model_config import KnowledgeGraphModelConfig @@ -210,6 +210,19 @@ def chat_session(self) -> ChatSession: chat_session = ChatSession(self._model_config, self.ontology, self.graph, self.cypher_system_instruction, self.qa_system_instruction, self.cypher_gen_prompt, self.qa_prompt, self.cypher_gen_prompt_history) return chat_session + + def cypher_session(self) -> CypherSession: + """ + Create a new Cypher session. + + Returns: + CypherSession: A new Cypher session instance. + """ + cypher_session = CypherSession(self._model_config, self.ontology, self.graph, self.cypher_system_instruction, + self.cypher_gen_prompt, self.cypher_gen_prompt_history) + return cypher_session + + def add_node(self, entity: str, attributes: dict) -> None: """ Add a node to the knowledge graph, checking if it matches the ontology From f2777d409b85efd454591b237e2067807b050be8 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Wed, 2 Jul 2025 17:27:34 +0300 Subject: [PATCH 2/9] inter_updates --- .gitignore | 1 + graphrag_sdk/chat_session.py | 346 +++++++++++++++++++---------------- graphrag_sdk/kg.py | 13 +- 3 files changed, 190 insertions(+), 170 deletions(-) diff --git a/.gitignore b/.gitignore index c3418c0c..729fc84c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ logs falkordb-data weaviate-data .deepeval_telemtry.txt +examples/ufc/ufc_agent.py diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index 1b0b02ab..35e4df4a 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -9,6 +9,104 @@ CYPHER_ERROR_RES = "Sorry, I could not find the answer to your question" +class ResponseDict(dict): + """ + A dictionary that also provides property access to response components. + Maintains backward compatibility while adding new property-based access. + """ + + def __init__(self, response: 'ChatResponse'): + self._response = response + super().__init__({ + "question": response.query, + "response": response.answer, + "context": response.context, + "cypher": response.cypher + }) + + @property + def query(self) -> str: + """The original question/query.""" + return self._response.query + + @property + def cypher(self) -> str: + """The generated Cypher query.""" + return self._response.cypher + + @property + def context(self) -> str: + """The extracted context from the graph.""" + return self._response.context + + @property + def answer(self) -> str: + """The final QA answer.""" + return self._response.answer + + @property + def execution_time(self) -> float: + """Query execution time in seconds.""" + return self._response.execution_time + + @property + def error(self) -> str: + """Error message if any step failed.""" + return self._response.error + +class ChatResponse: + """ + Represents a response from a chat session with access to different pipeline stages. + """ + + def __init__(self, question: str, context: str = None, cypher: str = None, + answer: str = None, execution_time: float = None, error: str = None): + self._question = question + self._context = context + self._cypher = cypher + self._answer = answer + self._execution_time = execution_time + self._error = error + + @property + def query(self) -> str: + """The original question/query.""" + return self._question + + @property + def cypher(self) -> str: + """The generated Cypher query.""" + return self._cypher + + @property + def context(self) -> str: + """The extracted context from the graph.""" + return self._context + + @property + def answer(self) -> str: + """The final QA answer.""" + return self._answer + + @property + def execution_time(self) -> float: + """Query execution time in seconds.""" + return self._execution_time + + @property + def error(self) -> str: + """Error message if any step failed.""" + return self._error + + def to_dict(self) -> dict: + """Convert response to dictionary format for backward compatibility.""" + return { + "question": self._question, + "response": self._answer, + "context": self._context, + "cypher": self._cypher + } + class ChatSession: """ Represents a chat session with a Knowledge Graph. @@ -24,8 +122,16 @@ class ChatSession: >>> from graphrag_sdk.model_config import KnowledgeGraphModelConfig >>> model_config = KnowledgeGraphModelConfig.with_model(model) >>> kg = KnowledgeGraph("test_kg", model_config, ontology) - >>> chat_session = kg.start_chat() - >>> chat_session.send_message("What is the capital of France?") + >>> session = kg.start_chat() + >>> response = session.send_message("What is the capital of France?") + >>> # Backward compatible dict access: + >>> print(response["question"]) # "What is the capital of France?" + >>> print(response["response"]) # "Paris" + >>> # New property access: + >>> print(response.query) # "What is the capital of France?" + >>> print(response.cypher) # "MATCH (c:City)..." + >>> print(response.context) # Retrieved context + >>> print(response.answer) # "Paris" """ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, graph: Graph, @@ -38,13 +144,18 @@ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, model_config (KnowledgeGraphModelConfig): The model configuration. ontology (Ontology): The ontology object. graph (Graph): The graph object. + cypher_system_instruction (str): System instruction for cypher generation. + qa_system_instruction (str): System instruction for QA. + cypher_gen_prompt (str): Prompt template for cypher generation. + qa_prompt (str): Prompt template for QA. + cypher_gen_prompt_history (str): Prompt template for cypher generation with history. Attributes: model_config (KnowledgeGraphModelConfig): The model configuration. ontology (Ontology): The ontology object. graph (Graph): The graph object. - cypher_chat_session (CypherChatSession): The Cypher chat session object. - qa_chat_session (QAChatSession): The QA chat session object. + cypher_chat_session: The Cypher chat session object. + qa_chat_session: The QA chat session object. """ self.model_config = model_config self.graph = graph @@ -65,12 +176,8 @@ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, self.qa_chat_session = model_config.qa.start_chat( qa_system_instruction ) - self.last_complete_response = { - "question": None, - "response": None, - "context": None, - "cypher": None - } + + self.last_complete_response = None # Metadata to store additional information about the chat session (currently only last query execution time) self.metadata = {"last_query_execution_time": None} @@ -85,11 +192,13 @@ def _generate_cypher_query(self, message: str) -> tuple: Returns: tuple: A tuple containing (context, cypher) """ + last_answer = self.last_complete_response.answer if self.last_complete_response else None + cypher_step = GraphQueryGenerationStep( graph=self.graph, chat_session=self.cypher_chat_session, ontology=self.ontology, - last_answer=self.last_complete_response["response"], + last_answer=last_answer, cypher_prompt=self.cypher_prompt, cypher_prompt_with_history=self.cypher_prompt_with_history ) @@ -99,7 +208,7 @@ def _generate_cypher_query(self, message: str) -> tuple: return (context, cypher) - def send_message(self, message: str) -> dict: + def send_message(self, message: str) -> ResponseDict: """ Sends a message to the chat session. @@ -107,23 +216,32 @@ def send_message(self, message: str) -> dict: message (str): The message to send. Returns: - dict: The response to the message in the following format: - {"question": message, - "response": answer, - "context": context, - "cypher": cypher} + ResponseDict: A dict-like object with backward compatibility that also provides property access: + - dict access: result["question"], result["response"], result["context"], result["cypher"] + - property access: result.query, result.answer, result.context, result.cypher, result.execution_time + """ + response = self._send_message_internal(message) + return ResponseDict(response) + + def _send_message_internal(self, message: str) -> ChatResponse: + """ + Internal method that returns the full ChatResponse object. """ (context, cypher) = self._generate_cypher_query(message) + execution_time = self.metadata.get("last_query_execution_time") - # If the cypher is empty, return an error message + # If the cypher is empty, return an error response if not cypher or len(cypher) == 0: - self.last_complete_response = { - "question": message, - "response": CYPHER_ERROR_RES, - "context": None, - "cypher": None - } - return self.last_complete_response + response = ChatResponse( + question=message, + context=None, + cypher=None, + answer=CYPHER_ERROR_RES, + execution_time=execution_time, + error="Could not generate valid cypher query" + ) + self.last_complete_response = response + return response qa_step = QAStep( chat_session=self.qa_chat_session, @@ -132,17 +250,18 @@ def send_message(self, message: str) -> dict: answer = qa_step.run(message, cypher, context) - self.last_complete_response = { - "question": message, - "response": answer, - "context": context, - "cypher": cypher - } + response = ChatResponse( + question=message, + context=context, + cypher=cypher, + answer=answer, + execution_time=execution_time + ) - return self.last_complete_response + self.last_complete_response = response + return response def send_message_stream(self, message: str) -> Iterator[str]: - """ Sends a message to the chat session and streams the response. @@ -153,17 +272,20 @@ def send_message_stream(self, message: str) -> Iterator[str]: str: Chunks of the response as they're generated. """ (context, cypher) = self._generate_cypher_query(message) + execution_time = self.metadata.get("last_query_execution_time") if not cypher or len(cypher) == 0: # Stream the error message for consistency with successful responses yield CYPHER_ERROR_RES - self.last_complete_response = { - "question": message, - "response": CYPHER_ERROR_RES, - "context": None, - "cypher": None - } + self.last_complete_response = ChatResponse( + question=message, + context=None, + cypher=None, + answer=CYPHER_ERROR_RES, + execution_time=execution_time, + error="Could not generate valid cypher query" + ) return qa_step = StreamingQAStep( @@ -176,144 +298,52 @@ def send_message_stream(self, message: str) -> Iterator[str]: yield chunk # Set the last answer using chat history to ensure we have the complete response - self.last_complete_response = { - "question": message, - "response": qa_step.chat_session.get_chat_history()[-1]['content'], - "context": context, - "cypher": cypher - } - -class CypherSession: - """ - Represents a Cypher-only session with a Knowledge Graph. - This class generates Cypher queries and extracts context without performing Q&A. - - Args: - model_config (KnowledgeGraphModelConfig): The model configuration to use. - ontology (Ontology): The ontology to use. - graph (Graph): The graph to query. - - Examples: - >>> from graphrag_sdk import KnowledgeGraph, Orchestrator - >>> from graphrag_sdk.ontology import Ontology - >>> from graphrag_sdk.model_config import KnowledgeGraphModelConfig - >>> model_config = KnowledgeGraphModelConfig.with_model(model) - >>> kg = KnowledgeGraph("test_kg", model_config, ontology) - >>> cypher_session = CypherSession(model_config, ontology, kg.graph, - ... cypher_system_instruction, cypher_gen_prompt, cypher_gen_prompt_history) - >>> result = cypher_session.search("What is the capital of France?") - """ - - def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, graph: Graph, - cypher_system_instruction: str, cypher_gen_prompt: str, cypher_gen_prompt_history: str): - """ - Initializes a new CypherSession object. - - Args: - model_config (KnowledgeGraphModelConfig): The model configuration. - ontology (Ontology): The ontology object. - graph (Graph): The graph object. - cypher_system_instruction (str): System instruction for cypher generation. - cypher_gen_prompt (str): Prompt template for cypher generation. - cypher_gen_prompt_history (str): Prompt template for cypher generation with history. - - Attributes: - model_config (KnowledgeGraphModelConfig): The model configuration. - ontology (Ontology): The ontology object. - graph (Graph): The graph object. - cypher_chat_session: The Cypher chat session object. - """ - self.model_config = model_config - self.graph = graph - self.ontology = ontology - - # Filter the ontology to remove unique and required attributes that are not needed for Q&A. - ontology_prompt = clean_ontology_for_prompt(ontology) - - cypher_system_instruction = cypher_system_instruction.format(ontology=ontology_prompt) - - self.cypher_prompt = cypher_gen_prompt - self.cypher_prompt_with_history = cypher_gen_prompt_history - - self.cypher_chat_session = model_config.cypher_generation.start_chat( - cypher_system_instruction - ) - - self.last_complete_response = { - "question": None, - "response": None, - "context": None, - "cypher": None - } - - # Metadata to store additional information about the chat session (currently only last query execution time) - self.metadata = {"last_query_execution_time": None} - - def _generate_cypher_query(self, message: str) -> tuple: - """ - Generate a Cypher query for the given message. + final_answer = qa_step.chat_session.get_chat_history()[-1]['content'] - Args: - message (str): The message to generate a query for. - - Returns: - tuple: A tuple containing (context, cypher) - """ - cypher_step = GraphQueryGenerationStep( - graph=self.graph, - chat_session=self.cypher_chat_session, - ontology=self.ontology, - last_answer=self.last_complete_response["response"], - cypher_prompt=self.cypher_prompt, - cypher_prompt_with_history=self.cypher_prompt_with_history + self.last_complete_response = ChatResponse( + question=message, + context=context, + cypher=cypher, + answer=final_answer, + execution_time=execution_time ) - (context, cypher, query_execution_time) = cypher_step.run(message) - self.metadata["last_query_execution_time"] = query_execution_time - - return (context, cypher) - - def search(self, message: str) -> dict: + def search(self, message: str) -> ChatResponse: """ Searches the knowledge graph by generating a Cypher query and extracting relevant context. + This method only performs cypher generation and context extraction without Q&A. Args: message (str): The search query or question. Returns: - dict: Search results containing: - - question: The original query - - context: Extracted context from the graph - - cypher: Generated Cypher query - - execution_time: Query execution time in seconds - - error: Error message if query generation failed (optional) + ChatResponse: Search results with cypher and context, but no answer. """ context, cypher = self._generate_cypher_query(message) execution_time = self.metadata.get("last_query_execution_time") - # Prepare base response - response = { - "question": message, - "context": context, - "cypher": cypher, - "execution_time": execution_time - } - # Handle query generation failure if not cypher: - response["error"] = "Could not generate valid cypher query" - context = cypher = None + response = ChatResponse( + question=message, + context=None, + cypher=None, + answer=None, + execution_time=execution_time, + error="Could not generate valid cypher query" + ) + else: + response = ChatResponse( + question=message, + context=context, + cypher=cypher, + answer=None, # No QA step for search + execution_time=execution_time + ) # Update session state - self.last_complete_response = { - "question": message, - "response": None, - "context": context, - "cypher": cypher - } - + self.last_complete_response = response return response - def clean_ontology_for_prompt(ontology: dict) -> str: """ diff --git a/graphrag_sdk/kg.py b/graphrag_sdk/kg.py index 5284a924..e816f1aa 100644 --- a/graphrag_sdk/kg.py +++ b/graphrag_sdk/kg.py @@ -4,7 +4,7 @@ from typing import Optional, Union from graphrag_sdk.ontology import Ontology from graphrag_sdk.source import AbstractSource -from graphrag_sdk.chat_session import ChatSession, CypherSession +from graphrag_sdk.chat_session import ChatSession from graphrag_sdk.attribute import AttributeType, Attribute from graphrag_sdk.helpers import map_dict_to_cypher_properties from graphrag_sdk.model_config import KnowledgeGraphModelConfig @@ -211,17 +211,6 @@ def chat_session(self) -> ChatSession: self.qa_system_instruction, self.cypher_gen_prompt, self.qa_prompt, self.cypher_gen_prompt_history) return chat_session - def cypher_session(self) -> CypherSession: - """ - Create a new Cypher session. - - Returns: - CypherSession: A new Cypher session instance. - """ - cypher_session = CypherSession(self._model_config, self.ontology, self.graph, self.cypher_system_instruction, - self.cypher_gen_prompt, self.cypher_gen_prompt_history) - return cypher_session - def add_node(self, entity: str, attributes: dict) -> None: """ From fa83b3b6068703ef9c2e17c95aa0b76b01abb1d7 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 13:27:18 +0300 Subject: [PATCH 3/9] refactor-changes --- graphrag_sdk/chat_session.py | 289 ++++++--------------------- graphrag_sdk/steps/qa_step.py | 24 ++- graphrag_sdk/steps/stream_qa_step.py | 51 ----- 3 files changed, 87 insertions(+), 277 deletions(-) delete mode 100644 graphrag_sdk/steps/stream_qa_step.py diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index 35e4df4a..322230c6 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -3,110 +3,11 @@ from typing import Iterator from graphrag_sdk.ontology import Ontology from graphrag_sdk.steps.qa_step import QAStep -from graphrag_sdk.steps.stream_qa_step import StreamingQAStep from graphrag_sdk.model_config import KnowledgeGraphModelConfig from graphrag_sdk.steps.graph_query_step import GraphQueryGenerationStep CYPHER_ERROR_RES = "Sorry, I could not find the answer to your question" -class ResponseDict(dict): - """ - A dictionary that also provides property access to response components. - Maintains backward compatibility while adding new property-based access. - """ - - def __init__(self, response: 'ChatResponse'): - self._response = response - super().__init__({ - "question": response.query, - "response": response.answer, - "context": response.context, - "cypher": response.cypher - }) - - @property - def query(self) -> str: - """The original question/query.""" - return self._response.query - - @property - def cypher(self) -> str: - """The generated Cypher query.""" - return self._response.cypher - - @property - def context(self) -> str: - """The extracted context from the graph.""" - return self._response.context - - @property - def answer(self) -> str: - """The final QA answer.""" - return self._response.answer - - @property - def execution_time(self) -> float: - """Query execution time in seconds.""" - return self._response.execution_time - - @property - def error(self) -> str: - """Error message if any step failed.""" - return self._response.error - -class ChatResponse: - """ - Represents a response from a chat session with access to different pipeline stages. - """ - - def __init__(self, question: str, context: str = None, cypher: str = None, - answer: str = None, execution_time: float = None, error: str = None): - self._question = question - self._context = context - self._cypher = cypher - self._answer = answer - self._execution_time = execution_time - self._error = error - - @property - def query(self) -> str: - """The original question/query.""" - return self._question - - @property - def cypher(self) -> str: - """The generated Cypher query.""" - return self._cypher - - @property - def context(self) -> str: - """The extracted context from the graph.""" - return self._context - - @property - def answer(self) -> str: - """The final QA answer.""" - return self._answer - - @property - def execution_time(self) -> float: - """Query execution time in seconds.""" - return self._execution_time - - @property - def error(self) -> str: - """Error message if any step failed.""" - return self._error - - def to_dict(self) -> dict: - """Convert response to dictionary format for backward compatibility.""" - return { - "question": self._question, - "response": self._answer, - "context": self._context, - "cypher": self._cypher - } - class ChatSession: """ Represents a chat session with a Knowledge Graph. @@ -123,15 +24,18 @@ class ChatSession: >>> model_config = KnowledgeGraphModelConfig.with_model(model) >>> kg = KnowledgeGraph("test_kg", model_config, ontology) >>> session = kg.start_chat() + >>> + >>> # Full QA pipeline >>> response = session.send_message("What is the capital of France?") - >>> # Backward compatible dict access: >>> print(response["question"]) # "What is the capital of France?" >>> print(response["response"]) # "Paris" - >>> # New property access: - >>> print(response.query) # "What is the capital of France?" - >>> print(response.cypher) # "MATCH (c:City)..." - >>> print(response.context) # Retrieved context - >>> print(response.answer) # "Paris" + >>> print(response["context"]) # Retrieved context + >>> print(response["cypher"]) # "MATCH (c:City)..." + >>> + >>> # Just generate Cypher query without QA + >>> context, cypher = session.generate_cypher_query("Who are the actors in The Matrix?") + >>> print(context) # Retrieved context + >>> print(cypher) # "MATCH (a:Actor)-[:ACTED_IN]->(m:Movie {title: 'The Matrix'}) RETURN a.name" """ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, graph: Graph, @@ -177,38 +81,50 @@ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, qa_system_instruction ) - self.last_complete_response = None + # Initialize steps once during construction + self.qa_step = QAStep( + chat_session=self.qa_chat_session, + qa_prompt=self.qa_prompt, + ) + self.cypher_step = GraphQueryGenerationStep( + graph=self.graph, + chat_session=self.cypher_chat_session, + ontology=self.ontology, + last_answer=None, # Will be updated dynamically + cypher_prompt=self.cypher_prompt, + cypher_prompt_with_history=self.cypher_prompt_with_history + ) + + self.last_answer = None - # Metadata to store additional information about the chat session (currently only last query execution time) - self.metadata = {"last_query_execution_time": None} + def _update_last_answer(self, answer: str): + """Update the last answer in both the session and cypher step.""" + self.last_answer = answer + self.cypher_step.last_answer = answer - def _generate_cypher_query(self, message: str) -> tuple: + def generate_cypher_query(self, message: str) -> tuple: """ - Generate a Cypher query for the given message. + Generate a Cypher query for the given message without running QA. + + This method allows users to get just the Cypher query and context + without executing the full question-answering pipeline. Args: message (str): The message to generate a query for. Returns: - tuple: A tuple containing (context, cypher) + tuple: A tuple containing (context, cypher) where: + - context (str): The extracted context from the graph + - cypher (str): The generated Cypher query """ - last_answer = self.last_complete_response.answer if self.last_complete_response else None - - cypher_step = GraphQueryGenerationStep( - graph=self.graph, - chat_session=self.cypher_chat_session, - ontology=self.ontology, - last_answer=last_answer, - cypher_prompt=self.cypher_prompt, - cypher_prompt_with_history=self.cypher_prompt_with_history - ) + # Update the last_answer for this query + self.cypher_step.last_answer = self.last_answer - (context, cypher, query_execution_time) = cypher_step.run(message) - self.metadata["last_query_execution_time"] = query_execution_time + (context, cypher, _) = self.cypher_step.run(message) return (context, cypher) - def send_message(self, message: str) -> ResponseDict: + def send_message(self, message: str) -> dict: """ Sends a message to the chat session. @@ -216,50 +132,32 @@ def send_message(self, message: str) -> ResponseDict: message (str): The message to send. Returns: - ResponseDict: A dict-like object with backward compatibility that also provides property access: - - dict access: result["question"], result["response"], result["context"], result["cypher"] - - property access: result.query, result.answer, result.context, result.cypher, result.execution_time + dict: A dictionary containing the response with keys: + - "question": The original question + - "response": The answer + - "context": The extracted context from the graph + - "cypher": The generated Cypher query """ - response = self._send_message_internal(message) - return ResponseDict(response) - - def _send_message_internal(self, message: str) -> ChatResponse: - """ - Internal method that returns the full ChatResponse object. - """ - (context, cypher) = self._generate_cypher_query(message) - execution_time = self.metadata.get("last_query_execution_time") + (context, cypher) = self.generate_cypher_query(message) # If the cypher is empty, return an error response if not cypher or len(cypher) == 0: - response = ChatResponse( - question=message, - context=None, - cypher=None, - answer=CYPHER_ERROR_RES, - execution_time=execution_time, - error="Could not generate valid cypher query" - ) - self.last_complete_response = response - return response + return { + "question": message, + "response": CYPHER_ERROR_RES, + "context": None, + "cypher": None + } - qa_step = QAStep( - chat_session=self.qa_chat_session, - qa_prompt=self.qa_prompt, - ) + answer = self.qa_step.run(message, cypher, context) + self._update_last_answer(answer) - answer = qa_step.run(message, cypher, context) - - response = ChatResponse( - question=message, - context=context, - cypher=cypher, - answer=answer, - execution_time=execution_time - ) - - self.last_complete_response = response - return response + return { + "question": message, + "response": answer, + "context": context, + "cypher": cypher + } def send_message_stream(self, message: str) -> Iterator[str]: """ @@ -271,79 +169,20 @@ def send_message_stream(self, message: str) -> Iterator[str]: Yields: str: Chunks of the response as they're generated. """ - (context, cypher) = self._generate_cypher_query(message) - execution_time = self.metadata.get("last_query_execution_time") + (context, cypher) = self.generate_cypher_query(message) if not cypher or len(cypher) == 0: # Stream the error message for consistency with successful responses yield CYPHER_ERROR_RES - - self.last_complete_response = ChatResponse( - question=message, - context=None, - cypher=None, - answer=CYPHER_ERROR_RES, - execution_time=execution_time, - error="Could not generate valid cypher query" - ) return - qa_step = StreamingQAStep( - chat_session=self.qa_chat_session, - qa_prompt=self.qa_prompt, - ) - # Yield chunks of the response as they're generated - for chunk in qa_step.run(message, cypher, context): + for chunk in self.qa_step.run_stream(message, cypher, context): yield chunk # Set the last answer using chat history to ensure we have the complete response - final_answer = qa_step.chat_session.get_chat_history()[-1]['content'] - - self.last_complete_response = ChatResponse( - question=message, - context=context, - cypher=cypher, - answer=final_answer, - execution_time=execution_time - ) - - def search(self, message: str) -> ChatResponse: - """ - Searches the knowledge graph by generating a Cypher query and extracting relevant context. - This method only performs cypher generation and context extraction without Q&A. - - Args: - message (str): The search query or question. - - Returns: - ChatResponse: Search results with cypher and context, but no answer. - """ - context, cypher = self._generate_cypher_query(message) - execution_time = self.metadata.get("last_query_execution_time") - - # Handle query generation failure - if not cypher: - response = ChatResponse( - question=message, - context=None, - cypher=None, - answer=None, - execution_time=execution_time, - error="Could not generate valid cypher query" - ) - else: - response = ChatResponse( - question=message, - context=context, - cypher=cypher, - answer=None, # No QA step for search - execution_time=execution_time - ) - - # Update session state - self.last_complete_response = response - return response + final_answer = self.qa_step.chat_session.get_chat_history()[-1]['content'] + self._update_last_answer(final_answer) def clean_ontology_for_prompt(ontology: dict) -> str: """ diff --git a/graphrag_sdk/steps/qa_step.py b/graphrag_sdk/steps/qa_step.py index bf25e5c8..f0f18a77 100644 --- a/graphrag_sdk/steps/qa_step.py +++ b/graphrag_sdk/steps/qa_step.py @@ -1,5 +1,5 @@ import logging -from typing import Optional +from typing import Optional, Iterator from graphrag_sdk.steps.Step import Step from graphrag_sdk.models import GenerativeModelChatSession @@ -50,3 +50,25 @@ def run(self, question: str, cypher: str, context: str) -> str: qa_response = self.chat_session.send_message(qa_prompt) return qa_response.text + + def run_stream(self, question: str, cypher: str, context: str) -> Iterator[str]: + """ + Run the QA step and stream the response chunks. + + Args: + question (str): The question being asked. + cypher (str): The Cypher query to run. + context (str): Context for the QA. + + Returns: + Iterator[str]: A generator that yields response chunks. + """ + qa_prompt = self.qa_prompt.format( + context=context, cypher=cypher, question=question + ) + + logger.debug(f"QA Prompt (Stream): {qa_prompt}") + + # Send the message and stream the response + for chunk in self.chat_session.send_message_stream(qa_prompt): + yield chunk diff --git a/graphrag_sdk/steps/stream_qa_step.py b/graphrag_sdk/steps/stream_qa_step.py deleted file mode 100644 index ab2d2bae..00000000 --- a/graphrag_sdk/steps/stream_qa_step.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -from typing import Optional, Iterator -from graphrag_sdk.steps.Step import Step -from graphrag_sdk.models import GenerativeModelChatSession - - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - -class StreamingQAStep(Step): - """ - QA Step that supports streaming responses - """ - - def __init__( - self, - chat_session: GenerativeModelChatSession, - config: Optional[dict] = None, - qa_prompt: Optional[str] = None, - ) -> None: - """ - Initialize the QA Step. - - Args: - chat_session (GenerativeModelChatSession): The chat session for handling the QA. - config (Optional[dict]): Optional configuration for the step. - qa_prompt (Optional[str]): The prompt template for question answering. - """ - self.config = config or {} - self.chat_session = chat_session - self.qa_prompt = qa_prompt - - def run(self, question: str, cypher: str, context: str) -> Iterator[str]: - """ - Run the QA step and stream the response chunks. - - Args: - question (str): The question being asked. - cypher (str): The Cypher query to run. - context (str): Context for the QA. - - Returns: - Iterator[str]: A generator that yields response chunks. - """ - qa_prompt = self.qa_prompt.format( - context=context, cypher=cypher, question=question - ) - logger.debug(f"QA Prompt: {qa_prompt}") - # Send the message and stream the response - for chunk in self.chat_session.send_message_stream(qa_prompt): - yield chunk \ No newline at end of file From 4eae718c545adb00453ced74b7a82309caa3b52c Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 15:20:30 +0300 Subject: [PATCH 4/9] add-agent --- examples/ufc/agent/falkor_agent.py | 195 +++++++ examples/ufc/demo-ufc.ipynb | 214 +++---- examples/ufc/ontology.json | 204 +++---- examples/ufc/pydantic-ai-agent-demo.ipynb | 663 ++++++++++++++++++++++ graphrag_sdk/chat_session.py | 38 +- 5 files changed, 1060 insertions(+), 254 deletions(-) create mode 100644 examples/ufc/agent/falkor_agent.py create mode 100644 examples/ufc/pydantic-ai-agent-demo.ipynb diff --git a/examples/ufc/agent/falkor_agent.py b/examples/ufc/agent/falkor_agent.py new file mode 100644 index 00000000..3f99d5dd --- /dev/null +++ b/examples/ufc/agent/falkor_agent.py @@ -0,0 +1,195 @@ +from __future__ import annotations +from typing import Dict, List, Optional +from dataclasses import dataclass +from pydantic import BaseModel, Field +from dotenv import load_dotenv +from rich.markdown import Markdown +from rich.console import Console +from rich.live import Live +import asyncio +import os + +from graphrag_sdk.models.litellm import LiteModel + +from pydantic_ai.providers.openai import OpenAIProvider +from pydantic_ai.models.openai import OpenAIModel +from pydantic_ai import Agent, RunContext +from graphrag_sdk import KnowledgeGraph +from graphrag_sdk.chat_session import CypherSession +from graphrag_sdk.model_config import KnowledgeGraphModelConfig + +load_dotenv() + +# ========== Define dependencies ========== +@dataclass +class FalkorDependencies: + """Dependencies for the Falkor agent.""" + cypher_session: CypherSession + +# ========== Helper function to get model configuration ========== +def get_model(): + """Configure and return the LLM model to use.""" + model_choice = os.getenv('MODEL_CHOICE', 'gpt-4.1-mini') + api_key = os.getenv('OPENAI_API_KEY', 'no-api-key-provided') + + return OpenAIModel(model_choice, provider=OpenAIProvider(api_key=api_key)) + +# ========== Create the Falkor agent ========== +falkor_agent = Agent( + get_model(), + system_prompt="""You are a knowledge graph assistant that helps users query a FalkorDB knowledge graph. + +When a user provides ANY input (questions, entity names, keywords, or statements), you MUST use the search_falkor tool with the EXACT, COMPLETE user input as the query parameter. Do not modify, shorten, or extract keywords from the user's input. + +The knowledge graph system is designed to handle: +- Full questions: "Who is Salsa Boy?" +- Entity names: "Salsa Boy" +- Keywords: "fighters", "matches", "UFC" +- Statements: "Show me information about recent fights" +- Any other text input + +The tool will return: +- A Cypher query that was generated to search the graph (automatically adapted to the input type) +- Context data extracted from the graph using that query +- Execution time information + +After receiving the results, explain what the Cypher query does and interpret the context data to provide a helpful answer. Focus on the entities, relationships, and graph patterns found in the results. If the input was just an entity name, provide comprehensive information about that entity and its connections.""", + deps_type=FalkorDependencies +) + +# ========== Define a result model for Falkor search ========== +class FalkorSearchResult(BaseModel): + """Model representing a search result from FalkorDB.""" + cypher: str = Field(description="The generated Cypher query") + context: str = Field(description="The extracted context from the knowledge graph") + execution_time: Optional[float] = Field(None, description="Query execution time in milliseconds") + +# ========== Falkor search tool ========== +@falkor_agent.tool +async def search_falkor(ctx: RunContext[FalkorDependencies], query: str) -> List[FalkorSearchResult]: + """Search the FalkorDB knowledge graph with the given query - returns only cypher and context. + + Args: + ctx: The run context containing dependencies + query: The search query to find information in the knowledge graph + + Returns: + A list of search results containing cypher queries and context that match the query + """ + # Access the KnowledgeGraph client from dependencies + cypher_session = ctx.deps.cypher_session + + try: + # Create a chat session and use the new method that only generates cypher and extracts context + result = cypher_session.search(query) + # print(result) + # Check if there was an error + if result.get('error'): + raise Exception(result['error']) + + # Format the result + formatted_result = FalkorSearchResult( + cypher=result.get('cypher', ''), + context=result.get('context', ''), + execution_time=result.get('execution_time') + ) + + return [formatted_result] + except Exception as e: + # Log the error + print(f"Error searching FalkorDB: {str(e)}") + raise + +# ========== Main execution function ========== +async def main(): + """Run the Falkor agent with user queries.""" + print("Falkor Agent - Powered by Pydantic AI, FalkorDB GraphRAG SDK") + print("Enter 'exit' to quit the program.") + + # FalkorDB connection parameters + falkor_host = os.environ.get('FALKORDB_HOST', '127.0.0.1') + falkor_port = int(os.environ.get('FALKORDB_PORT', '6379')) + falkor_username = os.environ.get('FALKORDB_USERNAME') + falkor_password = os.environ.get('FALKORDB_PASSWORD') + + model_falkor = LiteModel() + + + # Initialize model configuration + model_config = KnowledgeGraphModelConfig.with_model(model_falkor) + + # Connect to FalkorDB to load existing ontology + from falkordb import FalkorDB + from graphrag_sdk.ontology import Ontology + + db = FalkorDB(host=falkor_host, port=falkor_port, username=falkor_username, password=falkor_password) + graph = db.select_graph("ufc") + + # Load ontology from existing graph + try: + ontology = Ontology.from_kg_graph(graph) + print("Loaded ontology from existing knowledge graph.") + except Exception as e: + print(f"Could not load ontology from existing graph: {str(e)}") + print("Using empty ontology...") + ontology = None + + # Initialize KnowledgeGraph with custom cypher instructions + kg_client = KnowledgeGraph( + name="ufc", + model_config=model_config, + ontology=ontology, + host=falkor_host, + port=falkor_port, + username=falkor_username, + password=falkor_password) + + cypher_session = kg_client.cypher_session() + # print(cypher_session.search("Who is Salsa Boy?")) + + + console = Console() + messages = [] + + try: + while True: + # Get user input + user_input = input("\n[You] ") + + # Check if user wants to exit + if user_input.lower() in ['exit', 'quit', 'bye', 'goodbye']: + print("Goodbye!") + break + + try: + # Process the user input and output the response + print("\n[Assistant]") + with Live('', console=console, vertical_overflow='visible') as live: + # Pass the KnowledgeGraph client as a dependency + deps = FalkorDependencies(cypher_session=cypher_session) + + async with falkor_agent.run_stream( + user_input, message_history=messages, deps=deps + ) as result: + curr_message = "" + async for message in result.stream_text(delta=True): + curr_message += message + live.update(Markdown(curr_message)) + + # Add the new messages to the chat history + messages.extend(result.all_messages()) + + except Exception as e: + print(f"\n[Error] An error occurred: {str(e)}") + finally: + # Close any connections if needed + print("\nFalkor connection closed.") + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\nProgram terminated by user.") + except Exception as e: + print(f"\nUnexpected error: {str(e)}") + raise diff --git a/examples/ufc/demo-ufc.ipynb b/examples/ufc/demo-ufc.ipynb index 2fa95529..13e68dde 100644 --- a/examples/ufc/demo-ufc.ipynb +++ b/examples/ufc/demo-ufc.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -94,7 +94,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Process Documents: 100%|██████████| 2/2 [00:35<00:00, 17.79s/it]\n" + "Process Documents: 100%|██████████| 2/2 [00:28<00:00, 14.03s/it]\n" ] } ], @@ -177,16 +177,10 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"title_bout\",\n", - " \"type\": \"boolean\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", " \"name\": \"weight_class\",\n", " \"type\": \"string\",\n", " \"unique\": false,\n", - " \"required\": false\n", + " \"required\": true\n", " },\n", " {\n", " \"name\": \"method\",\n", @@ -195,7 +189,7 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"round\",\n", + " \"name\": \"rounds\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", @@ -207,22 +201,16 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"time_format\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": false\n", - " },\n", - " {\n", - " \"name\": \"details\",\n", + " \"name\": \"referee\",\n", " \"type\": \"string\",\n", " \"unique\": false,\n", - " \"required\": false\n", + " \"required\": true\n", " }\n", " ],\n", " \"description\": \"\"\n", " },\n", " {\n", - " \"label\": \"Person\",\n", + " \"label\": \"Fighter\",\n", " \"attributes\": [\n", " {\n", " \"name\": \"name\",\n", @@ -240,27 +228,27 @@ " \"description\": \"\"\n", " },\n", " {\n", - " \"label\": \"Referee\",\n", + " \"label\": \"FightStatistics\",\n", " \"attributes\": [\n", " {\n", - " \"name\": \"name\",\n", + " \"name\": \"fight_id\",\n", " \"type\": \"string\",\n", " \"unique\": true,\n", " \"required\": true\n", - " }\n", - " ],\n", - " \"description\": \"\"\n", - " },\n", - " {\n", - " \"label\": \"FightStatistics\",\n", - " \"attributes\": [\n", + " },\n", " {\n", - " \"name\": \"statistics_id\",\n", + " \"name\": \"fighter_name\",\n", " \"type\": \"string\",\n", " \"unique\": true,\n", " \"required\": true\n", " },\n", " {\n", + " \"name\": \"result\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", " \"name\": \"knockdowns\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", @@ -339,7 +327,13 @@ " \"label\": \"RoundStatistics\",\n", " \"attributes\": [\n", " {\n", - " \"name\": \"round_statistics_id\",\n", + " \"name\": \"fight_id\",\n", + " \"type\": \"string\",\n", + " \"unique\": true,\n", + " \"required\": true\n", + " },\n", + " {\n", + " \"name\": \"fighter_name\",\n", " \"type\": \"string\",\n", " \"unique\": true,\n", " \"required\": true\n", @@ -347,7 +341,7 @@ " {\n", " \"name\": \"round_number\",\n", " \"type\": \"number\",\n", - " \"unique\": false,\n", + " \"unique\": true,\n", " \"required\": true\n", " },\n", " {\n", @@ -375,67 +369,91 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"takedown_percentage\",\n", + " \"name\": \"head_strikes\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"submissions_attempted\",\n", + " \"name\": \"head_strikes_attempted\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"reversals\",\n", + " \"name\": \"body_strikes\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", - " }\n", - " ],\n", - " \"description\": \"\"\n", - " },\n", - " {\n", - " \"label\": \"SignificantStrikeBreakdown\",\n", - " \"attributes\": [\n", + " },\n", " {\n", - " \"name\": \"breakdown_id\",\n", - " \"type\": \"string\",\n", - " \"unique\": true,\n", + " \"name\": \"body_strikes_attempted\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", + " \"name\": \"leg_strikes\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", + " \"name\": \"leg_strikes_attempted\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"head\",\n", + " \"name\": \"distance_strikes\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"body\",\n", + " \"name\": \"distance_strikes_attempted\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"leg\",\n", + " \"name\": \"clinch_strikes\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"distance\",\n", + " \"name\": \"clinch_strikes_attempted\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"clinch\",\n", + " \"name\": \"ground_strikes\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"ground\",\n", + " \"name\": \"ground_strikes_attempted\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", + " \"name\": \"takedown_percentage\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", + " \"name\": \"submissions_attempted\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", + " \"name\": \"reversals\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", @@ -446,7 +464,7 @@ " ],\n", " \"relations\": [\n", " {\n", - " \"label\": \"HOSTED\",\n", + " \"label\": \"HAS_FIGHT\",\n", " \"source\": {\n", " \"label\": \"Event\"\n", " },\n", @@ -455,17 +473,17 @@ " },\n", " \"attributes\": [\n", " {\n", - " \"name\": \"main_event\",\n", - " \"type\": \"boolean\",\n", + " \"name\": \"order\",\n", + " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": false\n", " }\n", " ]\n", " },\n", " {\n", - " \"label\": \"PARTICIPATED\",\n", + " \"label\": \"PARTICIPATED_IN\",\n", " \"source\": {\n", - " \"label\": \"Person\"\n", + " \"label\": \"Fighter\"\n", " },\n", " \"target\": {\n", " \"label\": \"Fight\"\n", @@ -480,23 +498,6 @@ " ]\n", " },\n", " {\n", - " \"label\": \"OFFICIATED\",\n", - " \"source\": {\n", - " \"label\": \"Referee\"\n", - " },\n", - " \"target\": {\n", - " \"label\": \"Fight\"\n", - " },\n", - " \"attributes\": [\n", - " {\n", - " \"name\": \"role\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " }\n", - " ]\n", - " },\n", - " {\n", " \"label\": \"HAS_STATISTICS\",\n", " \"source\": {\n", " \"label\": \"Fight\"\n", @@ -504,14 +505,7 @@ " \"target\": {\n", " \"label\": \"FightStatistics\"\n", " },\n", - " \"attributes\": [\n", - " {\n", - " \"name\": \"fighter_name\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " }\n", - " ]\n", + " \"attributes\": []\n", " },\n", " {\n", " \"label\": \"HAS_ROUND_STATISTICS\",\n", @@ -521,65 +515,37 @@ " \"target\": {\n", " \"label\": \"RoundStatistics\"\n", " },\n", - " \"attributes\": [\n", - " {\n", - " \"name\": \"fighter_name\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " }\n", - " ]\n", + " \"attributes\": []\n", " },\n", " {\n", - " \"label\": \"HAS_BREAKDOWN\",\n", + " \"label\": \"HAS_FIGHTER_STATISTICS\",\n", " \"source\": {\n", - " \"label\": \"FightStatistics\"\n", + " \"label\": \"Fighter\"\n", " },\n", " \"target\": {\n", - " \"label\": \"SignificantStrikeBreakdown\"\n", + " \"label\": \"FightStatistics\"\n", " },\n", - " \"attributes\": [\n", - " {\n", - " \"name\": \"round_number\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", - " \"required\": false\n", - " }\n", - " ]\n", + " \"attributes\": []\n", " },\n", " {\n", - " \"label\": \"HAS_ROUND_BREAKDOWN\",\n", + " \"label\": \"HAS_FIGHTER_ROUND_STATISTICS\",\n", " \"source\": {\n", - " \"label\": \"RoundStatistics\"\n", + " \"label\": \"Fighter\"\n", " },\n", " \"target\": {\n", - " \"label\": \"SignificantStrikeBreakdown\"\n", + " \"label\": \"RoundStatistics\"\n", " },\n", - " \"attributes\": [\n", - " {\n", - " \"name\": \"breakdown_id\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " }\n", - " ]\n", + " \"attributes\": []\n", " },\n", " {\n", - " \"label\": \"PARTICIPATED_AS_REFEREE\",\n", + " \"label\": \"HAS_FIGHTER\",\n", " \"source\": {\n", - " \"label\": \"Referee\"\n", + " \"label\": \"FightStatistics\"\n", " },\n", " \"target\": {\n", - " \"label\": \"Event\"\n", + " \"label\": \"Fighter\"\n", " },\n", - " \"attributes\": [\n", - " {\n", - " \"name\": \"role\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": false\n", - " }\n", - " ]\n", + " \"attributes\": []\n", " }\n", " ]\n", "}\n" @@ -608,7 +574,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Process Documents: 100%|██████████| 8/8 [01:18<00:00, 9.77s/it]\n" + "Process Documents: 100%|██████████| 8/8 [01:02<00:00, 7.77s/it]\n" ] } ], @@ -644,8 +610,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'question': 'Who is Salsa Boy?', 'response': 'Salsa Boy is Waldo Cortes-Acosta.', 'context': '[[\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\']]', 'cypher': \"\\nMATCH (p:Person)\\nWHERE p.nickname CONTAINS 'Salsa Boy'\\nRETURN p\\n\"}\n", - "{'question': 'Tell me about one of his fights?', 'response': 'Waldo Cortes-Acosta, also known as Salsa Boy, fought Lukasz Brzeski at UFC Fight Night: Holloway vs. The Korean Zombie on August 26, 2023. He won the fight by KO/TKO with punches to the head at distance in the first round at 3:01. The bout was in the Heavyweight division and was scheduled for 3 rounds.', 'context': '[[\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Win\"}]->()\\', \\'(:Fight{details:\"Punches to Head At Distance\",fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",round:1,time:\"3:01\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\'], [\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Win\"}]->()\\', \\'(:Fight{details:\"Decision\",fight_id:\"Arlovski_vs_Cortes-Acosta_UFCFN_2024-01-13\",method:\"Decision - Unanimous\",round:3,time:\"5:00\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\'], [\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Win\"}]->()\\', \\'(:Fight{details:\"Decision\",fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",round:3,time:\"5:00\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\'], [\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Loss\"}]->()\\', \\'(:Fight{details:\"Decision\",fight_id:\"MarcosRogerioDeLima_vs_WaldoCortesAcosta_UFCFightNightSongVsSimon_2023-04-29\",method:\"Decision - Unanimous\",round:3,time:\"5:00\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\']]', 'cypher': \"\\nMATCH (p:Person)-[pa:PARTICIPATED]->(f:Fight)\\nWHERE p.name CONTAINS 'Waldo Cortes-Acosta'\\nRETURN p, pa, f\\n\"}\n" + "{'question': 'Who is Salsa Boy?', 'response': 'Salsa Boy is Waldo Cortes-Acosta.', 'context': '[[\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:9,knockdowns:0,leg_strikes:1,leg_strikes_attempted:1,reversals:0,round_number:1,significant_strikes:6,significant_strikes_attempted:10,significant_strikes_percentage:60.0,submissions_attempted:1,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:13,distance_strikes_attempted:27,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:22,head_strikes_attempted:51,knockdowns:0,leg_strikes:3,leg_strikes_attempted:3,reversals:0,round_number:2,significant_strikes:25,significant_strikes_attempted:54,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:50.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:11,distance_strikes_attempted:17,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:17,head_strikes_attempted:32,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:21,significant_strikes_attempted:37,significant_strikes_percentage:56.0,submissions_attempted:0,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:5,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:17,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:24,knockdowns:0,leg_strikes:15,leg_strikes_attempted:16,reversals:0,round_number:1,significant_strikes:20,significant_strikes_attempted:39,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:2,clinch_strikes_attempted:2,distance_strikes:19,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:1,head_strikes:13,head_strikes_attempted:30,knockdowns:0,leg_strikes:2,leg_strikes_attempted:21,reversals:0,round_number:2,significant_strikes:19,significant_strikes_attempted:37,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:43,distance_strikes_attempted:74,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:34,head_strikes_attempted:62,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:3,significant_strikes:43,significant_strikes_attempted:74,significant_strikes_percentage:58,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:48,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:8,head_strikes_attempted:36,knockdowns:0,leg_strikes:3,leg_strikes_attempted:5,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:48,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:14,distance_strikes_attempted:44,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:29,knockdowns:0,leg_strikes:6,leg_strikes_attempted:8,reversals:0,round_number:2,significant_strikes:14,significant_strikes_attempted:44,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:6,clinch_strikes:3,clinch_strikes_attempted:5,distance_strikes:17,distance_strikes_attempted:39,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:10,head_strikes_attempted:32,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:20,significant_strikes_attempted:44,significant_strikes_percentage:45,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:5,clinch_strikes:2,clinch_strikes_attempted:4,distance_strikes:10,distance_strikes_attempted:12,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:9,head_strikes_attempted:36,knockdowns:1,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33,submissions_attempted:0,takedown_percentage:0})\\']]', 'cypher': \"\\nMATCH (f:Fighter)\\nWHERE toLower(f.nickname) = 'salsa boy'\\nOPTIONAL MATCH (f)-[r]-(connected)\\nRETURN f, r, connected\\n\"}\n", + "{'question': 'Tell me about one of his fights?', 'response': 'On May 11, 2024, at UFC Fight Night: Lewis vs. Nascimento in St. Louis, Missouri, Waldo Cortes-Acosta fought Robelis Despaigne in a heavyweight bout. Waldo Cortes-Acosta won the fight by unanimous decision after three rounds. He landed 52 significant strikes out of 101 attempts (51.49%), attempted 1 submission, and completed 3 takedowns out of 4 attempts (75% takedown accuracy). The fight went the full distance, with each round lasting 5 minutes.', 'context': '[[\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:9,knockdowns:0,leg_strikes:1,leg_strikes_attempted:1,reversals:0,round_number:1,significant_strikes:6,significant_strikes_attempted:10,significant_strikes_percentage:60.0,submissions_attempted:1,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:7,knockdowns:0,leg_strikes:0,leg_strikes_attempted:0,reversals:0,round_number:1,significant_strikes:2,significant_strikes_attempted:7,significant_strikes_percentage:28.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:13,distance_strikes_attempted:27,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:22,head_strikes_attempted:51,knockdowns:0,leg_strikes:3,leg_strikes_attempted:3,reversals:0,round_number:2,significant_strikes:25,significant_strikes_attempted:54,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:50.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:3,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:8,distance_strikes_attempted:12,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:21,knockdowns:0,leg_strikes:4,leg_strikes_attempted:4,reversals:0,round_number:2,significant_strikes:13,significant_strikes_attempted:28,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:11,distance_strikes_attempted:17,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:17,head_strikes_attempted:32,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:21,significant_strikes_attempted:37,significant_strikes_percentage:56.0,submissions_attempted:0,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:5,distance_strikes_attempted:8,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:8,knockdowns:0,leg_strikes:5,leg_strikes_attempted:8,reversals:0,round_number:3,significant_strikes:11,significant_strikes_attempted:17,significant_strikes_percentage:64.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:9,knockdowns:0,leg_strikes:1,leg_strikes_attempted:1,reversals:0,round_number:1,significant_strikes:6,significant_strikes_attempted:10,significant_strikes_percentage:60.0,submissions_attempted:1,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:7,knockdowns:0,leg_strikes:0,leg_strikes_attempted:0,reversals:0,round_number:1,significant_strikes:2,significant_strikes_attempted:7,significant_strikes_percentage:28.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:13,distance_strikes_attempted:27,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:22,head_strikes_attempted:51,knockdowns:0,leg_strikes:3,leg_strikes_attempted:3,reversals:0,round_number:2,significant_strikes:25,significant_strikes_attempted:54,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:50.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:3,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:8,distance_strikes_attempted:12,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:21,knockdowns:0,leg_strikes:4,leg_strikes_attempted:4,reversals:0,round_number:2,significant_strikes:13,significant_strikes_attempted:28,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:11,distance_strikes_attempted:17,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:17,head_strikes_attempted:32,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:21,significant_strikes_attempted:37,significant_strikes_percentage:56.0,submissions_attempted:0,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:5,distance_strikes_attempted:8,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:8,knockdowns:0,leg_strikes:5,leg_strikes_attempted:8,reversals:0,round_number:3,significant_strikes:11,significant_strikes_attempted:17,significant_strikes_percentage:64.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:7,clinch_strikes:1,clinch_strikes_attempted:2,distance_strikes:26,distance_strikes_attempted:32,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:11,knockdowns:0,leg_strikes:10,leg_strikes_attempted:11,reversals:0,round_number:1,significant_strikes:27,significant_strikes_attempted:34,significant_strikes_percentage:79,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:5,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:17,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:24,knockdowns:0,leg_strikes:15,leg_strikes_attempted:16,reversals:0,round_number:1,significant_strikes:20,significant_strikes_attempted:39,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:2,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:13,distance_strikes_attempted:17,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:10,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:2,significant_strikes:15,significant_strikes_attempted:19,significant_strikes_percentage:78,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:2,clinch_strikes_attempted:2,distance_strikes:19,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:1,head_strikes:13,head_strikes_attempted:30,knockdowns:0,leg_strikes:2,leg_strikes_attempted:21,reversals:0,round_number:2,significant_strikes:19,significant_strikes_attempted:37,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:17,distance_strikes_attempted:23,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:10,knockdowns:0,leg_strikes:5,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:17,significant_strikes_attempted:23,significant_strikes_percentage:73,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:43,distance_strikes_attempted:74,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:34,head_strikes_attempted:62,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:3,significant_strikes:43,significant_strikes_attempted:74,significant_strikes_percentage:58,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:7,clinch_strikes:1,clinch_strikes_attempted:2,distance_strikes:26,distance_strikes_attempted:32,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:11,knockdowns:0,leg_strikes:10,leg_strikes_attempted:11,reversals:0,round_number:1,significant_strikes:27,significant_strikes_attempted:34,significant_strikes_percentage:79,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:5,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:17,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:24,knockdowns:0,leg_strikes:15,leg_strikes_attempted:16,reversals:0,round_number:1,significant_strikes:20,significant_strikes_attempted:39,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:2,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:13,distance_strikes_attempted:17,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:10,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:2,significant_strikes:15,significant_strikes_attempted:19,significant_strikes_percentage:78,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:2,clinch_strikes_attempted:2,distance_strikes:19,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:1,head_strikes:13,head_strikes_attempted:30,knockdowns:0,leg_strikes:2,leg_strikes_attempted:21,reversals:0,round_number:2,significant_strikes:19,significant_strikes_attempted:37,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:17,distance_strikes_attempted:23,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:10,knockdowns:0,leg_strikes:5,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:17,significant_strikes_attempted:23,significant_strikes_percentage:73,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:43,distance_strikes_attempted:74,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:34,head_strikes_attempted:62,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:3,significant_strikes:43,significant_strikes_attempted:74,significant_strikes_percentage:58,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:12,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:40,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:21,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:40,significant_strikes_percentage:37,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:48,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:8,head_strikes_attempted:36,knockdowns:0,leg_strikes:3,leg_strikes_attempted:5,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:48,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:8,body_strikes_attempted:13,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:20,distance_strikes_attempted:41,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:20,knockdowns:0,leg_strikes:6,leg_strikes_attempted:9,reversals:0,round_number:2,significant_strikes:20,significant_strikes_attempted:41,significant_strikes_percentage:48,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:14,distance_strikes_attempted:44,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:29,knockdowns:0,leg_strikes:6,leg_strikes_attempted:8,reversals:0,round_number:2,significant_strikes:14,significant_strikes_attempted:44,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:9,body_strikes_attempted:15,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:23,distance_strikes_attempted:46,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:22,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:23,significant_strikes_attempted:46,significant_strikes_percentage:50,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:6,clinch_strikes:3,clinch_strikes_attempted:5,distance_strikes:17,distance_strikes_attempted:39,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:10,head_strikes_attempted:32,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:20,significant_strikes_attempted:44,significant_strikes_percentage:45,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:12,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:40,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:21,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:40,significant_strikes_percentage:37,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:48,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:8,head_strikes_attempted:36,knockdowns:0,leg_strikes:3,leg_strikes_attempted:5,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:48,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:8,body_strikes_attempted:13,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:20,distance_strikes_attempted:41,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:20,knockdowns:0,leg_strikes:6,leg_strikes_attempted:9,reversals:0,round_number:2,significant_strikes:20,significant_strikes_attempted:41,significant_strikes_percentage:48,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:14,distance_strikes_attempted:44,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:29,knockdowns:0,leg_strikes:6,leg_strikes_attempted:8,reversals:0,round_number:2,significant_strikes:14,significant_strikes_attempted:44,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:9,body_strikes_attempted:15,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:23,distance_strikes_attempted:46,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:22,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:23,significant_strikes_attempted:46,significant_strikes_percentage:50,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:6,clinch_strikes:3,clinch_strikes_attempted:5,distance_strikes:17,distance_strikes_attempted:39,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:10,head_strikes_attempted:32,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:20,significant_strikes_attempted:44,significant_strikes_percentage:45,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:5,clinch_strikes:2,clinch_strikes_attempted:4,distance_strikes:10,distance_strikes_attempted:12,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:9,head_strikes_attempted:36,knockdowns:1,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:3,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:12,distance_strikes_attempted:42,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:1,head_strikes_attempted:7,knockdowns:0,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63.64,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:15,total_strikes_attempted:23})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:5,clinch_strikes:2,clinch_strikes_attempted:4,distance_strikes:10,distance_strikes_attempted:12,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:9,head_strikes_attempted:36,knockdowns:1,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63.64,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:15,total_strikes_attempted:23})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:3,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:12,distance_strikes_attempted:42,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:1,head_strikes_attempted:7,knockdowns:0,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63,submissions_attempted:0,takedown_percentage:0})\\']]', 'cypher': \"\\nMATCH (f:Fighter)\\nWHERE toLower(f.name) = 'waldo cortes-acosta'\\nMATCH (f)-[p:PARTICIPATED_IN]->(fi:Fight)\\nOPTIONAL MATCH (fi)<-[hf:HAS_FIGHT]-(e:Event)\\nOPTIONAL MATCH (fi)-[hs:HAS_STATISTICS]->(fs:FightStatistics)\\nOPTIONAL MATCH (fi)-[hrs:HAS_ROUND_STATISTICS]->(rs:RoundStatistics)\\nRETURN f, p, fi, hf, e, hs, fs, hrs, rs\\n\"}\n" ] } ], diff --git a/examples/ufc/ontology.json b/examples/ufc/ontology.json index d0ee2570..83fc392b 100644 --- a/examples/ufc/ontology.json +++ b/examples/ufc/ontology.json @@ -33,17 +33,11 @@ "unique": true, "required": true }, - { - "name": "title_bout", - "type": "boolean", - "unique": false, - "required": true - }, { "name": "weight_class", "type": "string", "unique": false, - "required": false + "required": true }, { "name": "method", @@ -52,7 +46,7 @@ "required": true }, { - "name": "round", + "name": "rounds", "type": "number", "unique": false, "required": true @@ -64,22 +58,16 @@ "required": true }, { - "name": "time_format", - "type": "string", - "unique": false, - "required": false - }, - { - "name": "details", + "name": "referee", "type": "string", "unique": false, - "required": false + "required": true } ], "description": "" }, { - "label": "Person", + "label": "Fighter", "attributes": [ { "name": "name", @@ -97,26 +85,26 @@ "description": "" }, { - "label": "Referee", + "label": "FightStatistics", "attributes": [ { - "name": "name", + "name": "fight_id", "type": "string", "unique": true, "required": true - } - ], - "description": "" - }, - { - "label": "FightStatistics", - "attributes": [ + }, { - "name": "statistics_id", + "name": "fighter_name", "type": "string", "unique": true, "required": true }, + { + "name": "result", + "type": "string", + "unique": false, + "required": true + }, { "name": "knockdowns", "type": "number", @@ -196,7 +184,13 @@ "label": "RoundStatistics", "attributes": [ { - "name": "round_statistics_id", + "name": "fight_id", + "type": "string", + "unique": true, + "required": true + }, + { + "name": "fighter_name", "type": "string", "unique": true, "required": true @@ -204,7 +198,7 @@ { "name": "round_number", "type": "number", - "unique": false, + "unique": true, "required": true }, { @@ -232,67 +226,91 @@ "required": true }, { - "name": "takedown_percentage", + "name": "head_strikes", "type": "number", "unique": false, "required": true }, { - "name": "submissions_attempted", + "name": "head_strikes_attempted", "type": "number", "unique": false, "required": true }, { - "name": "reversals", + "name": "body_strikes", "type": "number", "unique": false, "required": true - } - ], - "description": "" - }, - { - "label": "SignificantStrikeBreakdown", - "attributes": [ + }, { - "name": "breakdown_id", - "type": "string", - "unique": true, + "name": "body_strikes_attempted", + "type": "number", + "unique": false, "required": true }, { - "name": "head", + "name": "leg_strikes", "type": "number", "unique": false, "required": true }, { - "name": "body", + "name": "leg_strikes_attempted", "type": "number", "unique": false, "required": true }, { - "name": "leg", + "name": "distance_strikes", "type": "number", "unique": false, "required": true }, { - "name": "distance", + "name": "distance_strikes_attempted", "type": "number", "unique": false, "required": true }, { - "name": "clinch", + "name": "clinch_strikes", "type": "number", "unique": false, "required": true }, { - "name": "ground", + "name": "clinch_strikes_attempted", + "type": "number", + "unique": false, + "required": true + }, + { + "name": "ground_strikes", + "type": "number", + "unique": false, + "required": true + }, + { + "name": "ground_strikes_attempted", + "type": "number", + "unique": false, + "required": true + }, + { + "name": "takedown_percentage", + "type": "number", + "unique": false, + "required": true + }, + { + "name": "submissions_attempted", + "type": "number", + "unique": false, + "required": true + }, + { + "name": "reversals", "type": "number", "unique": false, "required": true @@ -303,7 +321,7 @@ ], "relations": [ { - "label": "HOSTED", + "label": "HAS_FIGHT", "source": { "label": "Event" }, @@ -312,17 +330,17 @@ }, "attributes": [ { - "name": "main_event", - "type": "boolean", + "name": "order", + "type": "number", "unique": false, "required": false } ] }, { - "label": "PARTICIPATED", + "label": "PARTICIPATED_IN", "source": { - "label": "Person" + "label": "Fighter" }, "target": { "label": "Fight" @@ -336,23 +354,6 @@ } ] }, - { - "label": "OFFICIATED", - "source": { - "label": "Referee" - }, - "target": { - "label": "Fight" - }, - "attributes": [ - { - "name": "role", - "type": "string", - "unique": false, - "required": true - } - ] - }, { "label": "HAS_STATISTICS", "source": { @@ -361,14 +362,7 @@ "target": { "label": "FightStatistics" }, - "attributes": [ - { - "name": "fighter_name", - "type": "string", - "unique": false, - "required": true - } - ] + "attributes": [] }, { "label": "HAS_ROUND_STATISTICS", @@ -378,65 +372,37 @@ "target": { "label": "RoundStatistics" }, - "attributes": [ - { - "name": "fighter_name", - "type": "string", - "unique": false, - "required": true - } - ] + "attributes": [] }, { - "label": "HAS_BREAKDOWN", + "label": "HAS_FIGHTER_STATISTICS", "source": { - "label": "FightStatistics" + "label": "Fighter" }, "target": { - "label": "SignificantStrikeBreakdown" + "label": "FightStatistics" }, - "attributes": [ - { - "name": "round_number", - "type": "number", - "unique": false, - "required": false - } - ] + "attributes": [] }, { - "label": "HAS_ROUND_BREAKDOWN", + "label": "HAS_FIGHTER_ROUND_STATISTICS", "source": { - "label": "RoundStatistics" + "label": "Fighter" }, "target": { - "label": "SignificantStrikeBreakdown" + "label": "RoundStatistics" }, - "attributes": [ - { - "name": "breakdown_id", - "type": "string", - "unique": false, - "required": true - } - ] + "attributes": [] }, { - "label": "PARTICIPATED_AS_REFEREE", + "label": "HAS_FIGHTER", "source": { - "label": "Referee" + "label": "FightStatistics" }, "target": { - "label": "Event" + "label": "Fighter" }, - "attributes": [ - { - "name": "role", - "type": "string", - "unique": false, - "required": false - } - ] + "attributes": [] } ] } \ No newline at end of file diff --git a/examples/ufc/pydantic-ai-agent-demo.ipynb b/examples/ufc/pydantic-ai-agent-demo.ipynb new file mode 100644 index 00000000..6a4d6c15 --- /dev/null +++ b/examples/ufc/pydantic-ai-agent-demo.ipynb @@ -0,0 +1,663 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f6c5bc1a", + "metadata": {}, + "source": [ + "# 🥋 FalkorDB Agent with Pydantic AI - UFC Knowledge Graph Demo\n", + "\n", + "This notebook demonstrates how to create an intelligent **UFC-focused AI agent** using **FalkorDB** and **Pydantic AI** that can query a comprehensive UFC knowledge graph powered by the **GraphRAG SDK**.\n", + "\n", + "## What You'll Learn\n", + "\n", + "- How to set up a Pydantic AI agent with custom tools\n", + "- How to integrate FalkorDB with the GraphRAG SDK\n", + "- How to create a knowledge graph search tool for your agent\n", + "- How to build an interactive chat interface with streaming responses\n", + "- How to handle dependencies and error management in agent workflows\n", + "\n", + "## Prerequisites\n", + "\n", + "Before running this notebook, ensure you have:\n", + "\n", + "- A running FalkorDB instance with UFC data loaded (see [demo-ufc.ipynb](./demo-ufc.ipynb))\n", + "- OpenAI API key for the language model\n", + "- Python environment with required packages (installed below)" + ] + }, + { + "cell_type": "markdown", + "id": "72eeeaf6", + "metadata": {}, + "source": [ + "## 🔧 Installation Requirements\n", + "\n", + "This notebook requires three main packages. Install them with the cell below:\n", + "\n", + "### Required Packages:\n", + "- **GraphRAG SDK**: Provides FalkorDB integration, Rich console formatting, OpenAI client, and graph operations\n", + "- **Pydantic AI**: Modern AI agent framework with type safety and structured data handling \n", + "- **Gradio**: Web interface framework for creating interactive chat applications\n", + "\n", + "\n", + "**Note**: The GraphRAG SDK includes most dependencies we need, so the installation is streamlined!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "77ff4ed8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", + "deepeval 2.6.7 requires anthropic<0.50.0,>=0.49.0, but you have anthropic 0.55.0 which is incompatible.\n", + "google-genai 1.22.0 requires httpx<1.0.0,>=0.28.1, but you have httpx 0.27.2 which is incompatible.\n", + "mistralai 1.8.2 requires httpx>=0.28.1, but you have httpx 0.27.2 which is incompatible.\u001b[0m\u001b[31m\n", + "\u001b[0m\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", + "deepeval 2.6.7 requires anthropic<0.50.0,>=0.49.0, but you have anthropic 0.55.0 which is incompatible.\n", + "ollama 0.2.1 requires httpx<0.28.0,>=0.27.0, but you have httpx 0.28.1 which is incompatible.\u001b[0m\u001b[31m\n", + "\u001b[0m\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", + "deepeval 2.6.7 requires anthropic<0.50.0,>=0.49.0, but you have anthropic 0.55.0 which is incompatible.\n", + "ollama 0.2.1 requires httpx<0.28.0,>=0.27.0, but you have httpx 0.28.1 which is incompatible.\u001b[0m\u001b[31m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "!pip install graphrag_sdk --quiet\n", + "!pip install pydantic-ai --quiet\n", + "!pip install gradio --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "c56eedd2", + "metadata": {}, + "source": [ + "## 1. Import Required Libraries\n", + "\n", + "First, let's import all the necessary libraries for our FalkorDB and Pydantic AI agent." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f77206f2", + "metadata": {}, + "outputs": [], + "source": [ + "import gradio as gr\n", + "from typing import List\n", + "from falkordb import FalkorDB\n", + "from dataclasses import dataclass\n", + "from __future__ import annotations\n", + "from pydantic import BaseModel, Field\n", + "from graphrag_sdk import KnowledgeGraph\n", + "from pydantic_ai import Agent, RunContext\n", + "from graphrag_sdk.ontology import Ontology\n", + "from graphrag_sdk.models.litellm import LiteModel\n", + "from graphrag_sdk.chat_session import ChatSession\n", + "from pydantic_ai.models.openai import OpenAIModel\n", + "from pydantic_ai.providers.openai import OpenAIProvider\n", + "from graphrag_sdk.model_config import KnowledgeGraphModelConfig" + ] + }, + { + "cell_type": "markdown", + "id": "557fbb3e", + "metadata": {}, + "source": [ + "## 2. Load Environment Variables and Configuration\n", + "\n", + "Load environment variables for API keys and database connection parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b453b7de", + "metadata": {}, + "outputs": [], + "source": [ + "# Configuration parameters\n", + "MODEL_CHOICE = 'gpt-4o-mini'\n", + "OPENAI_API_KEY = \"your-openai-api-key-here\" # Replace with your actual API key or use os.getenv\n", + "\n", + "# FalkorDB connection parameters\n", + "FALKORDB_HOST = \"localhost\" # Replace with your FalkorDB host\n", + "FALKORDB_PORT = 6379 # Default port for FalkorDB\n", + "FALKORDB_USERNAME = \"your-falkordb-username\" # can be None if not required\n", + "FALKORDB_PASSWORD = \"your-falkordb-password\" # can be None if not required\n", + "\n", + "GRAPH_NAME = \"ufc\"" + ] + }, + { + "cell_type": "markdown", + "id": "3e73cf2d", + "metadata": {}, + "source": [ + "## 3. Set Up the LLM Model\n", + "\n", + "Configure the OpenAI model that will power our Pydantic AI agent." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4658d5db", + "metadata": {}, + "outputs": [], + "source": [ + "def get_model():\n", + " \"\"\"Configure and return the LLM model to use.\"\"\"\n", + " return OpenAIModel(MODEL_CHOICE, provider=OpenAIProvider(api_key=OPENAI_API_KEY))" + ] + }, + { + "cell_type": "markdown", + "id": "8f4c9697", + "metadata": {}, + "source": [ + "## 4. Connect to FalkorDB and Load Ontology\n", + "\n", + "Establish connection to FalkorDB and load the existing UFC knowledge graph ontology." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9fbb3930", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔌 Connecting to FalkorDB...\n", + "✅ Loaded ontology from existing knowledge graph.\n", + " Entities: 5\n", + " Relations: 7\n" + ] + } + ], + "source": [ + "# Connect to FalkorDB\n", + "print(\"🔌 Connecting to FalkorDB...\")\n", + "db = FalkorDB(host=FALKORDB_HOST, port=FALKORDB_PORT, username=FALKORDB_USERNAME, password=FALKORDB_PASSWORD)\n", + "graph = db.select_graph(GRAPH_NAME)\n", + "\n", + "# Load ontology from existing graph\n", + "try:\n", + " ontology = Ontology.from_kg_graph(graph)\n", + " print(\"✅ Loaded ontology from existing knowledge graph.\")\n", + " print(f\" Entities: {len(ontology.entities) if ontology.entities else 0}\")\n", + " print(f\" Relations: {len(ontology.relations) if ontology.relations else 0}\")\n", + "except Exception as e:\n", + " print(f\"⚠️ Could not load ontology from existing graph: {str(e)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "a33122bd", + "metadata": {}, + "source": [ + "## 5. Initialize KnowledgeGraph and ChatSession\n", + "\n", + "Set up the GraphRAG SDK components for knowledge graph interaction." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ff61d0da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔗 Initializing KnowledgeGraph client...\n", + "✅ ChatSession created successfully!\n" + ] + } + ], + "source": [ + "# Initialize LiteModel for GraphRAG SDK (by default - gpt-4.1)\n", + "model_falkor = LiteModel()\n", + "\n", + "# Initialize model configuration\n", + "model_config = KnowledgeGraphModelConfig.with_model(model_falkor)\n", + "\n", + "# Initialize KnowledgeGraph\n", + "print(\"🔗 Initializing KnowledgeGraph client...\")\n", + "kg_client = KnowledgeGraph(\n", + " name=GRAPH_NAME,\n", + " model_config=model_config,\n", + " ontology=ontology,\n", + " host=FALKORDB_HOST,\n", + " port=FALKORDB_PORT,\n", + " username=FALKORDB_USERNAME,\n", + " password=FALKORDB_PASSWORD\n", + ")\n", + "\n", + "# Create a chat session for agent queries (replaces cypher_session)\n", + "chat_session = kg_client.chat_session()\n", + "print(\"✅ ChatSession created successfully!\")" + ] + }, + { + "cell_type": "markdown", + "id": "582a0874", + "metadata": {}, + "source": [ + "## 6. Define Agent Dependencies\n", + "\n", + "Create a dependencies class to pass the ChatSession to our agent." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "12c49613", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "📦 Agent dependencies defined!\n" + ] + } + ], + "source": [ + "@dataclass\n", + "class FalkorDependencies:\n", + " \"\"\"Dependencies for the Falkor agent.\"\"\"\n", + " chat_session: ChatSession\n", + "\n", + "print(\"📦 Agent dependencies defined!\")" + ] + }, + { + "cell_type": "markdown", + "id": "4cb562e0", + "metadata": {}, + "source": [ + "## 7. Create the Pydantic AI Agent\n", + "\n", + "Define our main agent with a comprehensive system prompt for UFC knowledge graph querying." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "11e090fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🤖 Falkor Agent created successfully!\n" + ] + } + ], + "source": [ + "# Create the Falkor agent\n", + "falkor_agent = Agent(\n", + " get_model(),\n", + " system_prompt=\"\"\"You are a knowledge graph assistant that helps users query a FalkorDB knowledge graph.\n", + "\n", + "When a user provides ANY input (questions, entity names, keywords, or statements), you MUST use the search_falkor tool with the EXACT, COMPLETE user input as the query parameter. Do not modify, shorten, or extract keywords from the user's input.\n", + "\n", + "The knowledge graph system is designed to handle:\n", + "- Full questions: \"Who is Salsa Boy?\"\n", + "- Entity names: \"Salsa Boy\"\n", + "- Keywords: \"fighters\", \"matches\", \"UFC\"\n", + "- Statements: \"Show me information about recent fights\"\n", + "- Any other text input\n", + "\n", + "The tool will return:\n", + "- A Cypher query that was generated to search the graph (automatically adapted to the input type)\n", + "- Context data extracted from the graph using that query\n", + "\n", + "After receiving the results, explain what the Cypher query does and interpret the context data to provide a helpful answer. Focus on the entities, relationships, and graph patterns found in the results. If the input was just an entity name, provide comprehensive information about that entity and its connections.\n", + "Try to be concise but thorough in your explanations, ensuring the user understands the graph data and its implications.\"\"\",\n", + " deps_type=FalkorDependencies\n", + ")\n", + "\n", + "print(\"🤖 Falkor Agent created successfully!\")" + ] + }, + { + "cell_type": "markdown", + "id": "ececa87d", + "metadata": {}, + "source": [ + "## 8. Define the Search Result Model\n", + "\n", + "Create a Pydantic model to structure the results from our FalkorDB search tool.\n", + "\n", + "This model ensures type safety and clear data structure for the agent's search results." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f2125ee9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "📊 Search result model defined!\n" + ] + } + ], + "source": [ + "class FalkorSearchResult(BaseModel):\n", + " \"\"\"Model representing a search result from FalkorDB.\"\"\"\n", + " cypher: str = Field(description=\"The generated Cypher query\")\n", + " context: str = Field(description=\"The extracted context from the knowledge graph\")\n", + "\n", + "print(\"📊 Search result model defined!\")" + ] + }, + { + "cell_type": "markdown", + "id": "c62de651", + "metadata": {}, + "source": [ + "## 9. Register the Search Tool\n", + "\n", + "Create and register the main tool that allows our agent to search the FalkorDB knowledge graph." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "04f08b47", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔍 Search tool registered with agent!\n" + ] + } + ], + "source": [ + "@falkor_agent.tool\n", + "async def search_falkor(ctx: RunContext[FalkorDependencies], query: str) -> List[FalkorSearchResult]:\n", + " \"\"\"Search the FalkorDB knowledge graph with the given query - returns only cypher and context.\n", + " \n", + " Args:\n", + " ctx: The run context containing dependencies\n", + " query: The search query to find information in the knowledge graph\n", + " \n", + " Returns:\n", + " A list of search results containing cypher queries and context that match the query\n", + " \"\"\"\n", + " # Access the ChatSession from dependencies\n", + " chat_session = ctx.deps.chat_session\n", + " \n", + " try:\n", + " # Use the chat session's generate_cypher_query method to get cypher and context\n", + " # This method returns (context, cypher) tuple\n", + " context, cypher = chat_session.generate_cypher_query(query)\n", + " \n", + " # Format the result to match the expected interface\n", + " formatted_result = FalkorSearchResult(\n", + " cypher=cypher or '',\n", + " context=context or ''\n", + " )\n", + " \n", + " return [formatted_result]\n", + " except Exception as e:\n", + " # Log the error\n", + " print(f\"Error searching FalkorDB: {str(e)}\")\n", + " raise\n", + "\n", + "print(\"🔍 Search tool registered with agent!\")" + ] + }, + { + "cell_type": "markdown", + "id": "24961420", + "metadata": {}, + "source": [ + "## 10. Gradio Chat Function\n", + "\n", + "Create the Gradio integration function for web-based chat interface." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "727dda29", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🌐 Gradio chat function defined!\n" + ] + } + ], + "source": [ + "async def gradio_chat_function(message, history):\n", + " \"\"\"\n", + " Gradio chat function that processes user messages and returns agent responses.\n", + " \n", + " Args:\n", + " message (str): The user's message\n", + " history (List): Chat history from Gradio\n", + " \n", + " Returns:\n", + " str: The agent's response\n", + " \"\"\"\n", + " deps = FalkorDependencies(chat_session=chat_session)\n", + " \n", + " try:\n", + " # Convert Gradio history to our message format\n", + " message_history = []\n", + " for user_msg, assistant_msg in history:\n", + " if user_msg:\n", + " message_history.append({\"role\": \"user\", \"content\": user_msg})\n", + " if assistant_msg:\n", + " message_history.append({\"role\": \"assistant\", \"content\": assistant_msg})\n", + " \n", + " # Get response from agent\n", + " result = await falkor_agent.run(\n", + " message,\n", + " deps=deps\n", + " )\n", + " \n", + " return result.data\n", + " \n", + " except Exception as e:\n", + " return f\"❌ Error: {str(e)}\"\n", + "\n", + "print(\"🌐 Gradio chat function defined!\")" + ] + }, + { + "cell_type": "markdown", + "id": "b99435d7", + "metadata": {}, + "source": [ + "## 11. Create Gradio Web Interface\n", + "\n", + "Build an elegant web-based chat interface using Gradio for easy interaction with the UFC knowledge graph agent." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "f4873577", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🌐 Gradio interface created!\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_210415/1172376482.py:23: UserWarning: You have not specified a value for the `type` parameter. Defaulting to the 'tuples' format for chatbot messages, but this is deprecated and will be removed in a future version of Gradio. Please set type='messages' instead, which uses openai-style dictionaries with 'role' and 'content' keys.\n", + " chatbot = gr.Chatbot(height=400, show_copy_button=True)\n" + ] + } + ], + "source": [ + "def create_gradio_interface():\n", + " \"\"\"Create a clean, elegant Gradio chat interface.\"\"\"\n", + " \n", + " def chat(message, history):\n", + " \"\"\"Handle chat interactions.\"\"\"\n", + " if not message.strip():\n", + " return history, \"\"\n", + " \n", + " try:\n", + " import asyncio\n", + " response = asyncio.run(gradio_chat_function(message, history))\n", + " history.append((message, response))\n", + " return history, \"\"\n", + " except Exception as e:\n", + " history.append((message, f\"❌ Error: {str(e)}\"))\n", + " return history, \"\"\n", + " \n", + " # Create interface with minimal, clean design\n", + " with gr.Blocks(title=\"🥋 UFC Knowledge Graph Agent\", theme=gr.themes.Soft()) as interface:\n", + " \n", + " gr.Markdown(\"# 🥋 UFC Knowledge Graph Agent\\n### Ask anything about UFC fighters, fights, and events!\")\n", + " \n", + " chatbot = gr.Chatbot(height=400, show_copy_button=True)\n", + " \n", + " with gr.Row():\n", + " msg = gr.Textbox(placeholder=\"Ask me about UFC...\", scale=4, container=False)\n", + " submit = gr.Button(\"🚀 Ask\", variant=\"primary\")\n", + " clear = gr.Button(\"🗑️\", variant=\"secondary\")\n", + " \n", + " # Example questions\n", + " gr.Examples([\n", + " \"Who is Salsa Boy?\",\n", + " \"Show me recent UFC fights\",\n", + " \"Which fighters have the most wins?\",\n", + " \"What are the UFC weight classes?\"\n", + " ], msg)\n", + " \n", + " # Event handling\n", + " submit.click(chat, [msg, chatbot], [chatbot, msg])\n", + " msg.submit(chat, [msg, chatbot], [chatbot, msg])\n", + " clear.click(lambda: [], outputs=chatbot)\n", + " \n", + " return interface\n", + "\n", + "# Create and launch interface\n", + "gradio_interface = create_gradio_interface()\n", + "print(\"🌐 Gradio interface created!\")" + ] + }, + { + "cell_type": "markdown", + "id": "aca6e293", + "metadata": {}, + "source": [ + "## 12. Launch the Interface\n", + "\n", + "Launch the Gradio web interface and start chatting with the UFC knowledge graph agent!" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "ef8586ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🚀 Launching UFC Knowledge Graph Agent...\n", + "* Running on local URL: http://127.0.0.1:7860\n", + "* Running on local URL: http://127.0.0.1:7860\n", + "* Running on public URL: https://79db0b9f73212d5729.gradio.live\n", + "\n", + "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n", + "* Running on public URL: https://79db0b9f73212d5729.gradio.live\n", + "\n", + "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n" + ] + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_210415/2419845010.py:29: DeprecationWarning: `result.data` is deprecated, use `result.output` instead.\n", + " return result.data\n" + ] + } + ], + "source": [ + "# Launch the interface\n", + "print(\"🚀 Launching UFC Knowledge Graph Agent...\")\n", + "gradio_interface.launch(share=True, inbrowser=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "sdk", + "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.10.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index 322230c6..85ad613d 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -95,12 +95,17 @@ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, cypher_prompt_with_history=self.cypher_prompt_with_history ) - self.last_answer = None + self.last_complete_response = { + "question": None, + "response": None, + "context": None, + "cypher": None + } - def _update_last_answer(self, answer: str): - """Update the last answer in both the session and cypher step.""" - self.last_answer = answer - self.cypher_step.last_answer = answer + def _update_last_complete_response(self, response_dict: dict): + """Update the last complete response in both the session and cypher step.""" + self.last_complete_response = response_dict + self.cypher_step.last_answer = response_dict.get("response") def generate_cypher_query(self, message: str) -> tuple: """ @@ -118,7 +123,7 @@ def generate_cypher_query(self, message: str) -> tuple: - cypher (str): The generated Cypher query """ # Update the last_answer for this query - self.cypher_step.last_answer = self.last_answer + self.cypher_step.last_answer = self.last_complete_response.get("response") (context, cypher, _) = self.cypher_step.run(message) @@ -150,14 +155,17 @@ def send_message(self, message: str) -> dict: } answer = self.qa_step.run(message, cypher, context) - self._update_last_answer(answer) - - return { + + response = { "question": message, "response": answer, "context": context, "cypher": cypher } + + self._update_last_complete_response(response) + + return response def send_message_stream(self, message: str) -> Iterator[str]: """ @@ -180,9 +188,17 @@ def send_message_stream(self, message: str) -> Iterator[str]: for chunk in self.qa_step.run_stream(message, cypher, context): yield chunk - # Set the last answer using chat history to ensure we have the complete response + # Set the last answer using chat history to ensure complete response final_answer = self.qa_step.chat_session.get_chat_history()[-1]['content'] - self._update_last_answer(final_answer) + + final_response = { + "question": message, + "response": final_answer, + "context": context, + "cypher": cypher + } + + self._update_last_complete_response(final_response) def clean_ontology_for_prompt(ontology: dict) -> str: """ From 51b3bb82a8561048d440a1a89cec32d81bd7e94c Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 15:40:23 +0300 Subject: [PATCH 5/9] rm-unrelevant --- .gitignore | 3 +- examples/ufc/agent/falkor_agent.py | 195 ----------------------------- graphrag_sdk/kg.py | 1 - tests/test_streaming_response.py | 1 - 4 files changed, 1 insertion(+), 199 deletions(-) delete mode 100644 examples/ufc/agent/falkor_agent.py diff --git a/.gitignore b/.gitignore index 729fc84c..6ff98ff6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ output logs falkordb-data weaviate-data -.deepeval_telemtry.txt -examples/ufc/ufc_agent.py +.deepeval_telemtry.txt \ No newline at end of file diff --git a/examples/ufc/agent/falkor_agent.py b/examples/ufc/agent/falkor_agent.py deleted file mode 100644 index 3f99d5dd..00000000 --- a/examples/ufc/agent/falkor_agent.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import annotations -from typing import Dict, List, Optional -from dataclasses import dataclass -from pydantic import BaseModel, Field -from dotenv import load_dotenv -from rich.markdown import Markdown -from rich.console import Console -from rich.live import Live -import asyncio -import os - -from graphrag_sdk.models.litellm import LiteModel - -from pydantic_ai.providers.openai import OpenAIProvider -from pydantic_ai.models.openai import OpenAIModel -from pydantic_ai import Agent, RunContext -from graphrag_sdk import KnowledgeGraph -from graphrag_sdk.chat_session import CypherSession -from graphrag_sdk.model_config import KnowledgeGraphModelConfig - -load_dotenv() - -# ========== Define dependencies ========== -@dataclass -class FalkorDependencies: - """Dependencies for the Falkor agent.""" - cypher_session: CypherSession - -# ========== Helper function to get model configuration ========== -def get_model(): - """Configure and return the LLM model to use.""" - model_choice = os.getenv('MODEL_CHOICE', 'gpt-4.1-mini') - api_key = os.getenv('OPENAI_API_KEY', 'no-api-key-provided') - - return OpenAIModel(model_choice, provider=OpenAIProvider(api_key=api_key)) - -# ========== Create the Falkor agent ========== -falkor_agent = Agent( - get_model(), - system_prompt="""You are a knowledge graph assistant that helps users query a FalkorDB knowledge graph. - -When a user provides ANY input (questions, entity names, keywords, or statements), you MUST use the search_falkor tool with the EXACT, COMPLETE user input as the query parameter. Do not modify, shorten, or extract keywords from the user's input. - -The knowledge graph system is designed to handle: -- Full questions: "Who is Salsa Boy?" -- Entity names: "Salsa Boy" -- Keywords: "fighters", "matches", "UFC" -- Statements: "Show me information about recent fights" -- Any other text input - -The tool will return: -- A Cypher query that was generated to search the graph (automatically adapted to the input type) -- Context data extracted from the graph using that query -- Execution time information - -After receiving the results, explain what the Cypher query does and interpret the context data to provide a helpful answer. Focus on the entities, relationships, and graph patterns found in the results. If the input was just an entity name, provide comprehensive information about that entity and its connections.""", - deps_type=FalkorDependencies -) - -# ========== Define a result model for Falkor search ========== -class FalkorSearchResult(BaseModel): - """Model representing a search result from FalkorDB.""" - cypher: str = Field(description="The generated Cypher query") - context: str = Field(description="The extracted context from the knowledge graph") - execution_time: Optional[float] = Field(None, description="Query execution time in milliseconds") - -# ========== Falkor search tool ========== -@falkor_agent.tool -async def search_falkor(ctx: RunContext[FalkorDependencies], query: str) -> List[FalkorSearchResult]: - """Search the FalkorDB knowledge graph with the given query - returns only cypher and context. - - Args: - ctx: The run context containing dependencies - query: The search query to find information in the knowledge graph - - Returns: - A list of search results containing cypher queries and context that match the query - """ - # Access the KnowledgeGraph client from dependencies - cypher_session = ctx.deps.cypher_session - - try: - # Create a chat session and use the new method that only generates cypher and extracts context - result = cypher_session.search(query) - # print(result) - # Check if there was an error - if result.get('error'): - raise Exception(result['error']) - - # Format the result - formatted_result = FalkorSearchResult( - cypher=result.get('cypher', ''), - context=result.get('context', ''), - execution_time=result.get('execution_time') - ) - - return [formatted_result] - except Exception as e: - # Log the error - print(f"Error searching FalkorDB: {str(e)}") - raise - -# ========== Main execution function ========== -async def main(): - """Run the Falkor agent with user queries.""" - print("Falkor Agent - Powered by Pydantic AI, FalkorDB GraphRAG SDK") - print("Enter 'exit' to quit the program.") - - # FalkorDB connection parameters - falkor_host = os.environ.get('FALKORDB_HOST', '127.0.0.1') - falkor_port = int(os.environ.get('FALKORDB_PORT', '6379')) - falkor_username = os.environ.get('FALKORDB_USERNAME') - falkor_password = os.environ.get('FALKORDB_PASSWORD') - - model_falkor = LiteModel() - - - # Initialize model configuration - model_config = KnowledgeGraphModelConfig.with_model(model_falkor) - - # Connect to FalkorDB to load existing ontology - from falkordb import FalkorDB - from graphrag_sdk.ontology import Ontology - - db = FalkorDB(host=falkor_host, port=falkor_port, username=falkor_username, password=falkor_password) - graph = db.select_graph("ufc") - - # Load ontology from existing graph - try: - ontology = Ontology.from_kg_graph(graph) - print("Loaded ontology from existing knowledge graph.") - except Exception as e: - print(f"Could not load ontology from existing graph: {str(e)}") - print("Using empty ontology...") - ontology = None - - # Initialize KnowledgeGraph with custom cypher instructions - kg_client = KnowledgeGraph( - name="ufc", - model_config=model_config, - ontology=ontology, - host=falkor_host, - port=falkor_port, - username=falkor_username, - password=falkor_password) - - cypher_session = kg_client.cypher_session() - # print(cypher_session.search("Who is Salsa Boy?")) - - - console = Console() - messages = [] - - try: - while True: - # Get user input - user_input = input("\n[You] ") - - # Check if user wants to exit - if user_input.lower() in ['exit', 'quit', 'bye', 'goodbye']: - print("Goodbye!") - break - - try: - # Process the user input and output the response - print("\n[Assistant]") - with Live('', console=console, vertical_overflow='visible') as live: - # Pass the KnowledgeGraph client as a dependency - deps = FalkorDependencies(cypher_session=cypher_session) - - async with falkor_agent.run_stream( - user_input, message_history=messages, deps=deps - ) as result: - curr_message = "" - async for message in result.stream_text(delta=True): - curr_message += message - live.update(Markdown(curr_message)) - - # Add the new messages to the chat history - messages.extend(result.all_messages()) - - except Exception as e: - print(f"\n[Error] An error occurred: {str(e)}") - finally: - # Close any connections if needed - print("\nFalkor connection closed.") - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("\nProgram terminated by user.") - except Exception as e: - print(f"\nUnexpected error: {str(e)}") - raise diff --git a/graphrag_sdk/kg.py b/graphrag_sdk/kg.py index e816f1aa..8f53dfef 100644 --- a/graphrag_sdk/kg.py +++ b/graphrag_sdk/kg.py @@ -211,7 +211,6 @@ def chat_session(self) -> ChatSession: self.qa_system_instruction, self.cypher_gen_prompt, self.qa_prompt, self.cypher_gen_prompt_history) return chat_session - def add_node(self, entity: str, attributes: dict) -> None: """ Add a node to the knowledge graph, checking if it matches the ontology diff --git a/tests/test_streaming_response.py b/tests/test_streaming_response.py index f6b2b9b4..6afdd4a4 100644 --- a/tests/test_streaming_response.py +++ b/tests/test_streaming_response.py @@ -161,7 +161,6 @@ def test_streaming(self, knowledge_graph_setup, delete_kg): answer = ''.join(received_chunks) assert answer.strip() == response_dict["response"].strip(), "Combined chunks (using join) should match last complete response" - assert chat.metadata.get("last_query_execution_time") is not None, "Expected last query execution time to be set" # Create a test case for evaluation test_case = LLMTestCase( From 30837b97a67b159d6c55610ad27673ef2e8405a9 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 15:53:27 +0300 Subject: [PATCH 6/9] fix-un-changes --- .gitignore | 2 +- examples/ufc/demo-ufc.ipynb | 214 ++++++++++++++++++------------- examples/ufc/ontology.json | 204 +++++++++++++++++------------ graphrag_sdk/chat_session.py | 15 ++- graphrag_sdk/kg.py | 1 - tests/test_streaming_response.py | 1 + 6 files changed, 258 insertions(+), 179 deletions(-) diff --git a/.gitignore b/.gitignore index 6ff98ff6..c3418c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ output logs falkordb-data weaviate-data -.deepeval_telemtry.txt \ No newline at end of file +.deepeval_telemtry.txt diff --git a/examples/ufc/demo-ufc.ipynb b/examples/ufc/demo-ufc.ipynb index 13e68dde..2fa95529 100644 --- a/examples/ufc/demo-ufc.ipynb +++ b/examples/ufc/demo-ufc.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -94,7 +94,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Process Documents: 100%|██████████| 2/2 [00:28<00:00, 14.03s/it]\n" + "Process Documents: 100%|██████████| 2/2 [00:35<00:00, 17.79s/it]\n" ] } ], @@ -177,10 +177,16 @@ " \"required\": true\n", " },\n", " {\n", + " \"name\": \"title_bout\",\n", + " \"type\": \"boolean\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " },\n", + " {\n", " \"name\": \"weight_class\",\n", " \"type\": \"string\",\n", " \"unique\": false,\n", - " \"required\": true\n", + " \"required\": false\n", " },\n", " {\n", " \"name\": \"method\",\n", @@ -189,7 +195,7 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"rounds\",\n", + " \"name\": \"round\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", @@ -201,16 +207,22 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"referee\",\n", + " \"name\": \"time_format\",\n", " \"type\": \"string\",\n", " \"unique\": false,\n", - " \"required\": true\n", + " \"required\": false\n", + " },\n", + " {\n", + " \"name\": \"details\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": false\n", " }\n", " ],\n", " \"description\": \"\"\n", " },\n", " {\n", - " \"label\": \"Fighter\",\n", + " \"label\": \"Person\",\n", " \"attributes\": [\n", " {\n", " \"name\": \"name\",\n", @@ -228,27 +240,27 @@ " \"description\": \"\"\n", " },\n", " {\n", - " \"label\": \"FightStatistics\",\n", + " \"label\": \"Referee\",\n", " \"attributes\": [\n", " {\n", - " \"name\": \"fight_id\",\n", + " \"name\": \"name\",\n", " \"type\": \"string\",\n", " \"unique\": true,\n", " \"required\": true\n", - " },\n", + " }\n", + " ],\n", + " \"description\": \"\"\n", + " },\n", + " {\n", + " \"label\": \"FightStatistics\",\n", + " \"attributes\": [\n", " {\n", - " \"name\": \"fighter_name\",\n", + " \"name\": \"statistics_id\",\n", " \"type\": \"string\",\n", " \"unique\": true,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"result\",\n", - " \"type\": \"string\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", " \"name\": \"knockdowns\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", @@ -327,13 +339,7 @@ " \"label\": \"RoundStatistics\",\n", " \"attributes\": [\n", " {\n", - " \"name\": \"fight_id\",\n", - " \"type\": \"string\",\n", - " \"unique\": true,\n", - " \"required\": true\n", - " },\n", - " {\n", - " \"name\": \"fighter_name\",\n", + " \"name\": \"round_statistics_id\",\n", " \"type\": \"string\",\n", " \"unique\": true,\n", " \"required\": true\n", @@ -341,7 +347,7 @@ " {\n", " \"name\": \"round_number\",\n", " \"type\": \"number\",\n", - " \"unique\": true,\n", + " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", @@ -369,91 +375,67 @@ " \"required\": true\n", " },\n", " {\n", - " \"name\": \"head_strikes\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", - " \"name\": \"head_strikes_attempted\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", - " \"name\": \"body_strikes\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", - " \"name\": \"body_strikes_attempted\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", - " \"name\": \"leg_strikes\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", - " \"required\": true\n", - " },\n", - " {\n", - " \"name\": \"leg_strikes_attempted\",\n", + " \"name\": \"takedown_percentage\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"distance_strikes\",\n", + " \"name\": \"submissions_attempted\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"distance_strikes_attempted\",\n", + " \"name\": \"reversals\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", - " },\n", + " }\n", + " ],\n", + " \"description\": \"\"\n", + " },\n", + " {\n", + " \"label\": \"SignificantStrikeBreakdown\",\n", + " \"attributes\": [\n", " {\n", - " \"name\": \"clinch_strikes\",\n", - " \"type\": \"number\",\n", - " \"unique\": false,\n", + " \"name\": \"breakdown_id\",\n", + " \"type\": \"string\",\n", + " \"unique\": true,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"clinch_strikes_attempted\",\n", + " \"name\": \"head\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"ground_strikes\",\n", + " \"name\": \"body\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"ground_strikes_attempted\",\n", + " \"name\": \"leg\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"takedown_percentage\",\n", + " \"name\": \"distance\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"submissions_attempted\",\n", + " \"name\": \"clinch\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", " },\n", " {\n", - " \"name\": \"reversals\",\n", + " \"name\": \"ground\",\n", " \"type\": \"number\",\n", " \"unique\": false,\n", " \"required\": true\n", @@ -464,7 +446,7 @@ " ],\n", " \"relations\": [\n", " {\n", - " \"label\": \"HAS_FIGHT\",\n", + " \"label\": \"HOSTED\",\n", " \"source\": {\n", " \"label\": \"Event\"\n", " },\n", @@ -473,17 +455,17 @@ " },\n", " \"attributes\": [\n", " {\n", - " \"name\": \"order\",\n", - " \"type\": \"number\",\n", + " \"name\": \"main_event\",\n", + " \"type\": \"boolean\",\n", " \"unique\": false,\n", " \"required\": false\n", " }\n", " ]\n", " },\n", " {\n", - " \"label\": \"PARTICIPATED_IN\",\n", + " \"label\": \"PARTICIPATED\",\n", " \"source\": {\n", - " \"label\": \"Fighter\"\n", + " \"label\": \"Person\"\n", " },\n", " \"target\": {\n", " \"label\": \"Fight\"\n", @@ -498,6 +480,23 @@ " ]\n", " },\n", " {\n", + " \"label\": \"OFFICIATED\",\n", + " \"source\": {\n", + " \"label\": \"Referee\"\n", + " },\n", + " \"target\": {\n", + " \"label\": \"Fight\"\n", + " },\n", + " \"attributes\": [\n", + " {\n", + " \"name\": \"role\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " }\n", + " ]\n", + " },\n", + " {\n", " \"label\": \"HAS_STATISTICS\",\n", " \"source\": {\n", " \"label\": \"Fight\"\n", @@ -505,7 +504,14 @@ " \"target\": {\n", " \"label\": \"FightStatistics\"\n", " },\n", - " \"attributes\": []\n", + " \"attributes\": [\n", + " {\n", + " \"name\": \"fighter_name\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " }\n", + " ]\n", " },\n", " {\n", " \"label\": \"HAS_ROUND_STATISTICS\",\n", @@ -515,37 +521,65 @@ " \"target\": {\n", " \"label\": \"RoundStatistics\"\n", " },\n", - " \"attributes\": []\n", + " \"attributes\": [\n", + " {\n", + " \"name\": \"fighter_name\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " }\n", + " ]\n", " },\n", " {\n", - " \"label\": \"HAS_FIGHTER_STATISTICS\",\n", + " \"label\": \"HAS_BREAKDOWN\",\n", " \"source\": {\n", - " \"label\": \"Fighter\"\n", + " \"label\": \"FightStatistics\"\n", " },\n", " \"target\": {\n", - " \"label\": \"FightStatistics\"\n", + " \"label\": \"SignificantStrikeBreakdown\"\n", " },\n", - " \"attributes\": []\n", + " \"attributes\": [\n", + " {\n", + " \"name\": \"round_number\",\n", + " \"type\": \"number\",\n", + " \"unique\": false,\n", + " \"required\": false\n", + " }\n", + " ]\n", " },\n", " {\n", - " \"label\": \"HAS_FIGHTER_ROUND_STATISTICS\",\n", + " \"label\": \"HAS_ROUND_BREAKDOWN\",\n", " \"source\": {\n", - " \"label\": \"Fighter\"\n", + " \"label\": \"RoundStatistics\"\n", " },\n", " \"target\": {\n", - " \"label\": \"RoundStatistics\"\n", + " \"label\": \"SignificantStrikeBreakdown\"\n", " },\n", - " \"attributes\": []\n", + " \"attributes\": [\n", + " {\n", + " \"name\": \"breakdown_id\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": true\n", + " }\n", + " ]\n", " },\n", " {\n", - " \"label\": \"HAS_FIGHTER\",\n", + " \"label\": \"PARTICIPATED_AS_REFEREE\",\n", " \"source\": {\n", - " \"label\": \"FightStatistics\"\n", + " \"label\": \"Referee\"\n", " },\n", " \"target\": {\n", - " \"label\": \"Fighter\"\n", + " \"label\": \"Event\"\n", " },\n", - " \"attributes\": []\n", + " \"attributes\": [\n", + " {\n", + " \"name\": \"role\",\n", + " \"type\": \"string\",\n", + " \"unique\": false,\n", + " \"required\": false\n", + " }\n", + " ]\n", " }\n", " ]\n", "}\n" @@ -574,7 +608,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Process Documents: 100%|██████████| 8/8 [01:02<00:00, 7.77s/it]\n" + "Process Documents: 100%|██████████| 8/8 [01:18<00:00, 9.77s/it]\n" ] } ], @@ -610,8 +644,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'question': 'Who is Salsa Boy?', 'response': 'Salsa Boy is Waldo Cortes-Acosta.', 'context': '[[\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:9,knockdowns:0,leg_strikes:1,leg_strikes_attempted:1,reversals:0,round_number:1,significant_strikes:6,significant_strikes_attempted:10,significant_strikes_percentage:60.0,submissions_attempted:1,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:13,distance_strikes_attempted:27,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:22,head_strikes_attempted:51,knockdowns:0,leg_strikes:3,leg_strikes_attempted:3,reversals:0,round_number:2,significant_strikes:25,significant_strikes_attempted:54,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:50.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:11,distance_strikes_attempted:17,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:17,head_strikes_attempted:32,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:21,significant_strikes_attempted:37,significant_strikes_percentage:56.0,submissions_attempted:0,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:5,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:17,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:24,knockdowns:0,leg_strikes:15,leg_strikes_attempted:16,reversals:0,round_number:1,significant_strikes:20,significant_strikes_attempted:39,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:2,clinch_strikes_attempted:2,distance_strikes:19,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:1,head_strikes:13,head_strikes_attempted:30,knockdowns:0,leg_strikes:2,leg_strikes_attempted:21,reversals:0,round_number:2,significant_strikes:19,significant_strikes_attempted:37,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:43,distance_strikes_attempted:74,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:34,head_strikes_attempted:62,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:3,significant_strikes:43,significant_strikes_attempted:74,significant_strikes_percentage:58,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:48,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:8,head_strikes_attempted:36,knockdowns:0,leg_strikes:3,leg_strikes_attempted:5,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:48,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:14,distance_strikes_attempted:44,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:29,knockdowns:0,leg_strikes:6,leg_strikes_attempted:8,reversals:0,round_number:2,significant_strikes:14,significant_strikes_attempted:44,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:6,clinch_strikes:3,clinch_strikes_attempted:5,distance_strikes:17,distance_strikes_attempted:39,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:10,head_strikes_attempted:32,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:20,significant_strikes_attempted:44,significant_strikes_percentage:45,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:HAS_FIGHTER_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:5,clinch_strikes:2,clinch_strikes_attempted:4,distance_strikes:10,distance_strikes_attempted:12,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:9,head_strikes_attempted:36,knockdowns:1,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33,submissions_attempted:0,takedown_percentage:0})\\']]', 'cypher': \"\\nMATCH (f:Fighter)\\nWHERE toLower(f.nickname) = 'salsa boy'\\nOPTIONAL MATCH (f)-[r]-(connected)\\nRETURN f, r, connected\\n\"}\n", - "{'question': 'Tell me about one of his fights?', 'response': 'On May 11, 2024, at UFC Fight Night: Lewis vs. Nascimento in St. Louis, Missouri, Waldo Cortes-Acosta fought Robelis Despaigne in a heavyweight bout. Waldo Cortes-Acosta won the fight by unanimous decision after three rounds. He landed 52 significant strikes out of 101 attempts (51.49%), attempted 1 submission, and completed 3 takedowns out of 4 attempts (75% takedown accuracy). The fight went the full distance, with each round lasting 5 minutes.', 'context': '[[\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:9,knockdowns:0,leg_strikes:1,leg_strikes_attempted:1,reversals:0,round_number:1,significant_strikes:6,significant_strikes_attempted:10,significant_strikes_percentage:60.0,submissions_attempted:1,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:7,knockdowns:0,leg_strikes:0,leg_strikes_attempted:0,reversals:0,round_number:1,significant_strikes:2,significant_strikes_attempted:7,significant_strikes_percentage:28.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:13,distance_strikes_attempted:27,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:22,head_strikes_attempted:51,knockdowns:0,leg_strikes:3,leg_strikes_attempted:3,reversals:0,round_number:2,significant_strikes:25,significant_strikes_attempted:54,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:50.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:3,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:8,distance_strikes_attempted:12,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:21,knockdowns:0,leg_strikes:4,leg_strikes_attempted:4,reversals:0,round_number:2,significant_strikes:13,significant_strikes_attempted:28,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:11,distance_strikes_attempted:17,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:17,head_strikes_attempted:32,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:21,significant_strikes_attempted:37,significant_strikes_percentage:56.0,submissions_attempted:0,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:52,significant_strikes_attempted:101,significant_strikes_percentage:51.49,submissions_attempted:1,takedown_percentage:75.0,takedowns:3,takedowns_attempted:4,total_strikes:130,total_strikes_attempted:204})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:5,distance_strikes_attempted:8,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:8,knockdowns:0,leg_strikes:5,leg_strikes_attempted:8,reversals:0,round_number:3,significant_strikes:11,significant_strikes_attempted:17,significant_strikes_percentage:64.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:9,knockdowns:0,leg_strikes:1,leg_strikes_attempted:1,reversals:0,round_number:1,significant_strikes:6,significant_strikes_attempted:10,significant_strikes_percentage:60.0,submissions_attempted:1,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:2,distance_strikes_attempted:4,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:7,knockdowns:0,leg_strikes:0,leg_strikes_attempted:0,reversals:0,round_number:1,significant_strikes:2,significant_strikes_attempted:7,significant_strikes_percentage:28.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:13,distance_strikes_attempted:27,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:22,head_strikes_attempted:51,knockdowns:0,leg_strikes:3,leg_strikes_attempted:3,reversals:0,round_number:2,significant_strikes:25,significant_strikes_attempted:54,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:50.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:3,clinch_strikes:0,clinch_strikes_attempted:1,distance_strikes:8,distance_strikes_attempted:12,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:21,knockdowns:0,leg_strikes:4,leg_strikes_attempted:4,reversals:0,round_number:2,significant_strikes:13,significant_strikes_attempted:28,significant_strikes_percentage:46.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:11,distance_strikes_attempted:17,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:17,head_strikes_attempted:32,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:21,significant_strikes_attempted:37,significant_strikes_percentage:56.0,submissions_attempted:0,takedown_percentage:100.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",referee:\"Josh Stewart\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-05-11\",location:\"St. Louis, Missouri, USA\",name:\"UFC Fight Night: Lewis vs. Nascimento\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:26,significant_strikes_attempted:52,significant_strikes_percentage:50.0,submissions_attempted:0,takedown_percentage:0.0,takedowns:0,takedowns_attempted:0,total_strikes:28,total_strikes_attempted:59})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:0,body_strikes_attempted:0,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:5,distance_strikes_attempted:8,fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",fighter_name:\"Robelis Despaigne\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:2,head_strikes_attempted:8,knockdowns:0,leg_strikes:5,leg_strikes_attempted:8,reversals:0,round_number:3,significant_strikes:11,significant_strikes_attempted:17,significant_strikes_percentage:64.0,submissions_attempted:0,takedown_percentage:0.0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:7,clinch_strikes:1,clinch_strikes_attempted:2,distance_strikes:26,distance_strikes_attempted:32,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:11,knockdowns:0,leg_strikes:10,leg_strikes_attempted:11,reversals:0,round_number:1,significant_strikes:27,significant_strikes_attempted:34,significant_strikes_percentage:79,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:5,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:17,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:24,knockdowns:0,leg_strikes:15,leg_strikes_attempted:16,reversals:0,round_number:1,significant_strikes:20,significant_strikes_attempted:39,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:2,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:13,distance_strikes_attempted:17,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:10,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:2,significant_strikes:15,significant_strikes_attempted:19,significant_strikes_percentage:78,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:2,clinch_strikes_attempted:2,distance_strikes:19,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:1,head_strikes:13,head_strikes_attempted:30,knockdowns:0,leg_strikes:2,leg_strikes_attempted:21,reversals:0,round_number:2,significant_strikes:19,significant_strikes_attempted:37,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:17,distance_strikes_attempted:23,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:10,knockdowns:0,leg_strikes:5,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:17,significant_strikes_attempted:23,significant_strikes_percentage:73,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:59,significant_strikes_attempted:76,significant_strikes_percentage:77.63,submissions_attempted:0,takedown_percentage:42.86,takedowns:3,takedowns_attempted:7,total_strikes:76,total_strikes_attempted:88})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:43,distance_strikes_attempted:74,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:34,head_strikes_attempted:62,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:3,significant_strikes:43,significant_strikes_attempted:74,significant_strikes_percentage:58,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:7,clinch_strikes:1,clinch_strikes_attempted:2,distance_strikes:26,distance_strikes_attempted:32,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:11,knockdowns:0,leg_strikes:10,leg_strikes_attempted:11,reversals:0,round_number:1,significant_strikes:27,significant_strikes_attempted:34,significant_strikes_percentage:79,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:5,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:17,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:24,knockdowns:0,leg_strikes:15,leg_strikes_attempted:16,reversals:0,round_number:1,significant_strikes:20,significant_strikes_attempted:39,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:2,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:13,distance_strikes_attempted:17,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:10,knockdowns:0,leg_strikes:4,leg_strikes_attempted:5,reversals:0,round_number:2,significant_strikes:15,significant_strikes_attempted:19,significant_strikes_percentage:78,submissions_attempted:0,takedown_percentage:50})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:2,clinch_strikes_attempted:2,distance_strikes:19,distance_strikes_attempted:36,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:1,head_strikes:13,head_strikes_attempted:30,knockdowns:0,leg_strikes:2,leg_strikes_attempted:21,reversals:0,round_number:2,significant_strikes:19,significant_strikes_attempted:37,significant_strikes_percentage:51,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:17,distance_strikes_attempted:23,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Marcos Rogerio de Lima\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:7,head_strikes_attempted:10,knockdowns:0,leg_strikes:5,leg_strikes_attempted:5,reversals:0,round_number:3,significant_strikes:17,significant_strikes_attempted:23,significant_strikes_percentage:73,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Loss\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",method:\"Decision - Unanimous\",referee:\"Herb Dean\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-04-29\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Song vs. Simon\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:82,significant_strikes_attempted:150,significant_strikes_percentage:54.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:1,total_strikes:95,total_strikes_attempted:156})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:43,distance_strikes_attempted:74,fight_id:\"UFCFN-SongSimon-LimaCortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:34,head_strikes_attempted:62,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:3,significant_strikes:43,significant_strikes_attempted:74,significant_strikes_percentage:58,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:12,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:40,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:21,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:40,significant_strikes_percentage:37,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:48,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:8,head_strikes_attempted:36,knockdowns:0,leg_strikes:3,leg_strikes_attempted:5,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:48,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:8,body_strikes_attempted:13,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:20,distance_strikes_attempted:41,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:20,knockdowns:0,leg_strikes:6,leg_strikes_attempted:9,reversals:0,round_number:2,significant_strikes:20,significant_strikes_attempted:41,significant_strikes_percentage:48,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:14,distance_strikes_attempted:44,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:29,knockdowns:0,leg_strikes:6,leg_strikes_attempted:8,reversals:0,round_number:2,significant_strikes:14,significant_strikes_attempted:44,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:9,body_strikes_attempted:15,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:23,distance_strikes_attempted:46,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:22,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:23,significant_strikes_attempted:46,significant_strikes_percentage:50,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:58,significant_strikes_attempted:127,significant_strikes_percentage:45.67,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:58,total_strikes_attempted:127})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:6,clinch_strikes:3,clinch_strikes_attempted:5,distance_strikes:17,distance_strikes_attempted:39,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:10,head_strikes_attempted:32,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:20,significant_strikes_attempted:44,significant_strikes_percentage:45,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:12,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:40,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:21,knockdowns:0,leg_strikes:6,leg_strikes_attempted:7,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:40,significant_strikes_percentage:37,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:4,body_strikes_attempted:7,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:15,distance_strikes_attempted:48,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:8,head_strikes_attempted:36,knockdowns:0,leg_strikes:3,leg_strikes_attempted:5,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:48,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:8,body_strikes_attempted:13,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:20,distance_strikes_attempted:41,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:4,head_strikes_attempted:20,knockdowns:0,leg_strikes:6,leg_strikes_attempted:9,reversals:0,round_number:2,significant_strikes:20,significant_strikes_attempted:41,significant_strikes_percentage:48,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:2,body_strikes_attempted:6,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:14,distance_strikes_attempted:44,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:6,head_strikes_attempted:29,knockdowns:0,leg_strikes:6,leg_strikes_attempted:8,reversals:0,round_number:2,significant_strikes:14,significant_strikes_attempted:44,significant_strikes_percentage:31,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:9,body_strikes_attempted:15,clinch_strikes:0,clinch_strikes_attempted:0,distance_strikes:23,distance_strikes_attempted:46,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Andrei Arlovski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:5,head_strikes_attempted:22,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:23,significant_strikes_attempted:46,significant_strikes_percentage:50,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",method:\"Decision - Unanimous\",referee:\"Marc Goddard\",rounds:3,time:\"5:00\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2024-01-13\",location:\"Las Vegas, Nevada, USA\",name:\"UFC Fight Night: Ankalaev vs. Walker 2\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:0,passes:0,result:\"Win\",reversals:0,significant_strikes:49,significant_strikes_attempted:136,significant_strikes_percentage:36.03,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:50,total_strikes_attempted:137})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:5,body_strikes_attempted:6,clinch_strikes:3,clinch_strikes_attempted:5,distance_strikes:17,distance_strikes_attempted:39,fight_id:\"UFCFN-Ankalaev-Walker2-Arlovski-CortesAcosta\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:10,head_strikes_attempted:32,knockdowns:0,leg_strikes:5,leg_strikes_attempted:6,reversals:0,round_number:3,significant_strikes:20,significant_strikes_attempted:44,significant_strikes_percentage:45,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:5,clinch_strikes:2,clinch_strikes_attempted:4,distance_strikes:10,distance_strikes_attempted:12,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:9,head_strikes_attempted:36,knockdowns:1,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",knockdowns:1,passes:0,result:\"Win\",reversals:0,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33.33,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:16,total_strikes_attempted:46})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:3,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:12,distance_strikes_attempted:42,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:1,head_strikes_attempted:7,knockdowns:0,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63.64,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:15,total_strikes_attempted:23})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:5,clinch_strikes:2,clinch_strikes_attempted:4,distance_strikes:10,distance_strikes_attempted:12,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Waldo Cortes-Acosta\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:9,head_strikes_attempted:36,knockdowns:1,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:15,significant_strikes_attempted:45,significant_strikes_percentage:33,submissions_attempted:0,takedown_percentage:0})\\'], [\\'(:Fighter{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED_IN{result:\"Win\"}]->()\\', \\'(:Fight{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",referee:\"Mark Craig\",rounds:1,time:\"3:01\",weight_class:\"Heavyweight\"})\\', \\'()-[:HAS_FIGHT{order:1}]->()\\', \\'(:Event{date:\"2023-08-26\",location:\"Kallang, Singapore\",name:\"UFC Fight Night: Holloway vs. The Korean Zombie\"})\\', \\'()-[:HAS_STATISTICS]->()\\', \\'(:FightStatistics{fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",knockdowns:0,passes:0,result:\"Loss\",reversals:0,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63.64,submissions_attempted:0,takedown_percentage:0,takedowns:0,takedowns_attempted:0,total_strikes:15,total_strikes_attempted:23})\\', \\'()-[:HAS_ROUND_STATISTICS]->()\\', \\'(:RoundStatistics{body_strikes:3,body_strikes_attempted:3,clinch_strikes:3,clinch_strikes_attempted:3,distance_strikes:12,distance_strikes_attempted:42,fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",fighter_name:\"Lukasz Brzeski\",ground_strikes:0,ground_strikes_attempted:0,head_strikes:1,head_strikes_attempted:7,knockdowns:0,leg_strikes:3,leg_strikes_attempted:4,reversals:0,round_number:1,significant_strikes:14,significant_strikes_attempted:22,significant_strikes_percentage:63,submissions_attempted:0,takedown_percentage:0})\\']]', 'cypher': \"\\nMATCH (f:Fighter)\\nWHERE toLower(f.name) = 'waldo cortes-acosta'\\nMATCH (f)-[p:PARTICIPATED_IN]->(fi:Fight)\\nOPTIONAL MATCH (fi)<-[hf:HAS_FIGHT]-(e:Event)\\nOPTIONAL MATCH (fi)-[hs:HAS_STATISTICS]->(fs:FightStatistics)\\nOPTIONAL MATCH (fi)-[hrs:HAS_ROUND_STATISTICS]->(rs:RoundStatistics)\\nRETURN f, p, fi, hf, e, hs, fs, hrs, rs\\n\"}\n" + "{'question': 'Who is Salsa Boy?', 'response': 'Salsa Boy is Waldo Cortes-Acosta.', 'context': '[[\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\']]', 'cypher': \"\\nMATCH (p:Person)\\nWHERE p.nickname CONTAINS 'Salsa Boy'\\nRETURN p\\n\"}\n", + "{'question': 'Tell me about one of his fights?', 'response': 'Waldo Cortes-Acosta, also known as Salsa Boy, fought Lukasz Brzeski at UFC Fight Night: Holloway vs. The Korean Zombie on August 26, 2023. He won the fight by KO/TKO with punches to the head at distance in the first round at 3:01. The bout was in the Heavyweight division and was scheduled for 3 rounds.', 'context': '[[\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Win\"}]->()\\', \\'(:Fight{details:\"Punches to Head At Distance\",fight_id:\"Waldo Cortes-Acosta v Lukasz Brzeski UFC Fight Night: Holloway vs. The Korean Zombie 2023-08-26\",method:\"KO/TKO\",round:1,time:\"3:01\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\'], [\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Win\"}]->()\\', \\'(:Fight{details:\"Decision\",fight_id:\"Arlovski_vs_Cortes-Acosta_UFCFN_2024-01-13\",method:\"Decision - Unanimous\",round:3,time:\"5:00\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\'], [\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Win\"}]->()\\', \\'(:Fight{details:\"Decision\",fight_id:\"WaldoCortesAcosta_vs_RobelisDespaigne_UFCFN_Lewis_Nascimento_2024-05-11\",method:\"Decision - Unanimous\",round:3,time:\"5:00\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\'], [\\'(:Person{name:\"Waldo Cortes-Acosta\",nickname:\"Salsa Boy\"})\\', \\'()-[:PARTICIPATED{result:\"Loss\"}]->()\\', \\'(:Fight{details:\"Decision\",fight_id:\"MarcosRogerioDeLima_vs_WaldoCortesAcosta_UFCFightNightSongVsSimon_2023-04-29\",method:\"Decision - Unanimous\",round:3,time:\"5:00\",time_format:\"3 Rounds (5-5-5)\",title_bout:False,weight_class:\"Heavyweight\"})\\']]', 'cypher': \"\\nMATCH (p:Person)-[pa:PARTICIPATED]->(f:Fight)\\nWHERE p.name CONTAINS 'Waldo Cortes-Acosta'\\nRETURN p, pa, f\\n\"}\n" ] } ], diff --git a/examples/ufc/ontology.json b/examples/ufc/ontology.json index 83fc392b..d0ee2570 100644 --- a/examples/ufc/ontology.json +++ b/examples/ufc/ontology.json @@ -33,11 +33,17 @@ "unique": true, "required": true }, + { + "name": "title_bout", + "type": "boolean", + "unique": false, + "required": true + }, { "name": "weight_class", "type": "string", "unique": false, - "required": true + "required": false }, { "name": "method", @@ -46,7 +52,7 @@ "required": true }, { - "name": "rounds", + "name": "round", "type": "number", "unique": false, "required": true @@ -58,16 +64,22 @@ "required": true }, { - "name": "referee", + "name": "time_format", "type": "string", "unique": false, - "required": true + "required": false + }, + { + "name": "details", + "type": "string", + "unique": false, + "required": false } ], "description": "" }, { - "label": "Fighter", + "label": "Person", "attributes": [ { "name": "name", @@ -85,26 +97,26 @@ "description": "" }, { - "label": "FightStatistics", + "label": "Referee", "attributes": [ { - "name": "fight_id", + "name": "name", "type": "string", "unique": true, "required": true - }, + } + ], + "description": "" + }, + { + "label": "FightStatistics", + "attributes": [ { - "name": "fighter_name", + "name": "statistics_id", "type": "string", "unique": true, "required": true }, - { - "name": "result", - "type": "string", - "unique": false, - "required": true - }, { "name": "knockdowns", "type": "number", @@ -184,13 +196,7 @@ "label": "RoundStatistics", "attributes": [ { - "name": "fight_id", - "type": "string", - "unique": true, - "required": true - }, - { - "name": "fighter_name", + "name": "round_statistics_id", "type": "string", "unique": true, "required": true @@ -198,7 +204,7 @@ { "name": "round_number", "type": "number", - "unique": true, + "unique": false, "required": true }, { @@ -226,91 +232,67 @@ "required": true }, { - "name": "head_strikes", - "type": "number", - "unique": false, - "required": true - }, - { - "name": "head_strikes_attempted", - "type": "number", - "unique": false, - "required": true - }, - { - "name": "body_strikes", - "type": "number", - "unique": false, - "required": true - }, - { - "name": "body_strikes_attempted", - "type": "number", - "unique": false, - "required": true - }, - { - "name": "leg_strikes", - "type": "number", - "unique": false, - "required": true - }, - { - "name": "leg_strikes_attempted", + "name": "takedown_percentage", "type": "number", "unique": false, "required": true }, { - "name": "distance_strikes", + "name": "submissions_attempted", "type": "number", "unique": false, "required": true }, { - "name": "distance_strikes_attempted", + "name": "reversals", "type": "number", "unique": false, "required": true - }, + } + ], + "description": "" + }, + { + "label": "SignificantStrikeBreakdown", + "attributes": [ { - "name": "clinch_strikes", - "type": "number", - "unique": false, + "name": "breakdown_id", + "type": "string", + "unique": true, "required": true }, { - "name": "clinch_strikes_attempted", + "name": "head", "type": "number", "unique": false, "required": true }, { - "name": "ground_strikes", + "name": "body", "type": "number", "unique": false, "required": true }, { - "name": "ground_strikes_attempted", + "name": "leg", "type": "number", "unique": false, "required": true }, { - "name": "takedown_percentage", + "name": "distance", "type": "number", "unique": false, "required": true }, { - "name": "submissions_attempted", + "name": "clinch", "type": "number", "unique": false, "required": true }, { - "name": "reversals", + "name": "ground", "type": "number", "unique": false, "required": true @@ -321,7 +303,7 @@ ], "relations": [ { - "label": "HAS_FIGHT", + "label": "HOSTED", "source": { "label": "Event" }, @@ -330,17 +312,17 @@ }, "attributes": [ { - "name": "order", - "type": "number", + "name": "main_event", + "type": "boolean", "unique": false, "required": false } ] }, { - "label": "PARTICIPATED_IN", + "label": "PARTICIPATED", "source": { - "label": "Fighter" + "label": "Person" }, "target": { "label": "Fight" @@ -354,6 +336,23 @@ } ] }, + { + "label": "OFFICIATED", + "source": { + "label": "Referee" + }, + "target": { + "label": "Fight" + }, + "attributes": [ + { + "name": "role", + "type": "string", + "unique": false, + "required": true + } + ] + }, { "label": "HAS_STATISTICS", "source": { @@ -362,7 +361,14 @@ "target": { "label": "FightStatistics" }, - "attributes": [] + "attributes": [ + { + "name": "fighter_name", + "type": "string", + "unique": false, + "required": true + } + ] }, { "label": "HAS_ROUND_STATISTICS", @@ -372,37 +378,65 @@ "target": { "label": "RoundStatistics" }, - "attributes": [] + "attributes": [ + { + "name": "fighter_name", + "type": "string", + "unique": false, + "required": true + } + ] }, { - "label": "HAS_FIGHTER_STATISTICS", + "label": "HAS_BREAKDOWN", "source": { - "label": "Fighter" + "label": "FightStatistics" }, "target": { - "label": "FightStatistics" + "label": "SignificantStrikeBreakdown" }, - "attributes": [] + "attributes": [ + { + "name": "round_number", + "type": "number", + "unique": false, + "required": false + } + ] }, { - "label": "HAS_FIGHTER_ROUND_STATISTICS", + "label": "HAS_ROUND_BREAKDOWN", "source": { - "label": "Fighter" + "label": "RoundStatistics" }, "target": { - "label": "RoundStatistics" + "label": "SignificantStrikeBreakdown" }, - "attributes": [] + "attributes": [ + { + "name": "breakdown_id", + "type": "string", + "unique": false, + "required": true + } + ] }, { - "label": "HAS_FIGHTER", + "label": "PARTICIPATED_AS_REFEREE", "source": { - "label": "FightStatistics" + "label": "Referee" }, "target": { - "label": "Fighter" + "label": "Event" }, - "attributes": [] + "attributes": [ + { + "name": "role", + "type": "string", + "unique": false, + "required": false + } + ] } ] } \ No newline at end of file diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index 85ad613d..36bbb364 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -102,6 +102,9 @@ def __init__(self, model_config: KnowledgeGraphModelConfig, ontology: Ontology, "cypher": None } + # Metadata to store additional information about the chat session (currently only last query execution time) + self.metadata = {"last_query_execution_time": None} + def _update_last_complete_response(self, response_dict: dict): """Update the last complete response in both the session and cypher step.""" self.last_complete_response = response_dict @@ -124,8 +127,16 @@ def generate_cypher_query(self, message: str) -> tuple: """ # Update the last_answer for this query self.cypher_step.last_answer = self.last_complete_response.get("response") - - (context, cypher, _) = self.cypher_step.run(message) + + try: + (context, cypher, query_execution_time) = self.cypher_step.run(message) + except Exception as e: + # If there's an error, return empty context and cypher with error message + context = None + cypher = CYPHER_ERROR_RES + query_execution_time = None + + self.metadata["last_query_execution_time"] = query_execution_time return (context, cypher) diff --git a/graphrag_sdk/kg.py b/graphrag_sdk/kg.py index 8f53dfef..752bbeb6 100644 --- a/graphrag_sdk/kg.py +++ b/graphrag_sdk/kg.py @@ -210,7 +210,6 @@ def chat_session(self) -> ChatSession: chat_session = ChatSession(self._model_config, self.ontology, self.graph, self.cypher_system_instruction, self.qa_system_instruction, self.cypher_gen_prompt, self.qa_prompt, self.cypher_gen_prompt_history) return chat_session - def add_node(self, entity: str, attributes: dict) -> None: """ Add a node to the knowledge graph, checking if it matches the ontology diff --git a/tests/test_streaming_response.py b/tests/test_streaming_response.py index 6afdd4a4..f6b2b9b4 100644 --- a/tests/test_streaming_response.py +++ b/tests/test_streaming_response.py @@ -161,6 +161,7 @@ def test_streaming(self, knowledge_graph_setup, delete_kg): answer = ''.join(received_chunks) assert answer.strip() == response_dict["response"].strip(), "Combined chunks (using join) should match last complete response" + assert chat.metadata.get("last_query_execution_time") is not None, "Expected last query execution time to be set" # Create a test case for evaluation test_case = LLMTestCase( From 2858ee58971266cc61436c0fceffb23b67285639 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 17:57:44 +0300 Subject: [PATCH 7/9] rm-un-changes --- examples/ufc/pydantic-ai-agent-demo.ipynb | 663 ---------------------- graphrag_sdk/fixtures/prompts.py | 136 ++--- graphrag_sdk/helpers.py | 128 ++--- 3 files changed, 98 insertions(+), 829 deletions(-) delete mode 100644 examples/ufc/pydantic-ai-agent-demo.ipynb diff --git a/examples/ufc/pydantic-ai-agent-demo.ipynb b/examples/ufc/pydantic-ai-agent-demo.ipynb deleted file mode 100644 index 6a4d6c15..00000000 --- a/examples/ufc/pydantic-ai-agent-demo.ipynb +++ /dev/null @@ -1,663 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f6c5bc1a", - "metadata": {}, - "source": [ - "# 🥋 FalkorDB Agent with Pydantic AI - UFC Knowledge Graph Demo\n", - "\n", - "This notebook demonstrates how to create an intelligent **UFC-focused AI agent** using **FalkorDB** and **Pydantic AI** that can query a comprehensive UFC knowledge graph powered by the **GraphRAG SDK**.\n", - "\n", - "## What You'll Learn\n", - "\n", - "- How to set up a Pydantic AI agent with custom tools\n", - "- How to integrate FalkorDB with the GraphRAG SDK\n", - "- How to create a knowledge graph search tool for your agent\n", - "- How to build an interactive chat interface with streaming responses\n", - "- How to handle dependencies and error management in agent workflows\n", - "\n", - "## Prerequisites\n", - "\n", - "Before running this notebook, ensure you have:\n", - "\n", - "- A running FalkorDB instance with UFC data loaded (see [demo-ufc.ipynb](./demo-ufc.ipynb))\n", - "- OpenAI API key for the language model\n", - "- Python environment with required packages (installed below)" - ] - }, - { - "cell_type": "markdown", - "id": "72eeeaf6", - "metadata": {}, - "source": [ - "## 🔧 Installation Requirements\n", - "\n", - "This notebook requires three main packages. Install them with the cell below:\n", - "\n", - "### Required Packages:\n", - "- **GraphRAG SDK**: Provides FalkorDB integration, Rich console formatting, OpenAI client, and graph operations\n", - "- **Pydantic AI**: Modern AI agent framework with type safety and structured data handling \n", - "- **Gradio**: Web interface framework for creating interactive chat applications\n", - "\n", - "\n", - "**Note**: The GraphRAG SDK includes most dependencies we need, so the installation is streamlined!" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "77ff4ed8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", - "deepeval 2.6.7 requires anthropic<0.50.0,>=0.49.0, but you have anthropic 0.55.0 which is incompatible.\n", - "google-genai 1.22.0 requires httpx<1.0.0,>=0.28.1, but you have httpx 0.27.2 which is incompatible.\n", - "mistralai 1.8.2 requires httpx>=0.28.1, but you have httpx 0.27.2 which is incompatible.\u001b[0m\u001b[31m\n", - "\u001b[0m\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", - "deepeval 2.6.7 requires anthropic<0.50.0,>=0.49.0, but you have anthropic 0.55.0 which is incompatible.\n", - "ollama 0.2.1 requires httpx<0.28.0,>=0.27.0, but you have httpx 0.28.1 which is incompatible.\u001b[0m\u001b[31m\n", - "\u001b[0m\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", - "deepeval 2.6.7 requires anthropic<0.50.0,>=0.49.0, but you have anthropic 0.55.0 which is incompatible.\n", - "ollama 0.2.1 requires httpx<0.28.0,>=0.27.0, but you have httpx 0.28.1 which is incompatible.\u001b[0m\u001b[31m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "!pip install graphrag_sdk --quiet\n", - "!pip install pydantic-ai --quiet\n", - "!pip install gradio --quiet" - ] - }, - { - "cell_type": "markdown", - "id": "c56eedd2", - "metadata": {}, - "source": [ - "## 1. Import Required Libraries\n", - "\n", - "First, let's import all the necessary libraries for our FalkorDB and Pydantic AI agent." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f77206f2", - "metadata": {}, - "outputs": [], - "source": [ - "import gradio as gr\n", - "from typing import List\n", - "from falkordb import FalkorDB\n", - "from dataclasses import dataclass\n", - "from __future__ import annotations\n", - "from pydantic import BaseModel, Field\n", - "from graphrag_sdk import KnowledgeGraph\n", - "from pydantic_ai import Agent, RunContext\n", - "from graphrag_sdk.ontology import Ontology\n", - "from graphrag_sdk.models.litellm import LiteModel\n", - "from graphrag_sdk.chat_session import ChatSession\n", - "from pydantic_ai.models.openai import OpenAIModel\n", - "from pydantic_ai.providers.openai import OpenAIProvider\n", - "from graphrag_sdk.model_config import KnowledgeGraphModelConfig" - ] - }, - { - "cell_type": "markdown", - "id": "557fbb3e", - "metadata": {}, - "source": [ - "## 2. Load Environment Variables and Configuration\n", - "\n", - "Load environment variables for API keys and database connection parameters." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b453b7de", - "metadata": {}, - "outputs": [], - "source": [ - "# Configuration parameters\n", - "MODEL_CHOICE = 'gpt-4o-mini'\n", - "OPENAI_API_KEY = \"your-openai-api-key-here\" # Replace with your actual API key or use os.getenv\n", - "\n", - "# FalkorDB connection parameters\n", - "FALKORDB_HOST = \"localhost\" # Replace with your FalkorDB host\n", - "FALKORDB_PORT = 6379 # Default port for FalkorDB\n", - "FALKORDB_USERNAME = \"your-falkordb-username\" # can be None if not required\n", - "FALKORDB_PASSWORD = \"your-falkordb-password\" # can be None if not required\n", - "\n", - "GRAPH_NAME = \"ufc\"" - ] - }, - { - "cell_type": "markdown", - "id": "3e73cf2d", - "metadata": {}, - "source": [ - "## 3. Set Up the LLM Model\n", - "\n", - "Configure the OpenAI model that will power our Pydantic AI agent." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "4658d5db", - "metadata": {}, - "outputs": [], - "source": [ - "def get_model():\n", - " \"\"\"Configure and return the LLM model to use.\"\"\"\n", - " return OpenAIModel(MODEL_CHOICE, provider=OpenAIProvider(api_key=OPENAI_API_KEY))" - ] - }, - { - "cell_type": "markdown", - "id": "8f4c9697", - "metadata": {}, - "source": [ - "## 4. Connect to FalkorDB and Load Ontology\n", - "\n", - "Establish connection to FalkorDB and load the existing UFC knowledge graph ontology." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "9fbb3930", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔌 Connecting to FalkorDB...\n", - "✅ Loaded ontology from existing knowledge graph.\n", - " Entities: 5\n", - " Relations: 7\n" - ] - } - ], - "source": [ - "# Connect to FalkorDB\n", - "print(\"🔌 Connecting to FalkorDB...\")\n", - "db = FalkorDB(host=FALKORDB_HOST, port=FALKORDB_PORT, username=FALKORDB_USERNAME, password=FALKORDB_PASSWORD)\n", - "graph = db.select_graph(GRAPH_NAME)\n", - "\n", - "# Load ontology from existing graph\n", - "try:\n", - " ontology = Ontology.from_kg_graph(graph)\n", - " print(\"✅ Loaded ontology from existing knowledge graph.\")\n", - " print(f\" Entities: {len(ontology.entities) if ontology.entities else 0}\")\n", - " print(f\" Relations: {len(ontology.relations) if ontology.relations else 0}\")\n", - "except Exception as e:\n", - " print(f\"⚠️ Could not load ontology from existing graph: {str(e)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "a33122bd", - "metadata": {}, - "source": [ - "## 5. Initialize KnowledgeGraph and ChatSession\n", - "\n", - "Set up the GraphRAG SDK components for knowledge graph interaction." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "ff61d0da", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔗 Initializing KnowledgeGraph client...\n", - "✅ ChatSession created successfully!\n" - ] - } - ], - "source": [ - "# Initialize LiteModel for GraphRAG SDK (by default - gpt-4.1)\n", - "model_falkor = LiteModel()\n", - "\n", - "# Initialize model configuration\n", - "model_config = KnowledgeGraphModelConfig.with_model(model_falkor)\n", - "\n", - "# Initialize KnowledgeGraph\n", - "print(\"🔗 Initializing KnowledgeGraph client...\")\n", - "kg_client = KnowledgeGraph(\n", - " name=GRAPH_NAME,\n", - " model_config=model_config,\n", - " ontology=ontology,\n", - " host=FALKORDB_HOST,\n", - " port=FALKORDB_PORT,\n", - " username=FALKORDB_USERNAME,\n", - " password=FALKORDB_PASSWORD\n", - ")\n", - "\n", - "# Create a chat session for agent queries (replaces cypher_session)\n", - "chat_session = kg_client.chat_session()\n", - "print(\"✅ ChatSession created successfully!\")" - ] - }, - { - "cell_type": "markdown", - "id": "582a0874", - "metadata": {}, - "source": [ - "## 6. Define Agent Dependencies\n", - "\n", - "Create a dependencies class to pass the ChatSession to our agent." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "12c49613", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "📦 Agent dependencies defined!\n" - ] - } - ], - "source": [ - "@dataclass\n", - "class FalkorDependencies:\n", - " \"\"\"Dependencies for the Falkor agent.\"\"\"\n", - " chat_session: ChatSession\n", - "\n", - "print(\"📦 Agent dependencies defined!\")" - ] - }, - { - "cell_type": "markdown", - "id": "4cb562e0", - "metadata": {}, - "source": [ - "## 7. Create the Pydantic AI Agent\n", - "\n", - "Define our main agent with a comprehensive system prompt for UFC knowledge graph querying." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "11e090fe", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🤖 Falkor Agent created successfully!\n" - ] - } - ], - "source": [ - "# Create the Falkor agent\n", - "falkor_agent = Agent(\n", - " get_model(),\n", - " system_prompt=\"\"\"You are a knowledge graph assistant that helps users query a FalkorDB knowledge graph.\n", - "\n", - "When a user provides ANY input (questions, entity names, keywords, or statements), you MUST use the search_falkor tool with the EXACT, COMPLETE user input as the query parameter. Do not modify, shorten, or extract keywords from the user's input.\n", - "\n", - "The knowledge graph system is designed to handle:\n", - "- Full questions: \"Who is Salsa Boy?\"\n", - "- Entity names: \"Salsa Boy\"\n", - "- Keywords: \"fighters\", \"matches\", \"UFC\"\n", - "- Statements: \"Show me information about recent fights\"\n", - "- Any other text input\n", - "\n", - "The tool will return:\n", - "- A Cypher query that was generated to search the graph (automatically adapted to the input type)\n", - "- Context data extracted from the graph using that query\n", - "\n", - "After receiving the results, explain what the Cypher query does and interpret the context data to provide a helpful answer. Focus on the entities, relationships, and graph patterns found in the results. If the input was just an entity name, provide comprehensive information about that entity and its connections.\n", - "Try to be concise but thorough in your explanations, ensuring the user understands the graph data and its implications.\"\"\",\n", - " deps_type=FalkorDependencies\n", - ")\n", - "\n", - "print(\"🤖 Falkor Agent created successfully!\")" - ] - }, - { - "cell_type": "markdown", - "id": "ececa87d", - "metadata": {}, - "source": [ - "## 8. Define the Search Result Model\n", - "\n", - "Create a Pydantic model to structure the results from our FalkorDB search tool.\n", - "\n", - "This model ensures type safety and clear data structure for the agent's search results." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "f2125ee9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "📊 Search result model defined!\n" - ] - } - ], - "source": [ - "class FalkorSearchResult(BaseModel):\n", - " \"\"\"Model representing a search result from FalkorDB.\"\"\"\n", - " cypher: str = Field(description=\"The generated Cypher query\")\n", - " context: str = Field(description=\"The extracted context from the knowledge graph\")\n", - "\n", - "print(\"📊 Search result model defined!\")" - ] - }, - { - "cell_type": "markdown", - "id": "c62de651", - "metadata": {}, - "source": [ - "## 9. Register the Search Tool\n", - "\n", - "Create and register the main tool that allows our agent to search the FalkorDB knowledge graph." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "04f08b47", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔍 Search tool registered with agent!\n" - ] - } - ], - "source": [ - "@falkor_agent.tool\n", - "async def search_falkor(ctx: RunContext[FalkorDependencies], query: str) -> List[FalkorSearchResult]:\n", - " \"\"\"Search the FalkorDB knowledge graph with the given query - returns only cypher and context.\n", - " \n", - " Args:\n", - " ctx: The run context containing dependencies\n", - " query: The search query to find information in the knowledge graph\n", - " \n", - " Returns:\n", - " A list of search results containing cypher queries and context that match the query\n", - " \"\"\"\n", - " # Access the ChatSession from dependencies\n", - " chat_session = ctx.deps.chat_session\n", - " \n", - " try:\n", - " # Use the chat session's generate_cypher_query method to get cypher and context\n", - " # This method returns (context, cypher) tuple\n", - " context, cypher = chat_session.generate_cypher_query(query)\n", - " \n", - " # Format the result to match the expected interface\n", - " formatted_result = FalkorSearchResult(\n", - " cypher=cypher or '',\n", - " context=context or ''\n", - " )\n", - " \n", - " return [formatted_result]\n", - " except Exception as e:\n", - " # Log the error\n", - " print(f\"Error searching FalkorDB: {str(e)}\")\n", - " raise\n", - "\n", - "print(\"🔍 Search tool registered with agent!\")" - ] - }, - { - "cell_type": "markdown", - "id": "24961420", - "metadata": {}, - "source": [ - "## 10. Gradio Chat Function\n", - "\n", - "Create the Gradio integration function for web-based chat interface." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "727dda29", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🌐 Gradio chat function defined!\n" - ] - } - ], - "source": [ - "async def gradio_chat_function(message, history):\n", - " \"\"\"\n", - " Gradio chat function that processes user messages and returns agent responses.\n", - " \n", - " Args:\n", - " message (str): The user's message\n", - " history (List): Chat history from Gradio\n", - " \n", - " Returns:\n", - " str: The agent's response\n", - " \"\"\"\n", - " deps = FalkorDependencies(chat_session=chat_session)\n", - " \n", - " try:\n", - " # Convert Gradio history to our message format\n", - " message_history = []\n", - " for user_msg, assistant_msg in history:\n", - " if user_msg:\n", - " message_history.append({\"role\": \"user\", \"content\": user_msg})\n", - " if assistant_msg:\n", - " message_history.append({\"role\": \"assistant\", \"content\": assistant_msg})\n", - " \n", - " # Get response from agent\n", - " result = await falkor_agent.run(\n", - " message,\n", - " deps=deps\n", - " )\n", - " \n", - " return result.data\n", - " \n", - " except Exception as e:\n", - " return f\"❌ Error: {str(e)}\"\n", - "\n", - "print(\"🌐 Gradio chat function defined!\")" - ] - }, - { - "cell_type": "markdown", - "id": "b99435d7", - "metadata": {}, - "source": [ - "## 11. Create Gradio Web Interface\n", - "\n", - "Build an elegant web-based chat interface using Gradio for easy interaction with the UFC knowledge graph agent." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "f4873577", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🌐 Gradio interface created!\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_210415/1172376482.py:23: UserWarning: You have not specified a value for the `type` parameter. Defaulting to the 'tuples' format for chatbot messages, but this is deprecated and will be removed in a future version of Gradio. Please set type='messages' instead, which uses openai-style dictionaries with 'role' and 'content' keys.\n", - " chatbot = gr.Chatbot(height=400, show_copy_button=True)\n" - ] - } - ], - "source": [ - "def create_gradio_interface():\n", - " \"\"\"Create a clean, elegant Gradio chat interface.\"\"\"\n", - " \n", - " def chat(message, history):\n", - " \"\"\"Handle chat interactions.\"\"\"\n", - " if not message.strip():\n", - " return history, \"\"\n", - " \n", - " try:\n", - " import asyncio\n", - " response = asyncio.run(gradio_chat_function(message, history))\n", - " history.append((message, response))\n", - " return history, \"\"\n", - " except Exception as e:\n", - " history.append((message, f\"❌ Error: {str(e)}\"))\n", - " return history, \"\"\n", - " \n", - " # Create interface with minimal, clean design\n", - " with gr.Blocks(title=\"🥋 UFC Knowledge Graph Agent\", theme=gr.themes.Soft()) as interface:\n", - " \n", - " gr.Markdown(\"# 🥋 UFC Knowledge Graph Agent\\n### Ask anything about UFC fighters, fights, and events!\")\n", - " \n", - " chatbot = gr.Chatbot(height=400, show_copy_button=True)\n", - " \n", - " with gr.Row():\n", - " msg = gr.Textbox(placeholder=\"Ask me about UFC...\", scale=4, container=False)\n", - " submit = gr.Button(\"🚀 Ask\", variant=\"primary\")\n", - " clear = gr.Button(\"🗑️\", variant=\"secondary\")\n", - " \n", - " # Example questions\n", - " gr.Examples([\n", - " \"Who is Salsa Boy?\",\n", - " \"Show me recent UFC fights\",\n", - " \"Which fighters have the most wins?\",\n", - " \"What are the UFC weight classes?\"\n", - " ], msg)\n", - " \n", - " # Event handling\n", - " submit.click(chat, [msg, chatbot], [chatbot, msg])\n", - " msg.submit(chat, [msg, chatbot], [chatbot, msg])\n", - " clear.click(lambda: [], outputs=chatbot)\n", - " \n", - " return interface\n", - "\n", - "# Create and launch interface\n", - "gradio_interface = create_gradio_interface()\n", - "print(\"🌐 Gradio interface created!\")" - ] - }, - { - "cell_type": "markdown", - "id": "aca6e293", - "metadata": {}, - "source": [ - "## 12. Launch the Interface\n", - "\n", - "Launch the Gradio web interface and start chatting with the UFC knowledge graph agent!" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "ef8586ec", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🚀 Launching UFC Knowledge Graph Agent...\n", - "* Running on local URL: http://127.0.0.1:7860\n", - "* Running on local URL: http://127.0.0.1:7860\n", - "* Running on public URL: https://79db0b9f73212d5729.gradio.live\n", - "\n", - "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n", - "* Running on public URL: https://79db0b9f73212d5729.gradio.live\n", - "\n", - "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n" - ] - }, - { - "data": { - "text/html": [ - "
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_210415/2419845010.py:29: DeprecationWarning: `result.data` is deprecated, use `result.output` instead.\n", - " return result.data\n" - ] - } - ], - "source": [ - "# Launch the interface\n", - "print(\"🚀 Launching UFC Knowledge Graph Agent...\")\n", - "gradio_interface.launch(share=True, inbrowser=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "sdk", - "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.10.18" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/graphrag_sdk/fixtures/prompts.py b/graphrag_sdk/fixtures/prompts.py index 7871cbbf..1f79c5d8 100644 --- a/graphrag_sdk/fixtures/prompts.py +++ b/graphrag_sdk/fixtures/prompts.py @@ -399,88 +399,52 @@ """ CYPHER_GEN_SYSTEM = """ -Task: Generate OpenCypher statements to query a graph database based on natural language questions. - -Core Requirements: -- Use ONLY the entities, relationship types, and properties defined in the provided ontology -- Generate syntactically valid OpenCypher statements -- Return comprehensive results including all relevant entities, relationships, and their attributes -- Maintain correct relationship direction: arrows point from source to target as defined in ontology - -Query Construction Rules: -1. Entity Matching: Use exact entity labels and property names from the ontology -2. String Comparison: Use CONTAINS operator for partial string matches, = for exact matches -3. Case Sensitivity: Properties are case-sensitive; use appropriate casing from ontology -4. Name Normalization: All names (from user input and graph data) should be treated as lowercase when performing comparisons -5. Comprehensive Results: Return complete entity objects and relationships, not just requested attributes -6. Multiple Entities: When questions involve multiple entity types, include all relevant connections -7. Simple Queries: For declarative statements or single entity names, extract the relevant entity and return it with its direct relationships (1-hop only) - -Relationship Handling: -- Respect relationship direction as defined in ontology (source -> target) -- Use appropriate relationship types exactly as specified -- For bidirectional queries, specify direction explicitly or use undirected syntax when appropriate - -Advanced Features Available: -- Variable length paths: -[:TYPE*minHops..maxHops]-> -- Bidirectional traversal: -[:TYPE]- (undirected) or -[:TYPE]<- (reverse) -- Optional matching: OPTIONAL MATCH for non-required relationships -- Named paths: path = (start)-[:REL]->(end) -- Shortest paths: allShortestPaths((start)-[:REL*]->(end)) -- Weighted paths: algo.SPpaths() for single-pair, algo.SSpaths() for single-source - -Error Handling: -- If the question cannot be answered with the provided ontology, return: "UNABLE_TO_GENERATE: [brief reason]" -- If entities or relationships mentioned don't exist in ontology, return: "UNABLE_TO_GENERATE: Required entities/relationships not found in ontology" - -Output Format: -- Return ONLY the OpenCypher statement enclosed in triple backticks -- No explanations, apologies, or additional text -- Ensure query is syntactically correct before returning - -Query Validation Checklist: -- All entities exist in ontology ✓ -- All relationships exist and have correct direction ✓ -- All properties exist for their respective entities ✓ -- Syntax is valid OpenCypher ✓ -- Query returns comprehensive results ✓ +Task: Generate OpenCypher statement to query a graph database. + +Instructions: +Use only the provided entities, relationships types and properties in the ontology. +The output must be only a valid OpenCypher statement. +Respect the order of the relationships, the arrows should always point from the "start" to the "end". +Respect the types of entities of every relationship, according to the ontology. +The OpenCypher statement must return all the relevant entities, not just the attributes requested. +The output of the OpenCypher statement will be passed to another model to answer the question, hence, make sure the OpenCypher statement returns all relevant entities, relationships, and attributes. +If the answer required multiple entities, return all the entities, relations, relationships, and their attributes. +If you cannot generate a OpenCypher statement based on the provided ontology, explain the reason to the user. +For String comparison, use the `CONTAINS` operator. +Do not use any other relationship types or properties that are not provided. +Do not respond to any questions that might ask anything else than for you to construct a OpenCypher statement. +Do not include any text except the generated OpenCypher statement, enclosed in triple backticks. +Do not include any explanations or apologies in your responses. +Do not return just the attributes requested in the question, but all related entities, relations, relationships, and attributes. +Do not change the order of the relationships, the arrows should always point from the "start" to the "end". + +The following instructions describe extra functions that can be used in the OpenCypher statement: + +Match: Describes relationships between entities using ASCII art patterns. Entities are represented by parentheses and relationships by brackets. Both can have aliases and labels. +Variable length relationships: Find entities a variable number of hops away using -[:TYPE*minHops..maxHops]->. +Bidirectional path traversal: Specify relationship direction or omit it for either direction. +Named paths: Assign a path in a MATCH clause to a single alias for future use. +Shortest paths: Find all shortest paths between two entities using allShortestPaths(). +Single-Pair minimal-weight paths: Find minimal-weight paths between a pair of entities using algo.SPpaths(). +Single-Source minimal-weight paths: Find minimal-weight paths from a given source entity using algo.SSpaths(). Ontology: {ontology} -Example: -Question: "Which managers own technology stocks?" -Expected Output: "MATCH (m:Manager)-[:OWNS]->(s:Stock) -WHERE toLower(s.sector) CONTAINS 'technology' -RETURN m, s" - -Simple Entity Query Example: -Question: "Apple" or "Show me Apple" -Expected Output: "MATCH (c:Company) -WHERE toLower(c.name) = 'apple' -OPTIONAL MATCH (c)-[r]-(connected) -RETURN c, r, connected" -""" +For example, given the question "Which managers own Neo4j stocks?", the OpenCypher statement should look like this: +MATCH (m:Manager)-[:OWNS]->(s:Stock) +WHERE s.name CONTAINS 'Neo4j' +RETURN m, s""" CYPHER_GEN_PROMPT = """ -Generate an OpenCypher statement to answer the following question using the provided ontology. - -Requirements: -- Use only entities, relationships, and properties from the ontology -- Maintain correct relationship directions (source -> target) -- Return all relevant entities and relationships, not just requested attributes -- Ensure syntactically valid OpenCypher +Using the ontology provided, generate an OpenCypher statement to query the graph database returning all relevant entities, relationships, and attributes to answer the question below. +If you cannot generate a OpenCypher statement for any reason, return an empty response. +Respect the order of the relationships, the arrows should always point from the "source" to the "target". +Please think if your answer is a valid Cypher query, and correct it if it is not. Question: {question} - -Validation Steps: -1. Identify required entities and relationships from the question -2. Verify all components exist in the ontology -3. Construct query with proper syntax and direction -4. Ensure comprehensive result set - -Generated OpenCypher:""" +Your generated Cypher: """ CYPHER_GEN_PROMPT_WITH_ERROR = """ @@ -496,31 +460,17 @@ """ CYPHER_GEN_PROMPT_WITH_HISTORY = """ -Generate an OpenCypher statement to answer the following question using the provided ontology. +Using the ontology provided, generate an OpenCypher statement to query the graph database returning all relevant entities, relationships, and attributes to answer the question below. -Context Analysis: -- First, determine if the last answer provided is relevant to the current question -- If relevant, consider incorporating information from it into the query (e.g., entity IDs, specific names, or context) -- If not relevant, ignore the previous answer and generate the query based solely on the current question +First, determine if the last answers provided to the user over the entire conversation is relevant to the current question. If it is relevant, you may consider incorporating information from it into the query. If it is not relevant, ignore it and generate the query based solely on the question. -Requirements: -- Use only entities, relationships, and properties from the ontology -- Maintain correct relationship directions (source -> target) -- Return all relevant entities and relationships, not just requested attributes -- Ensure syntactically valid OpenCypher -- Apply name normalization (use toLower() for name comparisons) +If you cannot generate an OpenCypher statement for any reason, return an empty string. + +Respect the order of the relationships; the arrows should always point from the "source" to the "target". Last Answer: {last_answer} Question: {question} - -Validation Steps: -1. Assess relevance of last answer to current question -2. Identify required entities and relationships from the question -3. Verify all components exist in the ontology -4. Construct query with proper syntax and direction -5. Ensure comprehensive result set - -Generated OpenCypher:""" +Your generated Cypher: """ GRAPH_QA_SYSTEM = """ You are an assistant that helps to form nice and human understandable answers. diff --git a/graphrag_sdk/helpers.py b/graphrag_sdk/helpers.py index b6f494d4..8c7693fe 100644 --- a/graphrag_sdk/helpers.py +++ b/graphrag_sdk/helpers.py @@ -221,60 +221,50 @@ def validate_cypher_relation_directions( list[str]: A list of errors if relation directions are incorrect. """ errors = [] - - # Pattern to match complete relationship patterns: (node)-[rel]->(node) or (node)<-[rel]-(node) - # This handles both directions and various node/relationship syntaxes - relationship_pattern = r'\(([^)]*)\)\s*(?)\s*\(([^)]*)\)' - - matches = re.finditer(relationship_pattern, cypher, re.IGNORECASE) - - for match in matches: + relations = list(re.finditer(r"\[.*?\]", cypher)) + i = 0 + for relation in relations: try: - source_node = match.group(1).strip() - left_arrow = match.group(2) # '<' if present - relation_content = match.group(3).strip() - right_arrow = match.group(4) # '>' if present - target_node = match.group(5).strip() - - # Skip if no relationship label found - if not relation_content or relation_content == '': - continue - - # Extract relationship label from content like "r:RELATIONSHIP_TYPE" or ":RELATIONSHIP_TYPE" - relation_label_match = re.search(r':\s*([A-Za-z_][A-Za-z0-9_]*)', relation_content) - if not relation_label_match: - continue - - relation_label = relation_label_match.group(1).strip() - - # Determine direction: <- means reverse, -> means forward, -- means undirected - is_directed_left = bool(left_arrow) # <- - is_directed_right = bool(right_arrow) # -> - - # Skip undirected relationships (--) as they don't have direction constraints - if not is_directed_left and not is_directed_right: - continue - - # Extract node labels from source and target - source_label = _extract_node_label(source_node) - target_label = _extract_node_label(target_node) - - # Skip if we can't extract labels (e.g., variables without labels) - if not source_label or not target_label: + relation_label = ( + re.search(r"(?:\[)(?:\w)*(?:\:)([^\d\{\]\*\.\:]+)", relation.group(0)) + .group(1) + .strip() + ) + prev_relation = relations[i - 1] if i > 0 else None + next_relation = relations[i + 1] if i < len(relations) - 1 else None + before = ( + cypher[prev_relation.end() : relation.start()] + if prev_relation + else cypher[: relation.start()] + ) + if "," in before: + before = before.split(",")[-1] + rel_before = re.search(r"([^\)\],]+)", before[::-1]).group(0)[::-1] + after = ( + cypher[relation.end() : next_relation.start()] + if next_relation + else cypher[relation.end() :] + ) + rel_after = re.search(r"([^\(\[,]+)", after).group(0) + entity_before = re.search(r"\(.+:(.*?)\)", before).group(0) + entity_after = re.search(r"\(([^\),]+)(\)?)", after).group(0) + if rel_before == "-" and rel_after == "->": + source = entity_before + target = entity_after + elif rel_before == "<-" and rel_after == "-": + source = entity_after + target = entity_before + else: continue - - # If direction is left (<-), swap source and target - if is_directed_left and not is_directed_right: - source_label, target_label = target_label, source_label - - # Get ontology relations with this label + + source_label = re.search(r"(?:\:)([^\)\{]+)", source).group(1).strip() + target_label = re.search(r"(?:\:)([^\)\{]+)", target).group(1).strip() + ontology_relations = ontology.get_relations_with_label(relation_label) - + if len(ontology_relations) == 0: - # This error is already handled by validate_cypher_relations_exist - continue - - # Check if any ontology relation matches the direction + errors.append(f"Relation {relation_label} not found in ontology") + found_relation = False for ontology_relation in ontology_relations: if ( @@ -283,31 +273,23 @@ def validate_cypher_relation_directions( ): found_relation = True break - + if not found_relation: errors.append( - f"Relation '{relation_label}' does not connect {source_label} to {target_label}. " - f"Make sure the relation direction is correct. " - f"Valid relations: {', '.join([str(r) for r in ontology_relations])}" + """ + Relation {relation_label} does not connect {source_label} to {target_label}. Make sure the relation direction is correct. + Valid relations: + {valid_relations} +""".format( + relation_label=relation_label, + source_label=source_label, + target_label=target_label, + valid_relations="\n".join([str(e) for e in ontology_relations]), + ) ) - - except Exception as e: - # Skip problematic patterns rather than failing + + i += 1 + except Exception: continue - - return errors -def _extract_node_label(node_content: str) -> str: - """ - Extract node label from node content like 'n:Person' or ':Person' or 'person:Person'. - Returns the label or empty string if not found. - """ - if not node_content: - return "" - - # Look for pattern like ":Label" in the node content - label_match = re.search(r':\s*([A-Za-z_][A-Za-z0-9_]*)', node_content) - if label_match: - return label_match.group(1).strip() - - return "" + return errors From 14d3e273fdb57725cb7bf65405f7c8179401cfe6 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 18:02:16 +0300 Subject: [PATCH 8/9] fix-doc --- graphrag_sdk/chat_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index 36bbb364..0ce758e9 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -23,7 +23,7 @@ class ChatSession: >>> from graphrag_sdk.model_config import KnowledgeGraphModelConfig >>> model_config = KnowledgeGraphModelConfig.with_model(model) >>> kg = KnowledgeGraph("test_kg", model_config, ontology) - >>> session = kg.start_chat() + >>> session = kg.chat_session() >>> >>> # Full QA pipeline >>> response = session.send_message("What is the capital of France?") From 5feec7a7d7cad29374876e6e3a509aeeba3f3538 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Mon, 7 Jul 2025 18:09:49 +0300 Subject: [PATCH 9/9] fix-cofp-func --- graphrag_sdk/chat_session.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/graphrag_sdk/chat_session.py b/graphrag_sdk/chat_session.py index 0ce758e9..c0c70983 100644 --- a/graphrag_sdk/chat_session.py +++ b/graphrag_sdk/chat_session.py @@ -130,7 +130,7 @@ def generate_cypher_query(self, message: str) -> tuple: try: (context, cypher, query_execution_time) = self.cypher_step.run(message) - except Exception as e: + except Exception: # If there's an error, return empty context and cypher with error message context = None cypher = CYPHER_ERROR_RES @@ -211,29 +211,29 @@ def send_message_stream(self, message: str) -> Iterator[str]: self._update_last_complete_response(final_response) -def clean_ontology_for_prompt(ontology: dict) -> str: +def clean_ontology_for_prompt(ontology: Ontology) -> str: """ Cleans the ontology by removing 'unique' and 'required' keys and prepares it for use in a prompt. Args: - ontology (dict): The ontology to clean and transform. + ontology (Ontology): The ontology to clean and transform. Returns: str: The cleaned ontology as a JSON string. """ # Convert the ontology object to a JSON. - ontology = ontology.to_json() + ontology_json = ontology.to_json() # Remove unique and required attributes from the ontology. - for entity in ontology["entities"]: + for entity in ontology_json.get("entities", []): for attribute in entity["attributes"]: - del attribute['unique'] - del attribute['required'] + attribute.pop('unique', None) + attribute.pop('required', None) - for relation in ontology["relations"]: + for relation in ontology_json.get("relations", []): for attribute in relation["attributes"]: - del attribute['unique'] - del attribute['required'] + attribute.pop('unique', None) + attribute.pop('required', None) # Return the transformed ontology as a JSON string - return json.dumps(ontology) \ No newline at end of file + return json.dumps(ontology_json) \ No newline at end of file