diff --git a/.env.example b/.env.example index 3cd5f8c..29f941a 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ OPENAI_API_KEY=sk-example-1234567890abcdef1234567890abcdef ANTHROPIC_API_KEY=sk-example-1234567890abcdef1234567890abcdef -FIREWORKS_API_KEY=sk-example-1234567890abcdef1234567890abcdef \ No newline at end of file +FIREWORKS_API_KEY=sk-example-1234567890abcdef1234567890abcdef diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..99e3dcf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,109 @@ +name: 🐛 Bug report +description: Submit a bug report to help us improve BrickLLM. +labels: ["type:bug", "status:needs-triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this problem! + We really appreciate the community's efforts to improve BrickLLM. + + - type: checkboxes + attributes: + label: Checklist + description: Please confirm and check all the following options. + options: + - label: I have searched the [existing issues](https://github.com/EURAC-EEBgroup/brick-llm/issues) for similar issues. + required: true + - label: I added a very descriptive title to this issue. + required: true + - label: I have provided sufficient information below to help reproduce this issue. + required: true + - type: textarea + attributes: + label: Summary + description: Type here a clear and concise description of the problem. Aim for 2-3 sentences. + validations: + required: true + - type: textarea + attributes: + label: Reproducible Code Example + render: Python + description: | + If applicable, please provide a [self-contained minimal code example](https://stackoverflow.com/help/minimal-reproducible-example) that reproduces the problem you ran into. + If we can copy it, run it, and see it right away, there's a much higher chance we will be able to help you. + placeholder: | + import brickllm + + # Your code here + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Please provide the steps we should take to reproduce the bug. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: Explain what you expect to happen when you go through the steps above, assuming there were no bugs. + validations: + required: false + - type: textarea + attributes: + label: Current Behavior + placeholder: | + Error message: + ``` + Traceback (most recent call last): + File "example.py", line 1, in + import brickllm + ``` + description: | + Explain the buggy behavior you experience when you go through the steps above. + If you have error messages or stack traces please provide them here as well. + If applicable, add screenshots to help explain your problem. + + _Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in._ + validations: + required: false + - type: checkboxes + attributes: + label: Is this a regression? + description: Did this use to work the way you expected in the past? + options: + - label: Yes, this used to work in a previous version. + required: false + - type: textarea + attributes: + label: Debug info + description: | + Please share some system information related to the environment your app is running in. + + Example: + - **BrickLLM version**: 0.1.0 _(get it with `$ pip show brickllm`)_ + - **Python version**: 3.9 _(get it with `$ python --version`)_ + - **Operating System**: MacOs 12.6 + - **Browser**: Chrome + value: | + - BrickLLM version: + - Python version: + - Operating System: + - Browser: + validations: + required: false + - type: textarea + attributes: + label: Additional Information + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + _Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in._ + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..1fc718b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,51 @@ +name: ✨ Feature request +description: Suggest a feature or enhancement for BrickLLM. +labels: ["type:enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a feature or enhancement for BrickLLM! + We really appreciate the community's efforts to improve BrickLLM ❤️ + - type: checkboxes + attributes: + label: Checklist + description: Please confirm and check all the following options. + options: + - label: I have searched the [existing issues](https://github.com/EURAC-EEBgroup/brick-llm/issues) for similar feature requests. + required: true + - label: I added a descriptive title and summary to this issue. + required: true + - type: textarea + attributes: + label: Summary + description: Type here a clear and concise description of the feature or enhancement request. Aim for 2-3 sentences. + validations: + required: true + - type: textarea + attributes: + label: Why? + description: Please outline the problem, motivation, or use case related to this feature request. + placeholder: | + I'm always frustrated when ... + validations: + required: false + - type: textarea + attributes: + label: How? + description: | + Please describe the solution or implementation you'd like to see. This might include suggestions for new parameters, UI mockups etc. + Don't worry if you don't have a clear solution in mind; any input helps! + placeholder: | + Introduce a new command called `brickllm new_feature` that ... + validations: + required: false + - type: textarea + attributes: + label: Additional Context + description: | + Links? References? Anything that will give us more context about the feature request here! + + _Tip: You can attach images by clicking this area to highlight it and then dragging files in._ + validations: + required: false diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..908adce --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,120 @@ +name: Release + +on: + push: + branches: + - main + - pre/* + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install Poetry + run: pip install poetry + + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + + - name: Build the package + run: poetry build + + test: + name: Test + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install Poetry + run: pip install poetry + + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + + - name: Run Linting and Formatting Checks + run: | + poetry run ruff check brickllm tests + poetry run black --check brickllm tests + poetry run isort --check-only brickllm tests + + # - name: Run Type Checking + # run: poetry run mypy brickllm tests + + - name: Run Tests with Coverage + run: poetry run pytest --cov=brickllm --cov-report=xml tests/ + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: coverage.xml + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + release: + name: Release + runs-on: ubuntu-latest + needs: [build, test] + if: | + github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/pre/')) || + github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged && + (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'pre/')) + permissions: + contents: write + issues: write + pull-requests: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install Poetry + run: pip install poetry + + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v4.1.0 + with: + semantic_version: 23 + extra_plugins: | + semantic-release-pypi@3 + @semantic-release/git + @semantic-release/commit-analyzer@12 + @semantic-release/release-notes-generator@13 + @semantic-release/github@10 + @semantic-release/changelog@6 + conventional-changelog-conventionalcommits@7 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..beddc32 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,34 @@ +name: Deploy documentation to GitHub Pages + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry install --no-root + + - name: Build documentation + run: | + poetry run mkdocs build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site diff --git a/.gitignore b/.gitignore index 1eeff0b..65892cf 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +.ruff_cache/ cover/ # Translations @@ -147,4 +148,4 @@ cython_debug/ # macOS .DS_Store -dev.ipynb \ No newline at end of file +dev.ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..57e42dd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: + - repo: https://github.com/psf/black + rev: 24.8.0 + hooks: + - id: black + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + exclude: mkdocs.yml diff --git a/.releaserc.yml b/.releaserc.yml new file mode 100644 index 0000000..574eb74 --- /dev/null +++ b/.releaserc.yml @@ -0,0 +1,55 @@ +plugins: + - - "@semantic-release/commit-analyzer" + - preset: conventionalcommits + - - "@semantic-release/release-notes-generator" + - writerOpts: + commitsSort: + - subject + - scope + preset: conventionalcommits + presetConfig: + types: + - type: feat + section: Features + - type: fix + section: Bug Fixes + - type: chore + section: chore + - type: docs + section: Docs + - type: style + hidden: true + - type: refactor + section: Refactor + - type: perf + section: Perf + - type: test + section: Test + - type: build + section: Build + - type: ci + section: CI + - "@semantic-release/changelog" + - "semantic-release-pypi" + - "@semantic-release/github" + - - "@semantic-release/git" + - assets: + - CHANGELOG.md + - pyproject.toml + message: |- + ci(release): ${nextRelease.version} [skip ci] + + ${nextRelease.notes} +branches: + #child branches coming from tagged version for bugfix (1.1.x) or new features (1.x) + #maintenance branch + - name: "+([0-9])?(.{+([0-9]),x}).x" + channel: "stable" + #release a production version when merging towards main + - name: "main" + channel: "stable" + #prerelease branch + - name: "pre/beta" + channel: "dev" + prerelease: "beta" +debug: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc7bb47 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,93 @@ +## [1.1.2](https://github.com/EURAC-EEBgroup/brick-llm/compare/v1.1.1...v1.1.2) (2024-11-05) + + +### Bug Fixes + +* add citation label ([27fc2c6](https://github.com/EURAC-EEBgroup/brick-llm/commit/27fc2c694fb9ed73110ec09b068d5fba5dffe111)) + +## [1.1.1](https://github.com/EURAC-EEBgroup/brick-llm/compare/v1.1.0...v1.1.1) (2024-11-05) + + +### Bug Fixes + +* change for zenodo ([1194a52](https://github.com/EURAC-EEBgroup/brick-llm/commit/1194a528f3a023fa35f10a94d53a331df93b515c)) + +## [1.1.0](https://github.com/EURAC-EEBgroup/brick-llm/compare/v1.0.1...v1.1.0) (2024-11-05) + + +### Features + +* BSD license ([656faf0](https://github.com/EURAC-EEBgroup/brick-llm/commit/656faf01911ccfc66e21c2ff6bda8a77b1dcd35c)) + +## [1.0.1](https://github.com/EURAC-EEBgroup/brick-llm/compare/v1.0.0...v1.0.1) (2024-11-05) + + +### Bug Fixes + +* replaced banner for pypi ([218a60f](https://github.com/EURAC-EEBgroup/brick-llm/commit/218a60f00ddfc7c121e388f1805b75c28bd6200b)) + +## 1.0.0 (2024-11-05) + + +### Features + +* add abstract graph, new state, examples ([ecd7f94](https://github.com/EURAC-EEBgroup/brick-llm/commit/ecd7f940649d1d41ffc354781e277790a01f206f)) +* add makefile ([03aa829](https://github.com/EURAC-EEBgroup/brick-llm/commit/03aa8295cb6251ecbccaf4df63efd19252bd1a4f)) +* add mkdocs ([d38421e](https://github.com/EURAC-EEBgroup/brick-llm/commit/d38421e9808112c3fc7de431de45783a8ce735d6)) +* add validation ([835a6ba](https://github.com/EURAC-EEBgroup/brick-llm/commit/835a6baf19fcf9ab1f7ad21d5b0d21a4c600659a)) +* added custom models ([67cc309](https://github.com/EURAC-EEBgroup/brick-llm/commit/67cc30972b4fee5ad34f252e50881eada85a2df0)) +* added ISSUE_TEMPLATE and ci cd workflows ([5969ec5](https://github.com/EURAC-EEBgroup/brick-llm/commit/5969ec5ad5808dd8f3285fac9c9c9553470be4bd)) +* added pre-commit and tox integration ([25dff8b](https://github.com/EURAC-EEBgroup/brick-llm/commit/25dff8b192b3ef4158a5fddca43ec82ae191ca05)) +* added return types ([f495549](https://github.com/EURAC-EEBgroup/brick-llm/commit/f4955499494082d0073a5982664ac6bfaae9a3a4)) +* BrickSchemaGraphLocal workflow definition ([b80e855](https://github.com/EURAC-EEBgroup/brick-llm/commit/b80e8554389eef596701fde2755b7ce09fe64707)) +* **refactor:** created lib ([980b0e7](https://github.com/EURAC-EEBgroup/brick-llm/commit/980b0e72f16f8e4685279dff8394531f1a818f4a)) +* fine-tuned prompt template ([7a22eb8](https://github.com/EURAC-EEBgroup/brick-llm/commit/7a22eb8d78ded987811d8da9c2daa851ee873d2b)) +* initial release ([8a8f5a1](https://github.com/EURAC-EEBgroup/brick-llm/commit/8a8f5a169639c3524d926daa590d934e4188c279)) +* initial release ([804a19f](https://github.com/EURAC-EEBgroup/brick-llm/commit/804a19fcc935e12ae149e18997614dfe8a7d46c0)) +* initial release ([8f9d7aa](https://github.com/EURAC-EEBgroup/brick-llm/commit/8f9d7aaaaa94f2eb8e93aa2fee724aded11e09da)) +* integration of ollama fine-tuned model ([633bb93](https://github.com/EURAC-EEBgroup/brick-llm/commit/633bb93312fde7ea6fd987fd471690cee705092e)) +* **agent:** langgraph porting ([34f614a](https://github.com/EURAC-EEBgroup/brick-llm/commit/34f614a557afc5b0aae5773b9e0954122c8de30e)) +* local generation node using the fine-tuning ([dd625ca](https://github.com/EURAC-EEBgroup/brick-llm/commit/dd625caa776d8cf987a4a0d6d887c05e31eb82eb)) +* move graph compilation inside class ([e753523](https://github.com/EURAC-EEBgroup/brick-llm/commit/e7535232410a7e81cbbc50a95da8067991ce020a)) +* rdf parser from raw llm response ([740143e](https://github.com/EURAC-EEBgroup/brick-llm/commit/740143ece49cc2c10f97dd956c8b89d4238978aa)) +* update config and states for hosting also the local model instructions ([4b85fc7](https://github.com/EURAC-EEBgroup/brick-llm/commit/4b85fc7d07d48fd287b8b99902f406cdc0fbe417)) +* validate local llm (we need another function since the return node is different ([f228764](https://github.com/EURAC-EEBgroup/brick-llm/commit/f228764b5757c4f1c84714a2aee4321969d144c3)) + + +### Bug Fixes + +* **mermaid:** added correct output image path ([ff42d5a](https://github.com/EURAC-EEBgroup/brick-llm/commit/ff42d5a107c39c60e1ff5be0fa7354b27961e8c5)) +* added StringIO conversion ([5698f49](https://github.com/EURAC-EEBgroup/brick-llm/commit/5698f49be5f1c2c0251f37816f890fb475e5a65e)) +* dockerfile ([68c03bf](https://github.com/EURAC-EEBgroup/brick-llm/commit/68c03bfbf905ed101f025f3600fa6135d43c85c0)) +* fixed how we pass llm in graph ([eaa1c03](https://github.com/EURAC-EEBgroup/brick-llm/commit/eaa1c03fe6641d4d12a0cc6e53c2061c34eccda4)) +* fixed imports ([2e3ca0f](https://github.com/EURAC-EEBgroup/brick-llm/commit/2e3ca0f132e721641cd973614696985d70155bb0)) +* moved env variable loading inside class ([3911bd9](https://github.com/EURAC-EEBgroup/brick-llm/commit/3911bd900120a49dd1a8c34e0eb36964544442ac)) +* pre-defined namespace ([9ad2a09](https://github.com/EURAC-EEBgroup/brick-llm/commit/9ad2a09c453bfca0de983c7d6b3a1624f263506d)) +* problem in iteration loop ([eac5cd6](https://github.com/EURAC-EEBgroup/brick-llm/commit/eac5cd617dec77af76e497f5a2b879cb729a8259)) +* removed cache ([5a17fdd](https://github.com/EURAC-EEBgroup/brick-llm/commit/5a17fdd16500d402fd95ea94d31b2e0c5d98d690)) +* replace '#' with '.' in prompts ([32d9923](https://github.com/EURAC-EEBgroup/brick-llm/commit/32d99239c05ee01922e7ba2ff51776dc0f3fb88a)) +* unused imports ([35c9d8c](https://github.com/EURAC-EEBgroup/brick-llm/commit/35c9d8cda731d7bd2fef7e3f1a3f385a7d47e4d8)) + + +### chore + +* add simpler building description ([57dcb2e](https://github.com/EURAC-EEBgroup/brick-llm/commit/57dcb2e3fc3f13e1194f1488d6fc27157a1dc6ce)) +* modified cicd ([580199b](https://github.com/EURAC-EEBgroup/brick-llm/commit/580199bed69754fbaa0150c6789292dc20151b63)) +* rename folder containers into finetuned ([0eca876](https://github.com/EURAC-EEBgroup/brick-llm/commit/0eca87685ca37e04e6a83d55fd54c7a3915edeb2)) + + +### Docs + +* add logo to readme ([1f7d153](https://github.com/EURAC-EEBgroup/brick-llm/commit/1f7d1537e1270834e4c5e4c3ca462bb7d42e8aaa)) +* add type validation ([2685307](https://github.com/EURAC-EEBgroup/brick-llm/commit/2685307f08e9ac6e3c4dcb53d6be132a8c2a599b)) +* added all docstrings ([cede480](https://github.com/EURAC-EEBgroup/brick-llm/commit/cede480e5ca4a720df5422ba91930b6038c0e09c)) +* added Dockerfile ([441df65](https://github.com/EURAC-EEBgroup/brick-llm/commit/441df65604ad4485b0f39c487dd779bdc0a001ec)) +* added logo ([1987d44](https://github.com/EURAC-EEBgroup/brick-llm/commit/1987d440b6bfde8cbcf6d89199368f7ac793b007)) +* improve installation readme ([56bf5f6](https://github.com/EURAC-EEBgroup/brick-llm/commit/56bf5f6f3c372ad6ebbdaad05c44b46855ed040e)) +* improved example ([9b06758](https://github.com/EURAC-EEBgroup/brick-llm/commit/9b067584813f64c2b39436921f6e3ada40705a4d)) +* improved readme ([b7788fa](https://github.com/EURAC-EEBgroup/brick-llm/commit/b7788faa31b36f09586eb699936dfdce4ee9d829)) +* modified banner ([f01cb0e](https://github.com/EURAC-EEBgroup/brick-llm/commit/f01cb0e0085ff0013f58eafd052abbffad3717a8)) +* modified custom example ([85074cc](https://github.com/EURAC-EEBgroup/brick-llm/commit/85074cc43697ee8f05e53c75e4b860bd5f894fff)) +* new logo ([55b2508](https://github.com/EURAC-EEBgroup/brick-llm/commit/55b25084679f056ffbb22deceecf592a76eaab5a)) +* save ttl ([50ee39f](https://github.com/EURAC-EEBgroup/brick-llm/commit/50ee39f2c6c26b29cbeac182f633995f60ba2c32)) +* Update README.md with local llm example ([e7a7e8e](https://github.com/EURAC-EEBgroup/brick-llm/commit/e7a7e8e32ca4d31eabc9c5eddc5993ef802687cd)) diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..f9db6b4 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,17 @@ +cff-version: 1.1.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Marco" + given-names: "Perini" + orcid: "https://orcid.org/0009-0008-6620-829X" +- family-names: "Daniele" + given-names: "Antonucci" + orcid: "https://orcid.org/0000-0002-4736-0711" +- family-names: "Rocco" + given-names: "Giudice" + orcid: "https://orcid.org/0009-0009-4013-4373" +title: "EURAC-EEBgroup/brick-llm" +version: v1.1.1 +doi: 10.5281/zenodo.14039358 +date-released: 2024-11-05 +url: "https://github.com/EURAC-EEBgroup/brick-llm" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0761bcb --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Energy Efficient Buildings @EURAC + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5cf0cdb --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +# Makefile for Project Automation + +.PHONY: install lint type-check test docs serve-docs build all clean + +# Variables +PACKAGE_NAME = brickllm +TEST_DIR = tests + +# Default target +all: lint type-check test docs + +# Install project dependencies +install: + poetry install + +# Linting and Formatting Checks +lint: + poetry run ruff check $(PACKAGE_NAME) $(TEST_DIR) + poetry run black --check $(PACKAGE_NAME) $(TEST_DIR) + poetry run isort --check-only $(PACKAGE_NAME) $(TEST_DIR) + +# Type Checking with MyPy +type-check: + poetry run mypy $(PACKAGE_NAME) $(TEST_DIR) + +# Run Tests with Coverage +test: + poetry run pytest --cov=$(PACKAGE_NAME) --cov-report=xml $(TEST_DIR)/ + +# Build Documentation using MkDocs +docs: + poetry run mkdocs build + +# Serve Documentation Locally +serve-docs: + poetry run mkdocs serve + +# Run Pre-Commit Hooks +pre-commit: + poetry run pre-commit run --all-files + +# Clean Up Generated Files +clean: + rm -rf dist/ + rm -rf build/ + rm -rf *.egg-info + rm -rf htmlcov/ + rm -rf .mypy_cache/ + rm -rf .pytest_cache/ + rm -rf .ruff_cache/ + rm -rf site/ + +# Build the Package +build: + poetry build diff --git a/README.md b/README.md index 822c355..737ebfb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,268 @@

- BrickLLM + BrickLLM

+# 🧱 BrickLLM + +BrickLLM is a Python library for generating RDF files following the BrickSchema ontology using Large Language Models (LLMs). + +## Features + +- Generate BrickSchema-compliant RDF files from natural language descriptions of buildings and facilities +- Support for multiple LLM providers (OpenAI, Anthropic, Fireworks) +- Customizable graph execution with LangGraph +- Easy-to-use API for integrating with existing projects + +## 💻 Installation + +You can install BrickLLM using pip: + +``` bash +pip install brickllm +``` + +
+Development Installation + +[Poetry](https://python-poetry.org/) is used for dependency management during development. To install BrickLLM for contributing, follow these steps: + +``` bash +# Clone the repository +git clone https://github.com/EURAC-EEBgroup/brickllm-lib.git +cd brick-llm + +# Create a virtual environment +python -m venv .venv + +# Activate the virtual environment +source .venv/bin/activate # Linux/Mac +.venv\Scripts\activate # Windows + +# Install Poetry and dependencies +pip install poetry +poetry install + +# Install pre-commit hooks +pre-commit install +``` + +
+ +## 🚀 Quick Start + +Here's a simple example of how to use BrickLLM: + +``` python +from brickllm.graphs import BrickSchemaGraph + +building_description = """ +I have a building located in Bolzano. +It has 3 floors and each floor has 1 office. +There are 2 rooms in each office and each room has three sensors: +- Temperature sensor; +- Humidity sensor; +- CO sensor. +""" + +# Create an instance of BrickSchemaGraph with a predefined provider +brick_graph = BrickSchemaGraph(model="openai") + +# Display the graph structure +brick_graph.display() + +# Prepare input data +input_data = { + "user_prompt": building_description +} + +# Run the graph +result = brick_graph.run(input_data=input_data, stream=False) + +# Print the result +print(result) + +# save the result to a file +brick_graph.save_ttl_output("my_building.ttl") +``` + +
+Using Custom LLM Models + +BrickLLM supports using custom LLM models. Here's an example using OpenAI's GPT-4o: + +``` python +from brickllm.graphs import BrickSchemaGraph +from langchain_openai import ChatOpenAI + +custom_model = ChatOpenAI(temperature=0, model="gpt-4o") +brick_graph = BrickSchemaGraph(model=custom_model) + +# Prepare input data +input_data = { + "user_prompt": building_description +} + +# Run the graph with the custom model +result = brick_graph.run(input_data=input_data, stream=False) +``` +
+ +
+Using Local LLM Models +

BrickLLM supports using local LLM models employing the Ollama framework. Currently, only our finetuned model is supported.

+ +### Option 1: Using Docker Compose + +You can easily set up and run the Ollama environment using Docker Compose. The finetuned model file will be automatically downloaded inside the container. Follow these steps: + +1. Clone the repository and navigate to the `finetuned` directory containing the `Dockerfile` and `docker-compose.yml`. +2. Run the following command to build and start the container: + ```bash + docker-compose up --build -d + ``` +3. Verify that the docker is running on localhost:11434: + ```bash + docker ps + ``` + if result is: + ``` + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 1e9bff7c2f7b finetuned-ollama-llm:latest "/entrypoint.sh" 42 minutes ago Up 42 minutes 11434/tcp compassionate_wing + ``` + + so run the docker image specifying the port: + ```bash + docker run -d -p 11434:11434 finetuned-ollama-llm:latest + docker ps + ``` + + the result will be like: + ``` + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + df8b31d4ed86 finetuned-ollama-llm:latest "/entrypoint.sh" 7 seconds ago Up 7 seconds 0.0.0.0:11434->11434/tcp eloquent_jennings + ``` + check if ollama is runnin in the port 11434: + ``` + curl http://localhost:11434 + ``` + Result should be: + ``` + Ollama is running + ``` +This will download the model file, create the model in Ollama, and serve it on port `11434`. The necessary directories will be created automatically. + +### Option 2: Manual Setup + +If you prefer to set up the model manually, follow these steps: + +1. Download the `.gguf` file from here. +2. Create a file named `Modelfile` with the following content: + ```bash + FROM ./unsloth.Q4_K_M.gguf + ``` + +3. Place the downloaded `.gguf` file in the same folder as the `Modelfile`. +4. Ensure Ollama is running on your system. +5. Run the following command to create the model in Ollama: + ```bash + ollama create llama3.1:8b-brick-v8 -f Modelfile + ``` + +Once you've set up the model in Ollama, you can use it in your code as follows: + +``` python +from brickllm.graphs import BrickSchemaGraphLocal + +instructions = """ +Your job is to generate a RDF graph in Turtle format from a description of energy systems and sensors of a building in the following input, using the Brick ontology. +### Instructions: +- Each subject, object of predicate must start with a @prefix. +- Use the prefix bldg: with IRI for any created entities. +- Use the prefix brick: with IRI for any Brick entities and relationships used. +- Use the prefix unit: with IRI and its ontology for any unit of measure defined. +- When encoding the timeseries ID of the sensor, you must use the following format: ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId 'timeseriesID' ]. +- When encoding identifiers or external references, such as building/entities IDs, use the following schema: ref:hasExternalReference [ a ref:ExternalReference ; ref:hasExternalReference ‘id/reference’ ]. +- When encoding numerical reference, use the schema [brick:value 'value' ; \n brick:hasUnit unit:'unit' ] . +-When encoding coordinates, use the schema brick:coordinates [brick:latitude "lat" ; brick:longitude "long" ]. +The response must be the RDF graph that includes all the @prefix of the ontologies used in the triples. The RDF graph must be created in Turtle format. Do not add any other text or comment to the response. +""" + +building_description = """ +The building (external ref: 'OB103'), with coordinates 33.9614, -118.3531, has a total area of 500 m². It has three zones, each with its own air temperature sensor. +The building has an electrical meter that monitors data of a power sensor. An HVAC equipment serves all three zones and its power usage is measured by a power sensor. + +Timeseries IDs and unit of measure of the sensors: +- Building power consumption: '1b3e-29dk-8js7-f54v' in watts. +- HVAC power consumption: '29dh-8ks3-fvjs-d92e' in watts. +- Temperature sensor zone 1: 't29s-jk83-kv82-93fs' in celsius. +- Temperature sensor zone 2: 'f29g-js92-df73-l923' in celsius. +- Temperature sensor zone 3: 'm93d-ljs9-83ks-29dh' in celsius. +""" + +# Create an instance of BrickSchemaGraphLocal +brick_graph_local = BrickSchemaGraphLocal(model="llama3.1:8b-brick") + +# Display the graph structure +brick_graph_local.display() + +# Prepare input data +input_data = { + "user_prompt": building_description, + "instructions": instructions +} + +# Run the graph +result = brick_graph_local.run(input_data=input_data, stream=False) + +# Print the result +print(result) + +# Save the result to a file +brick_graph_local.save_ttl_output("my_building_local.ttl") +``` +
+ +## 📖 Documentation + +For more detailed information on how to use BrickLLM, please refer to our [documentation](https://brickllm.com/docs). + +## ▶️ Web Application + +A web app is available to use the library directly through an interface at the following link (). +The application can also be used locally as described in the dedicated repository [BrickLLM App](https://github.com/EURAC-EEBgroup/Brick_ontology_tool). + +**Note**: The tool is currently being deployed on our servers and on the MODERATE platform. It will be online shortly ! + +## 🤝 Contributing + +We welcome contributions to BrickLLM! Please see our [contributing guidelines](CONTRIBUTING.md) for more information. + +## 📜 License + +BrickLLM is released under the BSD-3-Clause License. See the [LICENSE](LICENSE) file for details. + +## Contact + +For any questions or support, please contact: + +- Marco Perini +- Daniele Antonucci +- Rocco Giudice + +## Citation + +Please cite us if you use the library + +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14039358.svg)](https://zenodo.org/doi/10.5281/zenodo.14039358) + +## 💙 Aknwoledegments +This work was carried out within European projects: + +

+ Moderate + +Moderate - Horizon Europe research and innovation programme under grant agreement No 101069834, with the aim of contributing to the development of open products useful for defining plausible scenarios for the decarbonization of the built environment +BrickLLM is developed and maintained by the Energy Efficiency in Buildings group at EURAC Research. Thanks to the contribution of: +- Moderate project: Horizon Europe research and innovation programme under grant agreement No 101069834 +- Politecnico of Turin, in particular to @Rocco Giudice for his work in developing model generation using local language model diff --git a/brickllm/__init__.py b/brickllm/__init__.py index c78cc29..cdbd062 100644 --- a/brickllm/__init__.py +++ b/brickllm/__init__.py @@ -1,11 +1,12 @@ -from .schemas import ElemListSchema, RelationshipsSchema, TTLSchema -from .states import State from .configs import GraphConfig +from .schemas import ElemListSchema, RelationshipsSchema, TTLSchema +from .states import State, StateLocal __all__ = [ "ElemListSchema", "RelationshipsSchema", "TTLSchema", "State", + "StateLocal", "GraphConfig", -] \ No newline at end of file +] diff --git a/brickllm/compiled_graphs.py b/brickllm/compiled_graphs.py index 280f8e4..d22e627 100644 --- a/brickllm/compiled_graphs.py +++ b/brickllm/compiled_graphs.py @@ -1,3 +1,5 @@ -from .graphs import BrickSchemaGraph +from .graphs import BrickSchemaGraph, BrickSchemaGraphLocal -brickschema_graph = BrickSchemaGraph()._compiled_graph() \ No newline at end of file +# compiled graph +brickschema_graph = BrickSchemaGraph()._compiled_graph() +brickschema_graph_local = BrickSchemaGraphLocal()._compiled_graph() diff --git a/brickllm/configs.py b/brickllm/configs.py index d1e590f..b291e85 100644 --- a/brickllm/configs.py +++ b/brickllm/configs.py @@ -1,5 +1,10 @@ -from typing import TypedDict, Literal +from typing import Literal, TypedDict, Union + +from langchain.chat_models.base import BaseChatModel + # Define the config class GraphConfig(TypedDict): - model_name: Literal["anthropic", "openai", "fireworks"] \ No newline at end of file + model: Union[ + Literal["anthropic", "openai", "fireworks", "llama3.1:8b-brick"], BaseChatModel + ] diff --git a/brickllm/edges/__init__.py b/brickllm/edges/__init__.py index c9e5e8a..2778969 100644 --- a/brickllm/edges/__init__.py +++ b/brickllm/edges/__init__.py @@ -1,4 +1,4 @@ from .validate_condition import validate_condition +from .validate_condition_local import validate_condition_local -__all__ = ["validate_condition"] - +__all__ = ["validate_condition", "validate_condition_local"] diff --git a/brickllm/edges/validate_condition.py b/brickllm/edges/validate_condition.py index e233635..5806c36 100644 --- a/brickllm/edges/validate_condition.py +++ b/brickllm/edges/validate_condition.py @@ -1,14 +1,21 @@ -from typing import Literal -from langgraph.graph import END +from typing import Any, Dict, Literal -def validate_condition(state) -> Literal["schema_to_ttl", "__end__"]: - # Often, we will use state to decide on the next node to visit +def validate_condition(state: Dict[str, Any]) -> Literal["schema_to_ttl", "__end__"]: + """ + Validate the condition for the next node to visit. + + Args: + state (Dict[str, Any]): The current state containing the validation result. + + Returns: + Literal["schema_to_ttl", "__end__"]: The next node to visit. + """ + is_valid = state.get("is_valid", False) - max_iter = state.get("validation_max_iter", 1) + max_iter = state.get("validation_max_iter", 0) if max_iter > 0 and not is_valid: - state["validation_max_iter"] = max_iter - 1 return "schema_to_ttl" - return END \ No newline at end of file + return "__end__" diff --git a/brickllm/edges/validate_condition_local.py b/brickllm/edges/validate_condition_local.py new file mode 100644 index 0000000..f6f30a4 --- /dev/null +++ b/brickllm/edges/validate_condition_local.py @@ -0,0 +1,23 @@ +from typing import Any, Dict, Literal + + +def validate_condition_local( + state: Dict[str, Any] +) -> Literal["generation_local", "__end__"]: + """ + Validate the condition for the next node to visit. + + Args: + state (Dict[str, Any]): The current state containing the validation result. + + Returns: + Literal["generation_local", "__end__"]: The next node to visit. + """ + + is_valid = state.get("is_valid", False) + max_iter = state.get("validation_max_iter", 2) + + if max_iter > 0 and not is_valid: + return "generation_local" + + return "__end__" diff --git a/brickllm/graphs/__init__.py b/brickllm/graphs/__init__.py index 2e99686..f7f390f 100644 --- a/brickllm/graphs/__init__.py +++ b/brickllm/graphs/__init__.py @@ -1,5 +1,5 @@ +from .abstract_graph import AbstractBrickSchemaGraph from .brickschema_graph import BrickSchemaGraph +from .brickschema_graph_local import BrickSchemaGraphLocal -__all__ = [ - "BrickSchemaGraph", -] \ No newline at end of file +__all__ = ["AbstractBrickSchemaGraph", "BrickSchemaGraph", "BrickSchemaGraphLocal"] diff --git a/brickllm/graphs/abstract_graph.py b/brickllm/graphs/abstract_graph.py new file mode 100644 index 0000000..bde38c8 --- /dev/null +++ b/brickllm/graphs/abstract_graph.py @@ -0,0 +1,86 @@ +import os +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Union + +from langchain.chat_models.base import BaseChatModel +from langgraph.graph import StateGraph +from PIL import Image + +from ..helpers.llm_models import _get_model + + +class AbstractBrickSchemaGraph(ABC): + def __init__(self, model: Union[str, BaseChatModel] = "openai"): + self.model = _get_model(model) + self.workflow = None + self.graph = None + self.config = {"configurable": {"thread_id": "1", "llm_model": self.model}} + self.result = None + + @abstractmethod + def build_graph(self): + """Build the graph by adding nodes and edges.""" + pass + + def compile_graph(self): + """Compile the graph.""" + try: + self.graph = self.workflow.compile() + except Exception as e: + raise ValueError(f"Failed to compile the graph: {e}") + + def _compiled_graph(self) -> StateGraph: + """Check if the graph is compiled and return the compiled graph.""" + if self.graph is None: + raise ValueError( + "Graph is not compiled yet. Please compile the graph first." + ) + return self.graph + + @abstractmethod + def run( + self, input_data: Dict[str, Any], stream: bool = False + ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: + """Run the graph with the given input data.""" + pass + + def display(self, filename: str = "graph.png") -> None: + """Display the compiled graph as an image.""" + if self.graph is None: + raise ValueError( + "Graph is not compiled yet. Please compile the graph first." + ) + + # Save the image to the specified file + self.graph.get_graph().draw_mermaid_png(output_file_path=filename) + + # Open the image using PIL (Pillow) + if os.path.exists(filename): + with Image.open(filename) as img: + img.show() + else: + raise FileNotFoundError( + f"Failed to generate the graph image file: {filename}" + ) + + def get_state_snapshots(self) -> List[Dict[str, Any]]: + """Get all the state snapshots from the graph execution.""" + all_states = [] + for state in self.graph.get_state_history(self.config): + all_states.append(state) + return all_states + + def save_ttl_output(self, output_file: str = "brick_schema_output.ttl") -> None: + """Save the TTL output to a file.""" + if self.result is None: + raise ValueError("No result found. Please run the graph first.") + + ttl_output = self.result.get("ttl_output", None) + + if ttl_output: + with open(output_file, "w") as f: + f.write(ttl_output) + else: + raise ValueError( + "No TTL output found in the result. Please run the graph with a valid prompt." + ) diff --git a/brickllm/graphs/brickschema_graph.py b/brickllm/graphs/brickschema_graph.py index 658d07b..7e62c1a 100644 --- a/brickllm/graphs/brickschema_graph.py +++ b/brickllm/graphs/brickschema_graph.py @@ -1,22 +1,30 @@ -from langgraph.graph import START, END, StateGraph -from .. import State, GraphConfig +from typing import Any, Dict, List, Union + +from langchain.chat_models.base import BaseChatModel +from langgraph.graph import END, START, StateGraph + +from .. import GraphConfig, State +from ..edges import validate_condition from ..nodes import ( - get_elements, get_elem_children, get_relationships, - schema_to_ttl, validate_schema, get_sensors + get_elem_children, + get_elements, + get_relationships, + get_sensors, + schema_to_ttl, + validate_schema, ) -from ..edges import validate_condition -from PIL import Image -import os +from .abstract_graph import AbstractBrickSchemaGraph -from dotenv import load_dotenv -load_dotenv() -class BrickSchemaGraph: - def __init__(self): - """Initialize the StateGraph object and build the graph.""" - # Define a new graph +class BrickSchemaGraph(AbstractBrickSchemaGraph): + def __init__(self, model: Union[str, BaseChatModel] = "openai"): + super().__init__(model) + self.build_graph() + self.compile_graph() + + def build_graph(self): self.workflow = StateGraph(State, config_schema=GraphConfig) - + # Build graph by adding nodes self.workflow.add_node("get_elements", get_elements) self.workflow.add_node("get_elem_children", get_elem_children) @@ -34,66 +42,21 @@ def __init__(self): self.workflow.add_conditional_edges("validate_schema", validate_condition) self.workflow.add_edge("get_relationships", "get_sensors") self.workflow.add_edge("get_sensors", END) - - # Compile graph - try: - self.graph = self.workflow.compile() - except Exception as e: - raise ValueError(f"Failed to compile the graph: {e}") - - # Hardcoding the thread_id for now - self.config = {"configurable": {"thread_id": "1"}} - - def _compiled_graph(self): - """Check if the graph is compiled and return the compiled graph.""" - if self.graph is None: - raise ValueError("Graph is not compiled yet. Please compile the graph first.") - return self.graph - def display(self, filename="graph.png") -> None: - """Display the compiled graph as an image. - - Args: - filename (str): The filename to save the graph image. - """ - if self.graph is None: - raise ValueError("Graph is not compiled yet. Please compile the graph first.") - - # Save the image to the specified file - self.graph.get_graph().draw_mermaid_png(output_file_path=filename) - - # Open the image using PIL (Pillow) - if os.path.exists(filename): - with Image.open(filename) as img: - img.show() - else: - raise FileNotFoundError(f"Failed to generate the graph image file: {filename}") - - def run(self, prompt, stream=False): - """Run the graph with the given user prompt. - - Args: - user_prompt (str): The user-provided natural language prompt. - stream (bool): Whether to stream the execution (True) or run without streaming (False). - """ - input_data = {"user_prompt": prompt} + def run( + self, input_data: Dict[str, Any], stream: bool = False + ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: + if "user_prompt" not in input_data: + raise ValueError("Input data must contain a 'user_prompt' key.") if stream: events = [] - # Stream the content of the graph state at each node - for event in self.graph.stream(input_data, self.config, stream_mode="values"): + for event in self.graph.stream( + input_data, self.config, stream_mode="values" + ): events.append(event) + self.result = events[-1] return events else: - # Invoke the graph without streaming - result = self.graph.invoke(input_data, self.config) - return result - - def get_state_snapshots(self) -> list: - """Get all the state snapshots from the graph execution.""" - - all_states = [] - for state in self.graph.get_state_history(self.config): - all_states.append(state) - - return all_states \ No newline at end of file + self.result = self.graph.invoke(input_data, self.config) + return self.result diff --git a/brickllm/graphs/brickschema_graph_local.py b/brickllm/graphs/brickschema_graph_local.py new file mode 100644 index 0000000..6301115 --- /dev/null +++ b/brickllm/graphs/brickschema_graph_local.py @@ -0,0 +1,48 @@ +from typing import Any, Dict, List, Union + +from langchain.chat_models.base import BaseChatModel +from langgraph.graph import START, StateGraph + +from .. import GraphConfig, StateLocal +from ..edges import validate_condition_local +from ..nodes import generation_local, validate_schema +from .abstract_graph import AbstractBrickSchemaGraph + + +class BrickSchemaGraphLocal(AbstractBrickSchemaGraph): + def __init__(self, model: Union[str, BaseChatModel]): + super().__init__(model) + self.build_graph() + self.compile_graph() + + def build_graph(self): + self.workflow = StateGraph(StateLocal, config_schema=GraphConfig) + + # Build graph by adding nodes + self.workflow.add_node("generation_local", generation_local) + self.workflow.add_node("validate_schema", validate_schema) + + # Add edges to define the flow logic + self.workflow.add_edge(START, "generation_local") + self.workflow.add_edge("generation_local", "validate_schema") + self.workflow.add_conditional_edges("validate_schema", validate_condition_local) + + def run( + self, input_data: Dict[str, Any], stream: bool = False + ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: + if "user_prompt" not in input_data or "instructions" not in input_data: + raise ValueError( + "Input data must contain both 'user_prompt' and 'instructions' keys." + ) + + if stream: + events = [] + for event in self.graph.stream( + input_data, self.config, stream_mode="values" + ): + events.append(event) + self.result = events[-1] + return events + else: + self.result = self.graph.invoke(input_data, self.config) + return self.result diff --git a/brickllm/helpers/__init__.py b/brickllm/helpers/__init__.py index 2f59ea6..5ab8e7d 100644 --- a/brickllm/helpers/__init__.py +++ b/brickllm/helpers/__init__.py @@ -1,8 +1,9 @@ from .llm_models import _get_model from .prompts import ( - get_elem_instructions, get_elem_children_instructions, + get_elem_instructions, get_relationships_instructions, + prompt_template_local, schema_to_ttl_instructions, ttl_example, ) @@ -14,4 +15,5 @@ "get_relationships_instructions", "schema_to_ttl_instructions", "ttl_example", -] \ No newline at end of file + "prompt_template_local", +] diff --git a/brickllm/helpers/llm_models.py b/brickllm/helpers/llm_models.py index 1817e6f..ecda551 100644 --- a/brickllm/helpers/llm_models.py +++ b/brickllm/helpers/llm_models.py @@ -1,19 +1,35 @@ +from typing import Union -from functools import lru_cache +from langchain.chat_models.base import BaseChatModel from langchain_anthropic import ChatAnthropic -from langchain_openai import ChatOpenAI +from langchain_community.llms import Ollama from langchain_fireworks import ChatFireworks +from langchain_openai import ChatOpenAI -@lru_cache(maxsize=4) -def _get_model(model_name: str): - if model_name == "openai": - model = ChatOpenAI(temperature=0, model="gpt-4o") - elif model_name == "anthropic": - model = ChatAnthropic(temperature=0, model="claude-3-sonnet-20240229") - elif model_name == "fireworks": - model = ChatFireworks(temperature=0, model="accounts/fireworks/models/llama-v3p1-70b-instruct") - else: - raise ValueError(f"Unsupported model type: {model_name}") +def _get_model(model: Union[str, BaseChatModel]) -> BaseChatModel: + """ + Get the LLM model based on the provided model type. + + Args: + model (Union[str, BaseChatModel]): The model type as a string or an instance of BaseChatModel. - return model \ No newline at end of file + Returns: + BaseChatModel: The LLM model instance. + """ + + if isinstance(model, BaseChatModel): + return model + + if model == "openai": + return ChatOpenAI(temperature=0, model="gpt-4o") + elif model == "anthropic": + return ChatAnthropic(temperature=0, model_name="claude-3-sonnet-20240229") + elif model == "fireworks": + return ChatFireworks( + temperature=0, model="accounts/fireworks/models/llama-v3p1-70b-instruct" + ) + elif model == "llama3.1:8b-brick": + return Ollama(model="llama3.1:8b-brick-v8") + else: + raise ValueError(f"Unsupported model type: {model}") diff --git a/brickllm/helpers/prompts.py b/brickllm/helpers/prompts.py index 49082a5..11a0bf5 100644 --- a/brickllm/helpers/prompts.py +++ b/brickllm/helpers/prompts.py @@ -1,4 +1,8 @@ -get_elem_instructions = """ +""" +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 @@ -7,7 +11,7 @@ ELEMENTS: {elements_dict} \n """ -get_elem_children_instructions = """ +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 @@ -20,20 +24,20 @@ ELEMENTS HIERARCHY: {elements_list} \n """ -get_relationships_instructions = """ +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 hash 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 + 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 + 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} """ -ttl_example = """ +ttl_example: str = """ @prefix bldg: . @prefix brick: . @prefix prj: . @@ -82,7 +86,7 @@ brick:isPartOf bldg:Milano_Residence_1 . """ -schema_to_ttl_instructions = """ +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 @@ -99,3 +103,14 @@ COMPONENTS DICT: {sensors_dict}\n """ + +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: +""" diff --git a/brickllm/nodes/__init__.py b/brickllm/nodes/__init__.py index 2976d00..59a4b93 100644 --- a/brickllm/nodes/__init__.py +++ b/brickllm/nodes/__init__.py @@ -1,3 +1,4 @@ +from .generation_local import generation_local from .get_elem_children import get_elem_children from .get_elements import get_elements from .get_relationships import get_relationships @@ -12,4 +13,5 @@ "get_sensors", "schema_to_ttl", "validate_schema", -] \ No newline at end of file + "generation_local", +] diff --git a/brickllm/nodes/generation_local.py b/brickllm/nodes/generation_local.py new file mode 100644 index 0000000..3c3d089 --- /dev/null +++ b/brickllm/nodes/generation_local.py @@ -0,0 +1,34 @@ +from typing import Any, Dict + +from .. import StateLocal +from ..helpers import prompt_template_local +from ..utils import extract_rdf_graph + + +def generation_local(state: StateLocal, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate the RDF graph using the local LLM from a prompt containing the building description and the instruction + + Args: + state (StateLocal): The current state containing the user prompt and the instructions. + config (dict): Configuration dictionary containing the language model. + + Returns: + dict: A dictionary containing the output generated. + """ + + print("---One shot generation with local LLM Node---") + + instructions = state["instructions"] + user_prompt = state["user_prompt"] + + llm = config.get("configurable", {}.get("llm_model"))["llm_model"] + + message = prompt_template_local.format( + instructions=instructions, user_prompt=user_prompt + ) + + answer = llm.invoke(message) + ttl_output = extract_rdf_graph(answer) + + return {"ttl_output": ttl_output} diff --git a/brickllm/nodes/get_elem_children.py b/brickllm/nodes/get_elem_children.py index e55bb2c..0c8c70b 100644 --- a/brickllm/nodes/get_elem_children.py +++ b/brickllm/nodes/get_elem_children.py @@ -1,12 +1,23 @@ +from typing import Any, Dict + from langchain_core.messages import HumanMessage, SystemMessage -from .. import State, ElemListSchema +from .. import ElemListSchema, State from ..helpers import get_elem_children_instructions -from ..utils import get_children_hierarchy, create_hierarchical_dict, filter_elements -from ..helpers import _get_model +from ..utils import create_hierarchical_dict, filter_elements, get_children_hierarchy + + +def get_elem_children(state: State, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Identify child elements for each category in the element list using a language model. + Args: + state (State): The current state containing the user prompt and element list. + config (dict): Configuration dictionary containing the language model. -def get_elem_children(state: State, config): + Returns: + dict: A dictionary containing the hierarchical structure of identified elements. + """ print("---Get Elem Children Node---") user_prompt = state["user_prompt"] @@ -15,12 +26,17 @@ def get_elem_children(state: State, config): category_dict = {} for category in categories: children_list = get_children_hierarchy(category, flatten=True) - children_string = "\n".join([f"{parent} -> {child}" for parent, child in children_list]) + children_string = "\n".join( + [ + f"{parent} -> {child}" + for parent, child in children_list + if isinstance(parent, str) and isinstance(child, str) + ] + ) category_dict[category] = children_string # Get the model name from the config - model_name = config.get('configurable', {}).get("model_name", "fireworks") - llm = _get_model(model_name) + llm = config.get("configurable", {}).get("llm_model") # Enforce structured output structured_llm = llm.with_structured_output(ElemListSchema) @@ -28,11 +44,16 @@ def get_elem_children(state: State, config): identified_children = [] for category in categories: # if the category is not "\n", then add the category to the prompt - if category_dict[category] != '': - # System message - system_message = get_elem_children_instructions.format(prompt=user_prompt, elements_list=category_dict[category]) + if category_dict[category] != "": + # System message + system_message = get_elem_children_instructions.format( + prompt=user_prompt, elements_list=category_dict[category] + ) # Generate question - elements = structured_llm.invoke([SystemMessage(content=system_message)]+[HumanMessage(content="Find the elements.")]) + elements = structured_llm.invoke( + [SystemMessage(content=system_message)] + + [HumanMessage(content="Find the elements.")] + ) identified_children.extend(elements.elem_list) else: identified_children.append(category) @@ -44,4 +65,4 @@ def get_elem_children(state: State, config): # create hierarchical dictionary hierarchical_dict = create_hierarchical_dict(filtered_children, properties=True) - return {"elem_hierarchy": hierarchical_dict} \ No newline at end of file + return {"elem_hierarchy": hierarchical_dict} diff --git a/brickllm/nodes/get_elements.py b/brickllm/nodes/get_elements.py index 3d64e0b..beb32d0 100644 --- a/brickllm/nodes/get_elements.py +++ b/brickllm/nodes/get_elements.py @@ -1,16 +1,29 @@ +from typing import Any, Dict + from langchain_core.messages import HumanMessage, SystemMessage -from .. import State, ElemListSchema -from ..helpers import get_elem_instructions, _get_model -from ..utils import get_hierarchical_info, get_brick_definition +from .. import ElemListSchema, State +from ..helpers import get_elem_instructions +from ..utils import get_brick_definition, get_hierarchical_info + + +def get_elements(state: State, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Process the user prompt to identify elements within specified categories + using a language model. + Args: + state (State): The current state containing the user prompt. + config (dict): Configuration dictionary containing the language model. -def get_elements(state: State, config): + Returns: + dict: A dictionary containing the list of identified elements. + """ print("---Get Elements Node---") user_prompt = state["user_prompt"] - categories = ['Point', 'Equipment', 'Location', 'Collection'] + categories = ["Point", "Equipment", "Location", "Collection"] category_dict = {} # Get hierarchy info for each category @@ -26,16 +39,20 @@ def get_elements(state: State, config): category_dict[category] = children_dict # Get the model name from the config - model_name = config.get('configurable', {}).get("model_name", "fireworks") - llm = _get_model(model_name) + llm = config.get("configurable", {}).get("llm_model") # Enforce structured output structured_llm = llm.with_structured_output(ElemListSchema) # System message - system_message = get_elem_instructions.format(prompt=user_prompt, elements_dict=category_dict) + system_message = get_elem_instructions.format( + prompt=user_prompt, elements_dict=category_dict + ) # Generate question - answer = structured_llm.invoke([SystemMessage(content=system_message)]+[HumanMessage(content="Find the elements.")]) + answer = structured_llm.invoke( + [SystemMessage(content=system_message)] + + [HumanMessage(content="Find the elements.")] + ) - return {"elem_list": answer.elem_list} \ No newline at end of file + return {"elem_list": answer.elem_list} diff --git a/brickllm/nodes/get_relationships.py b/brickllm/nodes/get_relationships.py index b24a0fe..81eb08b 100644 --- a/brickllm/nodes/get_relationships.py +++ b/brickllm/nodes/get_relationships.py @@ -1,15 +1,25 @@ - import json from collections import defaultdict +from typing import Any, Dict from langchain_core.messages import HumanMessage, SystemMessage -from .. import State, RelationshipsSchema -from ..helpers import get_relationships_instructions, _get_model +from .. import RelationshipsSchema, State +from ..helpers import get_relationships_instructions from ..utils import build_hierarchy, find_sensor_paths -def get_relationships(state: State, config): +def get_relationships(state: State, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Determine relationships between building components using a language model. + + Args: + state (State): The current state containing the user prompt and element hierarchy. + config (dict): Configuration dictionary containing the language model. + + Returns: + dict: A dictionary containing the grouped sensor paths. + """ print("---Get Relationships Node---") user_prompt = state["user_prompt"] @@ -19,16 +29,20 @@ def get_relationships(state: State, config): building_structure_json = json.dumps(building_structure, indent=2) # Get the model name from the config - model_name = config.get('configurable', {}).get("model_name", "fireworks") - llm = _get_model(model_name) + llm = config.get("configurable", {}).get("llm_model") # Enforce structured output structured_llm = llm.with_structured_output(RelationshipsSchema) # System message - system_message = get_relationships_instructions.format(prompt=user_prompt, building_structure=building_structure_json) + system_message = get_relationships_instructions.format( + prompt=user_prompt, building_structure=building_structure_json + ) # Generate question - answer = structured_llm.invoke([SystemMessage(content=system_message)]+[HumanMessage(content="Find the relationships.")]) + answer = structured_llm.invoke( + [SystemMessage(content=system_message)] + + [HumanMessage(content="Find the relationships.")] + ) try: tree_dict = build_hierarchy(answer.relationships) @@ -39,7 +53,7 @@ def get_relationships(state: State, config): sensor_paths = find_sensor_paths(tree_dict) grouped_sensors = defaultdict(list) for sensor in sensor_paths: - grouped_sensors[sensor['path']].append(sensor['name']) + grouped_sensors[sensor["path"]].append(sensor["name"]) grouped_sensor_dict = dict(grouped_sensors) - return {"sensors_dict": grouped_sensor_dict} \ No newline at end of file + return {"sensors_dict": grouped_sensor_dict} diff --git a/brickllm/nodes/get_sensors.py b/brickllm/nodes/get_sensors.py index 2f550f7..0a386a4 100644 --- a/brickllm/nodes/get_sensors.py +++ b/brickllm/nodes/get_sensors.py @@ -1,29 +1,40 @@ +from typing import Any, Dict + from .. import State -def get_sensors(state: State): +def get_sensors(state: State) -> Dict[str, Any]: + """ + Retrieve sensor information for the building structure. + + Args: + state (State): The current state. + + Returns: + dict: A dictionary containing sensor UUIDs mapped to their locations. + """ print("---Get Sensor Node---") uuid_dict = { "Building#1>Floor#1>Office#1>Room#1": [ { - "name":"Temperature_Sensor#1", - "uuid":"aaaa-bbbb-cccc-dddd", + "name": "Temperature_Sensor#1", + "uuid": "aaaa-bbbb-cccc-dddd", }, { - "name":"Humidity_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": "Temperature_Sensor#2", + "uuid": "aaaa-bbbb-cccc-dddd", }, { - "name":"Humidity_Sensor#2", - "uuid":"aaaa-bbbb-cccc-dddd", - } + "name": "Humidity_Sensor#2", + "uuid": "aaaa-bbbb-cccc-dddd", + }, ], } - return {"uuid_dict": uuid_dict} \ No newline at end of file + return {"uuid_dict": uuid_dict} diff --git a/brickllm/nodes/schema_to_ttl.py b/brickllm/nodes/schema_to_ttl.py index c4203a5..741a3e4 100644 --- a/brickllm/nodes/schema_to_ttl.py +++ b/brickllm/nodes/schema_to_ttl.py @@ -1,12 +1,23 @@ import json +from typing import Any, Dict from langchain_core.messages import HumanMessage, SystemMessage from .. import State, TTLSchema -from ..helpers import schema_to_ttl_instructions, ttl_example, _get_model +from ..helpers import schema_to_ttl_instructions, ttl_example -def schema_to_ttl(state: State, config): +def schema_to_ttl(state: State, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate a TTL (Turtle) script from the building description and component hierarchy. + + Args: + state (State): The current state containing the user prompt, sensors, and element hierarchy. + config (dict): Configuration dictionary containing the language model. + + Returns: + dict: A dictionary containing the generated TTL output. + """ print("---Schema To TTL Node---") user_prompt = state["user_prompt"] @@ -17,8 +28,7 @@ def schema_to_ttl(state: State, config): elem_hierarchy_json = json.dumps(elem_hierarchy, indent=2) # Get the model name from the config - model_name = config.get('configurable', {}).get("model_name", "fireworks") - llm = _get_model(model_name) + llm = config.get("configurable", {}).get("llm_model") # Enforce structured output structured_llm = llm.with_structured_output(TTLSchema) @@ -28,11 +38,13 @@ def schema_to_ttl(state: State, config): prompt=user_prompt, sensors_dict=sensors_dict_json, elem_hierarchy=elem_hierarchy_json, - ttl_example=ttl_example + ttl_example=ttl_example, ) # Generate question - answer = structured_llm.invoke([SystemMessage(content=system_message)]+[HumanMessage(content="Generate the TTL.")]) - print(answer.ttl_output) + answer = structured_llm.invoke( + [SystemMessage(content=system_message)] + + [HumanMessage(content="Generate the TTL.")] + ) - return {"ttl_output": answer.ttl_output} \ No newline at end of file + return {"ttl_output": answer.ttl_output} diff --git a/brickllm/nodes/validate_schema.py b/brickllm/nodes/validate_schema.py index 4b8a7fb..913f1d8 100644 --- a/brickllm/nodes/validate_schema.py +++ b/brickllm/nodes/validate_schema.py @@ -1,17 +1,36 @@ -import random +from typing import Any, Dict -from .. import State +from ..utils import validate_ttl -def validate_schema(state: State): +def validate_schema(state) -> Dict[str, Any]: + """ + Validate the generated TTL output against the BrickSchema. + + Args: + state (State): The current state containing the TTL output and validation parameters. + + Returns: + dict: A dictionary containing the validation status and report. + """ print("---Validate Schema Node---") - # ttl_output = state["ttl_output"] + ttl_output = state.get("ttl_output", None) + max_iter = state.get("validation_max_iter", 2) + + max_iter -= 1 - # Validate the schema - if random.random() < 0.5: + if ttl_output is None: + return { + "is_valid": False, + "validation_report": "Empty TTL output.", + "validation_max_iter": max_iter, + } - # 50% of the time, we return Node 2 - return {"is_valid": True} + is_valid, report = validate_ttl(ttl_output) - return {"is_valid": False} \ No newline at end of file + return { + "is_valid": is_valid, + "validation_report": report, + "validation_max_iter": max_iter, + } diff --git a/brickllm/ontologies/brick_hierarchy.json b/brickllm/ontologies/brick_hierarchy.json index 28b7b62..76fb8b7 100644 --- a/brickllm/ontologies/brick_hierarchy.json +++ b/brickllm/ontologies/brick_hierarchy.json @@ -486,4 +486,4 @@ "Safety_System": {} } } -} \ No newline at end of file +} diff --git a/brickllm/schemas.py b/brickllm/schemas.py index 06e91a0..dd61887 100644 --- a/brickllm/schemas.py +++ b/brickllm/schemas.py @@ -1,12 +1,18 @@ from typing import List, Tuple + from pydantic.v1 import BaseModel, Field + # pydantic schemas class ElemListSchema(BaseModel): elem_list: List[str] + class RelationshipsSchema(BaseModel): relationships: List[Tuple[str, ...]] + class TTLSchema(BaseModel): - ttl_output: str = Field(..., description="The generated BrickSchema turtle/rdf script.") \ No newline at end of file + ttl_output: str = Field( + ..., description="The generated BrickSchema turtle/rdf script." + ) diff --git a/brickllm/states.py b/brickllm/states.py index 5b5e897..f36074b 100644 --- a/brickllm/states.py +++ b/brickllm/states.py @@ -1,7 +1,9 @@ -from typing import Any, List, Dict +from typing import Any, Dict, List + from typing_extensions import TypedDict -# graph state + +# state for BrickSchemaGraph class class State(TypedDict): user_prompt: str elem_list: List[str] @@ -11,6 +13,17 @@ class State(TypedDict): # rel_tree: Dict[str, Any] sensors_dict: Dict[str, List[str]] is_valid: bool + validation_report: str validation_max_iter: int uuid_dict: Dict[str, Any] ttl_output: str + + +# state for BrickSchemaGraphLocal class +class StateLocal(TypedDict): + instructions: str + user_prompt: str + is_valid: bool + validation_report: str + validation_max_iter: int + ttl_output: str diff --git a/brickllm/utils/__init__.py b/brickllm/utils/__init__.py index 792f499..5524b11 100644 --- a/brickllm/utils/__init__.py +++ b/brickllm/utils/__init__.py @@ -1,46 +1,47 @@ from .get_hierarchy_info import ( - find_parents, - get_children, - flatten_hierarchy, - get_hierarchical_info, - get_all_subchildren, - get_children_hierarchy, - filter_elements, - create_hierarchical_dict, - find_sensor_paths, - build_hierarchy, - extract_ttl_content + build_hierarchy, + create_hierarchical_dict, + extract_ttl_content, + filter_elements, + find_parents, + find_sensor_paths, + flatten_hierarchy, + get_all_subchildren, + get_children, + get_children_hierarchy, + get_hierarchical_info, ) - from .query_brickschema import ( - get_brick_definition, - get_query_result, - clean_result, - query_subclass, - query_properties, - iterative_subclasses, - general_query + clean_result, + general_query, + get_brick_definition, + get_query_result, + iterative_subclasses, + query_properties, + query_subclass, + validate_ttl, ) +from .rdf_parser import extract_rdf_graph __all__ = [ - 'find_parents', - 'get_children', - 'flatten_hierarchy', - 'get_hierarchical_info', - 'get_all_subchildren', - 'get_children_hierarchy', - 'filter_elements', - 'create_hierarchical_dict', - 'find_sensor_paths', - 'build_hierarchy', - 'extract_ttl_content', - 'get_brick_definition', - 'get_query_result', - 'clean_result', - 'query_subclass', - 'query_properties', - 'iterative_subclasses', - 'general_query' + "find_parents", + "get_children", + "flatten_hierarchy", + "get_hierarchical_info", + "get_all_subchildren", + "get_children_hierarchy", + "filter_elements", + "create_hierarchical_dict", + "find_sensor_paths", + "build_hierarchy", + "extract_ttl_content", + "get_brick_definition", + "get_query_result", + "clean_result", + "query_subclass", + "query_properties", + "iterative_subclasses", + "general_query", + "validate_ttl", + "extract_rdf_graph", ] - - diff --git a/brickllm/utils/get_hierarchy_info.py b/brickllm/utils/get_hierarchy_info.py index b5ce41d..307e1eb 100644 --- a/brickllm/utils/get_hierarchy_info.py +++ b/brickllm/utils/get_hierarchy_info.py @@ -2,19 +2,37 @@ import os import re from collections import defaultdict -from .query_brickschema import general_query +from typing import Any, Dict, List, Optional, Tuple, Union + import pkg_resources +from .query_brickschema import general_query # Path to the JSON file -brick_hierarchy_path = pkg_resources.resource_filename(__name__, os.path.join('..', 'ontologies', 'brick_hierarchy.json')) +brick_hierarchy_path = pkg_resources.resource_filename( + __name__, os.path.join("..", "ontologies", "brick_hierarchy.json") +) # Load the JSON file with open(brick_hierarchy_path) as f: data = json.load(f) + # Function to recursively find parents -def find_parents(current_data, target, parents=None): +def find_parents( + current_data: Dict[str, Any], target: str, parents: Optional[List[str]] = None +) -> Tuple[bool, List[str]]: + """ + Recursively find the parent nodes of a target node in a hierarchical data structure. + + Args: + current_data (Dict[str, Any]): The current level of the hierarchy to search. + target (str): The target node to find parents for. + parents (Optional[List[str]], optional): Accumulated list of parent nodes. Defaults to None. + + Returns: + Tuple[bool, List[str]]: A tuple containing a boolean indicating if the target was found and a list of parent nodes. + """ if parents is None: parents = [] for key, value in current_data.items(): @@ -26,8 +44,19 @@ def find_parents(current_data, target, parents=None): return True, result return False, [] + # Function to get the children of a node -def get_children(current_data, target): +def get_children(current_data: Dict[str, Any], target: str) -> List[str]: + """ + Get the children of a target node in a hierarchical data structure. + + Args: + current_data (Dict[str, Any]): The current level of the hierarchy to search. + target (str): The target node to find children for. + + Returns: + List[str]: A list of child nodes. + """ if target in current_data: return list(current_data[target].keys()) for key, value in current_data.items(): @@ -37,8 +66,24 @@ def get_children(current_data, target): return children return [] + # Function to flatten the hierarchy -def flatten_hierarchy(current_data, parent=None, result=None): +def flatten_hierarchy( + current_data: Dict[str, Any], + parent: Optional[str] = None, + result: Optional[List[Tuple[str, str]]] = None, +) -> List[Tuple[str, str]]: + """ + Flatten a hierarchical data structure into a list of parent-child tuples. + + Args: + current_data (Dict[str, Any]): The current level of the hierarchy to flatten. + parent (Optional[str], optional): The parent node. Defaults to None. + result (Optional[List[Tuple[str, str]]], optional): Accumulated list of parent-child tuples. Defaults to None. + + Returns: + List[Tuple[str, str]]: A list of tuples representing parent-child relationships. + """ if result is None: result = [] for key, value in current_data.items(): @@ -48,18 +93,43 @@ def flatten_hierarchy(current_data, parent=None, result=None): flatten_hierarchy(value, key, result) return result + # Main function to get hierarchy info -def get_hierarchical_info(key): +def get_hierarchical_info(key: str) -> Tuple[List[str], List[str]]: + """ + Get the hierarchical information of a node, including its parents and children. + + Args: + key (str): The target node to get information for. + + Returns: + Tuple[List[str], List[str]]: A tuple containing a list of parent nodes and a list of child nodes. + """ # Get parents found, parents = find_parents(data, key) # Get children children = get_children(data, key) return (parents, children) + # Function to recursively get all children and subchildren -def get_all_subchildren(current_data, target): +def get_all_subchildren(current_data: Dict[str, Any], target: str) -> Dict[str, Any]: + """ + Recursively get all children and subchildren of a target node. + + Args: + current_data (Dict[str, Any]): The current level of the hierarchy to search. + target (str): The target node to find children for. + + Returns: + Dict[str, Any]: A dictionary representing the subtree of the target node. + """ if target in current_data: - return current_data[target] + sub_tree = current_data[target] + if isinstance(sub_tree, dict): + return sub_tree + else: + return {} for key, value in current_data.items(): if isinstance(value, dict): result = get_all_subchildren(value, target) @@ -67,14 +137,37 @@ def get_all_subchildren(current_data, target): return result return {} + # Main function to get hierarchy dictionary -def get_children_hierarchy(key, flatten=False): +def get_children_hierarchy( + key: str, flatten: bool = False +) -> Union[Dict[str, Any], List[Tuple[str, str]]]: + """ + Get the hierarchy of children for a target node, optionally flattening the result. + + Args: + key (str): The target node to get children for. + flatten (bool, optional): Whether to flatten the hierarchy. Defaults to False. + + Returns: + Union[Dict[str, Any], List[Tuple[str, str]]]: A dictionary representing the hierarchy or a list of parent-child tuples if flattened. + """ if flatten: return flatten_hierarchy(get_all_subchildren(data, key)) return get_all_subchildren(data, key) + # Function to filter elements based on the given conditions -def filter_elements(elements): +def filter_elements(elements: List[str]) -> List[str]: + """ + Filter elements based on their hierarchical relationships. + + Args: + elements (List[str]): A list of elements to filter. + + Returns: + List[str]: A list of filtered elements. + """ elements_info = {element: get_hierarchical_info(element) for element in elements} filtered_elements = [] @@ -90,8 +183,21 @@ def filter_elements(elements): return filtered_elements -def create_hierarchical_dict(elements, properties=False): - hierarchy = {} + +def create_hierarchical_dict( + elements: List[str], properties: bool = False +) -> Dict[str, Any]: + """ + Create a hierarchical dictionary from a list of elements, optionally including properties. + + Args: + elements (List[str]): A list of elements to include in the hierarchy. + properties (bool, optional): Whether to include properties in the hierarchy. Defaults to False. + + Returns: + Dict[str, Any]: A dictionary representing the hierarchical structure. + """ + hierarchy: Dict[str, Any] = {} for category in elements: parents, _ = get_hierarchical_info(category) @@ -112,36 +218,67 @@ def create_hierarchical_dict(elements, properties=False): # remove "message" key from the dictionary for prop in elem_property.keys(): elem_property[prop].pop("message") - current_level[category] = elem_property + current_level[category] = {"properties": elem_property} else: current_level[category] = {} return hierarchy -def find_sensor_paths(tree, path=None): + +def find_sensor_paths( + tree: Dict[str, Any], path: Optional[List[str]] = None +) -> List[Dict[str, str]]: + """ + Find paths to sensor nodes in a hierarchical tree structure. + + Args: + tree (Dict[str, Any]): The hierarchical tree structure. + path (Optional[List[str]], optional): Accumulated path to the current node. Defaults to None. + + Returns: + List[Dict[str, str]]: A list of dictionaries containing sensor names and their paths. + """ if path is None: path = [] - current_path = path + [tree['name']] - if 'children' not in tree or not tree['children']: - if re.search(r'Sensor', tree['name']): - sensor_path = '>'.join(current_path[:-1]) - return [{'name': tree['name'], 'path': sensor_path}] + current_path = path + [tree.get("name", "")] + if "children" not in tree or not tree["children"]: + if re.search(r"Sensor", tree.get("name", "")): + sensor_path = ">".join(current_path[:-1]) + return [{"name": tree.get("name", ""), "path": sensor_path}] return [] sensor_paths = [] - for child in tree['children']: + for child in tree["children"]: sensor_paths.extend(find_sensor_paths(child, current_path)) return sensor_paths -def build_hierarchy(relationships): + +def build_hierarchy(relationships: List[Tuple[str, str]]) -> Dict[str, Any]: + """ + Build a hierarchical tree structure from a list of parent-child relationships. + + Args: + relationships (List[Tuple[str, str]]): A list of tuples representing parent-child relationships. + + Returns: + Dict[str, Any]: A dictionary representing the hierarchical tree structure. + """ + # Helper function to recursively build the tree structure - def build_tree(node, tree_dict): - return {'name': node, 'children': [build_tree(child, tree_dict) for child in tree_dict[node]]} if tree_dict[node] else {'name': node} + def build_tree(node: str, tree_dict: Dict[str, List[str]]) -> Dict[str, Any]: + return ( + { + "name": node, + "children": [build_tree(child, tree_dict) for child in tree_dict[node]], + } + if tree_dict[node] + else {"name": node, "children": []} + ) # Create a dictionary to hold parent-children relationships - tree_dict = defaultdict(list) + tree_dict: Dict[str, List[str]] = defaultdict(list) nodes = set() # Fill the dictionary with data from relationships @@ -150,15 +287,30 @@ def build_tree(node, tree_dict): nodes.update([parent, child]) # Find the root (a node that is never a child) - root = next(node for node in tree_dict if all(node != child for _, child in relationships)) + root_candidates = { + node for node in nodes if node not in {child for _, child in relationships} + } + 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 + def extract_ttl_content(input_string: str) -> str: - # Use regex to match content between ```python and ``` - match = re.search(r"```code\s*(.*?)\s*```", input_string, re.DOTALL) - if match: - return match.group(1).strip() - return "" + """ + Extract content between code block markers in a string. + + Args: + input_string (str): The input string containing code blocks. + + Returns: + str: The extracted content between the code block markers. + """ + # Use regex to match content between ```code and ``` + match = re.search(r"```code\s*(.*?)\s*```", input_string, re.DOTALL) + if match: + return match.group(1).strip() + return "" diff --git a/brickllm/utils/query_brickschema.py b/brickllm/utils/query_brickschema.py index c421648..30a92d1 100644 --- a/brickllm/utils/query_brickschema.py +++ b/brickllm/utils/query_brickschema.py @@ -1,56 +1,123 @@ import os import re -from rdflib import Graph, URIRef, Namespace +from io import StringIO +from typing import Dict, List, Optional, Tuple, Union + import pkg_resources +import pyshacl +from rdflib import Graph, Namespace, URIRef +from rdflib.query import ResultRow # Path to the Brick schema Turtle file -brick_ttl_path = pkg_resources.resource_filename(__name__, os.path.join('..', 'ontologies', 'Brick.ttl')) +brick_ttl_path = pkg_resources.resource_filename( + __name__, os.path.join("..", "ontologies", "Brick.ttl") +) # Load the Brick schema Turtle file g = Graph() -g.parse(brick_ttl_path, format='ttl') +g.parse(brick_ttl_path, format="ttl") # Define the namespaces from the prefixes namespaces = { - 'brick': Namespace('https://brickschema.org/schema/Brick#'), + "brick": Namespace("https://brickschema.org/schema/Brick#"), } + # Function to get the definition from the TTL file def get_brick_definition(element_name: str) -> str: - normalized_key = element_name.replace('_', '').lower() + """ + Get the definition of an element from the Brick schema Turtle file. + + Args: + element_name (str): The name of the element to get the definition for. + + Returns: + str: The definition of the element, or "No definition available" if not found. + """ + normalized_key = element_name.replace("_", "").lower() for prefix, namespace in namespaces.items(): uri = namespace[element_name] - for s, p, o in g.triples((uri, URIRef("http://www.w3.org/2004/02/skos/core#definition"), None)): + for s, p, o in g.triples( + (uri, URIRef("http://www.w3.org/2004/02/skos/core#definition"), None) + ): return str(o) uri = namespace[normalized_key] - for s, p, o in g.triples((uri, URIRef("http://www.w3.org/2004/02/skos/core#definition"), None)): + for s, p, o in g.triples( + (uri, URIRef("http://www.w3.org/2004/02/skos/core#definition"), None) + ): return str(o) return "No definition available" + # Function to get the query result without using pandas -def get_query_result(query): +def get_query_result(query: str) -> List[Dict[str, str]]: + """ + Execute a SPARQL query on the Brick schema graph and return the results. + + Args: + query (str): The SPARQL query to execute. + + Returns: + List[Dict[str, str]]: A list of dictionaries representing the query results. + """ result = g.query(query) # Convert the result to a list of dictionaries where keys are the variable names - query_vars = list(result.vars) - data = [] + query_vars = list(result.vars) if result.vars is not None else [] + data: List[Dict[str, Optional[str]]] = [] for row in result: - data.append({str(var): str(row[var]) if row[var] else None for var in query_vars}) + if isinstance(row, ResultRow): + data.append( + {str(var): str(row[var]) if row[var] else None for var in query_vars} + ) # Remove entries with None values and reset index cleaned_data = [ - {key: value for key, value in row.items() if value is not None} - for row in data + {key: value for key, value in row.items() if value is not None} for row in data ] return cleaned_data + # Function to clean the result, extracting the needed part of the URI -def clean_result(data): - return [re.findall(r'#(\w+)', value)[0] for value in data if re.findall(r'#(\w+)', value)] +def clean_result(data: List[str]) -> List[str]: + """ + Extract the relevant part of a URI from a list of data. + + Args: + data (List[str]): A list of URIs to clean. + + Returns: + List[str]: A list of extracted parts from the URIs. + """ + return [ + re.findall(r"#(\w+)", value)[0] + for value in data + if re.findall(r"#(\w+)", value) + ] + # Function to create a SPARQL query for subclasses -def query_subclass(element): +def query_subclass(element: str) -> str: + """ + Create a SPARQL query to find subclasses of a given element. + + Args: + element (str): The element to find subclasses for. + + Returns: + str: The SPARQL query string. + """ return f"SELECT ?subclass WHERE {{ brick:{element} rdfs:subClassOf ?subclass . }}" + # Function to create a SPARQL query for properties -def query_properties(element): +def query_properties(element: str) -> str: + """ + Create a SPARQL query to find properties of a given element. + + Args: + element (str): The element to find properties for. + + Returns: + str: The SPARQL query string. + """ return f""" SELECT ?property ?message ?path ?class WHERE {{ brick:{element} sh:property ?property . @@ -60,39 +127,116 @@ def query_properties(element): }} """ + # Function to iteratively find subclasses -def iterative_subclasses(element): - subclasses = [] +def iterative_subclasses(element: str) -> List[str]: + """ + Iteratively find all subclasses of a given element. + + Args: + element (str): The element to find subclasses for. + + Returns: + List[str]: A list of subclasses. + """ + subclasses: List[str] = [] sub_class_data = get_query_result(query_subclass(element)) - subClass = clean_result([row['subclass'] for row in sub_class_data]) if sub_class_data else [] + subClass = ( + clean_result([row["subclass"] for row in sub_class_data]) + if sub_class_data + else [] + ) while subClass: subclasses.append(subClass[0]) - if subClass[0] in {'Collection', 'Equipment', 'Location', 'Measureable', 'Point'}: + if subClass[0] in { + "Collection", + "Equipment", + "Location", + "Measureable", + "Point", + }: break sub_class_data = get_query_result(query_subclass(subClass[0])) - subClass = clean_result([row['subclass'] for row in sub_class_data]) if sub_class_data else [] + subClass = ( + clean_result([row["subclass"] for row in sub_class_data]) + if sub_class_data + else [] + ) return subclasses + # General query function to retrieve properties and relationships -def general_query(element): +def general_query(element: str) -> Dict[str, Dict[str, Union[str, List[str]]]]: + """ + Retrieve properties and relationships for a given element. + + Args: + element (str): The element to retrieve properties and relationships for. + + Returns: + Dict[str, Dict[str, Union[str, List[str]]]]: A dictionary containing properties and their constraints. + """ subclasses = iterative_subclasses(element) if not subclasses: return {} query_data = get_query_result(query_properties(subclasses[-1])) - relationships = {} + relationships: Dict[str, Dict[str, Union[str, List[str]]]] = {} for row in query_data: - property_name = clean_result([row['path']])[0] + property_name = clean_result([row["path"]])[0] if property_name not in relationships: relationships[property_name] = { - 'message': row['message'], - 'constraint': clean_result([row['class']]) + "message": row["message"], + "constraint": clean_result([row["class"]]), } else: - relationships[property_name]['constraint'].extend(clean_result([row['class']])) + if isinstance(relationships[property_name]["constraint"], list): + relationships[property_name]["constraint"].extend( + clean_result([row["class"]]) + ) + else: + relationships[property_name]["constraint"] = clean_result( + [row["class"]] + ) + + return {"property": relationships} - return {'property': relationships} +def validate_ttl(ttl_file: str, method: str = "pyshacl") -> Tuple[bool, str]: + """ + Validate a TTL file using the specified method. + + Args: + ttl_file (str): The TTL file to validate. + method (str): The method to use for validation. Default is 'pyshacl'. + + Returns: + Tuple[bool, str]: A tuple containing a boolean indicating if the validation was successful and a validation report or error message. + """ + # Load the ttl file + output_graph = Graph() + try: + output_graph.parse(StringIO(ttl_file), format="ttl") + except Exception as e: + return False, f"Failed to parse the TTL file. Content: {e}" + + if method == "pyshacl": + valid, results_graph, report = pyshacl.validate( + output_graph, + shacl_graph=g, + ont_graph=g, + inference="rdfs", + abort_on_first=False, + allow_infos=False, + allow_warnings=False, + meta_shacl=False, + advanced=False, + js=False, + debug=False, + ) + return valid, report + else: + return False, "Method not found" diff --git a/brickllm/utils/rdf_parser.py b/brickllm/utils/rdf_parser.py new file mode 100644 index 0000000..0170d20 --- /dev/null +++ b/brickllm/utils/rdf_parser.py @@ -0,0 +1,56 @@ +import re + + +def extract_rdf_graph(llm_response: str) -> str: + + all_lines = llm_response.splitlines() + for i, line in enumerate(all_lines): + all_lines[i] = line.strip() + llm_response = "\n".join(all_lines).strip() + + if not llm_response.strip().startswith("@prefix "): + # Try to find RDF content within backticks + backtick_pattern = re.compile(r"```(.*?)```", re.DOTALL) + match = backtick_pattern.search(llm_response) + if match: + llm_response = match.group(1).strip() + # return rdf_graph + # If no backticks, look for RDF starting with @prefix + rdf_start_pattern = re.compile(r"@prefix [^\s]*: <[^>]*", re.DOTALL) + match = rdf_start_pattern.search(llm_response) + if match: + start_index = match.start() + rdf_content = llm_response[start_index:].strip() + else: + # If no valid RDF content is found, raise an error + raise ValueError("No valid RDF found in the provided graph content.") + else: + rdf_content = llm_response.strip() + lines = rdf_content.splitlines() + if lines and lines[-1].strip().endswith("```"): + rdf_graph = "\n".join(lines[:-1]) # Remove the last line + else: + flag_last_line = False + while not flag_last_line: + last_line = lines[-1].strip() if lines else "" + if any( + word in last_line + for word in ["note", "Note", "Please", "please", "Here", "here"] + ): + lines.pop() + # TODO: Extract user namespace from instructions and insert instead of bldg: + elif not ( + last_line.startswith("bldg:") + or last_line.startswith("ref:") + or last_line.startswith("unit:") + or last_line.startswith("brick:") + or last_line.startswith("a") + ): # or any(["note", "Note", "Please"]) in last_line: + lines.pop() + elif "```" in last_line.strip(): + lines.pop() + else: + flag_last_line = True + rdf_graph = "\n".join(lines).strip() + + return rdf_graph diff --git a/brickllm/utils/validate_ttl.py b/brickllm/utils/validate_ttl.py deleted file mode 100644 index e075b7b..0000000 --- a/brickllm/utils/validate_ttl.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -https://github.com/baeda-polito/portable-app-framework/blob/main/src/portable_app_framework/utils/util_qualify.py -""" - -import os -from rdflib import Graph -import pyshacl -from logging import getLogger - -logger = getLogger(__name__) - - -class BasicValidationInterface: - """ - This class is used to validate a graph using the Brick basic validation as described here: - https://github.com/gtfierro/shapes/blob/main/verify.py - """ - - def __init__(self, graph: Graph): - # use the wrapper BrickGraph to initialize the graph - self.graph = graph - self.graph.parse(os.path.join(os.path.dirname(__file__), "..", "libraries", "Brick-nightly.ttl"), format='ttl') - - def validate(self) -> bool: - """ - Validate the graph - :return: print the validation report - """ - # validate - valid, results_graph, report = pyshacl.validate(self.graph, - shacl_graph=self.graph, - ont_graph=self.graph, - inference='rdfs', - abort_on_first=False, - allow_infos=False, - allow_warnings=False, - meta_shacl=False, - advanced=False, - js=False, - debug=False) - - logger.debug(f"[Brick] Is valid? {valid}") - if not valid: - print("-" * 79) - print(report) - print("-" * 79) - - return valid \ No newline at end of file diff --git a/docs/assets/brickllm_banner.png b/docs/assets/brickllm_banner.png new file mode 100644 index 0000000..b73570f Binary files /dev/null and b/docs/assets/brickllm_banner.png differ diff --git a/docs/assets/brickllm_logo.png b/docs/assets/brickllm_logo.png index cdee007..5c2aabd 100644 Binary files a/docs/assets/brickllm_logo.png and b/docs/assets/brickllm_logo.png differ diff --git a/docs/assets/moderate_logo.png b/docs/assets/moderate_logo.png new file mode 100644 index 0000000..a1dcd6f Binary files /dev/null and b/docs/assets/moderate_logo.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..47961d7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,8 @@ +# Welcome to BrickLLM + +BrickLLM is a Python library designed to generate RDF files that comply with the BrickSchema ontology using Large Language Models (LLMs). + +- [Overview](overview.md) +- [Installation](installation.md) +- [Usage](usage.md) +- [API Reference](reference/brickllm.md) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..e2bda60 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,18 @@ +# Installation + +BrickLLM can be installed using [pip](https://pypi.org/project/brickllm/). + +```bash +pip install brickllm +``` + +Alternatively, using [Poetry](https://python-poetry.org/), which handles dependency management. + +```bash +# Clone the repository +git clone https://github.com/EURAC-EEBgroup/brickllm-lib.git +cd brickllm-lib + +# Install dependencies +poetry install +``` diff --git a/docs/modules/brickllm.md b/docs/modules/brickllm.md new file mode 100644 index 0000000..c7bf15f --- /dev/null +++ b/docs/modules/brickllm.md @@ -0,0 +1,37 @@ +# BrickLLM Module + +The `brickllm` package is the main package containing all modules and components related to generating BrickSchema-compliant RDF files using LLMs. + +## Contents + +### Edges +- [validate_condition.py](../reference/edges/validate_condition.md) + +### Graphs +- [brickschema_graph.py](../reference/graphs/brickschema_graph.md) + +### Helpers +- [llm_models.py](../reference/helpers/llm_models.md) +- [prompts.py](../reference/helpers/prompts.md) + +### Nodes +- [get_elem_children.py](../reference/nodes/get_elem_children.md) +- [get_elements.py](../reference/nodes/get_elements.md) +- [get_relationships.py](../reference/nodes/get_relationships.md) +- [get_sensors.py](../reference/nodes/get_sensors.md) +- [schema_to_ttl.py](../reference/nodes/schema_to_ttl.md) +- [validate_schema.py](../reference/nodes/validate_schema.md) + +### Ontologies +- [brick_hierarchy.json](../reference/ontologies/brick_hierarchy.md) +- [Brick.ttl](../reference/ontologies/Brick.md) + +### Utils +- [get_hierarchy_info.py](../reference/utils/get_hierarchy_info.md) +- [query_brickschema.py](../reference/utils/query_brickschema.md) + +### Root-Level Files +- [compiled_graphs.py](../reference/compiled_graphs.md) +- [configs.py](../reference/configs.md) +- [schemas.py](../reference/schemas.md) +- [states.py](../reference/states.md) diff --git a/docs/modules/edges.md b/docs/modules/edges.md new file mode 100644 index 0000000..9d48bf9 --- /dev/null +++ b/docs/modules/edges.md @@ -0,0 +1,8 @@ + +# Edges Module + +The `edges` module contains components related to the validation of relationships (edges) between entities in the graph. + +## Contents + +- [validate_condition.py](../reference/edges/validate_condition.md) diff --git a/docs/modules/graphs.md b/docs/modules/graphs.md new file mode 100644 index 0000000..18b94ad --- /dev/null +++ b/docs/modules/graphs.md @@ -0,0 +1,8 @@ + +# Graphs Module + +The `graphs` module contains components that manage and execute the BrickSchema graph operations. + +## Contents + +- [brickschema_graph.py](../reference/graphs/brickschema_graph.md) diff --git a/docs/modules/helpers.md b/docs/modules/helpers.md new file mode 100644 index 0000000..d093fc3 --- /dev/null +++ b/docs/modules/helpers.md @@ -0,0 +1,9 @@ + +# Helpers Module + +The `helpers` module contains utility functions and prompts that assist in LLM interactions and data processing. + +## Contents + +- [llm_models.py](../reference/helpers/llm_models.md) +- [prompts.py](../reference/helpers/prompts.md) diff --git a/docs/modules/nodes.md b/docs/modules/nodes.md new file mode 100644 index 0000000..e260432 --- /dev/null +++ b/docs/modules/nodes.md @@ -0,0 +1,13 @@ + +# Nodes Module + +The `nodes` module contains specialized components that perform tasks such as element identification, hierarchy construction, and RDF conversion. + +## Contents + +- [get_elem_children.py](../reference/nodes/get_elem_children.md) +- [get_elements.py](../reference/nodes/get_elements.md) +- [get_relationships.py](../reference/nodes/get_relationships.md) +- [get_sensors.py](../reference/nodes/get_sensors.md) +- [schema_to_ttl.py](../reference/nodes/schema_to_ttl.md) +- [validate_schema.py](../reference/nodes/validate_schema.md) diff --git a/docs/modules/ontologies.md b/docs/modules/ontologies.md new file mode 100644 index 0000000..b56cd1d --- /dev/null +++ b/docs/modules/ontologies.md @@ -0,0 +1,9 @@ + +# Ontologies Module + +The `ontologies` module contains the BrickSchema and hierarchy files used for representing and managing the building systems. + +## Contents + +- [brick_hierarchy.json](../reference/ontologies/brick_hierarchy.md) +- [Brick.ttl](../reference/ontologies/Brick.md) diff --git a/docs/modules/utils.md b/docs/modules/utils.md new file mode 100644 index 0000000..5cad60e --- /dev/null +++ b/docs/modules/utils.md @@ -0,0 +1,9 @@ + +# Utils Module + +The `utils` module contains components related to querying and retrieving data from the BrickSchema. + +## Contents + +- [get_hierarchy_info.py](../reference/utils/get_hierarchy_info.md) +- [query_brickschema.py](../reference/utils/query_brickschema.md) diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..5b29e12 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,25 @@ +# Overview + +BrickLLM is a Python library designed to generate RDF (Resource Description Framework) files that comply with the BrickSchema ontology using Large Language Models (LLMs). The library leverages advanced natural language processing capabilities to interpret building descriptions and convert them into structured, machine-readable formats suitable for building automation and energy management systems. + +## Main Features + +- **Multi-provider LLM Support**: Supports multiple LLM providers, including OpenAI, Anthropic, and Fireworks AI. +- **Natural Language to RDF Conversion**: Converts natural language descriptions of buildings and facilities into BrickSchema-compliant RDF. +- **Customizable Graph Execution**: Utilizes LangGraph for flexible and customizable graph-based execution of the conversion process. +- **Ontology Integration**: Incorporates the BrickSchema ontology for accurate and standardized representation of building systems. +- **Extensible Architecture**: Designed with modularity in mind, allowing for easy extension and customization. + +## How It Works + +1. **User Input**: The process begins with a natural language description of a building or facility provided by the user. + +2. **LLM Processing**: The description is processed by a selected LLM to extract relevant building components and their relationships. + +3. **Graph Execution**: The extracted information is passed through a series of nodes in a graph structure: + - **Element Identification**: Identifies building elements from the user prompt. + - **Hierarchy Construction**: Builds a hierarchical structure of the identified elements. + - **Relationship Mapping**: Determines relationships between the components. + - **TTL Generation**: Converts the structured data into Turtle (TTL) format, which is a serialization of RDF. + +4. **Output Generation**: The final output is a BrickSchema-compliant RDF file in TTL format, representing the building's structure and systems. diff --git a/docs/reference/brickllm.md b/docs/reference/brickllm.md new file mode 100644 index 0000000..521400e --- /dev/null +++ b/docs/reference/brickllm.md @@ -0,0 +1,32 @@ +# BrickLLM API Reference + +Welcome to the API Reference for the `BrickLLM` library. This section of the documentation provides detailed information about the modules, classes, functions, and other components that make up the BrickLLM library. + +## Overview + +`BrickLLM` is a Python library designed to generate BrickSchema-compliant RDF files using Large Language Models (LLMs). It provides various utilities for parsing natural language building descriptions, extracting relevant components, and constructing a structured graph that represents the building and its systems. + +This API reference covers all the essential modules and functions available in the library, including components for: + +- **Element identification** +- **Relationship mapping** +- **TTL generation** (Turtle format RDF) +- **Ontology integration** (BrickSchema) + +## Key Modules + +- **Edges**: Validation of relationships (edges) between entities in the graph. +- **Graphs**: Handles the orchestration of graph-based operations with the BrickSchema. +- **Helpers**: Contains utility functions and predefined LLM prompts to aid in the generation process. +- **Nodes**: Specialized nodes that handle various tasks such as extracting elements, constructing hierarchies, and generating RDF files. +- **Ontologies**: Includes the BrickSchema ontology and hierarchical data. +- **Utils**: Utility functions for querying the BrickSchema and handling RDF data. + +## How to Use the API Reference + +- Navigate to individual modules and components via the side navigation or by clicking on the links in the module overview sections. +- Each module contains a breakdown of its functions, classes, and attributes, with descriptions and examples where applicable. + +For an in-depth guide on how to use the library in practice, see the [Usage](../usage.md) section. + +Happy coding! diff --git a/docs/reference/compiled_graphs.md b/docs/reference/compiled_graphs.md new file mode 100644 index 0000000..6352916 --- /dev/null +++ b/docs/reference/compiled_graphs.md @@ -0,0 +1,4 @@ + +# compiled_graphs.py + +::: brickllm.compiled_graphs diff --git a/docs/reference/configs.md b/docs/reference/configs.md new file mode 100644 index 0000000..119d6bb --- /dev/null +++ b/docs/reference/configs.md @@ -0,0 +1,4 @@ + +# configs.py + +::: brickllm.configs diff --git a/docs/reference/edges/validate_condition.md b/docs/reference/edges/validate_condition.md new file mode 100644 index 0000000..a393dbb --- /dev/null +++ b/docs/reference/edges/validate_condition.md @@ -0,0 +1,4 @@ + +# validate_condition.py + +::: brickllm.edges.validate_condition diff --git a/docs/reference/graphs/brickschema_graph.md b/docs/reference/graphs/brickschema_graph.md new file mode 100644 index 0000000..26c60bf --- /dev/null +++ b/docs/reference/graphs/brickschema_graph.md @@ -0,0 +1,4 @@ + +# brickschema_graph.py + +::: brickllm.graphs.brickschema_graph diff --git a/docs/reference/helpers/llm_models.md b/docs/reference/helpers/llm_models.md new file mode 100644 index 0000000..0bc774f --- /dev/null +++ b/docs/reference/helpers/llm_models.md @@ -0,0 +1,4 @@ + +# llm_models.py + +::: brickllm.helpers.llm_models diff --git a/docs/reference/helpers/prompts.md b/docs/reference/helpers/prompts.md new file mode 100644 index 0000000..8ed54f2 --- /dev/null +++ b/docs/reference/helpers/prompts.md @@ -0,0 +1,4 @@ + +# prompts.py + +::: brickllm.helpers.prompts diff --git a/docs/reference/nodes/get_elem_children.md b/docs/reference/nodes/get_elem_children.md new file mode 100644 index 0000000..69d7ba0 --- /dev/null +++ b/docs/reference/nodes/get_elem_children.md @@ -0,0 +1,4 @@ + +# get_elem_children.py + +::: brickllm.nodes.get_elem_children diff --git a/docs/reference/nodes/get_elements.md b/docs/reference/nodes/get_elements.md new file mode 100644 index 0000000..f0d601f --- /dev/null +++ b/docs/reference/nodes/get_elements.md @@ -0,0 +1,4 @@ + +# get_elements.py + +::: brickllm.nodes.get_elements diff --git a/docs/reference/nodes/get_relationships.md b/docs/reference/nodes/get_relationships.md new file mode 100644 index 0000000..98f70f5 --- /dev/null +++ b/docs/reference/nodes/get_relationships.md @@ -0,0 +1,4 @@ + +# get_relationships.py + +::: brickllm.nodes.get_relationships diff --git a/docs/reference/nodes/get_sensors.md b/docs/reference/nodes/get_sensors.md new file mode 100644 index 0000000..34809a4 --- /dev/null +++ b/docs/reference/nodes/get_sensors.md @@ -0,0 +1,4 @@ + +# get_sensors.py + +::: brickllm.nodes.get_sensors diff --git a/docs/reference/nodes/schema_to_ttl.md b/docs/reference/nodes/schema_to_ttl.md new file mode 100644 index 0000000..2f9b0d2 --- /dev/null +++ b/docs/reference/nodes/schema_to_ttl.md @@ -0,0 +1,4 @@ + +# schema_to_ttl.py + +::: brickllm.nodes.schema_to_ttl diff --git a/docs/reference/nodes/validate_schema.md b/docs/reference/nodes/validate_schema.md new file mode 100644 index 0000000..2db98ed --- /dev/null +++ b/docs/reference/nodes/validate_schema.md @@ -0,0 +1,4 @@ + +# validate_schema.py + +::: brickllm.nodes.validate_schema diff --git a/docs/reference/ontologies/Brick.md b/docs/reference/ontologies/Brick.md new file mode 100644 index 0000000..ee1e656 --- /dev/null +++ b/docs/reference/ontologies/Brick.md @@ -0,0 +1,4 @@ + +# Brick.ttl + +This file contains the BrickSchema ontology in Turtle (TTL) format. diff --git a/docs/reference/ontologies/brick_hierarchy.md b/docs/reference/ontologies/brick_hierarchy.md new file mode 100644 index 0000000..c2c9203 --- /dev/null +++ b/docs/reference/ontologies/brick_hierarchy.md @@ -0,0 +1,4 @@ + +# brick_hierarchy.json + +This file contains the hierarchical structure of the BrickSchema entities in JSON format. diff --git a/docs/reference/schemas.md b/docs/reference/schemas.md new file mode 100644 index 0000000..6e825bc --- /dev/null +++ b/docs/reference/schemas.md @@ -0,0 +1,4 @@ + +# schemas.py + +::: brickllm.schemas diff --git a/docs/reference/states.md b/docs/reference/states.md new file mode 100644 index 0000000..9390f4e --- /dev/null +++ b/docs/reference/states.md @@ -0,0 +1,4 @@ + +# states.py + +::: brickllm.states diff --git a/docs/reference/utils/get_hierarchy_info.md b/docs/reference/utils/get_hierarchy_info.md new file mode 100644 index 0000000..980f0a1 --- /dev/null +++ b/docs/reference/utils/get_hierarchy_info.md @@ -0,0 +1,4 @@ + +# get_hierarchy_info.py + +::: brickllm.utils.get_hierarchy_info diff --git a/docs/reference/utils/query_brickschema.md b/docs/reference/utils/query_brickschema.md new file mode 100644 index 0000000..9cd5812 --- /dev/null +++ b/docs/reference/utils/query_brickschema.md @@ -0,0 +1,4 @@ + +# query_brickschema.py + +::: brickllm.utils.query_brickschema diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..8f04b05 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1 @@ +# Usage diff --git a/examples/brickschema_ttl.py b/examples/brickschema_ttl.py deleted file mode 100644 index 0bc9fee..0000000 --- a/examples/brickschema_ttl.py +++ /dev/null @@ -1,25 +0,0 @@ -from brickllm.graphs import BrickSchemaGraph - -# Specify the user prompt -building_description = """ -I have a building located in Bolzano. -It has 3 floors and each floor has 1 office. -There are 2 rooms in each office and each room has three sensors: -- Temperature sensor; -- Humidity sensor; -- CO sensor. -""" - -# Create an instance of BrickSchemaGraph -brick_graph = BrickSchemaGraph() - -# Display the graph -brick_graph.display() - -# Run the graph without streaming -result = brick_graph.run( - prompt=building_description, - stream=False -) - -print(result) \ No newline at end of file diff --git a/examples/example_custom_llm.py b/examples/example_custom_llm.py new file mode 100644 index 0000000..b548c58 --- /dev/null +++ b/examples/example_custom_llm.py @@ -0,0 +1,36 @@ +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI + +from brickllm.graphs import BrickSchemaGraph + +# Load environment variables +load_dotenv() + +# Specify the user prompt +building_description = """ +I have a building located in Bolzano. +It has 3 floors and each floor has 1 office. +There are 2 rooms in each office and each room has three sensors: +- Temperature sensor; +- Humidity sensor; +- CO sensor. +""" + +# Create an instance of BrickSchemaGraph with a custom model +custom_model = ChatOpenAI(temperature=0, model="gpt-4o") +brick_graph = BrickSchemaGraph(model=custom_model) + +# Display the graph structure +brick_graph.display() + +# Prepare input data +input_data = {"user_prompt": building_description} + +# Run the graph with the custom model +result = brick_graph.run(input_data=input_data, stream=False) + +# Print the result +print(result) + +# save the result to a file +brick_graph.save_ttl_output("my_building_custom.ttl") diff --git a/examples/example_finetuned_local.py b/examples/example_finetuned_local.py new file mode 100644 index 0000000..f25b00f --- /dev/null +++ b/examples/example_finetuned_local.py @@ -0,0 +1,51 @@ +from dotenv import load_dotenv + +from brickllm.graphs import BrickSchemaGraphLocal + +# Load environment variables +load_dotenv() + +# Specify the instructions for local processing +instructions = """ +Your job is to generate a RDF graph in Turtle format from a description of energy systems and sensors of a building in the following input, using the Brick ontology. +### Instructions: +- Each subject, object of predicate must start with a @prefix. +- Use the prefix bldg: with IRI for any created entities. +- Use the prefix brick: with IRI for any Brick entities and relationships used. +- Use the prefix unit: with IRI and its ontology for any unit of measure defined. +- When encoding the timeseries ID of the sensor, you must use the following format: ref:hasExternalReference [ a ref:TimeseriesReference ; ref:hasTimeseriesId 'timeseriesID' ]. +- When encoding identifiers or external references, such as building/entities IDs, use the following schema: ref:hasExternalReference [ a ref:ExternalReference ; ref:hasExternalReference ‘id/reference’ ]. +- When encoding numerical reference, use the schema [brick:value 'value' ; \n brick:hasUnit unit:'unit' ] . +-When encoding coordinates, use the schema brick:coordinates [brick:latitude "lat" ; brick:longitude "long" ]. +The response must be the RDF graph that includes all the @prefix of the ontologies used in the triples. The RDF graph must be created in Turtle format. Do not add any other text or comment to the response. +""" + +building_description = """ +The building (external ref: 'OB103'), with coordinates 33.9614, -118.3531, has a total area of 500 m². It has three zones, each with its own air temperature sensor. +The building has an electrical meter that monitors data of a power sensor. An HVAC equipment serves all three zones and its power usage is measured by a power sensor. + +Timeseries IDs and unit of measure of the sensors: +- Building power consumption: '1b3e-29dk-8js7-f54v' in watts. +- HVAC power consumption: '29dh-8ks3-fvjs-d92e' in watts. +- Temperature sensor zone 1: 't29s-jk83-kv82-93fs' in celsius. +- Temperature sensor zone 2: 'f29g-js92-df73-l923' in celsius. +- Temperature sensor zone 3: 'm93d-ljs9-83ks-29dh' in celsius. +""" + +# Create an instance of BrickSchemaGraphLocal +brick_graph_local = BrickSchemaGraphLocal(model="llama3.1:8b-brick") + +# Display the graph structure +brick_graph_local.display(filename="graph_local.png") + +# Prepare input data +input_data = {"user_prompt": building_description, "instructions": instructions} + +# Run the graph +result = brick_graph_local.run(input_data=input_data, stream=False) + +# Print the result +print(result) + +# Save the result to a file +brick_graph_local.save_ttl_output("my_building_local.ttl") diff --git a/examples/example_openai.py b/examples/example_openai.py new file mode 100644 index 0000000..9d8107d --- /dev/null +++ b/examples/example_openai.py @@ -0,0 +1,34 @@ +from dotenv import load_dotenv + +from brickllm.graphs import BrickSchemaGraph + +# Load environment variables +load_dotenv() + +# Specify the user prompt +building_description = """ +I have a building located in Bolzano. +It has 3 floors and each floor has 1 office. +There are 2 rooms in each office and each room has three sensors: +- Temperature sensor; +- Humidity sensor; +- CO 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") + +# Prepare input data +input_data = {"user_prompt": building_description} + +# Run the graph +result = brick_graph.run(input_data=input_data, stream=False) + +# Print the result +print(result) + +# save the result to a file +brick_graph.save_ttl_output("my_building.ttl") diff --git a/finetuned/Dockerfile b/finetuned/Dockerfile new file mode 100644 index 0000000..07aaba8 --- /dev/null +++ b/finetuned/Dockerfile @@ -0,0 +1,24 @@ +# Use the official Ollama Docker image +FROM ollama/ollama:latest + +# Install curl to download the model file +RUN apt-get update && apt-get install -y curl + +# Set a working directory for the model files +WORKDIR /root/.llm + +# Download the .gguf file and place it in the .llm directory +RUN curl -L -o unsloth.Q4_K_M.gguf https://huggingface.co/Giudice7/llama31-8B-brick-v8/resolve/main/unsloth.Q4_K_M.gguf + +# Create the Modelfile in the .llm directory +RUN echo "FROM ./unsloth.Q4_K_M.gguf" > Modelfile + +# Copy the entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Expose port 11434 +EXPOSE 11434 + +# Use the custom entrypoint script +ENTRYPOINT ["/entrypoint.sh"] diff --git a/finetuned/docker-compose.yml b/finetuned/docker-compose.yml new file mode 100644 index 0000000..9048483 --- /dev/null +++ b/finetuned/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + ollama-llm: + build: + context: . + dockerfile: Dockerfile + ports: + - "11434:11434" + volumes: + - ./llm:/data + container_name: brickllm-ollama + restart: unless-stopped diff --git a/finetuned/entrypoint.sh b/finetuned/entrypoint.sh new file mode 100644 index 0000000..2fcb7e9 --- /dev/null +++ b/finetuned/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Start Ollama in the background +ollama serve & + +# Function to check if Ollama is running +function check_ollama { + curl --silent --fail http://localhost:11434/v1/models > /dev/null +} + +# Wait for Ollama to be available +echo "Waiting for Ollama to be ready..." +while ! check_ollama; do + sleep 2 +done + +echo "Ollama is ready!" + +# Create the model +ollama create llama3.1:8b-brick-v8 -f /root/.llm/Modelfile + +# Bring Ollama to the foreground +echo "Restarting Ollama in foreground..." +pkill ollama +exec ollama serve diff --git a/langgraph.json b/langgraph.json index 05a7779..2d4f705 100644 --- a/langgraph.json +++ b/langgraph.json @@ -4,4 +4,4 @@ "agent": "./brickllm/compiled_graphs.py:brickschema_graph" }, "env": ".env" - } \ No newline at end of file + } diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..edbfa11 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,98 @@ +site_name: "BrickLLM Documentation" +site_description: "Library for generating RDF files following BrickSchema ontology using LLM." +site_url: "https://eurac-eebgroup.github.io/brickllm-lib/" +repo_url: "https://github.com/EURAC-EEBgroup/brickllm-lib" +repo_name: "EURAC-EEBgroup/brickllm-lib" +site_dir: "site" + +nav: + - Home: index.md + - Getting Started: + - Overview: overview.md + - Installation: installation.md + - Usage: usage.md + - Modules: + - BrickLLM: modules/brickllm.md + - Edges: modules/edges.md + - Graphs: modules/graphs.md + - Helpers: modules/helpers.md + - Nodes: modules/nodes.md + - Ontologies: modules/ontologies.md + - Utils: modules/utils.md + - API Reference: + - Edges: + - Validate Condition: reference/edges/validate_condition.md + - Graphs: + - Brickschema Graph: reference/graphs/brickschema_graph.md + - Helpers: + - LLM Models: reference/helpers/llm_models.md + - Prompts: reference/helpers/prompts.md + - Nodes: + - Get Element Children: reference/nodes/get_elem_children.md + - Get Elements: reference/nodes/get_elements.md + - Get Relationships: reference/nodes/get_relationships.md + - Get Sensors: reference/nodes/get_sensors.md + - Schema to TTL: reference/nodes/schema_to_ttl.md + - Validate Schema: reference/nodes/validate_schema.md + - Utils: + - Get Hierarchy Info: reference/utils/get_hierarchy_info.md + - Query Brickschema: reference/utils/query_brickschema.md + - Ontologies: + - Brick: reference/ontologies/Brick.md + - Brick Hierarchy: reference/ontologies/brick_hierarchy.md + - Root-Level: + - BrickLLM: reference/brickllm.md + - Compiled Graphs: reference/compiled_graphs.md + - Configs: reference/configs.md + - Schemas: reference/schemas.md + - States: reference/states.md + +theme: + name: material + logo: assets/brickllm_logo.png + favicon: assets/favicon.ico + features: + - navigation.tabs + - navigation.top + - search.highlight + - search.suggest + - toc.follow + palette: + - scheme: default + primary: 'indigo' + accent: 'indigo' + toggle: + icon: material/weather-night + name: Switch to dark mode + - scheme: slate + primary: 'indigo' + accent: 'indigo' + toggle: + icon: material/weather-sunny + name: Switch to light mode + +plugins: + - search + - mkdocstrings: + handlers: + python: + selection: + filters: ["!^_"] + rendering: + show_root_toc_entry: false + show_source: false + +markdown_extensions: + - admonition + - codehilite + - toc: + permalink: "§" + - pymdownx.details + - pymdownx.superfences + - pymdownx.tabbed + - pymdownx.highlight + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg diff --git a/pyproject.toml b/pyproject.toml index 05ad99d..f9cceeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,59 +1,87 @@ -[tool.poetry] -name = "brickllm" -version = "0.0.1" -description = "Library for generating RDF files following BrickSchema ontology using LLM" -authors = ["Marco Perini ", "Daniele Antonucci "] -license = "MIT" -readme = "README.md" -homepage = "https://brickllm.com/" -repository = "https://github.com/EURAC-EEBgroup/brickllm-lib" -documentation = "https://brickllm.com/docs" -keywords = [ - "brickllm", - "brickschema", - "rdf", - "ontologies", - "knowledge graph", - "semantic web", - "ai", - "artificial intelligence", - "gpt", - "machine learning", - "natural language processing", - "nlp", - "openai", - "building automation", - "iot", - "graph", - "ontology", -] - -classifiers = [ - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", -] - -packages = [{include = "brickllm"}] - -[tool.poetry.dependencies] -python = ">=3.9,<4.0" -langgraph = "0.2.23" -langchain_openai = "0.2.0" -langchain-fireworks = "0.2.0" -langchain-anthropic = "0.2.1" -langchain_community = "0.3.0" -langchain_core = "0.3.5" -rdflib = ">=6.2.0,<7" -pyshacl = "0.21" - -[tool.poetry.dev-dependencies] -pytest = "8.0.0" -pytest-mock = "3.14.0" -pylint = "^3.2.5" - - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.poetry] +name = "brickllm" +version = "1.1.2" +description = "Library for generating RDF files following BrickSchema ontology using LLM" +authors = ["Marco Perini ", "Daniele Antonucci "] +license = "BSD-3-Clause" +readme = "README.md" +homepage = "https://brickllm.com/" +repository = "https://github.com/EURAC-EEBgroup/brickllm-lib" +documentation = "https://brickllm.com/docs" +keywords = [ + "brickllm", + "brickschema", + "rdf", + "ontologies", + "knowledge graph", + "semantic web", + "ai", + "artificial intelligence", + "gpt", + "machine learning", + "natural language processing", + "nlp", + "openai", + "building automation", + "iot", + "graph", + "ontology", +] + +classifiers = [ + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] + +packages = [{include = "brickllm"}] + +[tool.poetry.dependencies] +python = ">=3.9,<4.0" +langgraph = "0.2.23" +langchain_openai = "0.2.0" +langchain-fireworks = "0.2.0" +langchain-anthropic = "0.2.1" +langchain_community = "0.3.0" +langchain_core = "0.3.5" +rdflib = ">=6.2.0,<7" +pyshacl = "0.21" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4" +pytest-sugar = "*" +pytest-cov = "*" +black = "*" +mypy = "*" +ruff = "*" +isort = "*" +pre-commit = "*" +types-setuptools = "^75.1.0.20240917" +mkdocs = "^1.5" +mkdocs-material = "^9.2" +mkdocstrings-python = "^1.1" + +[tool.black] +line-length = 88 +target-version = ["py39"] + +[tool.isort] +profile = "black" + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +select = ["F", "E", "W", "C"] +ignore = ["E203", "E501"] # Ignore conflicts with Black + +[tool.mypy] +python_version = "3.9" +strict = true +disallow_untyped_calls = true +ignore_missing_imports = true + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/prova_test.py b/tests/prova_test.py new file mode 100644 index 0000000..377f7c3 --- /dev/null +++ b/tests/prova_test.py @@ -0,0 +1,40 @@ +import pytest + + +def addition(a, b): + return a + b + + +def subtraction(a, b): + return a - b + + +def multiplication(a, b): + return a * b + + +def division(a, b): + if b == 0: + raise ZeroDivisionError("Cannot divide by zero!") + return a / b + + +def test_addition(): + assert addition(2, 3) == 5 + + +def test_subtraction(): + assert subtraction(2, 3) == -1 + + +def test_multiplication(): + assert multiplication(2, 3) == 6 + + +def test_division(): + assert division(2, 3) == 2 / 3 + + +def test_division_by_zero(): + with pytest.raises(ZeroDivisionError): + division(2, 0)