diff --git a/CHANGELOG.md b/CHANGELOG.md index 776e1ab..0d9d2d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## [1.3.0-beta.1](https://github.com/EURAC-EEBgroup/brick-llm/compare/v1.2.0...v1.3.0-beta.1) (2025-01-02) + + +### Features + +* get_sensor implementation ([e100a88](https://github.com/EURAC-EEBgroup/brick-llm/commit/e100a886acb536077d4ec69bae07f7f84626178d)) +* get_sensor_presence edge ([b7a3e59](https://github.com/EURAC-EEBgroup/brick-llm/commit/b7a3e597e107844fd484068111037756d25d1cd4)) +* implementing ollama 3.2 through ChatOllama models ([bf3cf6b](https://github.com/EURAC-EEBgroup/brick-llm/commit/bf3cf6bab72add3f449a3932d51e360a76c18da3)) + + +### Bug Fixes + +* extract answer content in local generation ([a8f7e39](https://github.com/EURAC-EEBgroup/brick-llm/commit/a8f7e3954c25b1d2584a40c7329a64f9b77f199f)) +* poetry run pre-commit ([edf4664](https://github.com/EURAC-EEBgroup/brick-llm/commit/edf4664deda8a8faaa4e0b1d5ce92276f2bea137)) +* prompt correction ([cf5babd](https://github.com/EURAC-EEBgroup/brick-llm/commit/cf5babd71b703f4df2764b7e410524923df8253b)) +* prompt correction ([3e8d61a](https://github.com/EURAC-EEBgroup/brick-llm/commit/3e8d61a9e008e8e4f24153ab31bf781f910a6cd0)) +* prompt engineering ([a7ae3bc](https://github.com/EURAC-EEBgroup/brick-llm/commit/a7ae3bcfbf69fa369ef7a0a21c39596aa9c13791)) + + +### chore + +* added missing dependencies and linting ([4e4169c](https://github.com/EURAC-EEBgroup/brick-llm/commit/4e4169c053f48a3bd2bf3252a052c64565558ae2)) + + +### Docs + +* added env note ([23c57e2](https://github.com/EURAC-EEBgroup/brick-llm/commit/23c57e249931c71130ae8d1b880bee1950b1501d)) + ## [1.2.0](https://github.com/EURAC-EEBgroup/brick-llm/compare/v1.1.2...v1.2.0) (2024-11-11) diff --git a/README.md b/README.md index 655f806..05efe41 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ pip install poetry poetry install # Install pre-commit hooks -pre-commit install +poetry runpre-commit install ``` @@ -52,7 +52,7 @@ pre-commit install Here's a simple example of how to use BrickLLM: -> [!NOTE] +> [!NOTE] > You must first create a [.env](.env.example) file with the API keys of the specified LLM provider (if not local) and load them in the environment ``` python diff --git a/brickllm/__init__.py b/brickllm/__init__.py index 748d6b9..1b4eda7 100644 --- a/brickllm/__init__.py +++ b/brickllm/__init__.py @@ -3,6 +3,7 @@ from .schemas import ( ElemListSchema, RelationshipsSchema, + SensorSchema, TTLSchema, TTLToBuildingPromptSchema, ) @@ -17,4 +18,5 @@ "StateLocal", "GraphConfig", "custom_logger", + "SensorSchema", ] diff --git a/brickllm/edges/__init__.py b/brickllm/edges/__init__.py index 2778969..0a63108 100644 --- a/brickllm/edges/__init__.py +++ b/brickllm/edges/__init__.py @@ -1,4 +1,5 @@ +from .check_sensor_presence import check_sensor_presence from .validate_condition import validate_condition from .validate_condition_local import validate_condition_local -__all__ = ["validate_condition", "validate_condition_local"] +__all__ = ["validate_condition", "validate_condition_local", "check_sensor_presence"] diff --git a/brickllm/edges/check_sensor_presence.py b/brickllm/edges/check_sensor_presence.py new file mode 100644 index 0000000..28c894b --- /dev/null +++ b/brickllm/edges/check_sensor_presence.py @@ -0,0 +1,42 @@ +from typing import Any, Dict, Literal + +from ..logger import custom_logger +from ..utils import get_hierarchical_info + + +def check_sensor_presence( + state: Dict[str, Any] +) -> Literal["get_sensors", "schema_to_ttl"]: + """ + Check if the sensors are present in the building structure. + + Args: + state (Dict[str, Any]): The current state containing the sensor structure. + + Returns: + Literal["get_sensors", "schema_to_ttl"]: The next node to visit. + """ + + custom_logger.eurac("📡 Checking for sensor presence") + + elem_list = state.get("elem_list", []) + + parents, children = get_hierarchical_info("Point") + + children_dict = {} + for child in children: + children_dict[child] = get_hierarchical_info(child)[1] + + # Flatten the dictionary in a list + children_list = [elem for sublist in children_dict.values() for elem in sublist] + + is_sensor = False + + for elem in elem_list: + if elem in children_list: + is_sensor = True + + if is_sensor: + return "get_sensors" + else: + return "schema_to_ttl" diff --git a/brickllm/graphs/brickschema_graph.py b/brickllm/graphs/brickschema_graph.py index 1c9c960..bfd9c95 100644 --- a/brickllm/graphs/brickschema_graph.py +++ b/brickllm/graphs/brickschema_graph.py @@ -4,7 +4,7 @@ from langgraph.graph import END, START, StateGraph from .. import GraphConfig, State -from ..edges import validate_condition +from ..edges import check_sensor_presence, validate_condition from ..nodes import ( get_elem_children, get_elements, @@ -30,6 +30,7 @@ def build_graph(self): self.workflow.add_node("get_elem_children", get_elem_children) self.workflow.add_node("get_relationships", get_relationships) self.workflow.add_node("schema_to_ttl", schema_to_ttl) + # self.workflow.add_node("sensor_presence", sensor_presence) self.workflow.add_node("validate_schema", validate_schema) self.workflow.add_node("get_sensors", get_sensors) @@ -37,11 +38,15 @@ def build_graph(self): self.workflow.add_edge(START, "get_elements") self.workflow.add_edge("get_elements", "get_elem_children") self.workflow.add_edge("get_elem_children", "get_relationships") - self.workflow.add_edge("get_relationships", "schema_to_ttl") + self.workflow.add_conditional_edges( + "get_relationships", + check_sensor_presence, + {"get_sensors": "get_sensors", "schema_to_ttl": "schema_to_ttl"}, + ) + self.workflow.add_edge("get_sensors", "schema_to_ttl") self.workflow.add_edge("schema_to_ttl", "validate_schema") self.workflow.add_conditional_edges("validate_schema", validate_condition) - self.workflow.add_edge("get_relationships", "get_sensors") - self.workflow.add_edge("get_sensors", END) + self.workflow.add_edge("validate_schema", END) def run( self, input_data: Dict[str, Any], stream: bool = False diff --git a/brickllm/helpers/__init__.py b/brickllm/helpers/__init__.py index 12ec76c..25360fe 100644 --- a/brickllm/helpers/__init__.py +++ b/brickllm/helpers/__init__.py @@ -3,6 +3,7 @@ get_elem_children_instructions, get_elem_instructions, get_relationships_instructions, + get_sensors_instructions, prompt_template_local, schema_to_ttl_instructions, ttl_example, @@ -18,4 +19,5 @@ "ttl_example", "prompt_template_local", "ttl_to_user_prompt", + "get_sensors_instructions", ] diff --git a/brickllm/helpers/llm_models.py b/brickllm/helpers/llm_models.py index ecda551..77e2645 100644 --- a/brickllm/helpers/llm_models.py +++ b/brickllm/helpers/llm_models.py @@ -4,6 +4,7 @@ from langchain_anthropic import ChatAnthropic from langchain_community.llms import Ollama from langchain_fireworks import ChatFireworks +from langchain_ollama import ChatOllama from langchain_openai import ChatOpenAI @@ -23,6 +24,8 @@ def _get_model(model: Union[str, BaseChatModel]) -> BaseChatModel: if model == "openai": return ChatOpenAI(temperature=0, model="gpt-4o") + elif model == "ollama3.2": + return ChatOllama(model="llama3.2") elif model == "anthropic": return ChatAnthropic(temperature=0, model_name="claude-3-sonnet-20240229") elif model == "fireworks": @@ -31,5 +34,10 @@ def _get_model(model: Union[str, BaseChatModel]) -> BaseChatModel: ) elif model == "llama3.1:8b-brick": return Ollama(model="llama3.1:8b-brick-v8") + elif model == "llama32-3B-brick": + return Ollama(model="hf.co/Giudice7/llama32-3B-brick-demo:latest") + else: - raise ValueError(f"Unsupported model type: {model}") + raise ValueError( + f"Unsupported model type: {model}. Load your own BaseChatModel if this one is not supported." + ) diff --git a/brickllm/helpers/prompts.py b/brickllm/helpers/prompts.py index 158342d..eb49d1f 100644 --- a/brickllm/helpers/prompts.py +++ b/brickllm/helpers/prompts.py @@ -1,137 +1,144 @@ -""" -Module containing the prompts used for the LLM models -""" - -get_elem_instructions: str = """ - You are a BrickSchema ontology expert and you are provided with a user prompt which describes a building or facility.\n - You are provided with a list of common elements that can be used to describe a building or facility.\n - You are also provided with the elements description to understand what each element represents.\n - You are now asked to identify the elements presents in the user prompt, even if not explicitly mentioned.\n - USER PROMPT: {prompt} \n - ELEMENTS: {elements_dict} \n - """ # noqa - -get_elem_children_instructions: str = """ - You are a BrickSchema ontology expert and you are provided with a user prompt which describes a building or facility.\n - You are provided with a list of common elements that can be used to describe a building or facility.\n - You are now asked to identify the elements presents in the user prompt.\n - The elements provided are in the format of a hierarchy, - eg: `Sensor -> Position_Sensor, Sensor -> Energy_Sensor`\n - You must include only the elements in the list of common elements provided.\n - DO NOT repeat any elements and DO NOT include "->" in your response.\n - - USER PROMPT: {prompt} \n - ELEMENTS HIERARCHY: {elements_list} \n - """ # noqa - -get_relationships_instructions: str = """ - You are a BrickSchema ontology expert and are provided with a detailed description of a building or facility.\n - You are also provided with a hierarchical structure of identified building components.\n - Your task is to determine the relationships between these components based on the context within the building description and the provided hierarchical structure.\n - The relationships should reflect direct connections or associations as described or implied in the prompt.\n - Each element must be followed by a dot symbol (.) and a number to differentiate between elements of the same type (e.g., Room.1, Room.2).\n - An example of output is the following: [('Building.1', 'Floor.1'), ('Floor.1', 'Room.1'), ('Building.1','Floor.2'), ...]\n - DO NOT add relationships on the output but only the components names, always add first the parent and then the child.\n - If an element has no relationships, add an empty string in place of the missing component ("Room.1","").\n - Hierarchical structure: {building_structure}\n - USER PROMPT: {prompt} -""" # noqa - -ttl_example: str = """ - @prefix bldg: . - @prefix brick: . - @prefix prj: . - - bldg:CO_sensor a brick:CO ; - brick:hasTag bldg:hour ; - brick:hasUnit bldg:PPM ; - brick:isPointOf bldg: ; - brick:timeseries [ brick:hasTimeseriesId bldg:jkj4432uz43 ; - brick:storedAt bldg:example_DB ] . - - bldg:Indoor_humidity a brick:Relative_Humidity_Sensor ; - brick:hasTag bldg:hour ; - brick:hasUnit bldg:PERCENT ; - brick:isPointOf bldg:livingroom ; - brick:timeseries [ brick:hasTimeseriesId bldg:hfrt56478 ; - brick:storedAt bldg:example_DB ] . - - bldg:Indoor_temperature a brick:Air_Temperature_Sensor ; - brick:hasTag bldg:hour ; - brick:hasUnit bldg:DEG_C ; - brick:isPointOf bldg:livingroom ; - brick:timeseries [ brick:hasTimeseriesId bldg:rtg456789 ; - brick:storedAt bldg:example_DB ] . - - bldg:external_temperature a brick:Air_Temperature_Sensor ; - brick:hasTag bldg:hour ; - brick:hasUnit bldg:DEG_C ; - brick:isPointOf bldg:livingroom ; - brick:timeseries [ brick:hasTimeseriesId bldg:art53678 ; - brick:storedAt bldg:example_DB ] . - - bldg:example_db a brick:Database . - - prj:ThermoIot a brick:Site . - - bldg:Milano_Residence_1 a brick:Building ; - brick:buildingPrimaryFunction [ brick:value "Residential" ] ; - brick:hasLocation [ brick:value "Milano" ] ; - brick:isPartOf prj:ThermoIot . - - bldg: a brick:Room ; - brick:isPartOf bldg:Milano_Residence_1 . - - bldg:livingroom a brick:Room ; - brick:isPartOf bldg:Milano_Residence_1 . -""" # noqa - -schema_to_ttl_instructions: str = """ - You are a BrickSchema ontology expert and you are provided with a user prompt which describes a building or facility.\n - You are provided with a dictionary containing the detected components in the building description.\n - You are also provided with the hierarchical structure of the building components with their constraints BrickSchema compliant.\n - Your task is to generate a valid TTL (turtle) script that captures the hierarchy and relationships described in the input.\n - DO NOT add information that are not present in the input.\n - DO NOT add uuids or database id in the TTL if not specified in the prompt.\n - You must keep the enumeration with the hashtag for each component otherwise it will not be possible to recognize the components.\n - The TTL SCRIPT EXAMPLE is useful to understand the overall structure of the output, not the actual content.\n - TTL SCRIPT EXAMPLE: {ttl_example}\n - - COMPONENTS HIERARCHY: {elem_hierarchy}\n - - USER DESCRIPTION: {prompt}\n - - COMPONENTS DICT: {sensors_dict}\n -""" # noqa - -ttl_to_user_prompt: str = """ - You are a BrickSchema ontology expert tasked with generating a clear and concise description of a building or facility from a TTL script. - - Your output must follow these guidelines: - - Focus on the key building characteristics, components and relationships present in the TTL - - Maintain technical accuracy and use proper Brick terminology - - Keep descriptions clear and well-structured - - Only include information explicitly stated in the TTL script - - If no TTL content is provided, return an empty string - - Eventually, the user can provide additional instructions to help you generate the building description. - - {additional_instructions} - - - TTL script to analyze: - - {ttl_script} - -""" # noqa - -prompt_template_local: str = """ - Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request. - - {instructions} - - ### Input: - {user_prompt} - - ### Response: -""" # noqa +""" +Module containing the prompts used for the LLM models +""" + +get_elem_instructions: str = """ + You are an expert in indentifying semantic elements in a natural language prompt hich describes a building and/or energy systems.\n + You are provided with a dictionary containing the entities of an ontology (ELEMENTS) in a hierarchical way, which can be used to describe the building and/or the energy systems. + You are also provided with the elements description to understand what each element represents.\n + You are now asked to identify the entities of the ENTITIES dictionary presented in the user prompt (USER PROMPT), choosing the most specific one if it is possible among the ones provided. Return the entities of ENTITIES (with the proper underscores) presented in the USER PROMPT.\n + USER PROMPT: {prompt} \n + ENTITIES: {elements_dict} \n + """ # noqa + +get_elem_children_instructions: str = """ + You are a semantic ontology expert and you are provided with a user prompt (USER PROMPT) which describes a building and/or energy systems.\n + You are provided with a list of common elements organized in a hierarchy (ELEMENTS HIERARCHY).\n + You are now asked to identify the elements in the hierarchy presents in the user prompt.\n + The elements provided are in the format of a hierarchy, + eg: `Sensor -> Position_Sensor, Sensor -> Energy_Sensor`\n + You must include only the elements in the list of common elements provided.\n + DO NOT repeat any elements and DO NOT include "->" in your response.\n + + USER PROMPT: {prompt} \n + ELEMENTS HIERARCHY: {elements_list} \n + """ # noqa + +get_relationships_instructions: str = """ + You are a semantic ontology expert and you are provided with a user prompt (USER PROMPT) that describes a building and/or energy systems.\n + You are also provided with a hierarchical structure (HIERARCHICAL STRUCTURE) of the identified building or energy systems components in the prompt.\n + Your task is to determine the relationships between these components based on the context within the building description and the provided hierarchical structure.\n + The relationships should reflect direct connections or associations as described or implied in the prompt.\n + Each element must be followed by a dot symbol (.) and a number to differentiate between elements of the same type (e.g., Room.1, Room.2).\n + An example of output is the following: [('Building.1', 'Floor.1'), ('Floor.1', 'Room.1'), ('Building.1','Floor.2'), ...]\n + DO NOT add relationships on the output but only the components names, always add first the parent and then the child.\n + If an element has no relationships, add an empty string in place of the missing component ("Room.1","").\n + HIERARCHICAL STRUCTURE: {building_structure}\n + USER PROMPT: {prompt} +""" # noqa + + +get_sensors_instructions: str = """ + You are an expert in identifying information in a natural language prompt (USER PROMPT) that describes a building and/or energy systems.\n + Your task is to map information about sensors in the building into the provided hierarchical sensor structure (HIERARCHICAL SENSOR STRUCTURE).\n + You must look in the USER PROMPT for finding the UUID of the sensors and their unit of measures, if provided. If these information ar not provided in the USER PROMPT, return the HIERARCHICAL SENSOR STRUCTURE as it is.\n + The UUID of the sensors may be explicitly provided in the USER PROMPT or may be inferred from the context (they may be in parentheses or brackets).\n + To encode the unit of measures, use the names defined by the QUDT ontology.\n + Complete the HIERARCHICAL SENSOR STRUCTURE with the "uuid" and "unit" fields for each sensor, if provided in the USER PROMPT.\n + Remember, only provide units and ID if explicitly provided in the user prompt! If those information are not provided, return the dictionary with the empty field. + USER PROMPT: {prompt} + HIERARCHICAL SENSOR STRUCTURE: {sensor_structure} +""" + +ttl_example: str = """ + @prefix bldg: . + @prefix brick: . + @prefix unit: . + @prefix ref: . + @prefix xsd: . + + bldg:CO_sensor a brick:CO ; + brick:hasUnit unit:PPM ; + brick:isPointOf bldg:Milano_Residence_1 ; + ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId 'dvfs-dfwde-gaw'^^xsd:string ; ref:storedAt bldg:example_db ] + + bldg:Indoor_humidity a brick:Relative_Humidity_Sensor ; + brick:hasUnit unit:PERCENT ; + brick:isPointOf bldg:livingroom ; + ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId '23rs-432a-63cv'^^xsd:string ; + ref:storedAt bldg:example_db ] . + + bldg:Indoor_temperature a brick:Air_Temperature_Sensor ; + brick:hasUnit unit:DEG_C ; + brick:isPointOf bldg:livingroom ; + ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId 'rtg456789'^^xsd:string ; + ref:storedAt bldg:example_db ] . + + bldg:external_temperature a brick:Air_Temperature_Sensor ; + brick:hasUnit unit:DEG_C ; + brick:isPointOf bldg:livingroom ; + ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId 'art53678^^xsd:string' ; + ref:storedAt bldg:example_db ] . + + bldg:example_db a brick:Database . + + bldg:Milano_Residence_1 a brick:Building ; + brick:hasLocation [ brick:value "Milano"^^xsd:string ] . + + bldg: a brick:Room ; + brick:isPartOf bldg:Milano_Residence_1 . + + bldg:livingroom a brick:Room ; + brick:isPartOf bldg:Milano_Residence_1 . +""" # noqa + +schema_to_ttl_instructions: str = """ + You are an expert in generating ontology-based RDF graph from a user prompt, which describes a building or energy systems.\n + You are provided with a dictionary containing the hierarchy of the building/energy systems components (COMPONENTS HIERARCHY) detected in the user prompt (USER PROMP).\n + You are also provided with the list of the sensors (SENSOR LIST) identified in the user prompts, with additional information about uuid and unit of measures, if avaiable. + Your task is to generate a RDF graph in Turtle format that is compliant with the hierarchy and relationships described in the input. Use only the elements identified in the COMPONENTS HIERARCHY and SENSOR LIST, connecting each entities with the appropriate properties (presented in each element of the hierarchy).\n + DO NOT add information that are not present in the input.\n + To encode the uuid of the sensors, use the following schema: 'sensor' ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId 'uuid'^^xsd:string .].\n + To encode the unit of measure of the sensor, use the following schema: 'sensor' brick:hasUnit unit:UNIT, where unit is the @prefix of the unit ontology (@prefix unit: .).\n + Include all the @prefix declarations at the beginning of the output Turtle file.\n + I provide you an example of the output Turtle: the TTL SCRIPT EXAMPLE is useful to understand the overall structure of the output, not the actual content. Do not copy any information from this example.\n + TTL SCRIPT EXAMPLE: {ttl_example}\n + + COMPONENTS HIERARCHY: {elem_hierarchy}\n + + USER PROMPT: {prompt}\n + + SENSOR LIST: {uuid_list}\n +""" # noqa + +ttl_to_user_prompt: str = """ + You are a BrickSchema ontology expert tasked with generating a clear and concise description of a building or facility from a TTL script. + + Your output must follow these guidelines: + - Focus on the key building characteristics, components and relationships present in the TTL + - Maintain technical accuracy and use proper Brick terminology + - Keep descriptions clear and well-structured + - Only include information explicitly stated in the TTL script + - If no TTL content is provided, return an empty string + + Eventually, the user can provide additional instructions to help you generate the building description. + + {additional_instructions} + + + TTL script to analyze: + + {ttl_script} + +""" # noqa + +prompt_template_local: str = """ + Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request. + + {instructions} + + ### Input: + {user_prompt} + + ### Response: +""" # noqa diff --git a/brickllm/nodes/generation_local.py b/brickllm/nodes/generation_local.py index 7a8a8e4..d35e5f9 100644 --- a/brickllm/nodes/generation_local.py +++ b/brickllm/nodes/generation_local.py @@ -30,6 +30,6 @@ def generation_local(state: StateLocal, config: Dict[str, Any]) -> Dict[str, Any ) answer = llm.invoke(message) - ttl_output = extract_rdf_graph(answer) + ttl_output = extract_rdf_graph(answer.content) return {"ttl_output": ttl_output} diff --git a/brickllm/nodes/get_relationships.py b/brickllm/nodes/get_relationships.py index 70fb157..c03162c 100644 --- a/brickllm/nodes/get_relationships.py +++ b/brickllm/nodes/get_relationships.py @@ -1,5 +1,4 @@ import json -from collections import defaultdict from typing import Any, Dict from langchain_core.messages import HumanMessage, SystemMessage @@ -47,14 +46,21 @@ def get_relationships(state: State, config: Dict[str, Any]) -> Dict[str, Any]: try: tree_dict = build_hierarchy(answer.relationships) - except Exception as e: - print(f"Error building the hierarchy: {e}") + except Exception: + custom_logger.warning("Error building the hierarchy. Trying again.") # Group sensors by their paths - sensor_paths = find_sensor_paths(tree_dict) - grouped_sensors = defaultdict(list) + sensor_paths = [] + for root_node in tree_dict: + sensor_paths.extend(find_sensor_paths(tree_dict[root_node])) + + grouped_sensors = {} + for sensor in sensor_paths: - grouped_sensors[sensor["path"]].append(sensor["name"]) - grouped_sensor_dict = dict(grouped_sensors) + grouped_sensors[sensor["name"]] = { + "name": sensor["name"], + "uuid": None, + "unit": None, + } - return {"sensors_dict": grouped_sensor_dict} + return {"rel_tree": tree_dict, "sensors_dict": grouped_sensors} diff --git a/brickllm/nodes/get_sensors.py b/brickllm/nodes/get_sensors.py index ac6c207..f4b7349 100644 --- a/brickllm/nodes/get_sensors.py +++ b/brickllm/nodes/get_sensors.py @@ -1,41 +1,43 @@ +import json from typing import Any, Dict -from .. import State +from langchain_core.messages import HumanMessage, SystemMessage + +from .. import SensorSchema, State +from ..helpers import get_sensors_instructions from ..logger import custom_logger -def get_sensors(state: State) -> Dict[str, Any]: +def get_sensors(state: State, config: Dict[str, Any]) -> Dict[str, Any]: """ Retrieve sensor information for the building structure. Args: state (State): The current state. - + config (dict): Configuration dictionary containing the language model. Returns: dict: A dictionary containing sensor UUIDs mapped to their locations. """ custom_logger.eurac("📡 Getting sensors information") - uuid_dict = { - "Building#1>Floor#1>Office#1>Room#1": [ - { - "name": "Temperature_Sensor#1", - "uuid": "aaaa-bbbb-cccc-dddd", - }, - { - "name": "Humidity_Sensor#1", - "uuid": "aaaa-bbbb-cccc-dddd", - }, - ], - "Building#1>Floor#1>Office#1>Room#2": [ - { - "name": "Temperature_Sensor#2", - "uuid": "aaaa-bbbb-cccc-dddd", - }, - { - "name": "Humidity_Sensor#2", - "uuid": "aaaa-bbbb-cccc-dddd", - }, - ], - } - return {"uuid_dict": uuid_dict} + user_prompt = state["user_prompt"] + sensor_structure = state["sensors_dict"] + sensor_structure_json = json.dumps(sensor_structure, indent=2) + + # Get the model name from the config + llm = config.get("configurable", {}).get("llm_model") + + # Enforce structured output + structured_llm = llm.with_structured_output(SensorSchema) + # System message + system_message = get_sensors_instructions.format( + prompt=user_prompt, sensor_structure=sensor_structure_json + ) + + # Generate question + answer = structured_llm.invoke( + [SystemMessage(content=system_message)] + + [HumanMessage(content="Complete the sensor structure.")] + ) + + return {"uuid_list": answer.sensors} diff --git a/brickllm/nodes/schema_to_ttl.py b/brickllm/nodes/schema_to_ttl.py index f9486b1..078bd4a 100644 --- a/brickllm/nodes/schema_to_ttl.py +++ b/brickllm/nodes/schema_to_ttl.py @@ -22,10 +22,13 @@ def schema_to_ttl(state: State, config: Dict[str, Any]) -> Dict[str, Any]: custom_logger.eurac("📝 Generating TTL from schema") user_prompt = state["user_prompt"] - sensors_dict = state["sensors_dict"] + try: + sensors_dict = state["uuid_list"] + except KeyError: + sensors_dict = [] elem_hierarchy = state["elem_hierarchy"] - sensors_dict_json = json.dumps(sensors_dict, indent=2) + # sensors_dict_json = json.dumps(sensors_dict, indent=2) elem_hierarchy_json = json.dumps(elem_hierarchy, indent=2) # Get the model name from the config @@ -37,7 +40,7 @@ def schema_to_ttl(state: State, config: Dict[str, Any]) -> Dict[str, Any]: # System message system_message = schema_to_ttl_instructions.format( prompt=user_prompt, - sensors_dict=sensors_dict_json, + uuid_list=sensors_dict, elem_hierarchy=elem_hierarchy_json, ttl_example=ttl_example, ) diff --git a/brickllm/schemas.py b/brickllm/schemas.py index 5ba06b4..663cc4c 100644 --- a/brickllm/schemas.py +++ b/brickllm/schemas.py @@ -1,6 +1,6 @@ -from typing import List, Tuple +from typing import List, Optional, Tuple -from pydantic.v1 import BaseModel, Field +from pydantic import BaseModel, Field # pydantic schemas @@ -12,6 +12,16 @@ class RelationshipsSchema(BaseModel): relationships: List[Tuple[str, ...]] +class Sensor(BaseModel): + name: str = Field("name of the sensor") + uuid: Optional[str] = Field("Identifier of the sensor") + unit: Optional[str] = Field("Unit of measure of the sensor") + + +class SensorSchema(BaseModel): + sensors: List[Sensor] = Field("List of the sensors") + + class TTLSchema(BaseModel): ttl_output: str = Field( ..., description="The generated BrickSchema turtle/rdf script." diff --git a/brickllm/states.py b/brickllm/states.py index f36074b..acc49ca 100644 --- a/brickllm/states.py +++ b/brickllm/states.py @@ -2,6 +2,8 @@ from typing_extensions import TypedDict +from .schemas import Sensor + # state for BrickSchemaGraph class class State(TypedDict): @@ -10,12 +12,13 @@ class State(TypedDict): # elem_children_list: List[str] elem_hierarchy: Dict[str, Any] # relationships: List[Tuple[str, str]] - # rel_tree: Dict[str, Any] + rel_tree: Dict[str, Any] sensors_dict: Dict[str, List[str]] + is_sensor: bool is_valid: bool validation_report: str validation_max_iter: int - uuid_dict: Dict[str, Any] + uuid_list: List[Sensor] ttl_output: str diff --git a/brickllm/utils/get_hierarchy_info.py b/brickllm/utils/get_hierarchy_info.py index 307e1eb..86a5853 100644 --- a/brickllm/utils/get_hierarchy_info.py +++ b/brickllm/utils/get_hierarchy_info.py @@ -292,11 +292,12 @@ def build_tree(node: str, tree_dict: Dict[str, List[str]]) -> Dict[str, Any]: } if not root_candidates: raise ValueError("No root found in relationships") - root = next(iter(root_candidates)) - # Build the hierarchical structure starting from the root - hierarchy = build_tree(root, tree_dict) - return hierarchy + hierarchy_dict = {} + for root in root_candidates: + # Build the hierarchical structure starting from the root + hierarchy_dict[root] = build_tree(root, tree_dict) + return hierarchy_dict def extract_ttl_content(input_string: str) -> str: diff --git a/brickllm/utils/query_brickschema.py b/brickllm/utils/query_brickschema.py index 30a92d1..90c83ed 100644 --- a/brickllm/utils/query_brickschema.py +++ b/brickllm/utils/query_brickschema.py @@ -228,12 +228,12 @@ def validate_ttl(ttl_file: str, method: str = "pyshacl") -> Tuple[bool, str]: output_graph, shacl_graph=g, ont_graph=g, - inference="rdfs", + inference="both", abort_on_first=False, - allow_infos=False, - allow_warnings=False, + allow_infos=True, + allow_warnings=True, meta_shacl=False, - advanced=False, + advanced=True, js=False, debug=False, ) diff --git a/examples/example_custom_llm.py b/examples/example_custom_llm.py index 6fa0231..5b21aee 100644 --- a/examples/example_custom_llm.py +++ b/examples/example_custom_llm.py @@ -1,6 +1,5 @@ from dotenv import load_dotenv from langchain_openai import ChatOpenAI - from brickllm.graphs import BrickSchemaGraph # Load environment variables @@ -13,7 +12,7 @@ There are 2 rooms in each office and each room has three sensors: - Temperature sensor; - Humidity sensor; -- CO sensor. +- CO2 sensor. """ # Create an instance of BrickSchemaGraph with a custom model diff --git a/examples/example_openai.py b/examples/example_openai.py index d70c0b9..d2fa9e8 100644 --- a/examples/example_openai.py +++ b/examples/example_openai.py @@ -12,14 +12,14 @@ There are 2 rooms in each office and each room has three sensors: - Temperature sensor; - Humidity sensor; -- CO sensor. +- CO2 sensor. """ # Create an instance of BrickSchemaGraph with a predefined provider brick_graph = BrickSchemaGraph(model="openai") # Display the graph structure -brick_graph.display(file_name="graph_openai.png") +brick_graph.display(filename="graph_openai.png") # Prepare input data input_data = {"user_prompt": building_description} diff --git a/examples/my_building.ttl b/examples/my_building.ttl index e3497bb..686b5e3 100644 --- a/examples/my_building.ttl +++ b/examples/my_building.ttl @@ -1,8 +1,11 @@ @prefix bldg: . @prefix brick: . +@prefix unit: . +@prefix ref: . +@prefix xsd: . bldg:Bolzano_Building a brick:Building ; - brick:hasLocation [ brick:value "Bolzano" ] . + brick:hasLocation [ brick:value "Bolzano"^^xsd:string ] . bldg:Floor_1 a brick:Floor ; brick:isPartOf bldg:Bolzano_Building . @@ -13,83 +16,83 @@ bldg:Floor_2 a brick:Floor ; bldg:Floor_3 a brick:Floor ; brick:isPartOf bldg:Bolzano_Building . -bldg:Office_1 a brick:Office ; +bldg:Office_1_Floor_1 a brick:Room ; brick:isPartOf bldg:Floor_1 . -bldg:Office_2 a brick:Office ; +bldg:Office_1_Floor_2 a brick:Room ; brick:isPartOf bldg:Floor_2 . -bldg:Office_3 a brick:Office ; +bldg:Office_1_Floor_3 a brick:Room ; brick:isPartOf bldg:Floor_3 . -bldg:Room_1 a brick:Room ; - brick:isPartOf bldg:Office_1 . +bldg:Room_1_Office_1_Floor_1 a brick:Room ; + brick:isPartOf bldg:Office_1_Floor_1 . -bldg:Room_2 a brick:Room ; - brick:isPartOf bldg:Office_1 . +bldg:Room_2_Office_1_Floor_1 a brick:Room ; + brick:isPartOf bldg:Office_1_Floor_1 . -bldg:Room_3 a brick:Room ; - brick:isPartOf bldg:Office_2 . +bldg:Room_1_Office_1_Floor_2 a brick:Room ; + brick:isPartOf bldg:Office_1_Floor_2 . -bldg:Room_4 a brick:Room ; - brick:isPartOf bldg:Office_2 . +bldg:Room_2_Office_1_Floor_2 a brick:Room ; + brick:isPartOf bldg:Office_1_Floor_2 . -bldg:Room_5 a brick:Room ; - brick:isPartOf bldg:Office_3 . +bldg:Room_1_Office_1_Floor_3 a brick:Room ; + brick:isPartOf bldg:Office_1_Floor_3 . -bldg:Room_6 a brick:Room ; - brick:isPartOf bldg:Office_3 . +bldg:Room_2_Office_1_Floor_3 a brick:Room ; + brick:isPartOf bldg:Office_1_Floor_3 . -bldg:Temperature_Sensor_1 a brick:Temperature_Sensor ; - brick:isPointOf bldg:Room_1 . +bldg:Temperature_Sensor_Room_1_Office_1_Floor_1 a brick:Temperature_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_1 . -bldg:Humidity_Sensor_1 a brick:Humidity_Sensor ; - brick:isPointOf bldg:Room_1 . +bldg:Humidity_Sensor_Room_1_Office_1_Floor_1 a brick:Humidity_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_1 . -bldg:CO_Sensor_1 a brick:CO_Sensor ; - brick:isPointOf bldg:Room_1 . +bldg:CO2_Sensor_Room_1_Office_1_Floor_1 a brick:CO2_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_1 . -bldg:Temperature_Sensor_2 a brick:Temperature_Sensor ; - brick:isPointOf bldg:Room_2 . +bldg:Temperature_Sensor_Room_2_Office_1_Floor_1 a brick:Temperature_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_1 . -bldg:Humidity_Sensor_2 a brick:Humidity_Sensor ; - brick:isPointOf bldg:Room_2 . +bldg:Humidity_Sensor_Room_2_Office_1_Floor_1 a brick:Humidity_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_1 . -bldg:CO_Sensor_2 a brick:CO_Sensor ; - brick:isPointOf bldg:Room_2 . +bldg:CO2_Sensor_Room_2_Office_1_Floor_1 a brick:CO2_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_1 . -bldg:Temperature_Sensor_3 a brick:Temperature_Sensor ; - brick:isPointOf bldg:Room_3 . +bldg:Temperature_Sensor_Room_1_Office_1_Floor_2 a brick:Temperature_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_2 . -bldg:Humidity_Sensor_3 a brick:Humidity_Sensor ; - brick:isPointOf bldg:Room_3 . +bldg:Humidity_Sensor_Room_1_Office_1_Floor_2 a brick:Humidity_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_2 . -bldg:CO_Sensor_3 a brick:CO_Sensor ; - brick:isPointOf bldg:Room_3 . +bldg:CO2_Sensor_Room_1_Office_1_Floor_2 a brick:CO2_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_2 . -bldg:Temperature_Sensor_4 a brick:Temperature_Sensor ; - brick:isPointOf bldg:Room_4 . +bldg:Temperature_Sensor_Room_2_Office_1_Floor_2 a brick:Temperature_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_2 . -bldg:Humidity_Sensor_4 a brick:Humidity_Sensor ; - brick:isPointOf bldg:Room_4 . +bldg:Humidity_Sensor_Room_2_Office_1_Floor_2 a brick:Humidity_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_2 . -bldg:CO_Sensor_4 a brick:CO_Sensor ; - brick:isPointOf bldg:Room_4 . +bldg:CO2_Sensor_Room_2_Office_1_Floor_2 a brick:CO2_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_2 . -bldg:Temperature_Sensor_5 a brick:Temperature_Sensor ; - brick:isPointOf bldg:Room_5 . +bldg:Temperature_Sensor_Room_1_Office_1_Floor_3 a brick:Temperature_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_3 . -bldg:Humidity_Sensor_5 a brick:Humidity_Sensor ; - brick:isPointOf bldg:Room_5 . +bldg:Humidity_Sensor_Room_1_Office_1_Floor_3 a brick:Humidity_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_3 . -bldg:CO_Sensor_5 a brick:CO_Sensor ; - brick:isPointOf bldg:Room_5 . +bldg:CO2_Sensor_Room_1_Office_1_Floor_3 a brick:CO2_Sensor ; + brick:isPointOf bldg:Room_1_Office_1_Floor_3 . -bldg:Temperature_Sensor_6 a brick:Temperature_Sensor ; - brick:isPointOf bldg:Room_6 . +bldg:Temperature_Sensor_Room_2_Office_1_Floor_3 a brick:Temperature_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_3 . -bldg:Humidity_Sensor_6 a brick:Humidity_Sensor ; - brick:isPointOf bldg:Room_6 . +bldg:Humidity_Sensor_Room_2_Office_1_Floor_3 a brick:Humidity_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_3 . -bldg:CO_Sensor_6 a brick:CO_Sensor ; - brick:isPointOf bldg:Room_6 . +bldg:CO2_Sensor_Room_2_Office_1_Floor_3 a brick:CO2_Sensor ; + brick:isPointOf bldg:Room_2_Office_1_Floor_3 . diff --git a/pyproject.toml b/pyproject.toml index 4094c06..49484ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "brickllm" -version = "1.2.0" +version = "1.3.0b1" description = "Library for generating RDF files following BrickSchema ontology using LLM" authors = ["Marco Perini ", "Daniele Antonucci "] license = "BSD-3-Clause" @@ -43,10 +43,12 @@ langgraph = "0.2.23" langchain_openai = "0.2.0" langchain-fireworks = "0.2.0" langchain-anthropic = "0.2.1" +langchain-ollama = "^0.2.2" langchain_community = "0.3.0" -langchain_core = "0.3.5" +langchain_core = "^0.3.5" rdflib = ">=6.2.0,<7" pyshacl = "0.21" +python-dotenv = "^1.0.1" [tool.poetry.group.dev.dependencies] pytest = "^7.4"